Выбрать первую строку в каждой группе по группам?
как следует из названия, я хотел бы выбрать первую строку каждого набора строк, сгруппированных с 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 или более поздней версии запрос также может извлечь выгоду из индекс только сканирование если индекс меньше базового таблица. Индекс должен быть отсканирован полностью.
-
на несколько строк на клиента, это очень эффективно (тем более, если вам нужен отсортированный выходной). Выгода уменьшается с увеличением количества строк на одного клиента.
В идеале, у вас достаточноwork_mem
обрабатывать вовлеченный шаг сортировки в ОЗУ и не разливать на диск. Как правило, параметрwork_mem
слишком высокая может иметь неблагоприятные последствия. СчитатьSET LOCAL
для очень больших запросов. Найти, сколько вам нужно, сEXPLAIN ANALYZE
. Упоминание о "диска: " в шаге сортировки указывает на необходимость больше: -
на много строк на клиент, a свободный индекс сканирования было бы (намного) эффективнее, но в настоящее время это не реализовано в Postgres (до v10).
Есть более быстрые методы запроса чтобы заменить это. В частности, если у вас есть отдельная таблица с уникальными клиентами, что является типичным случаем использования. Но и если вы этого не сделаете:
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
):
-
выбрать все таблицы, результаты в 5958 строк в этом случае.
A: 567.218 ms B: 386.673 ms
-
используйте условие
WHERE customer BETWEEN x AND y
в результате 1000 строк.A: 249.136 ms B: 55.111 ms
-
выберите одного клиента с
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
этот запрос достаточно быстр, особенно если в таблице покупок есть составной индекс (клиент, итого).
замечание:
t1, t2-псевдоним подзапроса, который может быть удален в зависимости от базы данных.
предостережение: 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
но если вы хотите делать такие вещи, вы, вероятно, ищете окно функции