Seleziona la prima riga in ciascun gruppo GROUP BY?


1326

Come suggerisce il titolo, vorrei selezionare la prima riga di ogni serie di righe raggruppate con a GROUP BY.

In particolare, se ho una purchasestabella che assomiglia a questa:

SELECT * FROM purchases;

La mia uscita:

id | cliente | totale
--- + ---------- + ------
 1 | Joe | 5
 2 | Sally | 3
 3 | Joe | 2
 4 | Sally | 1

Vorrei richiedere idil più grande acquisto ( total) fatto da ciascuno customer. Qualcosa come questo:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY total DESC;

Uscita prevista:

PRIMO (id) | cliente | FIRST (totale)
---------- + ---------- + -------------
        1 | Joe | 5
        2 | Sally | 3

dal momento che stai cercando solo quello più grande, perché non fare una query MAX(total)?
phil294

4
La query @ phil294 per max (totale) non assocerà quel totale al valore 'id' della riga in cui si è verificato.
Gwideman,

Risposte:


1117

Su Oracle 9.2+ (non 8i + come indicato in origine), 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

Supportato da qualsiasi database:

Ma devi aggiungere la logica per rompere i legami:

  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 supporta anche le funzioni di finestra (il CTE deve tuttavia essere convertito in una tabella derivata). E Firebird 3.0 supporterà anche le funzioni di Window
a_horse_with_no_name

37
ROW_NUMBER() OVER(PARTITION BY [...])insieme ad alcune altre ottimizzazioni mi ha aiutato a ottenere una query da 30 secondi a pochi millisecondi. Grazie! (PostgreSQL 9.2)
Sam

8
Se ci sono più acquisti con ugualmente il più alto totalper un cliente, la prima query restituisce un vincitore arbitrario (a seconda dei dettagli delle implementazioni; idpuò cambiare per ogni esecuzione!). In genere (non sempre) si vorrebbe una riga per cliente, definita da criteri aggiuntivi come "quello con il più piccolo id". Per risolvere il problema, aggiungere idalla ORDER BYlista dei row_number(). Quindi si ottiene lo stesso risultato della seconda query, che è molto inefficiente per questo caso. Inoltre, avresti bisogno di un'altra subquery per ogni colonna aggiuntiva.
Erwin Brandstetter,

2
Google BigQuery supporta anche il comando ROW_NUMBER () della prima query. Ha funzionato come un incanto per noi
Prassitele

2
Si noti che la prima versione con la funzione finestra funziona a partire dalla versione 3.25.0 di SQLite: sqlite.org/windowfunctions.html#history
brianz

1150

In PostgreSQL questo è in genere più semplice e veloce (più ottimizzazione delle prestazioni di seguito):

SELECT DISTINCT ON (customer)
       id, customer, total
FROM   purchases
ORDER  BY customer, total DESC, id;

O più breve (se non altrettanto chiaro) con i numeri ordinali delle colonne di output:

SELECT DISTINCT ON (2)
       id, customer, total
FROM   purchases
ORDER  BY 2, 3 DESC, 1;

Se totalpuò essere NULL (non farà male in entrambi i modi, ma ti consigliamo di abbinare gli indici esistenti ):

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

Punti importanti

  • DISTINCT ONè un'estensione PostgreSQL dello standard (dove è definito solo DISTINCTl'intero SELECTelenco).

  • Elenca un numero qualsiasi di espressioni nella DISTINCT ONclausola, il valore della riga combinata definisce i duplicati. Il manuale:

    Ovviamente, due righe sono considerate distinte se differiscono per almeno un valore di colonna. I valori null sono considerati uguali in questo confronto.

    Enorme enfasi sulla mia.

  • DISTINCT ONpuò essere combinato con ORDER BY. Le espressioni principali in ORDER BYdevono essere nell'insieme delle espressioni in DISTINCT ON, ma è possibile riorganizzare l'ordine tra quelli liberamente. Esempio. È possibile aggiungere espressioni aggiuntiveORDER BY per selezionare una riga specifica da ciascun gruppo di peer. Oppure, come dice il manuale :

    Le DISTINCT ONespressioni devono corrispondere alle ORDER BY espressioni più a sinistra . La ORDER BYclausola conterrà normalmente espressioni aggiuntive che determinano la precedenza desiderata delle righe all'interno di ciascun DISTINCT ONgruppo.

    Ho aggiunto idcome ultimo elemento per interrompere i legami:
    "Scegli la riga con il più piccolo idda ciascun gruppo che condivide il più alto total".

    Per ordinare i risultati in un modo in disaccordo con l'ordinamento che determina il primo per gruppo, è possibile nidificare sopra la query in una query esterna con un'altra ORDER BY.Esempio.

  • Se totalpuò essere NULL, molto probabilmente si desidera la riga con il massimo valore non nullo. Aggiungi NULLS LASTcome dimostrato. Vedere:

  • L' SELECTelenco non è vincolato da espressioni in DISTINCT ONo ORDER BYin alcun modo. (Non necessario nel caso semplice sopra):

    • Non è necessario includere nessuna delle espressioni in DISTINCT ONo ORDER BY.

    • È possibile includere qualsiasi altra espressione SELECTnell'elenco. Questo è fondamentale per sostituire query molto più complesse con sottoquery e funzioni aggregate / window.

  • Ho provato con Postgres versioni 8.3 - 12. Ma la funzione è presente almeno dalla versione 7.1, quindi praticamente sempre.

