Выберите первую строку в каждой группе GROUP BY?

1326

Как следует из названия, я бы хотел выбрать первую строку каждого набора строк, сгруппированных с 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
Дэвид Волевер
источник
так как вы ищете только самый большой, почему бы не запросить MAX(total)?
phil294
4
@ phil294, запрашивающий max (total), не связывает эту сумму со значением 'id' строки, в которой это произошло.
Gwideman

Ответы:

1117

В 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
OMG пони
источник
2
Informix 12.x также поддерживает оконные функции (хотя CTE необходимо преобразовать в производную таблицу). И Firebird 3.0 также будет поддерживать оконные функции
a_horse_with_no_name
37
ROW_NUMBER() OVER(PARTITION BY [...])наряду с некоторыми другими оптимизациями, мне удалось сократить количество запросов с 30 до нескольких миллисекунд. Спасибо! (PostgreSQL 9.2)
Сэм
8
Если есть несколько покупок с одинаково высоким totalдля одного клиента, 1-й запрос возвращает произвольного победителя (в зависимости от деталей реализации; idможет меняться при каждом выполнении!). Обычно (не всегда) вы хотите одну строку для каждого клиента, определяемую дополнительными критериями, такими как «та, которая имеет наименьшее количество id». Чтобы исправить, добавьте idв ORDER BYсписок row_number(). Затем вы получите тот же результат, что и со вторым запросом, что очень неэффективно для этого случая. Кроме того, вам потребуется еще один подзапрос для каждого дополнительного столбца.
Эрвин Брандштеттер,
2
Google BigQuery также поддерживает команду ROW_NUMBER () первого запроса. Работал как шарм для нас
Praxiteles
2
Обратите внимание, что первая версия с оконной функцией работает с SQLite версии 3.25.0: sqlite.org/windowfunctions.html#history
brianz
1150

В 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. Начальные выражения в ORDER BYдолжны быть в наборе выражений в DISTINCT ON, но вы можете свободно менять порядок среди них. Пример. Вы можете добавить дополнительные выражения, чтобы ORDER BYвыбрать определенную строку из каждой группы пиров. Или, как сказано в руководстве :

    DISTINCT ONВыражение (ы) должно совпадать с крайним левым ORDER BY выражением (ы). Предложение ORDER BYобычно содержит дополнительные выражения, которые определяют желаемый приоритет строк в каждой DISTINCT ONгруппе.

    В 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, так что в основном всегда.

Индекс

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

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

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

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

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

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

эталонный тест

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

Эрвин Брандштеттер
источник
28
Это отличный ответ для большинства размеров баз данных, но я хочу отметить, что при приближении ~ миллион строк DISTINCT ONстановится чрезвычайно медленным. Реализация всегда сортирует всю таблицу и просматривает ее на наличие дубликатов, игнорируя все индексы (даже если вы создали требуемый многостолбцовый индекс). См. Объяснение xtended.com/2009/05/03/postgresql-optimizing-distinct для возможного решения.
Meekohi
14
Использование ординалов для «сокращения кода» - ужасная идея. Как насчет того, чтобы оставить имена столбцов, чтобы сделать их читаемыми?
KOTJMF
13
@ KOTJMF: Я предлагаю вам тогда пойти со своими личными предпочтениями. Я демонстрирую оба варианта обучения. Сокращения синтаксиса могут быть полезны для длинных выражений в SELECTсписке.
Эрвин Брандштеттер
1
@jangorecki: Исходный тест с 2011 года, у меня больше нет настроек. Но в любом случае пора было запускать тесты с pg 9.4 и pg 9.5. Подробности смотрите в добавленном ответе. , Вы можете добавить комментарий с результатом вашей установки ниже?
Эрвин Брандштеттер
2
@PirateApp: не из головы. DISTINCT ONподходит только для получения одного ряда на группу пиров.
Эрвин Брандштеттер
134

эталонный тест

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

Для Postgres 9.5 я провел второй тест с 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(и всеми опциями выключено ), лучшее из 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

Б. То же самое с 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

Связанные тесты

Вот новый тест "ogr" с 10M строками и 60k уникальных "клиентов" на Postgres 11.5 (по состоянию на сентябрь 2019 года). Результаты по-прежнему соответствуют тому, что мы видели до сих пор:

Оригинальный (устаревший) тест 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
Эрвин Брандштеттер
источник
5
Спасибо за отличный тест. Мне было интересно, выиграют ли новые данные BRIN от запроса данных о событиях, где у вас есть временная метка вместо общего числа . Это может потенциально дать ускорение для временных запросов.
jangorecki
3
@jangorecki: Любая огромная таблица с физически отсортированными данными может извлечь выгоду из индекса BRIN.
Эрвин Брандштеттер
@ErwinBrandstetter В 2. row_number()и 5. customer table with LATERALпримерах, что же обеспечить идентификатор будет наименьшим?
Артем Новиков
@ArtemNovikov: Ничего. Цель состоит в том, чтобы получить customer_id по строке с самым высоким total. Это ошибочное совпадение в тестовых данных вопроса о том, что idв выбранных строках также происходит наименьшее значение customer_id.
Эрвин Брандштеттер
1
@ArtemNovikov: разрешить сканирование только по индексу.
Эрвин Брандштеттер
55

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

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

