目录
[第一章 多表关联基础概念](#第一章 多表关联基础概念)
[1.1 什么是多表关联](#1.1 什么是多表关联)
[1.2 为什么需要多表关联](#1.2 为什么需要多表关联)
[1.3 银行业务案例:数据表设计](#1.3 银行业务案例:数据表设计)
[第二章 关联类型详解](#第二章 关联类型详解)
[2.1 INNER JOIN(内连接)](#2.1 INNER JOIN(内连接))
[2.1.1 概念性解释](#2.1.1 概念性解释)
[2.1.2 核心特点](#2.1.2 核心特点)
[2.1.3 基本语法](#2.1.3 基本语法)
[2.1.4 银行业务示例](#2.1.4 银行业务示例)
[2.2 LEFT JOIN(左外连接)](#2.2 LEFT JOIN(左外连接))
[2.2.1 概念性解释](#2.2.1 概念性解释)
[2.2.2 核心特点](#2.2.2 核心特点)
[2.2.3 基本语法](#2.2.3 基本语法)
[2.2.4 银行业务示例](#2.2.4 银行业务示例)
[2.3 RIGHT JOIN(右外连接)](#2.3 RIGHT JOIN(右外连接))
[2.3.1 概念性解释](#2.3.1 概念性解释)
[2.3.2 核心特点](#2.3.2 核心特点)
[2.3.3 示例(等价于上面的 LEFT JOIN)](#2.3.3 示例(等价于上面的 LEFT JOIN))
[2.4 FULL JOIN(全外连接)](#2.4 FULL JOIN(全外连接))
[2.4.1 概念性解释](#2.4.1 概念性解释)
[2.4.2 核心特点](#2.4.2 核心特点)
[2.4.3 基本语法](#2.4.3 基本语法)
[2.4.4 银行业务示例](#2.4.4 银行业务示例)
[2.5 交叉连接(CROSS JOIN)------ 扩展内容](#2.5 交叉连接(CROSS JOIN)—— 扩展内容)
[2.5.1 概念性解释](#2.5.1 概念性解释)
[2.5.2 语法](#2.5.2 语法)
[2.5.3 示例](#2.5.3 示例)
[2.6 自连接(Self JOIN)------ 扩展内容](#2.6 自连接(Self JOIN)—— 扩展内容)
[2.6.1 概念性解释](#2.6.1 概念性解释)
[2.6.2 示例](#2.6.2 示例)
[第三章 关联查询中的条件过滤](#第三章 关联查询中的条件过滤)
[3.1 ON 子句与 WHERE 子句的区别](#3.1 ON 子句与 WHERE 子句的区别)
[3.2 多条件关联](#3.2 多条件关联)
[第四章 EXISTS 与 NOT EXISTS 子查询](#第四章 EXISTS 与 NOT EXISTS 子查询)
[4.1 EXISTS 基本概念](#4.1 EXISTS 基本概念)
[4.2 NOT EXISTS 基本概念](#4.2 NOT EXISTS 基本概念)
[4.3 EXISTS 与 IN 的比较](#4.3 EXISTS 与 IN 的比较)
[4.4 性能优化建议](#4.4 性能优化建议)
[第五章 多表关联实战](#第五章 多表关联实战)
[5.1 三表关联(内连接)](#5.1 三表关联(内连接))
[5.2 多层 LEFT JOIN(保留左表全部)](#5.2 多层 LEFT JOIN(保留左表全部))
[5.3 使用聚合函数的关联](#5.3 使用聚合函数的关联)
[5.4 联合查询(UNION)------ 扩展内容](#5.4 联合查询(UNION)—— 扩展内容)
第一章 多表关联基础概念
1.1 什么是多表关联
多表关联是关系型数据库的核心功能。通过关联条件,我们可以将多个表中的数据"连接"起来,实现一次查询获取分散在不同表中的相关数据。
零基础解释:想象你在银行工作。客户信息存在一张表,账户信息存在另一张表。要查询"张三的存款余额",你就需要把这两张表按"客户编号"关联起来,才能得到完整的答案。
1.2 为什么需要多表关联
-
避免数据冗余(遵循数据库设计规范):如果把客户信息和账户信息全塞在一张表里,一个客户有多个账户就会重复存储客户的姓名、地址等,浪费空间且容易出错。
-
提高数据一致性:客户姓名只存一处,修改一次,所有关联账户自动生效。
-
实现复杂业务查询:例如"找出所有本月交易额超过 10 万元的客户及其开户支行"。
1.3 银行业务案例:数据表设计
为了讲解关联查询,我们先创建一套银行业务的数据库对象。以下所有 SQL 均适用于 PostgreSQL(也兼容多数关系数据库)。
sql
-- 创建银行业务的 schema(命名空间)
CREATE SCHEMA IF NOT EXISTS bank;
-- 1. 客户表 (customers)
CREATE TABLE bank.customers (
customer_id INTEGER PRIMARY KEY, -- 客户编号
full_name VARCHAR(50) NOT NULL, -- 客户姓名
id_card VARCHAR(18) UNIQUE, -- 身份证号
phone VARCHAR(20), -- 联系电话
register_date DATE DEFAULT CURRENT_DATE
);
-- 2. 支行表 (branches)
CREATE TABLE bank.branches (
branch_id INTEGER PRIMARY KEY, -- 支行编号
branch_name VARCHAR(50) NOT NULL, -- 支行名称
city VARCHAR(30), -- 所在城市
manager_name VARCHAR(50) -- 行长姓名
);
-- 3. 账户表 (accounts)
CREATE TABLE bank.accounts (
account_id INTEGER PRIMARY KEY, -- 账户号
customer_id INTEGER NOT NULL, -- 所属客户编号
branch_id INTEGER NOT NULL, -- 开户支行编号
account_type VARCHAR(20) DEFAULT '储蓄账户', -- 账户类型:储蓄/支票/信用卡
balance NUMERIC(12,2) DEFAULT 0.00, -- 余额(单位:元)
open_date DATE DEFAULT CURRENT_DATE,
status VARCHAR(10) DEFAULT '正常', -- 正常/冻结/销户
FOREIGN KEY (customer_id) REFERENCES bank.customers(customer_id),
FOREIGN KEY (branch_id) REFERENCES bank.branches(branch_id)
);
-- 4. 交易记录表 (transactions)
CREATE TABLE bank.transactions (
trans_id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL, -- 发生交易的账户
trans_type VARCHAR(10) NOT NULL, -- 存入/取款/转账/消费
amount NUMERIC(12,2) NOT NULL,
trans_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
target_account INTEGER, -- 转账时的对方账户号
FOREIGN KEY (account_id) REFERENCES bank.accounts(account_id)
);
-- 插入支行数据
INSERT INTO bank.branches VALUES (1, '总行营业部', '北京', '王行长');
INSERT INTO bank.branches VALUES (2, '浦东分行', '上海', '李行长');
INSERT INTO bank.branches VALUES (3, '深圳科技园支行', '深圳', '张行长');
-- 插入客户数据
INSERT INTO bank.customers VALUES (101, '张三', '110101199001011234', '13800001111', '2020-01-10');
INSERT INTO bank.customers VALUES (102, '李四', '310101199502023456', '13912345678', '2021-03-15');
INSERT INTO bank.customers VALUES (103, '王芳', '440301198803036789', '13687654321', '2019-11-20');
INSERT INTO bank.customers VALUES (104, '赵雷', '510101200012048888', '15900000000', '2023-05-01');
-- 插入账户数据
INSERT INTO bank.accounts VALUES (1001, 101, 1, '储蓄账户', 50000.00, '2020-01-15', '正常');
INSERT INTO bank.accounts VALUES (1002, 101, 1, '支票账户', 12000.00, '2020-02-01', '正常');
INSERT INTO bank.accounts VALUES (1003, 102, 2, '储蓄账户', 8000.00, '2021-03-20', '正常');
INSERT INTO bank.accounts VALUES (1004, 103, 3, '储蓄账户', 200000.00, '2019-12-01', '正常');
INSERT INTO bank.accounts VALUES (1005, 103, 3, '信用卡账户', 5000.00, '2020-06-01', '正常');
INSERT INTO bank.accounts VALUES (1006, 104, 2, '储蓄账户', 1000.00, '2023-05-10', '冻结'); -- 冻结账户
-- 注意:没有账户的客户?赵雷有账户(1006),但状态冻结。我们还可以插入一个无账户的客户测试外连接。
INSERT INTO bank.customers VALUES (105, '孙梅', '420106199810105555', '17711112222', '2022-08-08');
-- 插入交易记录
INSERT INTO bank.transactions VALUES (5001, 1001, '存入', 20000.00, '2025-01-10 09:30:00', NULL);
INSERT INTO bank.transactions VALUES (5002, 1001, '取款', 5000.00, '2025-01-15 14:20:00', NULL);
INSERT INTO bank.transactions VALUES (5003, 1003, '存入', 3000.00, '2025-02-01 11:00:00', NULL);
INSERT INTO bank.transactions VALUES (5004, 1004, '转账', 10000.00, '2025-02-10 16:00:00', 1001);
INSERT INTO bank.transactions VALUES (5005, 1002, '消费', 200.00, '2025-02-14 19:30:00', NULL);
INSERT INTO bank.transactions VALUES (5006, 1005, '消费', 1800.00, '2025-02-18 10:00:00', NULL);
说明:以上数据中,客户孙梅(customer_id=105)没有任何账户,用于演示外连接;赵雷账户被冻结;张三有两个账户(储蓄+支票)。
第二章 关联类型详解
2.1 INNER JOIN(内连接)
2.1.1 概念性解释
INNER JOIN 就像两个朋友圈的交集 。只返回两个表中都匹配的记录。如果某客户没有账户,或者某账户不属于任何客户,则不会出现在结果中。
银行业务场景:查询"所有已有账户的客户及其开户支行名称"。如果一个客户还没开户(比如刚注册),就不会出现。
2.1.2 核心特点
-
只返回两个表中都存在的匹配记录
-
类似于数学中的"交集"
-
是最常用的一种关联方式
2.1.3 基本语法
sql
SELECT 列1
,列2
, ...
FROM 表1
INNER JOIN 表2
ON 表1.关联列 = 表2.关联列;
2.1.4 银行业务示例
sql
-- 查询每个账户对应的客户姓名和账户余额(只显示有账户的客户)
SELECT a.account_id
,c.full_name AS 客户姓名
,a.account_type
,a.balance
FROM bank.accounts a
INNER JOIN bank.customers c
ON a.customer_id = c.customer_id;
结果:孙梅(无账户)不会出现,赵雷的账户(状态冻结)仍会出现,因为他有账户记录。
2.2 LEFT JOIN(左外连接)
2.2.1 概念性解释
LEFT JOIN 以左表为主,完整展示左表的所有记录,右边没有匹配的列就用 NULL 填充。
银行业务场景:查询"所有客户及其账户信息"。即使客户尚未开立任何账户,我们也希望看到该客户的信息,而账户列显示为空。
2.2.2 核心特点
-
返回左表的所有记录,不管右表是否有匹配
-
右表无匹配时显示 NULL
-
常用于"包含所有......并显示相关......"的场景
2.2.3 基本语法
sql
SELECT columns
FROM 表1
LEFT JOIN 表2
ON 表1.关联列 = 表2.关联列;
2.2.4 银行业务示例
sql
-- 查询所有客户及其账户号(包括没有账户的客户)
SELECT c.customer_id
,c.full_name
,a.account_id
,a.balance
FROM bank.customers c
LEFT JOIN bank.accounts a
ON c.customer_id = a.customer_id;
结果:孙梅(customer_id=105)会显示,但 account_id 和 balance 为 NULL。
2.3 RIGHT JOIN(右外连接)
2.3.1 概念性解释
RIGHT JOIN 以右表为主,与 LEFT JOIN 功能对称。实际工作中很少单独使用,因为可以通过调换表顺序用 LEFT JOIN 实现。
银行业务场景:查询"所有账户及其所属客户信息",如果以 accounts 为右表,效果等同于把 accounts 放左边做 LEFT JOIN。
2.3.2 核心特点
-
返回右表的所有记录,左表无匹配时显示 NULL
-
可用 LEFT JOIN 替代(交换表的顺序)
2.3.3 示例(等价于上面的 LEFT JOIN)
sql
-- 查询所有账户及其客户姓名(右连接写法)
SELECT a.account_id
,c.full_name
FROM bank.customers c
RIGHT JOIN bank.accounts a
ON c.customer_id = a.customer_id;
-- 这条语句与 "accounts LEFT JOIN customers" 结果相同
2.4 FULL JOIN(全外连接)
2.4.1 概念性解释
FULL JOIN 返回两个表的所有记录,匹配的合并在一起,不匹配的部分用 NULL 填充。相当于 LEFT JOIN 和 RIGHT JOIN 的并集。
银行业务场景:查询"所有客户和所有账户的完全对应关系"。即使客户没有账户,或者账户没有绑定客户(理论上不会发生,因为外键约束),都会出现在结果中。
2.4.2 核心特点
-
返回两个表的全部记录
-
匹配的记录合并显示
-
不匹配的部分用 NULL 填充
2.4.3 基本语法
sql
SELECT columns
FROM 表1
FULL OUTER JOIN 表2
ON 表1.关联列 = 表2.关联列;
2.4.4 银行业务示例
sql
-- 查询所有客户与所有账户的完整关系
SELECT c.full_name
,a.account_id
,a.balance
FROM bank.customers c
FULL OUTER JOIN bank.accounts a
ON c.customer_id = a.customer_id;
结果:包含所有客户(即使无账户)和所有账户(即使无客户,但受外键约束,所有账户都有客户,所以此例中不会出现无客户的账户)。
2.5 交叉连接(CROSS JOIN)------ 扩展内容
2.5.1 概念性解释
CROSS JOIN 返回两个表的笛卡尔积:左表的每一行与右表的每一行组合。结果行数 = 左表行数 × 右表行数。
银行业务场景:极少直接使用,但可用于生成测试数据。例如,想为每个客户生成每个支行的虚拟推荐关系。
2.5.2 语法
sql
SELECT *
FROM 表1
CROSS JOIN 表2;
2.5.3 示例
sql
-- 列出所有客户和所有支行的组合(生成潜在业务关系)
SELECT c.full_name, b.branch_name
FROM bank.customers c
CROSS JOIN bank.branches b;
2.6 自连接(Self JOIN)------ 扩展内容
2.6.1 概念性解释
自连接是将一张表与自身进行关联。必须使用表别名来区分"两个不同的实例"。
银行业务场景:查询"同一个支行中,余额超过支行平均余额的账户"。或者更简单的:查询"每个员工的上司"(但我们的表没有员工,可以用客户表演示?不够贴切)。我们可以用账户表:查询"同一个客户名下,余额大于另一账户的账户对"。
2.6.2 示例
sql
-- 查找同一个客户名下,余额比另一个账户高的账户对
SELECT a1.account_id AS 账户A
,a2.account_id AS 账户B
,a1.balance AS 余额A
,a2.balance AS 余额B
FROM bank.accounts a1
JOIN bank.accounts a2
ON a1.customer_id = a2.customer_id
AND a1.balance > a2.balance;
第三章 关联查询中的条件过滤
3.1 ON 子句与 WHERE 子句的区别
在关联查询中,ON 用于指定连接条件,WHERE 用于对连接后的结果集进行筛选。把条件放在 ON 中还是 WHERE 中,对于 INNER JOIN 效果相同,但对于 OUTER JOIN 则完全不同。
(1)示例对比
sql
-- 案例:左连接 + ON 条件(过滤条件在ON中)
SELECT c.full_name
,a.account_id
,a.balance
FROM bank.customers c
LEFT JOIN bank.accounts a
ON c.customer_id = a.customer_id
AND a.balance > 10000;
-- 案例:左连接 + WHERE 条件(过滤条件在WHERE中)
SELECT c.full_name
,a.account_id
,a.balance
FROM bank.customers c
LEFT JOIN bank.accounts a
ON c.customer_id = a.customer_id
WHERE a.balance > 10000;
结果差异:
-
第一种(ON):保留所有客户,只有那些余额>10000 的账户才会显示账户信息;余额≤10000 或没有账户的客户仍会显示(账户列为 NULL)。
-
第二种(WHERE):因为 WHERE 条件要求
a.balance > 10000,不满足该条件的行(包括客户无账户时的 NULL)会被整体过滤,最终只显示"有账户且余额>10000"的客户,等同于内连接。
建议:如果希望左表全部保留,附加过滤条件应写在 ON 子句中;如果希望过滤整个结果,写在 WHERE 中。
3.2 多条件关联
可以在 ON 中使用 AND 连接多个条件。
sql
-- 查询客户张三的储蓄账户信息
SELECT c.full_name
,a.account_id
,a.balance
FROM bank.customers c
JOIN bank.accounts a
ON c.customer_id = a.customer_id
AND a.account_type = '储蓄账户'
WHERE c.full_name = '张三';
第四章 EXISTS 与 NOT EXISTS 子查询
4.1 EXISTS 基本概念
EXISTS 是一个用于判断子查询 中是否存在至少一行数据 的逻辑运算符:当子查询能返回任何结果(哪怕只有一行)时,EXISTS 的条件就为"真";反之,如果子查询一行也查不到,条件就为"假"。它通常用在 WHERE 子句中,例如"查询所有有过交易记录的客户"------只需判断 EXISTS (SELECT 1 FROM accounts JOIN transactions ON ... WHERE accounts.customer_id = customers.customer_id),一旦存在匹配的交易记录,该客户就会被选中。因为 EXISTS 只关心"有没有数据",不在乎具体数据内容,所以子查询里写 SELECT 1 或 SELECT * 效果一样,且执行效率通常较高。
银行业务场景:查询"发生过交易的客户"。
(1)语法
sql
SELECT 列 FROM 表1
WHERE EXISTS (SELECT 1 FROM 表2 WHERE 连接条件);
(2)示例
sql
-- 查找至少有一笔交易记录的客户
SELECT c.full_name
FROM bank.customers c
WHERE EXISTS (
SELECT 1
FROM bank.accounts a
JOIN bank.transactions t
ON a.account_id = t.account_id
WHERE a.customer_id = c.customer_id
);
4.2 NOT EXISTS 基本概念
NOT EXISTS 与 EXISTS 正好相反:当子查询没有返回任何行 时,条件为"真";只要子查询至少查出一行,条件就为"假"。因此,它常用于查找"不存在某种关联"的记录,例如"查询从未有过交易记录的客户"------写成 WHERE NOT EXISTS (SELECT 1 FROM accounts JOIN transactions ON ... WHERE accounts.customer_id = customers.customer_id),那些在子查询中找不到匹配交易行的客户就会被选中。同样,NOT EXISTS 只关心行是否存在,不关心数据内容,且能正确处理子查询中的 NULL 值,避免了 NOT IN 可能出现的空结果陷阱。
银行业务场景:查询"从未进行过任何交易的客户"。
(1)示例
sql
-- 查找从未有过交易的客户(包括无账户的客户)
SELECT c.full_name
FROM bank.customers c
WHERE NOT EXISTS (
SELECT 1
FROM bank.accounts a
JOIN bank.transactions t
ON a.account_id = t.account_id
WHERE a.customer_id = c.customer_id
);
4.3 EXISTS 与 IN 的比较
| 特性 | EXISTS / NOT EXISTS | IN / NOT IN |
|---|---|---|
| NULL 安全 | ✅ 安全,不受子查询中 NULL 影响 | ❌ NOT IN 遇到 NULL 可能返回空集 |
| 性能(大数据量) | 通常更快(可提前停止) | 可能较慢(需去重、处理 NULL) |
| 可读性 | 稍复杂 | 更直观 |
(1)NOT IN 的 NULL 陷阱示例
sql
-- 错误示例:子查询中可能包含 NULL,导致结果为空
SELECT *
FROM bank.customers
WHERE customer_id NOT IN (
SELECT customer_id
FROM bank.accounts
WHERE 1=0
); -- 故意无数据,但假设 accounts 表有 NULL customer_id?
-- 实际更常见:子查询某列有 NULL,NOT IN 会不返回任何行。
-- 解决办法:子查询中添加 IS NOT NULL 或使用 NOT EXISTS。
4.4 性能优化建议
-
子查询中连接字段(如
customer_id)务必建立索引。 -
在
EXISTS子查询中使用SELECT 1(或任意常量),无需SELECT *。 -
对于存在性检查,
EXISTS通常比JOIN更高效,因为它不需要去重。
第五章 多表关联实战
5.1 三表关联(内连接)
sql
-- 查询每笔交易对应的客户姓名、开户支行、交易类型和金额
SELECT t.trans_id
,c.full_name AS 客户
,b.branch_name AS 支行
,t.trans_type
,t.amount
,t.trans_time
FROM bank.transactions t
INNER JOIN bank.accounts a
ON t.account_id = a.account_id
INNER JOIN bank.customers c
ON a.customer_id = c.customer_id
INNER JOIN bank.branches b
ON a.branch_id = b.branch_id
LIMIT 10;
5.2 多层 LEFT JOIN(保留左表全部)
sql
-- 查询所有支行、支行下的账户、账户的交易记录(即使没有账户或没有交易也显示)
SELECT b.branch_name
,a.account_id
,a.balance
,t.trans_type
,t.amount
FROM bank.branches b
LEFT JOIN bank.accounts a
ON b.branch_id = a.branch_id
LEFT JOIN bank.transactions t
ON a.account_id = t.account_id
ORDER BY b.branch_name, a.account_id;
5.3 使用聚合函数的关联
sql
-- 统计每个支行的客户数(去重)、账户总数、平均余额
SELECT b.branch_name
,COUNT(DISTINCT a.customer_id) AS 客户数量
,COUNT(a.account_id) AS 账户总数
,AVG(a.balance) AS 平均余额
FROM bank.branches b
LEFT JOIN bank.accounts a
ON b.branch_id = a.branch_id
GROUP BY b.branch_id, b.branch_name
ORDER BY 平均余额 DESC;
5.4 联合查询(UNION)------ 扩展内容
UNION 将两个查询的结果上下拼接 ,要求列数和类型一致。UNION ALL 包含重复行,UNION 去重。
银行业务场景:查询所有"账户余额高于 50000 的客户"和"有过取款交易的客户"合并名单。
sql
-- 高余额客户
SELECT customer_id FROM bank.accounts WHERE balance > 50000
UNION
-- 有过取款交易的客户(通过账户关联)
SELECT a.customer_id
FROM bank.transactions t
JOIN bank.accounts a ON t.account_id = a.account_id
WHERE t.trans_type = '取款';
总结
| 关联类型 | 记忆口诀 | 银行业务典型场景 |
|---|---|---|
| INNER JOIN | 只要两边都有 | 查询有账户的客户 |
| LEFT JOIN | 左表全保留,右边可能空 | 查询所有客户及其账户(包括无账户客户) |
| RIGHT JOIN | 右表全保留,左边可能空 | 极少用,可被 LEFT JOIN 替代 |
| FULL JOIN | 两边全保留,互相填空 | 合并两个独立清单 |
| CROSS JOIN | 所有组合,风险大 | 生成测试数据 |
| EXISTS | "有"就行 | 发生过交易的客户 |
| NOT EXISTS | "没有"就行 | 从未交易的客户 |