一个表中有超过 3000 万个条目,执行一次选择计数需要 19 分钟
Posted
技术标签:
【中文标题】一个表中有超过 3000 万个条目,执行一次选择计数需要 19 分钟【英文标题】:With more than 30 millions entries in a table, a select count on it takes 19 minutes to perform 【发布时间】:2016-07-07 09:20:01 【问题描述】:我在 Windows 7 64 位操作系统上使用 PostgreSQL 9.4.8 32 位。
我在 3 个 2T 的磁盘上使用 RAID 5。 CPU 为 Xeon E3-1225v3,内存为 8G。
在一个表中,我已经插入了超过 3000 万个条目(我想增加到 5000 万个)。
在此表上执行选择计数 (*) 需要 19 分钟以上。第二次执行此查询将其缩短到 14 分钟,但仍然很慢。索引似乎没有做任何事情。
我的 postgresql.conf 在文件末尾是这样设置的:
max_connections = 100
shared_buffers = 512MB
effective_cache_size = 6GB
work_mem = 13107kB
maintenance_work_mem = 512MB
checkpoint_segments = 32
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.2
这是该表的架构:
CREATE TABLE recorder.records
(
recorder_id smallint NOT NULL DEFAULT 200,
rec_start timestamp with time zone NOT NULL,
rec_end timestamp with time zone NOT NULL,
deleted boolean NOT NULL DEFAULT false,
channel_number smallint NOT NULL,
channel_name text,
from_id text,
from_name text,
to_id text,
to_name text,
type character varying(32),
hash character varying(128),
codec character varying(16),
id uuid NOT NULL,
status smallint,
duration interval,
CONSTRAINT records_pkey PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
)
CREATE INDEX "idxRecordChanName"
ON recorder.records
USING btree
(channel_name COLLATE pg_catalog."default");
CREATE INDEX "idxRecordChanNumber"
ON recorder.records
USING btree
(channel_number);
CREATE INDEX "idxRecordEnd"
ON recorder.records
USING btree
(rec_end);
CREATE INDEX "idxRecordFromId"
ON recorder.records
USING btree
(from_id COLLATE pg_catalog."default");
CREATE INDEX "idxRecordStart"
ON recorder.records
USING btree
(rec_start);
CREATE INDEX "idxRecordToId"
ON recorder.records
USING btree
(to_id COLLATE pg_catalog."default");
CREATE INDEX "idxRecordsStart"
ON recorder.records
USING btree
(rec_start);
CREATE TRIGGER trig_update_duration
AFTER INSERT
ON recorder.records
FOR EACH ROW
EXECUTE PROCEDURE recorder.fct_update_duration();
我的查询是这样的:
select count(*) from recorder.records as rec where rec.rec_start < '2016-01-01' and channel_number != 42;
解释这个查询的分析:
Aggregate (cost=1250451.14..1250451.15 rows=1 width=0) (actual time=956017.494..956017.494 rows=1 loops=1)
-> Seq Scan on records rec (cost=0.00..1195534.66 rows=21966592 width=0) (actual time=34.581..950947.593 rows=23903295 loops=1)
Filter: ((rec_start < '2016-01-01 00:00:00-06'::timestamp with time zone) AND (channel_number <> 42))
Rows Removed by Filter: 7377886
Planning time: 0.348 ms
Execution time: 956017.586 ms
现在也一样,但禁用 seqscan :
Aggregate (cost=1456272.87..1456272.88 rows=1 width=0) (actual time=929963.288..929963.288 rows=1 loops=1)
-> Bitmap Heap Scan on records rec (cost=284158.85..1401356.39 rows=21966592 width=0) (actual time=118685.228..925629.113 rows=23903295 loops=1)
Recheck Cond: (rec_start < '2016-01-01 00:00:00-06'::timestamp with time zone)
Rows Removed by Index Recheck: 2798893
Filter: (channel_number <> 42)
Rows Removed by Filter: 612740
Heap Blocks: exact=134863 lossy=526743
-> Bitmap Index Scan on "idxRecordStart" (cost=0.00..278667.20 rows=22542169 width=0) (actual time=118628.930..118628.930 rows=24516035 loops=1)
Index Cond: (rec_start < '2016-01-01 00:00:00-06'::timestamp with time zone)
Planning time: 0.279 ms
Execution time: 929965.547 ms
我怎样才能使这种查询更快?
已添加: 我已经使用 rec_start 和 channel_number 创建了一个索引,经过 57 分钟的真空分析,现在查询在 3 分钟多一点内完成:
CREATE INDEX "plopLindex"
ON recorder.records
USING btree
(rec_start, channel_number);
详细解释同一查询的缓冲区:
explain (analyse, buffers, verbose) select count(*) from recorder.records as rec where rec.rec_start < '2016-01-01' and channel_number != 42;
Aggregate (cost=875328.61..875328.62 rows=1 width=0) (actual time=199610.874..199610.874 rows=1 loops=1)
Output: count(*)
Buffers: shared hit=69490 read=550462 dirtied=75118 written=51880"
-> Index Only Scan using "plopLindex" on recorder.records rec (cost=0.56..814734.15 rows=24237783 width=0) (actual time=66.115..197609.019 rows=23903295 loops=1)
Output: rec_start, channel_number
Index Cond: (rec.rec_start < '2016-01-01 00:00:00-06'::timestamp with time zone)
Filter: (rec.channel_number <> 42)
Rows Removed by Filter: 612740
Heap Fetches: 5364345
Buffers: shared hit=69490 read=550462 dirtied=75118 written=51880
Planning time: 12.416 ms
Execution time: 199610.988 ms
然后第二次执行这个查询(没有解释):11secs!进步很大。
【问题讨论】:
您在什么硬件上运行?什么类型和多少个磁盘?如果您使用的是单个 SATA 驱动器,您需要做的第一件事就是获得更快的存储空间。 您几乎可以肯定是受 IO 限制的。当您运行查询时,您的磁盘 IO 统计信息是什么样的?见superuser.com/questions/177256/… 和blogs.technet.microsoft.com/benp/2010/08/19/… 这意味着您受 IO 限制。解决这不是一个真正的编程问题 - 它是硬件和数据库配置。你会在serverfault.com 或dba.stackexchange.com 上得到更好的答案。如果可以的话,一个快速的测试是将 RAID-5 阵列转换为 RAID-0 阵列(无冗余 - 不要在生产系统上这样做! ) 看看有多大帮助。这至少会让您了解 RAID 1+0 配置的速度。如果磁盘是 5,400 RPM 或其他慢速类型,请购买更快的磁盘。并添加更多 RAM - 这永远不会受到伤害。 8GB 的数据量并不多。 第二次执行更快的事实是很自然的,因为第二次大部分数据都被缓存了。您将看到在explain (analyze, buffers)
输出中,如果shared hit=69490
的数字取自Postgres 共享内存,则会增加。我想看看表(或索引)是否膨胀,但堆提取的数量似乎是合理的。
您也可以尝试 Postgres 9.5,或者根据您的发布计划等待 9.6,它可以使用并行线程来做到这一点(如果硬盘可以应付的话,这确实可以提高性能)
【参考方案1】:
看到你的行数,这对我来说听起来并不异常,并且在其他 RDBMS 上也是如此。
您有太多行无法快速获得结果,并且由于您有一个 WHERE
子句,因此快速获得行数的唯一解决方案是创建特定的表来跟踪它,并填充TRIGGER
on INSERT
,或批处理作业。
TRIGGER
解决方案是 100% 准确但更密集,批处理解决方案是近似但更灵活,并且您增加批处理作业频率的次数越多,您的统计数据就越准确;
在您的情况下,我会选择第二种解决方案并创建一个或多个聚合表。
例如,您可以有一个批处理作业来计算按日期和渠道分组的所有行
满足这一特定需求的聚合表示例如下:
CREATE TABLE agr_table (AGR_TYPE CHAR(50), AGR_DATE DATE, AGR_CHAN SMALLINT, AGR_CNT INT)
您的批处理作业可以:
DELETE FROM agr_table WHERE AGR_TYPE='group_by_date_and_channel';
INSERT INTO agr_table
SELECT 'group_by_date_and_channel', rec_start, channel_number, count(*) as cnt
FROM recorder.records
GROUP BY rec_start, channel_number
;
然后您可以通过以下方式快速检索统计信息:
SELECT SUM(cnt)
FROM agr_table
WHERE AGR_DATE < '2016-01-01' and AGR_CHAN != 42
当然,这是一个非常简单的例子。您应该根据需要快速检索的统计数据来设计聚合表。
我建议仔细阅读Postgres Slow Counting 和Postgres Count Estimate
【讨论】:
【参考方案2】:是的,您已经创建了正确的索引来覆盖您的查询参数。 Thomas G 还向您建议了一个不错的解决方法。我完全同意。
但我还想与您分享另一件事:第二次运行只用了 11 秒(比第一次运行 3 分钟)在我看来您正面临“缓存问题”。
当您运行第一次执行时,postgres 将表页从磁盘抓取到 RAM,而当您执行第二次运行时,它需要的所有内容都已在内存中,并且只用了 11 秒即可运行。
我曾经遇到过完全相同的问题,我的“最佳”解决方案只是给 postgres 更多 shared_buffers
。我不依赖操作系统的文件缓存。我将大部分内存保留给 postgres 使用。但是,在 Windows 中进行简单的更改是很痛苦的。您有操作系统限制,并且 Windows“浪费”了太多内存来自行运行它。这是一个耻辱。
相信我...您不必更改硬件来添加更多 RAM(无论哪种方式,添加更多内存总是一件好事!)。最有效的改变是改变你的操作系统。如果你有一个“专用”服务器,为什么要浪费如此宝贵的内存与视频/声音/驱动程序/服务/AV/等……在那些你不(或永远不会)使用的东西上?
转到 Linux 操作系统(也许是 Ubuntu 服务器?)并在完全相同的硬件上获得更高的性能。
将 kernel.shmmax 更改为更大的值:
sysctl -w kernel.shmmax=14294967296
echo kernel.shmmax = 14294967296 >>/etc/sysctl.conf
然后您可以将 postgresql.conf 更改为:
shared_buffers = 6GB
effective_cache_size = 7GB
work_mem = 128MB
您会立即感受到不同。
【讨论】:
我同意使用 Linux 文件系统可能会提高性能(NTFS 在高负载下似乎不能很好地工作)。但我不同意在 Windows 上更改shared_buffers
是“***中的痛苦”的说法 - 完全一样:更改 postgresql.conf
然后重新启动服务
当我过去使用 Windows(在 Windows 7 之前)时,我曾试图以“遥远的方式”改变它,但我总是失败。 Postgres 从未启动,说我必须更改 OS shmmax。听到他们“修复”了 Windows 共享内存限制听起来很棒。以上是关于一个表中有超过 3000 万个条目,执行一次选择计数需要 19 分钟的主要内容,如果未能解决你的问题,请参考以下文章