Join SQL: selezione degli ultimi record in una relazione uno-a-molti


298

Supponiamo che io abbia una tabella di clienti e una tabella di acquisti. Ogni acquisto appartiene a un cliente. Voglio ottenere un elenco di tutti i clienti insieme al loro ultimo acquisto in un'istruzione SELECT. Qual è la migliore pratica? Qualche consiglio sulla costruzione di indici?

Usa questi nomi di tabella / colonna nella tua risposta:

  • cliente: id, nome
  • acquisto: id, customer_id, item_id, data

E in situazioni più complicate, sarebbe (dal punto di vista delle prestazioni) vantaggioso denormalizzare il database inserendo l'ultimo acquisto nella tabella dei clienti?

Se si garantisce che l'ID (acquisto) sia ordinato per data, le dichiarazioni possono essere semplificate usando qualcosa di simile LIMIT 1?


Sì, potrebbe valere la pena denormalizzare (se migliora molto le prestazioni, che puoi scoprire testando entrambe le versioni). Ma gli aspetti negativi della denormalizzazione sono generalmente da evitare.
Vince Bowdren,

Risposte:


451

Questo è un esempio del greatest-n-per-groupproblema che è apparso regolarmente su StackOverflow.

Ecco come di solito consiglio di risolverlo:

SELECT c.*, p1.*
FROM customer c
JOIN purchase p1 ON (c.id = p1.customer_id)
LEFT OUTER JOIN purchase p2 ON (c.id = p2.customer_id AND 
    (p1.date < p2.date OR (p1.date = p2.date AND p1.id < p2.id)))
WHERE p2.id IS NULL;

Spiegazione: data una riga p1, non ci dovrebbero essere righe p2con lo stesso cliente e una data successiva (o, nel caso di legami, una versione successiva id). Quando scopriamo che ciò è vero, allora p1è l'acquisto più recente per quel cliente.

Per quanto riguarda gli indici, mi piacerebbe creare un indice composto in purchasesopra le colonne ( customer_id, date, id). Ciò può consentire il collegamento esterno mediante un indice di copertura. Assicurati di testare sulla tua piattaforma, poiché l'ottimizzazione dipende dall'implementazione. Utilizza le funzionalità del tuo RDBMS per analizzare il piano di ottimizzazione. Ad esempio EXPLAINsu MySQL.


Alcune persone usano le subquery invece della soluzione mostrata sopra, ma trovo che la mia soluzione semplifichi la risoluzione dei legami.


3
Favorevolmente, in generale. Ma ciò dipende dal marchio del database utilizzato e dalla quantità e dalla distribuzione dei dati nel database. L'unico modo per ottenere una risposta precisa è per te testare entrambe le soluzioni con i tuoi dati.
Bill Karwin,

27
Se desideri includere i clienti che non hanno mai effettuato un acquisto, modifica l'acquisto JOIN p1 ON (c.id = p1.customer_id) in acquisto LEIN JOIN p1 ON (c.id = p1.customer_id)
GordonM

5
@russds, hai bisogno di qualche colonna unica che puoi usare per risolvere il pareggio. Non ha senso avere due righe identiche in un database relazionale.
Bill Karwin,

6
Qual è lo scopo di "WHERE p2.id IS NULL"?
clu,

3
questa soluzione funziona solo se sono presenti più di 1 record acquisti. se esiste un collegamento 1: 1, NON funziona. lì deve essere "DOVE (p2.id È NULL o p1.id = p2.id)
Bruno Jennrich,

126

Puoi anche provare a farlo usando una sottoselezione

SELECT  c.*, p.*
FROM    customer c INNER JOIN
        (
            SELECT  customer_id,
                    MAX(date) MaxDate
            FROM    purchase
            GROUP BY customer_id
        ) MaxDates ON c.id = MaxDates.customer_id INNER JOIN
        purchase p ON   MaxDates.customer_id = p.customer_id
                    AND MaxDates.MaxDate = p.date

La selezione dovrebbe aderire a tutti i clienti e alla data dell'ultimo acquisto.


4
Grazie, questo mi ha appena salvato - questa soluzione sembra più fattibile e mantenibile rispetto agli altri elencati + non è specifica del prodotto
Daveo,

Come lo modificherei se volessi ottenere un cliente anche se non ci fossero acquisti?
clu,

3
@clu: cambia INNER JOINin a LEFT OUTER JOIN.
Sasha Chedygov,

3
Sembra che questo presupponga che ci sia un solo acquisto in quel giorno. Se ce ne fossero due otterresti due righe di output per un cliente, penso?
artfulrobot,

1
@IstiaqueAhmed: l'ultimo INNER JOIN prende quel valore Max (data) e lo lega alla tabella di origine. Senza quell'unione, le uniche informazioni che avresti dalla purchasetabella sono la data e il customer_id, ma la query richiede tutti i campi dalla tabella.
Ridere Vergil il

26

Non hai specificato il database. Se è uno che consente le funzioni analitiche, potrebbe essere più veloce utilizzare questo approccio rispetto a GROUP BY uno (sicuramente più veloce in Oracle, molto probabilmente più veloce nelle ultime edizioni di SQL Server, non conoscerne altri).

La sintassi in SQL Server sarebbe:

SELECT c.*, p.*
FROM customer c INNER JOIN 
     (SELECT RANK() OVER (PARTITION BY customer_id ORDER BY date DESC) r, *
             FROM purchase) p
