Pretend that the single row just extracted is the only row in the recursive table and run the recursive-select, adding all results to the queue.
『查询根节点所有后代』通用 SQL
下面 SQL 基本可用于 MySQL 和 SQLite(不支持的特性,数据库会报错,改掉即可)
PRAGMA cache_size =-204800; -- 允许 SQLite 缓存 200 MB-- 闭包表查询SELECTCOUNT(*), SUM(code), SUM(CHAR_LENGTH(name)) -- SQLite 写法:SUM(LENGTH(name))FROM closure_tree
FORCE INDEX (idx_closure_tree) -- 我这测试,MySQL 不加这行,耗时翻好几倍。SQLite 需去掉此行JOIN closure ON id = descendant
WHERE ancestor =0;
-- 递归邻接表查询WITHRECURSIVE
find(id, code, name, is_leaf) AS (
SELECT id, code, name, is_leaf
FROM adjacent
WHERE pid =0UNIONALLSELECT b.id, b.code, b.name, b.is_leaf
FROM find a
JOIN adjacent b ONNOT a.is_leaf AND b.pid = a.id
)
SELECTCOUNT(*), SUM(code), SUM(CHAR_LENGTH(name)) -- SQLite 写法:SUM(LENGTH(name))FROM find;
-- 理想中,没有递归损耗的邻接表查询SELECTCOUNT(*), SUM(b.code), SUM(CHAR_LENGTH(b.name)) -- SQLite 写法:SUM(LENGTH(b.name))FROM adjacent a
LEFTJOIN adjacent b ON b.pid = a.id -- SQLite 需要 LEFT JOIN,否则耗时翻几倍WHERENOT a.is_leaf;
『查询根节点第 5 层后代』通用 SQL
PRAGMA cache_size =-204800; -- 允许 SQLite 缓存 200 MB-- 闭包表查询SELECTCOUNT(*), SUM(code), SUM(CHAR_LENGTH(name)) -- SQLite 写法:SUM(LENGTH(name))FROM closure_tree
FORCE INDEX (idx_closure_tree) -- 我这测试,MySQL 不加这行,耗时翻好几倍。SQLite 需去掉此行JOIN closure ON id = descendant
WHERE ancestor =0AND distance =5;
-- 递归邻接表查询WITHRECURSIVE
var(depth) AS (
SELECT5
),
-- 递归部分查前 N - 1 层
find(id, is_leaf, depth) AS (
SELECT0, FALSE, var.depth -1FROM var
UNIONALLSELECT b.id, b.is_leaf, a.depth -1FROM find a
JOIN adjacent b ON b.pid = a.id
WHERE a.depth >0ANDNOT a.is_leaf
)
-- 最后一次性 JOIN 第 N 层SELECTCOUNT(*), SUM(b.code), SUM(CHAR_LENGTH(b.name)) -- SQLite 写法:SUM(LENGTH(b.name))FROM find a
CROSSJOIN adjacent b ON a.id = b.pid -- SQLite 要加 CROSS,否则耗时翻几倍WHERE a.depth =0;
-- 理想中,没有递归损耗的邻接表查询(需要根据层数 N,动态生成 SQL)SELECTCOUNT(*), SUM(t5.code), SUM(CHAR_LENGTH(t5.name)) -- SQLite 写法:SUM(LENGTH(t5.name))FROM adjacent t1
JOIN adjacent t2 ON t2.pid = t1.id
JOIN adjacent t3 ON t3.pid = t2.id
JOIN adjacent t4 ON t4.pid = t3.id
JOIN adjacent t5 ON t5.pid = t4.id
WHERE t1.pid =0;
MySQL 一键建表 SQL
(在我低配笔记本和固态上,大约执行了 1 分钟)
-- 允许 200 MB 的内存表SET max_heap_table_size =200<<20;
-- 建临时数据表,装载 csv 数据,以及计算序号和父子关系CREATE TABLE data (
code BIGINTNOT NULL,
p_code BIGINTNOT NULL,
type TINYINT NOT NULL,
name VARCHAR(25) NOT NULL,
id INTNOT NULL,
pid INTNOT NULL,
PRIMARY KEY (code) USING BTREE,
INDEX USING BTREE (id),
INDEX USING BTREE (pid, id)
) ENGINE = MEMORY;
-- 加载 csv
LOAD DATA INFILE 'area_code_2022.csv'INTOTABLE data
CHARACTER SET UTF8MB4
FIELDS TERMINATED BY','
ENCLOSED BY'"'
(code, name, type, p_code);
-- 按照 code 顺序计算 idUPDATE data
JOIN (SELECT code, ROW_NUMBER() OVER win row_num
FROM data
WINDOW win AS (ORDERBY code)) t USING(code)
SET id = row_num;
-- 计算 parent_id(不存在的标0)UPDATE data a
LEFTJOIN data b ON b.code = a.p_code
SET a.pid = IFNULL(b.id, 0);
-- 建邻接表,并从临时数据表填充数据CREATE TABLE adjacent (
id INTNOT NULL,
pid INTNOT NULL,
is_leaf BOOL NOT NULL,
type TINYINT NOT NULL,
code BIGINTNOT NULL,
name VARCHAR(25) NOT NULL,
PRIMARY KEY (pid, id)
)
SELECT-1 pid, 0 id, FALSE is_leaf, 0 type, 0 code, '' name
UNIONALLSELECT pid, id, type =5 is_leaf, type, code, name
FROM data;
-- 建闭包表主表,并从临时数据表填充数据CREATE TABLE closure (
id INTNOT NULL,
type TINYINT NOT NULL,
code BIGINTNOT NULL,
name VARCHAR(25) NOT NULL,
PRIMARY KEY (id)
)
SELECT0 id, 0 type, 0 code, '' name
UNIONALLSELECT id, type, code, name
FROM data;
-- 建闭包表树形关系表CREATE TABLE closure_tree (
ancestor INTNOT NULL,
descendant INTNOT NULL,
distance TINYINT NOT NULL,
PRIMARY KEY (descendant, distance)
);
-- 递归构建树形关系INSERT INTO closure_tree(ancestor, descendant, distance)
WITHRECURSIVE
parent_of(orig_id, id, dist) AS (
SELECT id, id, 0FROM data
UNIONALLSELECT orig_id, pid, dist +1FROM parent_of
JOIN data USING(id)
WHERE id
)
SELECT id, orig_id, dist
FROM parent_of;
-- 为闭包表树形关系表建二级索引CREATE INDEX idx_closure_tree ON closure_tree (ancestor, distance);
-- 丢弃临时数据表DROPTABLE data;