MySQL 核心实战:多表关系设计与多表查询全解析

43 阅读18分钟

在实际的 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 字段列表 FROM1
INNER JOIN2 ON1.关联字段 =2.关联字段
[WHERE 条件];

-- 隐式内连接(等价于INNER JOIN)
SELECT 字段列表 FROM1, 表2 WHERE1.关联字段 =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 字段列表 FROM1
LEFT JOIN2 ON1.关联字段 =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 字段列表 FROM1
RIGHT JOIN2 ON1.关联字段 =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 字段列表 FROM1 CROSS JOIN2;

四、总结与拓展

本文以 “部门 - 员工” 这一经典场景为核心,讲解了多表关系的三种类型(一对多、一对一、多对多),以及多表查询的六大方式(内连接、外连接、子查询、自连接、联合查询、交叉连接),并补充了性能优化技巧。核心要点总结:

  1. 多表关系设计(部门 - 员工场景)

    • 一对多:员工表加dept_id外键关联部门表;
    • 一对一:员工详情表复用员工表 ID 作为主键;
    • 多对多:员工 - 角色通过中间表emp_role关联。
  2. 多表查询选择

    • 基础关联查询:优先内连接;
    • 需保留无匹配记录:用左 / 右外连接;
    • 表内层级关系(如员工 - 上级):用自连接;
    • 合并结果集:用 UNION ALL(优先,无查重)。
  3. 性能优化核心

    • 关联字段加索引;
    • 避免 SELECT *;
    • 用 EXPLAIN 分析执行计划;
    • 减少表连接数量。

拓展方向:

  • 分库分表:高并发场景下,员工表 / 部门表数据量过大时,可通过 Sharding-JDBC 拆分表并实现多表查询;
  • 视图:将常用的多表查询(如 “员工 - 部门 - 角色”)封装为视图,简化查询逻辑;
  • 存储过程:复杂的统计查询(如 “各部门年度薪资报表”)可封装为存储过程,提升执行效率。

多表关系设计和多表查询是 MySQL 的核心难点,也是企业级开发中最常用的技能。掌握这些知识,不仅能保证数据结构的合理性,还能高效提取业务所需的数据,为系统的性能和稳定性打下坚实基础。