MySQL数据操作与查询笔记之DML、DQL、多表连接与子查询

 更新时间:2026年07月29日 10:24:42   作者:是上好佳佳佳呀  
在MySQL 数据库中多表连接是一个非常重要的概念,它允许我们通过在多个表之间建立关联来检索和处理数据,这篇文章主要介绍了MySQL数据操作与查询笔记之DML、DQL、多表连接与子查询的相关资料,需要的朋友可以参考下

前言

本文是 MySQL 系列的第二篇。上篇学习了 DDL(数据库和表结构的管理),本篇进入表内部的数据操作:DML(增删改)和 DQL(查询),同样沿着增删改查的逻辑主线展开。

一、DML:表记录的增加、修改、删除

如果说DDL 管容器的形状,那DML 就管容器里的内容:对表中记录的增删改。

1.1 增加记录:INSERT

-- 不指定字段:必须按表结构的列顺序给全所有列的值
INSERT INTO 表 VALUES(值1, 值2, 值3, ...);

-- 指定字段:只给指定列赋值,未指定的列使用默认值或 NULL
INSERT INTO 表(字段1, 字段2, ...) VALUES(值1, 值2, ...);

-- 不指定字段,批量插入多行
INSERT INTO 表 VALUES(值1, 值2, ...), (值1, 值2, ...), ...;

-- 指定字段,批量插入多行
INSERT INTO 表(字段1, 字段2, ...) VALUES(值1, 值2, ...), (值1, 值2, ...), ...;
写法适用场景注意事项
不指定字段表结构简单列少、确认给全了所有值值顺序必须和建表时的列顺序一致;省略自增主键时填 NULL 或 0
指定字段推荐,明确清晰,表结构变化后不受影响未指定的列自动填默认值或 NULL(NOT NULL 且无默认值时报错)
单行逐条插入每行一条语句
批量一次性插入多行VALUES 后跟多组括号,逗号分隔,效率比逐条插入高
-- 示例
INSERT INTO users(username, password, email, age) 
VALUES('zhangsan', '123456', 'zs@xx.com', 25);

-- 批量
INSERT INTO users(username, password, email) VALUES
('lisi', 'abc123', 'ls@xx.com'),
('wangwu', 'qwe456', 'ww@xx.com');

AUTO_INCREMENT 配合主键:INSERT 时不指定主键值,或设为 NULL/0,数据库自动递增分配下一个序号。

1.2 修改记录:UPDATE

UPDATE 表名 SET 字段1 = 值1, 字段2 = 值2, ... WHERE 条件;
注意事项说明
必须加 WHERE不加 WHERE 会修改全表所有行,这是生产环境中最常见的事故之一
SET 多个字段逗号分隔,一次可更新多列
-- 修改指定用户的邮箱和年龄
UPDATE users SET email = 'new_zs@xx.com', age = 26 WHERE username = 'zhangsan';

1.3 删除记录:DELETE vs TRUNCATE

DELETE FROM 表名 WHERE 条件;    -- 删除符合条件的行
DELETE FROM 表名;               -- ⚠️ 删除全表数据(逐行删,慢)
TRUNCATE TABLE 表名;            -- 清空全表数据(直接释放空间,快)
对比DELETETRUNCATE
删除方式逐行删除,记录日志直接释放整张表的存储空间
是否可加 WHERE✅ 可以,删指定行❌ 不能,只能清空全表
自增序列不重置重置归零
速度慢(逐行写日志,可回滚)快(不逐行记录)
属于DML(数据操作)DDL(结构操作,本质是删表重建)

DELETE 加 WHERE 删指定行;不加 WHERE 清空全表但不重置自增;TRUNCATE 清空全表并重置自增。生产环境 DELETE 不加 WHERE 是经典翻车操作。

核心特性对比表

特性DELETETRUNCATEDROP
操作本质DML(数据操作语言)DDL(数据定义语言)DDL(数据定义语言)
删除内容删除数据行(可部分删除)清空全部数据删除整个表(结构+数据+索引)
表结构保留保留(重置为初始状态,相当于空表)彻底删除
空间回收不释放磁盘空间,仅标记可复用释放空间,直接重建表释放空间,表完全移除
自增列重置(AUTO_INCREMENT)保留原值,继续递增
(例如删光所有行后,新插入行的 ID 会从之前的最大值 +1 继续)
重置为初始值(通常为1)表不存在,无所谓重置

一句话区别

  • DROP:删表结构,表彻底消失。
  • TRUNCATE:清空表数据,保留表结构,重置一切。
  • DELETE:删除行数据,保留表结构,不重置计数器,不释放空间。

二、DQL:数据查询

DQL 的核心动词 SELECT,是 SQL 中最复杂、用得最多的部分。

先给出 SELECT 完整语法骨架,后面逐个拆解:

SELECT [DISTINCT] 字段1 [AS 别名], 字段2, ...
FROM 表名 [AS 别名]
[WHERE 条件]
[GROUP BY 分组字段]
[HAVING 分组后过滤条件]
[ORDER BY 排序字段 [ASC|DESC]]
[LIMIT 返回行数 [OFFSET 起始行]];

2.1 基本查询:SELECT … FROM

SELECT * FROM 表名;                          -- 查全表所有行所有列
SELECT 列1, 列2 FROM 表名;                    -- 查指定列(所有行)
SELECT 列1 AS 别名1, 列2 AS 别名2 FROM 表名;  -- 给结果列起别名
SELECT 表别名.列1 FROM 表名 AS 表别名;         -- 给表起别名(多表查询时常用)

AS 可以省略。别名如果和 SQL 关键字重名,需要用反引号包裹(如 `order`),但建议直接避开关键字。

2.2 条件筛选:WHERE

