使用日期范围的可能递归 CTE 查询

发布于 2024-11-29 09:46:09 字数 4186 浏览 0 评论 0原文

不知道如何表达这个标题!

我有以下数据:

IF OBJECT_ID ('tempdb..#data') IS NOT NULL DROP TABLE #data
CREATE TABLE #data
(
id UNIQUEIDENTIFIER
,reference NVARCHAR(30)
,start_date DATETIME
,end_date DATETIME
,lapse_date DATETIME
,value_received DECIMAL(18,3)
)

INSERT INTO #data VALUES ('BE91B9C1-C02F-46F7-9B63-4D0B25D9BA2F','168780','2006-05-01 00:00:00.000',NULL,'2011-09-27 00:00:00.000',537.42)
INSERT INTO #data VALUES ('B538F123-C839-447A-B300-5D16EACF4560','320858','2011-08-08 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('1922465D-2A55-434D-BAAA-8E15D681CF12','306597','2011-04-08 00:00:00.000','2011-06-22 13:14:40.083','2011-08-07 00:00:00.000',12)
INSERT INTO #data VALUES ('7DF8FBCC-B490-4892-BDC5-8FD2D73B0323','321461','2011-07-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',8.44)
INSERT INTO #data VALUES ('1EC2E754-F325-4313-BDFC-9010E255F6FE','74215','2000-10-31 00:00:00.000',NULL,'2011-08-30 00:00:00.000',258)
INSERT INTO #data VALUES ('9E59B09C-0198-48AC-8EEC-A0D76CEA9385','169194','2008-06-25 00:00:00.000',NULL,'2011-09-25 00:00:00.000',1766.4)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','288039','2010-09-01 00:00:00.000','2011-07-29 00:00:00.000','2011-08-21 00:00:00.000',55)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','324423','2011-08-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',5)
INSERT INTO #data VALUES ('D5E5197A-E8E1-468C-9991-C8712224C2BF','323395','2011-08-25 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('0EC4976C-16B9-4C99-BD07-D0CBDF014D32','323741','2011-08-25 00:00:00.000',NULL,NULL,0)

并且我希望能够根据以下条件将所有引用分组为“活动”、“已失效”或“新”类别:

  • Active 有一个开始日期小于参考月份的最后日期,失效日期在上个月最后一天之后,并且 value_received > 0;

  • New 的开始日期在参考月份内;

  • Lapsed 的失效日期在参考月份内。

然后将这些定义应用于滚动 13 个月的每个参考文献(从现在开始一直追溯到 2010 年 7 月),以便每个月我可以看到有多少参考文献属于每个组。

我可以使用以下内容来定义当前月份的值:

select 
id
,reference
,start_date
,end_date
,lapse_date 
,value_received
,CASE   WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
        AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) --last day of current month
        AND value_received > 0
        THEN 'Active'
        WHEN lapse_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start
            AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
        THEN 'lapse'
        WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
        AND start_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
        THEN 'New'
        ELSE 'Not applicable'
 END AS [type]
from #data

但我看不到一种很好/有效的方法来执行此操作(除了重复此查询 13 次并合并结果之外,我知道这很糟糕)

这是否是使用当前月份作为锚点并使用递归的情况(如果是这样,一些指针将非常受欢迎)?

一如既往地感谢任何帮助:)

* 编辑以包括实际的解决方案 *

如果有人感兴趣,这是我使用的最终查询:

;WITH Months as 
(
SELECT DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) as month_end
,0 AS level
UNION ALL     
SELECT DATEADD(month, -1, month_end)as month_end
,level + 1 FROM Months
WHERE level < 13 
) 
SELECT 
DATENAME(Month,month_end) + ' ' + DATENAME(YEAR,month_end) as date
,SUM(CASE WHEN start_date <= month_end
        AND Month(start_date) <> MONTH(Month_end)
        AND lapse_date > Month_end 
 THEN 1 ELSE 0 END) AS Active
