sql - 优化分组最大查询

标签 sql postgresql query-optimization greatest-n-per-group groupwise-maximum

select * 
from records 
where id in ( select max(id) from records group by option_id )

此查询即使在数百万行上也能正常工作。然而,正如您从解释语句的结果中看到的那样:

                                               QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop  (cost=30218.84..31781.62 rows=620158 width=44) (actual time=1439.251..1443.458 rows=1057 loops=1)
->  HashAggregate  (cost=30218.41..30220.41 rows=200 width=4) (actual time=1439.203..1439.503 rows=1057 loops=1)
     ->  HashAggregate  (cost=30196.72..30206.36 rows=964 width=8) (actual time=1438.523..1438.807 rows=1057 loops=1)
           ->  Seq Scan on records records_1  (cost=0.00..23995.15 rows=1240315 width=8) (actual time=0.103..527.914 rows=1240315 loops=1)
->  Index Scan using records_pkey on records  (cost=0.43..7.80 rows=1 width=44) (actual time=0.002..0.003 rows=1 loops=1057)
     Index Cond: (id = (max(records_1.id)))
Total runtime: 1443.752 ms

(cost=0.00..23995.15 rows=1240315 width=8) <- 这里说它正在扫描所有行,这显然是低效的。

我还尝试重新排序查询:

select r.* from records r
inner join (select max(id) id from records group by option_id) r2 on r2.id= r.id;

                                               QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------

Nested Loop  (cost=30197.15..37741.04 rows=964 width=44) (actual time=835.519..840.452 rows=1057 loops=1)
->  HashAggregate  (cost=30196.72..30206.36 rows=964 width=8) (actual time=835.471..835.836 rows=1057 loops=1)
     ->  Seq Scan on records  (cost=0.00..23995.15 rows=1240315 width=8) (actual time=0.336..348.495 rows=1240315 loops=1)
->  Index Scan using records_pkey on records r  (cost=0.43..7.80 rows=1 width=44) (actual time=0.003..0.003 rows=1 loops=1057)
     Index Cond: (id = (max(records.id)))
Total runtime: 840.809 ms

(cost=0.00..23995.15 rows=1240315 width=8) <- 仍在扫描所有行。

我尝试在 (option_id)(option_id, id)(option_id, id desc) 上使用和不使用索引, 它们都对查询计划没有任何影响。

有没有办法在不扫描所有行的情况下在 Postgres 中执行分组最大查询?

我正在以编程方式寻找的是一个索引,它存储每个 option_id 插入到记录表中时的最大 id。这样,当我查询 option_id 的最大值时,我只需要扫描索引记录的次数与存在不同 option_id 的次数相同。

我已经看到来自高级用户的 select distinct on 答案(感谢@Clodoaldo Neto 为我提供了搜索关键字)。这就是它不起作用的原因:

create index index_name on records(option_id, id desc)

select distinct on (option_id) *
from records
order by option_id, id desc
                                               QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------
Unique  (cost=0.43..76053.10 rows=964 width=44) (actual time=0.049..1668.545 rows=1056 loops=1)
  ->  Index Scan using records_option_id_id_idx on records  (cost=0.43..73337.25 rows=1086342 width=44) (actual time=0.046..1368.300 rows=1086342 loops=1)
Total runtime: 1668.817 ms

太好了,它正在使用索引。然而,使用索引扫描所有 ID 并没有多大意义。根据我的执行情况,它实际上比简单的顺序扫描要慢。

有趣的是,MySQL 5.5 能够简单地使用 records(option_id, id) 上的索引优化查询

mysql> select count(1) from records;

+----------+
| count(1) |
+----------+
|  1086342 |
+----------+

1 row in set (0.00 sec)

mysql> explain extended select * from records
       inner join ( select max(id) max_id from records group by option_id ) mr
                                                      on mr.max_id= records.id;

+------+----------+--------------------------+
| rows | filtered | Extra                    |
+------+----------+--------------------------+
| 1056 |   100.00 |                          |
|    1 |   100.00 |                          |
|  201 |   100.00 | Using index for group-by |
+------+----------+--------------------------+

3 rows in set, 1 warning (0.02 sec)

最佳答案

假设 options 中的行相对 records 中的多行。

通常,您会有一个从records.option_id 引用的查找options,最好使用foreign key constraint .如果您不这样做,我建议创建一个来强制执行参照完整性:

CREATE TABLE options (
  option_id int  PRIMARY KEY
, option    text UNIQUE NOT NULL
);

INSERT INTO options
SELECT DISTINCT option_id, 'option' || option_id -- dummy option names
FROM   records;

那么就没有必要模拟 loose index scan任何更多,这变得非常简单和快速。相关子查询可以在 (option_id, id) 上使用普通索引。

SELECT option_id, (SELECT max(id)
                   FROM   records
                   WHERE  option_id = o.option_id) AS max_id
FROM   options o
ORDER  BY 1;

这包括在表 records 中没有匹配的选项。 max_id 为 NULL,如果需要,您可以在外部 SELECT 中轻松删除此类行。

或者(同样的结果):

SELECT option_id, (SELECT id
                   FROM   records
                   WHERE  option_id = o.option_id
                   ORDER  BY id DESC NULLS LAST
                   LIMIT  1) AS max_id
FROM   options o
ORDER  BY 1;

可能会稍微快一点。子查询使用排序顺序 DESC NULLS LAST - 与忽略 NULL 值的聚合函数 max() 相同。仅排序 DESC 将首先得到 NULL:

完美的索引:

CREATE INDEX on records (option_id, id DESC NULLS LAST);

当列被定义为 NOT NULL 时,索引排序顺序并不重要。

仍然可以对小表 options 进行顺序扫描,这是获取所有行的最快方式。 ORDER BY 可能引入索引(仅)扫描以获取预先排序的行。
大表 records 只能通过(位图)索引扫描访问,或者,如果可能的话,index-only scan .

db<> fiddle here - 显示简单情况下的两个仅索引扫描
<子>旧sqlfiddle

或者在 Postgres 9.3+ 中使用 LATERAL 连接获得类似的效果:

关于sql - 优化分组最大查询,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24244026/

相关文章:

sql - 更新表中的 20 行确实很慢

postgresql - docker-compose up 与 docker-compose run,为什么后者似乎没有启动服务?

sql - 2 列的范围查询

mysql - 优化这个慢mysql查询

ruby-on-rails - 在哪里修补Rails ActiveRecord::find()以便首先检查内存中的集合?

java - JPQL 中的 LEFT JOIN ON()

SQL Server 状态监视器

php - 使用 UNION 时结果如何在两个表之间混合

sql - 如何快速批量更新postgres中的序列号

database - 使用存储过程模拟 postgresql