股票最长连续上涨区间的起始日期和结束日期
问题描述
库表 stock 记录多支股票的每日收盘价,字段为股票代码 CODE、交易日期 DT、收盘价 CL。现在要针对指定的目标股票,找出它历史上连续上涨天数最多的那个区间,并给出该区间的起始日期和结束日期。
源数据
stock 表(只列出目标股票 CODE = 100046 的记录,已按 DT 升序):
CODE |
DT |
CL |
100046 |
2009-01-01 00:00:00 |
3.89 |
100046 |
2009-01-02 00:00:00 |
3.93 |
100046 |
2009-01-05 00:00:00 |
3.78 |
100046 |
2009-01-06 00:00:00 |
4.04 |
100046 |
2009-01-07 00:00:00 |
3.77 |
100046 |
2009-01-08 00:00:00 |
4.02 |
100046 |
2009-01-09 00:00:00 |
4.42 |
100046 |
2009-01-12 00:00:00 |
4.86 |
100046 |
2009-01-13 00:00:00 |
4.44 |
100046 |
2009-01-14 00:00:00 |
4.88 |
100046 |
2009-01-15 00:00:00 |
4.98 |
100046 |
2009-01-16 00:00:00 |
4.99 |
期望结果
NoRisingDays 是区间编号:
NoRisingDays |
start_date |
end_date |
2 |
2009-01-07 00:00:00 |
2009-01-12 00:00:00 |
3 |
2009-01-13 00:00:00 |
2009-01-16 00:00:00 |
1-02 收盘价由 3.89 涨到 3.93,这是连续上涨;01-05 由 3.93 跌到 3.78,上涨中断,区间编号从 1 开始递增,新的一段从这里起步。
编号 2 这一段:2009-01-07 到 2009-01-12,共 4 个交易日。起点 01-07 的 3.77 低于 01-06 的 4.04,随后 4.02、4.42、4.86 一路走高,期间没有一天下跌。
编号 3 这一段:2009-01-13 到 2009-01-16,也是 4 个交易日。起点 01-13 的 4.44 低于 01-12 的 4.86,随后 4.88、4.98、4.99 连续上涨,长度与编号 2 并列最长。
SQLazy 分步实现
续上涨的本质是“不下跌的一段一段”。先把数据按日期排好序,再用分段功能盯着收盘价:收盘价一旦变小,就说明上一段上涨结束了,组号加一,开始新的一段。这样每一条记录都拿到了自己所属的上涨区间号。接着按区间号分组数一下行数,行数就是这段连续上涨的天数。最后挑出天数最大的那个区间,取它的第一条日期和最后一条日期,就是答案。
下面逐一解释这些步骤。
Name |
Anchor |
Statement |
t1 |
stock |
filter CODE = 100046 |
t2 |
t1 |
sort DT asc |
t3 |
t2 |
segment CL down as NoRisingDays |
t4 |
t3 |
compute count DT as ContinuousDays; partition NoRisingDays |
t5 |
t4 |
rank ContinuousDays max |
result |
t5 |
summarize min DT as start_date, max DT as end_date; group NoRisingDays |
第 1 步:取出目标股票
filter CODE = 100046
从 stock 表里筛出 CODE 等于 100046 的记录,后面的计算只针对这一只股票。比较用的是符号“=”,不是 SQL 里的“==”。
第 2 步:按日期升序排序
sort DT asc
把筛选后的记录按交易日 DT 升序排好。分段是按记录顺序一段一段走的,顺序不对,分出来的区间就没有意义。

第 3 步:收盘价变小就新开一段
segment CL down as NoRisingDays
这是本题的核心一步。分段功能针对收盘价 CL 使用固定条件“变小(down)”:当某一天的收盘价比前一天低时,组号加一;不低(上涨或持平)时组号不变。生成的组号写入新列 NoRisingDays,同一段内的记录组号相同,组号相同的一批记录就是一段连续的上涨行情。
这里不需要写 CL < CL[-1] 这样的跨行比较,也不用管第一条记录没有前一天怎么办,“变小”这个条件本身就表达了“跌了就另起一段”的业务含义。