SELECT * FROM 表名 WHERE 条件;
运算符类型运算符说明示例
比较= > < >= <= != <>!=<> 都表示不等于WHERE age >= 18
逻辑AND OR NOT多条件组合WHERE age >= 18 AND city = '北京'
模糊匹配LIKE% 匹配任意多个字符,_ 匹配单个字符WHERE name LIKE '张%'
连续范围BETWEEN ... AND ...闭区间,包含两端WHERE age BETWEEN 20 AND 30
离散范围IN (...)匹配列表中的任意一个值WHERE city IN ('北京', '上海')
空值判断IS NULL / IS NOT NULLNULL 不能用 = 判断WHERE email IS NOT NULL

NULL 不能用 =!= 比较,因为 NULL 表示"未知",任何值与 NULL 比较结果都是 NULL(既不是 TRUE 也不是 FALSE)。必须用 IS NULLIS NOT NULL

2.3 聚合函数

聚合函数对一组行做统计计算,返回单个值。

函数全称作用注意
COUNT(col)count统计行数COUNT(*) 统计所有行含 NULL;COUNT(列) 忽略 NULL
MAX(col)maximum最大值适用于数值、日期、字符串
MIN(col)minimum最小值同上
SUM(col)sum求和仅数值类型,忽略 NULL
AVG(col)average平均值仅数值类型,忽略 NULL
-- 聚合函数示例
SELECT COUNT(*)   FROM users;              -- 统计总行数
SELECT MAX(age)   FROM users;              -- 最大年龄
SELECT MIN(age)   FROM users;              -- 最小年龄
SELECT SUM(balance) FROM users;            -- 余额总和
SELECT AVG(age)   FROM users;              -- 平均年龄
SELECT COUNT(*), MAX(age), MIN(age), AVG(age) FROM users;  -- 一次查多个

⚠️注意!聚合函数可以写在 SELECT 后面,也可以写在 HAVING 后面(见 2.4)。不能写在 WHERE 后面。具体原因请见后续讲解。

2.4 分组聚合:GROUP BY + HAVING

GROUP BY 将数据按指定列的值分组值相同的行归入同一组,然后对每组分别用聚合函数统计。分两步:先分组,再聚合

-- 按城市分组,统计每个城市的人数和平均年龄
SELECT city, COUNT(*) AS 人数, AVG(age) AS 平均年龄
FROM users
GROUP BY city;
问题答案
此时SELECT 后面能写什么?此时只能是分组字段(GROUP BY 后面的列)或被聚合函数包裹的字段。写其他字段,MySQL 旧版本不报错但结果不确定
⭐聚合函数能写在哪?SELECT 后面和 HAVING 后面。不能写在 WHERE 后面
⭐HAVING 和 WHERE 的区别?WHERE 在分组过滤原始行;HAVING 在分组聚合过滤分组结果

聚合函数之所以不能出现在 WHERE 子句中,根本原因在于 SQL 的逻辑执行顺序决定了WHERE 在分组前过滤原始行,聚合在分组后才计算,所以 WHERE 里不能用聚合;要筛选分组结果,用 HAVING。

处理顺序:WHERE → GROUP BY → 聚合计算 → HAVING → ORDER BY → LIMIT

-- 错误示例:SELECT 里写了非分组字段 username,且未被聚合函数包裹
-- MySQL 5.7+ 默认报错:Expression #2 of SELECT list is not in GROUP BY clause
SELECT city, username, COUNT(*) FROM users GROUP BY city;   -- ❌

-- 正确写法:只放分组字段和聚合函数
SELECT city, COUNT(*), MAX(age) FROM users GROUP BY city;   -- ✅
-- WHERE 先筛人,GROUP BY 再分组,HAVING 再筛分组结果
SELECT city, COUNT(*) AS cnt
FROM users
WHERE age >= 18           -- 先筛选成年人
GROUP BY city             -- 再按城市分组
HAVING cnt >= 5;          -- 最后只要人数 ≥5 的城市

2.5 排序:ORDER BY

SELECT * FROM 表名 ORDER BY 列1 [ASC|DESC], 列2 [ASC|DESC];
关键字全称含义
ASCascending升序,从小到大(默认,可省略)
DESCdescending降序,从大到小

多列排序时,先按第一列排,第一列值相同时再按第二列排。

2.6 去重:DISTINCT

SELECT DISTINCT 列1, 列2 FROM 表名;

DISTINCT 对SELECT 后面所有列的整行组合去重,不是只对紧跟的第一个字段去重。SELECT DISTINCT city, age 返回的是"城市+年龄"不重复的所有组合。

2.7 限制行数:LIMIT

SELECT * FROM 表名 LIMIT N;             -- 只返回前 N 行
SELECT * FROM 表名 LIMIT M, N;          -- 跳过 M 行,返回 N 行(M 从 0 开始)
SELECT * FROM 表名 LIMIT N OFFSET M;    -- 同上,更明确的写法

M 是起始偏移(从 0 计数),N 是返回行数。

-- 假设每页 10 条
-- 第 1 页:LIMIT 0, 10   (跳过 0 行,取 10 行)
-- 第 2 页:LIMIT 10, 10  (跳过 10 行,取 10 行)
-- 第 3 页:LIMIT 20, 10  (跳过 20 行,取 10 行)
-- 公式:LIMIT (页码-1)*每页条数, 每页条数
SELECT * FROM users LIMIT 0, 10;   -- 第 1 页
SELECT * FROM users LIMIT 10, 10;  -- 第 2 页

三、SQL 语句执行顺序

⚠️注意!写 SQL 的顺序和数据库实际执行的顺序不是一回事

书写顺序执行顺序说明
SELECT5计算 SELECT 列表中的表达式
FROM1先确定数据从哪张表来
WHERE2筛选原始行
GROUP BY3分组
HAVING4对分组聚合后的结果过滤(可以用聚合函数)
ORDER BY6排序(此时才能用 SELECT 中定义的别名)
LIMIT7截取行数

这个顺序不是 MySQL 独有的,所有关系型数据库的执行顺序基本一致,因为这是 SQL 标准的定义。

用一条实际语句演示为什么书写顺序和执行顺序不同:

