【问题标题】:Get first and last Order and the highest value Item in each order for each Customer, all of which are separate tables获取每个客户的第一个和最后一个订单以及每个订单中价值最高的项目,所有这些都是单独的表
【发布时间】:2021-11-12 10:35:51
【问题描述】:

我需要按 OrderDate 查找每个客户的第一个和最后一个订单,以及每个订单中业务量最高的商品的名称和 SKU。作为参考,Customer 表有 >150k 记录,Orders 和 OrderDetails(这些是 Items)还有很多。

注意:订单及其各自的商品应与客户位于同一行

订单

OrderID   OrderDate     CustomerID   BusinessVolumeTotal   Subtotal
13212     '2021-09-06'  512312       500.00                25.60

订单详情

OrderID   ItemCode   ItemDescription   BusinessVolume
13212     'SKW-BS'   'Some item'       450.00

在我的第一个查询中,我试图坚持加入而不是子查询,这导致了这个

select distinct(c.CustomerID), c.FirstName + ' ' + c.LastName as Name,
    cs.CustomerStatusDescription as Status,
    ct.CustomerTypeDescription as Type, pv.Volume80 as G3,
    fo.OrderID,fo.OrderDate,fo.SubTotal,fo.Country, fod.ItemCode, fod.ItemDescription, fopt.PriceTypeID,
    lo.OrderID,lo.OrderDate,lo.SubTotal,lo.Country, lod.ItemCode, lod.ItemDescription, lopt.PriceTypeID
from Customers c
    left join CustomerTypes ct on ct.CustomerTypeID = c.CustomerTypeID
    left join CustomerStatuses cs on cs.CustomerStatusID = c.CustomerStatusID
    left join PeriodVolumes pv on pv.CustomerID = c.CustomerID
    left join Orders fo on fo.CustomerID = c.CustomerID -- First Order
    left join Orders lo on lo.CustomerID = c.CustomerID -- Last Order
    left join OrderDetails fod on fod.OrderID = fo.OrderID
    left join OrderDetails lod on lod.OrderID = lo.OrderID
    left join PriceTypes fopt on fo.PriceTypeID = fopt.PriceTypeID
    left join PriceTypes lopt on lo.PriceTypeID = lopt.PriceTypeID
where c.CustomerStatusID in (1,2)
  and c.CustomerTypeID in (2,3)
  and pv.PeriodTypeID = 2
  /* First Order */
  and fo.OrderID = (select top 1(OrderID) from Orders where CustomerID = c.CustomerID and OrderStatusID>=7 order by OrderDate )
  and fod.ItemID = (select top 1(ItemID) from OrderDetails where OrderID = fo.OrderID order by BusinessVolume)
  /* Last Order */
  and lo.OrderID = (select top 1(OrderID) from Orders where CustomerID = c.CustomerID and OrderStatusID>=7 order by OrderDate desc)
  and lod.ItemID = (select top 1(ItemID) from OrderDetails where OrderID = lo.OrderID order by BusinessVolume desc)
  and pv.PeriodID = (select PeriodID from Periods where PeriodTypeID=2 and StartDate <= @now and EndDate >= @now)

但这最终会执行大约 6-7 分钟。从解释计划来看,它的大部分内容似乎都被基于 OrderStatusID >= 7 的订单键查找占用了。

所以我尝试使用窗口函数来实现:

select distinct(c.CustomerID), c.FirstName + ' ' + c.LastName as Name, cs.CustomerStatusDescription as Status,
   ct.CustomerTypeDescription as Type,  pv.Volume80 as G3, 
   fal.*
