Выбрать первую строку в каждой группе по группам?

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

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

SELECT * FROM purchases;

Мой Вывод:

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

Я хотел бы запросить id самой большой покупки (total) каждого customer. Что-то вроде этого:--10-->

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

11 ответов


на 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

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

SELECT DISTINCT ON (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 может быть NULL (не повредит в любом случае, но вы захотите соответствовать существующим индексам):

...
ORDER  BY customer, total DESC NULLS LAST, id;

основные моменты

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

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

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

    смелый акцент шахта.

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

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

    если total может быть NULL, вы скорее всего требуется строка с наибольшим ненулевым значением. Добавить NULLS LAST как показали. Подробности:

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

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

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

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

индекс

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

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

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

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

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

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

Benchmark

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


Benchmark

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

для Postgres 9.5 я провел 2-й тест с эффективно 86446 различными клиентами. См. ниже (avg. 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 чтобы компенсировать обычно больше столбцов.

фиктивные данные, ПК, индекс-типичная таблица также имеет некоторые мертвые кортежи:

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 I используется та же настройка, но с 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 (и все варианты выкл), лучше 5 бежит.

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

A. Postgres 9.4 с 200k строк и ~ 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. То же, что и B., Но с ~ 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

Оригинал (устар) ориентир от

я провел три теста с 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
    

тот же тест повторяется с индексом описано в другой ответ!--58-->

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

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

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


в 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 позволяет указать область упорядочения только для себя, поэтому она не ограничивает структуру всего запроса. Существует также синтаксис для сортировки нулей, если вам нужно что-то сделать отличается от значения по умолчанию.
  • как только мы построим массив, мы возьмем первый элемент. (Массивы Postgres являются 1-индексированными, а не 0-индексированными).
  • можно использовать array_agg аналогичным образом для вашего третьего выходного столбца, но max(total) проще.
  • в отличие от DISTINCT ON, используя array_agg позволяет сохранить ваш GROUP BY, если вы хотите этого по другим причинам.

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

select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;

я использую этот способ (только 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 ;
$$;

-- 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 ;
$$;

-- 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;

предостережение: он игнорирует нулевые строки


Edit 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, которое дает вам первые и последние функции; по-видимому, быстрее, чем выше.


Edit 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);

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


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

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);

Запрос:

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)) является самым высоким, мы должны доказать это Мы хотим доказать, либо

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

или

  • ∃x T(клиент, 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

и это ответ мы необходимость.


принятое решение OMG Ponies "поддерживается любой базой данных" имеет хорошую скорость от моего теста.

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

поддерживается база данных:

select * from purchase
join (
    select min(id) as id from purchase
    join (
        select customer, max(total) as total from purchase
        group by customer
    ) t1 using (customer, total)
    group by customer
) t2 using (id)
order by customer

этот запрос достаточно быстр, особенно если в таблице покупок есть составной индекс (клиент, итого).

замечание:

  1. t1, t2-псевдоним подзапроса, который может быть удален в зависимости от базы данных.

  2. предостережение: the using (...) предложение в настоящее время не поддерживается в MS-SQL и Oracle db по состоянию на январь 2017 года. Вы должны расширить его самостоятельно, например on t2.id = purchase.id так далее. Синтаксис USING работает в SQLite, MySQL и PostgreSQL.


  • если вы хотите выбрать любую (по вашему конкретному условию) строку из набора агрегированных строк.

  • если вы хотите использовать другой (sum/avg) функция агрегации в дополнение к max/min. Таким образом, вы не можете использовать ключ с DISTINCT ON

вы можете использовать следующий подзапрос:

SELECT  
    (  
       SELECT **id** FROM t2   
       WHERE id = ANY ( ARRAY_AGG( tf.id ) ) AND amount = MAX( tf.amount )   
    ) id,  
    name,   
    MAX(amount) ma,  
    SUM( ratio )  
FROM t2  tf  
GROUP BY name

вы можете заменить amount = MAX( tf.amount ) С любым условием вы хотите с одним ограничением: этот подзапрос не должен возвращать более одного row

но если вы хотите делать такие вещи, вы, вероятно, ищете окно функции