Indice

L' indice perfetto per la query sopra sarebbe un indice multi-colonna che si estende su tutte e tre le colonne in sequenza corrispondente e con l'ordinamento corrispondente:

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

Potrebbe essere troppo specializzato. Ma usalo se le prestazioni di lettura per la particolare query sono cruciali. Se è presente DESC NULLS LASTnella query, utilizzare lo stesso nell'indice in modo che l'ordinamento corrisponda e l'indice sia applicabile.

Efficacia / Ottimizzazione delle prestazioni

Pesare costi e vantaggi prima di creare indici personalizzati per ogni query. Il potenziale dell'indice sopra dipende in gran parte dalla distribuzione dei dati .

L'indice viene utilizzato perché fornisce dati preordinati. In Postgres 9.2 o versioni successive la query può anche beneficiare di una scansione dell'indice solo se l'indice è più piccolo della tabella sottostante. L'indice deve essere scansionato nella sua interezza, però.

Prova delle prestazioni

Ho avuto un semplice benchmark qui che è ormai obsoleto. L'ho sostituito con un benchmark dettagliato in questa risposta separata .


28
Questa è un'ottima risposta per la maggior parte delle dimensioni del database, ma voglio sottolineare che quando ci si avvicina a ~ milioni di righe DISTINCT ONdiventa estremamente lento. L'implementazione ordina sempre l'intera tabella ed esegue la scansione per duplicati, ignorando tutti gli indici (anche se è stato creato l'indice multi-colonna richiesto). Vedere spieginextended.com/2009/05/03/postgresql-optimizing-distinct per una possibile soluzione.
Meekohi,

14
Usare gli ordinali per "abbreviare il codice" è un'idea terribile. Che ne dici di lasciare i nomi delle colonne per renderlo leggibile?
KOTJMF,

13
@KOTJMF: ti suggerisco di seguire le tue preferenze personali. Dimostro entrambe le opzioni per educare. La scorciatoia della sintassi può essere utile per le espressioni lunghe SELECTnell'elenco.
Erwin Brandstetter,

1
@jangorecki: il benchmark originale è del 2011, non ho più l'installazione. Ma era giunto il momento di eseguire comunque i test con pg 9.4 e pg 9.5. Vedi i dettagli nella risposta aggiunta. . Potresti aggiungere un commento con il risultato della tua installazione qui sotto?
Erwin Brandstetter,

2
@PirateApp: non dalla cima della mia testa. DISTINCT ONè utile solo per ottenere una riga per gruppo di colleghi.
Erwin Brandstetter,

134

Prova delle prestazioni

Testare i candidati più interessanti con Postgres 9.4 e 9.5 con una tabella realistica a metà strada di 200k righe in purchasese 10k distintecustomer_id ( media 20 righe per cliente ).

Per Postgres 9.5 ho eseguito un secondo test con effettivamente 86446 clienti distinti. Vedi sotto ( media 2.3 righe per cliente ).

Impostare

Tavolo principale

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

Uso un serial(vincolo PK aggiunto di seguito) e un numero intero customer_idpoiché si tratta di una configurazione più tipica. Inoltre aggiuntosome_column per compensare tipicamente più colonne.

Dati fittizi, PK, indice - una tabella tipica ha anche alcune tuple morte:

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 tabella - per query superiori

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;

