3数据库系统概念(原书第7版)

SQL 基础

结构化查询语言

SELECTJOIN子查询聚合函数视图

第3章:SQL 基础(Introduction to SQL)

一、导读

1.1 本章学习目标

SQL(Structured Query Language)是关系数据库的标准查询语言。本章讲解 SQL 的基本语法:数据查询、数据定义、数据操纵。

本章的核心学习目标包括:

  • 掌握 SELECT-FROM-WHERE 基本查询结构
  • 理解 JOIN 操作:内连接、外连接
  • 掌握子查询:嵌套查询、相关子查询
  • 理解聚合函数:COUNT、SUM、AVG、MAX、MIN
  • 掌握 GROUP BY 和 HAVING 子句
  • 理解视图的定义和用途

1.2 为什么 SQL 如此重要

SQL 是数据库领域最广泛使用的语言。无论是数据分析师、后端工程师、还是数据科学家,都需要掌握 SQL。理解 SQL 的工作原理,对于编写高效查询、优化数据库性能至关重要。

SQL 自 1986 年成为 ANSI 标准以来,经历了多次修订(SQL:92、SQL:99、SQL:2003 等),每次修订都增加了新的功能,如窗口函数、JSON 支持等。掌握 SQL 不仅是数据库开发的基础,也是数据分析、数据工程等领域的核心技能。

1.3 核心问题引导

在学习本章时,请思考以下问题:

SQL 查询的逻辑执行顺序是什么?为什么理解执行顺序很重要?

JOIN 操作的本质是什么?不同类型的 JOIN 有什么区别?

子查询和 JOIN 可以互相替代吗?性能上有何差异?

NULL 值在 SQL 中的语义是什么?如何正确处理 NULL?


二、核心概念详解

2.1 SELECT-FROM-WHERE 基本查询

SQL 查询的基本结构是 SELECT-FROM-WHERE:

sql
SELECT 列名1, 列名2, ...
FROM 表名
WHERE 条件;

2.1.1 简单查询

sql
SELECT * FROM Student;

SELECT name, gpa
FROM Student
WHERE dept = '计算机';

SELECT name
FROM Student
WHERE gpa > 3.5;

2.1.2 条件组合

sql
SELECT * FROM Student
WHERE dept = '计算机' AND gpa > 3.5;

SELECT * FROM Student
WHERE dept = '计算机' OR dept = '数学';

SELECT * FROM Student
WHERE dept IN ('计算机', '数学', '物理');

SELECT * FROM Student
WHERE gpa BETWEEN 3.5 AND 4.0;

SELECT * FROM Student
WHERE name LIKE '张%';

2.1.3 去重与排序

sql
SELECT DISTINCT dept FROM Student;

SELECT name, gpa
FROM Student
ORDER BY gpa DESC;

SELECT name, gpa
FROM Student
ORDER BY dept ASC, gpa DESC;

DISTINCT 关键字用于消除重复行,ORDER BY 子句用于对结果排序。可以指定升序(ASC)或降序(DESC),默认是升序。

2.2 JOIN 操作

JOIN 用于从多个表中查询数据。

2.2.1 内连接(INNER JOIN)

只返回两个表中匹配的行。

sql
SELECT S.name, C.title
FROM Student S
INNER JOIN Takes T ON S.id = T.student_id
INNER JOIN Course C ON T.course_id = C.id;

2.2.2 左外连接(LEFT OUTER JOIN)

返回左表的所有行,右表不匹配的行用 NULL 填充。

sql
SELECT S.name, C.title
FROM Student S
LEFT OUTER JOIN Takes T ON S.id = T.student_id
LEFT OUTER JOIN Course C ON T.course_id = C.id;

2.2.3 自然连接(NATURAL JOIN)

自动根据同名列进行连接。

sql
SELECT *
FROM Student
NATURAL JOIN Takes;

2.2.4 交叉连接(CROSS JOIN)

返回两个表的笛卡尔积。

sql
SELECT S.name, C.title
FROM Student S
CROSS JOIN Course C;