from Customers c
left join CustomerTypes ct on ct.CustomerTypeID = c.CustomerTypeID
left join CustomerStatuses cs on cs.CustomerStatusID = c.CustomerStatusID
left join PeriodVolumes pv on pv.CustomerID = c.CustomerID
left join(
    select
        CustomerID,
        max(case when MinDate = 1 then OrderID end)                 FirstOrderID,
        max(case when MinDate = 1 then OrderDate end)               FirstOrderDate,
        max(case when MinDate = 1 then BusinessVolumeTotal end)     FirstBVTotal,
        max(case when MinDate = 1 then PriceTypeDescription end)    FirstPriceType,
        max(case when MinDate = 1 then ItemCode end)                FirstItemCode,
        max(case when MinDate = 1 then ItemDescription end)         FirstItemDescription,
        max(case when MaxDate = 1 then OrderID end)                 LastOrderID,
        max(case when MaxDate = 1 then OrderDate end)               LastOrderDate,
        max(case when MaxDate = 1 then BusinessVolumeTotal end)     LastBVTotal,
        max(case when MaxDate = 1 then PriceTypeDescription end)    LastPriceType,
        max(case when MaxDate = 1 then ItemCode end)                LastItemCode,
        max(case when MaxDate = 1 then ItemDescription end)         LastItemDescription
    from
        (
            select distinct o.CustomerID,
            o.OrderID,
            o.OrderDate,
            o.BusinessVolumeTotal,
            PT.PriceTypeDescription,
            RANK() over (partition by o.CustomerID order by OrderDate) as MinDate,
            RANK() over (partition by o.CustomerID order by OrderDate desc) as MaxDate,
            FIRST_VALUE(ItemCode) over (partition by od.OrderID order by BusinessVolume desc) as ItemCode,
            FIRST_VALUE(ItemDescription) over (partition by od.OrderID order by BusinessVolume desc) as ItemDescription
            from Orders o
                     left join OrderDetails od on od.OrderID = o.OrderID
                     left join PriceTypes PT on o.PriceTypeID = PT.PriceTypeID
            where o.OrderStatusID >= 7
        ) fal
    group by CustomerID
) fal on c.CustomerId = fal.CustomerID
where c.CustomerStatusID in (1,2)
and c.CustomerTypeID in (2,3)
and pv.PeriodTypeID = 2
    /* CurrentG3 */
and pv.PeriodID = (select PeriodID from Periods where PeriodTypeID=2 and StartDate <= @now and EndDate >= @now)

唉,这最终执行的时间更长。如果可能的话,我需要一种方法来优化它。

二次查询

我还需要过去 3、6 和 12 个月内每个订单的数量和总和。我目前在原始返回结果后以编程方式作为辅助查询执行此操作,并转发 CustomerID,如下所示:

select count(OrderID) as Cnt, sum(BusinessVolumeTotal) as Bv, CustomerID
from Orders where OrderStatusID > 6 and OrderTypeID in (1,4,8,11)
and OrderDate >= @timeAgo and CustomerID in @ids group by CustomerID

乘以 3,因为 3、6 和 12 个月。理想情况下,我也想制作原版的这一部分,但我真的不知道该怎么做,尤其是加入与订单的复杂程度。

所以理想情况下,我最终会得到这样的结果表

CustomerID  Name       CustomerStatus  CustomerType  FirstOrderID  FirstOrderDate  FirstBVTotal FirstItemCode  FirstItemDesc  FirstPriceType  LastOrderID  LastOrderDate  LastBVTotal  LastItemCode  LastItemDesc  LastPriceType  ThreeMonthCount  ThreeMonthTotal  SixMonthCount SixMonthTotal  TwelveMonthCount  TwelveMonthTotal
512312     'Jane Doe'  'Active'        'Retail'      13212         '2020-06-06'    50.00        'Item1'        'Item 1 desc'  'Retail'        14321        '2021-09-01'   200.00       'Item2'       'Item 2 desc' 'Retail'       45               4305.00          76            8545.60        183               21542.95

任何关于如何优化或减少查询的帮助和建议,以及您认为我做错的任何事情都将不胜感激。

附:我不知道标题是否合适,如果我以后可以更改它,我已经有一段时间没有使用 SO 来提出一个问题了。

更新

查询1的实际执行计划:
https://www.brentozar.com/pastetheplan/?id=SJd56RSmK

查询2的实际执行计划:
https://www.brentozar.com/pastetheplan/?id=BJ7QHk87Y