-- 书写顺序(你敲的):
SELECT city, COUNT(*) AS cnt
FROM users
WHERE age >= 18
GROUP BY city
HAVING cnt >= 5
ORDER BY cnt DESC
LIMIT 3;

-- 执行顺序(数据库实际做的):
-- ① FROM users           → 找到 users 表
-- ② WHERE age >= 18      → 筛掉未成年,只保留成年人行
-- ③ GROUP BY city        → 把剩下的行按 city 分组
-- ④ COUNT(*)             → 计算每组有多少人
-- ⑤ HAVING cnt >= 5      → 只保留人数 ≥5 的城市组
-- ⑥ ORDER BY cnt DESC    → 按人数降序排列
-- ⑦ LIMIT 3              → 取前 3 行

为什么设计成不一致?因为 SQL 是声明式语言,你声明"我要什么结果",数据库自己决定"怎么查效率最高"。书写顺序贴近人的自然表达(先说想看什么字段),执行顺序是数据库优化器认为最高效的路径。

四、DML + DQL 增删改查总览

操作语句
增(INSERT)INSERT INTO 表(列,...) VALUES(值,...);
删(DELETE)DELETE FROM 表 WHERE 条件;
改(UPDATE)UPDATE 表 SET 列=值 WHERE 条件;
查(SELECT)SELECT 列 FROM 表 WHERE 条件 GROUP BY 列 HAVING 条件 ORDER BY 列 LIMIT;
清空表TRUNCATE TABLE 表名;(DDL,重置自增)

五、多表查询

数据分布在多张表中,查询时往往需要把它们关联起来。

5.1 为什么需要多表查询?

如果所有数据塞在一张表里会怎样?以员工管理为例:

idnamedept_namedept_locationdept_budget
1张三技术部3楼500万
2李四技术部3楼500万
3王五市场部5楼200万

问题很明显:

  • 数据冗余:技术部的地址和预算在每一个技术部员工行里都重复存储。技术部 100 人,楼号和预算就重复 100 次。
  • 更新异常:技术部搬到 4 楼,需要更新所有技术部员工的行。漏一条就数据不一致。
  • 插入异常:新成立"人事部"但还没招到人,部门信息没地方存(因为表的主键是员工 id,没有员工就无法录入部门)。
  • 删除异常:开除市场部最后一个员工王五,市场部的信息(5楼、200万预算)也跟着丢失了。

这些问题在数据库理论中叫插入异常、删除异常、更新异常、数据冗余,都是"一张大表塞所有数据"的后果。

解决方法:分表

拆分前:一个大表中,部门信息每个员工行都存一份
拆分后:
  employees 表:id, name, dept_id          ← 只存部门编号
  departments 表:id, dept_name, location, budget  ← 部门信息独立存储

查询时用 SQL 把它们"拼回来",这就是多表查询的本质。

分表遵循数据库范式(Normalization)原则。范式有 1NF 到 5NF,实际开发做到第三范式(3NF)即可。范式的核心思想:一个事实只存一处,减少冗余,保证一致性

5.2 表与表之间的关系

多表查询之前,先理清表和表之间有哪几种关系,这决定了外键的位置和 JOIN 的方向。

记一个关键规则:users 是主表(被引用的),orders 是从表(引用别人的)。外键始终建在从表上,指向主表的主键

一对多(1 : N)

一条 A 表记录对应多条 B 表记录,一条 B 表记录只对应一条 A 表记录。

departments(1) ←──→(N) employees
  一个部门有多个员工,一个员工只属于一个部门

实现方式:在"多"的那一方(employees)加一列外键,指向"一"的那一方(departments)的主键。

-- "一"的一方
CREATE TABLE departments (
    id       INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name     VARCHAR(50) NOT NULL,
    location VARCHAR(100),
    budget   DECIMAL(12, 2)
);

-- "多"的一方:dept_id 是外键
CREATE TABLE employees (
    id       INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name     VARCHAR(30) NOT NULL,
    salary   DECIMAL(10, 2),
    dept_id  INT UNSIGNED,
    FOREIGN KEY (dept_id) REFERENCES departments(id)
);

一对一(1 : 1)

一条 A 表记录对应至多一条 B 表记录,反之亦然。

users(1) ←──→(1) user_profiles
  一个用户有一份详细资料

使用场景:把不常用的、比较大的字段拆分出去(垂直分表),主表只留常用字段,提高查询效率。

实现方式:在任意一方加外键 + UNIQUE 约束。

CREATE TABLE user_profiles (
    id     INT UNSIGNED PRIMARY KEY,
    avatar VARCHAR(200),
    bio    TEXT,
    FOREIGN KEY (id) REFERENCES users(id)
);

为什么不直接放 users 表?因为查用户列表时通常不需要这些大字段,分开后主表行更小、扫描更快。

多对多(M : N)

一条 A 表记录对应多条 B 表记录,一条 B 表记录也对应多条 A 表记录。

students(M) ←──→(N) courses
  一个学生选多门课,一门课有多个学生选

实现方式:必须引入中间表(关联表/桥接表),把 M:N 拆成两个 1:N。

-- 学生表
CREATE TABLE students (
    id   INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(30) NOT NULL
);

-- 课程表
CREATE TABLE courses (
    id   INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL
);

-- 中间表(选课记录):两个外键组合
CREATE TABLE student_courses (
    student_id INT UNSIGNED,
    course_id  INT UNSIGNED,
    score      DECIMAL(3, 1),
    PRIMARY KEY (student_id, course_id),          -- 复合主键,防止重复选课
    FOREIGN KEY (student_id) REFERENCES students(id),
    FOREIGN KEY (course_id)  REFERENCES courses(id)
);
关系类型举例实现方式外键在哪
一对多 (1:N)部门 ↔ 员工、用户 ↔ 订单"多"方加外键列多的一方
一对一 (1:1)用户 ↔ 详细资料、身份证 ↔ 人任一方加外键 + UNIQUE任一方
多对多 (M:N)学生 ↔ 课程、订单 ↔ 商品新建中间表,拆成两个 1:N中间表

