在PostgreSQL中使用GIN索引时如何加快ORDER BY排序?


13

我有一个这样的表:

CREATE TABLE products (
  id serial PRIMARY KEY, 
  category_ids integer[],
  published boolean NOT NULL,
  score integer NOT NULL,
  title varchar NOT NULL);

一个产品可以属于多个类别。category_ids列包含所有产品类别的ID列表。

典型查询如下所示(始终搜索单个类别):

SELECT * FROM products WHERE published
  AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;

为了加快速度,我使用以下索引:

CREATE INDEX idx_test1 ON products
  USING GIN (category_ids gin__int_ops) WHERE published;

除非一个类别中的产品太多,否则这将有很大帮助。它可以快速筛选出属于该类别的产品,但是必须以一种困难的方式(没有索引)进行排序操作。

一个已安装的btree_gin扩展程序,使我可以像这样构建多列GIN索引:

CREATE INDEX idx_test2 ON products USING GIN (
  category_ids gin__int_ops, score, title) WHERE published;

但是Postgres不想使用它进行排序。即使我DESC在查询中删除了说明符。

非常欢迎使用任何替代方法来优化任务。


附加信息:

  • PostgreSQL 9.4,具有intarray扩展
  • 产品总数目前为260k,但预计将显着增长(多达1000万,这是多租户电子商务平台)
  • 每个类别1..10000(最多可以增长到100k)的产品,平均水平低于100,但是那些产品数量众多的类别往往会吸引更多的请求

以下查询计划是从较小的测试系统(选定类别中的4680个产品,表中总共20万个产品)获得的:

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

注意#1:82 ms可能看起来并不那么可怕,但这是因为排序缓冲区适合内存。一旦从产品表中选择了所有列(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;

Sort Method: quicksort Memory: 903kB对于4680产品,这使我们回到〜80 ms。当产品数量增长到100k时仍然会很慢。


在此页面上:hlinnaka.iki.fi/2014/03/28/…有评论说btree_gin不能用于排序。
Mladen Uzelac

好的,我改了标题以允许更多选择。
Yaroslav Stavnichiy

您是否一直在寻找一个类别?并请提供更多基本信息:Postgres版本,基数,每个类别的行(最小/平均/最大)。考虑标签信息中的指示信息postgresql-performance。和:score可以为NULL,但您仍按而score DESC不是进行排序score DESC NULLS LAST。一个或另一个似乎不对...
Erwin Brandstetter 2015年

我已根据要求添加了其他信息。我一直在寻找单一类别。而score事实上是NOT NULL -我已经纠正了该表的定义。
Yaroslav Stavnichiy

Answers:


10

我做了很多实验,这是我的发现。

杜松子酒和排序

当前的GIN索引(自9.4版起)无法协助订购

在PostgreSQL当前支持的索引类型中,只有B树可以产生排序后的输出-其他索引类型以未指定的,依赖于实现的顺序返回匹配的行。

work_mem

感谢Chris指出了此配置参数。它默认为4MB,如果您的记录集更大,则增加到work_mem适当的值(可以从中找到EXPLAIN ANALYSE)可以大大加快排序操作。

ALTER SYSTEM SET work_mem TO '32MB';

重新启动服务器以使更改生效,然后再次检查:

SHOW work_mem;

原始查询

我已经用65万种产品填充了数据库,其中某些类别最多可容纳4万种产品。我通过删除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

正如我们所看到的work_mem那样,我们还不够Sort Method: external merge Disk: 29656kB(这里的数字是大概的,它在内存中的快速排序需要稍微超过32MB)。

减少内存占用

不要选择完整的记录进行排序,使用ID,应用排序,偏移量和限制,然后仅加载我们需要的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

注意Sort Method: quicksort Memory: 7396kB。结果好多了。

JOIN和其他B树索引

正如克里斯建议的那样,我创建了其他索引:

CREATE INDEX idx_test7 ON products (score DESC, title);

首先,我尝试像这样加入:

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;

查询计划略有不同,但结果相同:

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

在使用各种偏移量和产品计数时,我无法使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

仍未使用B树索引,结果集不适合work_mem,因此结果较差。

但是在某些情况下,拥有大量产品较小偏移量的 PostgreSQL现在决定使用B树索引:

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

实际上这很合乎逻辑,因为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

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

这是最坏的情况,选择类别中的产品很多且偏移量很大。当offset = 300时,执行时间仅为0.5 ms。