【问题讨论】:

  • 如果可能,最好提供带有表结构和预期输出的少量样本数据(最少 10 或最多 15)。
  • 请参阅paste the plan,了解在您的问题中包含执行计划的方法。为表(包括索引)提供 DDL 也会很有帮助。
  • 更新了两个查询的实际执行计划

标签: sql sql-server tsql window-functions


【解决方案1】:

我认为对于这种类型的查询,您需要牢记两个要点:

  • 使用窗口函数获得良好性能的关键是不要引入不必要的排序。因此,尽管您可以使用ROW_NUMBER 获得任一方向的第一个订单,但您不应使用另一个相反的ROW_NUMBER 获得最后一个订单。而是使用LEAD 检查下一行是否存在,从而告诉您这是否是最后一行。然后,您可以使用条件聚合。
  • 通常有两种方法来计算第一个/最后一个:如上所述的行编号解决方案,或APPLY,它可以准确地挑选出您需要的那个。
    我认为对于OrderDetails,我们应该使用申请,因为我们需要为每个客户找到两个订单。这确实需要良好的索引,所以如果 OrderDetails 没有很好的索引,那么您可能还需要为此切换到行编号解决方案。
select
    c.CustomerID,
    c.FirstName + ' ' + c.LastName as Name,
    cs.CustomerStatusDescription as Status,
    ct.CustomerTypeDescription as Type,
    pv.Volume80 as G3,
    o.FirstOrderID,
    o.FirstOrderDate,
    o.FirstSubTotal,
    o.FirstCountry,
    fod.ItemCode as FirstItemCode,
    fod.ItemDescription as FirstItemDescription,
    fopt.PriceTypeDescription as FirstPriceTypeDescription,
    o.LastOrderID,
    o.LastOrderDate,
    o.LastSubTotal,
    o.LastCountry,
    lod.ItemCode as LastItemCode,
    lod.ItemDescription as LastItemDescription,
    lopt.PriceTypeDescription as LastPriceTypeDescription 
from Customers c
left join CustomerTypes ct on ct.CustomerTypeID = c.CustomerTypeID
left join CustomerStatuses cs on cs.CustomerStatusID = c.CustomerStatusID
left join PeriodVolumes pv on pv.CustomerID = c.CustomerID
  and pv.PeriodTypeID = 2
  and pv.PeriodID = (
    select top 1 PeriodID
    from Periods p
    where p.PeriodTypeID = 2
      and p.StartDate <= @now
      and p.EndDate >= @now
  )
left join (
    select
      o.CustomerID,
      min(case when rn = 1 then OrderID end) as FirstOrderId,
      min(case when rn = 1 then OrderDate end) as FirstOrderDate,
      min(case when rn = 1 then SubTotal end) as FirstSubTotal,
      min(case when rn = 1 then Country end) as FirstCountry,
      min(case when nx is null then OrderID end) as LastOrderId,
      min(case when nx is null then OrderDate end) as LastOrderDate,
      min(case when nx is null then SubTotal end) as LastSubTotal,
      min(case when nx is null then Country end) as LastCountry,
      count(case when o.OrderDate >= DATEADD(month, -3, GETDATE()) then 1 end) as ThreeMonthCount,
      sum(case when o.OrderDate >= DATEADD(month, -3, GETDATE()) then BusinessVolumeTotal end) as ThreeMonthTotal,
      count(case when o.OrderDate >= DATEADD(month, -6, GETDATE()) then 1 end) as SixMonthCount,
      sum(case when o.OrderDate >= DATEADD(month, -6, GETDATE()) then BusinessVolumeTotal end) as SixMonthTotal,
      count(case when o.OrderDate >= DATEADD(month, -12, GETDATE()) then 1 end) as TwelveMonthCount,
      sum(case when o.OrderDate >= DATEADD(month, -12, GETDATE()) then BusinessVolumeTotal end) as TwelveMonthTotal
    from (
        select *,
            ROW_NUMBER() over (partition by o.CustomerID order by OrderDate) as rn,
            LEAD(OrderID) over (partition by o.CustomerID order by OrderDate) as nx
        from Orders o
        where o.OrderStatusID >= 7
          and o.OrderTypeID in (1,4,8,11)
          and o.OrderDate >= @timeAgo
    ) o
    group by o.CustomerID
) o on o.CustomerID = c.CustomerID
outer apply (
    select top 1
      od.ItemCode,
      od.ItemDescription
    from OrderDetails od
    order by od.BusinessVolume desc
    where od.OrderID = o.FirstOrderId
) fod
outer apply (
    select top 1
      od.ItemCode,
      od.ItemDescription
    from OrderDetails od
    order by od.BusinessVolume desc
    where od.OrderID = o.LastOrderId
) lod
left join PriceTypes fopt on fopt.PriceTypeID = o.FirstPriceTypeID 
left join PriceTypes lopt on lopt.PriceTypeID = o.LastPriceTypeID 
where c.CustomerStatusID in (1,2)
  and c.CustomerTypeID in (2,3);

