Comment rejoindre les dernières lignes d'une table?
Question
Je rencontre fréquemment des problèmes de ce type et je n'ai pas encore trouvé de bonne solution:
Supposons que nous ayons deux tables de base de données représentant un système de commerce électronique.
userData (userId, name, ...)
orderData (orderId, userId, orderType, createDate, ...)
Pour tous les utilisateurs du système, sélectionnez leurs informations d'utilisateur, leurs informations de commande les plus récentes avec type = '1' et leurs informations de commande les plus récentes avec type = '2'. Je veux le faire en une seule requête. Voici un exemple de résultat:
(userId, name, ..., orderId1, orderType1, createDate1, ..., orderId2, orderType2, createDate2, ...)
(101, 'Bob', ..., 472, '1', '4/25/2008', ..., 382, '2', '3/2/2008', ...)
La solution
Cela devrait fonctionner, vous devrez ajuster les noms de table / colonne:
select ud.name,
order1.order_id,
order1.order_type,
order1.create_date,
order2.order_id,
order2.order_type,
order2.create_date
from user_data ud,
order_data order1,
order_data order2
where ud.user_id = order1.user_id
and ud.user_id = order2.user_id
and order1.order_id = (select max(order_id)
from order_data od1
where od1.user_id = ud.user_id
and od1.order_type = 'Type1')
and order2.order_id = (select max(order_id)
from order_data od2
where od2.user_id = ud.user_id
and od2.order_type = 'Type2')
Dénormaliser vos données peut également être une bonne idée. Ce genre de chose sera assez coûteux à faire. Vous pouvez donc ajouter une last_order_date
à votre userData.
Autres conseils
J'ai proposé trois approches différentes pour résoudre ce problème:
- Utilisation des pivots
- Utilisation des instructions de cas
- Utilisation de requêtes en ligne dans la clause where
Toutes les solutions supposent que nous déterminons le "plus récent". order en fonction de la colonne orderId
. L'utilisation de la colonne createDate
ajouterait de la complexité en raison de collisions d'horodatage et entraverait sérieusement les performances, car createDate
ne fait probablement pas partie de la clé indexée. J'ai seulement testé ces requêtes avec MS SQL Server 2005, je ne sais donc pas si elles fonctionneront sur votre serveur.
Les solutions (1) et (2) fonctionnent presque à l'identique. En fait, ils entraînent tous deux le même nombre de lectures dans la base de données.
La solution (3) n'est pas l'approche recommandée pour travailler avec de grands ensembles de données. Il fait systématiquement des centaines de lectures logiques supérieures à (1) et (2). Lors du filtrage pour un utilisateur spécifique, l’approche (3) est comparable aux autres méthodes. Dans le cas d’un utilisateur unique, une diminution de la durée de l’unité de traitement permet de contrer le nombre considérablement plus élevé de lectures; Toutefois, à mesure que le lecteur de disque devient plus occupé et que des casses manquantes se produisent, ce léger avantage disparaîtra.
Conclusion
Pour le scénario présenté, utilisez l'approche pivot si elle est prise en charge par votre SGBD. Il nécessite moins de code que l'instruction case et simplifie l'ajout ultérieur de types d'ordre.
Veuillez noter que, dans certains cas, PIVOT n’est pas assez flexible et que les fonctions de valeur caractéristique utilisant des instructions case sont la solution.
Code
Approche (1) à l'aide de PIVOT:
select
ud.userId, ud.fullname,
od1.orderId as orderId1, od1.createDate as createDate1, od1.orderType as orderType1,
od2.orderId as orderId2, od2.createDate as createDate2, od2.orderType as orderType2
from userData ud
inner join (
select userId, [1] as typeOne, [2] as typeTwo
from (select
userId, orderType, orderId
from orderData) as orders
PIVOT
(
max(orderId)
FOR orderType in ([1], [2])
) as LatestOrders) as LatestOrders on
LatestOrders.userId = ud.userId
inner join orderData od1 on
od1.orderId = LatestOrders.typeOne
inner join orderData od2 on
od2.orderId = LatestOrders.typeTwo
Approche (2) à l'aide des instructions de cas:
select
ud.userId, ud.fullname,
od1.orderId as orderId1, od1.createDate as createDate1, od1.orderType as orderType1,
od2.orderId as orderId2, od2.createDate as createDate2, od2.orderType as orderType2
from userData ud
-- assuming not all users will have orders use outer join
inner join (
select
od.userId,
-- can be null if no orders for type
max (case when orderType = 1
then ORDERID
else null
end) as maxTypeOneOrderId,
-- can be null if no orders for type
max (case when orderType = 2
then ORDERID
else null
end) as maxTypeTwoOrderId
from orderData od
group by userId) as maxOrderKeys on
maxOrderKeys.userId = ud.userId
inner join orderData od1 on
od1.ORDERID = maxTypeTwoOrderId
inner join orderData od2 on
OD2.ORDERID = maxTypeTwoOrderId
Approche (3) utilisant des requêtes en ligne dans la clause where (basée sur la réponse de Steve K.):
select ud.userId,ud.fullname,
order1.orderId, order1.orderType, order1.createDate,
order2.orderId, order2.orderType, order2.createDate
from userData ud,
orderData order1,
orderData order2
where ud.userId = order1.userId
and ud.userId = order2.userId
and order1.orderId = (select max(orderId)
from orderData od1
where od1.userId = ud.userId
and od1.orderType = 1)
and order2.orderId = (select max(orderId)
from orderData od2
where od2.userId = ud.userId
and od2.orderType = 2)
Script permettant de générer des tables et 1000 utilisateurs avec 100 commandes chacun:
CREATE TABLE [dbo].[orderData](
[orderId] [int] IDENTITY(1,1) NOT NULL,
[createDate] [datetime] NOT NULL,
[orderType] [tinyint] NOT NULL,
[userId] [int] NOT NULL
)
CREATE TABLE [dbo].[userData](
[userId] [int] IDENTITY(1,1) NOT NULL,
[fullname] [nvarchar](50) NOT NULL
)
-- Create 1000 users with 100 order each
declare @userId int
declare @usersAdded int
set @usersAdded = 0
while @usersAdded < 1000
begin
insert into userData (fullname) values ('Mario' + ltrim(str(@usersAdded)))
set @userId = @@identity
declare @orderSetsAdded int
set @orderSetsAdded = 0
while @orderSetsAdded < 10
begin
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-06-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-02-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-08-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-09-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-01-08', 1)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-06-06', 2)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-02-02', 2)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-08-09', 2)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-09-01', 2)
insert into orderData (userId, createDate, orderType)
values ( @userId, '01-01-04', 2)
set @orderSetsAdded = @orderSetsAdded + 1
end
set @usersAdded = @usersAdded + 1
end
Petit extrait de code pour tester les performances des requêtes sur MS SQL Server en plus de SQL Profiler:
-- Uncomment these to clear some caches
--DBCC DROPCLEANBUFFERS
--DBCC FREEPROCCACHE
set statistics io on
set statistics time on
-- INSERT TEST QUERY HERE
set statistics time off
set statistics io off
Désolé, je n'ai pas oracle devant moi, mais voici la structure de base de ce que je ferais dans oracle:
SELECT b.user_id, b.orderid, b.orderType, b.createDate, <etc>,
a.name
FROM orderData b, userData a
WHERE a.userid = b.userid
AND (b.userid, b.orderType, b.createDate) IN (
SELECT userid, orderType, max(createDate)
FROM orderData
WHERE orderType IN (1,2)
GROUP BY userid, orderType)
Exemple de solution T-SQL (MS SQL):
SELECT
u.*
, o1.*
, o2.*
FROM
(
SELECT
, userData.*
, (SELECT TOP 1 orderId.url FROM orderData WHERE orderData.userId=userData.userId AND orderType=1 ORDER BY createDate DESC)
AS order1Id
, (SELECT TOP 1 orderId.url FROM orderData WHERE orderData.userId=userData.userId AND orderType=2 ORDER BY createDate DESC)
AS order2Id
FROM userData
) AS u
LEFT JOIN orderData o1 ON (u.order1Id=o1.orderId)
LEFT JOIN orderData o2 ON (u.order2Id=o2.orderId)
Dans SQL 2005, vous pouvez également utiliser la fonction RANK () OVER. (Mais autant que je sache, c'est une fonctionnalité entièrement spécifique à MSSQL)
Vous pourrez peut-être faire une requête d’union pour cela. La syntaxe exacte nécessite du travail, en particulier le groupe par section, mais l'union devrait pouvoir le faire.
Par exemple:
SELECT orderId, orderType, createDate
FROM orderData
WHERE type=1 AND MAX(createDate)
GROUP BY orderId, orderType, createDate
UNION
SELECT orderId, orderType, createDate
FROM orderData
WHERE type=2 AND MAX(createDate)
GROUP BY orderId, orderType, createDate
Leur plus récent, vous voulez dire tout nouveau dans la journée en cours? Vous pouvez toujours vérifier auprès de votre createDate et obtenir toutes les données utilisateur et de commande si la date de création > = du jour en cours.
SELECT * FROM
"orderData", "userData"
WHERE
"userData"."userId" ="orderData"."userId"
AND "orderData".createDate >= current_date;
MISE À JOUR
Voici ce que vous voulez après votre commentaire ici:
SELECT * FROM
"orderData", "userData"
WHERE
"userData"."userId" ="orderData"."userId"
AND "orderData".type = '1'
AND "orderData"."orderId" = (
SELECT "orderId" FROM "orderData"
WHERE
"orderType" = '1'
ORDER "orderId" DESC
LIMIT 1
)
J'utilise des choses comme celle-ci dans MySQL:
SELECT
u.*,
SUBSTRING_INDEX( MAX( CONCAT( o1.createDate, '##', o1.otherfield)), '##', -1) as o2_orderfield,
SUBSTRING_INDEX( MAX( CONCAT( o2.createDate, '##', o2.otherfield)), '##', -1) as o2_orderfield
FROM
userData as u
LEFT JOIN orderData AS o1 ON (o1.userId=u.userId AND o1.orderType=1)
LEFT JOIN orderData AS o2 ON (o1.userId=u.userId AND o2.orderType=2)
GROUP BY u.userId
En bref, utilisez MAX () pour obtenir le plus récent, en ajoutant le champ de critère (createDate) au (x) champ (s) intéressant (s) (otherfield). SUBSTRING_INDEX () supprime ensuite la date.
OTOH, si vous avez besoin d’un nombre arbitraire de commandes (si userType peut être n’importe quel nombre et non un ENUM limité); il est préférable de traiter avec une requête distincte, quelque chose comme ceci:
select * from orderData where userId=XXX order by orderType, date desc group by orderType
pour chaque utilisateur.
En supposant que orderId est monotone, augmentant avec le temps:
SELECT *
FROM userData u
INNER JOIN orderData o
ON o.userId = u.userId
INNER JOIN ( -- This subquery gives the last order of each type for each customer
SELECT MAX(o2.orderId)
--, o2.userId -- optional - include if joining for a particular customer
--, o2.orderType -- optional - include if joining for a particular type
FROM orderData o2
GROUP BY o2.userId
,o2.orderType
) AS LastOrders
ON LastOrders.orderId = o.orderId -- expand join to include customer or type if desired
Ensuite, faites pivoter le client ou, si vous utilisez SQL Server, une fonctionnalité PIVOT existe
Voici un moyen de déplacer les données de type 1 et 2 sur la même ligne:
(en plaçant les informations de type 1 et de type 2 dans leurs propres sélections qui sont ensuite utilisées dans la clause from.)
SELECT
a.name, ud1.*, ud2.*
FROM
userData a,
(SELECT user_id, orderid, orderType, reateDate, <etc>,
FROM orderData b
WHERE (userid, orderType, createDate) IN (
SELECT userid, orderType, max(createDate)
FROM orderData
WHERE orderType = 1
GROUP BY userid, orderType) ud1,
(SELECT user_id, orderid, orderType, createDate, <etc>,
FROM orderData
WHERE (userid, orderType, createDate) IN (
SELECT userid, orderType, max(createDate)
FROM orderData
WHERE orderType = 2
GROUP BY userid, orderType) ud2
Voici comment je le fais. Ceci est du SQL standard et fonctionne dans n’importe quelle marque de base de données.
SELECT u.userId, u.name, o1.orderId, o1.orderType, o1.createDate,
o2.orderId, o2.orderType, o2.createDate
FROM userData AS u
LEFT OUTER JOIN (
SELECT o1a.orderId, o1a.userId, o1a.orderType, o1a.createDate
FROM orderData AS o1a
LEFT OUTER JOIN orderData AS o1b ON (o1a.userId = o1b.userId
AND o1a.orderType = o1b.orderType AND o1a.createDate < o1b.createDate)
WHERE o1a.orderType = 1 AND o1b.orderId IS NULL) AS o1 ON (u.userId = o1.userId)
LEFT OUTER JOIN (
SELECT o2a.orderId, o2a.userId, o2a.orderType, o2a.createDate
FROM orderData AS o2a
LEFT OUTER JOIN orderData AS o2b ON (o2a.userId = o2b.userId
AND o2a.orderType = o2b.orderType AND o2a.createDate < o2b.createDate)
WHERE o2a.orderType = 2 AND o2b.orderId IS NULL) o2 ON (u.userId = o2.userId);
Notez que si vous avez plusieurs commandes de l'un ou l'autre type dont les dates sont égales à la date la plus récente, vous obtiendrez plusieurs lignes dans le jeu de résultats. Si vous avez plusieurs ordres des deux types, vous obtiendrez N x M lignes dans le jeu de résultats. Je vous recommande donc de récupérer les lignes de chaque type dans des requêtes distinctes.
Steve K a absolument raison, merci! J'ai bien réécrit un peu sa réponse pour expliquer le fait qu'il pourrait ne pas y avoir d'ordre pour un type particulier (ce que j'ai omis de mentionner, je ne peux donc rien reprocher à Steve K).
Voici ce que j'ai fini par utiliser:
select ud.name,
order1.orderId,
order1.orderType,
order1.createDate,
order2.orderId,
order2.orderType,
order2.createDate
from userData ud
left join orderData order1
on order1.orderId = (select max(orderId)
from orderData od1
where od1.userId = ud.userId
and od1.orderType = '1')
left join orderData order2
on order2.orderId = (select max(orderId)
from orderData od2
where od2.userId = ud.userId
and od2.orderType = '2')
where ...[some limiting factors on the selection of users]...;