PostgreSQL 多表查询 #

JOIN 概述 #

JOIN 用于根据表之间的关联关系,将多个表的数据组合在一起。

text
┌─────────────────────────────────────────────────────────────┐
│                    JOIN 类型                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  INNER JOIN    只返回匹配的行                               │
│  LEFT JOIN     返回左表所有行,右表匹配行                   │
│  RIGHT JOIN    返回右表所有行,左表匹配行                   │
│  FULL JOIN     返回两表所有行                               │
│  CROSS JOIN    笛卡尔积                                     │
│  SELF JOIN     自连接(同一表连接)                         │
│                                                             │
│  ┌───────────┐         ┌───────────┐                       │
│  │   表 A    │         │   表 B    │                       │
│  │  ┌─────┐  │         │  ┌─────┐  │                       │
│  │  │     │  │         │  │     │  │                       │
│  │  │  A  ├──┼─────────┼──┤  B  │  │  INNER JOIN           │
│  │  │     │  │         │  │     │  │                       │
│  │  └─────┘  │         │  └─────┘  │                       │
│  │  ┌─────┐  │         │           │                       │
│  │  │     │  │         │           │  LEFT JOIN            │
│  │  └─────┘  │         │           │                       │
│  │           │         │  ┌─────┐  │                       │
│  │           │         │  │     │  │  RIGHT JOIN           │
│  │           │         │  └─────┘  │                       │
│  └───────────┘         └───────────┘                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

示例数据 #

sql
-- 创建示例表
CREATE TABLE departments (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    location VARCHAR(100)
);

CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    department_id INTEGER REFERENCES departments(id),
    salary DECIMAL(10, 2),
    manager_id INTEGER REFERENCES employees(id)
);

CREATE TABLE projects (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    department_id INTEGER REFERENCES departments(id)
);

INSERT INTO departments (name, location) VALUES
    ('Engineering', 'Building A'),
    ('Sales', 'Building B'),
    ('Marketing', 'Building C');
    -- id: 1, 2, 3

INSERT INTO employees (name, department_id, salary, manager_id) VALUES
    ('Alice', 1, 90000.00, NULL),      -- CEO
    ('Bob', 1, 80000.00, 1),           -- Engineering
    ('Charlie', 1, 85000.00, 1),       -- Engineering
    ('Diana', 2, 70000.00, 1),         -- Sales
    ('Eve', 2, 65000.00, 4),           -- Sales
    ('Frank', NULL, 60000.00, 1);      -- No department

INSERT INTO projects (name, department_id) VALUES
    ('Project Alpha', 1),
    ('Project Beta', 1),
    ('Project Gamma', 2);

INNER JOIN #

基本 INNER JOIN #

sql
-- INNER JOIN:只返回两表都匹配的行
SELECT 
    e.name AS employee_name,
    e.salary,
    d.name AS department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;

-- 输出:
--  employee_name | salary  | department_name
-- ---------------+---------+----------------
--  Alice         | 90000.00| Engineering
--  Bob           | 80000.00| Engineering
--  Charlie       | 85000.00| Engineering
--  Diana         | 70000.00| Sales
--  Eve           | 65000.00| Sales
-- Frank 没有部门,不显示

-- 使用 USING(列名相同时)
SELECT 
    e.name,
    d.name AS department
FROM employees e
INNER JOIN departments d USING (id);  -- 不推荐,这里 id 含义不同

-- 多条件连接
SELECT 
    e.name,
    d.name AS department,
    d.location
FROM employees e
INNER JOIN departments d 
    ON e.department_id = d.id 
    AND d.location = 'Building A';

多表 INNER JOIN #

sql
-- 连接三个表
SELECT 
    e.name AS employee,
    d.name AS department,
    p.name AS project
FROM employees e
INNER JOIN departments d ON e.department_id = d.id
INNER JOIN projects p ON d.id = p.department_id;

-- 输出:
--  employee | department  |    project
-- ----------+-------------+---------------
--  Alice    | Engineering | Project Alpha
--  Alice    | Engineering | Project Beta
--  Bob      | Engineering | Project Alpha
--  Bob      | Engineering | Project Beta
--  Charlie  | Engineering | Project Alpha
--  Charlie  | Engineering | Project Beta
--  Diana    | Sales       | Project Gamma
--  Eve      | Sales       | Project Gamma

