选择最小值时不使用索引的PostgreSQL多列组

Posted

技术标签:

【中文标题】选择最小值时不使用索引的PostgreSQL多列组【英文标题】:PostgreSQL multi-column group by not using index when selecting minimum 【发布时间】:2021-02-07 11:55:50 【问题描述】:

在对多个列执行GROUP BY 操作后在 PostgreSQL(11、12、13)的列上选择 MIN 时,不会使用在分组列上创建的任何索引:https://dbfiddle.uk/?rdbms=postgres_13&fiddle=30e0f341940f4c1fa6013677643a0baf

CREATE TABLE tags (id serial, series int, index int, page int);
CREATE INDEX ON tags (page, series, index);

INSERT INTO tags (series, index, page)
SELECT
    ceil(random() * 10),
    ceil(random() * 100),
    ceil(random() * 1000)
FROM generate_series(1, 100000);

EXPLAIN ANALYZE
SELECT tags.page, tags.series, MIN(tags.index)
FROM tags GROUP BY tags.page, tags.series;
HashAggregate  (cost=2291.00..2391.00 rows=10000 width=12) (actual time=108.968..133.153 rows=9999 loops=1)
  Group Key: page, series
  Batches: 1  Memory Usage: 1425kB
  ->  Seq Scan on tags  (cost=0.00..1541.00 rows=100000 width=12) (actual time=0.015..55.240 rows=100000 loops=1)
Planning Time: 0.257 ms
Execution Time: 133.771 ms

理论上,索引应该允许数据库以(tags.page, tags.series) 的步长进行查找,而不是执行全盘扫描。这将导致上述数据集的处理行数为 10,000,而不是 100,000。 This link 描述了没有分组列的方法。

This answer(以及this one)建议使用带有排序的DISTINCT ON 而不是GROUP BY,但这会产生这个查询计划:

Unique  (cost=0.42..5680.42 rows=10000 width=12) (actual time=0.066..268.038 rows=9999 loops=1)
  ->  Index Only Scan using tags_page_series_index_idx on tags  (cost=0.42..5180.42 rows=100000 width=12) (actual time=0.064..227.219 rows=100000 loops=1)
        Heap Fetches: 100000
Planning Time: 0.426 ms
Execution Time: 268.712 ms

虽然现在正在使用索引,但它似乎仍在扫描完整的行集。使用 SET enable_seqscan=OFF 时,GROUP BY 查询会降级为相同的行为。

如何鼓励 PostgreSQL 使用多列索引?

【问题讨论】:

【参考方案1】:

如果您可以从另一个表中提取一组不同的页面、系列,那么您可以使用横向连接来破解它:

CREATE TABLE pageseries AS SELECT DISTINCT page,series FROM tags ORDER BY page,series;
EXPLAIN ANALYZE SELECT p.*, minindex FROM pageseries p CROSS JOIN LATERAL (SELECT index minindex FROM tags t WHERE t.page=p.page AND t.series=p.series ORDER BY page,series,index LIMIT 1) x;
 Nested Loop  (cost=0.42..8720.00 rows=10000 width=12) (actual time=0.039..56.013 rows=10000 loops=1)
   ->  Seq Scan on pageseries p  (cost=0.00..145.00 rows=10000 width=8) (actual time=0.012..1.872 rows=10000 loops=1)
   ->  Limit  (cost=0.42..0.84 rows=1 width=12) (actual time=0.005..0.005 rows=1 loops=10000)
         ->  Index Only Scan using tags_page_series_index_idx on tags t  (cost=0.42..4.62 rows=10 width=12) (actual time=0.004..0.004 rows=1 loops=10000)
               Index Cond: ((page = p.page) AND (series = p.series))
               Heap Fetches: 0
 Planning Time: 0.168 ms
 Execution Time: 57.077 ms

...但不一定更快:

EXPLAIN ANALYZE                                                                                                                                              SELECT tags.page, tags.series, MIN(tags.index)
FROM tags GROUP BY tags.page, tags.series;

 HashAggregate  (cost=2291.00..2391.00 rows=10000 width=12) (actual time=56.177..58.923 rows=10000 loops=1)
   Group Key: page, series
   Batches: 1  Memory Usage: 1425kB
   ->  Seq Scan on tags  (cost=0.00..1541.00 rows=100000 width=12) (actual time=0.010..12.845 rows=100000 loops=1)
 Planning Time: 0.129 ms
 Execution Time: 59.644 ms

如果嵌套循环中的迭代次数很少,换句话说,如果不同的(页面,系列)数量很少,它会大大加快。我将单独尝试系列,因为它只有 10 个不同的值:

CREATE TABLE series AS SELECT DISTINCT series FROM tags;
EXPLAIN ANALYZE SELECT p.*, minindex FROM series p CROSS JOIN LATERAL (SELECT index minindex FROM tags t WHERE t.series=p.series ORDER BY series,index LIMIT 1) x;
 Nested Loop  (cost=0.29..886.18 rows=2550 width=8) (actual time=0.081..0.264 rows=10 loops=1)
   ->  Seq Scan on series p  (cost=0.00..35.50 rows=2550 width=4) (actual time=0.007..0.010 rows=10 loops=1)
   ->  Limit  (cost=0.29..0.31 rows=1 width=8) (actual time=0.024..0.024 rows=1 loops=10)
         ->  Index Only Scan using tags_series_index_idx on tags t  (cost=0.29..211.29 rows=10000 width=8) (actual time=0.023..0.023 rows=1 loops=10)
               Index Cond: (series = p.series)
               Heap Fetches: 0
 Planning Time: 0.198 ms
 Execution Time: 0.292 ms

在这种情况下,绝对值得,因为查询只命中 10/100000 行。其他查询达到 10000/100000 行,即表的 10%,高于索引真正有用的阈值。

请注意,将基数较低的列放在最前面会导致索引更小:

CREATE INDEX ON tags (series, page, index);
select pg_relation_size( 'tags_page_series_index_idx' );
          4284416
select pg_relation_size( 'tags_series_page_index_idx' );
          3104768

...但它并没有使查询变得更快。

如果这类东西真的很重要,不妨试试 clickhouse 或 dolphindb。

【讨论】:

【参考方案2】:

为了支持这种事情,PostgreSQL 必须有类似 index skip scan 之类的东西,并且只有在组很少的情况下才有效。

如果该查询的速度很重要,您可以考虑使用物化视图。

【讨论】:

以上是关于选择最小值时不使用索引的PostgreSQL多列组的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL 多列索引未完全使用

postgresql:具有外键的多个多列索引?

带表达式的多列索引(PostgreSQL 和 Rails)

Postgresql:适用于(时间戳,字符串)的多列索引

postgresql 9.6 建立多列索引测试

Postgresql获取具有多列的每个组的最大值[重复]