,SUM(CASE WHEN start_date <= Month_end 
        AND DATENAME(MONTH,start_date) + ' ' + DATENAME(YEAR,start_date) = 
        DATENAME(MONTH,month_end) + ' ' + DATENAME(YEAR,month_end)
THEN 1 ELSE 0 END) AS New
,SUM(CASE WHEN lapse_date <= Month_end 
        AND Month(lapse_date) = MONTH(Month_end)
THEN 1 ELSE 0 END) AS lapse
FROM #data
CROSS JOIN Months
WHERE id IS NOT NULL
AND start_date IS NOT NULL
GROUP BY DATENAME(Month,month_end)  + ' ' + DATENAME(YEAR,month_end) 
ORDER by MAX(level) ASC

Not sure how to even phrase the title on this one!

I have the following data:

IF OBJECT_ID ('tempdb..#data') IS NOT NULL DROP TABLE #data
CREATE TABLE #data
(
id UNIQUEIDENTIFIER
,reference NVARCHAR(30)
,start_date DATETIME
,end_date DATETIME
,lapse_date DATETIME
,value_received DECIMAL(18,3)
)

INSERT INTO #data VALUES ('BE91B9C1-C02F-46F7-9B63-4D0B25D9BA2F','168780','2006-05-01 00:00:00.000',NULL,'2011-09-27 00:00:00.000',537.42)
INSERT INTO #data VALUES ('B538F123-C839-447A-B300-5D16EACF4560','320858','2011-08-08 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('1922465D-2A55-434D-BAAA-8E15D681CF12','306597','2011-04-08 00:00:00.000','2011-06-22 13:14:40.083','2011-08-07 00:00:00.000',12)
INSERT INTO #data VALUES ('7DF8FBCC-B490-4892-BDC5-8FD2D73B0323','321461','2011-07-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',8.44)
INSERT INTO #data VALUES ('1EC2E754-F325-4313-BDFC-9010E255F6FE','74215','2000-10-31 00:00:00.000',NULL,'2011-08-30 00:00:00.000',258)
INSERT INTO #data VALUES ('9E59B09C-0198-48AC-8EEC-A0D76CEA9385','169194','2008-06-25 00:00:00.000',NULL,'2011-09-25 00:00:00.000',1766.4)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','288039','2010-09-01 00:00:00.000','2011-07-29 00:00:00.000','2011-08-21 00:00:00.000',55)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','324423','2011-08-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',5)
INSERT INTO #data VALUES ('D5E5197A-E8E1-468C-9991-C8712224C2BF','323395','2011-08-25 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('0EC4976C-16B9-4C99-BD07-D0CBDF014D32','323741','2011-08-25 00:00:00.000',NULL,NULL,0)

And I want to be able to group all references into a category of 'active', 'lapsed' or 'new' based upon the following criteria:

  • Active has a start date that is less than the last date of the reference month, a lapse date after the last day of the prior month and a value_received > 0;

  • New has a start date which falls within the reference month;

  • Lapsed has a lapse date which falls within the reference month.

And to then apply these definitions for each reference for a rolling 13 months (so from Now going back as far as July 2010) so that for each month I can see how many references fall into each group.

I am able to use the following to define this for the current month:

select 
id
,reference
,start_date
,end_date
,lapse_date 
,value_received
,CASE   WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
        AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) --last day of current month
        AND value_received > 0
        THEN 'Active'
        WHEN lapse_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start
            AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
        THEN 'lapse'
        WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
        AND start_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
        THEN 'New'
        ELSE 'Not applicable'
 END AS [type]
from #data

But I can't see a nice / efficient way of doing this (other than to repeat this query 13 times and union the results, which I know is just awful)

Would this be a case for using the current month as an anchor and using recursion (if so, some pointers would be most appreciated)?

Any help most appreciated as always :)

* Edited to include actual solution *

In case it's of interest to anyone, this is the final query I used:

;WITH Months as 
(
SELECT DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) as month_end
,0 AS level
UNION ALL     
SELECT DATEADD(month, -1, month_end)as month_end
,level + 1 FROM Months
WHERE level < 13 
) 
SELECT 
DATENAME(Month,month_end) + ' ' + DATENAME(YEAR,month_end) as date
,SUM(CASE WHEN start_date <= month_end
        AND Month(start_date) <> MONTH(Month_end)
        AND lapse_date > Month_end 
 THEN 1 ELSE 0 END) AS Active
