将 PostgreSQL 递归 CTE 转换为 SQL Server
Posted
技术标签:
【中文标题】将 PostgreSQL 递归 CTE 转换为 SQL Server【英文标题】:Converting PostgreSQL recursive CTE to SQL Server 【发布时间】:2021-10-14 03:18:26 【问题描述】:我在将一些递归 CTE 代码从 PostgreSQL 调整到 SQL Server 时遇到问题,来自《用数据对抗流失》一书
这是工作的 PostgreSQL 代码:
with recursive
active_period_params as (
select interval '30 days' as allowed_gap,
'2021-09-30'::date as calc_date
),
active as (
-- anchor
select distinct account_id, min(start_date) as start_date
from subscription inner join active_period_params
on start_date <= calc_date
and (end_date > calc_date or end_date is null)
group by account_id
UNION
-- recursive
select s.account_id, s.start_date
from subscription s
cross join active_period_params
inner join active e on s.account_id=e.account_id
and s.start_date < e.start_date
and s.end_date >= (e.start_date-allowed_gap)::date
)
select account_id, min(start_date) as start_date
from active
group by account_id
这是我转换为 SQL Server 的尝试。它陷入了一个循环。我认为这个问题与 SQL Server 所需的 UNION ALL 有关。
with
active_period_params as (
select 30 as allowed_gap,
cast('2021-09-30' as date) as calc_date
),
active as (
-- anchor
select distinct account_id, min(start_date) as start_date
from subscription inner join active_period_params
on start_date <= calc_date
and (end_date > calc_date or end_date is null)
group by account_id
UNION ALL
-- recursive
select s.account_id, s.start_date
from subscription s
cross join active_period_params
inner join active e on s.account_id=e.account_id
and s.start_date < e.start_date
and s.end_date >= dateadd(day, -allowed_gap, e.start_date)
)
select account_id, min(start_date) as start_date
from active
group by account_id
订阅表是属于客户的订阅列表。客户可以拥有多个日期重叠或日期之间有间隔的订阅。 null end_date 表示订阅当前处于活动状态并且没有定义的 end_date。下面是单个客户 (account_id = 15) 的示例数据:
subscription
---------------------------------------------------
| id | account_id | start_date | end_date |
---------------------------------------------------
| 6 | 15 | 01/06/2021 | null |
| 5 | 15 | 01/01/2021 | null |
| 4 | 15 | 01/06/2020 | 01/02/2021 |
| 3 | 15 | 01/04/2020 | 15/05/2020 |
| 2 | 15 | 01/03/2020 | 15/05/2020 |
| 1 | 15 | 01/06/2019 | 01/01/2020 |
预期的查询结果(由 PostgreSQL 代码产生):
------------------------------
| account_id | start_date |
------------------------------
| 15 | 01/03/2020 |
问题: 上面的 SQL Server 代码卡在一个循环中,没有产生结果。
PostgreSQL 代码说明:
-
锚块查找在 calc_date (30/09/2021) (id 5 & 6) 处于活动状态的 subs,并返回 min start_date (01/01/2021)
然后,递归块查找在 allowed_gap 中存在的任何较早的 subs,这是在 1) 中找到的 min_start 日期前 30 天。 id 4 符合此条件,因此新的最小 start_date 为 01/06/2020
递归重复并在 allowed_gap (01/06/2020 - 30 天) 内找到两个子项。在这些潜艇(id 2 和 3)中,新的最小 start_date 是 01/03/2020
递归未能在 allowed_gap 内找到更早的子(01/03/2020 - 30 天)
查询返回 account_id 15 的开始日期 01/03/2020
任何帮助表示赞赏!
【问题讨论】:
请提供minimal reproducible example,即样本数据、您想要的结果和您的尝试。 我可以看到至少有一个区别dateadd(day, allowed_gap, e.start_date)
应该是dateadd(day, -allowed_gap, e.start_date)
。两个系统之间的一大区别是 Postgres 能够区分 rCTE,而 SQL Server 不能,因为它逐行处理它。让我感到震惊的是,这种递归可能使用计数表之类的东西做得更好,但如果没有minimal reproducible example,就很难判断。
@Charlieface 感谢您注意到错误 - 已修复
注意 - 将 DISTINCT 与 GROUP BY 子句一起使用几乎总是毫无意义的。 GROUP BY 将保证每组分组列都有一行。添加 distinct 没有任何用处。
【参考方案1】:
问题似乎与 SQL Server 处理递归 CTE 的方式有关。
这是一种gaps-and-islands问题,实际上不需要递归。
有很多解决方案,这里是一个。鉴于您的要求,可能有更有效的方法,但这应该可以帮助您入门。
使用LAG
,我们可以识别在下一行的指定间隙内的行
我们使用运行中的COUNT
为每组连续的行指定一个 ID
我们按该 ID 分组,取最小的start_date
,过滤掉不符合条件的组
再次分组以获得每个帐户的最小值
DECLARE @allowed_gap int = 30,
@calc_date datetime = cast('2021-09-30' as date);
WITH PrevValues AS (
SELECT *,
IsStart = CASE WHEN ISNULL(LAG(end_date) OVER (PARTITION BY account_id
ORDER BY start_date), '2099-01-01') < DATEADD(day, -@allowed_gap, start_date)
THEN 1 END
FROM subscription
),
Groups AS (
SELECT *,
GroupId = COUNT(IsStart) OVER (PARTITION BY account_id
ORDER BY start_date ROWS UNBOUNDED PRECEDING)
FROM PrevValues
),
ByGroup AS (
SELECT
account_id,
GroupId,
start_date = MIN(start_date)
FROM Groups
GROUP BY account_id, GroupId
HAVING COUNT(CASE WHEN start_date <= @calc_date
and (end_date > @calc_date or end_date is null) THEN 1 END) > 0
)
SELECT
account_id,
start_date = MIN(start_date)
FROM ByGroup
GROUP BY account_id;
db<>fiddle
【讨论】:
以上是关于将 PostgreSQL 递归 CTE 转换为 SQL Server的主要内容,如果未能解决你的问题,请参考以下文章