MySQL 中优雅统计"只算周一到周五"的到访数据
在实际业务中,我们常常需要统计工作日(周一至周五) 的访问量、订单量或用户活跃度,而剔除周末数据。MySQL 提供了多种日期函数来实现这一需求,但不同方案在可读性、性能、索引利用上差异显著。本文将从基础方法到高级优化,系统讲解如何高效统计工作日数据,并提供生产级建议。
一、业务场景与挑战
假设有一张访问记录表 visits:
sql
CREATE TABLE visits (
id INT PRIMARY KEY AUTO_INCREMENT,
visit_date DATETIME NOT NULL, -- 访问时间
user_id INT,
page VARCHAR(100)
);
需求:统计每天(或某时间段内)工作日的人次,且要求查询高效、支持大数据量。
挑战:
visit_date是DATETIME类型,包含时分秒。- 直接对日期字段使用
WEEKDAY()等函数会导致索引失效。 - 数据量百万级以上时,全表扫描不可接受。
二、基础查询方法对比
MySQL 提供多个函数用于判断星期几,各有特点:
| 函数 | 返回值范围 | 周一对应值 | 周日对应值 | 特点 |
|---|---|---|---|---|
WEEKDAY(date) |
0 ~ 6 | 0 | 6 | 以周一为起点,适合"周一到周五"判断(<=4) |
DAYOFWEEK(date) |
1 ~ 7 | 2 | 1 | 以周日为起点,判断工作日需 BETWEEN 2 AND 6 |
DAYNAME(date) |
字符串 ('Monday'...) | 'Monday' | 'Sunday' | 可读性好,但分组/过滤时需字符串比较 |
DATE_FORMAT(date, '%w') |
0 ~ 6(周日=0) | 1 | 0 | 兼容性一般,周日为0需注意 |
推荐使用 WEEKDAY() ,因为其返回值直接对应"周一=0,周五=4",条件 <=4 语义清晰。
方法1:使用 WEEKDAY() 函数
sql
SELECT
DATE(visit_date) AS visit_day,
COUNT(*) AS visit_count
FROM visits
WHERE WEEKDAY(visit_date) <= 4 -- 0~4 周一到周五
GROUP BY DATE(visit_date)
ORDER BY visit_day;
优点 :简单直观。
缺点 :WEEKDAY(visit_date) 无法使用 visit_date 上的普通索引。
方法2:使用 DAYOFWEEK() 函数
sql
SELECT
DATE(visit_date) AS visit_day,
COUNT(*) AS visit_count
FROM visits
WHERE DAYOFWEEK(visit_date) BETWEEN 2 AND 6 -- 2=周一,6=周五
GROUP BY DATE(visit_date);
两者性能相近,但 WEEKDAY 更贴近中国人"周一为一周第一天"的习惯。
方法3:增加星期名称列(可读性优先)
sql
SELECT
DATE(visit_date) AS visit_day,
CASE WEEKDAY(visit_date)
WHEN 0 THEN '周一'
WHEN 1 THEN '周二'
WHEN 2 THEN '周三'
WHEN 3 THEN '周四'
WHEN 4 THEN '周五'
ELSE '周末'
END AS weekday_cn,
COUNT(*) AS visit_count
FROM visits
WHERE WEEKDAY(visit_date) <= 4
GROUP BY DATE(visit_date);
三、性能优化方案
当数据量达到百万级且查询频繁时,必须解决 函数导致索引失效 的问题。以下按优化程度递增给出三种方案。
3.1 方案一:范围过滤 + 函数计算(小数据量可用)
如果查询时间范围较小(如一周),MySQL 会先根据 visit_date 的索引过滤出该周数据,再计算 WEEKDAY。此时性能尚可。
sql
-- 假设查询2025-05-12到2025-05-18这一周的数据
SELECT DATE(visit_date), COUNT(*)
FROM visits
WHERE visit_date BETWEEN '2025-05-12 00:00:00' AND '2025-05-18 23:59:59'
AND WEEKDAY(visit_date) <= 4
GROUP BY DATE(visit_date);
3.2 方案二:虚拟列 + 函数索引(MySQL 8.0+)
MySQL 8.0 支持函数索引,可以直接在表达式上创建索引,无需修改表结构。
sql
-- 创建函数索引(直接对 WEEKDAY(visit_date) 建立索引)
CREATE INDEX idx_visit_weekday ON visits ((WEEKDAY(visit_date)));
查询时索引会自动生效:
sql
SELECT DATE(visit_date), COUNT(*)
FROM visits
WHERE WEEKDAY(visit_date) <= 4
GROUP BY DATE(visit_date);
⚠️ 注意:函数索引要求 MySQL 8.0.13+,并且函数必须标记为
DETERMINISTIC(如WEEKDAY本身是确定性的)。
3.3 方案三:存储生成列(虚拟列) + 普通索引(兼容更广)
对于 MySQL 5.7 或需要兼容更广泛版本的场景,可以增加一个虚拟列存储星期几,并对该列建立索引。
sql
-- 添加虚拟列(不占用额外存储,实时计算)
ALTER TABLE visits
ADD COLUMN weekday_val TINYINT
GENERATED ALWAYS AS (WEEKDAY(visit_date)) STORED; -- 或 VIRTUAL
-- 为虚拟列创建索引
CREATE INDEX idx_weekday ON visits(weekday_val);
-- 查询时使用虚拟列
SELECT DATE(visit_date), COUNT(*)
FROM visits
WHERE weekday_val <= 4
GROUP BY DATE(visit_date);
STORED:物理存储,占用空间但查询稍快。VIRTUAL:不占用空间,每次读取时计算,但索引仍然可用(8.0 前 VIRTUAL 列索引有限制,建议用 STORED)。
3.4 方案四:预聚合汇总表(终极性能)
如果统计需求固定为"按日统计工作日数据",可以维护一张日汇总表,通过定时任务或触发器增量更新。
sql
CREATE TABLE visits_daily_summary (
visit_date DATE PRIMARY KEY,
total_count INT DEFAULT 0,
weekday_count INT DEFAULT 0,
weekend_count INT DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
增量更新逻辑(每日凌晨执行或每笔写入时更新):
sql
INSERT INTO visits_daily_summary (visit_date, total_count, weekday_count)
SELECT
DATE(visit_date),
COUNT(*),
SUM(WEEKDAY(visit_date) <= 4)
FROM visits
WHERE visit_date >= CURDATE() - INTERVAL 1 DAY
AND visit_date < CURDATE()
GROUP BY DATE(visit_date)
ON DUPLICATE KEY UPDATE
total_count = total_count + VALUES(total_count),
weekday_count = weekday_count + VALUES(weekday_count);
查询时直接读汇总表,毫秒级响应,且完全避免函数计算。
四、方法选择流程图
< 10万 或 临时查询
>= 10万 且 MySQL 8.0+
>= 10万 且 MySQL 5.7
极高并发/实时报表
需要统计工作日访问量
数据量级?
直接使用 WEEKDAY 函数
创建函数索引
'CREATE INDEX idx ON visits ((WEEKDAY(visit_date)))'
添加存储生成列 + 索引
建立预聚合汇总表
查询简单,开发快
无需改表,性能中等
需要改表,但索引高效
性能最佳,需维护 ETL
五、常见陷阱与注意事项
- 时区问题 :
WEEKDAY()基于会话时区,如果数据时间是 UTC,查询时需转换CONVERT_TZ。 - 跨年周 :
WEEKDAY只判断星期几,不关心周数。若需按自然周统计(周一到周日),需结合YEARWEEK()。 - NULL 值 :
visit_date应为NOT NULL,否则WEEKDAY(NULL)返回 NULL,条件不成立。 - 性能误区 :即使使用函数索引,
WHERE WEEKDAY(date) <= 4 AND date BETWEEN ...仍可能部分走索引。应分析EXPLAIN确认。
六、综合示例:统计某月的工作日日均访问量
sql
-- 查询 2025年5月 工作日的日均访问量(使用虚拟列方案)
SELECT
COUNT(*) / COUNT(DISTINCT DATE(visit_date)) AS avg_weekday_visits
FROM visits
WHERE visit_date >= '2025-05-01'
AND visit_date < '2025-06-01'
AND weekday_val <= 4; -- 假设已添加虚拟列并建立索引
七、总结
| 方法 | 适用场景 | 索引利用 | 开发成本 | 维护成本 |
|---|---|---|---|---|
WEEKDAY() 直接过滤 |
临时查询、小表 | ❌ 全表扫描 | 低 | 无 |
| 函数索引 (8.0+) | 中等表,不想改结构 | ✅ 高效 | 中 | 低 |
| 虚拟列 + 索引 | 5.7 环境,中等表 | ✅ 高效 | 中 | 低 |
| 预聚合表 | 大表、高频统计 | ✅ 极快 | 高 | 中(需 ETL) |
推荐策略:
- 开发测试或低并发场景:直接使用
WEEKDAY(),简单可靠。 - 生产环境百万级数据:采用虚拟列 + 索引(兼容性好)。
- 实时大屏或 API 高频调用:使用预聚合表 或物化视图。