一个表中有超过 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 分钟的主要内容,如果未能解决你的问题,请参考以下文章

MySQL,如何合并表重复条目[重复]

当存在 NULL 时,性能选择与另一个表中的条目不匹配的行

随机选择,但每个名称都应至少选择一次

我想从数据库表中选择最后 5 个条目并将其显示在页面上。如何使用 JSP 实现它?

django admin中的ForeignKey字段

将列添加到现有 SQL Server 表 - 含义