Nel mio secondo test per 9.5 ho usato la stessa configurazione, ma con random() * 100000generate customer_idper ottenere solo poche righe per customer_id.

Dimensioni degli oggetti per la tabella purchases

Generato con questa query .

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

Interrogazioni

1. row_number()in CTE, ( vedi altra risposta )

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()in subquery (la mia ottimizzazione)

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( vedi altra risposta )

SELECT DISTINCT ON (customer_id)
       id, customer_id, total
FROM   purchases
ORDER  BY customer_id, total DESC, id;

4. rCTE con LATERALsubquery ( vedi qui )

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. customertabella con LATERAL( vedi qui )

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()con ORDER BY( vedi altra risposta )

SELECT (array_agg(id ORDER BY total DESC))[1] AS id
     , customer_id
     , max(total) AS total
FROM   purchases
GROUP  BY customer_id;

risultati

Tempo di esecuzione per le query precedenti con EXPLAIN ANALYZE(e tutte le opzioni disattivate ), la migliore delle 5 esecuzioni .

Tutte le query utilizzate un indice scansione solo su purchases2_3c_idx(tra gli altri gradini). Alcuni solo per le dimensioni più piccole dell'indice, altri in modo più efficace.

A. Postgres 9.4 con 200k file e ~ 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

B. Lo stesso con 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. Come B., ma con ~ 2.3 righe per 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

Benchmark correlati

Eccone uno nuovo con test "ogr" con 10 milioni di righe e 60.000 "clienti" unici su Postgres 11.5 (aggiornato a settembre 2019). I risultati sono ancora in linea con ciò che abbiamo visto finora:

Indice di riferimento originale (obsoleto) del 2011

Ho eseguito tre test con PostgreSQL 9.1 su una tabella di vita reale di 65579 righe e indici btree a colonna singola su ciascuna delle tre colonne coinvolte e ho ottenuto il miglior tempo di esecuzione di 5 esecuzioni.
Confrontando la prima query ( A) di @OMGPonies con la soluzione sopraDISTINCT ON ( B):

  1. Seleziona l'intera tabella, in questo caso risulta 5958 righe.

    A: 567.218 ms
    B: 386.673 ms
  2. Utilizzare la condizione WHERE customer BETWEEN x AND yrisultante in 1000 righe.

    A: 249.136 ms
    B:  55.111 ms
  3. Seleziona un singolo cliente con WHERE customer = x.

    A:   0.143 ms
    B:   0.072 ms

Lo stesso test si è ripetuto con l'indice descritto nell'altra risposta

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
Grazie per un ottimo punto di riferimento. Mi chiedevo se l'interrogazione dei dati sugli eventi in cui si ha un timestamp anziché il totale trarrebbe beneficio dal nuovo indice BRIN. Questo può potenzialmente aumentare la velocità per le query temporali.
jangorecki,

3
@jangorecki: qualsiasi enorme tabella con dati fisicamente ordinati può trarre vantaggio da un indice BRIN.
Erwin Brandstetter,

@ErwinBrandstetter Negli esempi 2. row_number()e 5. customer table with LATERAL, cosa garantisce che l'id sia il più piccolo?
Artem Novikov,

@ArtemNovikov: Niente. L'obiettivo è recuperare, per customer_id riga con il massimo total. È una coincidenza fuorviante nei dati di test della domanda che anche idnelle righe selezionate sia la più piccola per customer_id.
Erwin Brandstetter,

1
@ArtemNovikov: per consentire scansioni solo indice.
Erwin Brandstetter,

55

Questo è comune problema, che ha già soluzioni ben testate e altamente ottimizzate . Personalmente preferisco la soluzione di join sinistro di Bill Karwin (il post originale con molte altre soluzioni ).

Si noti che un mucchio di soluzioni a questo problema comune si può trovare sorprendentemente in una delle fonti ufficiali, il manuale di MySQL ! Vedi esempi di query comuni: le righe che trattengono il massimo del gruppo di una determinata colonna .


22
In che modo il manuale di MySQL è in qualche modo "ufficiale" per le domande Postgres / SQLite (per non parlare di SQL)? Inoltre, per essere chiari, la DISTINCT ONversione è molto più breve, più semplice e generalmente funziona meglio in Postgres rispetto alle alternative con un self LEFT JOINo semi-anti-join NOT EXISTS. È anche "ben testato".
Erwin Brandstetter,