TMS
источник
22
Как руководство MySQL каким-либо образом "официально" для вопросов Postgres / SQLite (не говоря уже о SQL)? Кроме того, чтобы быть ясным, DISTINCT ONверсия намного короче, проще и, как правило, работает лучше в Postgres, чем альтернативы с самостоятельной LEFT JOINили полу-анти-объединения NOT EXISTS. Это также "хорошо проверено".
Эрвин Брандштеттер
3
В дополнение к тому, что написал Эрвин, я бы сказал, что использование оконной функции (которая сегодня является распространенной функциональностью SQL) почти всегда быстрее, чем использование объединения с производной таблицей
a_horse_with_no_name
6
Отличные ссылки. Я не знал, что это называется самой большой проблемой для каждой группы. Спасибо.
Дэвид Манн
Вопрос не как для наибольшего n в группе, а для первого n.
reinierpost
1
В случае с двумя полями ордеров, который я попытался, «решение левого соединения Билла Карвина» дает плохую производительность. Смотрите мой комментарий ниже stackoverflow.com/a/8749095/684229
Джонни Вонг
30

В 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, на случай, если вы хотите этого по другим причинам.
Пол А Юнгвирт
источник
14

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

select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;
user2407394
источник
Спасибо, да, согласен с вами, объединение subq и внешнего запроса на самом деле занимает больше времени. Здесь "In" не будет проблемой, поскольку subq приведет только к одной строке. Кстати, на какую синтаксическую ошибку вы указываете ??
user2407394
ооо ... раньше "Teradata" .. редактировалось сейчас .. однако разрыв связей здесь не требуется, так как нужно найти наибольшее количество для каждого клиента ..
user2407394
Вы знаете, что вы получаете несколько строк для одного клиента в случае ничьей? Желательно ли это, зависит от точных требований. Обычно это не так. Для рассматриваемого вопроса название довольно ясно.
Эрвин Брандштеттер
Это не ясно из вопроса, если один и тот же клиент совершил покупку = Макс для двух разных идентификаторов, я думаю, что мы должны отобразить оба.
user2407394
10

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

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

matiu
источник
Используя этот подход пользовательской функции, а также. Достаточно универсален и прост. Зачем усложнять вещи, это значительно менее производительное решение, чем другие?
Сергей Щербаков
9

Запрос:

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) (эта сумма выше, чем все остальные суммы для этого клиента)

ИЛИ

  • ¬∃x T (customer, id) <T (customer, x) (для этого клиента не существует более высокой суммы)

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

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


Вернуться к 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

И это ответ, который нам нужен.

khaled_gomaa
источник
8

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

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);
Алехандро Саламанка Мазуэло
источник
Предложение USING очень стандартное. Просто у некоторых небольших систем баз данных его нет.
Хольгер Джейкобс
2
Это не находит покупки клиентов с наибольшей общей суммой
Джонни Вонг
7

В SQL Server вы можете сделать это:

SELECT *
FROM (
SELECT ROW_NUMBER()
OVER(PARTITION BY customer
ORDER BY total DESC) AS StRank, *
FROM Purchases) n
WHERE StRank = 1

Объяснение: здесь Группировка по выполняется на основе клиента, а затем заказывается по сумме, затем каждой такой группе присваивается серийный номер как StRank, и мы выбираем первого 1 клиента, чей StRank равен 1

Дивас Пудель
источник
Спасибо! Это отлично работало и было очень легко понять и реализовать.
Руохола
4

В PostgreSQL другой возможностью является использование first_valueоконной функции в сочетании с SELECT DISTINCT:

select distinct customer_id,
                first_value(row(id, total)) over(partition by customer_id order by total desc, id)
from            purchases;

Я создал композит (id, total), поэтому оба значения возвращаются одним и тем же агрегатом. Конечно, вы всегда можете подать заявку first_value()дважды.

pbillen
источник
3

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

Джонни Вонг
источник
2

Snowflake / Teradata поддерживает QUALIFYпредложение, которое работает как HAVINGдля оконных функций:

SELECT id, customer, total
FROM PURCHASES
QUALIFY ROW_NUMBER() OVER(PARTITION BY p.customer ORDER BY p.total DESC) = 1
Лукаш Шозда
источник
1
  • Если вы хотите выбрать любую (по вашему конкретному условию) строку из набора агрегированных строк.

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

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

Евгений Коньков
источник
1

Для SQl Server наиболее эффективным способом является:

with
ids as ( --condition for split table into groups
    select i from (values (9),(12),(17),(18),(19),(20),(22),(21),(23),(10)) as v(i) 
) 
,src as ( 
    select * from yourTable where  <condition> --use this as filter for other conditions
)
,joined as (
    select tops.* from ids 
    cross apply --it`s like for each rows
    (
        select top(1) * 
        from src
        where CommodityId = ids.i 
    ) as tops
)
select * from joined

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

BazSTR
источник