为读取性能配置 PostgreSQL

JPe*_*ier 47 postgresql performance postgresql-9.1 query-performance

我们的系统写入了大量数据(一种大数据系统)。写入性能足以满足我们的需求,但读取性能真的太慢了​​。

我们所有表的主键(约束)结构都相似:

timestamp(Timestamp) ; index(smallint) ; key(integer).
Run Code Online (Sandbox Code Playgroud)

一个表可以有数百万行,甚至数十亿行,而一个读请求通常是针对特定时间段(时间戳/索引)和标记的。查询返回大约 20 万行是很常见的。目前,我们每秒可以读取大约 15k 行,但我们需要快 10 倍。这是可能的,如果是,如何?

注意: PostgreSQL 是和我们的软件一起打包的,所以不同客户端的硬件是不一样的。

它是一个用于测试的虚拟机。VM 的主机是具有 24.0 GB RAM 的 Windows Server 2008 R2 x64。

服务器规范(虚拟机 VMWare)

Server 2008 R2 x64
2.00 GB of memory
Intel Xeon W3520 @ 2.67GHz (2 cores)
Run Code Online (Sandbox Code Playgroud)

postgresql.conf 优化

shared_buffers = 512MB (default: 32MB)
effective_cache_size = 1024MB (default: 128MB)
checkpoint_segment = 32 (default: 3)
checkpoint_completion_target = 0.9 (default: 0.5)
default_statistics_target = 1000 (default: 100)
work_mem = 100MB (default: 1MB)
maintainance_work_mem = 256MB (default: 16MB)
Run Code Online (Sandbox Code Playgroud)

表定义

CREATE TABLE "AnalogTransition"
(
  "KeyTag" integer NOT NULL,
  "Timestamp" timestamp with time zone NOT NULL,
  "TimestampQuality" smallint,
  "TimestampIndex" smallint NOT NULL,
  "Value" numeric,
  "Quality" boolean,
  "QualityFlags" smallint,
  "UpdateTimestamp" timestamp without time zone, -- (UTC)
  CONSTRAINT "PK_AnalogTransition" PRIMARY KEY ("Timestamp" , "TimestampIndex" , "KeyTag" ),
  CONSTRAINT "FK_AnalogTransition_Tag" FOREIGN KEY ("KeyTag")
      REFERENCES "Tag" ("Key") MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
  OIDS=FALSE,
  autovacuum_enabled=true
);
Run Code Online (Sandbox Code Playgroud)

询问

该查询在 pgAdmin3 中执行大约需要 30 秒,但如果可能,我们希望在 5 秒内获得相同的结果。

SELECT 
    "AnalogTransition"."KeyTag", 
    "AnalogTransition"."Timestamp" AT TIME ZONE 'UTC', 
    "AnalogTransition"."TimestampQuality", 
    "AnalogTransition"."TimestampIndex", 
    "AnalogTransition"."Value", 
    "AnalogTransition"."Quality", 
    "AnalogTransition"."QualityFlags", 
    "AnalogTransition"."UpdateTimestamp"
FROM "AnalogTransition"
WHERE "AnalogTransition"."Timestamp" >= '2013-05-16 00:00:00.000' AND "AnalogTransition"."Timestamp" <= '2013-05-17 00:00:00.00' AND ("AnalogTransition"."KeyTag" = 56 OR "AnalogTransition"."KeyTag" = 57 OR "AnalogTransition"."KeyTag" = 58 OR "AnalogTransition"."KeyTag" = 59 OR "AnalogTransition"."KeyTag" = 60)
ORDER BY "AnalogTransition"."Timestamp" DESC, "AnalogTransition"."TimestampIndex" DESC
LIMIT 500000;
Run Code Online (Sandbox Code Playgroud)

说明 1

"Limit  (cost=0.00..125668.31 rows=500000 width=33) (actual time=2.193..3241.319 rows=500000 loops=1)"
"  Buffers: shared hit=190147"
"  ->  Index Scan Backward using "PK_AnalogTransition" on "AnalogTransition"  (cost=0.00..389244.53 rows=1548698 width=33) (actual time=2.187..1893.283 rows=500000 loops=1)"
"        Index Cond: (("Timestamp" >= '2013-05-16 01:00:00-04'::timestamp with time zone) AND ("Timestamp" <= '2013-05-16 15:00:00-04'::timestamp with time zone))"
"        Filter: (("KeyTag" = 56) OR ("KeyTag" = 57) OR ("KeyTag" = 58) OR ("KeyTag" = 59) OR ("KeyTag" = 60))"
"        Buffers: shared hit=190147"
"Total runtime: 3863.028 ms"
Run Code Online (Sandbox Code Playgroud)

解释2

在我最近的测试中,选择我的数据花了 7 分钟!见下文:

"Limit  (cost=0.00..313554.08 rows=250001 width=35) (actual time=0.040..410721.033 rows=250001 loops=1)"
"  ->  Index Scan using "PK_AnalogTransition" on "AnalogTransition"  (cost=0.00..971400.46 rows=774511 width=35) (actual time=0.037..410088.960 rows=250001 loops=1)"
"        Index Cond: (("Timestamp" >= '2013-05-22 20:00:00-04'::timestamp with time zone) AND ("Timestamp" <= '2013-05-24 20:00:00-04'::timestamp with time zone) AND ("KeyTag" = 16))"
"Total runtime: 411044.175 ms"
Run Code Online (Sandbox Code Playgroud)

Erw*_*ter 59

数据对齐和存储大小

