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', ...)
Foi útil?

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:

  1. Usando pivôs
  2. Usando declarações de caso
  3. 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]...;
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top