¿Cómo unir las filas más nuevas de una tabla?
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', ...)
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:
- Uso de pivotes
- Uso de declaraciones de casos
- 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]...;