LEFT JOIN #

基本 LEFT JOIN #

sql
-- LEFT JOIN:返回左表所有行,右表匹配行(无匹配则为 NULL)
SELECT 
    e.name AS employee_name,
    e.salary,
    d.name AS department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;

-- 输出:
--  employee_name | salary  | department_name
-- ---------------+---------+----------------
--  Alice         | 90000.00| Engineering
--  Bob           | 80000.00| Engineering
--  Charlie       | 85000.00| Engineering
--  Diana         | 70000.00| Sales
--  Eve           | 65000.00| Sales
--  Frank         | 60000.00| NULL           -- Frank 没有部门

-- LEFT OUTER JOIN 是 LEFT JOIN 的完整写法
SELECT e.name, d.name AS department
FROM employees e
LEFT OUTER JOIN departments d ON e.department_id = d.id;

查找未匹配的行 #

sql
-- 查找没有部门的员工
SELECT e.name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id
WHERE d.id IS NULL;

-- 输出:
--   name
-- --------
--  Frank

-- 查找没有员工的项目
SELECT p.name AS project
FROM projects p
LEFT JOIN departments d ON p.department_id = d.id
LEFT JOIN employees e ON d.id = e.department_id
WHERE e.id IS NULL;

-- 查找没有项目的部门
SELECT d.name AS department
FROM departments d
LEFT JOIN projects p ON d.id = p.department_id
WHERE p.id IS NULL;

RIGHT JOIN #

sql
-- RIGHT JOIN:返回右表所有行,左表匹配行
SELECT 
    e.name AS employee_name,
    d.name AS department_name
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id;

-- 输出:
--  employee_name | department_name
-- ---------------+----------------
--  Alice         | Engineering
--  Bob           | Engineering
--  Charlie       | Engineering
--  Diana         | Sales
--  Eve           | Sales
--  NULL          | Marketing      -- Marketing 没有员工

-- RIGHT JOIN 可以改写为 LEFT JOIN(推荐)
SELECT 
    e.name AS employee_name,
    d.name AS department_name
FROM departments d
LEFT JOIN employees e ON e.department_id = d.id;

FULL JOIN #

sql
-- FULL JOIN:返回两表所有行
SELECT 
    e.name AS employee_name,
    d.name AS department_name
FROM employees e
FULL JOIN departments d ON e.department_id = d.id;

-- 输出:
--  employee_name | department_name
-- ---------------+----------------
--  Alice         | Engineering
--  Bob           | Engineering
--  Charlie       | Engineering
--  Diana         | Sales
--  Eve           | Sales
--  Frank         | NULL           -- Frank 没有部门
--  NULL          | Marketing      -- Marketing 没有员工

-- FULL OUTER JOIN 是 FULL JOIN 的完整写法

CROSS JOIN #

sql
-- CROSS JOIN:笛卡尔积(每行与每行组合)
SELECT 
    e.name AS employee,
    d.name AS department
FROM employees e
CROSS JOIN departments d;

-- 输出:6 员工 × 3 部门 = 18 行
--  employee | department
-- ----------+------------
--  Alice    | Engineering
--  Alice    | Sales
--  Alice    | Marketing
--  Bob      | Engineering
--  ...(共 18 行)

-- 使用场景:生成所有组合
-- 例如:每个员工对每个项目的权限矩阵

自连接 #

员工-经理关系 #

sql
-- 自连接:查找员工及其经理
SELECT 
    e.name AS employee,
    e.salary,
    m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

-- 输出:
--  employee | salary  | manager
-- ----------+---------+---------
--  Alice    | 90000.00| NULL
--  Bob      | 80000.00| Alice
--  Charlie  | 85000.00| Alice
--  Diana    | 70000.00| Alice
--  Eve      | 65000.00| Diana
--  Frank    | 60000.00| Alice

-- 查找薪资高于经理的员工
SELECT 
    e.name AS employee,
    e.salary AS emp_salary,
    m.name AS manager,
    m.salary AS mgr_salary
FROM employees e
INNER JOIN employees m ON e.manager_id = m.id
WHERE e.salary > m.salary;

组织层级 #

