在开始SQL练习之前,我们需要先搭建一个完整的练习环境。我推荐使用MySQL 8.0版本,这是目前最稳定的版本之一,支持大多数现代SQL特性。
对于本地开发环境,我建议使用以下两种方式之一:
bash复制docker run --name mysql-practice -e MYSQL_ROOT_PASSWORD=yourpassword -p 3306:3306 -d mysql:8.0
brew install mysqlbash复制sudo apt update
sudo apt install mysql-server
提示:无论选择哪种安装方式,安装完成后都建议运行
mysql_secure_installation进行基本安全配置。
连接MySQL后,我们先创建一个专用的练习数据库:
sql复制CREATE DATABASE sql_practice CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE sql_practice;
选择utf8mb4字符集是为了完整支持所有Unicode字符(包括emoji),这在现代应用中很常见。
我们先创建三个核心表:学生表、课程表和选课表。在设计表结构时,我通常会考虑以下几点:
sql复制-- 学生表
CREATE TABLE students (
student_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL COMMENT '学生姓名',
gender ENUM('男','女') NOT NULL COMMENT '性别',
age TINYINT UNSIGNED COMMENT '年龄',
class VARCHAR(50) COMMENT '班级',
enrollment_date DATE NOT NULL COMMENT '入学日期',
INDEX idx_class (class),
INDEX idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 课程表
CREATE TABLE courses (
course_id INT PRIMARY KEY AUTO_INCREMENT,
course_name VARCHAR(100) NOT NULL COMMENT '课程名称',
credit TINYINT UNSIGNED NOT NULL COMMENT '学分',
teacher VARCHAR(50) COMMENT '授课教师',
INDEX idx_course_name (course_name),
INDEX idx_teacher (teacher)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 选课表
CREATE TABLE enrollments (
enrollment_id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT NOT NULL COMMENT '学生ID',
course_id INT NOT NULL COMMENT '课程ID',
score DECIMAL(5,2) COMMENT '成绩',
enroll_date DATE NOT NULL COMMENT '选课日期',
FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES courses(course_id) ON DELETE CASCADE,
UNIQUE KEY uk_student_course (student_id, course_id),
INDEX idx_score (score)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
为了练习效果,我们需要插入足够多的示例数据。在实际项目中,我通常会准备至少20-30条学生记录和10-15门课程记录,这样能更好地模拟真实场景。
sql复制-- 插入学生数据
INSERT INTO students (name, gender, age, class, enrollment_date) VALUES
('张三', '男', 20, '计算机1班', '2023-09-01'),
('李四', '女', 19, '计算机1班', '2023-09-01'),
('王五', '男', 21, '计算机2班', '2023-09-01'),
('赵六', '女', 20, '计算机2班', '2023-09-01'),
('钱七', '男', 22, '计算机3班', '2023-09-01'),
('孙八', '女', 19, '计算机3班', '2023-09-01'),
('周九', '男', 20, '计算机1班', '2023-09-01'),
('吴十', '女', 21, '计算机2班', '2023-09-01'),
('郑十一', '男', 20, '计算机3班', '2023-09-01'),
('王十二', '女', 19, '计算机1班', '2023-09-01');
-- 插入课程数据
INSERT INTO courses (course_name, credit, teacher) VALUES
('数据库原理', 3, '张老师'),
('数据结构', 4, '李老师'),
('操作系统', 3, '王老师'),
('计算机网络', 3, '赵老师'),
('软件工程', 3, '钱老师'),
('算法设计与分析', 4, '孙老师'),
('计算机组成原理', 3, '周老师');
-- 插入选课数据
INSERT INTO enrollments (student_id, course_id, score, enroll_date) VALUES
(1, 1, 85.5, '2024-02-20'),
(1, 2, 92.0, '2024-02-20'),
(1, 4, 78.0, '2024-02-20'),
(2, 1, 78.0, '2024-02-20'),
(2, 3, 88.5, '2024-02-20'),
(3, 2, 95.0, '2024-02-20'),
(3, 4, 82.5, '2024-02-20'),
(4, 1, 90.0, '2024-02-20'),
(4, 3, 76.5, '2024-02-20'),
(5, 2, 87.0, '2024-02-20'),
(5, 5, 92.5, '2024-02-20'),
(6, 3, 84.0, '2024-02-20'),
(6, 6, 79.5, '2024-02-20'),
(7, 1, 88.0, '2024-02-20'),
(7, 7, 91.0, '2024-02-20'),
(8, 2, 93.5, '2024-02-20'),
(8, 4, 85.0, '2024-02-20'),
(9, 3, 77.0, '2024-02-20'),
(9, 5, 89.5, '2024-02-20'),
(10, 1, 94.0, '2024-02-20'),
(10, 6, 83.0, '2024-02-20');
最基本的查询语句是SELECT,但即使是简单的SELECT也有许多需要注意的地方。
sql复制-- 查询所有学生信息(实际项目中应避免使用SELECT *)
SELECT * FROM students;
-- 更好的写法是指定具体字段
SELECT student_id, name, gender, age, class, enrollment_date
FROM students;
注意:在生产环境中,我强烈建议不要使用
SELECT *,而是明确列出需要的字段。这不仅能减少网络传输量,还能避免表结构变更导致的意外问题。
WHERE子句是SQL中最常用的过滤条件,理解各种运算符的使用非常重要。
sql复制-- 查询年龄大于20岁的学生
SELECT name, age FROM students WHERE age > 20;
-- 查询姓'王'的学生(LIKE模糊查询)
SELECT * FROM students WHERE name LIKE '王%';
-- 查询年龄在19到21之间的学生
SELECT name, age FROM students WHERE age BETWEEN 19 AND 21;
-- 查询计算机1班和计算机3班的学生
SELECT name, class FROM students
WHERE class IN ('计算机1班', '计算机3班');
ORDER BY和LIMIT是处理结果集排序和分页的关键。
sql复制-- 按年龄降序排列
SELECT name, age FROM students ORDER BY age DESC;
-- 按班级升序,同班级按年龄降序
SELECT name, class, age FROM students
ORDER BY class ASC, age DESC;
-- 分页查询(每页3条,查询第2页)
SELECT student_id, name FROM students
ORDER BY student_id
LIMIT 3 OFFSET 3;
-- 或者简写为 LIMIT 3, 3
MySQL提供了多种聚合函数,最常用的有COUNT、SUM、AVG、MAX、MIN等。
sql复制-- 统计学生总数
SELECT COUNT(*) AS total_students FROM students;
-- 计算学生的平均年龄
SELECT AVG(age) AS average_age FROM students;
-- 查询最大和最小年龄
SELECT MAX(age) AS max_age, MIN(age) AS min_age FROM students;
GROUP BY允许我们按照一个或多个列对结果集进行分组。
sql复制-- 查询每个班级的学生人数
SELECT class, COUNT(*) AS student_count
FROM students
GROUP BY class;
-- 查询男生和女生的人数及平均年龄
SELECT gender, COUNT(*) AS count, AVG(age) AS avg_age
FROM students
GROUP BY gender;
-- 查询每个班级不同性别的学生人数
SELECT class, gender, COUNT(*) AS count
FROM students
GROUP BY class, gender
ORDER BY class, gender;
HAVING类似于WHERE,但它用于过滤分组后的结果。
sql复制-- 查询学生人数超过2人的班级
SELECT class, COUNT(*) AS student_count
FROM students
GROUP BY class
HAVING COUNT(*) > 2;
-- 查询平均年龄小于20岁的班级
SELECT class, AVG(age) AS avg_age
FROM students
GROUP BY class
HAVING AVG(age) < 20;
内连接只返回两个表中匹配的行。
sql复制-- 查询每个学生的选课信息
SELECT s.name, c.course_name, e.score
FROM students s
INNER JOIN enrollments e ON s.student_id = e.student_id
INNER JOIN courses c ON e.course_id = c.course_id;
-- 查询选了'数据结构'课程的学生及其成绩
SELECT s.name, e.score
FROM students s
INNER JOIN enrollments e ON s.student_id = e.student_id
INNER JOIN courses c ON e.course_id = c.course_id
WHERE c.course_name = '数据结构';
外连接会返回一个表的所有行,即使在另一个表中没有匹配。
sql复制-- 查询所有学生及其选课情况(包括未选课的学生)
SELECT s.name, c.course_name, e.score
FROM students s
LEFT JOIN enrollments e ON s.student_id = e.student_id
LEFT JOIN courses c ON e.course_id = c.course_id;
-- 查询所有课程及其选课情况(包括无人选的课程)
SELECT c.course_name, COUNT(e.student_id) AS student_count
FROM courses c
LEFT JOIN enrollments e ON c.course_id = e.course_id
GROUP BY c.course_id, c.course_name;
当连接多个表时,查询性能可能会成为问题。以下是一些优化建议:
SELECT *sql复制-- 优化后的查询示例
SELECT s.name, c.course_name, e.score
FROM enrollments e
FORCE INDEX (idx_score) -- 强制使用特定索引
JOIN students s ON e.student_id = s.student_id
JOIN courses c ON e.course_id = c.course_id
WHERE e.score > 85
LIMIT 100;
标量子查询返回单个值,可以用于SELECT、WHERE等子句中。
sql复制-- 查询成绩高于平均分的学生选课信息
SELECT s.name, c.course_name, e.score
FROM students s
INNER JOIN enrollments e ON s.student_id = e.student_id
INNER JOIN courses c ON e.course_id = c.course_id
WHERE e.score > (SELECT AVG(score) FROM enrollments);
-- 查询年龄大于平均年龄的学生
SELECT name, age FROM students
WHERE age > (SELECT AVG(age) FROM students);
行子查询返回单行多列的结果。
sql复制-- 查询与'张三'同龄同班级的学生
SELECT name, age, class FROM students
WHERE (age, class) = (
SELECT age, class FROM students WHERE name = '张三'
)
AND name != '张三';
表子查询返回多行多列的结果,可以当作临时表使用。
sql复制-- 查询选了最多课程的学生
SELECT s.name, COUNT(e.course_id) AS course_count
FROM students s
INNER JOIN enrollments e ON s.student_id = e.student_id
GROUP BY s.student_id, s.name
HAVING COUNT(e.course_id) = (
SELECT MAX(course_count)
FROM (
SELECT COUNT(course_id) AS course_count
FROM enrollments
GROUP BY student_id
) AS temp
);
插入数据时,我建议总是明确指定列名,这样即使表结构变更,SQL也不会出错。
sql复制-- 插入新学生(推荐写法)
INSERT INTO students (name, gender, age, class, enrollment_date)
VALUES ('新学生', '男', 20, '计算机4班', '2024-03-01');
-- 批量插入课程
INSERT INTO courses (course_name, credit, teacher)
VALUES
('人工智能基础', 3, '吴老师'),
('大数据技术', 3, '郑老师');
更新数据时一定要小心,忘记WHERE条件会导致全表更新!
sql复制-- 将'张三'的年龄更新为21岁
UPDATE students
SET age = 21
WHERE name = '张三';
-- 将所有选课成绩低于60分的调整为60分
UPDATE enrollments
SET score = 60
WHERE score < 60;
-- 基于子查询的更新
UPDATE students s
JOIN (
SELECT student_id, AVG(score) AS avg_score
FROM enrollments
GROUP BY student_id
) e ON s.student_id = e.student_id
SET s.status = '优秀'
WHERE e.avg_score >= 90;
删除操作是不可逆的,执行前最好先使用SELECT确认要删除的数据。
sql复制-- 删除没有选任何课程的学生
DELETE FROM students
WHERE student_id NOT IN (
SELECT DISTINCT student_id FROM enrollments
);
-- 删除成绩低于70分的选课记录
DELETE FROM enrollments
WHERE score < 70;
MySQL 8.0引入了窗口函数,极大增强了分析能力。
sql复制-- 查询每个学生的成绩排名(按课程)
SELECT
s.name,
c.course_name,
e.score,
RANK() OVER (PARTITION BY c.course_id ORDER BY e.score DESC) AS rank_in_course
FROM students s
INNER JOIN enrollments e ON s.student_id = e.student_id
INNER JOIN courses c ON e.course_id = c.course_id;
-- 查询每个学生的最好成绩
WITH student_best_scores AS (
SELECT
student_id,
FIRST_VALUE(score) OVER (
PARTITION BY student_id
ORDER BY score DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS best_score
FROM enrollments
GROUP BY student_id, score
)
SELECT s.name, sbs.best_score
FROM students s
JOIN student_best_scores sbs ON s.student_id = sbs.student_id
GROUP BY s.student_id, s.name, sbs.best_score;
CTE可以使复杂查询更易读和维护。
sql复制-- 使用CTE统计各班级的平均成绩
WITH class_scores AS (
SELECT s.class, c.course_name, e.score
FROM students s
INNER JOIN enrollments e ON s.student_id = e.student_id
INNER JOIN courses c ON e.course_id = c.course_id
)
SELECT class, AVG(score) AS class_average
FROM class_scores
GROUP BY class
ORDER BY class_average DESC;
EXPLAIN是优化SQL查询的重要工具。
sql复制EXPLAIN SELECT s.name, c.course_name, e.score
FROM students s
INNER JOIN enrollments e ON s.student_id = e.student_id
INNER JOIN courses c ON e.course_id = c.course_id
WHERE s.class = '计算机1班' AND e.score > 80;
合理的索引可以大幅提高查询性能。
sql复制-- 添加复合索引
ALTER TABLE students ADD INDEX idx_class_name (class, name);
-- 为经常用于筛选和排序的字段添加索引
ALTER TABLE enrollments ADD INDEX idx_student_score (student_id, score);
-- 查看表索引
SHOW INDEX FROM students;
注意:索引不是越多越好,每个索引都会增加写入时的开销。通常只为高频查询条件和JOIN字段添加索引。
sql复制SELECT
s.name,
s.class,
COUNT(e.course_id) AS course_count,
AVG(e.score) AS average_score,
MAX(e.score) AS highest_score,
MIN(e.score) AS lowest_score,
CASE
WHEN AVG(e.score) >= 90 THEN '优秀'
WHEN AVG(e.score) >= 80 THEN '良好'
WHEN AVG(e.score) >= 70 THEN '中等'
WHEN AVG(e.score) >= 60 THEN '及格'
ELSE '不及格'
END AS grade_level
FROM students s
LEFT JOIN enrollments e ON s.student_id = e.student_id
GROUP BY s.student_id, s.name, s.class
ORDER BY average_score DESC;
sql复制SELECT
c.teacher,
c.course_name,
COUNT(e.student_id) AS student_count,
AVG(e.score) AS average_score,
SUM(CASE WHEN e.score >= 90 THEN 1 ELSE 0 END) AS excellent_count,
SUM(CASE WHEN e.score < 60 THEN 1 ELSE 0 END) AS fail_count
FROM courses c
LEFT JOIN enrollments e ON c.course_id = e.course_id
GROUP BY c.teacher, c.course_name
ORDER BY average_score DESC;
sql复制-- 基于同学选课情况推荐课程
SELECT
s.name AS student_name,
rec_course.course_name AS recommended_course,
classmate_count.classmate_count,
avg_score.avg_score
FROM students s
JOIN (
-- 同班级同学选的课程
SELECT e.course_id, s2.class, COUNT(*) AS classmate_count
FROM enrollments e
JOIN students s2 ON e.student_id = s2.student_id
GROUP BY e.course_id, s2.class
) classmate_count ON classmate_count.class = s.class
JOIN courses rec_course ON rec_course.course_id = classmate_count.course_id
LEFT JOIN (
-- 课程平均分
SELECT course_id, AVG(score) AS avg_score
FROM enrollments
GROUP BY course_id
) avg_score ON avg_score.course_id = rec_course.course_id
LEFT JOIN enrollments e ON e.student_id = s.student_id AND e.course_id = rec_course.course_id
WHERE e.course_id IS NULL -- 排除已选课程
ORDER BY s.student_id, classmate_count.classmate_count DESC, avg_score.avg_score DESC;
问题:查询速度突然变慢
可能原因:
解决方案:
问题:外键约束失败
可能原因:
解决方案:
基础阶段:
中级阶段:
高级阶段:
书籍:
在线平台:
文档:
在实际工作中,我发现SQL能力的提升主要靠实践。建议读者在学习理论的同时,多动手解决实际问题,逐步积累经验。遇到问题时,学会阅读执行计划和查阅官方文档是非常重要的技能。