我还将给你一个行编号版本,从你的执行计划来看,它实际上可能更好。你需要尝试两个

select
    c.CustomerID,
    c.FirstName + ' ' + c.LastName as Name,
    cs.CustomerStatusDescription as Status,
    ct.CustomerTypeDescription as Type,
    pv.Volume80 as G3,
    o.FirstOrderID,
    o.FirstOrderDate,
    o.FirstSubTotal,
    o.FirstCountry,
    o.FirstItemCode,
    o.FirstItemDescription,
    o.FirstPriceTypeDescription,
    o.LastOrderID,
    o.LastOrderDate,
    o.LastSubTotal,
    o.LastCountry,
    o.LastItemCode,
    o.LastItemDescription,
    o.LastPriceTypeDescription 
from Customers c
left join CustomerTypes ct on ct.CustomerTypeID = c.CustomerTypeID
left join CustomerStatuses cs on cs.CustomerStatusID = c.CustomerStatusID
left join PeriodVolumes pv on pv.CustomerID = c.CustomerID
  and pv.PeriodTypeID = 2
  and pv.PeriodID = (
    select top 1 PeriodID
    from Periods p
    where p.PeriodTypeID = 2
      and p.StartDate <= @now
      and p.EndDate >= @now
  )
left join (
    select
      o.CustomerID,
      min(case when rn = 1 then o.OrderID end) as FirstOrderId,
      min(case when rn = 1 then o.OrderDate end) as FirstOrderDate,
      min(case when rn = 1 then o.SubTotal end) as FirstSubTotal,
      min(case when rn = 1 then o.Country end) as FirstCountry,
      min(case when rn = 1 then od.ItemCode end) as FirstItemCode,
      min(case when rn = 1 then od.ItemDescription end) as FirstItemDescription,
      min(case when rn = 1 then opt.PriceTypeDescription end) as FirstPriceTypeDescription,
      min(case when nx is null then o.OrderID end) as LastOrderId,
      min(case when nx is null then o.OrderDate end) as LastOrderDate,
      min(case when nx is null then o.SubTotal end) as LastSubTotal,
      min(case when nx is null then o.Country end) as LastCountry,
      min(case when nx is null then od.ItemCode end) as LastItemCode,
      min(case when nx is null then od.ItemDescription end) as LastItemDescription,
      min(case when nx is null then opt.PriceTypeDescription end) as LastPriceTypeDescription,
      count(case when o.OrderDate >= DATEADD(month, -3, GETDATE()) then 1 end) as ThreeMonthCount,
      sum(case when o.OrderDate >= DATEADD(month, -3, GETDATE()) then BusinessVolumeTotal end) as ThreeMonthTotal,
      count(case when o.OrderDate >= DATEADD(month, -6, GETDATE()) then 1 end) as SixMonthCount,
      sum(case when o.OrderDate >= DATEADD(month, -6, GETDATE()) then BusinessVolumeTotal end) as SixMonthTotal,
      count(case when o.OrderDate >= DATEADD(month, -12, GETDATE()) then 1 end) as TwelveMonthCount,
      sum(case when o.OrderDate >= DATEADD(month, -12, GETDATE()) then BusinessVolumeTotal end) as TwelveMonthTotal
    from (
        select *,
            ROW_NUMBER() over (partition by o.CustomerID order by OrderDate) as rn,
            LEAD(OrderID) over (partition by o.CustomerID order by OrderDate) as nx
        from Orders o
        where o.OrderStatusID >= 7
          and o.OrderTypeID in (1,4,8,11)
          and o.OrderDate >= @timeAgo
    ) o
    left join PriceTypes opt on opt.PriceTypeID = o.PriceTypeID 
    join (
        select *,
          ROW_NUMBER() over (partition by od.OrderID order by od.BusinessVolume desc) as rn
        from OrderDetails od
    ) od on od.OrderID = o.OrderId
    where rn = 1 or nx is null
) o on o.CustomerID = c.CustomerID
where c.CustomerStatusID in (1,2)
  and c.CustomerTypeID in (2,3);

