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)。
我对多个 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 的查询?
感谢埃尔文和其他所有参与的人。我完全按照 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)来完成,因为性能一直在稳步提升,尤其是对于大数据而言。