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:
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
?
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.
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.
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
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
)
)
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.
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]
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.
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
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.
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;