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', ...)
Était-ce utile?

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:

  1. Utilisation des pivots
  2. Utilisation des instructions de cas
  3. 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]...;
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top