静态大型 PostgreSQL 表的查询性能

jom*_*mmi 4 postgresql database-design read-only-database query-performance postgresql-performance

我试图尽可能详细地说明这一点。抱歉长度!

背景

protein_snp_assoc我在 PostgreSQL(版本 12.13)数据库上创建了以下分区表:

CREATE TABLE protein_snp_assoc (
  protein_id    int not null,
  snp_id        int not null,
  beta          double precision,
  se            double precision,
  logp          double precision
) PARTITION BY RANGE (snp_id);
Run Code Online (Sandbox Code Playgroud)

然后,我根据以下模板创建了 51 个分区,每个分区包含大约 1.5 亿行(总共 76.5 亿行):

CREATE TABLE IF NOT EXISTS protein_snp_assoc_(x) PARTITION OF protein_snp_assoc
  FOR VALUES FROM (y) TO (z);
Run Code Online (Sandbox Code Playgroud)

其中x范围从 1 到 51,并y, z定义间隔,每个长度为 150,000。例如,前两个和最后一个分区是:

protein_snp_assoc_1 FOR VALUES FROM (1) TO (150001),
protein_snp_assoc_2 FOR VALUES FROM (150001) TO (300001), ...
protein_snp_assoc_51 FOR VALUES FROM (7500001) TO (7650001)
Run Code Online (Sandbox Code Playgroud)

变量列protein_id有 1,000 个唯一值(1 到 1,000),并且snp_id有 7,500,000 个唯一值(1 到 7,650,001)。由于该对(snp_id, protein_id)唯一确定表中的一行,因此我使用这两列创建 BTree 索引,并将其snp_id作为最左侧的变量:

CREATE INDEX ON protein_snp_assoc (snp_id, protein_id);
Run Code Online (Sandbox Code Playgroud)

这将是一个静态数据库。目前,它约占总数据的 20%(因为我正在进行原型设计),但是一旦所有数据都添加到数据库中,就不会再添加(也不会删除)任何行。

典型查询

最常见的查询是 (a) 单个 SNP/蛋白质查询,(b) 单个蛋白质、多个 SNP 查询,以及 (c) 多个蛋白质和多个 SNP 查询。

VALUES当我在本网站上读到时,我使用的示例查询表明,当IN(...)有多个值时,它可以提高性能。

-- Single SNP/protein
SELECT 
  * 
FROM 
  protein_snp_assoc
WHERE
  snp_id IN (VALUES (1))
AND
  protein_id IN (VALUES(1));
  
-- Multiple SNPs, single protein
SELECT 
  * 
FROM 
  protein_snp_assoc
WHERE
  snp_id IN (VALUES (1), (2))
AND
  protein_id IN (VALUES(1));
  
-- Multiple SNPs, multiple proteins
SELECT 
  * 
FROM 
  protein_snp_assoc 
WHERE
  snp_id IN (VALUES (1), (2))
AND
  protein_id IN (VALUES (1),(2));
Run Code Online (Sandbox Code Playgroud)

每个查询的信息EXPLAIN ANALYZE可以在这里看到(pastebin 链接):

单个 SNP/蛋白质 (pastebin) ,多个 SNP, 单个蛋白质 (pastebin) ,多个 SNP, 多个蛋白质 (pastebin)

与 Arrow/parquet 的基准测试和比较

我对多个 SNP/蛋白质组合运行了 1,000 个查询,其中 SNP 和蛋白质在插入查询之前是随机抽取的。为了获得某种参考,我将用于填充数据库的原始数据文件转换为.parquet文件,并使用 R 和包运行类似的查询arrow。结果如下表所示(所有时间均以毫秒为单位,lq分别uq为 25% 和 75% 百分位数)。

指数 n_snps n_蛋白质 分钟 lq 意思是 中位数 昆士兰大学 最大限度
postgres 1 1 0.05900 14.71125 18.02112 17.92850 21.14350 45.2850
1 1 34.31822 44.62842 49.30316 46.29033 48.07222 577.1411
postgres 1 2 4.07100 20.97125 25.40618 25.15250 29.35375 68.7700
1 2 47.61873 61.47562 67.87060 63.99824 65.68011 629.5121
postgres 10 1 118.18900 167.11100 181.76304 180.50250 196.41475 262.9640
10 1 138.73902 164.25678 177.47847 167.69684 176.15489 704.3115
postgres 10 2 168.10500 231.74825 248.74577 248.45400 264.95825 330.2810
10 2 219.73495 269.54206 287.34815 281.79291 286.22803 819.4827
postgres 10 10 731.77300 893.28625 940.90282 941.69650 989.38625 1162.4810
10 10 930.18264 1038.39510 1089.43522 1080.01131 1100.22580 2313.4975
postgres 50 1 665.23800 799.89600 849.73860 850.91900 898.27900 1050.0710
50 1 682.10049 711.62065 766.24498 735.49283 750.97367 1335.6018