良好的索引对于良好的性能必不可少。我希望您的表上大致有以下索引,无论是集群的还是非集群的(集群索引 INCLUDE 每隔一列自动),如果需要,您显然可以添加其他 INCLUDE 列:

Customers (CustomerID) INCLUDE (FirstName, LastName)

CustomerTypes (CustomerTypeID) INCLUDE (CustomerTypeDescription)

CustomerStatuses (CustomerStatusID) INCLUDE (CustomerTypeDescription)

PeriodVolumes (CustomerID) INCLUDE (Volume80)

Periods (PeriodTypeID, StartDate, PeriodID) INCLUDE (EndDate)  -- can swap Start and End

Orders (CustomerID, OrderDate) INCLUDE (OrderStatusID, SubTotal, Country, BusinessVolumeTotal)

OrderDetails (OrderID, BusinessVolume) INCLUDE (ItemCode ItemDescription)

PriceTypes (PriceTypeID) INCLUDE (PriceTypeDescription)

您应该仔细考虑 INNERLEFT 连接,因为优化器可以更轻松地移动 INNER 连接。

还要注意,DISTINCT 不是函数,它是在整个列集上计算的。通常,可以假设如果DISTINCT 在查询中,则连接没有被正确考虑。

【讨论】:

  • 您在过滤的订单表上缺少别名,OrderDate &gt;= @timeAgo 仅适用于总和和计数,因此我将其删除,因为日期过滤器包含在案例中。这大大减少了我的执行时间,非常感谢。我完全忘记了APPLY 是一回事。使用LEAD 而不是创建另一个窗口的提示非常有见地。这是我所做的修改以及实际执行计划的粘贴计划brentozar.com/pastetheplan/?id=rkHxv18mKTLDR;执行时间从 ~6:30 下降到 0:52!
  • 如果您同意此执行计划,我可能会将其标记为已接受的答案。等待您的回复
  • 我想这已经足够了。查看实际输出的OrderDetails 的百分比,我认为也值得尝试一个行号,所以已经编辑了它。另外,我认为你应该在Period 子查询中使用top 1,因为它似乎没有给出独特的结果。尽可能考虑inner 加入
  • ROW_NUMBER 版本似乎需要大约 30 秒的额外时间才能执行。这是它的计划:link 所以我会坚持使用OUTER APPLY。您的rn 列不明确,Orders 子查询缺少GROUP BY o.CustomerID。请注意,我在最里面的查询中的WHERE 条件也与您的不同。而且我认为我将GROUP BY 声明放在了正确的位置。您可以查看是否查看 粘贴我的计划 链接。无论如何,非常感谢,这解决了我所有的问题,标记为已接受并 +1。
  • 没问题。看起来我建议的所有索引都没有到位
猜你喜欢
  • 2011-05-02
  • 2014-12-11
  • 1970-01-01
  • 1970-01-01
  • 2022-12-06
  • 1970-01-01
  • 1970-01-01
  • 2010-12-08
  • 1970-01-01
相关资源
最近更新 更多