2010-01-21 10 views
186

Supponiamo di avere 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'unica istruzione SELECT. Qual è la migliore pratica? Qualche consiglio sulla costruzione di indici?SQL join: selezionare l'ultimo record in una relazione uno-a-molti

Si prega di utilizzare questi nomi tabella/colonna nella sua risposta:

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

E nelle situazioni più complicate, Sarebbe (per quanto riguarda le prestazioni) utile per denormalizzare il database inserendo l'ultimo acquisto nella tabella dei clienti?

Se l'ID (acquisto) è garantito per essere ordinato per data, le dichiarazioni possono essere semplificate utilizzando qualcosa come LIMIT 1?

+0

Sì, potrebbe valere la pena denormalizzare (se migliora molto le prestazioni, che è possibile scoprire solo testando entrambe le versioni). Ma gli svantaggi della denormalizzazione di solito valgono la pena di essere evitati. –

+2

Correlato: http://jan.kneschke.de/projects/mysql/groupwise-max/ – igorw

risposta

293

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

Ecco come Io di solito raccomando 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 fila p1, ci dovrebbe essere nessuna riga p2 con lo stesso cliente e una data successiva (o in caso di parità, un seguito id). Quando lo riteniamo vero, lo p1 è l'acquisto più recente per quel cliente.

indici Per quanto riguarda, mi piacerebbe creare un indice composto in purchase sopra le colonne (customer_id, date, id). Ciò potrebbe consentire di eseguire l'unione esterna utilizzando un indice di copertura. Assicurati di testare sulla tua piattaforma, perché l'ottimizzazione dipende dall'implementazione. Utilizzare le funzionalità del proprio RDBMS per analizzare il piano di ottimizzazione. Per esempio. EXPLAIN su MySQL.


Alcune persone usano sottoquery invece della soluzione mostro sopra, ma ho trovato la mia soluzione rende più facile da risolvere legami.

+5

Come si confronta con i sottoselezionamenti per le prestazioni? – netvope

+2

Favorevolmente, in generale. Ma ciò dipende dalla marca del database che si utilizza e dalla quantità e dalla distribuzione dei dati nel database. L'unico modo per ottenere una risposta precisa è testare entrambe le soluzioni con i tuoi dati. –

+15

Se si desidera includere i clienti che non hanno mai effettuato un acquisto, modificare JOIN acquistare p1 ON (c.id = p1.customer_id) su LEFT JOIN acquistare p1 ON (c.id = p1.customer_id) – GordonM

86

Si potrebbe anche provare a fare questo usando un sub selezionare

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 select dovrebbero unirsi a tutti i clienti e la loro Ultima data di acquisto.

+4

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

+1

Wooow .Sei una vita più sicura Grazie Daveo ha ragione. Mi piace anche questo approccio il migliore.Vorrei poterti dare +10;) – driechel

+0

Come dovrei modificare questo se volessi ottenere un cliente anche se non ci fossero acquisti? – clu

21

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

sintassi SQL Server potrebbe essere:

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

Questa è la risposta sbagliata alla domanda perché stai utilizzando "RANK()" invece di "ROW_NUMBER()". Il RANK ti darà comunque lo stesso problema dei legami quando due acquisti hanno la stessa data esatta. Questo è ciò che fa la funzione Ranking; se il primo 2 corrisponde, a entrambi viene assegnato il valore 1 e il terzo record ottiene il valore 3. Con Row_Number, non c'è nessun legame, è univoco per l'intera partizione. – MikeTeeVee

+3

Provando l'approccio di Bill Karwin contro l'approccio di Madalina qui, con piani di esecuzione abilitati in SQL Server 2008, ho trovato che l'approcio di Bill Karwin aveva un costo di interrogazione del 43% rispetto all'approccio di Madalina che usava il 57%, quindi nonostante la sintassi più elegante di questa risposta, Preferirei ancora la versione di Bill! – Shawson

13

Un altro approccio sarebbe quello di utilizzare una condizione NOT EXISTS nella vostra condizione di join per testare per gli 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 
    ) 
) 
+0

Puoi spiegare la parte 'AND NOT EXISTS' in parole semplici? –

+0

Il sub select controlla se c'è una riga con un id più alto. Riceverai solo una riga nel tuo set di risultati, se non ne trovi una con id più alto. Quello dovrebbe essere il più alto unico. –

5

ho trovato questa discussione come soluzione al mio problema.

Ma quando li ho provati la performance era bassa. Bellow è 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 sia utile.

+0

per ottenere solo 1 ho usato 'top 1' e' ordered it by' MaxDate 'desc' –

1

Si prega di provare 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; 
0

Prova questo, vi aiuterà.

Ho usato questo nel mio progetto.

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

provata 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 funzione di aggregazione max() farà in modo che l'ultimo acquisto viene scelto da ciascun gruppo (ma presuppone che la colonna della data è in un formato in cui max() dà 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, vorrei utilizzare un indice sull'acquisto con (customer_id, data, [eventuali altre colonne acquisti che si desidera restituire nella selezione]].

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

Problemi correlati