Выберите первую строку в каждой группе GROUP BY? - PullRequest
1111 голосов
/ 27 сентября 2010

Как следует из названия, я бы хотел выбрать первую строку каждого набора строк, сгруппированных с GROUP BY.

В частности, если у меня есть таблица purchases, которая выглядит следующим образом:

SELECT * FROM purchases;

Мой вывод:

id | customer | total
---+----------+------
 1 | Joe      | 5
 2 | Sally    | 3
 3 | Joe      | 2
 4 | Sally    | 1

Я бы хотел запросить id крупнейшей покупки (total), совершенной каждым customer. Примерно так:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY total DESC;

Ожидаемый результат:

FIRST(id) | customer | FIRST(total)
----------+----------+-------------
        1 | Joe      | 5
        2 | Sally    | 3

Ответы [ 14 ]

975 голосов
/ 03 октября 2011

В PostgreSQL это обычно проще и быстрее (подробнее об оптимизации производительности ниже):

SELECT <b>DISTINCT ON</b> (customer)
       id, customer, total
FROM   purchases
ORDER  BY customer, total DESC, id;

Или короче (если не так ясно) с порядковыми номерами выходных столбцов:

SELECT DISTINCT ON (2)
       id, customer, total
FROM   purchases
ORDER  BY 2, 3 DESC, 1;

Если total может быть НЕДЕЙСТВИТЕЛЕН (не повредит в любом случае, но вы захотите соответствовать существующим индексам):

...
ORDER  BY customer, total DESC <b>NULLS LAST</b>, id;

Основные баллы

  • DISTINCT ON является расширением стандарта PostgreSQL (где определено только DISTINCT во всем списке SELECT).

  • Укажите любое количество выражений в предложении DISTINCT ON, объединенное значение строки определяет дубликаты. Руководство:

    Очевидно, что две строки считаются различными, если они отличаются по крайней мере значение одного столбца. В этом сравнении нулевые значения считаются равными.

    жирный акцент мой.

  • DISTINCT ON можно комбинировать с ORDER BY. Ведущие выражения должны соответствовать ведущим DISTINCT ON выражениям в том же порядке. Вы можете добавить дополнительные выражения к ORDER BY, чтобы выбрать конкретную строку из каждой группы пиров. Я добавил id как последний элемент для разрыва связей:

    "Выберите строку с наименьшим id из каждой группы, разделяющую наибольшее total."

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

  • Если total может быть НЕДЕЙСТВИТЕЛЕН, вам , наиболее вероятно , нужна строка с наибольшим ненулевым значением. Добавьте NULLS LAST как показано. Подробности:

  • Список SELECT никак не ограничен выражениями в DISTINCT ON или ORDER BY. (Не требуется в простом случае выше):

    • Вам не нужно включать любое из выражений в DISTINCT ON или ORDER BY.

    • Вы можете включить любое другое выражение в список SELECT. Это полезно для замены гораздо более сложных запросов подзапросами и агрегатными / оконными функциями.

  • Я тестировал с Postgres версии 8.3 - 12. Но эта функция была там, по крайней мере, начиная с версии 7.1, так что в основном всегда.

Индекс

Индекс perfect для вышеуказанного запроса будет представлять собой многостолбцовый индекс , охватывающий все три столбца в соответствующей последовательности и с соответствующим порядком сортировки:

CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);

Может быть слишком специализированным. Но используйте его, если производительность чтения для конкретного запроса имеет решающее значение. Если в запросе указано DESC NULLS LAST, используйте его в индексе, чтобы порядок сортировки соответствовал и индекс был применим.

Эффективность / Оптимизация производительности

Взвесьте затраты и выгоды перед созданием индивидуальных индексов для каждого запроса. Потенциал вышеуказанного индекса во многом зависит от распределения данных .

Индекс используется, потому что он предоставляет предварительно отсортированные данные. В Postgres 9.2 или более поздних версиях запрос также может быть полезен при сканировании только индекса , если индекс меньше базовой таблицы. Тем не менее, индекс должен быть отсканирован полностью.

Benchmark

У меня был простой тест, который уже устарел. Я заменил его подробным тестом в этом отдельном ответе .

951 голосов
/ 27 сентября 2010

На Oracle 9.2+ (не на 8i +, как первоначально было указано), SQL Server 2005+, PostgreSQL 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:

