TSQL查找连续三个月发生订单的方法

8
请帮我生成以下查询。假设我有客户表和订单表。
客户表
CustID CustName

1      AA     
2      BB
3      CC
4      DD  

订单表

OrderID  OrderDate          CustID
100      01-JAN-2000        1  
101      05-FEB-2000        1     
102      10-MAR-2000        1 
103      01-NOV-2000        2    
104      05-APR-2001        2 
105      07-MAR-2002        2
106      01-JUL-2003        1
107      01-SEP-2004        4
108      01-APR-2005        4
109      01-MAY-2006        3 
110      05-MAY-2007        1  
111      07-JUN-2007        1
112      06-JUL-2007        1 

我希望查找连续三个月都下过订单的客户。(可以使用SQL Server 2005和2008进行查询)。

期望的输出结果为:

CustName      Year   OrderDate   

    AA        2000  01-JAN-2000       
    AA        2000  05-FEB-2000
    AA        2000  10-MAR-2000

    AA        2007  05-MAY-2007        
    AA        2007  07-JUN-2007        
    AA        2007  06-JUL-2007         

如果将行'113,13-AUG-2007,1'添加到订单表中,您希望得到什么输出? AA的一个块包含4行输出,或者两个块,每个块包含3行? 如果您愿意,是“严格每次三个月”还是“每次三个或更多月”。 - Jonathan Leffler
抱歉耽搁了,我更喜欢精确三个月的时间。 - Gopi
你的意思是一个4个月的字符串会返回6行,一组包含1、2、3月,另一组包含2、3、4月,还是只是排除所有不完全为3个月的订单字符串? - ErikE
从我的查询中删除“distinct”,您将获得所有三个月组合的各种可能性。比如,如果您有“1234”,您将会得到“123”,“134”,“234”。 - Denis Valeev
你能提供一个样本数据和期望的结果吗?假设在这个范围内有超过3个月连续且每个月都有多个订单。 - Martin Smith
4个回答

8

编辑:我去掉了MAX() OVER (PARTITION BY ...),因为它似乎影响了性能。

;WITH cte AS ( 
SELECT    CustID  ,
          OrderDate,
          DATEPART(YEAR, OrderDate)*12 + DATEPART(MONTH, OrderDate) AS YM
 FROM     Orders
 ),
 cte1 AS ( 
SELECT    CustID  ,
          OrderDate,
          YM,
          YM - DENSE_RANK() OVER (PARTITION BY CustID ORDER BY YM) AS G
 FROM     cte
 ),
 cte2 As
 (
 SELECT CustID  ,
          MIN(OrderDate) AS Mn,
          MAX(OrderDate) AS Mx
 FROM cte1
GROUP BY CustID, G
HAVING MAX(YM)-MIN(YM) >=2 
 )
SELECT     c.CustName, o.OrderDate, YEAR(o.OrderDate) AS YEAR
FROM         Customers AS c INNER JOIN
                      Orders AS o ON c.CustID = o.CustID
INNER JOIN  cte2 c2 ON c2.CustID = o.CustID and o.OrderDate between Mn and Mx
order by c.CustName, o.OrderDate

1
需要使用DENSE_RANK函数,否则三个月内的四笔或以上销售将被忽视。 - OMG Ponies
Martin,使用我下面的测试数据,你的查询返回了两个月之间相隔的订单对,其中mx-mn >= 2,但count(*) = 2而不是>= 3。 - ErikE
@Emtucifor - 我想这个问题现在应该已经解决了。我需要按CustID, G进行分区,而不仅仅是 G。非常感谢您让我知道! - Martin Smith
Martin,我喜欢你的解决方案,因为它很优雅,但令我惊讶的是,我的“有趣好奇”解决方案实际上表现更好。我认为这里有一个教训,即排名函数确实有时会产生非常显著的成本!而且,与预期有些相反,CROSS JOIN可以是解决分组/排名问题的非常有效的方法。如果我们有LAG和LEAD窗口函数,也许它们能够赢得一切! - ErikE
@Emtucifor,“MAX()/MIN() OVER (PARTITION BY ...)”似乎是主要问题。去掉它可以在一定程度上改善情况。 - Martin Smith
2
它比某些东西改进了一点点!现在你的查询是明显的赢家。 - ErikE

