MySQL 表关联关系详解
本文档详细列举了MySQL中常见的表关联关系场景以及对应的SQL语句示例。
1. 一对一关系 (One-to-One)
场景:用户表和用户详情表
- 一个用户对应一个用户详情
- 通常用于将大表拆分,提高查询性能
-- 创建用户表
CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID,主键,自增',username VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名,唯一,不能为空',email VARCHAR(100) NOT NULL COMMENT '邮箱地址,不能为空',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,默认当前时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户基础信息表';-- 创建用户详情表
CREATE TABLE user_profiles (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '详情ID,主键,自增',user_id INT UNIQUE NOT NULL COMMENT '用户ID,外键,唯一,不能为空',first_name VARCHAR(50) COMMENT '名字',last_name VARCHAR(50) COMMENT '姓氏',phone VARCHAR(20) COMMENT '电话号码',address TEXT COMMENT '详细地址',FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户详细信息表';-- 插入测试数据
INSERT INTO users (username, email) VALUES
('张三', 'zhangsan@example.com'),
('李四', 'lisi@example.com'),
('王五', 'wangwu@example.com'),
('赵六', 'zhaoliu@example.com'),
('钱七', 'qianqi@example.com');INSERT INTO user_profiles (user_id, first_name, last_name, phone, address) VALUES
(1, '三', '张', '13800138001', '北京市朝阳区建国门外大街1号'),
(2, '四', '李', '13800138002', '上海市浦东新区陆家嘴环路1000号'),
(3, '五', '王', '13800138003', '深圳市南山区科技园中区科苑路15号'),
(4, '六', '赵', '13800138004', '广州市天河区珠江新城花城大道5号'),
(5, '七', '钱', '13800138005', '杭州市西湖区文三路259号');-- 查询用户及其详情
SELECT u.username, u.email, p.first_name, p.last_name, p.phone
FROM users u
LEFT JOIN user_profiles p ON u.id = p.user_id
WHERE u.id = 1;
2. 一对多关系 (One-to-Many)
场景:用户表和订单表
- 一个用户可以有多个订单
- 这是最常见的关联关系
-- 创建用户表
CREATE TABLE customers (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '客户ID,主键,自增',username VARCHAR(50) NOT NULL COMMENT '用户名,不能为空',email VARCHAR(100) NOT NULL COMMENT '邮箱地址,不能为空',phone VARCHAR(20) COMMENT '电话号码',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,默认当前时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户信息表';-- 创建订单表
CREATE TABLE orders (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID,主键,自增',user_id INT NOT NULL COMMENT '用户ID,外键,不能为空',order_number VARCHAR(50) NOT NULL COMMENT '订单编号,不能为空',total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额,不能为空',order_status VARCHAR(20) DEFAULT 'pending' COMMENT '订单状态,默认待处理',order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '订单日期,默认当前时间',FOREIGN KEY (user_id) REFERENCES customers(id) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单信息表';-- 插入测试数据
INSERT INTO customers (username, email, phone) VALUES
('张三', 'zhangsan@example.com', '13800138001'),
('李四', 'lisi@example.com', '13800138002'),
('王五', 'wangwu@example.com', '13800138003'),
('赵六', 'zhaoliu@example.com', '13800138004'),
('钱七', 'qianqi@example.com', '13800138005');INSERT INTO orders (user_id, order_number, total_amount, order_status) VALUES
(1, 'ORD2024001', 299.99, 'completed'),
(1, 'ORD2024002', 199.50, 'pending'),
(2, 'ORD2024003', 599.00, 'completed'),
(2, 'ORD2024004', 89.99, 'shipped'),
(3, 'ORD2024005', 1299.99, 'completed'),
(3, 'ORD2024006', 399.99, 'pending'),
(4, 'ORD2024007', 799.99, 'completed'),
(5, 'ORD2024008', 99.99, 'cancelled');-- 查询用户及其所有订单
SELECT c.username, o.order_number, o.total_amount, o.order_status, o.order_date
FROM customers c
LEFT JOIN orders o ON c.id = o.user_id
WHERE c.id = 1
ORDER BY o.order_date DESC;-- 查询每个用户的订单数量
SELECT c.username, COUNT(o.id) as order_count, SUM(o.total_amount) as total_spent
FROM customers c
LEFT JOIN orders o ON c.id = o.user_id
GROUP BY c.id, c.username;
3. 多对多关系 (Many-to-Many)
场景:学生表和课程表
- 一个学生可以选修多门课程
- 一门课程可以被多个学生选修
- 需要中间表来维护关系
-- 创建学生表
CREATE TABLE students (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID,主键,自增',name VARCHAR(50) NOT NULL COMMENT '学生姓名,不能为空',student_number VARCHAR(20) UNIQUE NOT NULL COMMENT '学号,唯一,不能为空',gender VARCHAR(10) COMMENT '性别',birth_date DATE COMMENT '出生日期',email VARCHAR(100) COMMENT '邮箱地址',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,默认当前时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生信息表';-- 创建课程表
CREATE TABLE courses (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '课程ID,主键,自增',course_name VARCHAR(100) NOT NULL COMMENT '课程名称,不能为空',course_code VARCHAR(20) UNIQUE NOT NULL COMMENT '课程代码,唯一,不能为空',credits INT NOT NULL COMMENT '学分,不能为空',teacher VARCHAR(50) COMMENT '任课教师',max_students INT DEFAULT 30 COMMENT '最大学生数,默认30人',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,默认当前时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程信息表';-- 创建学生-课程关联表(中间表)
CREATE TABLE student_courses (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '关联ID,主键,自增',student_id INT NOT NULL COMMENT '学生ID,外键,不能为空',course_id INT NOT NULL COMMENT '课程ID,外键,不能为空',enrollment_date DATE DEFAULT (CURRENT_DATE) COMMENT '选课日期,默认当前日期',grade VARCHAR(5) COMMENT '成绩',semester VARCHAR(20) COMMENT '学期',status VARCHAR(20) DEFAULT 'enrolled' COMMENT '状态,默认已选课',FOREIGN KEY (student_id) REFERENCES students(id) ON UPDATE CASCADE ON DELETE CASCADE,FOREIGN KEY (course_id) REFERENCES courses(id) ON UPDATE CASCADE ON DELETE CASCADE,UNIQUE KEY unique_student_course (student_id, course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生选课关联表';-- 插入测试数据
INSERT INTO students (name, student_number, gender, birth_date, email) VALUES
('张三', 'S2024001', '男', '2003-05-15', 'zhangsan@student.edu.cn'),
('李四', 'S2024002', '女', '2003-08-22', 'lisi@student.edu.cn'),
('王五', 'S2024003', '男', '2003-12-10', 'wangwu@student.edu.cn'),
('赵六', 'S2024004', '女', '2003-03-18', 'zhaoliu@student.edu.cn'),
('钱七', 'S2024005', '男', '2003-11-25', 'qianqi@student.edu.cn');INSERT INTO courses (course_name, course_code, credits, teacher, max_students) VALUES
('高等数学', 'MATH101', 4, '王教授', 50),
('大学英语', 'ENG101', 3, '李教授', 40),
('计算机基础', 'CS101', 3, '张教授', 35),
('数据结构', 'CS201', 4, '陈教授', 30),
('数据库原理', 'CS301', 3, '刘教授', 25);INSERT INTO student_courses (student_id, course_id, semester, grade) VALUES
(1, 1, '2024春', 'A'),
(1, 2, '2024春', 'B+'),
(1, 3, '2024春', 'A-'),
(2, 1, '2024春', 'B'),
(2, 2, '2024春', 'A'),
(2, 4, '2024春', 'B+'),
(3, 1, '2024春', 'A-'),
(3, 3, '2024春', 'A'),
(3, 4, '2024春', 'A'),
(4, 2, '2024春', 'B+'),
(4, 3, '2024春', 'B'),
(4, 5, '2024春', 'A-'),
(5, 1, '2024春', 'B'),
(5, 4, '2024春', 'A-'),
(5, 5, '2024春', 'A');-- 查询某个学生选修的所有课程
SELECT s.name as student_name, c.course_name, c.credits, sc.grade, sc.semester
FROM students s
JOIN student_courses sc ON s.id = sc.student_id
JOIN courses c ON sc.course_id = c.id
WHERE s.id = 1;-- 查询某门课程的所有学生
SELECT c.course_name, s.name as student_name, sc.enrollment_date, sc.grade
FROM courses c
JOIN student_courses sc ON c.id = sc.course_id
JOIN students s ON sc.student_id = s.id
WHERE c.id = 1;-- 查询每门课程的选课人数
SELECT c.course_name, c.teacher, COUNT(sc.student_id) as student_count
FROM courses c
LEFT JOIN student_courses sc ON c.id = sc.course_id
GROUP BY c.id, c.course_name, c.teacher;
4. 自关联关系 (Self-referencing)
场景:员工表(上下级关系)
- 员工可以有上级领导
- 一个领导可以管理多个下属
-- 创建员工表
CREATE TABLE employees (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '员工ID,主键,自增',name VARCHAR(50) NOT NULL COMMENT '员工姓名,不能为空',position VARCHAR(50) COMMENT '职位',manager_id INT COMMENT '直属上级ID,外键,可为空',salary DECIMAL(10,2) COMMENT '薪资',hire_date DATE COMMENT '入职日期',department VARCHAR(50) COMMENT '部门',email VARCHAR(100) COMMENT '邮箱地址',phone VARCHAR(20) COMMENT '电话号码',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,默认当前时间',FOREIGN KEY (manager_id) REFERENCES employees(id) ON UPDATE CASCADE ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='员工信息表';-- 插入示例数据
INSERT INTO employees (name, position, manager_id, salary, hire_date, department, email, phone) VALUES
('张三', '总经理', NULL, 20000.00, '2020-01-15', '管理层', 'zhangsan@company.com', '13800138001'),
('李四', '技术总监', 1, 18000.00, '2020-03-20', '技术部', 'lisi@company.com', '13800138002'),
('王五', '销售总监', 1, 17000.00, '2020-05-10', '销售部', 'wangwu@company.com', '13800138003'),
('赵六', '高级工程师', 2, 15000.00, '2021-01-20', '技术部', 'zhaoliu@company.com', '13800138004'),
('钱七', '前端工程师', 2, 12000.00, '2021-06-15', '技术部', 'qianqi@company.com', '13800138005'),
('孙八', '后端工程师', 2, 13000.00, '2021-08-20', '技术部', 'sunba@company.com', '13800138006'),
('周九', '销售经理', 3, 12000.00, '2021-02-10', '销售部', 'zhoujiu@company.com', '13800138007'),
('吴十', '销售专员', 7, 8000.00, '2022-01-15', '销售部', 'wushi@company.com', '13800138008'),
('郑十一', '销售专员', 7, 8500.00, '2022-03-20', '销售部', 'zhengshiyi@company.com', '13800138009'),
('马十二', '初级工程师', 4, 9000.00, '2022-07-01', '技术部', 'mashier@company.com', '13800138010');-- 查询员工及其直接上级
SELECT e.name as employee_name,e.position,e.department,m.name as manager_name,m.position as manager_position
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id
ORDER BY e.department, e.name;-- 查询某个经理的所有下属
SELECT m.name as manager_name,m.position as manager_position,e.name as subordinate_name,e.position as subordinate_position,e.department
FROM employees m
JOIN employees e ON m.id = e.manager_id
WHERE m.id = 2
ORDER BY e.name;
5. 复合关联关系
场景:电商系统中的订单、商品、分类关系
- 订单包含多个商品(一对多)
- 商品属于分类(多对一)
- 订单详情表作为中间表
-- 商品分类表
CREATE TABLE categories (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '分类ID,主键,自增',name VARCHAR(50) NOT NULL COMMENT '分类名称,不能为空',description TEXT COMMENT '分类描述',parent_id INT COMMENT '父分类ID,外键,可为空',sort_order INT DEFAULT 0 COMMENT '排序顺序,默认0',is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用,默认启用',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,默认当前时间',FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品分类表';-- 商品表
CREATE TABLE products (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID,主键,自增',name VARCHAR(100) NOT NULL COMMENT '商品名称,不能为空',sku VARCHAR(50) UNIQUE NOT NULL COMMENT '商品SKU,唯一,不能为空',price DECIMAL(10,2) NOT NULL COMMENT '商品价格,不能为空',category_id INT NOT NULL COMMENT '分类ID,外键,不能为空',stock INT DEFAULT 0 COMMENT '库存数量,默认0',description TEXT COMMENT '商品描述',brand VARCHAR(50) COMMENT '品牌',is_active BOOLEAN DEFAULT TRUE COMMENT '是否上架,默认上架',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,默认当前时间',FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品信息表';-- 订单表
CREATE TABLE shop_orders (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID,主键,自增',user_id INT NOT NULL COMMENT '用户ID,外键,不能为空',order_number VARCHAR(50) UNIQUE NOT NULL COMMENT '订单编号,唯一,不能为空',total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额,不能为空',shipping_fee DECIMAL(10,2) DEFAULT 0.00 COMMENT '运费,默认0',order_status VARCHAR(20) DEFAULT 'pending' COMMENT '订单状态,默认待处理',payment_status VARCHAR(20) DEFAULT 'unpaid' COMMENT '支付状态,默认未支付',shipping_address TEXT COMMENT '收货地址',order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '订单日期,默认当前时间',FOREIGN KEY (user_id) REFERENCES customers(id) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单信息表';-- 订单详情表
CREATE TABLE order_items (id INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单项ID,主键,自增',order_id INT NOT NULL COMMENT '订单ID,外键,不能为空',product_id INT NOT NULL COMMENT '商品ID,外键,不能为空',quantity INT NOT NULL COMMENT '购买数量,不能为空',unit_price DECIMAL(10,2) NOT NULL COMMENT '单价,不能为空',total_price DECIMAL(10,2) NOT NULL COMMENT '小计金额,不能为空',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,默认当前时间',FOREIGN KEY (order_id) REFERENCES shop_orders(id) ON UPDATE CASCADE ON DELETE CASCADE,FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单详情表';-- 插入测试数据
INSERT INTO categories (name, description, parent_id) VALUES
('电子产品', '各类电子设备', NULL),
('服装', '男装女装童装', NULL),
('图书', '各类图书杂志', NULL),
('手机', '智能手机相关产品', 1),
('电脑', '台式机笔记本配件', 1),
('男装', '男士服装', 2),
('女装', '女士服装', 2),
('小说', '各类小说', 3),
('技术书籍', '编程技术类书籍', 3);INSERT INTO products (name, sku, price, category_id, stock, description, brand) VALUES
('iPhone 15 Pro', 'IP15P128', 7999.00, 4, 50, '最新款iPhone 15 Pro 128GB', 'Apple'),
('华为P60 Pro', 'HWP60P256', 5999.00, 4, 30, '华为P60 Pro 256GB', '华为'),
('MacBook Pro 14', 'MBP14M3', 15999.00, 5, 20, 'MacBook Pro 14寸 M3芯片', 'Apple'),
('联想ThinkPad X1', 'TPX1C10', 12999.00, 5, 15, 'ThinkPad X1 Carbon 10代', '联想'),
('休闲T恤', 'TSHIRT001', 99.00, 6, 200, '纯棉休闲T恤', '优衣库'),
('连衣裙', 'DRESS001', 299.00, 7, 100, '夏季连衣裙', 'ZARA'),
('《三体》', 'BOOK001', 59.00, 8, 500, '刘慈欣科幻小说三体', '科幻世界'),
('《深入理解计算机系统》', 'BOOK002', 139.00, 9, 80, '计算机系统经典教材', '机械工业出版社');INSERT INTO shop_orders (user_id, order_number, total_amount, shipping_fee, order_status, payment_status, shipping_address) VALUES
(1, 'SO2024001', 8098.00, 99.00, 'completed', 'paid', '北京市朝阳区建国门外大街1号'),
(2, 'SO2024002', 12999.00, 0.00, 'shipped', 'paid', '上海市浦东新区陆家嘴环路1000号'),
(3, 'SO2024003', 398.00, 20.00, 'pending', 'unpaid', '深圳市南山区科技园中区科苑路15号'),
(4, 'SO2024004', 198.00, 15.00, 'completed', 'paid', '广州市天河区珠江新城花城大道5号'),
(5, 'SO2024005', 6058.00, 59.00, 'processing', 'paid', '杭州市西湖区文三路259号');INSERT INTO order_items (order_id, product_id, quantity, unit_price, total_price) VALUES
(1, 1, 1, 7999.00, 7999.00),
(2, 3, 1, 12999.00, 12999.00),
(3, 5, 2, 99.00, 198.00),
(3, 6, 1, 299.00, 299.00),
(4, 7, 2, 59.00, 118.00),
(4, 8, 1, 139.00, 139.00),
(5, 2, 1, 5999.00, 5999.00),
(5, 7, 1, 59.00, 59.00);-- 复杂查询:查询订单详情包含商品和分类信息
SELECT o.order_number,o.order_date,o.order_status,p.name as product_name,p.brand,c.name as category_name,oi.quantity,oi.unit_price,oi.total_price
FROM shop_orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
JOIN categories c ON p.category_id = c.id
WHERE o.id = 1
ORDER BY c.name, p.name;-- 查询每个分类的销售统计
SELECT c.name as category_name,COUNT(DISTINCT oi.order_id) as order_count,SUM(oi.quantity) as total_quantity,SUM(oi.total_price) as total_sales
FROM categories c
JOIN products p ON c.id = p.category_id
JOIN order_items oi ON p.id = oi.product_id
GROUP BY c.id, c.name
ORDER BY total_sales DESC;
外键约束说明
为什么只有 ON DELETE 和 ON UPDATE?
外键约束的目的是维护参照完整性,确保从表中的外键值始终指向主表中存在的主键值。只有以下两种操作会破坏这种完整性:
- DELETE(删除):当主表中的记录被删除时,从表中引用该记录的外键就会变成"悬挂引用"
- UPDATE(更新):当主表中的主键值被更新时,从表中的外键值就会指向一个不存在的主键
其他操作不需要约束的原因:
- INSERT(插入):
- 向主表插入新记录不会影响现有的外键关系
- 向从表插入记录时,外键约束会自动检查引用的主键是否存在
- SELECT(查询):只读操作,不会改变数据,无需约束
常用的外键约束选项:
ON DELETE 选项:
ON DELETE CASCADE
:删除主表记录时,自动删除关联的从表记录ON DELETE SET NULL
:删除主表记录时,将从表的外键字段设置为NULLON DELETE RESTRICT
:如果从表中有关联记录,则禁止删除主表记录ON DELETE NO ACTION
:与RESTRICT类似,但检查时机不同
ON UPDATE 选项:
ON UPDATE CASCADE
:更新主表主键时,自动更新从表的外键ON UPDATE SET NULL
:更新主表主键时,将从表的外键字段设置为NULLON UPDATE RESTRICT
:如果从表中有关联记录,则禁止更新主表主键ON UPDATE NO ACTION
:与RESTRICT类似,但检查时机不同