Как следует из названия, я бы хотел выбрать первую строку каждого набора строк, сгруппированных с GROUP BY
.
В частности, если у меня есть purchases
таблица, которая выглядит следующим образом:
SELECT * FROM purchases;
Мой вывод:
id | клиент | Всего --- + ---------- + ------ 1 | Джо | 5 2 | Салли | 3 3 | Джо | 2 4 | Салли | 1
Я хотел бы запросить самую id
крупную покупку ( total
), сделанную каждым customer
. Что-то вроде этого:
SELECT FIRST(id), customer, FIRST(total)
FROM purchases
GROUP BY customer
ORDER BY total DESC;
Ожидаемый результат:
ПЕРВЫЙ (id) | клиент | ПЕРВЫЙ (всего) ---------- + ---------- + ------------- 1 | Джо | 5 2 | Салли | 3
sql
sqlite
postgresql
group-by
greatest-n-per-group
Дэвид Волевер
источник
источник
MAX(total)
?Ответы:
В Oracle 9.2+ (а не в 8i +, как было изначально указано), SQL Server 2005+, PostgreSQL 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:
Поддерживается любой базой данных:
Но вам нужно добавить логику, чтобы разорвать связи:
источник
ROW_NUMBER() OVER(PARTITION BY [...])
наряду с некоторыми другими оптимизациями, мне удалось сократить количество запросов с 30 до нескольких миллисекунд. Спасибо! (PostgreSQL 9.2)total
для одного клиента, 1-й запрос возвращает произвольного победителя (в зависимости от деталей реализации;id
может меняться при каждом выполнении!). Обычно (не всегда) вы хотите одну строку для каждого клиента, определяемую дополнительными критериями, такими как «та, которая имеет наименьшее количествоid
». Чтобы исправить, добавьтеid
вORDER BY
списокrow_number()
. Затем вы получите тот же результат, что и со вторым запросом, что очень неэффективно для этого случая. Кроме того, вам потребуется еще один подзапрос для каждого дополнительного столбца.В PostgreSQL это обычно проще и быстрее (подробнее об оптимизации производительности ниже):
Или короче (если не так ясно) с порядковыми номерами выходных столбцов:
Если
total
может быть NULL (не повредит в любом случае, но вы захотите соответствовать существующим индексам ):Основные моменты
DISTINCT ON
является расширением стандарта PostgreSQL (где определяется толькоDISTINCT
весьSELECT
список).Укажите любое количество выражений в
DISTINCT ON
предложении, объединенное значение строки определяет дубликаты. Руководство:Жирный акцент мой.
DISTINCT ON
могут быть объединены сORDER BY
. Начальные выражения вORDER BY
должны быть в наборе выражений вDISTINCT ON
, но вы можете свободно менять порядок среди них. Пример. Вы можете добавить дополнительные выражения, чтобыORDER BY
выбрать определенную строку из каждой группы пиров. Или, как сказано в руководстве :В
id
качестве последнего пункта я добавил разрыв связи:«Выберите строку с наименьшим
id
из каждой группы, разделяющим наибольшееtotal
».Чтобы упорядочить результаты способом, который не согласуется с порядком сортировки, определяющим первое для каждой группы, вы можете вкладывать вышеуказанный запрос во внешний запрос с другим
ORDER BY
. Пример.Если
total
может быть NULL, вы, скорее всего, захотите строку с наибольшим ненулевым значением. ДобавитьNULLS LAST
вроде продемонстрировано. Видеть:SELECT
Список не ограничивается выражениямиDISTINCT ON
илиORDER BY
каким - либо образом. (Не требуется в простом случае выше):Вам не нужно включать любое из выражений в
DISTINCT ON
илиORDER BY
.Вы можете включить любое другое выражение в
SELECT
список. Это способствует замене гораздо более сложных запросов подзапросами и агрегатными / оконными функциями.Я тестировал с Postgres версии 8.3 - 12. Но эта функция была там, по крайней мере, начиная с версии 7.1, так что в основном всегда.
Индекс
Идеальный показатель для приведенного выше запроса будет индексом несколько столбцов , охватывающим все три столбца в соответствии последовательности и сопоставление порядка сортировки:
Может быть слишком специализированным. Но используйте его, если производительность чтения для конкретного запроса имеет решающее значение. Если
DESC NULLS LAST
в запросе используется то же самое, что и в индексе, чтобы порядок сортировки соответствовал и индекс был применим.Эффективность / Оптимизация производительности
Взвесьте стоимость и выгоду перед созданием индивидуальных индексов для каждого запроса. Потенциал вышеуказанного индекса во многом зависит от распределения данных .
Индекс используется, потому что он предоставляет предварительно отсортированные данные. В Postgres 9.2 или более поздних версиях запрос также может быть полезен при сканировании только индекса, если индекс меньше базовой таблицы. Тем не менее, индекс должен быть отсканирован полностью.
Для нескольких строк на клиента (высокая мощность в столбце
customer
) это очень эффективно. Тем более, если вам все равно нужно отсортировать вывод. Преимущество уменьшается с ростом числа строк на одного клиента.В идеале у вас достаточно
work_mem
для обработки соответствующего этапа сортировки в оперативной памяти, а не для разлива на диск. Но в целом установкаwork_mem
слишком высоко может иметь неблагоприятные последствия. РассмотримSET LOCAL
исключительно большие запросы. Найдите, сколько вам нужноEXPLAIN ANALYZE
. Упоминание « Disk: » в шаге сортировки указывает на необходимость большего:Для многих строк на одного клиента (низкая мощность в столбце
customer
) свободное сканирование индекса (так называемое «сканирование с пропуском») будет (намного) более эффективным, но оно не реализовано до Postgres 12. (Реализация сканирования только по индексу разработка для Postgres 13. Смотрите здесь и здесь .)На данный момент есть более быстрые методы запросов, чтобы заменить это. В частности, если у вас есть отдельная таблица с уникальными клиентами, это типичный вариант использования. Но также, если вы этого не сделаете:
эталонный тест
У меня был простой тест, который уже устарел. Я заменил его подробным тестом в этом отдельном ответе .
источник
DISTINCT ON
становится чрезвычайно медленным. Реализация всегда сортирует всю таблицу и просматривает ее на наличие дубликатов, игнорируя все индексы (даже если вы создали требуемый многостолбцовый индекс). См. Объяснение xtended.com/2009/05/03/postgresql-optimizing-distinct для возможного решения.SELECT
списке.DISTINCT ON
подходит только для получения одного ряда на группу пиров.эталонный тест
Тестирование наиболее интересных кандидатов с Postgres 9.4 и 9.5 с наполовину реалистической таблицей 200k строк в
purchases
и 10k отчетливойcustomer_id
( ср. 20 строк на одного клиента ).Для Postgres 9.5 я провел второй тест с 86446 различными клиентами. Смотрите ниже (в среднем 2,3 строки на клиента ).
Настроить
Главный стол
Я использую
serial
(ограничение PK добавлено ниже) и целое число,customer_id
так как это более типичная установка. Также добавлено,some_column
чтобы компенсировать обычно больше столбцов.Фиктивные данные, PK, index - типичная таблица также имеет несколько мертвых кортежей:
customer
таблица - для лучшего запросаВо втором тесте для 9.5 я использовал ту же настройку, но с
random() * 100000
генерацией,customer_id
чтобы получить только несколько строкcustomer_id
.Размеры объекта для стола
purchases
Сгенерировано с этим запросом .
Запросы
1.
row_number()
в CTE ( см. Другой ответ )2.
row_number()
в подзапросе (моя оптимизация)3.
DISTINCT ON
( см. Другой ответ )4. rCTE с
LATERAL
подзапросом ( см. Здесь )5.
customer
таблица сLATERAL
( см. Здесь )6.
array_agg()
сORDER BY
( см. Другой ответ )Результаты
Время выполнения вышеупомянутых запросов с
EXPLAIN ANALYZE
(и всеми опциями выключено ), лучшее из 5 запусков .Во всех запросах использовалось сканирование только по индексу
purchases2_3c_idx
(среди прочих шагов). Некоторые из них только для меньшего размера индекса, другие более эффективно.A. Postgres 9.4 с 200 тыс. Строк и ~ 20 на
customer_id
Б. То же самое с Postgres 9,5
C. То же, что и B., но с ~ 2,3 строками на
customer_id
Связанные тесты
Вот новый тест "ogr" с 10M строками и 60k уникальных "клиентов" на Postgres 11.5 (по состоянию на сентябрь 2019 года). Результаты по-прежнему соответствуют тому, что мы видели до сих пор:
Оригинальный (устаревший) тест 2011 года
Я выполнил три теста с PostgreSQL 9.1 для реальной таблицы из 65579 строк и одностолбцовых индексов btree для каждого из трех задействованных столбцов и показал лучшее время выполнения из 5 запусков.
Сравнение первого запроса @OMGPonies (
A
) с вышеуказаннымDISTINCT ON
решением (B
):Выделите всю таблицу, в этом случае получается 5958 строк.
Используйте условие, в
WHERE customer BETWEEN x AND y
результате чего получите 1000 строк.Выберите одного клиента с помощью
WHERE customer = x
.Тот же тест повторяется с индексом, описанным в другом ответе
источник
2. row_number()
и5. customer table with LATERAL
примерах, что же обеспечить идентификатор будет наименьшим?customer_id
по строке с самым высокимtotal
. Это ошибочное совпадение в тестовых данных вопроса о том, чтоid
в выбранных строках также происходит наименьшее значениеcustomer_id
.Это распространено Наибольший-н-в-группапроблема, которая уже имеет хорошо проверенные и высоко оптимизированные решения . Лично я предпочитаю левое решение Билла Карвина ( оригинальный пост с множеством других решений ).
Обратите внимание, что кучу решений этой распространенной проблемы можно найти в одном из самых официальных источников, руководстве по MySQL ! См. Примеры распространенных запросов :: Строки, содержащие максимум группы определенного столбца .
источник
DISTINCT ON
версия намного короче, проще и, как правило, работает лучше в Postgres, чем альтернативы с самостоятельнойLEFT JOIN
или полу-анти-объединенияNOT EXISTS
. Это также "хорошо проверено".В Postgres вы можете использовать
array_agg
так:Это даст вам самую
id
большую покупку каждого клиента.Некоторые вещи, на которые стоит обратить внимание:
array_agg
это агрегатная функция, поэтому она работает сGROUP BY
.array_agg
Позволяет указать порядок размещения только для себя, чтобы он не ограничивал структуру всего запроса. Существует также синтаксис для сортировки значений NULL, если вам нужно сделать что-то отличное от значения по умолчанию.array_agg
аналогичным образом для вашего третьего выходного столбца, ноmax(total)
проще.DISTINCT ON
использования,array_agg
позволяет вам сохранитьGROUP BY
, на случай, если вы хотите этого по другим причинам.источник
Решение не очень эффективное, как указал Эрвин, из-за присутствия SubQ
источник
Я использую этот способ (только postgresql): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29
Тогда ваш пример должен работать почти так:
CAVEAT: игнорирует пустые строки
Изменить 1 - использовать вместо этого расширение postgres
Теперь я использую этот способ: http://pgxn.org/dist/first_last_agg/
Для установки на Ubuntu 14.04:
Это расширение postgres, которое дает вам первую и последнюю функции; по-видимому, быстрее, чем вышеописанным способом.
Редактировать 2 - Порядок и фильтрация
Если вы используете агрегатные функции (подобные этим), вы можете упорядочить результаты без необходимости упорядочивать данные:
Таким образом, эквивалентный пример с упорядочением будет выглядеть примерно так:
Конечно, вы можете заказывать и фильтровать по своему усмотрению в совокупности; это очень мощный синтаксис.
источник
Запрос:
КАК ЭТО РАБОТАЕТ! (Я был там)
Мы хотим убедиться, что у нас только самая высокая сумма для каждой покупки.
Некоторые теоретические материалы (пропустите эту часть, если вы хотите понять только запрос)
Пусть Total будет функцией T (customer, id), где она возвращает значение с указанным именем и id. Чтобы доказать, что данный итог (T (customer, id)) является наибольшим, мы должны доказать, что мы хотим доказать либо
ИЛИ
При первом подходе нам понадобятся все записи для этого имени, которое мне не очень нравится.
Второму понадобится умный способ сказать, что не может быть рекорда выше этого.
Вернуться к SQL
Если мы оставили присоединяемые таблицы по имени и общему количеству меньше объединенной таблицы:
мы удостоверяемся, что все записи, которые имеют другую запись с более высоким общим количеством для того же пользователя, будут присоединены:
Это поможет нам отфильтровать наибольшую сумму по каждой покупке без необходимости группировать:
И это ответ, который нам нужен.
источник
Очень быстрое решение
и действительно очень быстро, если таблица индексируется по id:
источник
В SQL Server вы можете сделать это:
Объяснение: здесь Группировка по выполняется на основе клиента, а затем заказывается по сумме, затем каждой такой группе присваивается серийный номер как StRank, и мы выбираем первого 1 клиента, чей StRank равен 1
источник
Используйте
ARRAY_AGG
функцию для PostgreSQL , U-SQL , IBM DB2 и Google BigQuery SQL :источник
В PostgreSQL другой возможностью является использование
first_value
оконной функции в сочетании сSELECT DISTINCT
:Я создал композит
(id, total)
, поэтому оба значения возвращаются одним и тем же агрегатом. Конечно, вы всегда можете подать заявкуfirst_value()
дважды.источник
Принятое мной решение OMG Ponies «Поддерживается любой базой данных» имеет хорошую скорость из моего теста.
Здесь я предоставляю тот же подход, но более полное и чистое решение для любой базы данных. Рассматриваются связи (предположим, что требуется получить только одну строку для каждого клиента, даже несколько записей для максимальной общей суммы для каждого клиента), и другие поля покупки (например, purchase_payment_id) будут выбраны для реальных совпадающих строк в таблице покупок.
Поддерживается любой базой данных:
Этот запрос достаточно быстрый, особенно когда в таблице покупок есть составной индекс, такой как (клиент, итог).
Замечание:
t1, t2 - псевдоним подзапроса, который можно удалить в зависимости от базы данных.
Предостережение : данное
using (...)
предложение в настоящее время не поддерживается в MS-SQL и Oracle db по состоянию на январь 2017 года. Вы должны расширить его, например, до самого себя.on t2.id = purchase.id
Синтаксис USING работает в SQLite, MySQL и PostgreSQL.источник
Snowflake / Teradata поддерживает
QUALIFY
предложение, которое работает какHAVING
для оконных функций:источник
Если вы хотите выбрать любую (по вашему конкретному условию) строку из набора агрегированных строк.
Если вы хотите использовать другую (
sum/avg
) функцию агрегирования в дополнение кmax/min
. Таким образом, вы не можете использовать ключ сDISTINCT ON
Вы можете использовать следующий подзапрос:
Вы можете заменить
amount = MAX( tf.amount )
любое условие, которое хотите, одним ограничением: этот подзапрос не должен возвращать более одной строкиНо если вы хотите делать такие вещи, вы, вероятно, ищете оконные функции
источник
Для SQl Server наиболее эффективным способом является:
и не забудьте создать кластерный индекс для используемых столбцов
источник