WITH summary AS (
    SELECT p.id, 
           p.customer, 
           p.total, 
           ROW_NUMBER() OVER(PARTITION BY p.customer 
                                 ORDER BY p.total DESC) AS rk
      FROM PURCHASES p)
SELECT s.*
  FROM summary s
 WHERE s.rk = 1

Поддерживается любой базой данных:

Но вам нужно добавить логику, чтобы разорвать связи:

  SELECT MIN(x.id),  -- change to MAX if you want the highest
         x.customer, 
         x.total
    FROM PURCHASES x
    JOIN (SELECT p.customer,
                 MAX(total) AS max_total
            FROM PURCHASES p
        GROUP BY p.customer) y ON y.customer = x.customer
                              AND y.max_total = x.total
GROUP BY x.customer, x.total
113 голосов
/ 11 января 2016

Benchmark

Тестирование наиболее интересных кандидатов с Postgres 9.4 и 9.5 с реалистичной таблицей на полпути 200k строк в purchases и 10k различных customer_id ( в среднем 20 строк на клиента ).

Для Postgres 9.5 я провел 2-й тест с 86446 разными клиентами. См. Ниже ( в среднем 2,3 строки на клиента ).

Настройка

Основной стол

CREATE TABLE purchases (
  id          serial
, customer_id int  -- REFERENCES customer
, total       int  -- could be amount of money in Cent
, some_column text -- to make the row bigger, more realistic
);

Я использую serial (ограничение PK добавлено ниже) и целое число customer_id, так как это более типичная установка. Также добавлено some_column, чтобы компенсировать обычно больше столбцов.

Фиктивные данные, PK, index - типичная таблица также содержит несколько мертвых кортежей:

INSERT INTO purchases (customer_id, total, some_column)    -- insert 200k rows
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,200000) g;

ALTER TABLE purchases ADD CONSTRAINT purchases_id_pkey PRIMARY KEY (id);

DELETE FROM purchases WHERE random() > 0.9; -- some dead rows

INSERT INTO purchases (customer_id, total, some_column)
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,20000) g;  -- add 20k to make it ~ 200k

CREATE INDEX purchases_3c_idx ON purchases (customer_id, total DESC, id);

VACUUM ANALYZE purchases;

customer таблица - для улучшенного запроса

CREATE TABLE customer AS
SELECT customer_id, 'customer_' || customer_id AS customer
FROM   purchases
GROUP  BY 1
ORDER  BY 1;

ALTER TABLE customer ADD CONSTRAINT customer_customer_id_pkey PRIMARY KEY (customer_id);

VACUUM ANALYZE customer;

В моем втором тесте для 9,5 я использовал ту же настройку, но с random() * 100000 для генерации customer_id, чтобы получить только несколько строк на customer_id.

Размеры объекта для стола purchases

Сгенерировано с помощью этого запроса .

               what                | bytes/ct | bytes_pretty | bytes_per_row
-----------------------------------+----------+--------------+---------------
 core_relation_size                | 20496384 | 20 MB        |           102
 visibility_map                    |        0 | 0 bytes      |             0
 free_space_map                    |    24576 | 24 kB        |             0
 table_size_incl_toast             | 20529152 | 20 MB        |           102
 indexes_size                      | 10977280 | 10 MB        |            54
 total_size_incl_toast_and_indexes | 31506432 | 30 MB        |           157
 live_rows_in_text_representation  | 13729802 | 13 MB        |            68
 ------------------------------    |          |              |
 row_count                         |   200045 |              |
 live_tuples                       |   200045 |              |
 dead_tuples                       |    19955 |              |

Запросы

1. row_number() в CTE, ( см. Другой ответ )

WITH cte AS (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   )
SELECT id, customer_id, total
FROM   cte
WHERE  rn = 1;

2. row_number() в подзапросе (моя оптимизация)

SELECT id, customer_id, total
FROM   (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   ) sub
WHERE  rn = 1;

3. DISTINCT ON ( см. Другой ответ )

SELECT DISTINCT ON (customer_id)
       id, customer_id, total
FROM   purchases
ORDER  BY customer_id, total DESC, id;

4. rCTE с LATERAL подзапросом ( см. здесь )

