sql server用dateadd+datediff实现半小时分组桶,postgresql用date_bin最省心,mysql用unix_timestamp整除1800秒还原;核心是将时间转线性整数后整除对齐周期起点。

用 DATEPART 和算术运算构造半小时分组桶
SQL Server 没有原生的“按30分钟分组”函数,但可以用 DATEPART 提取分钟数,再除以30取整,配合小时、日期拼出时间桶。关键不是四舍五入,而是向下对齐到最近的起始点(如 10:00、10:30)。
常见错误是直接 GROUP BY DATEPART(minute, time_col) / 30,这会丢失小时和日期信息,导致不同天/小时的 0–29 分全混在一起。
- 正确做法:把时间先转成“距当天零点的分钟总数”,再整除30,最后转回 datetime:
DATEADD(minute, (DATEDIFF(minute, 0, time_col) / 30) * 30, 0)
- 更易读的写法(SQL Server 2022+):
CAST(FLOOR(CAST(time_col AS FLOAT) * 48) / 48 AS DATETIME2)
(因为一天48个半小时,乘48再向下取整再除48) - 注意
DATEDIFF(minute, 0, ...)中的0是 SQL Server 的日期字面量,等价于'1900-01-01',别写成字符串或NULL
PostgreSQL 用 date_bin 最省心
PostgreSQL 14+ 原生支持 date_bin,直接指定间隔和基准时间,自动对齐。这是目前最直观、不易出错的方式。
容易踩的坑是忽略 origin 参数——它决定第一个分组的起始点。默认是 '2000-01-01',如果数据集中在最近几年,可能导致第一组跨度异常大。
- 按半小时分组(从每小时0分开始):
date_bin('30 minutes', event_time, '2000-01-01') - 想从当天 00:00 开始对齐?把
origin换成CURRENT_DATE:date_bin('30 minutes', event_time, CURRENT_DATE) - 间隔必须是 interval 类型,不能写
'30m'或30,否则报错ERROR: invalid input syntax for type interval
MySQL 需手动计算时间戳偏移
MySQL 没有 date_bin,也不支持直接对 datetime 做除法。得靠 UNIX_TIMESTAMP 转为秒级整数,再做整除和还原。
典型错误是用 HOUR() 和 MINUTE() 单独处理,结果跨小时时逻辑断裂(比如 14:59 和 15:00 被分到不同组)。
- 安全做法:统一转时间戳,减去基准偏移后整除再加回:
FROM_UNIXTIME((UNIX_TIMESTAMP(event_time) - UNIX_TIMESTAMP('2000-01-01')) DIV 1800 * 1800 + UNIX_TIMESTAMP('2000-01-01'))(1800 秒 = 30 分钟) - 若只关心当天分组,可简化为:
FROM_UNIXTIME(UNIX_TIMESTAMP(event_time) DIV 1800 * 1800)
,但要注意时区——UNIX_TIMESTAMP默认用系统时区,而FROM_UNIXTIME用会话时区,不一致会导致偏移 - MySQL 8.0+ 支持 CTE,可先算出分组键再 JOIN,避免重复计算
通用技巧:任意间隔(如 45 分钟、7 小时)都适用同一模式
所有方案本质都是“对齐到最近的周期起点”。核心是两步:把时间线映射成线性整数(分钟数、秒数、毫秒数),再用整数除法截断到倍数边界。
最容易被忽略的是时区和精度损失。比如用 CAST(... AS INT) 截断 datetime,在 SQL Server 中可能因小数秒四舍五入导致跨桶;在 MySQL 中 UNIX_TIMESTAMP 返回秒级整数,天然丢弃微秒——如果业务依赖毫秒级精度,就得改用 UNIX_TIMESTAMP(...)*1000 并除以对应毫秒数。
另外,窗口函数(如 LAG)无法替代分组,别试图用它“模拟”时间桶;聚合必须走 GROUP BY 或窗口帧定义。