5.3 外键约束详解

外键(FOREIGN KEY)不只是概念,它是数据库层面的硬约束,用来保证引用完整性(Referential Integrity)。

外键的作用

CREATE TABLE employees (
    id       INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name     VARCHAR(30) NOT NULL,
    salary   DECIMAL(10, 2),
    dept_id  INT UNSIGNED,
    FOREIGN KEY (dept_id) REFERENCES departments(id)
);

有了外键约束之后:

  • 向 employees 插入 dept_id = 99 的行 → departments 中没有 id=99 的部门 → 报错,插入失败
  • 删除 departments 中 id=1 的部门 → employees 中还有该部门的员工 → 报错,删除失败

外键像一道闸门,保证 employees 的 dept_id 永远指向一个真实存在的部门,不会出现指向不存在的部门的员工。

外键的级联操作

当主表的记录被删除或更新时,从表引用它的行该怎么处理?由 ON DELETEON UPDATE 子句定义。

FOREIGN KEY (dept_id) REFERENCES departments(id)
    ON DELETE CASCADE      -- 主表行被删时,从表引用行也跟着删
    ON UPDATE CASCADE      -- 主表主键被改时,从表外键也跟着改
级联选项行为适用场景
CASCADE主表删/改,从表跟着删/改订单删除时订单项也删除(级联删除)
SET NULL主表删/改,从表外键设为 NULL部门删除,员工变成"待分配"(要求外键列允许 NULL)
RESTRICT禁止操作:如果从表还有引用行,主表就不能删/改默认行为,最安全
NO ACTION与 RESTRICT 类似,检查时机不同(事务提交时检查)MySQL 中等价于 RESTRICT

⚠️ CASCADE 在生产环境中要非常谨慎,删一个部门可能连锁删除其下所有员工,且不可回滚(如果没开事务)。大多数场景推荐用默认的 RESTRICT:先手动处理从表数据,再删主表。

5.4 连接查询(JOIN)

连接查询是多表查询最核心的方式。

5.4.1 笛卡尔积:所有连接的基础

笛卡尔积:两张表所有行的全部组合,A 的每一行 × B 的每一行,总行数 = A行数 × B行数。这是所有 JOIN 的底层运算。

两张表做连接时,数据库首先计算笛卡尔积(Cartesian Product),再用 ON 条件从中筛选。

-- 假设 departments 有 3 行,employees 有 5 行
-- 下面的查询返回 3 × 5 = 15 行
SELECT * FROM departments, employees;
-- 或显式写法
SELECT * FROM departments CROSS JOIN employees;
departments                     employees
┌────┬──────────┐              ┌────┬──────┬─────────┐
│ id │ name     │              │ id │ name │ dept_id │
├────┼──────────┤              ├────┼──────┼─────────┤
│ 1  │ 技术部   │              │ 1  │ 张三 │ 1       │
│ 2  │ 市场部   │              │ 2  │ 李四 │ 1       │
│ 3  │ 财务部   │              │ 3  │ 王五 │ 2       │
└────┴──────────┘              └────┴──────┴─────────┘

笛卡尔积(3 × 3 = 9 行):
┌────┬──────────┬────┬──────┬─────────┐
│d.id│ d.name   │e.id│e.name│ dept_id │
├────┼──────────┼────┼──────┼─────────┤
│ 1  │ 技术部   │ 1  │ 张三 │ 1       │  ← 有意义:d.id = e.dept_id
│ 1  │ 技术部   │ 2  │ 李四 │ 1       │  ← 有意义
│ 1  │ 技术部   │ 3  │ 王五 │ 2       │  ← 无意义:技术部 ID≠市场部员工的 dept_id
│ 2  │ 市场部   │ 1  │ 张三 │ 1       │  ← 无意义
│ 2  │ 市场部   │ 2  │ 李四 │ 1       │  ← 无意义
│ 2  │ 市场部   │ 3  │ 王五 │ 2       │  ← 有意义
│ 3  │ 财务部   │ 1  │ 张三 │ 1       │  ← 无意义
│ 3  │ 财务部   │ 2  │ 李四 │ 1       │  ← 无意义
│ 3  │ 财务部   │ 3  │ 王五 │ 2       │  ← 无意义
└────┴──────────┴────┴──────┴─────────┘

笛卡尔积中绝大多数行是无意义的组合。JOIN 的本质就是:先算笛卡尔积,再用 ON 条件从中筛选出有意义(匹配)的行

CROSS JOIN 在业务查询中几乎不会单独使用,但理解它是理解所有 JOIN 的基础,每种 JOIN 都是在笛卡尔积上叠加不同的"不匹配行处理策略"。

5.4.2 内连接(INNER JOIN)

内连接:两表都只保留匹配行,不匹配的行两边全部丢弃,取交集。

-- 显式写法(推荐)
SELECT e.name AS 员工, d.name AS 部门
FROM employees e
INNER JOIN departments d ON e.dept_id = d.id;

-- 隐式写法(不推荐,原因见下方)
SELECT e.name AS 员工, d.name AS 部门
FROM employees e, departments d
WHERE e.dept_id = d.id;
写法语法优点缺点
显式 JOIN ... ONFROM A JOIN B ON 条件连接条件(ON)和过滤条件(WHERE)分离,结构清晰;支持外连接—(推荐)
隐式 WHEREFROM A, B WHERE 条件简短连接条件和过滤条件混在 WHERE 中,难以区分;无法表达外连接;忘了写 WHERE 就直接返回笛卡尔积,不报错

⚠️ 始终使用显式 JOIN ... ON 写法。隐式写法如果忘了写 WHERE 条件,查询不会报错而是静默返回笛卡尔积,数据量稍大就是灾难。而且隐式写法无法表达 LEFT/RIGHT JOIN。

INNER JOIN 的执行逻辑(逐行走一遍):

  1. 计算两表的笛卡尔积(A 的每一行 × B 的每一行)
  2. 用 ON 条件(d.id = e.dept_id)逐行检查笛卡尔积
  3. 条件为 TRUE → 该组合行加入最终结果
  4. 条件为 FALSE 或 NULL → 该组合行直接丢弃
  5. 任何一方匹配不上,该行都不出现在结果中