WITH RECURSIVE cte AS (
   (  -- parentheses required
   SELECT id, customer_id, total
   FROM   purchases
   ORDER  BY customer_id, total DESC
   LIMIT  1
   )
   UNION ALL
   SELECT u.*
   FROM   cte c
   ,      LATERAL (
      SELECT id, customer_id, total
      FROM   purchases
      WHERE  customer_id > c.customer_id  -- lateral reference
      ORDER  BY customer_id, total DESC
      LIMIT  1
      ) u
   )
SELECT id, customer_id, total
FROM   cte
ORDER  BY customer_id;

5. customer таблица с LATERAL ( см. Здесь )

SELECT l.*
FROM   customer c
,      LATERAL (
   SELECT id, customer_id, total
   FROM   purchases
   WHERE  customer_id = c.customer_id  -- lateral reference
   ORDER  BY total DESC
   LIMIT  1
   ) l;

6. array_agg() с ORDER BY ( см. Другой ответ )

SELECT (array_agg(id ORDER BY total DESC))[1] AS id
     , customer_id
     , max(total) AS total
FROM   purchases
GROUP  BY customer_id;

Результаты

Время выполнения вышеуказанных запросов с EXPLAIN ANALYZE (и всеми опциями off ), лучший из 5 запусков .

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

A. Postgres 9.4 с 200 тыс. Строк и ~ 20 на customer_id

1. 273.274 ms  
2. 194.572 ms  
3. 111.067 ms  
4.  92.922 ms  
5.  37.679 ms  -- winner
6. 189.495 ms

B. То же самое с Postgres 9,5

1. 288.006 ms
2. 223.032 ms  
3. 107.074 ms  
4.  78.032 ms  
5.  33.944 ms  -- winner
6. 211.540 ms  

C. То же, что и Б., но с ~ 2,3 строками на customer_id

1. 381.573 ms
2. 311.976 ms
3. 124.074 ms  -- winner
4. 710.631 ms
5. 311.976 ms
6. 421.679 ms

Оригинальный (устаревший) тест 2011 года

Я выполнил три теста с PostgreSQL 9.1 для реальной таблицы из 65579 строк и одностолбечных индексов btree для каждого из трех задействованных столбцов и взял лучшее время выполнения из 5 прогонов.
Сравнение первого запроса @ OMGPonies * (A) с решением выше DISTINCT ON (B):

  1. Выбор всей таблицы, в данном случае 5958 строк.

    A: 567.218 ms
    B: 386.673 ms
    
  2. Использовать условие WHERE customer BETWEEN x AND y, в результате чего получается 1000 строк.

    A: 249.136 ms
    B:  55.111 ms
    
  3. Выберите одного клиента с помощью WHERE customer = x.

    A:   0.143 ms
    B:   0.072 ms
    

Тот же тест повторяется с индексом, описанным в другом ответе

CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);

1A: 277.953 ms  
1B: 193.547 ms

2A: 249.796 ms -- special index not used  
2B:  28.679 ms

3A:   0.120 ms  
3B:   0.048 ms
47 голосов
/ 27 июня 2013

Это распространенная проблема, которая уже имеет хорошо протестированные и высоко оптимизированные решения . Лично я предпочитаю решение left join Билла Карвина (оригинальное сообщение с множеством других решений ).

Обратите внимание, что кучу решений этой распространенной проблемы можно найти в одном из самых официальных источников, Руководство по MySQL ! См. Примеры распространенных запросов :: Строки, содержащие максимум группы определенного столбца .

27 голосов
/ 27 августа 2014

В Postgres вы можете использовать array_agg, например:

SELECT  customer,
        (array_agg(id ORDER BY total DESC))[1],
        max(total)
FROM purchases
GROUP BY customer

Это даст вам id от крупнейшей покупки каждого клиента.

Некоторые вещи на заметку:

  • array_agg - агрегатная функция, поэтому она работает с GROUP BY.
  • array_agg позволяет вам указать порядок, ограниченный только им самим, поэтому он не ограничивает структуру всего запроса. Существует также синтаксис для сортировки значений NULL, если вам нужно сделать что-то отличное от значения по умолчанию.
  • Как только мы построим массив, мы возьмем первый элемент. (Массивы Postgres индексируются 1, а не 0).
  • Вы можете использовать array_agg аналогичным образом для третьего выходного столбца, но max(total) проще.
  • В отличие от DISTINCT ON, использование array_agg позволяет сохранить GROUP BY на случай, если вы захотите этого по другим причинам.
