SQL详细语法教程(四)约束和多表查询
以下是对 MySQL 语法 按分类的 超详细拆解,从概念本质、语法细节、执行原理到复杂场景,逐层深挖,帮你彻底吃透这些核心知识点:
一、基础 - 约束体系(数据完整性的「规则引擎」)
1. 约束的本质:数据的「法律条文」
- 约束是 MySQL 强制数据遵循的规则,直接干预表的增 / 删 / 改操作,确保数据满足业务逻辑(如订单表的「金额必须 > 0」)。
- 核心价值:预防脏数据(无效 / 矛盾 / 冗余数据),让表结构「自解释业务规则」,避免依赖程序层校验。
2. 约束类型全解析(带极端场景 + 底层逻辑)
(1)主键约束(PRIMARY KEY)
- 绝对唯一性:表中每行必须有唯一标识,且值非空 + 不重复(相当于「身份证号」)。
- 物理存储优化:InnoDB 会自动基于主键建立 聚簇索引(数据按主键顺序存储),查询时直接定位数据页,速度极快。
- 语法细节:
-- 单字段主键(最常用) CREATE TABLE Employees (EmployeeID INT AUTO_INCREMENT, PRIMARY KEY (EmployeeID) -- 隐式 NOT NULL + 唯一 );-- 复合主键(多字段联合唯一,如「订单 + 商品」关联表) CREATE TABLE OrderItems (OrderID INT, ProductID INT, PRIMARY KEY (OrderID, ProductID) -- 两字段组合唯一 );-- Alter 新增主键(注意:已有数据必须满足唯一性,否则报错) ALTER TABLE Users ADD PRIMARY KEY (UserID);
- 极端场景:
- 若表已有数据含重复值,
ALTER ADD PRIMARY KEY
会直接失败,需先清理重复数据。 - 主键字段尽量用 INT/BIGINT(占空间小,索引效率高),避免用 UUID(字符串索引大,查询慢)。
- 若表已有数据含重复值,
(2)唯一性约束(UNIQUE)
- 逻辑约束:保证字段 / 字段组合的值唯一,但允许 NULL(与主键的区别:主键不允许 NULL,一个表只能有 1 个主键,但可多个 UNIQUE)。
- 索引实现:MySQL 会自动为 UNIQUE 字段建立 唯一索引(但不组织数据存储,仅用于校验)。
- 语法细节:
-- 单字段唯一(如用户邮箱) CREATE TABLE Users (Email VARCHAR(50) UNIQUE -- 隐式创建唯一索引 );-- 命名约束(方便删除/修改,推荐!) CREATE TABLE Products (ProductCode VARCHAR(20),CONSTRAINT unique_product_code UNIQUE (ProductCode) -- 显式命名约束 );-- 复合唯一(如「地区 + 门店编号」唯一) CREATE TABLE Stores (Region VARCHAR(20),StoreNo VARCHAR(10),UNIQUE (Region, StoreNo) -- 两字段组合唯一 );
- 与主键的区别:
- 主键是「物理 + 逻辑」约束(影响存储结构),UNIQUE 仅逻辑约束(数据存储无变化)。
- 主键字段不允许 NULL,UNIQUE 允许(NULL 不算重复,如多个 NULL 可共存)。
(3)外键约束(FOREIGN KEY)
- 跨表关联:建立表与表的「父子关系」,子表(外键表)的字段值必须在父表(主键表)中存在,或为 NULL(若外键允许)。
- 底层原理:
- 父表删除 / 更新记录时,子表关联数据的行为由 外键动作(ON DELETE/UPDATE)决定。
- InnoDB 会自动为外键字段建立 普通索引(加速关联查询)。
- 语法细节(关联 Departments 父表):
CREATE TABLE Employees (EmployeeID INT AUTO_INCREMENT,DepartmentID INT,PRIMARY KEY (EmployeeID),-- 外键关联,父表是 Departments,字段是 DepartmentIDFOREIGN KEY (DepartmentID) REFERENCES Departments(DepartmentID) -- 可选:外键动作(控制父表数据变更时,子表的行为)ON DELETE CASCADE -- 父表删除,子表关联数据自动删除ON UPDATE CASCADE -- 父表更新,子表关联数据自动更新 );
- 外键动作(关键!):
- RESTRICT(默认):父表数据有子表关联时,禁止删除 / 更新(报错阻止)。
- CASCADE:父表删除 / 更新,子表关联数据自动级联删除 / 更新(适合「强关联」场景,如订单删除,订单项也删)。
- SET NULL:父表删除 / 更新,子表外键字段设为 NULL(需外键字段允许 NULL,适合「弱关联」场景,如部门解散,员工部门字段置空)。
- NO ACTION:同 RESTRICT(SQL 标准语法,效果一样)。
(4)检查约束(CHECK)
- 逻辑校验:自定义条件校验字段值(如
Salary > 0
,Age BETWEEN 18 AND 60
)。 - MySQL 特殊点:MySQL 8.0 前,
CHECK
只是「语法糖」,不实际校验;8.0 后真正生效(严格校验)。 - 语法细节:
CREATE TABLE Employees (Salary DECIMAL(10,2),-- 8.0+ 严格校验:工资必须>0CHECK (Salary > 0) );-- 复杂条件(如邮箱格式,实际建议程序层校验,这里仅演示) CREATE TABLE Users (Email VARCHAR(50),CHECK (Email LIKE '%@%.%') -- 简单邮箱格式校验 );
- 历史坑点:MySQL 5.7 及以下,
CHECK
写了也没用,需用触发器 / 程序层实现校验。
(5)默认约束(DEFAULT)
- 插入兜底值:插入数据时,若字段未赋值,自动填充默认值(如
CREATE_TIME DEFAULT NOW()
)。 - 语法细节:
CREATE TABLE Orders (OrderStatus VARCHAR(20) DEFAULT '待支付', -- 未指定状态时,默认「待支付」CreateTime DATETIME DEFAULT CURRENT_TIMESTAMP -- 自动填充当前时间 );
- 注意:
DEFAULT
仅在 INSERT 未显式赋值 时生效,若赋值为NULL
(且字段允许 NULL),则存 NULL,不触发 DEFAULT。
二、基础 - 多表查询(关联数据的「拼图魔法」)
1. 多表查询的本质:关系数据库的核心能力
- 解决 「数据分散在多张表,但业务需要整合结果」 的问题(如「查订单时,同时获取用户信息 + 商品信息」)。
- 核心逻辑:通过「连接条件」(JOIN ON)匹配表之间的关联字段,将多张表「拼接」成临时结果集。
2. 子查询分类(按返回结果形态)
(1)行子查询(返回 1 行数据)
- 本质:子查询结果是「单行多列」,可用于 精准匹配(如对比某行的多字段值)。
- 语法 + 场景:
-- 需求:查与「员工 123」工资、部门都相同的其他员工 SELECT * FROM Employees WHERE (Salary, DepartmentID) = ( -- 匹配两行的多字段SELECT Salary, DepartmentID FROM Employees WHERE EmployeeID = 123 ) AND EmployeeID != 123; -- 排除自己
- 执行原理:子查询先返回 1 行(Salary=10000, DepartmentID=5),主查询用
(Salary, DepartmentID) = (10000,5)
筛选。
(2)表子查询(返回 1 个临时表)
- 本质:子查询结果是「多行多列」,可当作 临时表 参与主查询(常用于复杂过滤后,再关联其他表)。
- 语法 + 场景:
-- 需求:查「工资前 10 员工」的详细信息(含部门名称) SELECT e.*, d.DepartmentName FROM (-- 子查询:先筛选工资前 10 的员工(临时表)SELECT * FROM Employees ORDER BY Salary DESC LIMIT 10 ) AS e -- 给临时表起别名 e JOIN Departments d ON e.DepartmentID = d.DepartmentID; -- 再关联部门表
- 执行顺序:先执行子查询生成临时表(10 行),再与 Departments 表 JOIN,效率比直接多表 JOIN 高(减少关联数据量)。
(3)标量子查询(返回 1 个值)
- 本质:子查询结果是「单行单列」,可用于 比较 / 计算(如对比平均值、最大值)。
- 语法 + 场景:
-- 需求:查工资高于「公司平均工资」的员工 SELECT * FROM Employees WHERE Salary > (SELECT AVG(Salary) FROM Employees -- 子查询返回一个值(平均工资) );
- 执行原理:子查询先计算出平均工资(如 8000),主查询筛选
Salary > 8000
的行。
(4)列子查询(返回 1 列多行)
- 本质:子查询结果是「单列多行」,常用于 IN/ANY/ALL 等集合比较。
- 语法 + 场景:
-- 需求:查「属于研发部或市场部」的员工(部门 ID 来自子查询) SELECT * FROM Employees WHERE DepartmentID IN (SELECT DepartmentID FROM Departments WHERE DepartmentName IN ('研发部', '市场部') -- 子查询返回多个 ID );
- 替代语法(效率差异):
IN
适合子查询结果少的情况;若结果极多,用JOIN
更高效(避免多次匹配)。ANY/ALL
用法:-- 查工资比「研发部任意一人」高的员工(只要比其中一个高) SELECT * FROM Employees WHERE Salary > ANY (SELECT Salary FROM Employees WHERE DepartmentID = (SELECT DepartmentID FROM Departments WHERE DepartmentName = '研发部') );
三、多表查询 - 连接类型(JOIN 核心逻辑)
1. 内连接(INNER JOIN)
- 本质:只返回 两张表中匹配条件的记录(即连接条件成立的行),是最常用的连接方式。
- 执行原理:
- 对表 A 和表 B,逐行匹配
A.column = B.column
,仅保留匹配成功的行 → 临时结果集。
- 对表 A 和表 B,逐行匹配
- 语法 + 场景:
-- 基础用法(两表连接) SELECT e.EmployeeName, d.DepartmentName FROM Employees e JOIN Departments d ON e.DepartmentID = d.DepartmentID; -- 连接条件-- 多表内连接(三表及以上,如「订单 + 商品 + 用户」) SELECT o.OrderID, u.UserName, p.ProductName FROM Orders o JOIN Users u ON o.UserID = u.UserID JOIN Products p ON o.ProductID = p.ProductID;
- 特点:
- 结果集行数 = 匹配成功的行数,若某表无匹配,该行直接消失。
- 性能:因只保留匹配行,数据量小,查询速度快(尤其适合关联条件明确的场景)。
2. 外连接(OUTER JOIN)
(1)左外连接(LEFT JOIN / LEFT OUTER JOIN)
- 本质:返回 左表的全部记录,即使右表无匹配;右表无匹配的字段填
NULL
。 - 执行原理:
- 先取左表所有行 → 逐行匹配右表 → 匹配成功则合并数据;失败则右表字段填
NULL
。
- 先取左表所有行 → 逐行匹配右表 → 匹配成功则合并数据;失败则右表字段填
- 语法 + 场景:
-- 需求:查所有员工,包括「未分配部门」的(部门字段可能为 NULL) SELECT e.EmployeeName, d.DepartmentName FROM Employees e LEFT JOIN Departments d ON e.DepartmentID = d.DepartmentID;
- 关键场景:
- 统计「左表全量 + 右表关联」(如「用户表 + 订单表」,查所有用户,包括未下单的)。
(2)右外连接(RIGHT JOIN / RIGHT OUTER JOIN)
- 本质:与左外连接相反,返回 右表的全部记录,左表无匹配则填
NULL
。 - 语法 + 场景:
-- 需求:查所有部门,包括「无员工」的部门(员工字段可能为 NULL) SELECT e.EmployeeName, d.DepartmentName FROM Employees e RIGHT JOIN Departments d ON e.DepartmentID = d.DepartmentID;
- 注意:
- 左 / 右外连接可通过 交换表顺序 互相转换(如
A LEFT JOIN B
等价于B RIGHT JOIN A
),实际用LEFT JOIN
更多(逻辑更直观)。
- 左 / 右外连接可通过 交换表顺序 互相转换(如
(3)全外连接(FULL JOIN / FULL OUTER JOIN)
- MySQL 支持问题:MySQL 原生不支持
FULL JOIN
,需用 UNION + 左右外连接 模拟。 - 模拟语法:
-- 需求:查所有员工 + 所有部门,包括双方无匹配的记录 SELECT e.EmployeeName, d.DepartmentName FROM Employees e LEFT JOIN Departments d ON e.DepartmentID = d.DepartmentID UNION -- 合并左右外连接结果(去重) SELECT e.EmployeeName, d.DepartmentName FROM Employees e RIGHT JOIN Departments d ON e.DepartmentID = d.DepartmentID;
- 执行原理:
- 先
LEFT JOIN
取左表全量 + 右表匹配;再RIGHT JOIN
取右表全量 + 左表匹配 →UNION
去重(若用UNION ALL
则保留重复)。
- 先
3. 自连接(SELF JOIN)
- 本质:表与 自身连接(把一张表当两张表用),解决「表内层级关系」问题(如「员工表」的上级领导也在同一张表)。
- 语法 + 场景:
-- 需求:查员工姓名 + 其直属领导姓名(领导也在 Employees 表) SELECT e.EmployeeName, m.EmployeeName AS ManagerName FROM Employees e -- 自连接:e.ManagerID 关联 e.EmployeeID(自己关联自己) JOIN Employees m ON e.ManagerID = m.EmployeeID;
- 执行原理:
- 把
Employees
表拆成e
(员工)和m
(领导)两张虚拟表 → 匹配e.ManagerID = m.EmployeeID
。
- 把
- 典型场景:
- 层级结构(部门经理 → 员工)、评论回复(父评论 → 子评论)等。
4. 联合查询(UNION / UNION ALL)
- 本质:合并 多个查询的结果集(要求字段数、字段类型兼容)。
- 语法 + 差异:
-- UNION:合并后去重(效率低,需排序去重) SELECT Column1 FROM TableA UNION SELECT Column1 FROM TableB;-- UNION ALL:合并后保留所有行(效率高,直接拼接) SELECT Column1 FROM TableA UNION ALL SELECT Column1 FROM TableB;
- 关键场景:
- 合并不同条件的结果(如「查所有普通用户 + 所有管理员用户」)。
- 跨库查询(需开启
FEDERATED
引擎,实际少用,更多用程序层合并)。
四、多表查询 - 高级子查询(复杂场景的「手术刀」)
1. 标量子查询(复习 + 深化)
- 极端场景:
-- 需求:给每个员工的工资,加上公司平均工资的 10%(复杂计算场景)
UPDATE Employees
SET Salary = Salary + (SELECT AVG(Salary) * 0.1 FROM Employees -- 子查询先算出平均工资的 10%,作为标量值参与更新
)
WHERE DepartmentID = 5; -- 仅更新部门 5 的员工
- 执行细节:
- 子查询会先执行一次(计算出全公司平均工资的 10%,比如 800),然后主查询批量更新符合条件的员工(工资 + 800)。
- 若子查询返回 NULL(比如表为空),则
Salary + NULL
结果为 NULL,需用COALESCE
处理:
UPDATE Employees
SET Salary = Salary + COALESCE((SELECT AVG(Salary) * 0.1 FROM Employees), 0 -- 若平均工资为 NULL(表空),则加 0
)
WHERE DepartmentID = 5;
2. 列子查询(复习 + 深化)
- 复杂过滤场景:
-- 需求:查「工资高于其所在部门平均工资」的员工(子查询返回部门平均工资列)
SELECT EmployeeName, Salary, DepartmentID
FROM Employees e
WHERE Salary > (SELECT AVG(Salary) FROM Employees WHERE DepartmentID = e.DepartmentID -- 关联外层表的 DepartmentID,实现「按部门分组」
);
- 执行原理:
- 这是 相关子查询(子查询依赖主查询的
e.DepartmentID
),主查询每遍历一行员工数据,子查询就执行一次(按部门算平均工资),性能可能较低(数据量大时)。 - 优化方案:用
JOIN
改写(先算部门平均工资,再关联员工表):
- 这是 相关子查询(子查询依赖主查询的
WITH DeptAvg AS (SELECT DepartmentID, AVG(Salary) AS AvgSalaryFROM Employees GROUP BY DepartmentID
)
SELECT e.EmployeeName, e.Salary, e.DepartmentID
FROM Employees e
JOIN DeptAvg d
ON e.DepartmentID = d.DepartmentID
AND e.Salary > d.AvgSalary;
3. 行子查询(复习 + 深化)
- 跨表匹配场景:
-- 需求:同步「员工表」和「员工备份表」的工资(当员工 ID、部门 ID 都匹配时更新)
UPDATE Employees e
SET e.Salary = (SELECT b.Salary FROM EmployeeBackup bWHERE (b.EmployeeID, b.DepartmentID) = (e.EmployeeID, e.DepartmentID) -- 行匹配
)
WHERE EXISTS (SELECT 1 FROM EmployeeBackup bWHERE (b.EmployeeID, b.DepartmentID) = (e.EmployeeID, e.DepartmentID)
);
- 执行细节:
- 子查询返回的是「行数据」
(Salary)
,主查询通过行匹配条件(b.EmployeeID, b.DepartmentID) = (e.EmployeeID, e.DepartmentID)
找到对应的备份工资。 WHERE EXISTS
用于避免更新时子查询返回 NULL 导致主表字段被置空(若不加,无匹配的员工工资会被设为 NULL)。
- 子查询返回的是「行数据」
4. 表子查询(复习 + 深化)
- 多层嵌套 + 分页场景:
-- 需求:查「工资前 10 员工」的部门信息,且只取部门表的前 5 条结果(多层限制)
SELECT d.*
FROM (SELECT DepartmentID FROM (SELECT DepartmentID FROM Employees ORDER BY Salary DESC LIMIT 10 -- 先取工资前 10 员工的部门 ID(表子查询 1)) AS TopDepts GROUP BY DepartmentID -- 去重部门 ID(避免重复部门)
) AS UniqueDepts
JOIN Departments d
ON UniqueDepts.DepartmentID = d.DepartmentID
LIMIT 5; -- 再取前 5 个部门信息(最终结果限制)
- 执行顺序:
- 最内层表子查询:
SELECT DepartmentID FROM Employees ORDER BY Salary DESC LIMIT 10
→ 得到 10 个员工的部门 ID(可能重复)。 - 中间层:
GROUP BY DepartmentID
→ 去重,得到唯一部门 ID 列表。 - 最外层:关联
Departments
表,再限制取前 5 条部门信息。 - 这种多层嵌套在复杂报表场景常见,但要注意索引优化(如给
Employees.Salary
加索引)。
- 最内层表子查询:
五、多表关系设计(业务与表结构的「粘合剂」)
1. 三种核心关系(从业务到表结构)
(1)一对多(1:N)
- 业务场景:部门(1)→ 员工(N)、分类(1)→ 商品(N)。
- 表结构设计:
- 在「多」的一方(员工表)添加外键,关联「一」的一方(部门表)的主键。
CREATE TABLE Departments (DepartmentID INT PRIMARY KEY,DepartmentName VARCHAR(50) );CREATE TABLE Employees (EmployeeID INT PRIMARY KEY,DepartmentID INT,FOREIGN KEY (DepartmentID) REFERENCES Departments(DepartmentID) );
- 数据插入顺序:
- 必须先插父表(Departments),再插子表(Employees)(否则外键约束报错)。
- 删除父表记录时,需先删子表关联记录,或设置
ON DELETE CASCADE
。
(2)多对多(N:M)
- 业务场景:学生(N)→ 课程(M)、订单(N)→ 商品(M)。
- 表结构设计:
- 新增「中间表」,包含两个外键(分别关联双方主键),可加额外字段(如选课时间、购买数量)。
CREATE TABLE Students (StudentID INT PRIMARY KEY );CREATE TABLE Courses (CourseID INT PRIMARY KEY );-- 中间表(学生-课程关联) CREATE TABLE StudentCourses (StudentID INT,CourseID INT,PRIMARY KEY (StudentID, CourseID), -- 复合主键,避免重复关联FOREIGN KEY (StudentID) REFERENCES Students(StudentID),FOREIGN KEY (CourseID) REFERENCES Courses(CourseID) );
- 插入逻辑:
- 先插 Students、Courses 表,再插中间表(StudentCourses)。
- 查询时,通过中间表
JOIN
实现多对多关联:
SELECT s.StudentID, c.CourseID FROM Students s JOIN StudentCourses sc ON s.StudentID = sc.StudentID JOIN Courses c ON sc.CourseID = c.CourseID;
(3)一对一(1:1)
- 业务场景:用户(1)→ 用户详情(1)、订单(1)→ 发票(1)。
- 表结构设计(两种方式):
- 共享主键:详情表的主键同时是外键,关联主表主键(主表删,详情表删)。
CREATE TABLE Users (UserID INT PRIMARY KEY );CREATE TABLE UserProfiles (UserID INT PRIMARY KEY, -- 与 Users.UserID 相同FOREIGN KEY (UserID) REFERENCES Users(UserID) );
- 外键 + 唯一约束:详情表用外键关联主表,且外键字段加唯一约束(保证 1:1)。
CREATE TABLE UserProfiles (ProfileID INT PRIMARY KEY,UserID INT UNIQUE, -- 唯一约束,确保一个用户只有一个详情FOREIGN KEY (UserID) REFERENCES Users(UserID) );
- 使用场景:
- 拆分大表(如用户表字段太多,拆出不常用的详情表),或实现「可选扩展信息」(用户可填或不填详情)。
2. 关系设计的「坑点」与最佳实践
- 外键的性能争议:
- 优点:强制数据完整性,减少脏数据。
- 缺点:插入 / 删除时需校验外键,性能略低(高并发场景可考虑程序层控制,或用
ON DELETE CASCADE
减少操作)。
- 中间表的索引优化:
- 多对多中间表的复合主键(如
(StudentID, CourseID)
)自动包含索引,查询时直接用该索引,无需额外创建。
- 多对多中间表的复合主键(如
- 避免循环依赖:
- 表 A 外键关联表 B,表 B 外键关联表 A → 插入数据时死锁(需调整逻辑,用一方的外键允许 NULL 或延迟约束)。
六、执行计划与优化(让查询飞起来的「密码」)
1. 理解执行计划(EXPLAIN)
- 语法:
EXPLAIN SELECT * FROM Employees e
JOIN Departments d ON e.DepartmentID = d.DepartmentID;
- 关键字段解读:
- id:查询中操作执行的顺序(复杂查询有多个 id,越小越先执行)。
- select_type:查询类型(SIMPLE 简单查询、SUBQUERY 子查询、DERIVED 派生表等)。
- table:涉及的表。
- type:关联类型(性能从优到差:
system > const > eq_ref > ref > range > index > ALL
)。- eq_ref:主键 / 唯一索引匹配(最佳,如
JOIN
用主键关联)。 - ref:普通索引匹配(较好,如外键关联)。
- ALL:全表扫描(最差,需优化)。
- eq_ref:主键 / 唯一索引匹配(最佳,如
- rows:MySQL 认为会扫描的行数(越少越好)。
- Extra:额外信息(如
Using index
表示用到覆盖索引,Using filesort
表示需额外排序,性能差)。
2. 常见优化场景
(1)多表 JOIN 优化
- 问题:
type = ALL
(全表扫描),rows
极大。 - 优化:
- 给连接字段加索引(如
Employees.DepartmentID
和Departments.DepartmentID
加索引)。 - 小表驱动大表(
JOIN
时,让结果集小的表在左边,减少循环次数)。
- 给连接字段加索引(如
(2)子查询优化
- 问题:相关子查询(子查询依赖主查询字段)导致多次执行,性能差。
- 优化:用
JOIN
或WITH
语法改写(如前面列子查询的优化方案)。
(3)索引覆盖(Covering Index)
- 原理:查询所需字段都在索引中,无需回表查询数据行,速度极快。
- 示例:
-- 需求:查员工姓名和部门 ID(假设索引包含这两个字段)
SELECT EmployeeName, DepartmentID
FROM Employees
WHERE DepartmentID = 5;-- 优化:创建覆盖索引
CREATE INDEX idx_employee_department ON Employees (DepartmentID, EmployeeName);
- 执行计划:
Extra
会显示Using index
,表示直接从索引取数据,无需访问数据页。
3. 避坑指南
- 避免
SELECT *
:- 会导致回表查询所有字段,且无法利用覆盖索引。
- 分页查询优化(LIMIT 大偏移量):
- 问题:
LIMIT 100000, 10
需扫描前 100010 行,性能差。 - 优化:用主键定位,
WHERE EmployeeID > 100000 LIMIT 10
(需主键连续或有索引)。
- 问题:
- 索引失效场景:
- 字段用函数(
WHERE YEAR(CreateTime) = 2023
)、类型隐式转换(WHERE DepartmentID = '5'
,实际是 INT 类型)→ 索引失效,全表扫描。
- 字段用函数(