Yar*_*hiy 14 postgresql performance index postgresql-performance
我有一张这样的表:
CREATE TABLE products (
id serial PRIMARY KEY,
category_ids integer[],
published boolean NOT NULL,
score integer NOT NULL,
title varchar NOT NULL);
Run Code Online (Sandbox Code Playgroud)
一个产品可以属于多个类别。category_ids列包含所有产品类别的 id 列表。
典型的查询看起来像这样(总是搜索单个类别):
SELECT * FROM products WHERE published
AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;
Run Code Online (Sandbox Code Playgroud)
为了加快速度,我使用以下索引:
CREATE INDEX idx_test1 ON products
USING GIN (category_ids gin__int_ops) WHERE published;
Run Code Online (Sandbox Code Playgroud)
除非某一类别中的产品太多,否则这会很有帮助。它会快速过滤掉属于该类别的产品,但随后必须以艰难的方式完成排序操作(没有索引)。
已安装的btree_gin扩展允许我像这样构建多列 GIN 索引:
CREATE INDEX idx_test2 ON products USING GIN (
category_ids gin__int_ops, score, title) WHERE published;
Run Code Online (Sandbox Code Playgroud)
但是Postgres 不想使用它进行排序。即使我DESC在查询中删除了说明符。
任何优化任务的替代方法都非常受欢迎。
附加信息:
以下查询计划是从较小的测试系统中获得的(选定类别中的 4680 个产品,表中总共 200k 个产品):
Limit (cost=948.99..948.99 rows=1 width=72) (actual time=82.330..82.341 rows=20 loops=1)
-> Sort (cost=948.37..948.99 rows=245 width=72) (actual time=80.231..81.337 rows=4020 loops=1)
Sort Key: score, title
Sort Method: quicksort Memory: 928kB
-> Bitmap Heap Scan on products (cost=13.90..938.65 rows=245 width=72) (actual time=1.919..16.044 rows=4680 loops=1)
Recheck Cond: ((category_ids @> '{292844}'::integer[]) AND published)
Heap Blocks: exact=3441
-> Bitmap Index Scan on idx_test2 (cost=0.00..13.84 rows=245 width=0) (actual time=1.185..1.185 rows=4680 loops=1)
Index Cond: (category_ids @> '{292844}'::integer[])
Planning time: 0.202 ms
Execution time: 82.404 ms
Run Code Online (Sandbox Code Playgroud)
注意 #1:82 毫秒可能看起来并不那么可怕,但这是因为排序缓冲区适合内存。一旦我从 products 表中选择了所有列(SELECT * FROM ...在现实生活中大约有 60 列),它的Sort Method: external merge Disk: 5696kB执行时间就会加倍。这仅适用于 4680 种产品。
行动点#1(来自注释#1):为了减少排序操作的内存占用并因此加快速度,明智的做法是先获取、排序和限制产品ID,然后获取完整记录:
SELECT * FROM products WHERE id IN (
SELECT id FROM products WHERE published AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title LIMIT 20 OFFSET 8000
) ORDER BY score DESC, title;
Run Code Online (Sandbox Code Playgroud)
这让我们回到Sort Method: quicksort Memory: 903kB4680 产品大约 80 毫秒。当产品数量增长到 100k 时,仍然会很慢。
Yar*_*hiy 11
我做了很多实验,这里是我的发现。
GIN 索引当前(从 9.4 版开始)无法辅助排序。
在 PostgreSQL 当前支持的索引类型中,只有 B-tree 可以产生排序输出——其他索引类型以未指定的、依赖于实现的顺序返回匹配的行。
感谢 Chris 指出这个配置参数。它默认为 4MB,如果您的记录集更大,增加到work_mem适当的值(可以从 中找到EXPLAIN ANALYSE)可以显着加快排序操作。
ALTER SYSTEM SET work_mem TO '32MB';
Run Code Online (Sandbox Code Playgroud)
重新启动服务器以使更改生效,然后仔细检查:
SHOW work_mem;
Run Code Online (Sandbox Code Playgroud)
我已经用 650k 产品填充了我的数据库,其中一些类别最多包含 40k 产品。我通过删除published子句简化了查询:
SELECT * FROM products WHERE category_ids @> ARRAY [248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 30000;
Limit (cost=2435.62..2435.62 rows=1 width=1390) (actual time=1141.254..1141.256 rows=10 loops=1)
-> Sort (cost=2434.00..2435.62 rows=646 width=1390) (actual time=1115.706..1140.513 rows=30010 loops=1)
Sort Key: score, title
Sort Method: external merge Disk: 29656kB
-> Bitmap Heap Scan on products (cost=17.01..2403.85 rows=646 width=1390) (actual time=11.831..25.646 rows=41666 loops=1)
Recheck Cond: (category_ids @> '{248688}'::integer[])
Heap Blocks: exact=6471
-> Bitmap Index Scan on idx_products_category_ids_gin (cost=0.00..16.85 rows=646 width=0) (actual time=10.140..10.140 rows=41666 loops=1)
Index Cond: (category_ids @> '{248688}'::integer[])
Planning time: 0.288 ms
Execution time: 1146.322 ms
Run Code Online (Sandbox Code Playgroud)
正如我们所见work_mem,这还不够,所以我们有Sort Method: external merge Disk: 29656kB(这里的数字是近似值,内存中快速排序需要略多于 32MB)。
不要选择完整的记录进行排序,使用 ids,应用排序、偏移和限制,然后只加载我们需要的 10 条记录:
SELECT * FROM products WHERE id in (
SELECT id FROM products WHERE category_ids @> ARRAY[248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 30000
) ORDER BY score DESC, title;
Sort (cost=2444.10..2444.11 rows=1 width=1390) (actual time=707.861..707.862 rows=10 loops=1)
Sort Key: products.score, products.title
Sort Method: quicksort Memory: 35kB
-> Nested Loop (cost=2436.05..2444.09 rows=1 width=1390) (actual time=707.764..707.803 rows=10 loops=1)
-> HashAggregate (cost=2435.63..2435.64 rows=1 width=4) (actual time=707.744..707.746 rows=10 loops=1)
Group Key: products_1.id
-> Limit (cost=2435.62..2435.62 rows=1 width=72) (actual time=707.732..707.734 rows=10 loops=1)
-> Sort (cost=2434.00..2435.62 rows=646 width=72) (actual time=704.163..706.955 rows=30010 loops=1)
Sort Key: products_1.score, products_1.title
Sort Method: quicksort Memory: 7396kB
-> Bitmap Heap Scan on products products_1 (cost=17.01..2403.85 rows=646 width=72) (actual time=11.587..35.076 rows=41666 loops=1)
Recheck Cond: (category_ids @> '{248688}'::integer[])
Heap Blocks: exact=6471
-> Bitmap Index Scan on idx_products_category_ids_gin (cost=0.00..16.85 rows=646 width=0) (actual time=9.883..9.883 rows=41666 loops=1)
Index Cond: (category_ids @> '{248688}'::integer[])
-> Index Scan using products_pkey on products (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
Index Cond: (id = products_1.id)
Planning time: 0.682 ms
Execution time: 707.973 ms
Run Code Online (Sandbox Code Playgroud)
注意Sort Method: quicksort Memory: 7396kB。结果好多了。
正如克里斯所建议的,我已经创建了额外的索引:
CREATE INDEX idx_test7 ON products (score DESC, title);
Run Code Online (Sandbox Code Playgroud)
首先,我尝试像这样加入:
SELECT * FROM products NATURAL JOIN
(SELECT id FROM products WHERE category_ids @> ARRAY[248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 30000) c
ORDER BY score DESC, title;
Run Code Online (Sandbox Code Playgroud)
查询计划略有不同,但结果相同:
Sort (cost=2444.10..2444.11 rows=1 width=1390) (actual time=700.747..700.747 rows=10 loops=1)
Sort Key: products.score, products.title
Sort Method: quicksort Memory: 35kB
-> Nested Loop (cost=2436.05..2444.09 rows=1 width=1390) (actual time=700.651..700.690 rows=10 loops=1)
-> HashAggregate (cost=2435.63..2435.64 rows=1 width=4) (actual time=700.630..700.630 rows=10 loops=1)
Group Key: products_1.id
-> Limit (cost=2435.62..2435.62 rows=1 width=72) (actual time=700.619..700.619 rows=10 loops=1)
-> Sort (cost=2434.00..2435.62 rows=646 width=72) (actual time=697.304..699.868 rows=30010 loops=1)
Sort Key: products_1.score, products_1.title
Sort Method: quicksort Memory: 7396kB
-> Bitmap Heap Scan on products products_1 (cost=17.01..2403.85 rows=646 width=72) (actual time=10.796..32.258 rows=41666 loops=1)
Recheck Cond: (category_ids @> '{248688}'::integer[])
Heap Blocks: exact=6471
-> Bitmap Index Scan on idx_products_category_ids_gin (cost=0.00..16.85 rows=646 width=0) (actual time=9.234..9.234 rows=41666 loops=1)
Index Cond: (category_ids @> '{248688}'::integer[])
-> Index Scan using products_pkey on products (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
Index Cond: (id = products_1.id)
Planning time: 1.015 ms
Execution time: 700.918 ms
Run Code Online (Sandbox Code Playgroud)
使用各种偏移量和产品计数,我无法让 PostgreSQL 使用额外的 B 树索引。
所以我采用经典方式并创建了连接表:
CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id FROM products;
CREATE INDEX idx_prodcats_cat_prod_id ON prodcats (category_id, product_id);
SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 30000;
Limit (cost=122480.06..122480.09 rows=10 width=1390) (actual time=1290.360..1290.362 rows=10 loops=1)
-> Sort (cost=122405.06..122509.00 rows=41574 width=1390) (actual time=1264.250..1289.575 rows=30010 loops=1)
Sort Key: p.score, p.title
Sort Method: external merge Disk: 29656kB
-> Merge Join (cost=50.46..94061.13 rows=41574 width=1390) (actual time=117.746..182.048 rows=41666 loops=1)
Merge Cond: (p.id = c.product_id)
-> Index Scan using products_pkey on products p (cost=0.42..90738.43 rows=646067 width=1390) (actual time=0.034..116.313 rows=210283 loops=1)
-> Index Only Scan using idx_prodcats_cat_prod_id on prodcats c (cost=0.43..1187.98 rows=41574 width=4) (actual time=0.022..7.137 rows=41666 loops=1)
Index Cond: (category_id = 248688)
Heap Fetches: 0
Planning time: 0.873 ms
Execution time: 1294.826 ms
Run Code Online (Sandbox Code Playgroud)
仍然没有使用 B 树索引,结果集不适合work_mem,因此结果不佳。
但是在某些情况下,拥有大量产品和小偏移量的PostgreSQL现在决定使用B-tree索引:
SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 300;
Limit (cost=3986.65..4119.51 rows=10 width=1390) (actual time=264.176..264.574 rows=10 loops=1)
-> Nested Loop (cost=0.98..552334.77 rows=41574 width=1390) (actual time=250.378..264.558 rows=310 loops=1)
-> Index Scan using idx_test7 on products p (cost=0.55..194665.62 rows=646067 width=1390) (actual time=0.030..83.026 rows=108037 loops=1)
-> Index Only Scan using idx_prodcats_cat_prod_id on prodcats c (cost=0.43..0.54 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=108037)
Index Cond: ((category_id = 248688) AND (product_id = p.id))
Heap Fetches: 0
Planning time: 0.585 ms
Execution time: 264.664 ms
Run Code Online (Sandbox Code Playgroud)
这实际上很合乎逻辑,因为这里的 B 树索引不会产生直接结果,它仅用作顺序扫描的指南。
让我们与 GIN 查询进行比较:
SELECT * FROM products WHERE id in (
SELECT id FROM products WHERE category_ids @> ARRAY[248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 300
) ORDER BY score DESC, title;
Sort (cost=2519.53..2519.55 rows=10 width=1390) (actual time=143.809..143.809 rows=10 loops=1)
Sort Key: products.score, products.title
Sort Method: quicksort Memory: 35kB
-> Nested Loop (cost=2435.14..2519.36 rows=10 width=1390) (actual time=143.693..143.736 rows=10 loops=1)
-> HashAggregate (cost=2434.71..2434.81 rows=10 width=4) (actual time=143.678..143.680 rows=10 loops=1)
Group Key: products_1.id
-> Limit (cost=2434.56..2434.59 rows=10 width=72) (actual time=143.668..143.670 rows=10 loops=1)
-> Sort (cost=2433.81..2435.43 rows=646 width=72) (actual time=143.642..143.653 rows=310 loops=1)
Sort Key: products_1.score, products_1.title
Sort Method: top-N heapsort Memory: 68kB
-> Bitmap Heap Scan on products products_1 (cost=17.01..2403.85 rows=646 width=72) (actual time=11.625..31.868 rows=41666 loops=1)
Recheck Cond: (category_ids @> '{248688}'::integer[])
Heap Blocks: exact=6471
-> Bitmap Index Scan on idx_products_category_ids_gin (cost=0.00..16.85 rows=646 width=0) (actual time=9.916..9.916 rows=41666 loops=1)
Index Cond: (category_ids @> '{248688}'::integer[])
-> Index Scan using products_pkey on products (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
Index Cond: (id = products_1.id)
Planning time: 0.630 ms
Execution time: 143.921 ms
Run Code Online (Sandbox Code Playgroud)
GIN 的结果要好得多。我检查了产品数量和偏移量的各种组合,在任何情况下,连接表方法都不会更好。
为了让 PostgreSQL 充分利用索引进行排序,所有查询WHERE参数以及ORDER BY参数必须驻留在单个 B 树索引中。为此,我已将排序字段从产品复制到连接表:
CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id, score, title FROM products;
CREATE INDEX idx_prodcats_1 ON prodcats (category_id, score DESC, title, product_id);
SELECT * FROM products WHERE id in (SELECT product_id FROM prodcats WHERE category_id=248688 ORDER BY score DESC, title LIMIT 10 OFFSET 30000) ORDER BY score DESC, title;
Sort (cost=2149.65..2149.67 rows=10 width=1390) (actual time=7.011..7.011 rows=10 loops=1)
Sort Key: products.score, products.title
Sort Method: quicksort Memory: 35kB
-> Nested Loop (cost=2065.26..2149.48 rows=10 width=1390) (actual time=6.916..6.950 rows=10 loops=1)
-> HashAggregate (cost=2064.83..2064.93 rows=10 width=4) (actual time=6.902..6.904 rows=10 loops=1)
Group Key: prodcats.product_id
-> Limit (cost=2064.02..2064.71 rows=10 width=74) (actual time=6.893..6.895 rows=10 loops=1)
-> Index Only Scan using idx_prodcats_1 on prodcats (cost=0.56..2860.10 rows=41574 width=74) (actual time=0.010..6.173 rows=30010 loops=1)
Index Cond: (category_id = 248688)
Heap Fetches: 0
-> Index Scan using products_pkey on products (cost=0.42..8.45 rows=1 width=1390) (actual time=0.003..0.003 rows=1 loops=10)
Index Cond: (id = prodcats.product_id)
Planning time: 0.318 ms
Execution time: 7.066 ms
Run Code Online (Sandbox Code Playgroud)
这是选择类别中的大量产品和大偏移量的最坏情况。当 offset=300 时,执行时间仅为 0.5 ms。
不幸的是,维护这样的联结表需要额外的努力。它可以通过索引物化视图来完成,但这仅在您的数据更新很少时才有用,因为刷新此类物化视图是一项相当繁重的操作。
所以到目前为止我一直使用 GIN 索引,增加work_mem和减少内存占用查询。
这里有一些快速提示,可以帮助您提高表现。我将从最简单的技巧开始,这对您来说几乎毫不费力,然后在第一个技巧之后转向更困难的技巧。
work_mem因此,我立即发现您的解释计划中报告的排序Sort Method: external merge Disk: 5696kB消耗的内存少于 6 MB,但会溢出到磁盘。您需要增加文件work_mem中的设置,postgresql.conf使其足够大,以便排序可以适合内存。
编辑:此外,在进一步检查时,我发现在使用索引检查catgory_ids哪个符合您的条件后,位图索引扫描被迫变得“有损”,并且在从相关堆页中读取行时必须重新检查条件。 请参阅postgresql.org 上的这篇文章以获得比我给出的更好的解释。:P 最主要的一点是你的work_mem水平太低了。如果您尚未调整服务器上的默认设置,则它的性能不会很好。
此修复基本上不会花费您时间。只要更改为postgresql.conf,您就可以出发了!请参阅此性能调整页面以获取更多提示。
因此,您在模式设计中决定将 进行非规范化category_ids为整数数组,然后强制您使用 GIN 或 GIST 索引来获得快速访问。根据我的经验,您选择的 GIN 索引的读取速度比 GIST 更快,因此在这种情况下您做出了正确的选择。然而,GIN 是一个未排序的索引;可以将其视为键值对,其中等式谓词很容易检查,但索引无法促进诸如WHERE >、WHERE <或 之类的操作。ORDER BY
一个不错的方法是使用桥接表/连接表来标准化您的设计,用于指定数据库中的多对多关系。
在这种情况下,你有很多类别和一组相应的整数category_id,并且你有很多产品及其相应的product_id。不要将产品表中的列作为 s 的整数数组category_id,而是从架构中删除此数组列,然后创建一个表,如下所示
CREATE TABLE join_products_categories (product_id int, category_id int);
Run Code Online (Sandbox Code Playgroud)
然后,您可以在桥接表的两列上生成B树索引,
CREATE INDEX idx_products_in_join_table ON join_products_categories (product_id);
CREATE INDEX idx_products_in_join_table ON join_products_categories (category_id);
Run Code Online (Sandbox Code Playgroud)
这只是我的拙见,但这些变化可能会给您带来很大的改变。work_mem至少首先要尝试一下这种改变。
祝你好运!
编辑:
因此,如果随着时间的推移您的产品线扩展,某些查询可能会返回许多结果(数千、数万?),但这可能仍然只是整个产品线的一小部分。在这些情况下,如果在内存中进行排序,甚至可能非常昂贵,但是可以使用适当设计的索引来辅助排序。
请参阅描述Indexes 和 ORDER BY的官方 PostgreSQL 文档。
ORDER BY如果您创建符合您要求的索引
CREATE INDEX idx_product_sort ON products (score DESC, title);
Run Code Online (Sandbox Code Playgroud)
然后 Postgres 将优化并决定是否使用索引或执行显式排序将更具成本效益。请记住,不能保证Postgres 会使用该索引;它将寻求优化性能并在使用索引或显式排序之间进行选择。如果您创建此索引,请监视它以查看它的使用是否足以证明其创建的合理性,如果您的大多数排序都已显式完成,则将其删除。
尽管如此,在这一点上,您的“最大的性价比”改进可能会使用 more work_mem,但在某些情况下索引可以支持排序。