在实际的 Web 开发中,单一的数据表几乎无法满足业务需求 —— 用户、文章、评论、订单等数据必然分散在不同表中,而业务逻辑又需要将这些分散的数据关联起来。掌握多表关系设计是保证数据完整性的基础,掌握多表查询则是从关联表中提取有效信息的核心能力。本文将从多表关系的本质出发,结合实战案例讲解关系设计原则,再深入剖析多表查询的各类方式及优化技巧,帮你彻底搞定 MySQL 多表操作。
一、核心基础:理解多表关系的三种类型
数据库设计的核心是 “按业务拆分表,按关系关联表”,MySQL 中多表之间的关系可归纳为三类:一对一、一对多(多对一)、多对多,每种关系对应不同的实现方式和业务场景。
1. 一对多(多对一):最常见的业务关系
定义
A 表中的一条记录可以对应 B 表中的多条记录,而 B 表中的一条记录只能对应 A 表中的一条记录(反向视角即为 “多对一”)。
典型场景
- 部门管理系统:「部门表(dept)」→「员工表(emp)」:一个部门可包含多个员工,一个员工仅属于一个部门。;
- 电商系统:「订单表(order)」→「商品表(product)」(一个订单可包含多个商品?不,这里是订单 - 订单项 - 商品,正确场景:「分类表(category)」→「商品表(product)」,一个分类下有多个商品,一个商品仅属于一个分类)。
实现方式
在 “多” 的一方表中添加外键,关联 “一” 的一方表的主键。
实战案例(部门 - 员工) :
-- 1. 一的一方:部门表(dept)
create table dept
(
id int unsigned primary key auto_increment comment 'ID, 主键',
name varchar(10) not null unique comment '部门名称',
create_time datetime comment '创建时间',
update_time datetime comment '修改时间'
) comment '部门表';
-- 2. 多的一方:员工表(emp)
create table emp
(
id int unsigned primary key auto_increment comment 'ID,主键',
username varchar(20) not null unique comment '用户名',
password varchar(32) not null comment '密码',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别, 1:男, 2:女',
phone char(11) not null unique comment '手机号',
job tinyint unsigned comment '职位, 1:班主任,2:讲师,3:学工主管,4:教研主管,5:咨询师',
salary int unsigned comment '薪资',
image varchar(300) comment '头像',
entry_date date comment '入职日期',
# dept_id int unsigned COMMENT '关联的部门ID',
dept_id int unsigned COMMENT '关联的部门ID',
create_time datetime comment '创建时间',
update_time datetime comment '修改时间'
) comment '员工表';
-- 修改表: 添加外键约束
alter table emp add constraint fk_dept_id foreign key (dept_id) references dept(id);
关键说明:
- 外键约束不是必须的(如高性能场景会放弃外键,靠业务代码保证),但设计阶段可以添加,保证数据一致性。
- 在现在的企业开发中,很少会使用物理外键,都是使用逻辑外键。 甚至在一些数据库开发规范中,会明确指出禁止使用物理外键 foreign key
2. 一对一:最特殊的关系
定义
A 表中的一条记录仅对应 B 表中的一条记录,反之亦然。
典型场景
- 「用户表(user)」→「用户详情表(user_detail)」(用户的基础信息存在 user 表,头像、地址、生日等扩展信息存在 user_detail,拆分表提升查询效率);
- 「员工表(employee)」→「工牌表(employee_card)」(一个员工对应一个工牌)。
实现方式
- 方式 1:在任意一方添加外键,且外键设置为唯一(
UNIQUE); - 方式 2:两张表共用同一个主键(推荐,更高效)。
实战案例(用户 - 用户详情) :
-- 1. 用户基础表
CREATE TABLE `user` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID(主键)',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`email` VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 用户详情表(与用户表一对一,共用主键)
CREATE TABLE `user_detail` (
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID(主键+外键)',
`avatar` VARCHAR(255) COMMENT '用户头像',
`address` VARCHAR(500) COMMENT '用户地址',
`birthday` DATE COMMENT '生日',
PRIMARY KEY (`user_id`), -- 主键复用用户表ID
CONSTRAINT `fk_user_detail_user` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户详情表';
3. 多对多:需中间表关联
定义
A 表中的一条记录可对应 B 表中的多条记录,B 表中的一条记录也可对应 A 表中的多条记录。
典型场景
- 博客系统:「文章表(article)」→「标签表(tag)」(一篇文章可打多个标签,一个标签可关联多篇文章);
- 电商系统:「商品表(product)」→「订单表(order)」(一个商品可出现在多个订单,一个订单可包含多个商品)。
实现方式
创建中间关联表,表中包含两个外键,分别关联两张主表的主键,同时可添加中间表自身的属性(如关联时间、数量等)。
二、前置准备:插入测试数据(部门 - 员工场景)
为了演示多表查询效果,先插入基础测试数据:
-- 准备测试数据
insert into dept (id, name, create_time, update_time)
values (1, '学工部', now(), now()),
(2, '教研部', now(), now()),
(3, '咨询部', now(), now()),
(4, '就业部', now(), now()),
(5, '人事部', now(), now());
INSERT INTO emp
VALUES (1, 'shinaian', '123456', '施耐庵', 1, '13309090001', 4, 15000,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2000-01-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:35:35'),
(2, 'songjiang', '123456', '宋江', 1, '13309090002', 2, 8600,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2015-01-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:35:37'),
(3, 'lujunyi', '123456', '卢俊义', 1, '13309090003', 2, 8900,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2008-05-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:35:39'),
(4, 'wuyong', '123456', '吴用', 1, '13309090004', 2, 9200,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2007-01-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:35:41'),
(5, 'gongsunsheng', '123456', '公孙胜', 1, '13309090005', 2, 9500,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2012-12-05', 2, '2023-10-27 16:35:33',
'2023-10-27 16:35:43'),
(6, 'huosanniang', '123456', '扈三娘', 2, '13309090006', 3, 6500,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2013-09-05', 1, '2023-10-27 16:35:33',
'2023-10-27 16:35:45'),
(7, 'chaijin', '123456', '柴进', 1, '13309090007', 1, 4700,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2005-08-01', 1, '2023-10-27 16:35:33',
'2023-10-27 16:35:47'),
(8, 'likui', '123456', '李逵', 1, '13309090008', 1, 4800,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2014-11-09', 1, '2023-10-27 16:35:33',
'2023-10-27 16:35:49'),
(9, 'wusong', '123456', '武松', 1, '13309090009', 1, 4900,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2011-03-11', 1, '2023-10-27 16:35:33',
'2023-10-27 16:35:51'),
(10, 'lichong', '123456', '林冲', 1, '13309090010', 1, 5000,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2013-09-05', 1, '2023-10-27 16:35:33',
'2023-10-27 16:35:53'),
(11, 'huyanzhuo', '123456', '呼延灼', 1, '13309090011', 2, 9700,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2007-02-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:35:55'),
(12, 'xiaoliguang', '123456', '小李广', 1, '13309090012', 2, 10000,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2008-08-18', 2, '2023-10-27 16:35:33',
'2023-10-27 16:35:57'),
(13, 'yangzhi', '123456', '杨志', 1, '13309090013', 1, 5300,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2012-11-01', 1, '2023-10-27 16:35:33',
'2023-10-27 16:35:59'),
(14, 'shijin', '123456', '史进', 1, '13309090014', 2, 10600,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2002-08-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:36:01'),
(15, 'sunerniang', '123456', '孙二娘', 2, '13309090015', 2, 10900,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2011-05-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:36:03'),
(16, 'luzhishen', '123456', '鲁智深', 1, '13309090016', 2, 9600,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2010-01-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:36:05'),
(17, 'liying', '12345678', '李应', 1, '13309090017', 1, 5800,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2015-03-21', 1, '2023-10-27 16:35:33',
'2023-10-27 16:36:07'),
(18, 'shiqian', '123456', '时迁', 1, '13309090018', 2, 10200,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2015-01-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:36:09'),
(19, 'gudasao', '123456', '顾大嫂', 2, '13309090019', 2, 10500,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2008-01-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:36:11'),
(20, 'ruanxiaoer', '123456', '阮小二', 1, '13309090020', 2, 10800,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2018-01-01', 2, '2023-10-27 16:35:33',
'2023-10-27 16:36:13'),
(21, 'ruanxiaowu', '123456', '阮小五', 1, '13309090021', 5, 5200,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2015-01-01', 3, '2023-10-27 16:35:33',
'2023-10-27 16:36:15'),
(22, 'ruanxiaoqi', '123456', '阮小七', 1, '13309090022', 5, 5500,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2016-01-01', 3, '2023-10-27 16:35:33',
'2023-10-27 16:36:17'),
(23, 'ruanji', '123456', '阮籍', 1, '13309090023', 5, 5800,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2012-01-01', 3, '2023-10-27 16:35:33',
'2023-10-27 16:36:19'),
(24, 'tongwei', '123456', '童威', 1, '13309090024', 5, 5000,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2006-01-01', 3, '2023-10-27 16:35:33',
'2023-10-27 16:36:21'),
(25, 'tongmeng', '123456', '童猛', 1, '13309090025', 5, 4800,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2002-01-01', 3, '2023-10-27 16:35:33',
'2023-10-27 16:36:23'),
(26, 'yanshun', '123456', '燕顺', 1, '13309090026', 5, 5400,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2011-01-01', 3, '2023-10-27 16:35:33',
'2023-10-27 16:36:25'),
(27, 'lijun', '123456', '李俊', 1, '13309090027', 5, 6600,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2004-01-01', 3, '2023-10-27 16:35:33',
'2023-10-27 16:36:27'),
(28, 'lizhong', '123456', '李忠', 1, '13309090028', 5, 5000,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2007-01-01', 3, '2023-10-27 16:35:33',
'2023-10-27 16:36:29'),
(29, 'songqing', '123456', '宋清', 1, '13309090029', NULL, 5100,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2020-01-01', NULL, '2023-10-27 16:35:33',
'2023-10-27 16:36:31'),
(30, 'liyun', '123456', '李云', 1, '13309090030', NULL, NULL,
'https://dawn-itcast.oss-cn-hangzhou.aliyuncs.com/01.png', '2020-03-01', NULL, '2023-10-27 16:35:33',
'2023-10-27 16:36:31');
三、核心操作:多表查询的六大方式(部门 - 员工场景)
多表查询的本质是 “通过关联条件将多张表的数据拼接成一个结果集”,我们以 “部门 - 员工” 为核心场景,讲解六大常用查询方式。
1. 内连接(INNER JOIN):只查匹配的记录
定义
只返回多张表中满足关联条件的记录,是企业系统中最常用的多表查询方式(如 “查询有归属部门的员工及所属部门”)。
语法
-- 显式内连接(推荐)
SELECT 字段列表 FROM 表1
INNER JOIN 表2 ON 表1.关联字段 = 表2.关联字段
[WHERE 条件];
-- 隐式内连接(等价于INNER JOIN)
SELECT 字段列表 FROM 表1, 表2 WHERE 表1.关联字段 = 表2.关联字段;
实战案例
-- ============================= 内连接 ==========================
-- A. 查询所有员工的ID, 姓名 , 及所属的部门名称 (隐式、显式内连接实现)
-- 隐式内连接
select e.id, e.name, d.name
from dept d,
emp e
where e.dept_id = d.id;
-- 显式内连接
select e.id, e.name, d.name
from dept d
inner join emp e on e.dept_id = d.id;
-- B. 查询 性别为男, 且工资 高于8000 的员工的ID, 姓名, 及所属的部门名称 (隐式、显式内连接实现)
-- 隐式内连接
select e.id, e.name, d.name
from dept d,
emp e
where e.dept_id = d.id
and e.salary > 8000
and e.gender = 1;
-- 显式内连接
select e.id, e.name, d.name
from dept d
inner join emp e on e.dept_id = d.id
where e.salary > 8000
and e.gender = 1;
核心特点:内连接过滤掉 “无部门的员工” 和 “无员工的部门”,只保留匹配的记录。
2. 外连接:查匹配 + 不匹配的记录
外连接分为左外连接(LEFT JOIN)、右外连接(RIGHT JOIN)、全外连接(FULL JOIN) ,核心是 “保留某一方的所有记录,另一方匹配不上则显示 NULL”。
(1)左外连接(LEFT JOIN):保留左表所有记录
语法
SELECT 字段列表 FROM 表1
LEFT JOIN 表2 ON 表1.关联字段 = 表2.关联字段
[WHERE 条件];
实战案例
-- =============================== 外连接 ============================
-- A. 查询员工表 所有 员工的姓名, 和对应的部门名称 (左外连接)
select e.name, d.name
from emp e
left join dept d on e.dept_id = d.id;
-- B. 查询部门表 所有 部门的名称, 和对应的员工名称 (右外连接)
select e.name, d.name
from emp e
right join dept d on e.dept_id = d.id;
-- 部门是5条数据,3个部门匹配的员工28条,由于2个部门没有员工,所以最后28条+2条=30条
-- C. 查询工资 高于8000 的 所有员工的姓名, 和对应的部门名称 (左外连接)
select e.name, d.name
from emp e
left join dept d on e.dept_id = d.id
where e.salary > 8000;
(2)右外连接(RIGHT JOIN):保留右表所有记录
语法
SELECT 字段列表 FROM 表1
RIGHT JOIN 表2 ON 表1.关联字段 = 表2.关联字段
[WHERE 条件];
实战案例
-- B. 查询部门表 所有 部门的名称, 和对应的员工名称 (右外连接)
select e.name, d.name
from emp e
right join dept d on e.dept_id = d.id;
-- 部门是5条数据,3个部门匹配的员工28条,由于2个部门没有员工,所以最后28条+2条=30条
3. 子查询:嵌套查询(分步拆解业务)
定义
将一个查询结果作为另一个查询的条件 / 数据源,分为标量子查询(返回单个值)、列子查询(返回一列)、表子查询(返回多列多行) 。
实战案例1:标量子查询
-- ========================= 子查询 ================================
-- 标量子查询
-- A. 查询 最早入职 的员工信息
-- 分步实现
-- 第一步:查询最早入职日期
select min(entry_date)
from emp;
-- 2000-01-01
-- 第二步:根据日期筛选员工信息
select *
from emp
where entry_date = '2000-01-01';
-- 子查询实现
select *
from emp
where entry_date = (select min(entry_date) from emp);
-- B. 查询在 "阮小五" 入职之后入职的员工信息
-- 分步实现
-- 第一步:查询"阮小五"的入职时间
select entry_date
from emp
where name = '阮小五';
-- 2015-01-01
-- 第二步:根据阮小五的入职时间来查询在这个入职时间之后的员工信息
select *
from emp
where entry_date > '2015-01-01';
-- 子查询实现
select *
from emp
where entry_date > (select entry_date from emp where name = '阮小五');
实战案例2:列子查询
-- 列子查询
-- A. 查询 "教研部" 和 "咨询部" 的所有员工信息
-- 分步实现
-- 第一步:查询 "教研部" 和 "咨询部" 的部门id
select id
from dept
where name in ('教研部', '咨询部');
-- 第二步:根据部门id查询所有的员工信息
select *
from emp
where dept_id in (2, 3);
-- 子查询实现
select *
from emp
where dept_id in (select id
from dept
where name in ('教研部', '咨询部'));
实战案例3:行子查询
-- 行子查询
-- A. 查询与 "李忠" 的薪资 及 职位都相同的员工信息 ;
-- 分步查询
-- 第一步:查询出"李忠"的薪资和职位
select salary, job
from emp
where name = '李忠';
-- 第二步:根据"李忠"的薪资和职位查询出薪资和职位都相同的成员
select *
from emp
where salary = 5000
and job = 5
and name != '李忠';
-- 子查询实现
select *
from emp
where salary = (select salary
from emp
where name = '李忠')
and job = (select job
from emp
where name = '李忠')
and name != '李忠';
-- 优化
select *
from emp
where (salary, job) = (select salary, job
from emp
where name = '李忠')
and name != '李忠';
实战案例4:表子查询
-- 表子查询
-- A. 查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门信息
select e.*, d.*
from (select *
from emp
where entry_date > '2006-01-01'-- 返回虚拟表
) e,
dept d
where e.dept_id = d.id;
-- 表查询
select *
from emp e,
dept d
where e.entry_date > '2006-01-01'
and e.dept_id = d.id;
注意
- 子查询,表连接 推荐使用哪个?
- 答:一般推荐表连接,因为子查询构建虚拟表一般性能慢。但是具体哪个快要看具体的执行时间决定。
4. 自连接:表自身关联查询(员工 - 上级场景)
定义
将一张表当作两张表使用,通过别名区分,用于查询表内的层级关系(如员工与直属上级、部门的上下级)。
5. 联合查询(UNION/UNION ALL):合并结果集
定义
将多个 SELECT 语句的结果集合并为一个,要求各 SELECT 的字段数、字段类型一致。
UNION:去重合并(过滤重复记录,性能略低);UNION ALL:直接合并(不查重,性能更高,推荐优先使用)。
6. 交叉连接(CROSS JOIN):笛卡尔积(特殊场景)
定义
无需关联条件,返回两张表的笛卡尔积(表 1 行数 × 表 2 行数),实际开发中极少直接使用,仅用于生成测试数据等特殊场景。
语法
SELECT 字段列表 FROM 表1 CROSS JOIN 表2;
四、总结与拓展
本文以 “部门 - 员工” 这一经典场景为核心,讲解了多表关系的三种类型(一对多、一对一、多对多),以及多表查询的六大方式(内连接、外连接、子查询、自连接、联合查询、交叉连接),并补充了性能优化技巧。核心要点总结:
-
多表关系设计(部门 - 员工场景) :
- 一对多:员工表加
dept_id外键关联部门表; - 一对一:员工详情表复用员工表 ID 作为主键;
- 多对多:员工 - 角色通过中间表
emp_role关联。
- 一对多:员工表加
-
多表查询选择:
- 基础关联查询:优先内连接;
- 需保留无匹配记录:用左 / 右外连接;
- 表内层级关系(如员工 - 上级):用自连接;
- 合并结果集:用 UNION ALL(优先,无查重)。
-
性能优化核心:
- 关联字段加索引;
- 避免 SELECT *;
- 用 EXPLAIN 分析执行计划;
- 减少表连接数量。
拓展方向:
- 分库分表:高并发场景下,员工表 / 部门表数据量过大时,可通过 Sharding-JDBC 拆分表并实现多表查询;
- 视图:将常用的多表查询(如 “员工 - 部门 - 角色”)封装为视图,简化查询逻辑;
- 存储过程:复杂的统计查询(如 “各部门年度薪资报表”)可封装为存储过程,提升执行效率。
多表关系设计和多表查询是 MySQL 的核心难点,也是企业级开发中最常用的技能。掌握这些知识,不仅能保证数据结构的合理性,还能高效提取业务所需的数据,为系统的性能和稳定性打下坚实基础。