交叉连接的结果行数等于两个表行数的乘积,通常用于生成所有可能的组合。

2.3 子查询

子查询是嵌套在另一个查询中的查询。

2.3.1 嵌套子查询

sql
SELECT name
FROM Student
WHERE id IN (
    SELECT student_id
    FROM Takes
    WHERE course_id = 'CS101'
);

2.3.2 相关子查询

子查询引用了外层查询的列。

sql
SELECT S1.name, S1.dept, S1.gpa
FROM Student S1
WHERE S1.gpa = (
    SELECT MAX(S2.gpa)
    FROM Student S2
    WHERE S2.dept = S1.dept
);

2.3.3 EXISTS 操作符

sql
SELECT name
FROM Student S
WHERE EXISTS (
    SELECT *
    FROM Takes T
    WHERE T.student_id = S.id
);

2.3.4 标量子查询

标量子查询返回单个值,可以用在 SELECT 列表中。

sql
SELECT name,
       (SELECT AVG(gpa) FROM Student) AS avg_gpa
FROM Student;

2.4 聚合函数

聚合函数对一组值执行计算并返回单个值。

sql
SELECT COUNT(*) FROM Student;
SELECT AVG(gpa) FROM Student;
SELECT SUM(gpa) FROM Student;
SELECT MAX(gpa), MIN(gpa) FROM Student;

SELECT dept, COUNT(*) AS student_count
FROM Student
GROUP BY dept;

需要注意的是,聚合函数会忽略 NULL 值(COUNT(*) 除外)。如果某列全部为 NULL,AVG 的结果是 NULL 而不是 0。

2.5 GROUP BY 和 HAVING

GROUP BY 将结果分组,HAVING 过滤分组。

sql
SELECT dept, COUNT(*) AS student_count
FROM Student
GROUP BY dept
HAVING COUNT(*) > 10;

SELECT dept, AVG(gpa) AS avg_gpa
FROM Student
GROUP BY dept
HAVING AVG(gpa) > 3.5;

GROUP BY 子句中可以使用多个列进行多级分组。HAVING 子句与 WHERE 子句的区别在于:WHERE 在分组前过滤行,HAVING 在分组后过滤组。

2.6 视图

视图是虚拟表,不存储数据,只存储查询定义。

sql
CREATE VIEW CS_Student AS
SELECT *
FROM Student
WHERE dept = '计算机';

SELECT * FROM CS_Student WHERE gpa > 3.5;

DROP VIEW CS_Student;

视图的优点:

  • 简化复杂查询
  • 提供安全性(隐藏敏感数据)
  • 提供逻辑数据独立性

三、深入分析

3.1 SQL 执行顺序

SQL 的逻辑执行顺序是:

FROM(包括 JOIN)

WHERE

GROUP BY

HAVING

SELECT

ORDER BY

LIMIT

理解执行顺序对于编写正确和高效的 SQL 至关重要。例如,WHERE 子句中不能使用 SELECT 中定义的别名,因为 WHERE 在 SELECT 之前执行。

3.2 NULL 值处理

NULL 表示未知或缺失的值。NULL 与任何值的比较结果都是 UNKNOWN。

sql
SELECT * FROM Student WHERE gpa IS NULL;
SELECT * FROM Student WHERE gpa IS NOT NULL;
SELECT COALESCE(gpa, 0) FROM Student;
SELECT NULLIF(dept, '') FROM Student;

三值逻辑(TRUE、FALSE、UNKNOWN)影响 WHERE 和 HAVING 的行为:WHERE 只保留结果为 TRUE 的行。

3.3 字符串与日期操作

sql
SELECT CONCAT(name, ' - ', dept) FROM Student;
SELECT LENGTH(name) FROM Student;
SELECT UPPER(name), LOWER(name) FROM Student;
SELECT SUBSTRING(name, 1, 2) FROM Student;
SELECT CURRENT_DATE, CURRENT_TIME;
SELECT DATE_FORMAT(enroll_date, '%Y-%m') FROM Student;