departments                       employees
┌────┬──────────┐               ┌────┬──────┬─────────┐
│ id │ name     │               │ id │ name │ dept_id │
├────┼──────────┤               ├────┼──────┼─────────┤
│ 1  │ 技术部   │               │ 1  │ 张三 │ 1       │
│ 2  │ 市场部   │               │ 2  │ 李四 │ 1       │
│ 3  │ 财务部   │               │ 3  │ 王五 │ 2       │
└────┴──────────┘               └────┴──────┴─────────┘

INNER JOIN 结果(只保留匹配行,取交集):
┌────────┬──────────┬────────┬────────┐
│ dept.id│ dept.name│ emp.id │ emp.name│
├────────┼──────────┼────────┼────────┤
│   1    │ 技术部   │   1    │ 张三   │  ← d.id=1 = e.dept_id=1 ✓ 匹配
│   1    │ 技术部   │   2    │ 李四   │  ← d.id=1 = e.dept_id=1 ✓ 匹配
│   2    │ 市场部   │   3    │ 王五   │  ← d.id=2 = e.dept_id=2 ✓ 匹配
└────────┴──────────┴────────┴────────┘
← 财务部(d.id=3):employees 中无 dept_id=3 → 不出现
← 若 employees 中有 dept_id=NULL 的行:NULL 无法匹配任何值 → 不出现

INNER JOIN = 在笛卡尔积全集中,用 ON 筛出交集。和 LEFT JOIN 的关键区别:INNER JOIN 两边不匹配都丢弃,LEFT JOIN 保左全

5.4.3 左外连接(LEFT JOIN)

左连接:左表全部保留,右表匹配不上则填 NULL。

-- 列出所有部门及其员工人数(包括没有员工的部门)
SELECT d.name, COUNT(e.id) AS 员工数
FROM departments d
LEFT JOIN employees e ON d.id = e.dept_id
GROUP BY d.id, d.name;

LEFT JOIN 的执行逻辑(逐行走一遍):

  1. 取左表(departments)第一行
  2. 在右表(employees)中找所有满足 d.id = e.dept_id 的行
  3. 匹配上 → 拼接成结果行(1 个左表行 × N 个匹配的右表行)
  4. 匹配不上 → 生成一行,左表数据保留,右表列全部填 NULL
  5. 左表下一行,重复 2-4
departments (左)                employees (右)
┌────┬──────────┐              ┌────┬──────┬─────────┐
│ id │ name     │              │ id │ name │ dept_id │
├────┼──────────┤              ├────┼──────┼─────────┤
│ 1  │ 技术部   │              │ 1  │ 张三 │ 1       │
│ 2  │ 市场部   │              │ 2  │ 李四 │ 1       │
│ 3  │ 财务部   │              │ 3  │ 王五 │ 2       │
└────┴──────────┘              └────┴──────┴─────────┘

LEFT JOIN 结果:
┌────────┬──────────┬────────┬────────┐
│ dept.id│ dept.name│ emp.id │ emp.name│
├────────┼──────────┼────────┼────────┤
│   1    │ 技术部   │   1    │ 张三   │  ← 匹配成功,拼接
│   1    │ 技术部   │   2    │ 李四   │  ← 匹配成功,拼接
│   2    │ 市场部   │   3    │ 王五   │  ← 匹配成功,拼接
│   3    │ 财务部   │  NULL  │ NULL   │  ← 财务部无员工,右表全填 NULL
└────────┴──────────┴────────┴────────┘

5.4.4 右外连接(RIGHT JOIN)

右连接:右表全部保留,左表匹配不上则填 NULL。

-- 查询所有部门及其员工(包括没有员工的部门)
-- 写法1:RIGHT JOIN
SELECT e.name AS 员工, d.name AS 部门
FROM employees e
RIGHT JOIN departments d ON e.dept_id = d.id;

-- 写法2:等价 LEFT JOIN(推荐用这种,语义更直观)
SELECT e.name AS 员工, d.name AS 部门
FROM departments d
LEFT JOIN employees e ON d.id = e.dept_id;

RIGHT JOIN 的执行逻辑(逐行走一遍):

  1. 取右表(departments)第一行
  2. 在左表(employees)中找所有满足 e.dept_id = d.id 的行
  3. 匹配上 → 拼接成结果行(N 个匹配的左表行 × 1 个右表行)
  4. 匹配不上 → 生成一行,左表列全部填 NULL,右表数据保留
  5. 右表下一行,重复 2-4
employees (左)                   departments (右)
┌────┬──────┬─────────┐         ┌────┬──────────┐
│ id │ name │ dept_id │         │ id │ name     │
├────┼──────┼─────────┤         ├────┼──────────┤
│ 1  │ 张三 │ 1       │         │ 1  │ 技术部   │
│ 2  │ 李四 │ 1       │         │ 2  │ 市场部   │
│ 3  │ 王五 │ 2       │         │ 3  │ 财务部   │
└────┴──────┴─────────┘         └────┴──────────┘

RIGHT JOIN 结果(等价于 LEFT JOIN 调换表顺序):
┌────────┬────────┬──────────┬──────────┐
│ emp.id │emp.name│ dept.id  │ dept.name│
├────────┼────────┼──────────┼──────────┤
│   1    │ 张三   │    1     │ 技术部   │  ← 匹配成功,拼接
│   2    │ 李四   │    1     │ 技术部   │  ← 匹配成功,拼接
│   3    │ 王五   │    2     │ 市场部   │  ← 匹配成功,拼接
│  NULL  │ NULL   │    3     │ 财务部   │  ← 财务部无员工,左表全填 NULL
└────────┴────────┴──────────┴──────────┘

对比 LEFT JOIN 的结果(表顺序反过来):

