【发布时间】:2021-01-24 01:49:12
【问题描述】:
我目前正在使用存储移动设备的 PostgreSQL 12 数据库。其中,它有两个表device_types 和device_groups。数据分布是这样的,大多数device_groups 中只有很少的device_types,例如“2020 年发布的 iOS 设备”大约有 10 个device_types,而少数device_groups 有非常多的device_types,例如“现有的所有 Android 模型”,大约有 80'000 device_types。
这很麻烦,因为 PostgreSQL 查询规划器会做出错误的估计,因此在使用 device_group“所有 Android 模型存在”时,稍后会在查询中选择次优的连接类型。根据其统计数据,它预计平均只有极少数device_types,但实际上在某些情况下必须处理包含 80,000 台设备的中间结果集。
我在下面创建了一个最小的复制案例。
这是数据模型:
CREATE TABLE device_groups (
id integer CONSTRAINT device_groups_pk PRIMARY KEY
);
CREATE TABLE device_types (
id integer CONSTRAINT device_types_pk PRIMARY KEY,
device_group_id integer CONSTRAINT device_types_fk_device_groups_id REFERENCES device_groups(id)
);
现在我们插入一些倾斜的数据。大多数设备组有 ~10 个device_types,一个有 80k 个device_types:
/* create 100 device groups */
INSERT INTO device_groups(id)
SELECT i
FROM generate_series(1, 100) as t(i);
/* 99 device groups have only a few entries, 10 on average: */
INSERT INTO device_types(id, device_group_id)
SELECT i, trunc(random() * 98 + 1)
FROM generate_series(1, 1000) as t(i);
/* device group 100 has 80k devices: */
INSERT INTO device_types(id, device_group_id)
SELECT i, 100
FROM generate_series(1001, 81001) as t(i);
ANALYZE device_groups;
ANALYZE device_types;
毫不奇怪,这个查询运行良好,因为 Postgres 拥有估计结果基数所需的所有统计信息:
EXPLAIN ANALYZE
SELECT *
FROM device_types
WHERE device_types.device_group_id = 100;
/* => correctly estimates ~80k rows: */
/* Seq Scan on device_types (cost=0.00..1371.51 rows=79932 width=8) (actual time=0.104..665.699 rows=80001 loops=1) */
这个查询(我有点惊讶)也正确估计了它将返回多少行:
EXPLAIN ANALYZE
SELECT *
FROM device_types
JOIN device_groups ON device_types.device_group_id = device_groups.id
WHERE device_groups.id = 100;
/* => also correctly estimates ~80k rows: */
/* Nested Loop (cost=0.00..2173.08 rows=79932 width=12) (actual time=0.165..1930.591 rows=80001 loops=1) */
现在我们添加一个额外的间接层,通过添加第三个表 device_group_collections 来加入我们的连接,这就是问题所在:
CREATE TABLE device_group_collections (
device_group_id integer CONSTRAINT device_group_collections_fk_device_groups_id REFERENCES device_groups(id),
collection_id integer
);
INSERT INTO device_group_collections(device_group_id, collection_id) VALUES (100, 25);
ANALYZE device_group_collections;
EXPLAIN ANALYZE
SELECT *
FROM device_types
JOIN device_groups ON device_types.device_group_id = device_groups.id
JOIN device_group_collections ON device_group_collections.device_group_id = device_groups.id
WHERE device_group_collections.collection_id = 25;
/* => estimates only 810 rows, but in reality there will be 80'001 rows: */
/* Hash Join (cost=3.42..1484.29 rows=810 width=20) (actual time=19.817..1949.068 rows=80001 loops=1) */
这是 Postgres 查询计划器的一般限制,还是有什么方法可以帮助它理解数据集的偏差?
【问题讨论】:
-
我可能遗漏了一些东西,但 device_group_collections 中只有一条记录,请参阅 INSERT 语句。也没有主键,collection_id 上也没有索引。您还可以更改统计信息的设置,整体或仅此表:ALTER TABLE SET STATISTICS postgresql.org/docs/current/planner-stats.html
标签: sql postgresql