四、实践案例

4.1 案例一:电商订单分析查询

sql
SELECT C.name,
       COUNT(O.id) AS order_count,
       SUM(O.total_amount) AS total_spent,
       AVG(O.total_amount) AS avg_order
FROM Customer C
INNER JOIN Orders O ON C.id = O.customer_id
WHERE O.order_date >= '2024-01-01'
GROUP BY C.id, C.name
HAVING SUM(O.total_amount) > 10000
ORDER BY total_spent DESC
LIMIT 10;

这个查询综合运用了 JOIN、WHERE、GROUP BY、HAVING、ORDER BY 和 LIMIT,是典型的业务分析场景。

4.2 案例二:使用子查询找出每个部门薪资最高的员工

sql
SELECT E.name, E.salary, E.dept_id
FROM Employee E
WHERE E.salary = (
    SELECT MAX(E2.salary)
    FROM Employee E2
    WHERE E2.dept_id = E.dept_id
);

这个相关子查询对外层查询的每一行都执行一次内层查询,当数据量大时性能较差。可以改写为 JOIN 形式以提高效率。

4.3 案例三:使用 EXPLAIN 分析查询

sql
EXPLAIN SELECT * FROM Student WHERE dept = '计算机';
EXPLAIN SELECT * FROM Student WHERE id = 1;

通过 EXPLAIN 可以查看查询的执行计划,包括是否使用了索引、扫描了多少行等信息,是 SQL 优化的重要工具。


五、常见误区

5.1 误区一:"SELECT * 总是最好的"

SELECT * 会查询所有列,可能导致不必要的 I/O 和网络传输开销。应该只查询需要的列,这也能让数据库优化器选择更优的执行计划(如使用覆盖索引)。

5.2 误区二:"子查询总是比 JOIN 慢"

现代数据库优化器可以将子查询转换为 JOIN,性能差异不大。在某些场景下(如 EXISTS 子查询),子查询可能比等价的 JOIN 更高效,因为可以提前终止搜索。

5.3 误区三:"HAVING 和 WHERE 可以互换"

HAVING 用于过滤分组后的结果,可以使用聚合函数;WHERE 用于过滤分组前的行,不能使用聚合函数。两者在查询执行的不同阶段生效。

5.4 难点:理解相关子查询

相关子查询的执行效率较低,因为外层查询的每一行都要执行一次子查询。可以通过改写为 JOIN 或使用窗口函数来优化。

5.5 难点:理解 GROUP BY 的语义

GROUP BY 后的 SELECT 只能包含分组列和聚合函数。如果 SELECT 中包含非分组列且未被聚合,在严格模式下的数据库(如 MySQL 的 ONLY_FULL_GROUP_BY 模式)会报错。


六、本章小结

第3章讲解了 SQL 的基础知识:

SELECT-FROM-WHERE 是 SQL 查询的基本结构

JOIN 用于从多个表查询数据

子查询 可以嵌套在另一个查询中

聚合函数 对一组值执行计算

GROUP BY 和 HAVING 用于分组和过滤

视图 是虚拟表,简化复杂查询

后续章节将深入讲解 SQL 的高级特性。


七、思考题

解释 SQL 的逻辑执行顺序,并说明为什么 WHERE 子句中不能使用 SELECT 中定义的别名。

比较 INNER JOIN、LEFT OUTER JOIN、RIGHT OUTER JOIN 和 FULL OUTER JOIN 的区别,各举一个适用场景。

什么情况下子查询比 JOIN 更合适?请举例说明。

NULL 值在 SQL 中的语义是什么?为什么 NULL = NULL 的结果不是 TRUE?

编写一个 SQL 查询,找出选修了所有课程的学生(使用除法操作或 NOT EXISTS 实现)。

视图有哪些优点和局限性?视图可以被更新吗?在什么条件下可以更新?

解释 GROUP BY 和 DISTINCT 的区别。在什么情况下它们的结果相同?