site logo

Marico's space

SQL 日期和时间函数:真实查询实战指南

算法解析 2026-05-04 21:00:33 9

处理日期和时间这件事,看起来很简单,但当你盯着一个时间戳字段发愁,不知道为什么"最近30天"的查询结果总是莫名其妙时,就会意识到它其实相当棘手。日期和时间函数对于数据分析、报表生成以及任何涉及时间序列事件追踪的应用来说都至关重要——但不同数据库引擎之间的细微差异,连经验丰富的开发者也会栽跟头。

今天这篇文章,我们就来好好聊聊 PostgreSQL 和 MySQL 这两大主流数据库中最实用的日期/时间函数,全部都配上真实场景的例子。学完之后,你就知道怎么自信地按时间范围筛选、计算时长、按周期分组数据,以及避开那些最常见的坑了。

说实话,日期时间处理是我在代码审查中最常发现问题的领域之一。很多看似正确的查询,运行起来却要么性能拉胯,要么结果根本不对。所以掌握这些基础概念真的很有必要。


为什么日期时间函数这么重要

想想看有多少查询都跟时间有关:"查看最近7天内的订单"、"计算用户成为会员的时长"、"按月份统计营收"。几乎所有有价值的业务问题都有一个时间维度。

真正的挑战在于,不同的 SQL 数据库并不讲同一套"日期方言"。PostgreSQL 有 DATE_TRUNCAGE,MySQL 有 DATE_FORMATTIMESTAMPDIFF,标准 SQL 则给了我们 EXTRACTCURRENT_TIMESTAMP。知道哪些函数可用、各自有什么差异,能帮你省下好几个小时的调试时间。

我们整个教程会用一个统一的示例:一个电商数据库,包含 customers(用户)和 orders(订单)两张表。

-- 示例表结构
CREATE TABLE customers (
  customer_id   INT PRIMARY KEY,
  name          VARCHAR(100),
  joined_at     TIMESTAMP NOT NULL
);

CREATE TABLE orders (
  order_id      INT PRIMARY KEY,
  customer_id   INT REFERENCES customers(customer_id),
  placed_at     TIMESTAMP NOT NULL,
  shipped_at    TIMESTAMP,
  total_amount  DECIMAL(10,2)
);

1. 获取当前日期和时间

最基本的需求:"现在几点了?"主流数据库都支持这些函数,但语法略有不同:

-- PostgreSQL
SELECT NOW();                  -- 带时区的时间戳
SELECT CURRENT_TIMESTAMP;      -- 等同于NOW(),符合SQL标准
SELECT CURRENT_DATE;           -- 仅日期
SELECT CURRENT_TIME;           -- 仅时间

-- MySQL
SELECT NOW();                  -- 当前日期时间
SELECT CURDATE();              -- 仅日期
SELECT CURTIME();              -- 仅时间
SELECT UTC_TIMESTAMP();        -- UTC时间(这个很实用!)

实战场景:查询今天的所有订单。

-- PostgreSQL
SELECT order_id, placed_at, total_amount
FROM orders
WHERE placed_at::date = CURRENT_DATE;

-- MySQL
SELECT order_id, placed_at, total_amount
FROM orders
WHERE DATE(placed_at) = CURDATE();

小贴士:把时间戳转成日期(或者用 DATE() 函数)会强制数据库在比较前丢弃时间部分。否则 placed_at = CURRENT_DATE 这种写法会失败——因为时间戳 2026-04-28 14:33:00 和日期 2026-04-28 根本不相等。


2. 从日期中提取特定部分

有时候你只需要从时间戳里取出年份、月份或者小时。EXTRACT 是符合 SQL 标准的做法,PostgreSQL 和 MySQL 都支持:

-- 标准SQL语法 — PostgreSQL和MySQL通用
SELECT
  order_id,
  placed_at,
  EXTRACT(YEAR  FROM placed_at) AS order_year,
  EXTRACT(MONTH FROM placed_at) AS order_month,
  EXTRACT(DOW   FROM placed_at) AS day_of_week,  -- 0=周日(PostgreSQL)
  EXTRACT(HOUR  FROM placed_at) AS order_hour
FROM orders;

实战场景:按月分组统计订单,观察月度营收趋势。

SELECT
  EXTRACT(YEAR  FROM placed_at) AS yr,
  EXTRACT(MONTH FROM placed_at) AS mo,
  COUNT(*)                      AS order_count,
  SUM(total_amount)             AS revenue
FROM orders
GROUP BY yr, mo
ORDER BY yr, mo;

MySQL 专属的快捷函数:MySQL 还提供了一些专用的提取函数,用起来更直观:

-- MySQL专用快捷函数
SELECT
  YEAR(placed_at)    AS order_year,
  MONTH(placed_at)   AS order_month,
  DAY(placed_at)     AS order_day,
  HOUR(placed_at)    AS order_hour,
  DAYNAME(placed_at) AS weekday_name   -- 比如 "Tuesday"