正如您所看到的,随着 SNP 或蛋白质(或两者)数量的增加,PostgreSQL 和 Arrow 开始表现相似(尽管 Arrow 的最坏情况始终更糟)。

硬件

CPU(pastebin)。硬盘是希捷 IronWolf 10TB (ST10000VN0008)。内存是 64GB,但我看不到具体类型,因为我没有sudo机器的权限。操作系统:Ubuntu 22.04.1 LTS。

我的问题

基准测试的结果让我相信我的数据库没有优化。我担心当我开始向数据库添加更多数据时,性能会受到影响。有什么方法可以通过更好的设计、查询或其他类型的调整来加速涉及多个蛋白质和 SNP 的查询?

更新2023-03-12

感谢埃尔文和其他所有参与的人。我完全按照 Erwin 的指示进行(唯一的例外是我无法从 v12 更新到 v15),然后重新设置了这个新表的基准。下面是结果(与原始设计相比),其中index_order = snp_first是原始设计,index_order = protein_first是 Erwin 提出的设计:

索引顺序 n_snps n_蛋白质 分钟 lq 意思是 中位数 昆士兰大学 最大限度
snp_first 1 1 0.059 14.71125 18.02112 17.9285 21.14350 45.285
蛋白质优先 1 1 0.060 20.96200 24.87686 26.3945 30.31275 126.046
snp_first 1 2 4.071 20.97125 25.40618 25.1525 29.35375 68.770
蛋白质优先 1 2 2.764 37.02300 44.31820 45.7595 52.30925 84.515
snp_first 10 1 118.189 167.11100 181.76304 180.5025 196.41475 262.964
蛋白质优先 10 1 29.754 215.37700 221.30159 255.3445 276.62725 380.930
snp_first 10 2 168.105 231.74825 248.74577 248.4540 264.95825 330.281
蛋白质优先 10 2 88.473 320.08475 417.07273 461.6155 501.66350 593.604
snp_first 10 10 731.773 893.28625 940.90282 941.6965 989.38625 1162.481
蛋白质优先 10 10 1189.058 1906.78050 2040.40170 2054.9985 2194.80550 2595.215
snp_first 50 1 665.238 799.89600 849.73860 850.9190 898.27900 1050.071
蛋白质优先 50 1 200.521 910.52700 934.64351 1091.5340 1149.79875 1319.777

正如您所看到的,原始设计速度要快得多,尤其是在最耗时的查询上。本周我将与系统管理员讨论更新到 v15 的事宜,看看这是否会提高性能。无论如何,我认为这个实验已经证明这要么是一个查询问题(我编写的查询可能不是最佳的,请参阅有关我如何使用的评论VALUES),要么是一个硬件问题(服务器很旧)。

回答评论中的一些问题

jjanes:请参阅此粘贴箱:https ://pastebin.com/qQR3GtZ4

a_horse_with_no_name:这个VALUES想法来自这里:Optimizing a Postgres query with a large IN

我写了一个测试查询:

EXPLAIN
WITH value_list (protein_id, snp_id) as (
  values
    (1, 1),
    (1, 2)
)
SELECT 
  *
FROM protein_snp_assoc AS p
INNER JOIN
value_list v on (p.protein_id, snp_id) = (v.protein_id, v.snp_id);
Run Code Online (Sandbox Code Playgroud)

我认为这给了我相同的查询计划,WHERE/IN但现在我发现我错了。我会研究一下这个,看看是否更好。编辑:WHERE/IN它的性能似乎与和相当VALUES。所以我想这不是真正的瓶颈。

bobflux:我无法共享数据,但你可以轻松模拟它。这是 R 中的一个简单示例:

sim_data <- function(i, n_snps) {
  data.frame(
    protein_id = rep(i, n_snps),
    snp_id = 1:n_snps,
    beta = rnorm(n = n_snps, mean = 0, sd = 1),
    se = abs(rnorm(n = n_snps, mean = 0, sd = 1)),
    logp = abs(rnorm(n = n_snps, mean = 2, sd = 1))
  )
}
protein_id <- 10
n_snps <- 7650000
sim_data(protein_id, n_snps)
Run Code Online (Sandbox Code Playgroud)

