应用程序很少独自决定最终执行的 SQL。ORM 会拼接关联,视图会隐藏更多查询层,业务代码还可能在外面继续包装。多层组合之后,同一张表可能通过主键与自己连接,而这个 JOIN 既没有增加行,也没有提供新的信息。PostgreSQL 18 引入的 enable_self_join_elimination,正是用来帮助优化器识别并移除这类冗余自连接。
冗余自连接是怎样出现的
下面的查询看起来有两个数据来源,实际上 u1.id = u2.id 使用了主键等值连接。对 u1 的每一行来说,u2 至多只能匹配同一行:
SELECT u1.id, u1.email
FROM app_user AS u1
JOIN app_user AS u2 ON u2.id = u1.id
WHERE u1.status = 'active';
开发者通常不会手写这种 SQL,但它可能从以下组合中产生:
- ORM 为一个已经存在的实体关系再次添加 JOIN。
- 查询引用嵌套视图,而内外两层都关联了同一张表。
- 通用查询构造器为了复用过滤逻辑,引入了额外别名。
- 报表或权限组件把自己的 JOIN 叠加到业务查询上。
如果优化器能够证明连接不会改变结果,就可以把两个关系实例折叠为一个。这样不仅少执行一次表访问,也可能缩小后续规划空间,并减少无意义的中间操作。
关键在于“能够证明”。表名相同并不等于连接冗余;主键、唯一性、连接条件、过滤条件和所引用的列都会影响等价性判断。
用 EXPLAIN 验证,而不是凭 SQL 外观猜测
可以这样实践:在 PostgreSQL 18 测试实例中创建一张小表,分别打开和关闭该优化,然后比较执行计划。以下脚本可直接交给 psql 执行:
DROP TABLE IF EXISTS app_user;
CREATE TABLE app_user (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
status text NOT NULL
);
INSERT INTO app_user (email, status)
SELECT
'user' || n || '@example.com',
CASE WHEN n % 3 = 0 THEN 'disabled' ELSE 'active' END
FROM generate_series(1, 10000) AS n;
ANALYZE app_user;
SET enable_self_join_elimination = on;
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT u1.id, u1.email
FROM app_user AS u1
JOIN app_user AS u2 ON u2.id = u1.id
WHERE u1.status = 'active';
SET enable_self_join_elimination = off;
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT u1.id, u1.email
FROM app_user AS u1
JOIN app_user AS u2 ON u2.id = u1.id
WHERE u1.status = 'active';
RESET enable_self_join_elimination;
运行前确认服务器版本为 PostgreSQL 18,并确保当前角色有权修改会话级参数。观察重点不是一次执行耗时,而是计划中是否仍然存在两个 app_user 关系实例,以及 JOIN 节点、扫描次数和缓冲区访问是否发生变化。
在真实系统中,建议对生产慢查询的脱敏副本执行同样的 A/B 对比:
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 <<'SQL'
BEGIN;
SET LOCAL enable_self_join_elimination = on;
EXPLAIN (ANALYZE, BUFFERS, SETTINGS, FORMAT TEXT)
SELECT u1.id, u1.email
FROM app_user AS u1
JOIN app_user AS u2 ON u2.id = u1.id
WHERE u1.status = 'active';
ROLLBACK;
SQL
这里使用 SET LOCAL 并放在事务中,参数只影响当前事务,适合做受控验证。不要直接对高成本写查询使用 EXPLAIN ANALYZE,因为它会真正执行语句。
哪些查询不能简单消掉 JOIN
自连接优化并不是“看到同一张表两次就删除一次”。例如,员工与经理都存放在同一张表时,自连接表达的是两条不同记录之间的关系:
SELECT employee.name, manager.name AS manager_name
FROM employee
LEFT JOIN employee AS manager
ON manager.id = employee.manager_id;
这个 JOIN 不能被移除,因为 employee.manager_id 并不要求等于 employee.id,查询也确实读取了经理行的数据。
其他需要谨慎判断的情况包括:
- 连接条件不是主键或可证明唯一的键。
- 两个别名上存在不同过滤条件。
- 外连接的空值扩展会改变结果。
- 查询依赖两个关系实例的列、锁定行为或其他语义。
- 数据库缺少可供优化器使用的约束,虽然业务代码声称字段唯一,但模式中没有声明。
这也提醒我们:正确声明主键和唯一约束不仅维护数据完整性,还能为优化器提供证明查询等价性所需的信息。仅创建普通索引,并不等同于声明唯一性。
上线时把它当作规划器变更管理
升级 PostgreSQL 18 后,不应因为有了自连接消除就停止治理 ORM 查询。优化器可以清理一部分冗余,但复杂 SQL 仍会增加解析、规划和排障成本。更稳妥的采用方式是:
- 从
pg_stat_statements或 APM 中筛选包含重复表引用的高频、高耗时查询。 - 在接近生产的数据规模与统计信息下,对比参数开启和关闭时的
EXPLAIN输出。 - 检查主键、唯一约束和统计信息是否完整,不要用应用层假设代替数据库约束。
- 对关键查询保存计划基线,并在 PostgreSQL 18 升级测试中检查计划变化。
- 若发现异常,先使用会话级或事务级设置缩小影响范围,再判断是改写 SQL、修正模式,还是调整参数。
enable_self_join_elimination 的价值不只是少一个 JOIN。它说明现代数据库优化器正在更积极地处理由 ORM、视图和查询构造器带来的结构性冗余。不过,优化必须建立在可证明的关系约束上;完整的模式定义、可重复的计划测试和真实负载验证,仍然是升级过程中不可替代的工作。