ON (c.id = p.customer_id)
WHERE p.r = 1

10
Questa è la risposta errata alla domanda perché stai utilizzando "RANK ()" invece di "ROW_NUMBER ()". RANK ti darà lo stesso problema di vincoli quando due acquisti hanno la stessa identica data. Questo è ciò che fa la funzione di classifica; se i primi 2 corrispondono, a entrambi viene assegnato il valore di 1 e il 3 ° record ottiene un valore di 3. Con Row_Number, non c'è alcun legame, è univoco per l'intera partizione.
MikeTeeVee,

4
Provando l'approccio di Bill Karwin contro l'approccio di Madalina qui, con i piani di esecuzione abilitati su sql server 2008 ho scoperto che l'approccio di Bill Karwin aveva un costo di query del 43% rispetto all'approccio di Madalina che usava il 57% - quindi nonostante la sintassi più elegante di questa risposta, io favorirebbe comunque la versione di Bill!
Shawson,

26

Un altro approccio sarebbe quello di utilizzare una NOT EXISTScondizione nella condizione di partecipazione per testare acquisti successivi:

SELECT *
FROM customer c
LEFT JOIN purchase p ON (
       c.id = p.customer_id
   AND NOT EXISTS (
     SELECT 1 FROM purchase p1
     WHERE p1.customer_id = c.id
     AND p1.id > p.id
   )
)

Puoi spiegare la AND NOT EXISTSparte in parole semplici?
Istiaque Ahmed,

La sottoselezione controlla solo se esiste una riga con un ID più alto. Otterrai solo una riga nel set di risultati, se non viene trovato nessuno con ID superiore. Quello dovrebbe essere l'unico più alto.
Stefan Haberl,

2
Questa per me è la soluzione più leggibile . Se questo è importante.
fguillen,

:) Grazie. Cerco sempre la soluzione più leggibile, perché è importante.
Stefan Haberl

19

Ho trovato questa discussione come soluzione al mio problema.

Ma quando li ho provati le prestazioni erano basse. Muggito è il mio suggerimento per prestazioni migliori.

With MaxDates as (
SELECT  customer_id,
                MAX(date) MaxDate
        FROM    purchase
        GROUP BY customer_id
)

SELECT  c.*, M.*
FROM    customer c INNER JOIN
        MaxDates as M ON c.id = M.customer_id 

Spero che questo possa essere utile.


per ottenere solo 1 ho usato top 1e ordered it byMaxDatedesc
Roshna Omer

1
questa è una soluzione semplice e diretta, nel mio caso (molti clienti, pochi acquisti) più veloce del 10% rispetto alla soluzione di @Stefan Haberl e oltre 10 volte migliore della risposta accettata
Juraj Bezručka

Ottimo suggerimento sull'utilizzo di espressioni di tabella comuni (CTE) per risolvere questo problema. Ciò ha notevolmente migliorato le prestazioni delle query in molte situazioni.
AdamsTips

Migliore risposta imo, facile da leggere, la clausola MAX () offre grandi prestazioni rispetto a ORDER BY + LIMIT 1
MR

10

Se stai usando PostgreSQL puoi usare DISTINCT ONper trovare la prima riga in un gruppo.

SELECT customer.*, purchase.*
FROM customer
JOIN (
   SELECT DISTINCT ON (customer_id) *
   FROM purchase
   ORDER BY customer_id, date DESC
) purchase ON purchase.customer_id = customer.id

PostgreSQL Docs - Distinct On

Si noti che i DISTINCT ONcampi - qui customer_id- devono corrispondere ai campi più a sinistra nella ORDER BYclausola.

Avvertenza: questa è una clausola non standard.


8

Prova questo, aiuterà.

L'ho usato nel mio progetto.

SELECT 
*
FROM
customer c
OUTER APPLY(SELECT top 1 * FROM purchase pi 
WHERE pi.customer_id = c.Id order by pi.Id desc) AS [LastPurchasePrice]

Da dove viene l'alias "p"?
TiagoA

questo non funziona bene .... ci sono voluti un'eternità dove altri esempi qui hanno impiegato 2 secondi sul set di dati che ho ....
Joel_J

3

Testato su SQLite:

SELECT c.*, p.*, max(p.date)
FROM customer c
LEFT OUTER JOIN purchase p
ON c.id = p.customer_id
GROUP BY c.id

La max()funzione di aggregazione assicurerà che l'ultimo acquisto sia selezionato da ciascun gruppo (ma presuppone che la colonna della data sia in un formato in cui max () fornisce l'ultimo - che è normalmente il caso). Se si desidera gestire gli acquisti con la stessa data, è possibile utilizzare max(p.date, p.id).

In termini di indici, utilizzerei un indice per l'acquisto con (customer_id, data, [qualsiasi altra colonna di acquisto che desideri restituire nella tua selezione]).

Il LEFT OUTER JOIN(al contrario di INNER JOIN) farà in modo che siano inclusi anche i clienti che non hanno mai effettuato un acquisto.


non verrà eseguito in t-sql poiché la selezione c. * ha colonne non nel gruppo per clausola
Joel_J

1

Per favore prova questo,

SELECT 
c.Id,
c.name,
(SELECT pi.price FROM purchase pi WHERE pi.Id = MAX(p.Id)) AS [LastPurchasePrice]
FROM customer c INNER JOIN purchase p 
ON c.Id = p.customerId 
GROUP BY c.Id,c.name;
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.