数据库设计有个基本原则:数据分散存储,减少冗余。但问题来了——当我们需要从多个表中拼出完整信息时,该怎么办?答案就是 JOIN。PostgreSQL 提供了丰富的 JOIN 类型:内连接、左外连接、右外连接、全外连接以及交叉连接。掌握这些操作,几乎是深入数据库查询和数据分析的必经之路。这篇文章会从头
数据库设计有个基本原则:数据分散存储,减少冗余。但问题来了——当我们需要从多个表中拼出完整信息时,该怎么办?答案就是 JOIN。PostgreSQL 提供了丰富的 JOIN 类型:内连接、左外连接、右外连接、全外连接以及交叉连接。掌握这些操作,几乎是深入数据库查询和数据分析的必经之路。这篇文章会从头梳理每种 JOIN 的工作原理,并通过 Ja va 代码带你在实战中跑通它们。
JOIN 是 SQL 里用来把两个或多个表的行组合起来的机制。它基于列之间的关联关系——通常是主键和外键——来合并数据。通过 JOIN,我们能从多个表中提取出相关联的信息,形成一个逻辑上的统一视图。构建报表、做数据分析,都离不开它。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
举个电商系统的例子:customers 表存客户信息,orders 表存订单信息,两者通过客户 ID 关联。要查某个客户的所有订单,就必须把这两张表 JOIN 起来。
SELECT columns FROM table1 JOIN table2 ON table1.column = table2.column WHERE conditions;
SELECT:指定要返回的列。FROM table1:左表。JOIN table2:右表。ON table1.column = table2.column:连接条件。WHERE:可选的过滤条件。内连接是最常用的 JOIN 类型。它只返回两个表中都存在匹配记录的行——换句话说,左右两边的连接字段都有对应值,才会出现在结果里。如果某一行在其中一边找不到匹配项,整行就被排除。
先建两张简单的表:employees(员工)和 departments(部门)。
-- 创建部门表
CREATE TABLE departments (
dept_id SERIAL PRIMARY KEY,
dept_name VARCHAR(100) NOT NULL
);
-- 创建员工表
CREATE TABLE employees (
emp_id SERIAL PRIMARY KEY,
emp_name VARCHAR(100) NOT NULL,
dept_id INT, -- 外键关联到 departments 表
salary DECIMAL(10, 2),
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);
-- 插入部门数据
INSERT INTO departments (dept_name) VALUES
('Human Resources'),
('Engineering'),
('Marketing'),
('Finance');
-- 插入员工数据
INSERT INTO employees (emp_name, dept_id, salary) VALUES
('Alice Johnson', 1, 75000.00),
('Bob Smith', 2, 85000.00),
('Carol Da vis', 2, 90000.00),
('Da vid Wilson', 3, 65000.00),
('Eve Brown', 1, 70000.00),
('Frank Miller', 4, 80000.00),
('Grace Lee', NULL, 55000.00); -- Grace 没有分配部门
这个场景模拟了一个公司:员工通过 dept_id 与部门关联,有一个员工没部门。
用 INNER JOIN 查出所有有部门的员工及其部门名称:
SELECT e.emp_name, e.salary, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id = d.dept_id ORDER BY e.emp_name;
说明:
SELECT e.emp_name, e.salary, d.dept_name:选员工姓名、薪资、部门名。FROM employees e:主表 employees,别名 e。INNER JOIN departments d:连 departments,别名 d。ON e.dept_id = d.dept_id:连接条件。ORDER BY e.emp_name:按姓名排序。结果:
| emp_name | salary | dept_name |
|---|---|---|
| Alice Johnson | 75000.00 | Human Resources |
| Bob Smith | 85000.00 | Engineering |
| Carol Da vis | 90000.00 | Engineering |
| Da vid Wilson | 65000.00 | Marketing |
| Eve Brown | 70000.00 | Human Resources |
| Frank Miller | 80000.00 | Finance |
注意:员工 Grace Lee(dept_id 为 NULL)没出现在结果里——这正是 INNER JOIN 的特性:只保留匹配的行。
INNER JOIN 和 LEFT JOIN 最核心的区别:LEFT JOIN 会保留左表中所有行,不匹配的右表字段用 NULL 填充;而 INNER JOIN 则彻底丢弃这些行。
左外连接返回左表中的所有行,不管右表有没有匹配。右表找不到匹配时,结果中右表的字段就填 NULL。
想看看所有员工,包括没有部门的:
SELECT e.emp_name, e.salary, d.dept_name FROM employees e LEFT JOIN departments d ON e.dept_id = d.dept_id ORDER BY e.emp_name;
结果:
| emp_name | salary | dept_name |
|---|---|---|
| Alice Johnson | 75000.00 | Human Resources |
| Bob Smith | 85000.00 | Engineering |
| Carol Da vis | 90000.00 | Engineering |
| Da vid Wilson | 65000.00 | Marketing |
| Eve Brown | 70000.00 | Human Resources |
| Frank Miller | 80000.00 | Finance |
| Grace Lee | 55000.00 | NULL |
Grace Lee 出现了,dept_name 为 NULL,说明她没分配到任何部门。
右外连接与左外连接相反:它返回右表中的所有行,左表没匹配时用 NULL 填充。
这里我们新加一个 projects 表来演示(不然 departments 表所有部门都有员工,看不出效果)。
-- 创建项目表
CREATE TABLE projects (
project_id SERIAL PRIMARY KEY,
project_name VARCHAR(100) NOT NULL,
dept_id INT,
budget DECIMAL(12, 2)
);
-- 插入项目数据
INSERT INTO projects (project_name, dept_id, budget) VALUES
('Website Redesign', 1, 50000.00),
('Mobile App', 2, 100000.00),
('Market Research', 3, 25000.00),
('New Office Setup', 5, 75000.00); -- 部门ID 5 在 departments 中不存在
-- 查询项目及其所属部门 (使用 RIGHT JOIN)
SELECT p.project_name, p.budget, d.dept_name
FROM projects p
RIGHT JOIN departments d ON p.dept_id = d.dept_id
ORDER BY d.dept_name;
结果:
| project_name | budget | dept_name |
|---|---|---|
| Website Redesign | 50000.00 | Human Resources |
| Mobile App | 100000.00 | Engineering |
| Market Research | 25000.00 | Marketing |
| NULL | NULL | Finance |
Finance 部门在 projects 表中没有对应项目,所以 project_name 和 budget 都是 NULL——RIGHT JOIN 保留了右表(departments)的所有记录。
全外连接返回左表和右表中的所有行。左表没匹配时右表字段填 NULL,右表没匹配时左表字段填 NULL。
用 employees 和 departments 表演示:
SELECT e.emp_name, e.salary, d.dept_name FROM employees e FULL OUTER JOIN departments d ON e.dept_id = d.dept_id ORDER BY e.emp_name, d.dept_name;
结果:
| emp_name | salary | dept_name |
|---|---|---|
| Alice Johnson | 75000.00 | Human Resources |
| Bob Smith | 85000.00 | Engineering |
| Carol Da vis | 90000.00 | Engineering |
| Da vid Wilson | 65000.00 | Marketing |
| Eve Brown | 70000.00 | Human Resources |
| Frank Miller | 80000.00 | Finance |
| Grace Lee | 55000.00 | NULL |
| NULL | NULL | Finance |
所有员工(包括 Grace)和所有部门(包括 Finance)都在结果里——这在做数据对比或审计时非常有用。
适合需要全面了解两个表中所有数据的情况,尤其数据比对、差异分析。
交叉连接又叫笛卡尔积:第一个表的每一行与第二个表的每一行组合。结果行数 = 左表行数 × 右表行数。通常不带 ON 条件。
SELECT e.emp_name, d.dept_name FROM employees e CROSS JOIN departments d ORDER BY e.emp_name, d.dept_name;
结果很长(7员工×4部门 = 28行),列举部分:
| emp_name | dept_name |
|---|---|
| Alice Johnson | Finance |
| Alice Johnson | Human Resources |
| Alice Johnson | Marketing |
| Alice Johnson | Engineering |
| Bob Smith | Finance |
| Bob Smith | Human Resources |
| Bob Smith | Marketing |
| Bob Smith | Engineering |
| Carol Da vis | Finance |
| Carol Da vis | Human Resources |
| Carol Da vis | Marketing |
| Carol Da vis | Engineering |
| Da vid Wilson | Finance |
| Da vid Wilson | Human Resources |
| Da vid Wilson | Marketing |
| Da vid Wilson | Engineering |
| Eve Brown | Finance |
| Eve Brown | Human Resources |
| Eve Brown | Marketing |
| Eve Brown | Engineering |
| Frank Miller | Finance |
| Frank Miller | Human Resources |
| Frank Miller | Marketing |
| Frank Miller | Engineering |
| Grace Lee | Finance |
| Grace Lee | Human Resources |
| Grace Lee | Marketing |
| Grace Lee | Engineering |
每个员工与每个部门都组合了一遍。
理论看完了,接下来我们用 Ja va JDBC 连接 PostgreSQL,实际跑一遍这些 JOIN 查询。
employees 和 departments 表并插入数据。org.postgresql postgresql 42.6.0
import ja va.sql.Connection;
import ja va.sql.DriverManager;
import ja va.sql.SQLException;
public class DatabaseConnection {
private static final String URL = "jdbc:postgresql://localhost:5432/your_database_name";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
}
import ja va.sql.*;
import ja va.util.ArrayList;
import ja va.util.List;
class EmployeeWithDept {
private String employeeName;
private Double salary;
private String departmentName;
public EmployeeWithDept(String employeeName, Double salary, String departmentName) {
this.employeeName = employeeName;
this.salary = salary;
this.departmentName = departmentName;
}
// Getters and Setters
public String getEmployeeName() { return employeeName; }
public void setEmployeeName(String employeeName) { this.employeeName = employeeName; }
public Double getSalary() { return salary; }
public void setSalary(Double salary) { this.salary = salary; }
public String getDepartmentName() { return departmentName; }
public void setDepartmentName(String departmentName) { this.departmentName = departmentName; }
@Override
public String toString() {
return "EmployeeWithDept{" +
"employeeName='" + employeeName + '\'' +
", salary=" + salary +
", departmentName='" + departmentName + '\'' +
'}';
}
}
public class EmployeeReportService {
public List getEmployeesWithDepartments() throws SQLException {
List results = new ArrayList<>();
String sql = """
SELECT e.emp_name, e.salary, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
ORDER BY e.emp_name
""";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
String empName = rs.getString("emp_name");
Double salary = rs.getDouble("salary");
String deptName = rs.getString("dept_name");
results.add(new EmployeeWithDept(empName, salary, deptName));
}
}
return results;
}
public static void main(String[] args) {
EmployeeReportService service = new EmployeeReportService();
try {
List employees = service.getEmployeesWithDepartments();
System.out.println("=== 员工及其部门 (INNER JOIN) ===");
for (EmployeeWithDept emp : employees) {
System.out.println(emp);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
输出:
=== 员工及其部门 (INNER JOIN) ===
EmployeeWithDept{employeeName='Alice Johnson', salary=75000.0, departmentName='Human Resources'}
EmployeeWithDept{employeeName='Bob Smith', salary=85000.0, departmentName='Engineering'}
EmployeeWithDept{employeeName='Carol Da vis', salary=90000.0, departmentName='Engineering'}
EmployeeWithDept{employeeName='Da vid Wilson', salary=65000.0, departmentName='Marketing'}
EmployeeWithDept{employeeName='Eve Brown', salary=70000.0, departmentName='Human Resources'}
EmployeeWithDept{employeeName='Frank Miller', salary=80000.0, departmentName='Finance'}
public class EmployeeReportService {
// ... 上面已有方法
public List getAllEmployeesWithDepartments() throws SQLException {
List results = new ArrayList<>();
String sql = """
SELECT e.emp_name, e.salary, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
ORDER BY e.emp_name
""";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
String empName = rs.getString("emp_name");
Double salary = rs.getDouble("salary");
String deptName = rs.getString("dept_name");
results.add(new EmployeeWithDept(empName, salary, deptName));
}
}
return results;
}
public static void main(String[] args) {
EmployeeReportService service = new EmployeeReportService();
try {
List employees = service.getAllEmployeesWithDepartments();
System.out.println("=== 所有员工及其部门 (LEFT JOIN) ===");
for (EmployeeWithDept emp : employees) {
System.out.println(emp);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
输出中会出现 Grace Lee 的 departmentName 为 null。
根据部门 ID 获取员工:
public class EmployeeReportService {
// ... 上面已有方法
public List getEmployeesByDepartmentId(int deptId) throws SQLException {
List results = new ArrayList<>();
String sql = """
SELECT e.emp_name, e.salary, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
WHERE d.dept_id =
ORDER BY e.emp_name
""";
try (Connection conn = DatabaseConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, deptId);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
String empName = rs.getString("emp_name");
Double salary = rs.getDouble("salary");
String deptName = rs.getString("dept_name");
results.add(new EmployeeWithDept(empName, salary, deptName));
}
}
}
return results;
}
public static void main(String[] args) {
EmployeeReportService service = new EmployeeReportService();
try {
List engineeringEmployees = service.getEmployeesByDepartmentId(2);
System.out.println("=== Engineering 部门员工 ===");
for (EmployeeWithDept emp : engineeringEmployees) {
System.out.println(emp);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
输出:
=== Engineering 部门员工 ===
EmployeeWithDept{employeeName='Bob Smith', salary=85000.0, departmentName='Engineering'}
EmployeeWithDept{employeeName='Carol Da vis', salary=90000.0, departmentName='Engineering'}
WHERE 子句在 JOIN 之后执行,它作用于连接后的结果集。所以能用 WHERE 来进一步过滤。
SELECT e.emp_name, d.dept_name, e.salary FROM employees e INNER JOIN departments d ON e.dept_id = d.dept_id WHERE e.salary > 75000;
先做 INNER JOIN,再筛选薪资大于 75000 的行。
可以连多个表。假设还有 projects 表和 project_assignments 表:
SELECT e.emp_name, d.dept_name, p.project_name FROM employees e INNER JOIN departments d ON e.dept_id = d.dept_id INNER JOIN project_assignments pa ON e.emp_id = pa.emp_id INNER JOIN projects p ON pa.project_id = p.project_id;
SELECT e.emp_name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id = d.dept_id;
-- 错误 SELECT e.emp_name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id = d.dept_id WHERE dept_id = 1; -- 应该用 e.dept_id 或 d.dept_id -- 正确 SELECT e.emp_name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id = d.dept_id WHERE e.dept_id = 1;
WHERE 过滤最终结果,JOIN 定义如何组合表,意图不同。
连接两个大表时操作可能很慢。确保有合适的索引,必要时考虑分区或其他优化策略。
JOIN 查询是 PostgreSQL 中最核心的特性之一,它让我们能从多张表中灵活提取并整合数据。从简单的 INNER JOIN 到复杂的多表连接,掌握这些技能对构建高效、准确的数据库应用至关重要。
在 Ja va 里通过 JDBC 执行 JOIN 查询,能构建出功能丰富的数据驱动系统。无论是基础员工信息查询,还是复杂的业务报表,JOIN 都是不可或缺的工具。
随着数据量增长和分析需求复杂化,进一步学习子查询、窗口函数、CTE(公用表表达式)等会更有帮助。未来数据库可能更多与机器学习、实时分析结合,但扎实的基础查询能力始终是理解这些高级技术的基石。
希望这篇内容能帮你更好地理解 PostgreSQL 的 JOIN。如果你有疑问或者想聊更高级的用法,欢迎留言讨论!
参考链接:
Mermaid 图表:JOIN 类型比较



Mermaid 图表:JOIN 查询流程

Mermaid 图表:不同 JOIN 类型示意图


侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述