web-dev-qa-db-fra.com

Jointure SQL: sélection des derniers enregistrements dans une relation un à plusieurs

Supposons que j'ai une table de clients et une table d'achats. Chaque achat appartient à un client. Je souhaite obtenir une liste de tous les clients avec leur dernier achat dans une seule instruction SELECT. Quelle est la meilleure pratique? Des conseils sur la construction d'index?

Veuillez utiliser ces noms de table/colonne dans votre réponse:

  • client: identifiant, nom
  • purchase: id, customer_id, item_id, date

Et dans des situations plus complexes, serait-il avantageux (en termes de performances) de dénormaliser la base de données en plaçant le dernier achat dans la table des clients?

S'il est garanti que l'identifiant (d'achat) sera trié par date, les instructions peuvent-elles être simplifiées en utilisant quelque chose comme LIMIT 1?

252
netvope

Voici un exemple du problème greatest-n-per-group apparu régulièrement sur StackOverflow.

Voici comment je recommande habituellement de le résoudre:

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;

Explication: étant donné une ligne p1, il ne doit y avoir aucune ligne p2 avec le même client et une date ultérieure (ou, dans le cas de liens, une version ultérieure id). Lorsque nous trouvons que cela est vrai, alors p1 est l'achat le plus récent pour ce client.

En ce qui concerne les index, je créerais un index composé dans purchase sur les colonnes (customer_id, date, id). Cela peut permettre à la jointure externe d'être effectuée à l'aide d'un index couvrant. Assurez-vous de tester sur votre plate-forme, car l'optimisation dépend de l'implémentation. Utilisez les fonctionnalités de votre SGBDR pour analyser le plan d'optimisation. Par exemple. EXPLAIN sur MySQL.


Certaines personnes utilisent des sous-requêtes au lieu de la solution présentée ci-dessus, mais je trouve que ma solution facilite la résolution des liens.

397
Bill Karwin

Vous pouvez également essayer de le faire en utilisant une sous-sélection

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 sélection doit rejoindre tous les clients et leur dernière date d'achat.

108
Adriaan Stander

Vous n'avez pas spécifié la base de données. Si cette option autorise des fonctions analytiques, il peut être plus rapide d’utiliser cette approche que celle GROUP BY (certainement plus rapide dans Oracle, probablement dans les dernières éditions de SQL Server, ne connaissant pas les autres).

La syntaxe dans SQL Server serait la suivante:

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
24
Madalina Dragomir

Une autre approche consiste à utiliser une condition NOT EXISTS dans votre condition de jointure pour tester des achats ultérieurs:

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
   )
)
22
Stefan Haberl

J'ai trouvé ce fil comme solution à mon problème.

Mais lorsque je les ai essayés, la performance était faible. Voici ma suggestion pour une meilleure performance.

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 

J'espère que cela vous sera utile.

12
Mathee

Essayez ceci, cela vous aidera.

Je l'ai utilisé dans mon projet.

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]
6
Rahul Murari

Testé sur 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 fonction d'agrégation max() s'assurera que le dernier achat est sélectionné dans chaque groupe (mais suppose que la colonne de date est dans un format tel que max () donne le dernier (ce qui est normalement le cas). Si vous souhaitez gérer des achats avec la même date, vous pouvez utiliser max(p.date, p.id).

En termes d'index, j'utiliserais un index lors de l'achat avec (customer_id, date, [toute autre colonne d'achat que vous souhaitez renvoyer dans votre sélection]).

Le LEFT OUTER JOIN (par opposition à INNER JOIN) s'assurera que les clients qui n'ont jamais effectué d'achat sont également inclus.

3
Mark

Si vous utilisez PostgreSQL, vous pouvez utiliser DISTINCT ON pour trouver la première ligne d'un groupe.

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

Docs PostgreSQL - Distinct On

Notez que le ou les champs DISTINCT ON - ici customer_id - doivent correspondre au (x) champ (s) le plus à gauche de la clause ORDER BY.

Mise en garde: Il s'agit d'une clause non standard.

2
Tate Thurston

S'il vous plaît essayez ceci,

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;
1
Milad Shahbazi