本文介绍如何在 mysql 中高效统计“未被删除的用户”与“已激活且未被删除的课程”之间的关联数量,通过优化 join 顺序、复合索引设计及必要时的索引提示,显著提升 3000 万级参与者表的聚合查询性能。
在处理 courses(约 3 万行)、users(约 3 万行)和 participants(约 3000 万行)三表关联统计时,核心目标是:快速计算满足以下全部条件的唯一用户数:
直觉上,先过滤再连接(filter-before-join)比全量 JOIN 后 WHERE 更高效——而 MySQL 的查询优化器能否自动实现这一点,高度依赖索引结构与统计信息准确性。
SELECT COUNT(DISTINCT p.participant_id) FROM courses AS c INNER JOIN participants AS p ON c.id = p.course_id INNER JOIN users AS u ON p.participant_id = u.id WHERE u.deleted_at IS NULL AND c.active = 1 AND c.deleted_at IS NULL AND p.participant_type = 'Eloomi\\Models\\User';
该写法逻辑直观,且为 MySQL 优化器提供了明确的过滤路径。关键在于:让最小的、过滤性最强的表(courses)成为驱动表——因 active = 1 AND deleted_at IS NULL 可能仅保留少量活跃课程,大幅减少后续连接基数。
为使上述查询真正高效,需建立以下三个针对性索引(注意列序!):
-- 1. 优化 courses 过滤:覆盖 WHERE 条件,并隐含主键用于 JOIN ALTER TABLE courses ADD KEY idx_active_deleted (active, deleted_at); -- 2. 优化 participants 连接:以 course_id 驱动,同时覆盖 participant_type 过滤, -- 并包含 participant_id(供后续 JOIN users 使用) ALTER TABLE participants ADD KEY idx_course_type_id (course_id, participant_type, participant_id); -- 3. 优化 users 连接:确保 participant_id → users.id 查找高效, -- 同时支持 deleted_at 过滤(避免回表) ALTER TABLE users ADD KEY idx_id_deleted (id, deleted_at);
? 原理说明:idx_active_deleted 是一个覆盖索引,MySQL 可仅扫描索引即可完成 courses 表的过滤与主键获取;idx_course_type_id 让 participants 能基于 course_id 快速定位,再用 participant_type 精筛,最后直接拿到 participant_id;idx_id_deleted 则确保 u.id = ? 查找时能顺便验证 deleted_at IS NULL,无需回查数据行。
若 EXPLAIN 显示 users 表仍走主键(PRIMARY)而非 idx_id_deleted,可显式提示(index hint):
SELECT COUNT(DISTINCT p.participant_id) FROM courses AS c INNER JOIN participants AS p ON c.id = p.course_id INNER JOIN users AS u USE INDEX (idx_id_deleted) ON p.participant_id = u.id WHERE u.deleted_at IS NULL AND c.active = 1 AND c.deleted_at IS NULL AND p.participant_type = 'Eloomi\\Models\\User';
⚠️ 注意:USE INDEX 是强提示,仅在确认索引更优时使用;过度依赖可能掩盖统计信息陈旧等问题。建议配合 ANALYZE TABLE courses, participants, users; 更新统计信息。
始终运行 EXPLAIN FORMAT=TREE(MySQL 8.0+)或 EXPLAIN(5.7),观察:
分步验证过滤效果:
-- 检查活跃课程数量(应远小于 total) SELECT COUNT(*) FROM courses WHERE active = 1 AND deleted_at IS NULL; -- 检查关联用户类型分布 SELECT participant_type, COUNT(*) FROM participants GROUP BY participant_type;
对比执行时间:在生产数据副本上测试优化前后耗时(启用 SQL_NO_CACHE 或重启 MySQL 清理查询缓存)。
最终,合理的 JOIN 逻辑 + 精准的复合索引 + 必要的执行计划干预,可在毫秒至秒级完成该统计,无需将复杂性下沉至应用层。