sql
-- 多级层级查询
WITH RECURSIVE employee_hierarchy AS (
    SELECT 
        id,
        name,
        manager_id,
        1 AS level,
        name::TEXT AS path
    FROM employees
    WHERE manager_id IS NULL
    
    UNION ALL
    
    SELECT 
        e.id,
        e.name,
        e.manager_id,
        eh.level + 1,
        eh.path || ' -> ' || e.name
    FROM employees e
    JOIN employee_hierarchy eh ON e.manager_id = eh.id
)
SELECT 
    REPEAT('  ', level - 1) || name AS hierarchy,
    level
FROM employee_hierarchy
ORDER BY path;

自然连接 #

sql
-- NATURAL JOIN:自动使用同名列连接
-- 不推荐使用,因为不可控
SELECT e.name, d.name
FROM employees e
NATURAL JOIN departments d;

-- 等价于(如果两表都有 department_id 列)
SELECT e.name, d.name
FROM employees e
JOIN departments d USING (department_id);

JOIN 与 WHERE #

sql
-- ON vs WHERE 的区别
-- ON:连接条件
-- WHERE:过滤结果

-- LEFT JOIN + WHERE(过滤结果)
SELECT e.name, d.name AS department
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id
WHERE d.location = 'Building A';
-- 结果:只返回在 Building A 的员工
-- Frank 不会出现(因为 d.location 是 NULL)

-- LEFT JOIN + ON(连接条件)
SELECT e.name, d.name AS department
FROM employees e
LEFT JOIN departments d 
    ON e.department_id = d.id 
    AND d.location = 'Building A';
-- 结果:返回所有员工
-- Frank 会显示(department 为 NULL)
-- 其他部门的员工也会显示(department 为 NULL)

复杂 JOIN 查询 #

统计查询 #

sql
-- 每个部门的员工数量和平均薪资
SELECT 
    d.name AS department,
    COUNT(e.id) AS employee_count,
    COALESCE(ROUND(AVG(e.salary), 2), 0) AS avg_salary,
    COALESCE(SUM(e.salary), 0) AS total_salary
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id
GROUP BY d.id, d.name
ORDER BY employee_count DESC;

-- 输出:
--  department  | employee_count | avg_salary | total_salary
-- -------------+----------------+------------+--------------
--  Engineering |              3 |   85000.00 |    255000.00
--  Sales       |              2 |   67500.00 |    135000.00
--  Marketing   |              0 |       0.00 |         0.00

多条件连接 #

sql
-- 复杂条件连接
SELECT 
    e.name AS employee,
    d.name AS department,
    p.name AS project
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id
LEFT JOIN projects p ON d.id = p.department_id
WHERE e.salary > 70000
ORDER BY e.name, p.name;

使用子查询 #

sql
-- JOIN 子查询结果
SELECT 
    e.name,
    dept_stats.avg_salary,
    e.salary - dept_stats.avg_salary AS diff
FROM employees e
JOIN (
    SELECT 
        department_id,
        AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
) dept_stats ON e.department_id = dept_stats.department_id;

-- 使用 CTE 更清晰
WITH dept_stats AS (
    SELECT 
        department_id,
        AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
)
SELECT 
    e.name,
    dept_stats.avg_salary,
    e.salary - dept_stats.avg_salary AS diff
FROM employees e
JOIN dept_stats ON e.department_id = dept_stats.department_id;

JOIN 性能优化 #

使用索引 #

sql
-- 确保连接列有索引
CREATE INDEX idx_employees_department_id ON employees(department_id);
CREATE INDEX idx_projects_department_id ON projects(department_id);

-- 查看执行计划
EXPLAIN ANALYZE
SELECT e.name, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id;

选择合适的 JOIN 类型 #

sql
-- 小表驱动大表
-- 如果 employees 表大,departments 表小
-- 让小表在 JOIN 中先处理

-- 使用 INNER JOIN 而非 LEFT JOIN(如果不需要 NULL 值)
-- INNER JOIN 通常更快

避免笛卡尔积 #

sql
-- 错误:忘记连接条件
SELECT e.name, d.name
FROM employees e, departments d;  -- 笛卡尔积!

-- 正确:添加连接条件
SELECT e.name, d.name
FROM employees e
JOIN departments d ON e.department_id = d.id;

学习路径 #

text
进阶阶段
├── 高级查询
├── 多表查询(本文)
├── 索引优化
└── 事务处理

下一步 #

掌握了多表查询后,接下来学习 索引,了解如何优化查询性能!

最后更新:2026-03-29