LEFT JOIN 结果(FROM departments LEFT JOIN employees):
┌──────────┬──────────┬────────┬────────┐
│ dept.id  │ dept.name│ emp.id │emp.name│
├──────────┼──────────┼────────┼────────┤
│    1     │ 技术部   │   1    │ 张三   │
│    1     │ 技术部   │   2    │ 李四   │
│    2     │ 市场部   │   3    │ 王五   │
│    3     │ 财务部   │  NULL  │ NULL   │
└──────────┴──────────┴────────┴────────┘
← 列顺序不同,但信息完全等价

实践中几乎统一使用 LEFT JOIN("主表在左边"语义更直观)。RIGHT JOIN 总能改写为 LEFT JOIN,把 FROM 后面的表顺序对调即可。为了代码可读性,建议统一用 LEFT JOIN。

六、子查询(Subquery)

子查询就是"查询里的查询",一个 SELECT 嵌套在另一个 SQL 语句里面。内层的叫子查询(先执行),外层的叫主查询(后执行)。

理解子查询最自然的方式不是记"返回几行几列",而是看它写在 SQL 的哪个位置,不同位置决定了它能干什么、怎么写。

6.1 写在 WHERE 后面: 子查询提供过滤条件

这是子查询最常见的位置:里面的 SELECT 先算出一个结果,外层 WHERE 拿它当条件来过滤行。

① 和一个固定值比较

-- "查询薪资高于公司平均薪资的员工"
SELECT name, salary FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

② 查"在不在某个列表里"(IN)

-- "查询有员工的部门"
SELECT name FROM departments
WHERE id IN (
    SELECT DISTINCT dept_id FROM employees WHERE dept_id IS NOT NULL
);

执行过程:子查询先跑出所有被引用过的 dept_id 列表(如 1, 2, 3),外层判断 id 是否在这个列表中。

③ 和"列表中任意一个/全部"比较(ANY / ALL)

-- "薪资高于技术部任意一人的员工"(只要比最低的那个高就行)
SELECT name, salary FROM employees
WHERE salary > ANY (
    SELECT salary FROM employees WHERE dept_id = 1
);

-- "薪资高于技术部所有人的员工"(比最高的还高)
SELECT name, salary FROM employees
WHERE salary > ALL (
    SELECT salary FROM employees WHERE dept_id = 1
);
运算符含义帮你理解
> ANY (...)比子查询结果中至少一个等价于 > MIN(结果)
> ALL (...)比子查询结果中每一个都大等价于 > MAX(结果)
= ANY (...)等于子查询结果中任意一个等价于 IN (...)

④ 多列一起比较

-- "查询和'张三'同部门且同薪资的员工"
SELECT name FROM employees
WHERE (dept_id, salary) = (
    SELECT dept_id, salary FROM employees WHERE name = '张三'
);
-- 等价于 WHERE dept_id = (...) AND salary = (...),但一行搞定

⚠️ 写在 WHERE 后的子查询,返回的值必须能和外层做比较。如果子查询返回多行但你用了 =,数据库会直接报错。

6.2 写在 SELECT 后面 : 给每行附加一个计算结果

把子查询放在 SELECT 列表中:外层每查出一行,就触发子查询算一次,结果作为该行的一个新列。

-- "列出所有部门,每个部门后面显示该部门的员工人数"
SELECT d.name AS 部门,
       (SELECT COUNT(*) FROM employees e WHERE e.dept_id = d.id) AS 员工数
FROM departments d;
departments 表                    SELECT 后的子查询为每行附加计算结果:
┌────┬──────────┐                 ┌──────────┬────────┐
│ id │ name     │                 │ 部门     │ 员工数 │
├────┼──────────┤                 ├──────────┼────────┤
│ 1  │ 技术部   │  → 子查 COUNT  │ 技术部   │   3    │
│ 2  │ 市场部   │  → 子查 COUNT  │ 市场部   │   2    │
│ 3  │ 财务部   │  → 子查 COUNT  │ 财务部   │   2    │
│ 4  │ 人事部   │  → 子查 COUNT  │ 人事部   │   0    │
└────┴──────────┘                 └──────────┴────────┘

注意这里的子查询引用了外层 d.id,外层每换一行,子查询就用新的 d.id 重新统计一次。这是一种关联子查询(详见 6.5)。

放在 SELECT 后的子查询,必须只返回 1 行 1 列(单个值)。因为一个单元格只能填一个值。

6.3 写在 FROM 后面 :把子查询结果当临时表

子查询的结果是一张表,把它放在 FROM 后面,外层就能在这张"临时表"上继续 SELECT。

-- "从各部门的平均薪资统计中,筛选出平均薪资 > 12000 的部门"
SELECT dept_name, avg_salary
FROM (
    SELECT d.name AS dept_name, AVG(e.salary) AS avg_salary
    FROM departments d
    JOIN employees e ON d.id = e.dept_id
    GROUP BY d.id, d.name
) AS dept_stats          -- ← 必须给临时表起别名!
WHERE avg_salary > 12000;

执行过程:

  1. 先跑内层查询 → 得到一张表(部门名 + 平均薪资),起别名叫 dept_stats
  2. 外层把 dept_stats 当成普通表 → FROM dept_stats WHERE avg_salary > 12000
内层结果(dept_stats):          外层 WHERE 筛选后:
┌───────────┬────────────┐       ┌───────────┬────────────┐
│ dept_name │ avg_salary │       │ dept_name │ avg_salary │
├───────────┼────────────┤       ├───────────┼────────────┤
│ 技术部    │   17666    │  ✓    │ 技术部    │   17666    │
│ 市场部    │   11500    │  ✗    │ 财务部    │   13000    │
│ 财务部    │   13000    │  ✓    └───────────┴────────────┘
└───────────┴────────────┘

FROM 后的子查询必须给别名(MySQL 强制要求),即使别名在后续没用到。这种用法适合"分步计算":先在内层把复杂的中间结果算好,外层再简洁地筛选/排序。

6.4 写在 HAVING 后面 :对分组结果再过滤