nz_21:我用 R 和 bash 编写了自定义脚本。

Erw*_*ter 10

基于此:

  • protein_id有 1,000 个唯一值
  • snp_id有 7,500,000 个唯一值
  • 该对(snp_id, protein_id)唯一地确定表中的一行

典型查询:
(a) 单个 SNP/蛋白质查询
(b) 单个蛋白质、多个 SNP 查询
(c) 多个蛋白质和多个 SNP 查询

表是只读的。

我没有看到分区的好处。删除分区并使用普通表代替:

CREATE TABLE protein_snp_assoc (
  protein_id int
, snp_id     int
, beta       double precision
, se         double precision
, logp       double precision
);
Run Code Online (Sandbox Code Playgroud)

填写表格后,添加此 PK:

ALTER TABLE protein_snp_assoc
ADD CONSTRAINT protein_snp_assoc_pkey
    PRIMARY KEY (protein_id, snp_id) WITH (FILLFACTOR = 100);
Run Code Online (Sandbox Code Playgroud)

值得注意的是, PK 替换了您的索引protein_snp_assoc,但替换为前导protein_id,因为它通常是查询中的单个值。由于FILLFACTOR = 100您的表是只读的。B 树索引的默认值为 90。请参阅:

除了 10% 的节省之外,基础指数的规模是相同的。两integer列对于多列索引来说非常理想。看:

填充后,在 PK 上对表进行 一次聚类:

CLUSTER TABLE protein_snp_assoc USING protein_snp_assoc_pkey;
Run Code Online (Sandbox Code Playgroud)

对于大桌子来说价格昂贵,但只做一次。这会重写表和索引,使两者都处于原始状态。

或者(更好)如果可能的话,首先用排序的数据填充表。理想情况COPY下与FREEZE

COPY protein_snp_assoc FROM '/path/to/filename' WITH (FREEZE);
Run Code Online (Sandbox Code Playgroud)

..在同一事务中创建或截断表后。请阅读FREEZE手册中的相关内容。和这个

COPYCREATE TABLE当在与较早的命令或命令相同的事务中使用时速度最快 TRUNCATE。在这种情况下,不需要写入 WAL,因为如果发生错误,包含新加载数据的文件无论如何都会被删除。然而,这种考虑仅适用于wal_levelis时minimal,否则所有命令都必须写入 WAL。

您必须是超级用户,并且有权访问服务器文件系统,这应该是这样。否则,下一个最佳选择是\copy代替COPY.

无论哪种方式,只有在填写表格后才添加 PK 。这要便宜得多,而且您可以立即获得原始状态的索引。阅读有关填充数据库的手册。

从一开始就关闭autovacuum此只读表。VACUUM ANALYZE protein_snp_assoc并在填充后运行一个。(或者VACUUM FREEZE ANALYZE protein_snp_assoc如果您没有走这COPY ... FREEZE条路。)对于均匀的数据分布,列统计数据并不是非常重要,但您仍然需要它们以及更新的可见性图。

不管怎样,最重要的是,具有相同前导的行protein_id现在在物理上聚集在一起,这最大限度地减少了为满足查询而必须读取的数据页的数量。

该表在原始状态下大约有 420 GB,索引略高于 140 GB。75 亿行 x 每个索引元组 20 字节(加上一些开销)或每个表行 60 字节。看:

在我们最小化和优化磁盘占用空间之后,缓存索引将至关重要,尤其是对于 3.5" 旋转磁盘。64 GB RAM 并不算过分。Postgres(与底层操作系统合作)仍然会最频繁地缓存访问索引和表的部分,如果您碰巧一次只关注其中的一个子集protein_id,那么这仍然会覆盖大部分需要缓存的内容。如果您的查询始终遍布各处,则 256 GB 或更多会有帮助(很多)。

更新:您在评论中透露,行数是原来的 5 倍。并且阅读模式是完全随机的。所以你又回到了磁盘读取主导成本的情况。您必须获得更快的存储。SSD代替HDD,没有什么好的替代品。另外,您可以获得的所有 RAM 至少可以缓存索引中最常用的部分。

所有这一切都可以使用最新版本的 Postgres撰写本文时为 Postgres 15.2)来完成,因为性能一直在稳步提升,尤其是对于大数据而言。