4
这是我的版本。我只是把它作为一种好奇心提出来,展示解决问题的另一种思路。结果比Martin Smith的“分组岛屿”解决方案表现得更好,这让它变得更加有用。不过,一旦他摆脱了一些非常昂贵的聚合窗口函数,并使用真正的聚合函数,他的查询开始表现出色。
解决方案1:运行3个月或更长时间,通过检查前后1个月并使用半连接进行操作。
WITH Months AS (
   SELECT DISTINCT
      O.CustID,
      Grp = DateDiff(Month, '20000101', O.OrderDate)
   FROM
      CustOrder O
), Anchors AS (
   SELECT
      M.CustID,
      Ind = M.Grp + X.Offset
   FROM
      Months M
      CROSS JOIN (
         SELECT -1 UNION ALL SELECT 0 UNION ALL SELECT 1
      ) X (Offset)
   GROUP BY
      M.CustID,
      M.Grp + X.Offset
   HAVING
      Count(*) = 3
)
SELECT
   C.CustName,
   [Year] = Year(OrderDate),
   O.OrderDate
FROM
   Cust C
   INNER JOIN CustOrder O ON C.CustID = O.CustID
WHERE
   EXISTS (
      SELECT 1
      FROM
         Anchors A
      WHERE
         O.CustID = A.CustID
         AND O.OrderDate >= DateAdd(Month, A.Ind, '19991201')
         AND O.OrderDate < DateAdd(Month, A.Ind, '20000301')
   )
ORDER BY
   C.CustName,
   OrderDate;

解决方案2:精确的3个月模式。如果运行时间为4个月或更长,则排除这些值。这是通过检查前两个月和后两个月(实际上是寻找模式N,Y,Y,Y,N)来完成的。

WITH Months AS (
   SELECT DISTINCT
      O.CustID,
      Grp = DateDiff(Month, '20000101', O.OrderDate)
   FROM
      CustOrder O
), Anchors AS (
   SELECT
      M.CustID,
      Ind = M.Grp + X.Offset
   FROM
      Months M
      CROSS JOIN (
         SELECT -2 UNION ALL SELECT -1 UNION ALL SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2
      ) X (Offset)
   GROUP BY
      M.CustID,
      M.Grp + X.Offset
   HAVING
      Count(*) = 3
      AND Min(X.Offset) = -1
      AND Max(X.Offset) = 1
)
SELECT
   C.CustName,
   [Year] = Year(OrderDate),
   O.OrderDate
FROM
   Cust C
   INNER JOIN CustOrder O ON C.CustID = O.CustID
   INNER JOIN Anchors A
      ON O.CustID = A.CustID
      AND O.OrderDate >= DateAdd(Month, A.Ind, '19991201')
      AND O.OrderDate < DateAdd(Month, A.Ind, '20000301')
ORDER BY
   C.CustName,
   OrderDate;

