为啥这个计数查询这么慢?
Posted
技术标签:
【中文标题】为啥这个计数查询这么慢?【英文标题】:Why is this count query so slow?为什么这个计数查询这么慢? 【发布时间】:2012-10-22 15:07:13 【问题描述】:您好,我托管在 Heroku 上,在他们的 Ika 计划(7.5gb ram)上运行 postgresql 9.1.6。我有一张叫汽车的桌子。我需要执行以下操作:
SELECT COUNT(*) FROM "cars" WHERE "cars"."reference_id" = 'toyota_hilux'
现在这需要大量时间(64 秒!!!)
Aggregate (cost=2849.52..2849.52 rows=1 width=0) (actual time=63388.390..63388.391 rows=1 loops=1)
-> Bitmap Heap Scan on cars (cost=24.76..2848.78 rows=1464 width=0) (actual time=1169.581..63387.361 rows=739 loops=1)
Recheck Cond: ((reference_id)::text = 'toyota_hilux'::text)
-> Bitmap Index Scan on index_cars_on_reference_id (cost=0.00..24.69 rows=1464 width=0) (actual time=547.530..547.530 rows=832 loops=1)
Index Cond: ((reference_id)::text = 'toyota_hilux'::text)
Total runtime: 64112.412 ms
一点背景:
该表包含大约 320 万行,而我试图依靠的列具有以下设置:
reference_id character varying(50);
和索引:
CREATE INDEX index_cars_on_reference_id
ON cars
USING btree
(reference_id COLLATE pg_catalog."default" );
我做错了什么?我预计这种表现不是我应该期待的——或者我应该期待吗?
【问题讨论】:
afaik ,您应该期望如此,因为任何 where 子句都会强制进行全表扫描,而与标准/索引无关 hmmm 但是我怎样才能利用已经存在的reference_id 上的索引呢?顺便说一句,为什么在解释中使用它呢? this 可能会有所帮助.. 您似乎正在经历分散数据页上的许多磁盘寻道(位图堆扫描)的高延迟。您可以尝试立即第二次重新执行查询,以查看数据在缓存中时的差异。 EXPLAIN ANALYZE 的 BUFFERS 选项在这里也很有用。 @MikeChristensen:wiki 页面仅用于计算表中的 all 行,没有任何(where)条件。 用条件计算是完全不同的事情。 【参考方案1】:@Satya claims in his comment 并不完全正确。在存在匹配索引的情况下,如果表统计数据表明它会返回超过大约 5%(取决于)表,则计划程序仅选择全表扫描,因为这样扫描整个表会更快。
正如您从自己的问题中看到的那样,您的查询并非如此。它使用位图索引扫描,后跟位图堆扫描。虽然我本来期望一个普通的索引扫描。 (?)
我在您的解释输出中注意到另外两件事: 第一次扫描找到 832 行,而第二次将计数减少到 739。这表明您的索引中有很多死元组。
使用EXPLAIN ANALYZE
检查每个步骤后的执行时间,并可能将结果添加到您的问题中:
首先,使用 EXPLAIN ANALYZE 重新运行查询两到三次以填充缓存。与第一次相比,上一次运行的结果如何?
下一步:
VACUUM ANALYZE cars;
重新运行。
如果你在表上有很多写操作,我会设置一个低于 100 的填充因子。比如:
ALTER TABLE cars SET (fillfactor=90);
如果您的行大小很大或者您有 很多 的写入操作,请降低。那么:
VACUUM FULL ANALYZE cars;
这需要一段时间。重新运行。
或者,如果您有能力这样做(并且其他重要查询没有相互矛盾的要求):
CLUSTER cars USING index_cars_on_reference_id;
这会按照索引的物理顺序重写表,这应该会使这种查询快得多。
规范化架构
如果您需要非常快,请创建一个带有 serial
主键的表 car_type
,并从表 cars
中引用它。这会将必要的索引缩小到现在的一小部分。
不用说,在您尝试任何这些操作之前,您都会进行备份。
CREATE temp TABLE car_type (
car_type_id serial PRIMARY KEY
, car_type text
);
INSERT INTO car_type (car_type)
SELECT DISTINCT car_type_id FROM cars ORDER BY car_type_id;
ANALYZE car_type;
CREATE UNIQUE INDEX car_type_uni_idx ON car_type (car_type); -- unique types
ALTER TABLE cars RENAME COLUMN car_type_id TO car_type; -- rename old col
ALTER TABLE cars ADD COLUMN car_type_id int; -- add new int col
UPDATE cars c
SET car_type_id = ct.car_type_id
FROM car_type ct
WHERE ct.car_type = c.car_type;
ALTER TABLE cars DROP COLUMN car_type; -- drop old varchar col
CREATE INDEX cars_car_type_id_idx ON cars (car_type_id);
ALTER TABLE cars
ADD CONSTRAINT cars_car_type_id_fkey FOREIGN KEY (car_type_id )
REFERENCES car_type (car_type_id) ON UPDATE CASCADE; -- add fk
VACUUM FULL ANALYZE cars;
或者,如果你想全力以赴:
CLUSTER cars USING cars_car_type_id_idx;
您的查询现在看起来像这样:
SELECT count(*)
FROM cars
WHERE car_type_id = (SELECT car_type_id FROM car_type
WHERE car_type = 'toyota_hilux')
而且应该更快。主要是现在索引和表变小了,也因为integer
处理比varchar
处理快。不过,与 varchar
列上的聚簇表相比,增益不会显着。
一个受欢迎的副作用:如果你必须重命名一个类型,它现在是一个很小的 UPDATE
到一行,根本不会弄乱大表。
【讨论】:
如果 cat_type 表被加入而不是在子查询中,最终查询有什么不同吗? @Clodoaldo:如果您只想计算 one 类型(如示例中所示),则子查询应该更快。不过没关系。 这是一个很好的答案!我会尝试一下。你@ErwinBrandstetter 是否认为这对这个计数的性能意味着什么,汽车表实际上有 170 列?我不太关心 postgres 的时间间隔,但我的猜测是我不会期望它,因为我没有在这个查询中明确地触及这些列...... @NielsKristian 170 columns 可能是规范化问题。打开另一个关于它发布表结构的问题。 @NielsKristian:Clodoaldo 所说的,另外:是的,非常大的行意味着只有几行适合数据页。因此,必须访问更多数据页才能计数,这是影响性能的最重要因素。以上是关于为啥这个计数查询这么慢?的主要内容,如果未能解决你的问题,请参考以下文章
为啥这个查询这么慢? - PostgreSQL - 从 SERIAL、TIMESTAMP 和 NUMERIC(6,2) 中选择