Оптимизировать максимальный запрос groupwise

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 как они вставляются в таблицу рекордов. Таким образом, когда я запрашиваю максимумы option_ids, мне нужно только сканировать записи индекса столько раз, сколько есть разные option_ids.

Я видел 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

это здорово, он использует индекс. Однако использование индекса для сканирования всех идентификаторов не имеет большого смысла. Согласно моим исполнениям, это на самом деле медленнее, чем простое последовательное сканирование.

достаточно интересно, 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)

4 ответов


предполагая относительно несколько строк options на много строк в records.

как правило, у вас будет look-up стол options, на который ссылается records.option_id в идеале с ограничение внешнего ключа. Если вы этого не сделаете, я предлагаю создать один для обеспечения ссылочной целостности:

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;

тогда нам не нужно подражать свободный индекс сканирования больше и это становится очень просто и быстро. Коррелированные подзапросы могут использовать простой индекс на (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. Вы получаете NULL для max_id и вы можете легко удалить такие строки в наружной SELECT при необходимости.

или (тот же результат):

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

может быть немного быстрее. Подзапрос использует порядок сортировки DESC NULLS LAST - то же, что агрегатная функция max() который игнорирует значения NULL. Сортировка только DESC сначала будет иметь значение NULL:

Итак, идеальный индекс для этого:

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

не имеет большого значения, пока столбцы определены NOT NULL.

все еще может быть последовательное сканирование на маленькой таблице options, это самый быстрый способ получить все строки. The ORDER BY может привести к сканированию индекса (только) для предварительной сортировки строки.
Большой стол!--5--> доступен только через (растровое) сканирование индекса-или, если возможно,сканирование.

SQL Fiddle отображение двух индексных сканирований только для простого случая.

или использовать LATERAL присоединяется для аналогичного эффекта в Postgres 9.3+:


вы упоминаете, что хотите индекс, который индексирует только max (id) для каждого option_id. В настоящее время это не поддерживается PostgreSQL. Если такая функция будет добавлена в будущем, это, вероятно, будет сделано через механизм создания материализованного представления на агрегатном запросе, а затем индексирования материализованного представления. Я не ожидал, по крайней мере пару лет.

Теперь вы можете использовать рекурсивный запрос, чтобы он пропускал индекс к каждому уникальному стоимость option_id. См.страница Вики PostgreSQL общее описание методики.

как вы можете использовать это для своего случая, напишите рекурсивный запрос, чтобы вернуть различные значения 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;

это уродливо, но вы можете скрыть его за видом.

в моих руках это работает в 43ms, а не 513ms для on distinct разнообразие.

Он, вероятно, может быть сделано примерно в два раза быстрее, если вы можете найти способ включить max (id) в рекурсивный запрос, но я не мог найти способ сделать это. Проблема в том, что эти запросы имеют довольно ограничительный синтаксис, вы не можете использовать "limit" или "order by" в сочетании с UNION ALL.

этот запрос касается страницы, широко разбросанной по всему индексу, и если эти страницы не вписываются в кэш, то вы будете делать много неэффективного ввода-вывода. Однако, если этот тип запроса популярен, то Страниц индекса 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;

PostgreSQL не поддерживает свободное сканирование, которое MySQL может использовать для таких запросов. Это Using index for group-by вы видите на плане MySQL.

в основном, он возвращает первую или последнюю запись в диапазоне, соответствующем подмножеству составного ключа, а затем ищет следующее или Предыдущее значение этого подмножества.

в вашем случае он сначала возвращает последнее значение полного индекса на (option_id, id) (который по определению содержит MAX(id) для наибольшего option_id), затем ищет последнее значение с рядом с самым большим option_id и так далее.

оптимизатор 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


select distinct on (option_id) *
from records
order by option_id, id desc

индексы будут использоваться только в том случае, если мощностью благоприятный. Тем не менее, вы можете попробовать составной индекс

create index index_name on records(option_id, id desc)