第 4 步:数出每段有几天
compute count DT as ContinuousDays; partition NoRisingDays
计算列功能在 NoRisingDays 分区内统计记录条数:聚合算法 count 写在被聚合项 DT 之前,结果写入新列 ContinuousDays。分区内的每一行都会填上本段的总天数,所以这一步之后每条记录都带着“我所在的这段涨了几天”。因为 partition 已经按区间号分好了组,后面按 ContinuousDays 取最大值就等价于“找出最长的那个上涨区间”。

第 5 步:挑出最长的那一段
rank ContinuousDays max
排名功能按 ContinuousDays 取最大值对应的记录。最大值对应的记录可能不止一条,此时会返回全部并列的记录,所以并列最长的那几段都会保留下来,不会因为并列而漏掉一段。这一步只筛选记录,不改变记录顺序。

第 6 步:取每段的起止日期
summarize min DT as start_date, max DT as end_date; group NoRisingDays
汇总功能按 NoRisingDays 分组,组内取 DT 的最小值命名为 start_date,取 DT 的最大值命名为 end_date。因为上一步已经把记录限定在最长的那一段(或并列的几段)里,所以这里算出来的就是最长连续上涨区间的起始日期和结束日期。汇总的聚合算法写在被聚合项之前,写成 min DT 和 max DT。

编译生成 SQL
确认上述 6 步逻辑后,SQLazy 编译器自动生成原生 SQL(这里是 MySQL 语法):
WITH t2 AS (
SELECT CODE, DT, CL
FROM stock
WHERE CODE = 100046
),
t3 AS (
SELECT CODE, DT, CL
, SUM(CASE
WHEN CL < col__1 THEN 1
ELSE 0
END) OVER (ORDER BY CASE
WHEN DT IS NULL THEN 1
ELSE 0
END, DT ASC ROWS UNBOUNDED PRECEDING) + 1 AS NoRisingDays
FROM (
SELECT t2.*, LAG(CL, 1) OVER (ORDER BY CASE
WHEN DT IS NULL THEN 1
ELSE 0
END, DT ASC) AS col__1
FROM t2
) sub__2
),
t4 AS (
SELECT CODE, DT, CL, NoRisingDays
, COUNT(DT) OVER (PARTITION BY NoRisingDays ) AS ContinuousDays
FROM t3
),
sub__6 AS (
SELECT sub__5.*, RANK() OVER (ORDER BY ContinuousDays DESC) AS col_4
FROM t4 sub__5
)
SELECT CODE, DT, CL, NoRisingDays, ContinuousDays
FROM sub__6
WHERE col_4 = 1
SQLazy 让你用业务语言描述逻辑,而不是用 SQL 语法写嵌套查询。这道“最长连续上涨区间”的题,核心只有一句话:收盘价变小就新开一段。SQLazy 用 segment CL down 直接表达“跌了就另起一段”,而手写 SQL 要先构造 LAG 比较出涨跌标志,再用 SUM 累计出区间号,两层窗口函数套在一起,边界和空值都得自己兜住。
SQLazy 的价值在于分步。整个计算被拆成取数、排序、分段、计数、取最长、汇总六步,每一步都是一张可以直接查看的中间结果表:第 3 步之后能看到区间号,第 4 步之后能看到每段几天,最长的那一段是哪一段一目了然,不需要把整条 SQL 跑完再去猜中间算错了没有。
SQLazy 的另一个特点是把“先算出每段天数、再挑最大、再取日期”这种自然的业务顺序直接写成代码顺序:compute 在分区内计数,rank 取最大值对应的记录,summarize 在组内取最小和最大日期。汇总类功能里聚合算法写在被聚合项之前(count DT、min DT、max DT),读到代码就能对上业务说法。
分区分段的能力让这类“按区间算长度、再选区间”的问题不必写自连接:partition 把同一段记录归到一组,组内计数和组内取首尾日期都是现成的功能。SQLazy 负责把这套逻辑编译成目标数据库能跑的 SQL,同一段代码换一个数据库也不用改写法。
官方链接
SQLazy 在线体验:sqlazy.com(免费,无需注册)
SQLazy 项目仓库:github.com/SPLWare/SQLazy

英文版 https://c.esproc.com/article/1790746123924