3
In aggiunta a ciò che ha scritto Erwin, direi che l'uso di una funzione di finestra (che è la funzionalità SQL comune al giorno d'oggi) è quasi sempre più veloce rispetto all'utilizzo di un join con una tabella derivata
a_horse_with_no_name

6
Grandi riferimenti. Non sapevo che questo fosse chiamato il problema più grande per gruppo. Grazie.
David Mann,

La domanda fa , non come per il più grande n per gruppo, ma il primo n.
reinierpost,

1
In un caso di due campi di ordine che ho provato, la "soluzione di join sinistro di Bill Karwin" offre scarse prestazioni. Vedi il mio commento qui sotto stackoverflow.com/a/8749095/684229
Johnny Wong

30

In Postgres puoi usare array_aggcosì:

SELECT  customer,
        (array_agg(id ORDER BY total DESC))[1],
        max(total)
FROM purchases
GROUP BY customer

Questo ti darà il id più grande acquisto di ogni cliente.

Alcune cose da notare:

  • array_agg è una funzione aggregata, quindi funziona con GROUP BY .
  • array_aggti consente di specificare un ordinamento limitato a se stesso, in modo da non limitare la struttura dell'intera query. C'è anche una sintassi per come ordinare i NULL, se è necessario fare qualcosa di diverso dal valore predefinito.
  • Una volta creato l'array, prendiamo il primo elemento. (Gli array Postgres sono 1 indicizzati, non 0 indicizzati).
  • Puoi usare array_agg in modo simile per la terza colonna di output, ma max(total)è più semplice.
  • Diversamente DISTINCT ON, l'utilizzo array_aggti consente di mantenere il tuo GROUP BY, nel caso lo desideri per altri motivi.

14

La soluzione non è molto efficiente come indicato da Erwin, a causa della presenza di SubQ

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

Grazie, sì, d'accordo con te, l'unione tra subq e la query esterna richiede in realtà più tempo. "In" non sarà un problema qui poiché il subq risulterà solo una riga. A proposito, quale errore di sintassi stai indicando ??
user2407394,

ohh .. abituato a "Teradata" .. modificato ora .. tuttavia non è necessario rompere i legami qui in quanto è necessario trovare il totale più alto per ogni cliente ..
user2407394

Sei consapevole di ottenere più righe per un singolo cliente in caso di pareggio? Se ciò è desiderabile dipende dai requisiti esatti. Normalmente no. Per la domanda in corso, il titolo è abbastanza chiaro.
Erwin Brandstetter,

Questo non è chiaro dalla domanda, se lo stesso cliente ha acquisto = Max per 2 ID diversi, penso che dovremmo visualizzare entrambi.
user2407394

10

Uso in questo modo (solo 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
);

Quindi il tuo esempio dovrebbe funzionare quasi com'è:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY FIRST(total) DESC;

CAVEAT: ignora le righe NULL


Modifica 1: utilizzare invece l'estensione postgres

Ora uso in questo modo: http://pgxn.org/dist/first_last_agg/

Per installare su 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'

È un'estensione postgres che ti dà la prima e l'ultima funzione; apparentemente più veloce del modo sopra.


Modifica 2 - Ordinamento e filtro

Se si utilizzano funzioni aggregate (come queste), è possibile ordinare i risultati, senza la necessità di disporre dei dati già ordinati:

http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES

Quindi l'esempio equivalente, con l'ordinamento sarebbe qualcosa di simile:

SELECT first(id order by id), customer, first(total order by id)
  FROM purchases
 GROUP BY customer
 ORDER BY first(total);

Ovviamente puoi ordinare e filtrare come ritieni si adatti all'aggregato; è una sintassi molto potente.


Utilizzando anche questo approccio con funzioni personalizzate. Sufficientemente universale e semplice. Perché complicare le cose, questa soluzione è significativamente meno performante di altre?
Sergey Shcherbakov,

9

La query:

SELECT purchases.*
FROM purchases
LEFT JOIN purchases as p 
ON 
  p.customer = purchases.customer 
  AND 
  purchases.total < p.total
WHERE p.total IS NULL

COME FUNZIONA! (Ci sono stato)

Vogliamo assicurarci di avere solo il totale più alto per ogni acquisto.


Alcune cose teoriche (salta questa parte se vuoi solo capire la query)