,SUM(CASE WHEN start_date <= Month_end 
        AND DATENAME(MONTH,start_date) + ' ' + DATENAME(YEAR,start_date) = 
        DATENAME(MONTH,month_end) + ' ' + DATENAME(YEAR,month_end)
THEN 1 ELSE 0 END) AS New
,SUM(CASE WHEN lapse_date <= Month_end 
        AND Month(lapse_date) = MONTH(Month_end)
THEN 1 ELSE 0 END) AS lapse
FROM #data
CROSS JOIN Months
WHERE id IS NOT NULL
AND start_date IS NOT NULL
GROUP BY DATENAME(Month,month_end)  + ' ' + DATENAME(YEAR,month_end) 
ORDER by MAX(level) ASC

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

何以心动 2024-12-06 09:46:09

这里不需要“真正的”递归 CTE。不过,您可以使用一个作为月份引用:

;WITH Months
as
(
    SELECT DATEADD(day, -DATEPART(day, GETDATE())+1, GETDATE()) as 'MonthStart'
    UNION ALL
    SELECT DATEADD(month, -1, MonthStart) as 'MonthStart'
    FROM Months
)

然后您可以在上述查询中JOINSELECT TOP 13 * FROM Months

我不会尝试解析您的所有 CASE 语句,但本质上您可以在日期和 MonthStart 字段上使用 GROUP BY ,例如:

GROUP BY Datepart(year,monthstart), Datepart(month,monthstart)

并按月聚合。将所有选项(活动的、已失效的等)作为列并使用 SUM(CASE WHEN ... THEN 1 ELSE 0 END) 计算每个选项可能是最简单的,因为使用它会更容易一个GROUP BY

You don't need a "real" recursive CTE here. You can use one for the month references though:

;WITH Months
as
(
    SELECT DATEADD(day, -DATEPART(day, GETDATE())+1, GETDATE()) as 'MonthStart'
    UNION ALL
    SELECT DATEADD(month, -1, MonthStart) as 'MonthStart'
    FROM Months
)

Then you can JOIN to SELECT TOP 13 * FROM Months in your above query.

I'm not going to try to parse all your CASE statements, but essentially you can use a GROUP BY on the date and the MonthStart fields, like:

GROUP BY Datepart(year, monthstart), Datepart(month, monthstart)

and aggregate by month. It will probably be easiest to have all your options (active, lapsed, etc) as columns and calculate each with a SUM(CASE WHEN ... THEN 1 ELSE 0 END) as it will be easier with a GROUP BY.

來不及說愛妳 2024-12-06 09:46:09

您可以将您的请求与递归 CTE 交叉连接,这是一个好主意。

WITH thirteenMonthBack(myDate, level) as
(
   SELECT GETDATE() as myDate, 0 as level
   UNION ALL
   SELECT DATEADD(month, -1, myDate), level + 1
   FROM thirteenMonthBack
   WHERE level < 13
)
SELECT xxx
FROM youQuery
   CROSS JOIN thirteenMonthBack

You can cross join your request with a recursive CTE, this is a good idea.

WITH thirteenMonthBack(myDate, level) as
(
   SELECT GETDATE() as myDate, 0 as level
   UNION ALL
   SELECT DATEADD(month, -1, myDate), level + 1
   FROM thirteenMonthBack
   WHERE level < 13
)
SELECT xxx
FROM youQuery
   CROSS JOIN thirteenMonthBack
静谧幽蓝 2024-12-06 09:46:09
DECLARE @date DATE = GETDATE()

;WITH MonthsCTE AS (
    SELECT 1 [Month], DATEADD(DAY, -DATEPART(DAY, @date)+1, @date) as 'MonthStart'
    UNION ALL
    SELECT [Month] + 1, DATEADD(MONTH, 1, MonthStart)
    FROM MonthsCTE 
    WHERE [Month] < 12 )

SELECT * FROM MonthsCTE
DECLARE @date DATE = GETDATE()

;WITH MonthsCTE AS (
    SELECT 1 [Month], DATEADD(DAY, -DATEPART(DAY, @date)+1, @date) as 'MonthStart'
    UNION ALL
    SELECT [Month] + 1, DATEADD(MONTH, 1, MonthStart)
    FROM MonthsCTE 
    WHERE [Month] < 12 )

