学习前提

完成SQL分组、连接与NULL章节;能说明一行报表代表什么。

本课完成后你能做什么

通过唯一约束与分层聚合生成不会重复计数的进度报表。

先理解机制

先定义粒度:lessons一行代表一章,completed一行代表一个用户完成一章。联合主键(user_id,lesson_id)保证同一用户不能重复登记同一章。

WITH为查询命名中间结果。totals先统计每门课的总章节,done仅统计目标用户的完成章节,再把两个课程级结果连接回courses,避免把原始多表明细相乘。

空课程不能除以0,因此CASE明确返回0%。100.0确保实数除法,ROUND控制展示精度。这里进度以章节数量计算,不按时长加权,产品规则应在报表旁说明。

可运行示例

本系列使用SQLite 3方言。打开一个全新的内存练习数据库,例如终端执行 sqlite3 :memory:,再执行本课整段SQL。每章都自带建表和样例数据,不连接网站业务数据库。

PRAGMA foreign_keys=ON;
CREATE TABLE courses(id INTEGER PRIMARY KEY,name TEXT NOT NULL);
CREATE TABLE lessons(id INTEGER PRIMARY KEY,course_id INTEGER NOT NULL REFERENCES courses(id));
CREATE TABLE completed(user_id INTEGER NOT NULL,lesson_id INTEGER NOT NULL REFERENCES lessons(id),PRIMARY KEY(user_id,lesson_id));
INSERT INTO courses VALUES(1,'Python'),(2,'SQL'),(3,'Java');
INSERT INTO lessons VALUES(1,1),(2,1),(3,1),(4,2),(5,2);
INSERT INTO completed VALUES(7,1),(7,2),(8,3),(7,4);
WITH totals AS (
 SELECT course_id,COUNT(*) AS total FROM lessons GROUP BY course_id
), done AS (
 SELECT l.course_id,COUNT(*) AS finished FROM completed p
 JOIN lessons l ON l.id=p.lesson_id WHERE p.user_id=7 GROUP BY l.course_id
)
SELECT c.name,COALESCE(t.total,0) AS total,COALESCE(d.finished,0) AS finished,
 CASE WHEN COALESCE(t.total,0)=0 THEN 0.0
 ELSE ROUND(100.0*COALESCE(d.finished,0)/t.total,1) END AS percent
FROM courses c LEFT JOIN totals t ON t.course_id=c.id
LEFT JOIN done d ON d.course_id=c.id ORDER BY c.id;

推演与验证

用户7的Python完成2/3即66.7%,SQL完成1/2即50%,Java没有章节为0/0并按约定显示0%。用户8的记录不计入用户7。

常见错误与边界

生产系统还需用户表、授权验证和参数绑定。把WHERE中的7换成当前用户参数并不自动完成权限控制,本课只演示数据报表。

动手练习

把目标用户改为8,再改为没有记录的9,推演所有课程结果。

参考解法与理由

用户8的Python为1/3、33.3%,SQL为0/2、0%,Java为0%。用户9三门课完成数均为0,总章节数保持3、2、0,不应被用户筛选改变。

进一步阅读