12 голосов
/ 17 июня 2013

Решение не очень эффективно, как указал Эрвин, из-за присутствия SubQs

select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;
8 голосов
/ 10 марта 2015

Я использую этот способ (только postgresql): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29

-- Create a function that always returns the first non-NULL item
CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $1;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.first (
        sfunc    = public.first_agg,
        basetype = anyelement,
        stype    = anyelement
);

-- Create a function that always returns the last non-NULL item
CREATE OR REPLACE FUNCTION public.last_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $2;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.last (
        sfunc    = public.last_agg,
        basetype = anyelement,
        stype    = anyelement
);

Тогда ваш пример должен работать почти как:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY FIRST(total) DESC;

CAVEAT:Он игнорирует пустые строки


Редактировать 1 - Вместо этого использовать расширение postgres

Теперь я использую этот способ: http://pgxn.org/dist/first_last_agg/

Для установки в Ubuntu 14.04:

apt-get install postgresql-server-dev-9.3 git build-essential -y
git clone git://github.com/wulczer/first_last_agg.git
cd first_last_app
make && sudo make install
psql -c 'create extension first_last_agg'

Это расширение postgres, которое дает вам первую и последнюю функции;очевидно, быстрее, чем описанный выше способ.


Редактировать 2 - Упорядочивание и фильтрация

Если вы используете агрегатные функции (подобные этим), вы можете упорядочить результаты без необходимости иметьуже упорядоченные данные:

http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES

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

SELECT first(id order by id), customer, first(total order by id)
  FROM purchases
 GROUP BY customer
 ORDER BY first(total);

Конечно, вы можете упорядочивать и фильтровать по своему усмотрению в совокупности;это очень мощный синтаксис.

8 голосов
/ 08 апреля 2014

Очень быстрое решение

SELECT a.* 
FROM
    purchases a 
    JOIN ( 
        SELECT customer, min( id ) as id 
        FROM purchases 
        GROUP BY customer 
    ) b USING ( id );

и действительно очень быстрое, если таблица индексируется по id:

create index purchases_id on purchases (id);
6 голосов
/ 24 марта 2018

Запрос:

SELECT purchases.*
FROM purchases
LEFT JOIN purchases as p 
ON 
  p.customer = purchases.customer 
  AND 
  purchases.total < p.total
WHERE p.total IS NULL

КАК ЭТО РАБОТАЕТ! (Я был там)

Мы хотим убедиться, что у нас только самая высокая сумма для каждой покупки.


Некоторые теоретические материалы (пропустите эту часть, если вы только хотите понять запрос)

Пусть Total будет функцией T (customer, id), где она возвращает значение с учетом имени и id Чтобы доказать, что данная сумма (T (customer, id)) является наибольшей, мы должны доказать, что Мы хотим доказать либо

  • Tx T (customer, id)> T (customer, x) (эта сумма выше, чем все остальные всего для этого клиента)

OR

  • ¬∃x T (customer, id)

При первом подходе нам понадобятся все записи для этого имени, которое мне не очень нравится.

Второму понадобится умный способ сказать, что не может быть записи выше этой.


Вернуться к SQL

Если мы оставили присоединяемые таблицы с именем и общим количеством меньше объединенной таблицы:

      LEFT JOIN purchases as p 
      ON 
      p.customer = purchases.customer 
      AND 
      purchases.total < p.total

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

purchases.id, purchases.customer, purchases.total, p.id, p.customer, p.total
1           , Tom           , 200             , 2   , Tom   , 300
2           , Tom           , 300
3           , Bob           , 400             , 4   , Bob   , 500
4           , Bob           , 500
5           , Alice         , 600             , 6   , Alice   , 700
6           , Alice         , 700

Это поможет нам отфильтровать наибольшую сумму по каждой покупке без необходимости группировки:

WHERE p.total IS NULL

purchases.id, purchases.name, purchases.total, p.id, p.name, p.total
2           , Tom           , 300
4           , Bob           , 500
6           , Alice         , 700

И этот ответ нам нужен.

5 голосов
/ 04 апреля 2019

Использование функции ARRAY_AGG для PostgreSQL , U-SQL , IBM DB2 и Google BigQuery SQL :

SELECT customer, (ARRAY_AGG(id ORDER BY total DESC))[1], MAX(total)
FROM purchases
GROUP BY customer
...