FROM orders;

3. 使用 INTERVAL 进行日期运算

加减时间是常见需求:"最近30天的所有订单"、"未来7天内即将到期的订阅"等等。

-- PostgreSQL: 用INTERVAL关键字
SELECT * FROM orders
WHERE placed_at >= NOW() - INTERVAL '30 days';

-- MySQL: 用DATE_SUB()或者INTERVAL语法
SELECT * FROM orders
WHERE placed_at >= NOW() - INTERVAL 30 DAY;

-- MySQL另一种写法:
SELECT * FROM orders
WHERE placed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY);

INTERVAL 支持多种时间单位:

-- PostgreSQL示例
NOW() + INTERVAL '1 year'
NOW() - INTERVAL '3 months'
NOW() + INTERVAL '2 hours 30 minutes'
'2026-01-01'::date + INTERVAL '90 days'

-- MySQL示例
DATE_ADD(NOW(), INTERVAL 1 YEAR)
DATE_ADD(NOW(), INTERVAL -3 MONTH)
DATE_ADD(shipped_at, INTERVAL 14 DAY)   -- 预计送达日期

实战场景:标记发货超过14天但没有后续跟进记录的订单。

-- PostgreSQL
SELECT order_id, customer_id, shipped_at
FROM orders
WHERE shipped_at IS NOT NULL
  AND shipped_at < NOW() - INTERVAL '14 days';

4. 计算两个日期之间的差值

两个日期相差多少天?用户成为会员多久了?这些问题需要日期差值函数来解决。

DATEDIFF(MySQL)

-- MySQL: DATEDIFF返回天数差
SELECT
  order_id,
  placed_at,
  shipped_at,
  DATEDIFF(shipped_at, placed_at) AS days_to_ship
FROM orders
WHERE shipped_at IS NOT NULL;

TIMESTAMPDIFF(MySQL)

TIMESTAMPDIFF 更灵活——你可以指定返回的单位:

-- MySQL: 自行指定单位
SELECT
  c.customer_id,
  c.name,
  c.joined_at,
  TIMESTAMPDIFF(YEAR,  c.joined_at, NOW()) AS years_as_customer,
  TIMESTAMPDIFF(MONTH, c.joined_at, NOW()) AS months_as_customer,
  TIMESTAMPDIFF(DAY,   c.joined_at, NOW()) AS days_as_customer
FROM customers c;

AGE(PostgreSQL)

PostgreSQL 有一个优雅的 AGE() 函数,返回人类可读的间隔:

-- PostgreSQL
SELECT
  customer_id,
  name,
  joined_at,
  AGE(NOW(), joined_at)          AS customer_age,  -- 比如 "2 years 3 mons 12 days"
  EXTRACT(DAY FROM AGE(NOW(), joined_at))           AS days_as_customer
FROM customers;

实战场景:找出注册超过1年但最近90天内没有任何订单的用户(适合做唤醒营销的目标群体)。

-- PostgreSQL
SELECT
  c.customer_id,
  c.name,
  c.joined_at,
  MAX(o.placed_at) AS last_order_date
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE c.joined_at < NOW() - INTERVAL '1 year'
GROUP BY c.customer_id, c.name, c.joined_at
HAVING MAX(o.placed_at) < NOW() - INTERVAL '90 days'
    OR MAX(o.placed_at) IS NULL;

5. 使用 DATE_TRUNC 截断日期(PostgreSQL)

DATE_TRUNC 是 PostgreSQL 的一大神器,专门用于按时间段分组数据。它会把时间戳向下取整到指定周期的起始点:

SELECT DATE_TRUNC('month', NOW());
-- 结果: 2026-04-01 00:00:00+00

SELECT DATE_TRUNC('week',  NOW());
-- 结果: 2026-04-27 00:00:00+00(本周第一天,周一)

SELECT DATE_TRUNC('year',  NOW());
-- 结果: 2026-01-01 00:00:00+00

支持的时间精度包括:microsecondsmillisecondssecondminutehourdayweekmonthquarteryeardecadecentury

实战场景:生成周报,按周对齐统计营收:

-- PostgreSQL
SELECT
  DATE_TRUNC('week', placed_at)  AS week_start,
  COUNT(*)                        AS orders,
  SUM(total_amount)               AS revenue,
  AVG(total_amount)               AS avg_order_value
FROM orders
WHERE placed_at >= NOW() - INTERVAL '12 weeks'
GROUP BY DATE_TRUNC('week', placed_at)
ORDER BY week_start;

在 MySQL 中,可以用 DATE_FORMAT 近似实现:

-- MySQL: 用DATE_FORMAT按周分组
SELECT
  DATE_FORMAT(placed_at, '%Y-%u')  AS year_week,  -- 比如 "2026-17"
  COUNT(*)                          AS orders,
  SUM(total_amount)                 AS revenue
FROM orders
WHERE placed_at >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
GROUP BY DATE_FORMAT(placed_at, '%Y-%u')
ORDER BY year_week;

6. 格式化日期用于展示

原始时间戳对用户不友好。需要在报表和接口中做格式化:

-- PostgreSQL: TO_CHAR
SELECT TO_CHAR(placed_at, 'Month DD, YYYY')   AS display_date;  -- "April 28, 2026"
SELECT TO_CHAR(placed_at, 'YYYY-MM-DD HH24:MI') AS display_dt;   -- "2026-04-28 14:33"
SELECT TO_CHAR(placed_at, 'Day')               AS weekday;       -- "Tuesday"

-- MySQL: DATE_FORMAT
SELECT DATE_FORMAT(placed_at, '%M %d, %Y')     AS display_date;  -- "April 28, 2026"
SELECT DATE_FORMAT(placed_at, '%Y-%m-%d %H:%i') AS display_dt;   -- "2026-04-28 14:33"
SELECT DATE_FORMAT(placed_at, '%W')             AS weekday;       -- "Tuesday"

常见错误和坑

1. 对索引列使用函数会让查询性能崩掉

如果 placed_at 上有索引,下面这种写法就完全用不上索引:

-- ❌ 对列套函数会阻止索引生效
WHERE DATE(placed_at) = CURDATE()              -- MySQL
WHERE DATE_TRUNC('day', placed_at) = ...       -- PostgreSQL

正确做法是用范围比较,让列保持原样:

-- ✅ 范围过滤 — placed_at上的索引可以正常使用
WHERE placed_at >= '2026-04-28 00:00:00'
  AND placed_at <  '2026-04-29 00:00:00'

-- 或者动态写法:
WHERE placed_at >= CURRENT_DATE
  AND placed_at <  CURRENT_DATE + INTERVAL '1 day'

2. 忽略时区问题

PostgreSQL 的 NOW() 返回的是 timestamptz(带时区)。如果你的应用存的是 UTC 时间,但数据库会话用的是本地时区,结果可能会大相径庭。一定要明确指定:

-- PostgreSQL: 强制使用UTC
WHERE placed_at >= NOW() AT TIME ZONE 'UTC' - INTERVAL '24 hours'

在 MySQL 中,需要 UTC 时间时用 UTC_TIMESTAMP() 而不是 NOW(),这样不受会话设置影响。

3. NULL 的 shipped_at 会让时长计算出岔子

如果 shipped_at 是 NULL(订单还没发货),用 DATEDIFF 或直接相减都会返回 NULL——不是报错,就是静默的 NULL 传播。一定要做处理:

-- MySQL
SELECT
  order_id,
  COALESCE(DATEDIFF(shipped_at, placed_at), 'Not yet shipped') AS fulfillment_days
FROM orders;

4. DATEDIFF 的参数顺序因数据库而异

  • MySQL:DATEDIFF(结束日期, 开始日期) → 结束在开始之后则为正
  • SQL Server:DATEDIFF(单位, 开始日期, 结束日期) → 注意单位在最前面!
  • PostgreSQL:直接相减:结束日期 - 开始日期

在数据库之间切换的开发者经常在这里翻车。


总结:核心要点

在 SQL 中处理日期是一项通用技能,几乎对你写的每一条分析或报表查询都有帮助。需要记住的核心概念:

  • NOW() / CURRENT_TIMESTAMP 获取当前时刻;用 CURRENT_DATE 获取今天的日期
  • EXTRACT() 提取特定日期部分(年、月、周几)——符合 SQL 标准
  • INTERVAL 做日期运算——WHERE 子句中保持索引列干净,不要套函数
  • MySQL 用 DATEDIFF / TIMESTAMPDIFF,PostgreSQL 用 AGE() 或直接相减来处理时长计算
  • PostgreSQL 的 DATE_TRUNC 是按周/月/季度分组的好帮手
  • MySQL 用 DATE_FORMAT,PostgreSQL 用 TO_CHAR 来格式化日期展示
  • 始终考虑 NULL 和时区问题——大多数日期相关的 bug 都来自这两个坑

你最喜欢用哪个日期函数?

日期函数看起来平平无奇,但威力其实被大大低估了。一旦你熟练掌握了 DATE_TRUNC 做时间序列聚合,或者用 TIMESTAMPDIFF 做用户群组分析,你的 SQL 查询就能从"能用"进化到"真正有洞察力"。

欢迎在评论区留言——你平时最常用哪种日期/时间查询模式?很想看看大家都在解决什么样的实际问题!如果觉得这篇文章有用,转发给身边正在跟"最近30天"筛选条件较劲的同事吧。