Domanda

Ho spesso problemi con questo modulo e non ho ancora trovato una buona soluzione:

Supponiamo di avere due tabelle di database che rappresentano un sistema di e-commerce.

userData (userId, name, ...)
orderData (orderId, userId, orderType, createDate, ...)

Per tutti gli utenti nel sistema, selezionare le informazioni sull'utente, le informazioni sull'ordine più recenti con type = '1' e le informazioni sull'ordine più recenti con type = '2'. Voglio farlo in una sola query. Ecco un risultato di esempio:

(userId, name, ..., orderId1, orderType1, createDate1, ..., orderId2, orderType2, createDate2, ...)
(101, 'Bob', ..., 472, '1', '4/25/2008', ..., 382, '2', '3/2/2008', ...)
È stato utile?

Soluzione

Questo dovrebbe funzionare, dovrai adattare i nomi di tabella / colonna:

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')

Anche la denormalizzazione dei dati potrebbe essere una buona idea. Questo tipo di cose sarà abbastanza costoso da fare. Quindi potresti aggiungere un last_order_date al tuo userData.

Altri suggerimenti

Ho fornito tre diversi approcci per risolvere questo problema:

  1. Uso dei pivot
  2. Uso delle dichiarazioni dei casi
  3. Utilizzo di query incorporate nella clausola where

Tutte le soluzioni presuppongono che stiamo determinando il "più recente" ordine basato sulla colonna orderId . L'uso della colonna createDate aggiungerebbe complessità a causa di collisioni di data / ora e ostacolerebbe seriamente le prestazioni poiché createDate probabilmente non fa parte della chiave indicizzata. Ho testato solo queste query utilizzando MS SQL Server 2005, quindi non ho idea se funzioneranno sul tuo server.

Le soluzioni (1) e (2) funzionano in modo quasi identico. In effetti, entrambi generano lo stesso numero di letture dal database.

La soluzione (3) non è non l'approccio preferito quando si lavora con set di dati di grandi dimensioni. Rende costantemente centinaia di letture logiche più di (1) e (2). Quando si filtra per un utente specifico, l'approccio (3) è paragonabile agli altri metodi. Nel caso di un singolo utente, un calo del tempo della CPU aiuta a contrastare il numero significativamente più elevato di letture; tuttavia, quando l'unità disco diventa più occupata e si verificano errori di cache, questo leggero vantaggio scompare.

Conclusione

Per lo scenario presentato, utilizzare l'approccio pivot se è supportato dal proprio DBMS. Richiede meno codice dell'istruzione case e semplifica l'aggiunta di tipi di ordine in futuro.

Si noti, in alcuni casi, PIVOT non è abbastanza flessibile e le funzioni con valori caratteristici che utilizzano le istruzioni case sono la strada da percorrere.

Codice

Approccio (1) utilizzando 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

Approccio (2) utilizzando le dichiarazioni del caso:

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

Approccio (3) utilizzando query incorporate nella clausola where (basata sulla risposta di 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 per generare tabelle e 1000 utenti con 100 ordini ciascuno:

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

Piccolo frammento per testare le prestazioni delle query su MS SQL Server oltre a 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

Scusa se non ho l'oracolo davanti a me, ma questa è la struttura di base di ciò che farei in 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) 

Soluzione di esempio 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)

In SQL 2005 è inoltre possibile utilizzare la funzione RANK () OVER. (Ma AFAIK è completamente specifico per MSSQL)

Potresti essere in grado di fare una query sindacale per questo. La sintassi esatta richiede un po 'di lavoro, in particolare il gruppo per sezione, ma il sindacato dovrebbe essere in grado di farlo.

Ad esempio:

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

Il loro più nuovo intendi tutto nuovo al giorno d'oggi? Puoi sempre verificare con createDate e ottenere tutti i dati utente e ordine se createDate > = giorno corrente.

SELECT * FROM
"orderData", "userData"
WHERE
"userData"."userId"  ="orderData"."userId"
AND "orderData".createDate >= current_date;

AGGIORNAMENTO

Ecco cosa vuoi dopo il tuo commento qui:

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

)

Uso cose del genere in 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

In breve, utilizzare MAX () per ottenere il più recente, anteponendo il campo dei criteri (createDate) ai campi interessanti (altro campo). SUBSTRING_INDEX () quindi rimuove la data.

OTOH, se hai bisogno di un numero arbitrario di ordini (se userType può essere un numero qualsiasi e non un ENUM limitato); è meglio gestire una query separata, qualcosa del genere:

select * from orderData where userId=XXX order by orderType, date desc group by orderType

per ciascun utente.

Supponendo che orderId sia monotonico aumentando nel tempo:

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

Quindi pivot sul client o se si utilizza SQL Server, esiste una funzionalità PIVOT

Ecco un modo per spostare i dati di tipo 1 e 2 sulla stessa riga:
(inserendo le informazioni di tipo 1 e di tipo 2 nelle loro selezioni che poi vengono utilizzate nella clausola 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

Ecco come lo faccio. Questo è SQL standard e funziona in qualsiasi marca di database.

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);

Nota che se hai più ordini di entrambi i tipi le cui date sono uguali all'ultima data, otterrai più righe nel set di risultati. Se hai più ordini di entrambi i tipi, otterrai N x M righe nel set di risultati. Quindi ti consiglio di recuperare le righe di ogni tipo in query separate.

Steve K ha perfettamente ragione, grazie! Ho riscritto un po 'la sua risposta per spiegare il fatto che potrebbe non esserci alcun ordine per un tipo particolare (che non ho menzionato, quindi non posso criticare Steve K.)

Ecco cosa ho finito usando:

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]...;
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top