不幸的是,维护这样的连接表需要额外的精力。这可以通过建立索引的实例化视图来实现,但这仅在您的数据很少更新时才有用,因为刷新此类实例化视图是一项繁重的操作。

因此,到目前为止,我一直使用GIN索引,但增加work_mem和减少了内存占用量查询。


不是重启对于一般的变化work_mempostgresql.conf里设置。重新加载就足够了。另外,我要警告不要work_mem在多用户环境中全局设置过高(也不要过低)。如果您有一些需要更多的查询work_mem,则仅使用SET-或仅使用进行交易,将其设置为较高SET LOCAL。请参阅:dba.stackexchange.com/a/48633/3684
Erwin Brandstetter

真是个好答案。帮了我很多忙,特别是对磁盘->内存中排序操作,快速更改非常有帮助,谢谢!
里卡多·维拉米尔

4

这里有一些快速提示,可以帮助您提高性能。我将从最简单的技巧开始,这对您而言几乎是毫不费力的,然后在第一个技巧之后转到更困难的技巧。

1。 work_mem

因此,我马上就可以看到,您的解释计划中报告的一种类型Sort Method: external merge Disk: 5696kB消耗的内存不到6 MB,但会溢出到磁盘上。您需要将文件中的work_mem设置增加到postgresql.conf足以使排序适合内存的程度。

编辑:另外,在进一步检查中,我发现在使用索引检查catgory_ids符合条件的索引后,位图索引扫描被强制变为“有损”,并且在从相关堆页面中读取行时必须重新检查条件。 请参阅 postgresql.org 上的这篇文章,以获得比我给出的更好的解释。:P要点是您work_mem太低了。如果您尚未调整服务器上的默认设置,那么它将无法正常运行。

此修复程序将使您基本上没有时间去做。更改为postgresql.conf,您就出发了!有关更多提示,请参考此性能调整页面

2.模式变更

因此,您已在架构设计中决定将innormal标准化category_ids为整数数组,然后强制您使用GIN或GIST索引来获得快速访问。根据我的经验,您选择的GIN索引比GIST的读取速度更快,因此在这种情况下,您做出了正确的选择。但是,GIN是未排序的索引。认为它更像是一个键值,在平等谓词是容易检查,但如操作WHERE >WHERE <ORDER BY不被索引便利。

一种不错的方法是使用桥表/连接表(用于指定数据库中的多对多关系)来规范您的设计。

在这种情况下,您有许多类别和一组对应的整数category_ids,并且有许多乘积及其对应的product_ids。而不是产品表中的列是category_ids 的整数数组,而是从您的模式中删除该数组列,然后创建一个表为

CREATE TABLE join_products_categories (product_id int, category_id int);

然后,您可以在桥表的两列上生成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);

只是我的拙见,但这些变化可能对您有很大的影响。work_mem至少首先尝试进行更改。

祝你好运!

编辑:

建立额外的索引以协助排序

因此,如果您的产品线随着时间扩展,某些查询可能会返回许多结果(成千上万?),但仍可能只是您总产品线的一小部分。在这些情况下,如果在内存中进行排序甚至可能会非常昂贵,但是可以使用适当设计的索引来辅助排序。

请参阅PostgreSQL的官方文档,该文档描述了Indexes和ORDER BY

如果您创建与您的ORDER BY需求匹配的索引

CREATE INDEX idx_product_sort ON products (score DESC, title);

然后Postgres将优化并确定使用索引还是执行显式排序将更具成本效益。请记住,不能保证 Postgres将使用该索引。它将寻求优化性能,并在使用索引还是显式排序之间进行选择。如果创建此索引,请对其进行监视,以查看其是否足以用于创建索引,如果大多数显式已完成,则将其删除。

不过,在这一点上,您的“最大的节省”可能会使用更多work_mem,但是在某些情况下索引可以支持排序。


我也在考虑使用联结表来避免GIN。但是您没有指定如何帮助分类。我认为这无济于事。我尝试通过带有通过GIN查询收集的一组产品ID的产品表联接产品,我认为这与您提供的联接非常相似,并且该操作无法在得分和标题上使用b树索引。也许我建立了错误的索引。请您详细说明一下。
Yaroslav Stavnichiy

不好意思,也许我没有解释清楚。更改work_mem配置的目的是为了解决“磁盘排序”问题以及重新检查条件问题。随着产品数量的增加,您可能需要附加索引才能进行排序。请参阅上面的我的修改以进行澄清。
克里斯(Chris
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.