使用 PostgreSQL 14,我正在处理一个表
workplaces
,它模拟了一个工作场所树。除了 country_code
等属性外,每个工作场所还通过 parent_id
外键(如果没有父项则为 NULL)来标识其父工作场所:
CREATE TABLE workplaces(
id SERIAL PRIMARY KEY,
parent_id INTEGER REFERENCES(workplaces.id),
..
)
我现在想通过选择几个有趣的行然后识别它的所有后代(即孩子、孙子等)来识别此表中的工作场所组。我的想法是建立一个表,将工作场所 ID 映射到它们的“根祖先”。递归 CTE 完成这项工作:
WITH RECURSIVE
"ancestors" AS (
SELECT
id AS id,
id AS ancestor_or_self_id
FROM
workplaces
WHERE country_code = 'DE' AND type = 'clinic' AND duplicate_of IS NULL
UNION ALL (
SELECT
w.id AS id,
a.ancestor_or_self_id AS ancestor_or_self_id
FROM workplaces AS w
JOIN ancestors AS a ON w.parent_id = a.id
)
)
SELECT COUNT(*) FROM ancestors;
;
让我感到惊讶的是,这需要很长时间(在我的笔记本电脑上大约需要一秒钟)。几个数字可以说明这一点:
workplaces
表有 295k 行这是查询计划:
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=789384.97..789384.98 rows=1 width=8) (actual time=795.210..795.246 rows=1 loops=1)
CTE ancestors
-> Recursive Union (cost=1000.00..723164.45 rows=2943134 width=16) (actual time=41.765..788.647 rows=35431 loops=1)
-> Gather (cost=1000.00..23501.17 rows=3794 width=16) (actual time=41.764..54.481 rows=3758 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on workplaces (cost=0.00..22121.77 rows=1581 width=16) (actual time=37.064..49.939 rows=1253 loops=3)
Filter: ((duplicate_of IS NULL) AND ((country_code)::text = 'DE'::text) AND ((type)::text = 'clinic'::text))
Rows Removed by Filter: 97135
-> Merge Join (cost=58737.30..64080.06 rows=293934 width=16) (actual time=112.727..119.909 rows=5279 loops=6)
Merge Cond: (a.id = w.parent_id)
-> Sort (cost=3644.41..3739.26 rows=37940 width=16) (actual time=1.360..1.613 rows=5905 loops=6)
Sort Key: a.id
Sort Method: quicksort Memory: 25kB
-> WorkTable Scan on ancestors a (cost=0.00..758.80 rows=37940 width=16) (actual time=0.001..0.344 rows=5905 loops=6)
-> Materialize (cost=55092.89..56568.70 rows=295163 width=16) (actual time=109.127..115.263 rows=43088 loops=6)
-> Sort (cost=55092.89..55830.80 rows=295163 width=16) (actual time=109.126..111.831 rows=43088 loops=6)
Sort Key: w.parent_id
Sort Method: external merge Disk: 5696kB
-> Seq Scan on workplaces w (cost=0.00..23228.63 rows=295163 width=16) (actual time=0.007..67.569 rows=295163 loops=6)
-> CTE Scan on ancestors (cost=0.00..58862.68 rows=2943134 width=0) (actual time=41.769..793.929 rows=35431 loops=1)
Planning Time: 0.377 ms
Execution Time: 797.738 ms
我读到的是,大部分时间都花在了作为递归步骤的一部分(作为实现 CTE 的一部分)的行排序上:看起来 PostgreSQL 决定不在内存中执行此操作,而是在磁盘上执行合并排序。为什么会这样——不应该像我正在处理的那样只用两个整数和行数对表进行排序很容易放入内存?
workplaces
表上只有一个相关索引,在id
列上(因为它是主键)。我也尝试在 parent_id
列上设置索引,但这似乎没有任何帮助。
work_mem
设置 被记录为影响排序:在此实例中它被设置为仅 4MB。将它增加到 32MB 有很大帮助——它避免了外部磁盘合并,而是让 PostgreSQL 在内存中进行快速排序。
有没有一种方法可以在不增加内存的情况下加速查询?令我困惑的是,它一直在尝试对所有 295k 工作区行进行排序——我的假设是它只需要对每个递归步骤进行排序以执行合并排序(并且每个步骤产生的行数比 295k 少得多)。
无法访问数据,我只能猜测什么可以用于索引。
CREATE INDEX idx_workplaces_country_type
ON workplaces(country_code, type) -- maybe first type and then country_code
WHERE duplicate_of IS NULL;
绝对是这个:
CREATE INDEX idx_workplaces_parent_id
ON workplaces(parent_id);
请分享新的查询计划,以便我们可以看到某些更改/改进;
您还可以在索引中包含 id。这可以启用仅索引扫描:
CREATE INDEX idx_workplaces_parent_id_include_id
ON workplaces(parent_id) INCLUDE(id);
random_page_cost 应该代表随机 IO 与顺序 IO 相比的性能。默认值 4 过去适用于旋转磁盘,但对于 SSD,您应该选择一个较低的值。接近 1 的值是一个很好的起点,例如 1.1