SELECT * FROM MonthsCTE
扛刀软妹 2024-12-06 09:46:09
/*
 | The below SELECT statements show TWO examples of how this can be useful.  
 | Example 1 SELECT: Simple example of showing how to generate 12 days ahead based on date entered
 | Example 2 SELECT: This example shows how to generate 12 months ahead based on date entered
 |  This example tries to mimic as best it can Oracles use of LEVEL and CONNECT BY LEVEL
*/
WITH dynamicRecords(myDate, level) AS
(
   SELECT GETDATE() AS myDate, 1 AS level
   
   UNION ALL
   
   SELECT myDate + 1, level + 1          /* 12 Days - WHERE level < 12  */
   --SELECT DATEADD(month, 1, myDate), level + 1 /* 12 Months - WHERE level < 12 */
   FROM dynamicRecords
   WHERE level < 12
)
SELECT *
FROM dynamicRecords
Option  (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */

截图:
Oracle 级别等效屏幕截图

/* Original T-SQL Solution I found here: https://riptutorial.com/sql-server/example/11098/generating-date-range-with-recursive-cte
 | The below provides an example of how to generate the days within a date range of the dates entered.
 | The below SELECT statements show TWO examples of how this can be useful.  
 | Example 1 SELECT: Uses static dates to display ALL of the dates within the range for the dates entered
 | Example 2 SELECT: This example uses GETDATE() and then obtains the FOM day and the EOM day of the dates
 |              beging entered to then show all days in the month of the dates entered.
*/
With DateCte AS
(
    SELECT CAST('2021-04-21' AS DATE) AS BeginDate, CAST('2022-05-02' AS DATE) AS EndDate
    --SELECT CAST( GETDATE() - Day(GETDATE()) + 1 AS DATE ) AS BeginDate, CAST(EOMONTH(GETDATE()) AS DATE) AS EndDate
    UNION ALL
    SELECT  DateAdd(Day, 1, BeginDate), EndDate
    FROM    DateCte
    WHERE   BeginDate < EndDate
)
Select  BeginDate AS Dates
From    DateCte
Option  (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */
;

屏幕截图:
GenerateDateRecordsFromDateRange

/*
 | The below SELECT statements show TWO examples of how this can be useful.  
 | Example 1 SELECT: Simple example of showing how to generate 12 days ahead based on date entered
 | Example 2 SELECT: This example shows how to generate 12 months ahead based on date entered
 |  This example tries to mimic as best it can Oracles use of LEVEL and CONNECT BY LEVEL
*/
WITH dynamicRecords(myDate, level) AS
(
   SELECT GETDATE() AS myDate, 1 AS level
   
   UNION ALL
   
   SELECT myDate + 1, level + 1          /* 12 Days - WHERE level < 12  */
   --SELECT DATEADD(month, 1, myDate), level + 1 /* 12 Months - WHERE level < 12 */
   FROM dynamicRecords
   WHERE level < 12
)
SELECT *
FROM dynamicRecords
Option  (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */

Screenshots:
Oracle Level Equivalent Screenshot

/* Original T-SQL Solution I found here: https://riptutorial.com/sql-server/example/11098/generating-date-range-with-recursive-cte
 | The below provides an example of how to generate the days within a date range of the dates entered.
 | The below SELECT statements show TWO examples of how this can be useful.  
 | Example 1 SELECT: Uses static dates to display ALL of the dates within the range for the dates entered
 | Example 2 SELECT: This example uses GETDATE() and then obtains the FOM day and the EOM day of the dates
 |              beging entered to then show all days in the month of the dates entered.
*/
With DateCte AS
(
    SELECT CAST('2021-04-21' AS DATE) AS BeginDate, CAST('2022-05-02' AS DATE) AS EndDate
    --SELECT CAST( GETDATE() - Day(GETDATE()) + 1 AS DATE ) AS BeginDate, CAST(EOMONTH(GETDATE()) AS DATE) AS EndDate
    UNION ALL
    SELECT  DateAdd(Day, 1, BeginDate), EndDate
    FROM    DateCte
    WHERE   BeginDate < EndDate
)
Select  BeginDate AS Dates
From    DateCte
Option  (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */
;

Screenshot:
GenerateDateRecordsFromDateRange

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文