Вибрати перший рядок у кожній групі GROUP BY?


1321

Як випливає з назви, я хотів би вибрати перший рядок кожного набору рядків, згрупованих з a 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

Відповіді:


1115

У 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

2
Informix 12.x також підтримує віконні функції (хоча CTE потрібно перетворити у похідну таблицю). І Firebird 3.0 також підтримуватиме функції Window
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-го запиту, що дуже неефективно для цього випадку. Також вам знадобиться ще один підзапит для кожного додаткового стовпця.
Ервін Брандстеттер

2
Google BigQuery також підтримує команду ROW_NUMBER () першого запиту. Працював як принадність для нас
Праксителес

2
Зауважте, що перша версія з функцією вікна працює як у версії SQLite 3.25.0: sqlite.org/windowfunctions.html#history
brianz

1147

У 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стає надзвичайно повільно. Реалізація завжди сортує всю таблицю та сканує через неї дублікати, ігноруючи всі індекси (навіть якщо ви створили необхідний індекс з декількома стовпцями). Дивіться поясненняxinended.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для складання зазвичай більше стовпців.

Дані манекена, ПК, індекс - типова таблиця також містить кілька мертвих кортежів:

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 запусків .

Всі запити використовували Index Тільки сканування на purchases2_3c_idx(серед інших стадій). Деякі з них лише для меншого розміру індексу, інші більш ефективно.

A. Postgres 9,4 з 200k рядками та ~ 20 per 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. Те саме, що і Б., але з ~ 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" тестування з 10М рядками та 60 к. Унікальних "клієнтів" на 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 ! Дивіться приклади поширених запитів :: Рядки, що містять груповий максимум певного стовпця .


22
Як керівництво MySQL в будь-якому разі є "офіційним" для питань Postgres / SQLite (не кажучи вже про SQL)? Крім того, щоб бути зрозумілим, DISTINCT ONверсія набагато коротша, простіша і, як правило, краще працює в Postgres, ніж альтернативи з self LEFT JOINабо semi-anti-join NOT EXISTS. Він також "добре перевірений".
Erwin Brandstetter

3
Крім того, що написав Ервін, я б сказав, що використання віконної функції (яка сьогодні є загальним функціоналом SQL) майже завжди швидше, ніж використання з'єднання з похідною таблицею
a_horse_with_no_name

6
Чудові довідники. Я не знав, що це називається найбільшою проблемою n-per-group. Дякую.
Девід Манн

Питання ж НЕ , як для найбільшого п на групу , але перший п.
reinierpost

1
У випадку з двома полями замовлення я намагався, щоб "ліве рішення про приєднання Білла Карвіна" дало низьку ефективність. Дивіться мій коментар нижче stackoverflow.com/a/8749095/684229
Johnny Wong

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

14

Рішення не дуже ефективне, як вказував Ервін, через наявність SubQs

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

Дякую, так, згоден з вами, з'єднання між subq та зовнішнім запитом насправді займає більше часу. "In" тут не буде проблемою, оскільки subq призведе лише до одного рядка. До речі, на яку синтаксичну помилку ви вказуєте ??
користувач2407394

ой .. звик до "Терадати" .. редагував зараз..але розривання зв’язків тут не потрібно, оскільки потрібно знайти найвищий загальний для кожного клієнта ..
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: Ігнорує NULL рядки


Редагувати 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'

Це розширення на постгреси, яке дає вам першу та останню функції; мабуть швидше, ніж вищезгаданий спосіб.


Редагувати 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);

Звичайно, ви можете замовити та фільтрувати, коли вважаєте за потрібне в агрегаті; це дуже потужний синтаксис.


Використовуючи і цей підхід до функціональних функцій. Досить універсальний і простий. Навіщо ускладнювати речі, це значно менш ефективне рішення, ніж інші?
Сергій Щербаков

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)) є найвищим, ми повинні довести, що ми хочемо довести будь-яку

  • ∀x T (клієнт, ідентифікатор)> T (замовник, х) (ця сума вище, ніж усі інші для цього замовника)

АБО

  • ¬∃x T (замовник, ідентифікатор) <T (замовник, х) (для цього замовника немає вищої сумарної суми)

Перший підхід потребує нас, щоб ми отримали всі записи для того імені, яке мені не дуже подобається.

Другий потребує розумного способу сказати, що не може бути запису вище, ніж у цього.


Повернутися до 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

І це потрібна відповідь.


8

Дуже швидке рішення

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

і дуже швидко, якщо таблиця індексується ідентифікатором:

create index purchases_id on purchases (id);

Стаття USING дуже стандартна. Просто деякі незначні системи баз даних не мають цього.
Холгер Якобс

2
Це не знайде покупців з найбільшою сумою
Johnny Wong

7

У SQL Server ви можете зробити це:

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

Пояснення: Тут Group by робиться на основі замовника, а потім замовляє його загалом, то кожній такій групі присвоюється серійний номер як StRank, і ми виймаємо першого клієнта, StRank - 1


Дякую! Це прекрасно працювало і було дуже легко зрозуміти та реалізувати.
ruohola


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()двічі.


3

Прийняте рішення OMG Ponies "Підтримується будь-якою базою даних" має хорошу швидкість від мого тесту.

Тут я пропоную такий самий підхід, але більш повне та чисте рішення будь-якої бази даних. Враховуються зв'язки (припустимо бажання отримати лише один рядок для кожного клієнта, навіть декілька записів по максимуму на кожного клієнта), а інші поля покупки (наприклад, покупка_плата_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. Caveat : using (...)стаття в даний час не підтримується в MS-SQL та Oracle db станом на цю редакцію січня 2017 року. Ви повинні розширити її самостійно, наприклад, on t2.id = purchase.idтощо. Синтаксис USING працює у SQLite, MySQL та PostgreSQL.


2

Сніжинка / Терадата підтримує 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 найефективніший спосіб:

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.