Como ingressar nas linhas mais recentes de uma mesa?
Pergunta
Frequentemente, encontro problemas dessa forma e ainda não encontrei uma boa solução:
Suponha que tenhamos duas tabelas de banco de dados representando um sistema de comércio eletrônico.
userData (userId, name, ...)
orderData (orderId, userId, orderType, createDate, ...)
Para todos os usuários do sistema, selecione as informações do usuário, as informações mais recentes do pedido com o tipo = '1' e as informações mais recentes do pedido com o tipo = '2'. Eu quero fazer isso em uma consulta. Aqui está um exemplo de resultado:
(userId, name, ..., orderId1, orderType1, createDate1, ..., orderId2, orderType2, createDate2, ...)
(101, 'Bob', ..., 472, '1', '4/25/2008', ..., 382, '2', '3/2/2008', ...)
Solução
Isso deve funcionar, você terá que ajustar os nomes da tabela / coluna:
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 seus dados também pode ser uma boa ideia. Esse tipo de coisa será bastante cara de se fazer. Então você pode adicionar um last_order_date
para o seu userData.
Outras dicas
Eu forneci três abordagens diferentes para resolver este problema:
- Usando pivôs
- Usando declarações de caso
- Usando consultas embutidas na cláusula onde
Todas as soluções assumem que estamos determinando a ordem "mais recente" baseada no orderId
coluna. Usando o createDate
A coluna acrescentaria complexidade devido a colisões de registro de data e hora e impediria seriamente o desempenho desde createDate
Provavelmente não faz parte da chave indexada. Eu só testei essas consultas usando o MS SQL Server 2005, por isso não faço ideia se elas funcionarão no seu servidor.
As soluções (1) e (2) têm desempenho quase de forma idêntica. De fato, ambos resultam no mesmo número de leituras do banco de dados.
Solução (3) é não a abordagem preferida ao trabalhar com grandes conjuntos de dados. Faz consistentemente centenas de leituras lógicas mais que (1) e (2). Ao filtrar para um usuário específico, a abordagem (3) é comparável aos outros métodos. No caso do usuário único, uma queda no tempo da CPU ajuda a combater o número significativamente maior de leituras; No entanto, à medida que a unidade de disco se torna mais movimentada e ocorrem erros de cache, essa pequena vantagem desaparecerá.
Conclusão
Para o cenário apresentado, use a abordagem pivô se for suportada pelo seu DBMS. Requer menos código que a instrução CASE e simplifica a adição de tipos de pedidos no futuro.
Observe que, em alguns casos, o pivô não é flexível o suficiente e as funções de valor características usando as instruções de caso são o caminho a percorrer.
Código
Abordagem (1) Usando o pivô:
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
Abordagem (2) usando declarações de 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
Abordagem (3) usando consultas embutidas na cláusula WHERE (com base na resposta 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 gerar tabelas e 1000 usuários com 100 pedidos cada:
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
Pequeno trecho para teste de desempenho da consulta no MS SQL Server, além do 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
Desculpe, não tenho oráculo na minha frente, mas essa é a estrutura básica do que eu faria no 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)
Solução de amostra 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)
No SQL 2005, você também pode usar a função rank () sobre. (Mas afaik seu recurso completamente específico do MSSQL)
Você pode fazer uma consulta da União para isso. A sintaxe exata precisa de algum trabalho, especialmente o grupo por seção, mas o sindicato deve ser capaz de fazê -lo.
Por exemplo:
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
O mais novo deles você quer dizer tudo novo no dia atual? Você sempre pode verificar com o seu criado e obter todos os dados do usuário e do pedido, se o CreatedAtE> = dia atual.
SELECT * FROM
"orderData", "userData"
WHERE
"userData"."userId" ="orderData"."userId"
AND "orderData".createDate >= current_date;
ATUALIZADA
Aqui está o que você quer depois do seu comentário aqui:
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
)
Eu uso coisas assim em 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
Em resumo, use o max () para obter o mais novo, precendendo o campo de critérios (criado) para o (s) campo (s) interessante (s) (outro campo). Substring_index () tira a data.
OTOH, se você precisar de um número arbitrário de pedidos (se o userType puder ser qualquer número, e não uma enumeração limitada); É melhor lidar com uma consulta separada, algo assim:
select * from orderData where userId=XXX order by orderType, date desc group by orderType
para cada usuário.
Supondo que o OrderId seja monotônico aumentando com o 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
Em seguida, gira no cliente ou, se estiver usando o SQL Server, existe uma funcionalidade pivô
Aqui está uma maneira de mover os dados do tipo 1 e 2 para a mesma linha:
(Ao colocar as informações do Tipo 1 e do Tipo 2 em suas próprias seleções que são usadas na cláusula de 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
Eis como eu faço isso. Este é o SQL padrão e funciona em qualquer marca de banco de dados.
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);
Observe que, se você tiver vários pedidos de qualquer tipo cujas datas são iguais à data mais recente, você obterá várias linhas no conjunto de resultados. Se você tiver vários pedidos de ambos os tipos, obterá n x m linhas no conjunto de resultados. Então, eu recomendaria que você busque as linhas de cada tipo em consultas separadas.
Steve K está absolutamente certo, obrigado! Reescrevi um pouco a resposta dele para explicar o fato de que pode não haver ordem para um tipo específico (que eu não mencionei, então não posso culpar Steve K.)
Aqui está o que eu acabei 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]...;