【问题标题】:How to join the newest rows from a table?如何连接表中的最新行?
【发布时间】:2008-09-30 18:11:36
【问题描述】:

我经常遇到这种形式的问题,还没有找到好的解决方案:

假设我们有两个代表电子商务系统的数据库表。

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

对于系统中的所有用户,选择他们的用户信息、类型 = '1' 的最新订单信息以及类型 = '2' 的最新订单信息。我想在一个查询中做到这一点。这是一个示例结果:

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

【问题讨论】:

  • 是否有固定数量的订单类型?还是它们是动态的/未知的?
  • 它们是固定的(有两个),尽管它们实际上不是数字的。您可以将 '1' 和 '2' 替换为 'a' 和 'b' 并拥有相同的东西。

标签: sql aggregate pivot


【解决方案1】:

这应该可行,您必须调整表/列名称:

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

非规范化数据也可能是个好主意。这种类型的事情做起来会相当昂贵。因此,您可以将 last_order_date 添加到您的 userData。

【讨论】:

    【解决方案2】:

    我提供了三种不同的方法来解决这个问题:

    1. 使用枢轴
    2. 使用案例陈述
    3. 在 where 子句中使用内联查询

    所有解决方案都假设我们根据orderId 列确定“最新”顺序。使用createDate 列会由于时间戳冲突而增加复杂性并严重影响性能,因为createDate 可能不是索引键的一部分。我只使用 MS SQL Server 2005 测试了这些查询,所以我不知道它们是否可以在您的服务器上运行。

    解决方案 (1) 和 (2) 的性能几乎相同。事实上,它们都导致从数据库中读取相同数量的数据。

    解决方案 (3) 不是在处理大型数据集时的首选方法。它始终比 (1) 和 (2) 进行数百次逻辑读取。当过滤一个特定用户时,方法(3)与其他方法相当。在单用户情况下,cpu 时间的减少有助于抵消显着增加的读取次数;但是,随着磁盘驱动器变得更加繁忙和缓存未命中的发生,这种微小的优势将消失。

    结论

    对于呈现的场景,如果您的 DBMS 支持,请使用枢轴方法。与 case 语句相比,它需要更少的代码,并简化了将来添加订单类型的过程。

    请注意,在某些情况下,PIVOT 不够灵活,使用 case 语句的特征值函数是可行的方法。

    代码

    使用 PIVOT 的方法(1):

    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
    

    使用案例语句的方法(2):

    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
    

    方法 (3) 在 where 子句中使用内联查询(基于 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)
    

    生成表格和 1000 个用户的脚本,每个用户有 100 个订单:

    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
    

    用于在 MS SQL Server 上测试查询性能以及 SQL Profiler 的小 sn-p:

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

    【讨论】:

    • 哇,我非常感谢您回答的长度和细节。
    【解决方案3】:

    对不起,我面前没有甲骨文,但这是我在甲骨文中所做的基本结构:

    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) 
    

    【讨论】:

    • 抱歉,我从您的编辑中看到您希望将类型 1 和类型 2 数据放在同一行
    【解决方案4】:

    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)
    

    在 SQL 2005 中,您还可以使用 RANK ( ) OVER 函数。 (但 AFAIK 完全是 MSSQL 特有的功能)

    【讨论】:

      【解决方案5】:

      您也许可以为此进行联合查询。确切的语法需要一些工作,尤其是按部分分组,但联合应该能够做到。

      例如:

      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
      

      【讨论】:

      • 不,这让我得到两行,它们只是两个类别中的最新订单(无论哪个用户订购了它们)。我希望每个用户都有一行。
      • 伙计,你看起来很挑剔,我认为根据 Kevin 和我给你的信息,你应该能够调整我们的查询,以获得你想要的准确信息。
      【解决方案6】:

      他们最新的你是指当天的所有新的?如果 createDate >= 当前日期,您可以随时检查您的 createDate 并获取所有用户和订单数据。

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

      更新

      这是您在此处发表评论后想要的:

      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
      

      )

      【讨论】:

      • 不,我的意思是每个用户可能有 0...n 个订单。在他们的订单集合中查找最新的订单,特别是您可以限制类型的地方。
      【解决方案7】:

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

      简而言之,使用 MAX() 获取最新的,方法是将标准字段 (createDate) 附加到感兴趣的字段 (otherfield)。 SUBSTRING_INDEX() 然后去掉日期。

      OTOH,如果您需要任意数量的订单(如果 userType 可以是任意数量,而不是有限的 ENUM);最好使用单独的查询来处理,如下所示:

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

      针对每个用户。

      【讨论】:

        【解决方案8】:

        假设 orderId 随时间单调递增:

        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
        

        然后在客户端进行透视,或者如果使用 SQL Server,则有 PIVOT 功能

        【讨论】:

          【解决方案9】:

          这是将类型 1 和 2 数据移到同一行的一种方法:
          (通过将类型 1 和类型 2 信息放入它们自己的选择中,然后在 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
          

          【讨论】:

          • 在看到Steve K的方案之前就开始写上面的了,好多了
          【解决方案10】:

          这就是我的做法。这是标准 SQL,适用于任何品牌的数据库。

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

          请注意,如果您有多个日期等于最新日期的任一类型的订单,您将在结果集中获得多行。如果您有两种类型的多个订单,您将在结果集中获得 N x M 行。因此,我建议您在单独的查询中获取每种类型的行。

          【讨论】:

            【解决方案11】:

            Steve K 完全正确,谢谢!我确实稍微改写了他的答案,以说明可能没有特定类型的顺序(我没有提到,所以我不能责怪史蒂夫 K。)

            这是我最终使用的:

            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]...;
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2010-10-04
              • 2018-06-29
              • 1970-01-01
              • 1970-01-01
              • 2021-07-18
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多