和 WHERE 后类似,但作用于分组聚合之后。

-- "平均薪资高于全公司总平均的部门"
SELECT dept_id, AVG(salary) AS dept_avg
FROM employees
GROUP BY dept_id
HAVING AVG(salary) > (SELECT AVG(salary) FROM employees);

执行顺序:WHERE → GROUP BY → 聚合 → HAVING(子查询先算) → SELECT → ORDER BY。HAVING 后的子查询在分组完成之后才执行,所以能引用聚合结果。

6.5 关键概念:非关联子查询 vs 关联子查询

上面按"写在哪儿"讲完了用法,但有一个更底层的区别会影响性能和行为:子查询能不能脱离外层独立运行?

非关联子查询:独立运行一次,结果传给外层

子查询不引用外层任何列,你可以把它单独复制出来在数据库里跑通。

-- "查询技术部的所有员工"
SELECT name FROM employees
WHERE dept_id = (SELECT id FROM departments WHERE name = '技术部');
执行过程:
┌─────────────────────────────────────────────┐
│ ① 子查询先跑:SELECT id FROM departments    │
│    WHERE name = '技术部' → 结果:1           │
│                                             │
│ ② 用 1 替代子查询:                         │
│    WHERE dept_id = 1                        │
│                                             │
│ ③ 跑外层查询,返回结果                       │
└─────────────────────────────────────────────┘

子查询只跑 1 次,像一个先算好的常量。快,简单,绝大多数场景都是这种。

关联子查询:外层每行触发一次子查询

子查询引用了外层的列(如 e.dept_id),离开了外层它无法独立运行。

-- "查询薪资高于自己部门平均薪资的员工"
SELECT name, salary
FROM employees e
WHERE salary > (
    SELECT AVG(salary) FROM employees WHERE dept_id = e.dept_id
);
                                       ↑ 引用了外层 e.dept_id
执行过程(类比双层循环):
┌─────────────────────────────────────────────┐
│ 外层取第1行:张三(dept_id=1, salary=15000) │
│   → 子查询:AVG(salary) WHERE dept_id = 1   │
│   → 结果:17666                             │
│   → 15000 > 17666? → FALSE → 丢弃           │
│                                             │
│ 外层取第2行:李四(dept_id=2, salary=12000) │
│   → 子查询:AVG(salary) WHERE dept_id = 2   │
│   → 结果:11500                             │
│   → 12000 > 11500? → TRUE → 保留            │
│                                             │
│ 外层取第3行:王五(dept_id=1, salary=18000) │
│   → 子查询:AVG(salary) WHERE dept_id = 1   │
│   → 结果:17666                             │
│   → 18000 > 17666? → TRUE → 保留            │
│  ...                                        │
└─────────────────────────────────────────────┘

外层 N 行 → 子查询跑 N 次。外层行数多时性能会很差。

怎么区分?

