SQL中截断日期到小时的通用写法因数据库而异:PostgreSQL用date_trunc('hour', ts),MySQL推荐DATE_FORMAT(ts, '%Y-%m-%d %H:00:00'),SQL Server必须用DATEADD(HOUR, DATEDIFF(HOUR, 0, ts), 0),SQLite用strftime('%Y-%m-%d %H:00:00', ts);GROUP BY需与SELECT中表达式完全一致,且须注意时区转换与索引优化。

SQL中截断日期到小时的通用写法
几乎所有主流数据库都支持将日期截断到小时,但语法差异明显。关键不是“能不能”,而是“用哪个函数不会出错”。
- PostgreSQL 用
date_trunc('hour', created_at),最直观,也最不容易误判时区 - MySQL 8.0+ 推荐
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00'),DATE_SUB(created_at, INTERVAL SECOND(created_at) + 60 * MINUTE(created_at) SECOND)虽然等价但可读性差、易算错 - SQL Server 必须用
DATEADD(HOUR, DATEDIFF(HOUR, 0, created_at), 0),别信网上抄的CAST(FLOOR(CAST(...))写法——在边界时间(如 23:59:59)可能向下偏移一小时 - SQLite 只能靠
strftime('%Y-%m-%d %H:00:00', created_at),注意它默认按本地时区解析字符串,若字段存的是 UTC 时间,得先strftime('%Y-%m-%d %H:00:00', created_at, 'utc')
GROUP BY 时必须和 SELECT 中的截断表达式完全一致
这是实际写 SQL 时踩坑最多的地方:SELECT 里用了 date_trunc('hour', ts),GROUP BY 却写成 date_trunc('hour', ts)::text 或加了额外函数包装,结果报错或聚合错乱。
- PostgreSQL 报错典型提示:
column "ts" must appear in the GROUP BY clause or be used in an aggregate function,本质是表达式不匹配 - MySQL 允许 SELECT 和 GROUP BY 表达式不严格一致,但开启
ONLY_FULL_GROUP_BY后会拒绝执行——建议始终打开,避免隐式错误 - 安全做法:把截断逻辑提成子查询或 CTE,再在外部 SELECT 和 GROUP BY 中复用同一列别名,例如:
WITH hourly AS (SELECT date_trunc('hour', created_at) AS hour_start, amount FROM orders) SELECT hour_start, SUM(amount) FROM hourly GROUP BY hour_start;
时区处理不当会导致跨小时数据错位
数据库服务器时区、客户端连接时区、字段存储时区三者不一致时,“按小时分组”会直接偏离业务预期。比如订单表 created_at 存的是 UTC 时间,但你用 date_trunc('hour', created_at) 在东八区服务器上执行,得到的就是 UTC 小时,而非北京时间小时。
- PostgreSQL 中,优先用
created_at AT TIME ZONE 'Asia/Shanghai'显式转换后再截断:date_trunc('hour', created_at AT TIME ZONE 'Asia/Shanghai') - MySQL 没有原生 AT TIME ZONE,得用
CONVERT_TZ(created_at, '+00:00', '+08:00'),注意该函数依赖系统时区表,上线前务必验证是否加载成功 - 别依赖
SET time_zone = '+8:00'——这只影响 NOW() 等函数,对已存的 DATETIME 字段无作用
性能隐患:截断表达式无法利用原始日期索引
在 created_at 上建了 B-tree 索引,但写 GROUP BY date_trunc('hour', created_at) 后执行计划显示全表扫描,就是因为索引无法命中函数计算后的值。
- PostgreSQL 可建函数索引:
CREATE INDEX idx_orders_hourly ON orders (date_trunc('hour', created_at)); - MySQL 8.0+ 支持函数索引,但只接受确定性函数,
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00')符合要求;而NOW()、UUID()这类就不行 - 更稳的方案是冗余一个
hour_start字段(类型为 TIMESTAMP 或 DATETIME),写入时就计算好并索引它,避免每次查询都计算
时区和索引这两块,最容易在线上跑了一阵子才发现数据对不上或响应变慢,动手前先确认字段存的是什么时区、查询要按哪个时区切分、有没有现成索引能复用。

















