SELF JOIN本质是同一张表自关联,通过别名区分父子表,核心条件为子表.parent_id = 父表.id;查单层父子用INNER JOIN自动过滤根节点,查全量需LEFT JOIN配合COALESCE;parent_id字段必须建索引。

SELF JOIN 本质是把一张表当两张表用
SELF JOIN 不是特殊语法,就是普通 JOIN,只是左表和右表都指向同一张表(比如 categories),靠别名区分父子关系。关键不是“怎么写”,而是“哪两行该连”——通常靠 parent_id 字段关联:子节点的 parent_id 等于父节点的 id。
常见错误是搞反方向:写成 t1.id = t2.parent_id(这会查出所有子节点及其直接子节点,不是父子对)。正确方向是:t1.id = t2.parent_id 表示 t2 是 t1 的子节点,所以父子对是 (t1, t2);若要查“每个节点及其父节点”,就得写 t1.parent_id = t2.id,此时 t1 是子,t2 是父。
查单层父子关系:用 INNER JOIN 避免 NULL 父节点
如果只要“有父节点”的记录(即排除根节点),用 INNER JOIN 最干净。假设表结构为:id, name, parent_id(根节点 parent_id 为 NULL 或 0):
SELECT parent.name AS parent_name, child.name AS child_name, child.id AS child_id FROM categories AS child INNER JOIN categories AS parent ON child.parent_id = parent.id;
注意点:
-
child.parent_id = parent.id是核心条件,顺序不能颠倒 - 根节点(
parent_id IS NULL)自动被过滤掉,不需要额外WHERE - 如果根节点用
parent_id = 0,且id从 1 开始,得加AND child.parent_id != 0,否则可能误连到 id=0 的假父节点
查所有节点(含根节点)及其父信息:LEFT JOIN + COALESCE
想让根节点也出现(父名为 NULL 或显示为 “(root)”),必须用 LEFT JOIN,以子节点为主表:
SELECT child.name AS node_name, COALESCE(parent.name, '(root)') AS parent_name, parent.id AS parent_id FROM categories AS child LEFT JOIN categories AS parent ON child.parent_id = parent.id;
容易踩的坑:
- 别把
LEFT JOIN写成RIGHT JOIN—— 语义难读,且易混淆主次表 -
COALESCE(parent.name, '(root)')比IFNULL或ISNULL更通用(兼容 MySQL/PostgreSQL/SQL Server) - 如果
parent_id允许为 0 且无对应 id=0 记录,LEFT JOIN仍返回NULL,这时COALESCE才真正生效
性能与索引:parent_id 字段必须有索引
SELF JOIN 在数据量稍大(比如 >1 万行)时会明显变慢,瓶颈几乎总在 parent_id 字段没索引。执行计划里如果看到 type: ALL 或 Extra: Using where; Using join buffer,基本就是缺索引。
建索引命令(MySQL):
CREATE INDEX idx_categories_parent_id ON categories (parent_id);
补充说明:
- 不需要复合索引(如
(parent_id, id)),单列parent_id就够了 - PostgreSQL 用户注意:
parent_id列若允许NULL,B-tree 索引默认不存NULL值,但JOIN条件是等值匹配,不影响查询逻辑 - 如果频繁查深层级(如祖-父-子),SELF JOIN 就力不从心了,得换闭包表或递归 CTE,别硬撑
parent_id 加索引——这三处改完,90% 的分类树父子查询就稳了。

