如果有其他人想要使用,这是我的表格加载脚本:
IF Object_ID('CustOrder', 'U') IS NOT NULL DROP TABLE CustOrder
IF Object_ID('Cust', 'U') IS NOT NULL DROP TABLE Cust
GO
SET NOCOUNT ON
CREATE TABLE Cust (
  CustID int identity(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  CustName varchar(100) UNIQUE
)

CREATE TABLE CustOrder (
   OrderID int identity(100, 1) NOT NULL PRIMARY KEY CLUSTERED,
   CustID int NOT NULL FOREIGN KEY REFERENCES Cust (CustID),
   OrderDate smalldatetime NOT NULL
)

DECLARE @i int
SET @i = 1000
WHILE @i > 0 BEGIN
   WITH N AS (
      SELECT
         Nm =
            Char(Abs(Checksum(NewID())) % 26 + 65)
            + Char(Abs(Checksum(NewID())) % 26 + 97)
            + Char(Abs(Checksum(NewID())) % 26 + 97)
            + Char(Abs(Checksum(NewID())) % 26 + 97)
            + Char(Abs(Checksum(NewID())) % 26 + 97)
            + Char(Abs(Checksum(NewID())) % 26 + 97)
   )
   INSERT Cust
   SELECT N.Nm
   FROM N
   WHERE NOT EXISTS (
      SELECT 1
      FROM Cust C
      WHERE
         N.Nm = C.CustName
   )

   SET @i = @i - @@RowCount
END
WHILE @i < 50000 BEGIN
   INSERT CustOrder
   SELECT TOP (50000 - @i)
      Abs(Checksum(NewID())) % 1000 + 1,
      DateAdd(Day, Abs(Checksum(NewID())) % 10000, '19900101')
   FROM master.dbo.spt_values
   SET @i = @i + @@RowCount
END

性能

以下是3个月及以上查询的性能测试结果:

Query     CPU   Reads Duration
Martin 1  2297 299412   2348 
Martin 2   625    285    809
Denis     3641    401   3855
Erik      1855  94727   2077

这只是每个查询的一次运行,但数字相当有代表性。原来,Denis,你的查询表现并不那么糟糕。Martin的查询明显更胜一筹,但最初使用了一些过于昂贵的窗口函数策略,后来进行了修正。
当然,正如我所指出的,如果一个客户在同一天有两个订单,Denis的查询就不会拉取正确的行,所以他的查询除非他进行了修正,否则将无法参与竞争。
此外,不同的索引可能会对结果产生影响。我不确定。

不要逼我在我的解决方案中再添加两个连接,它已经三维了。:P - Denis Valeev
1
完成。我仅在您旧版本中保留了统计信息,以显示并非所有窗口函数操作都如此出色。如果不加选择地使用它们,它们会损害性能。 - ErikE

1

给你:

select distinct
 CustName
,year(OrderDate) [Year]
,OrderDate
from 
(
select 
 o2.OrderDate [prev]
,o1.OrderDate [curr]
,o3.OrderDate [next]
,c.CustName
from [order] o1 
join [order] o2 on o1.CustId = o2.CustId and datediff(mm, o2.OrderDate, o1.OrderDate) = 1
join [order] o3 on o1.CustId = o3.CustId and o2.OrderId <> o3.OrderId and datediff(mm, o3.OrderDate, o1.OrderDate) = -1
join Customer c on c.CustId = o1.CustId
) t
unpivot
(
    OrderDate for [DateName] in ([prev], [curr], [next])
)
unpvt
order by CustName, OrderDate

警告:此查询极其低效。 :) - Denis Valeev
Denis,很抱歉向您报告,当同一客户在同一天下了两个订单时,此查询未返回正确的结果。 - ErikE
@Emtucifor,我知道!但我们不知道@CSharpy需要什么! :) - Denis Valeev

0

这是我的看法。

select 100 as OrderID,convert(datetime,'01-JAN-2000') OrderDate,    1  as CustID  into #tmp union
    select 101,convert(datetime,'05-FEB-2000'),        1 union
    select 102,convert(datetime,'10-MAR-2000'),        1 union
    select 103,convert(datetime,'01-NOV-2000'),        2 union   
    select 104,convert(datetime,'05-APR-2001'),        2 union
    select 105,convert(datetime,'07-MAR-2002'),        2 union
    select 106,convert(datetime,'01-JUL-2003'),        1 union
    select 107,convert(datetime,'01-SEP-2004'),        4 union
    select 108,convert(datetime,'01-APR-2005'),        4 union
    select 109,convert(datetime,'01-MAY-2006'),        3 union
    select 110,convert(datetime,'05-MAY-2007'),        1 union 
    select 111,convert(datetime,'07-JUN-2007'),        1 union
    select 112,convert(datetime,'06-JUL-2007'),        1 


    ;with cte as
    (
        select
            *   
            ,convert(int,convert(char(6),orderdate,112)) - dense_rank() over(partition by custid order by orderdate) as g
        from #tmp
    ),
    cte2 as 
    (
    select 
        CustID
        ,g  
    from cte a
    group by CustID, g
    having count(g)>=3
    )
    select
        a.CustID
        ,Yr=Year(OrderDate)
        ,OrderDate
    from cte2 a join cte b
        on a.CustID=b.CustID and a.g=b.g

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接