Pregunta

Con frecuencia me encuentro con problemas de este formulario y aún no he encontrado una buena solución:

Supongamos que tenemos dos tablas de base de datos que representan un sistema de comercio electrónico.

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

Para todos los usuarios del sistema, seleccione su información de usuario, la información de su pedido más reciente con type = '1', y la información de su pedido más reciente con type = '2'. Quiero hacer esto en una consulta. Aquí hay un ejemplo de resultado:

(userId, name, ..., orderId1, orderType1, createDate1, ..., orderId2, orderType2, createDate2, ...)
(101, 'Bob', ..., 472, '1', '4/25/2008', ..., 382, '2', '3/2/2008', ...)
¿Fue útil?

Solución

Esto debería funcionar, tendrás que ajustar los nombres de tabla / columna:

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

Desnormalizar sus datos también puede ser una buena idea. Este tipo de cosas serán bastante caras de hacer. Por lo tanto, puede agregar un last_order_date a su userData.

Otros consejos

He proporcionado tres enfoques diferentes para resolver este problema:

  1. Uso de pivotes
  2. Uso de declaraciones de casos
  3. Usar consultas en línea en la cláusula where

Todas las soluciones suponen que estamos determinando las " más recientes " orden basada en la columna orderId . El uso de la columna createDate agregará complejidad debido a las colisiones de marca de tiempo y dificultará seriamente el rendimiento, ya que createDate probablemente no sea parte de la clave indexada. Solo he probado estas consultas con MS SQL Server 2005, por lo que no tengo idea de si funcionarán en su servidor.

Las soluciones (1) y (2) funcionan de manera casi idéntica. De hecho, ambos resultan en el mismo número de lecturas de la base de datos.

La solución (3) es no el enfoque preferido cuando se trabaja con grandes conjuntos de datos. Consecuentemente hace cientos de lecturas lógicas más que (1) y (2). Cuando se filtra por un usuario específico, el enfoque (3) es comparable a los otros métodos. En el caso de un solo usuario, una caída en el tiempo de la CPU ayuda a contrarrestar el número significativamente mayor de lecturas; sin embargo, a medida que la unidad de disco se vuelve más ocupada y se producen fallas en la memoria caché, esta ligera ventaja desaparecerá.

Conclusión

Para el escenario presentado, use el enfoque de pivote si su DBMS lo admite. Requiere menos código que la declaración del caso y simplifica agregar tipos de orden en el futuro.

Tenga en cuenta que, en algunos casos, PIVOT no es lo suficientemente flexible y las funciones de valor característico que usan declaraciones de casos son el camino a seguir.

Código

Enfoque (1) utilizando 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

Enfoque (2) utilizando declaraciones de casos:

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

Enfoque (3) mediante consultas en línea en la cláusula where (según la respuesta 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 para generar tablas y 1000 usuarios con 100 pedidos cada uno:

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

Fragmento pequeño para probar el rendimiento de las consultas en MS SQL Server además del Analizador de SQL:

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

Lo siento, no tengo oracle delante de mí, pero esta es la estructura básica de lo que haría en 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) 

Solución de muestra de 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)

En SQL 2005 también puedes usar la función RANK () OVER. (Pero AFAIK es una característica completamente específica de MSSQL)

Es posible que puedas hacer una consulta de unión para esto. La sintaxis exacta necesita algo de trabajo, especialmente el grupo por sección, pero el sindicato debería poder hacerlo.

Por ejemplo:

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

¿Su más reciente quiere decir todo nuevo en el día actual? Siempre puede consultar con su createDate y obtener todos los datos de usuarios y pedidos si createDate > = día actual.

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

ACTUALIZADO

Aquí está lo que quieres después de tu comentario aquí:

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 cosas como esta en 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 resumen, use MAX () para obtener el más nuevo, al anteponer el campo de criterios (createDate) a los campos interesantes (otherfield). SUBSTRING_INDEX () luego elimina la fecha.

OTOH, si necesita un número arbitrario de pedidos (si userType puede ser cualquier número, y no un ENUM limitado); es mejor manejar con una consulta separada, algo como esto:

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

para cada usuario.

Suponiendo que orderId es monotónico aumenta con el tiempo:

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

Luego gírelo en el cliente o si usa SQL Server, existe una funcionalidad PIVOT

Aquí hay una forma de mover los datos de tipo 1 y 2 a la misma fila:
(al colocar la información de tipo 1 y tipo 2 en sus propias selecciones que luego se usan en la cláusula 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

Así es como lo hago. Esto es SQL estándar y funciona en cualquier marca de base de datos.

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

Tenga en cuenta que si tiene varios pedidos de cualquier tipo cuyas fechas son iguales a la fecha más reciente, obtendrá varias filas en el conjunto de resultados. Si tiene varios pedidos de ambos tipos, obtendrá N x M filas en el conjunto de resultados. Por lo tanto, le recomendaría que busque las filas de cada tipo en consultas separadas.

Steve K tiene toda la razón, gracias! Reescribí un poco su respuesta para explicar el hecho de que podría no haber un orden para un tipo en particular (que no mencioné, por lo que no puedo culpar a Steve K.)

Esto es lo que terminé 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]...;
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top