特征非关联子查询关联子查询
子查询里出现了外层表的列?❌ 没有✅ 有(如 e.dept_id
能单独复制子查询运行吗?✅ 能❌ 不能(会报找不到列)
子查询跑几次?1 次外层行数次
性能外层行多时慢
典型写法WHERE x = (SELECT ...)WHERE x > (SELECT AVG...WHERE 列 = 外层.列)

理解这两种执行方式的区别,比记"返回几行几列"重要得多。它是排查慢查询和写出高效子查询的基础。

6.6 EXISTS:只问"有没有",不问"是什么"

EXISTS 是一种特殊的 WHERE 后子查询。它不返回数据,只告诉外层:里面的查询有没有找到行。

-- "查询至少有一名员工的部门"
SELECT name FROM departments d
WHERE EXISTS (
    SELECT 1 FROM employees e WHERE e.dept_id = d.id
);

EXISTS 的几个独特之处:

  • SELECT 1 是惯用写法:我们根本不关心返回什么列,只要有行就返回 TRUE。写成 SELECT * 效果一样,但 SELECT 1 明确传达了"我只在乎有没有结果"。
  • 短路执行:数据库在内层找到第一条匹配的行就立刻停止,不会傻傻扫完全表。这是它比 IN 快的关键原因。
  • 天然免疫 NULLNOT EXISTS 不会像 NOT IN 那样因为子查询结果含 NULL 而导致整个结果为空。
-- "查询没有员工的部门",两种写法对比
-- 写法A:NOT IN(有 NULL 风险)
SELECT name FROM departments
WHERE id NOT IN (SELECT dept_id FROM employees WHERE dept_id IS NOT NULL);
                                          -- ↑ 必须加 IS NOT NULL 防 NULL

-- 写法B:NOT EXISTS(天然安全,推荐)
SELECT name FROM departments d
WHERE NOT EXISTS (
    SELECT 1 FROM employees e WHERE e.dept_id = d.id
);

EXISTS vs IN 怎么选?

情况推荐原因
子查询结果集很小IN简单直观,先算列表再匹配
子查询表很大EXISTS找到第一条就停,不扫全表
需要 NOT 语义NOT EXISTS安全,不用操心 NULL
子查询需要关联外层列EXISTSEXISTS 天生就是关联子查询

七、联合查询(UNION)

UNION 将多条 SELECT 的结果纵向合并为一个结果集。JOIN 是横向拼接(加列),UNION 是纵向堆叠(加行)。

-- 把北京和上海员工的名单纵向合并为一张表
SELECT name, phone, city FROM employees WHERE city = '北京'
UNION
SELECT name, phone, city FROM employees WHERE city = '上海';
对比UNIONUNION ALL
去重✅ 自动去重,所有列完全相同的行只保留一行❌ 不去重,全部保留
性能较慢(需排序 + 比较去重)快(直接追加结果集)
使用场景明确需要去重时绝大多数场景,知道没重复,或需要保留全部行

使用 UNION 的硬性条件:

  • 每条 SELECT 的列数必须相同
  • 对应列的数据类型必须兼容(不要求完全相同,但需可隐式转换)
  • 最终结果集的列名以第一条 SELECT 的列名为准
  • ORDER BY 只能放在最后一条 SELECT 之后,对合并后的全集排序
-- 常见误区:ORDER BY 不能写在中间的 SELECT 里
SELECT name, salary FROM employees WHERE dept_id = 1
UNION ALL
SELECT name, salary FROM employees WHERE dept_id = 2
ORDER BY salary DESC;   -- ✅ 对整个合并结果排序

默认使用 UNION ALL,只有确定需要去重时才用 UNIONUNION 的去重依赖排序,百万级数据量下开销非常可观。

附:命令速查

分类操作语句
DML-增插入一行(指定字段)INSERT INTO 表(列1,列2) VALUES(值1,值2);
DML-增批量插入INSERT INTO 表(列1,列2) VALUES(值1,值2),(值3,值4);
DML-删条件删除DELETE FROM 表 WHERE 条件;
DML-删清空表(重置自增)TRUNCATE TABLE 表名;
DML-改条件更新UPDATE 表 SET 列1=值1, 列2=值2 WHERE 条件;
DQL基本查询SELECT 列1, 列2 FROM 表 WHERE 条件;
DQL模糊查询WHERE 列 LIKE '张%'
DQL范围查询WHERE 列 BETWEEN a AND b / WHERE 列 IN (v1, v2)
DQL空值判断WHERE 列 IS NULL / IS NOT NULL
DQL排序ORDER BY 列 ASC/DESC
DQL分组聚合GROUP BY 列 + COUNT/MAX/MIN/SUM/AVG
DQL分组后过滤HAVING 条件
DQL去重SELECT DISTINCT 列
DQL分页LIMIT 起始偏移, 行数
多表-连接内连接JOIN 表2 ON 条件
多表-连接左连接(保左全)LEFT JOIN 表2 ON 条件
多表-连接右连接(保右全)RIGHT JOIN 表2 ON 条件
多表-连接交叉连接(笛卡尔积)CROSS JOIN 表2
多表-连接自连接JOIN 同表 AS 别名 ON 条件
多表-连接多表连接FROM A JOIN B ON 条件 JOIN C ON 条件
多表-子查询WHERE 后:单值比较WHERE col > (SELECT AVG(col) FROM 表)
多表-子查询WHERE 后:IN 列表WHERE col IN (SELECT col FROM 表)
多表-子查询WHERE 后:ANY / ALLWHERE col > ANY/ALL (SELECT col FROM 表)
多表-子查询WHERE 后:多列比较WHERE (col1, col2) = (SELECT col1, col2 FROM ...)
多表-子查询FROM 后:派生表FROM (SELECT ...) AS 别名
多表-子查询EXISTS(有无判断)WHERE EXISTS (SELECT 1 FROM 表 WHERE 条件)
多表-联合去重合并SELECT ... UNION SELECT ...
多表-联合不去重合并SELECT ... UNION ALL SELECT ...
多表-DDL添加外键ALTER TABLE 从表 ADD FOREIGN KEY (列) REFERENCES 主表(列);

总结

到此这篇关于MySQL数据操作与查询笔记之DML、DQL、多表连接与子查询的文章就介绍到这了,更多相关MySQL DML、DQL、多表连接与子查询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Mysql字符集utf8和utf8mb4详解

    Mysql字符集utf8和utf8mb4详解

    文章介绍了MySQL中utf8和utf8mb4两种字符集的区别,包括编码方式、存储空间、索引长度以及支持的Unicode字符范围,同时,通过创建两个表并插入数据进行存储长度的比较,验证了上述理论
    2024-12-12
  • MySQL库的操作大全

    MySQL库的操作大全

    MySQL是一个数据库管理系统,在其中我们可以创建许多的数据库,数据库中中又可以存储许多的表,本文给大家介绍MySQL库的操作大全,感兴趣的朋友跟随小编一起看看吧
    2025-05-05
  • mysql启动服务报1058错误的解决方法

    mysql启动服务报1058错误的解决方法

    这篇文章主要介绍了mysql启动服务报1058错误的解决方法,需要的朋友可以参考下
    2014-03-03
  • mysql字符切割的四种方式汇总

    mysql字符切割的四种方式汇总

    这篇文章主要介绍了mysql字符切割的四种方式汇总,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • MySQL中使用表别名与字段别名的基本教程

    MySQL中使用表别名与字段别名的基本教程

    这篇文章主要介绍了MySQL中使用表别名与字段别名的基本教程,利用SELECT语句和AS子句进行取别名的操作,需要的朋友可以参考下
    2015-12-12
  • MySQL CTE (Common Table Expressions)示例全解析

    MySQL CTE (Common Table Expressions)示例全解

    MySQL 8.0引入CTE,支持递归查询,可创建临时命名结果集,提升复杂查询的可读性与维护性,适用于层次结构数据处理,但需注意性能和递归深度限制,本文给大家介绍MySQL CTE (Common Table Expressions)示例,感兴趣的朋友一起看看吧
    2025-07-07
  • MySql中怎样查询表是否被锁

    MySql中怎样查询表是否被锁

    这篇文章主要介绍了MySql中怎样查询表是否被锁问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • mysql 8.0.12 安装配置方法图文教程(windows10)

    mysql 8.0.12 安装配置方法图文教程(windows10)

    这篇文章主要为大家详细介绍了windows10下mysql 8.0.12 安装配置方法图文教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-08-08
  • MySQL分区表的基本入门教程

    MySQL分区表的基本入门教程

    这篇文章主要给大家介绍了关于MySQL分区表的基本入门教程,文中通过示例代码介绍的非常详细,对大家学习或者使用MySQL具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-05-05
  • MYSQL常用字符串函数和时间函数示例详解

    MYSQL常用字符串函数和时间函数示例详解

    字符串函数是最常用的的一种函数,在一个具体应用中通常会综合几个甚至几类函数来实现相应的应用,这篇文章主要介绍了MYSQL常用字符串函数和时间函数的相关资料,需要的朋友可以参考下
    2025-07-07

最新评论