如何在SQL中按三月窗口保留分组后单条数据并删除其余行?
当然可以实现!核心思路是先给每个(name, sex, id)组内的行按「三月时间窗口」分组,然后在每个窗口里只保留任意一行,删掉其他的。具体实现取决于你对「三月窗口」的定义,下面分两种常见情况说明:
情况1:按自然季度划分窗口(1-3月、4-6月、7-9月、10-12月)
这是最常用的方式,每个自然季度作为一个三个月窗口。用窗口函数ROW_NUMBER()给每个窗口内的行编号,然后只保留编号为1的行。
以MySQL 8.0+为例:
-- 先给每个组+季度内的行随机编号 WITH ranked_rows AS ( SELECT date, name, sex, id, status, ROW_NUMBER() OVER ( PARTITION BY name, sex, id, YEAR(date), QUARTER(date) ORDER BY RAND() -- 随机选一行,也可以改成ORDER BY date选最早/最晚的 ) AS row_num FROM your_table_name ) -- 删除编号大于1的行(也就是每个窗口只留第一行) DELETE FROM your_table_name WHERE (date, name, sex, id) IN ( SELECT date, name, sex, id FROM ranked_rows WHERE row_num > 1 );
如果你的表有主键(比如row_id),用主键判断更安全,避免因为重复的date/name/sex/id误删:
WITH ranked_rows AS ( SELECT row_id, ROW_NUMBER() OVER ( PARTITION BY name, sex, id, YEAR(date), QUARTER(date) ORDER BY RAND() ) AS row_num FROM your_table_name ) DELETE FROM your_table_name WHERE row_id IN ( SELECT row_id FROM ranked_rows WHERE row_num > 1 );
情况2:自定义滚动三个月窗口(从每个组的最早日期开始,每三个月一个窗口)
如果你的窗口不是自然季度,而是从每个组的第一条数据开始,每三个月划一个窗口(比如组内最早日期是2016-08-02,窗口就是2016-08-022016-11-02、2016-11-022017-02-02……),可以用递归CTE生成窗口,再给行分配窗口:
WITH RECURSIVE group_windows AS ( -- 初始化:每个组的最早日期作为第一个窗口的起始 SELECT name, sex, id, MIN(date) AS window_start, DATE_ADD(MIN(date), INTERVAL 3 MONTH) AS window_end FROM your_table_name GROUP BY name, sex, id UNION ALL -- 递归生成后续窗口,每个窗口的起始是上一个窗口的结束 SELECT gw.name, gw.sex, gw.id, gw.window_end, DATE_ADD(gw.window_end, INTERVAL 3 MONTH) FROM group_windows gw JOIN ( SELECT name, sex, id, MAX(date) AS max_date FROM your_table_name GROUP BY name, sex, id ) gd ON gw.name = gd.name AND gw.sex = gd.sex AND gw.id = gd.id WHERE gw.window_end < gd.max_date ), row_assignments AS ( -- 给每一行分配所属的窗口,并编号 SELECT t.*, ROW_NUMBER() OVER ( PARTITION BY t.name, t.sex, t.id, gw.window_start ORDER BY RAND() ) AS row_num FROM your_table_name t JOIN group_windows gw ON t.name = gw.name AND t.sex = gw.sex AND t.id = gw.id AND t.date >= gw.window_start AND t.date < gw.window_end ) -- 删除非首行的记录 DELETE FROM your_table_name WHERE (date, name, sex, id) IN ( SELECT date, name, sex, id FROM row_assignments WHERE row_num > 1 );
注意事项
- 如果用
ORDER BY RAND()是随机保留一行,你也可以换成ORDER BY date保留最早/最晚的,更可控。 - 不同SQL方言(比如PostgreSQL、SQL Server)的函数语法略有差异,比如PostgreSQL用
EXTRACT(YEAR FROM date)代替YEAR(date),random()代替RAND(),但逻辑是一样的。 - 执行删除前建议先运行
SELECT语句确认保留的行是否符合预期,避免误删数据。
内容的提问来源于stack exchange,提问作者Chen
相关产品推荐
相关产品推荐

