用 group by project_id, status + count(*) 统计各项目中每种状态的任务数量,需确保两字段同时分组、状态值规范,并通过条件聚合或字典表补全缺失状态,大表须建复合索引提升性能。

用 GROUP BY + COUNT 统计任务状态分布
直接在项目表和任务表关联后,按 project_id 和 status 两列分组,再用 COUNT(*) 就能拿到每个项目里每种状态的任务数量。别先想着 pivot 或子查询——多数场景下,基础聚合足够且最稳。
常见错误是只按 status 分组,漏掉 project_id,结果变成“全量任务的状态汇总”,而不是“每个项目的分布”。另外,如果某项目没有某个状态的任务(比如没有 'cancelled'),这条记录根本不会出现在结果里——这不是 bug,是 SQL 的默认行为。
- 必须同时
GROUP BY project_id, status,缺一不可 - 确保
status字段不含空格或大小写混用(例如'Done'和'done'会被当两个值) - 若需补全缺失状态(如强制显示 0 条 cancelled),得用
LEFT JOIN配合状态字典表,不是加个COALESCE就能解决
处理 NULL 状态或非法值
status 字段为空或存了非预期值(如 'unknown'、''、NULL)时,COUNT(*) 仍会统计,但语义模糊。建议在 WHERE 或 CASE 中显式归类,避免把脏数据混进业务报表。
例如,把空值统一映射为 'unassigned',再参与分组:
SELECT
project_id,
CASE
WHEN status IS NULL OR TRIM(status) = '' THEN 'unassigned'
ELSE status
END AS status_clean,
COUNT(*) AS task_count
FROM tasks
GROUP BY project_id, status_clean;
-
TRIM(status) = ''要单独判断,因为'' = NULL永远不成立 - 不要依赖应用层清洗:数据库查出来就是脏的,前端再处理容易误传误用
- 如果
status是外键字段,理论上不该出现非法值——检查约束是否启用
想横向展示状态列为宽表?用条件聚合
当需要把每个项目的各状态数量转成一行多列(如 todo_count、in_progress_count、done_count),就得放弃简单 GROUP BY,改用条件聚合。本质是用 SUM(CASE WHEN ... THEN 1 ELSE 0 END) 替代 COUNT。
示例:
SELECT project_id, SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END) AS todo_count, SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress_count, SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS done_count FROM tasks GROUP BY project_id;
- 不能用
COUNT(CASE ...):因为COUNT会忽略NULL,而CASE不匹配时返回NULL,导致计数变少 - 所有状态值必须提前知道并硬编码进 SQL;动态新增状态要改 SQL,不适合高频变更场景
- MySQL 8.0+ 可用
PIVOT,但 PostgreSQL/SQLite 不支持,跨库迁移时容易翻车
性能注意:大表上没索引会很慢
当 tasks 表超过百万行,又没在 (project_id, status) 上建复合索引,GROUP BY 会触发全表扫描+临时文件排序,查询可能从毫秒级拖到十几秒。
执行前先看执行计划:EXPLAIN SELECT ...,确认是否用了索引。没用就加:
CREATE INDEX idx_project_status ON tasks (project_id, status);
- 顺序很重要:
project_id在前,因为通常是查询过滤主维度 - 如果常按时间范围查(如最近一周),可考虑三字段索引:
(project_id, status, created_at) - 别给每个字段单独建索引——
project_id单列索引对这个查询几乎没用
status 常有出入,光靠 SQL 层过滤只能兜底,源头治理得靠字段级枚举约束或应用层校验。











