【问题标题】:Optimize groupwise maximum query优化分组最大查询
【发布时间】:2014-08-06 07:09:00
【问题描述】:
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)

【问题讨论】:

  • “但是使用索引扫描所有行并没有多大意义” --- 确实如此。索引小于整个数据集,它们更有可能在缓存中。它虽然不扫描实际行,但扫描索引。
  • 创建索引的原始查询的计划是什么?
  • @zerkms indexing option_id 没有区别(正如我在问题中所述)索引 option_id_id_desc 或 option_id_id 在查询计划中也没有区别。
  • 如果添加(option_id, id desc) 索引并针对给定表运行ANALYZE 会怎样?顺便说一句,你运行的是什么 posgtresql 版本?
  • “我尝试在 option_id 上放置和删除索引,这对查询计划没有影响。” --- 单个 option_id 上的索引不太可能以任何方式影响它,因为您仍然需要检索 MAX(id) 因此遍历所有行。

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


【解决方案1】:

假设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

dbfiddle here - 显示简单案例的两个仅索引扫描
sqlfiddle

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

【讨论】:

    【解决方案2】:

    PostgreSQL 不支持 MySQL 能够用于此类查询的松散扫描。这是您在 MySQL 计划中看到的Using index for group-by

    基本上,它返回与复合键子集匹配的范围内的第一个或最后一个条目,然后搜索该子集的下一个或上一个值。

    在您的情况下,它首先返回(option_id, id) 上整个索引的最后一个值(根据定义,MAX(id) 恰好是最大的option_id),然后搜索最大 @ 旁边的最后一个值987654326@等。

    PostgreSQL 的优化器无法构建这样的计划,但是,PostgreSQL 允许您在 SQL 中模拟它。如果您有很多记录但很少有不同的option_id,那么值得这样做。

    为此,首先创建索引:

    CREATE INDEX ix_records_option_id ON records (option_id, id);
    

    然后运行这个查询:

    WITH RECURSIVE q (option_id) AS
            (
            SELECT  MIN(option_id)
            FROM    records
            UNION ALL
            SELECT  (
                    SELECT  MIN(option_id)
                    FROM    records
                    WHERE   option_id > q.option_id
                    )
            FROM    q
            WHERE   option_id IS NOT NULL
            )
    SELECT  option_id,
            (
            SELECT  MAX(id)
            FROM    records r
            WHERE   r.option_id = q.option_id
            )
    FROM    q
    WHERE   option_id IS NOT NULL
    

    在 sqlfiddle.com 上查看:http://sqlfiddle.com/#!15/4d77d/4

    【讨论】:

      【解决方案3】:

      您提到想要一个仅索引每个 option_id 的 max(id) 的索引。 PostgreSQL 目前不支持此功能。如果以后加入这样的功能,可能会通过在聚合查询上制作物化视图,然后对物化视图进行索引的机制来完成。不过,我不会期望至少几年。

      不过,您现在可以做的是使用递归查询,使其跳过索引到 option_id 的每个唯一值。有关技术的一般描述,请参阅the PostgreSQL wiki page

      您可以将其用于您的案例的方式是编写递归查询以返回 option_id 的不同值,然后为其中的每一个子选择 max(id):

      with recursive dist as (
        select min(option_id) as option_id from records
      union all
        select (select min(option_id) from records where option_id > dist.option_id) 
           from dist where dist.option_id is not null
      ) 
      
      select option_id, 
        (select max(id) from records where records.option_id=dist.option_id)
      from dist where option_id is not null;
      

      它很丑,但你可以把它隐藏在视图后面。

      在我手中,这需要 43 毫秒,而不是 on distinct 品种的 513 毫秒。

      如果你能找到一种方法将 max(id) 合并到递归查询中,它可能会快两倍,但我找不到这样做的方法。问题是这些查询具有相当严格的语法,您不能将“limit”或“order by”与 UNION ALL 结合使用。

      此查询涉及广泛分散在整个索引中的页面,如果这些页面不适合缓存,那么您将执行大量低效的 IO。但是,如果这种类型的查询很流行,那么 1057 个叶子索引页在缓存中的停留将没有什么问题。

      这是设置我的测试用例的方式:

      create table records  as select floor(random()*1057)::integer as option_id, floor(random()*50000000)::integer as id from generate_series(1,1240315);
      create index on records (option_id ,id);
      explain analyze;
      

      【讨论】:

        【解决方案4】:
        select distinct on (option_id) *
        from records
        order by option_id, id desc
        

        只有在cardinality 是有利的情况下才会使用索引。那就是说你可以试试复合索引

        create index index_name on records(option_id, id desc)
        

        【讨论】:

          猜你喜欢
          • 2015-10-24
          • 2021-11-05
          • 1970-01-01
          • 2012-05-19
          • 2018-06-18
          • 2015-10-17
          • 1970-01-01
          • 1970-01-01
          • 2022-10-13
          相关资源
          最近更新 更多