Sia Total una funzione T (cliente, id) in cui restituisce un valore dato il nome e l'id Per dimostrare che il totale dato (T (cliente, id)) è il massimo che dobbiamo dimostrare che vogliamo provare

  • Tx T (cliente, ID)> T (cliente, x) (questo totale è superiore a tutti gli altri totali per quel cliente)

O

  • ¬∃x T (cliente, ID) <T (cliente, x) (non esiste un totale più elevato per quel cliente)

Il primo approccio avrà bisogno di noi per ottenere tutti i record per quel nome che non mi piace molto.

Il secondo avrà bisogno di un modo intelligente per dire che non può esserci record superiore a questo.


Torna a SQL

Se abbiamo lasciato unisce la tabella sul nome e il totale è inferiore alla tabella unita:

      LEFT JOIN purchases as p 
      ON 
      p.customer = purchases.customer 
      AND 
      purchases.total < p.total

ci assicuriamo che tutti i record che hanno un altro record con il totale più elevato per lo stesso utente vengano uniti:

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

Questo ci aiuterà a filtrare per il totale più alto per ogni acquisto senza raggruppamento necessario:

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

E questa è la risposta di cui abbiamo bisogno.


8

Soluzione molto veloce

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

e davvero molto veloce se la tabella è indicizzata da id:

create index purchases_id on purchases (id);

La clausola USING è molto standard. È solo che alcuni sistemi di database minori non ce l'hanno.
Holger Jakobs,

2
Questo non trova l'acquisto dei clienti con il totale più grande
Johnny Wong,

7

In SQL Server puoi farlo:

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

Spiegazione: Qui Group by viene eseguito sulla base del cliente e quindi lo ordina in totale, quindi a ciascun gruppo viene assegnato il numero di serie come StRank e stiamo eliminando il primo 1 cliente il cui StRank è 1


Grazie! Funzionava perfettamente ed era molto facile da capire e implementare.
ruohola,


4

In PostgreSQL, un'altra possibilità è utilizzare la first_valuefunzione finestra in combinazione con SELECT DISTINCT:

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

Ho creato un composito (id, total), quindi entrambi i valori vengono restituiti dallo stesso aggregato. Ovviamente puoi sempre fare domanda first_value()due volte.


3

La soluzione "Supportato da qualsiasi database" di OMG Ponies ha una buona velocità dal mio test.

Qui fornisco una soluzione per qualsiasi database con lo stesso approccio, ma più completa e pulita. Vengono presi in considerazione i legami (si presuppone che si desideri ottenere solo una riga per ciascun cliente, anche più record per il totale massimo per cliente) e altri campi di acquisto (ad es. Acquisto_payment_id) verranno selezionati per le righe corrispondenti reali nella tabella degli acquisti.

Supportato da qualsiasi database:

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

Questa query è ragionevolmente veloce soprattutto quando c'è un indice composito come (cliente, totale) nella tabella degli acquisti.

Commento:

  1. t1, t2 sono alias di subquery che potrebbero essere rimossi a seconda del database.

  2. Avvertenza : la using (...)clausola non è attualmente supportata in MS-SQL e Oracle db a partire da questa modifica a gennaio 2017. È necessario espanderla ad es on t2.id = purchase.id. Ecc. La sintassi USING funziona in SQLite, MySQL e PostgreSQL.


2

Snowflake / Teradata supporta la QUALIFYclausola che funziona come HAVINGper le funzioni con finestre:

SELECT id, customer, total
FROM PURCHASES
QUALIFY ROW_NUMBER() OVER(PARTITION BY p.customer ORDER BY p.total DESC) = 1

1
  • Se si desidera selezionare una riga (in base a una condizione specifica) dall'insieme di righe aggregate.

  • Se si desidera utilizzare un'altra sum/avgfunzione di aggregazione ( ) in aggiunta a max/min. Quindi non puoi usare indizio conDISTINCT ON

È possibile utilizzare la query secondaria successiva:

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

È possibile sostituire amount = MAX( tf.amount ) con qualsiasi condizione desiderata con una restrizione: questa sottoquery non deve restituire più di una riga

Ma se vuoi fare queste cose probabilmente stai cercando le funzioni della finestra


1

Per SQl Server il modo più efficiente è:

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

e non dimenticare di creare un indice cluster per le colonne utilizzate

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.