实际上,每个索引元组的开销是元组头的 8 个字节加上项目标识符的 4 个字节。

有关的:

我们有三列作为主键:

PRIMARY KEY ("Timestamp" , "TimestampIndex" , "KeyTag")

"Timestamp"      timestamp (8 bytes)
"TimestampIndex" smallint  (2 bytes)
"KeyTag"         integer   (4 bytes)
Run Code Online (Sandbox Code Playgroud)

结果是:

 页头中项目标识符的 4 个字节(不计入 8 个字节的倍数)

 索引元组头的 8 个字节
 8 字节“时间戳”
 2 字节“时间戳索引”
 用于数据对齐的 2 字节填充
 4 字节“KeyTag” 
 0 填充到最近的 8 个字节的倍数
-----
每个索引元组 28 个字节;加上一些字节的开销。

关于在此相关答案中测量对象大小:

多列索引中的列顺序

阅读这两个问题和答案以了解:

您拥有索引(主键)的方式,您可以在没有排序步骤的情况下检索行,这很吸引人,尤其是使用LIMIT. 但是检索行似乎非常昂贵。

通常,在多列索引中,"equality" 列应该放在最前面,"range" 列应该放在最后:

因此,尝试使用反向列顺序的附加索引:

CREATE INDEX analogransition_mult_idx1
   ON "AnalogTransition" ("KeyTag", "TimestampIndex", "Timestamp");
Run Code Online (Sandbox Code Playgroud)

这取决于数据分布。但是millions of row, even billion of rows这样可能会快得多。

由于数据对齐和填充,元组大小要大 8 个字节。如果您使用它作为普通索引,您可能会尝试删除第三列"Timestamp"。可能会快一点(因为它可能有助于排序)。

您可能希望保留两个索引。根据许多因素,您的原始索引可能更可取 - 特别是使用小的LIMIT.

autovacuum 和表统计信息

您的表统计信息需要是最新的。我确定你有autovacuum正在运行。

由于您的表似乎很大,并且统计信息对于正确的查询计划很重要,因此我将大幅增加相关列的统计目标

ALTER TABLE "AnalogTransition" ALTER "Timestamp" SET STATISTICS 1000;
Run Code Online (Sandbox Code Playgroud)

... 甚至更高,有数十亿行。最大值为 10000,默认为 100。

WHEREorORDER BY子句中涉及的所有列执行此操作。然后运行ANALYZE

表格布局

在此过程中,如果您应用所学的有关数据对齐和填充的知识,这种优化的表布局应该会节省一些磁盘空间并稍微提高性能(忽略 pk 和 fk):

CREATE TABLE "AnalogTransition"(
  "Timestamp" timestamp with time zone NOT NULL,
  "KeyTag" integer NOT NULL,
  "TimestampIndex" smallint NOT NULL,
  "TimestampQuality" smallint,
  "UpdateTimestamp" timestamp without time zone, -- (UTC)
  "QualityFlags" smallint,
  "Quality" boolean,
  "Value" numeric
);
Run Code Online (Sandbox Code Playgroud)

CLUSTER / pg_repack / pg_squeeze

要优化使用特定索引的查询的读取性能(无论是您的原始索引还是我建议的替代索引),您可以按照索引的物理顺序重写表。CLUSTER这样做,但它具有侵入性,并且在操作期间需要排他锁。
pg_repack是一种更复杂的替代方案,它可以在不使用表排他锁的情况下执行相同操作。
pg_squeeze是后来的类似工具(还没有使用过)。

这对大表有很大帮助,因为需要读取的表块要少得多。

内存

通常,2GB 的物理 RAM 不足以快速处理数十亿行。更多的 RAM 可能会有很长的路要走 - 伴随着适应的设置:显然更大effective_cache_size的开始。

  • 我只在 KeyTag 上添加了一个简单的索引,现在看起来非常快。我还将应用您关于数据对齐的建议。非常感谢! (2认同)

dez*_*zso 12

因此,从计划中我看到一件事:您的索引要么膨胀(然后与基础表一起),要么根本不适合这种查询(我试图在上面的最新评论中解决这个问题)。

索引的一行包含 14 个字节的数据(一些用于标题)。现在,根据计划中给出的数字进行计算:您从 190147 个页面中获得了 500,000 行——这意味着平均每页不到 3 个有用的行,也就是说,每个 8 kb 页面大约有 37 个字节。这是一个非常糟糕的比例,不是吗?由于索引的第一列是Timestamp字段并且它在查询中用作范围,因此规划器可以 - 并且确实 - 选择索引来查找匹配的行。但是条件中没有TimestampIndex提到WHERE,所以过滤KeyTag不是很有效,因为这些值应该随机出现在索引页面中。

因此,一种可能性是将索引定义更改为

CONSTRAINT "PK_AnalogTransition" PRIMARY KEY ("Timestamp", "KeyTag", "TimestampIndex")
Run Code Online (Sandbox Code Playgroud)

(或者,根据系统负载,将此索引创建为新索引:

CREATE INDEX CONCURRENTLY "idx_AnalogTransition" 
    ON "AnalogTransition" ("Timestamp", "KeyTag", "TimestampIndex");
Run Code Online (Sandbox Code Playgroud)
  • 这肯定需要一段时间,但您仍然可以在此期间工作。)

另一种可能是索引页的很大一部分被死行占用,这可以通过清理来删除。您使用设置创建了表格autovacuum_enabled=true- 但您是否曾经开始自动清理?还是VACUUM手动运行?