让我们在 PostgreSQL 13 数据库中创建两个测试表:
CREATE TABLE foo (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
value int NOT NULL
);
CREATE TABLE bar (
id bigint PRIMARY KEY,
category_id bigint NOT NULL,
foo_id bigint REFERENCES foo (id),
value int
);
CREATE INDEX bar_category_id_ix ON bar (category_id);
并禁用autovacuum
这些表:
ALTER TABLE foo SET (autovacuum_enabled = false);
ALTER TABLE bar SET (autovacuum_enabled = false);
将 500000 条(一百万的一半)记录插入foo
,将它们传输到bar
表并分析:
INSERT INTO foo (value) SELECT * FROM generate_series(1, 500000);
ANALYZE foo;
INSERT INTO bar (id, category_id, foo_id, value) SELECT id, 1, id, value FROM foo WHERE value <= 500000;
ANALYZE bar;
(可选)确保在这些表上仅执行ANALYZE
(no ):autovacuum
SELECT relname, last_autovacuum, last_vacuum, last_autoanalyze, last_analyze FROM pg_stat_user_tables WHERE relname IN ('foo', 'bar');
插入另一块 500000 条记录(但不要运行ANALYZE
):
INSERT INTO foo (value) SELECT * FROM generate_series(500001, 1000000);
INSERT INTO bar (id, category_id, foo_id, value) SELECT id, 2, id, value FROM foo WHERE value > 500000;
由于我们没有运行ANALYZE
表统计是过时的,它与包含半百万条记录的阶段foo
有关bar
。现在让我们检查查询计划:
EXPLAIN SELECT * FROM bar
JOIN foo ON bar.foo_id = foo.id
WHERE category_id = 2;
----
Nested Loop (cost=0.85..12.89 rows=1 width=40)
-> Index Scan using bar_category_id_ix on bar (cost=0.42..4.44 rows=1 width=28)
Index Cond: (category_id = 2)
-> Index Scan using foo_pkey on foo (cost=0.42..8.44 rows=1 width=12)
Index Cond: (id = bar.foo_id)
和
EXPLAIN SELECT * FROM bar
JOIN foo ON bar.foo_id = foo.id;
---
Hash Join (cost=32789.00..71320.29 rows=999864 width=40)
Hash Cond: (bar.foo_id = foo.id)
-> Seq Scan on bar (cost=0.00..17351.64 rows=999864 width=28)
-> Hash (cost=15406.00..15406.00 rows=1000000 width=12)
-> Seq Scan on foo (cost=0.00..15406.00 rows=1000000 width=12)
我了解第一个查询计划错误地估计了仅 1 行 ( rows=1
) 的条件category_id = 2
,因为统计信息已过时(ANALYZE
在插入记录之前执行category_id = 2
)。(1) 但是,第二个查询计划是如何达到rows=999864
对条件的良好估计 () 的bar.foo_id = foo.id
?
此外,如果我们运行:
EXPLAIN SELECT * FROM bar
JOIN foo ON bar.foo_id = foo.id
WHERE category_id = 1;
----
Hash Join (cost=32789.00..73819.95 rows=999864 width=40)
Hash Cond: (bar.foo_id = foo.id)
-> Seq Scan on bar (cost=0.00..19851.30 rows=999864 width=28)
Filter: (category_id = 1)
-> Hash (cost=15406.00..15406.00 rows=1000000 width=12)
-> Seq Scan on foo (cost=0.00..15406.00 rows=1000000 width=12)
(2) 为什么规划器估计条件为 999864 行category_id = 1
?统计数据应该显示大约 500000 行满足它?
注意:我提出这些问题是因为根据经验,我观察到即使不分析表,只包含主键列的条件也会产生更好的查询计划,但我在 PostgreSQL 官方文档中没有找到任何关于这种行为的信息。
您观察到的“魔力”在于查询计划器的这个细节。引用手册:
它的值
pg_class
是reltuples
和relpages
- 磁盘上的活动行数和数据页数。由于物理大小将大约增加一倍,Postgres 预计会有大约那么多行,这解释了顺序扫描的相当准确的估计。
中的值频率
pg_statistic
已过时,不能像行数那样简单地缩放。你需要为此而奔跑ANALYZE
。解释索引扫描的估计值。我看不出PK会在其中发挥什么特殊作用。