设置 LIMIT 时 Postgres SLOWER:除了添加虚拟“ORDER BY”之外如何修复?
Postgres SLOWER when a LIMIT is set: how to fix besides adding a dummy `ORDER BY`?
在 Postgres 中,某些查询在添加 LIMIT
:
时会慢很多
查询:
SELECT * FROM review WHERE clicker_id=28 ORDER BY done DESC LIMIT 4; -- 51 sec
SELECT * FROM review WHERE clicker_id=28 ORDER BY id, done DESC LIMIT 4; -- 0.020s
SELECT * FROM review WHERE clicker_id=28 LIMIT 4; -- 0.007s
SELECT * FROM review WHERE clicker_id=28 ORDER BY id; -- 0.007s
如您所见,我需要在 ORDER BY
中添加一个虚拟 ID 以加快速度。我试图理解为什么。
运行 EXPLAIN
在他们身上:
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY done DESC LIMIT 4;
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY id, done DESC LIMIT 4;
EXPLAIN SELECT * FROM review WHERE clicker_id=28 LIMIT 4;
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY id;
给出这个:
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY done DESC LIMIT 4
Limit (cost=0.44..249.76 rows=4 width=56)
-> Index Scan using review_done on review (cost=0.44..913081.13 rows=14649 width=56)
Filter: (clicker_id = 28)
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY id, done DESC LIMIT 4
Limit (cost=11970.75..11970.76 rows=4 width=56)
-> Sort (cost=11970.75..12007.37 rows=14649 width=56)
Sort Key: id, done DESC
-> Index Scan using review_clicker_id on review (cost=0.44..11751.01 rows=14649 width=56)
Index Cond: (clicker_id = 28)
EXPLAIN SELECT * FROM review WHERE clicker_id=28 LIMIT 4
Limit (cost=0.44..3.65 rows=4 width=56)
-> Index Scan using review_clicker_id on review (cost=0.44..11751.01 rows=14649 width=56)
Index Cond: (clicker_id = 28)
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY id
Sort (cost=12764.61..12801.24 rows=14649 width=56)
Sort Key: id
-> Index Scan using review_clicker_id on review (cost=0.44..11751.01 rows=14649 width=56)
Index Cond: (clicker_id = 28)
我不是 SQL 专家,但我认为 Postgres 期望查询比实际更快,因此使用了一种实际上不合适的方法来获取数据,对吗?
数据库:
-
review
table:
- 包含 22+ 百万行。
- 给定用户将获得 7 066 行顶部。
- 测试中的那个(id 28)当时有288
- 有这样的结构:
- id: bigint 自动递增 [nextval('public.review_id_seq')]
- 类型:review_type NULL
- 迭代:smallint NULL
- 重复:smallint NULL
- 由于:timestamptz NULL
- 完成:timestamptz NULL
- 添加:timestamptz NULL
- clicker_id: bigint NULL
- monologue_id: bigint NULL
- 有这些指标:
- UNIQUE 类型,clicker_id,monologue_id,迭代
- 索引clicker_id
- INDEX 完成,到期,monologue_id
- 索引号
- INDEX 完成 DESC
- 索引类型
其他详细信息:
环境:
- 这些查询 运行 正在使用 Postgres 9.6.14 进行开发。
- 运行 生产环境中的查询(Heroku Postgres,版本 9.6.16)差异不大,但仍然不是很大:缓慢的查询可能需要 600 毫秒。
可变速度:
- 有时,相同的查询(完全相同,或不同的 clicker_id)运行 会快很多(不到 1 秒),但我不明白为什么。我需要它们 始终 快速。
- 如果我对拥有 288 行的用户使用
LIMIT 288
,那么它会快得多(< 1 秒),但是如果我对拥有 7066 行的用户使用相同的方式,那么它又会变得非常慢.
在我想到使用虚拟机 ORDER BY
之前,我尝试了这些:
- 正在重新导入数据库。
analyze review;
- 将 done 的索引设置为 DESC(以前设置为 default/ASC。)[当时的挑战是有 no proper way to check if/when the index is done rebuilding。]
None 有帮助。
问题:
我的问题本身已经解决了,但是我还是不满意:
- 这个 "pattern" 是否有一个 名称,其中包括添加一个虚拟
ORDER BY
以加快速度?
- 以后如何发现此类问题? (这花了很长时间才弄清楚。)除非我遗漏了什么,否则
EXPLAIN
没那么有用:
- 对于慢速查询,成本低得令人误解,而对于快速变体,成本高得令人误解。
- 备选方案:是否有另一种方法来处理这个问题?(因为这个解决方案感觉像是 hack。)
谢谢!
类似问题:
- PostgreSQL query very slow with limit 1 几乎是同一个问题,除了他的查询在 LIMIT 1 时很慢,而在 LIMIT 3 或更高时很好。然后当然数据不一样了。
这里的根本问题是所谓的提前中止查询计划。这是来自 pgsql-hackers 的一个线程,描述了类似的东西:
https://www.postgresql.org/message-id/541A2335.3060100%40agliodbs.com
从那里引用,这就是为什么当存在 ORDER BY done DESC 时,规划器使用通常非常慢的索引扫描:
As usual, PostgreSQL is dramatically undercounting n_distinct: it shows
chapters.user_id at 146,000 and the ratio of to_user_id:from_user_id as
being 1:105 (as opposed to 1:6, which is about the real ratio). This
means that PostgreSQL thinks it can find the 20 rows within the first 2%
of the index ... whereas it actually needs to scan 50% of the index to
find them.
在您的情况下,规划器认为如果它只是开始遍历按 done desc 排序的行(IOW,使用 review_done 索引),它将找到 4 行 clicker_id=28迅速地。由于需要按 "done" 降序返回行,因此它认为这将节省一个排序步骤并且比检索答题器 28 的所有行然后排序更快。考虑到行的真实世界分布,通常情况并非如此,需要它在找到 4 行之前跳过大量行,clicker=28。
一种更通用的处理方法是使用 CTE(在 9.6 中,它仍然是一个优化栅栏 - 这在 PG 12 中发生了变化,仅供参考)来获取没有顺序的行,然后在外部查询中添加 ORDER BY。鉴于为用户获取所有行,对它们进行排序,然后返回您需要的行数对于您的数据集来说是完全合理的(即使是 7k 行点击器也不应该成为问题),您可以防止计划者相信提前中止如果在 CTE 中没有 ORDER BY 或 LIMIT,计划将是最快的,为您提供类似以下内容的查询:
WITH clicker_rows as (SELECT * FROM review WHERE clicker_id=28)
select * From clicker_rows ORDER BY done DESC LIMIT 4;
这应该是可靠的快速,同时仍然遵守您想要的 ORDER BY 和 LIMIT。不过,我不确定这种模式是否有名称。
在 Postgres 中,某些查询在添加 LIMIT
:
查询:
SELECT * FROM review WHERE clicker_id=28 ORDER BY done DESC LIMIT 4; -- 51 sec
SELECT * FROM review WHERE clicker_id=28 ORDER BY id, done DESC LIMIT 4; -- 0.020s
SELECT * FROM review WHERE clicker_id=28 LIMIT 4; -- 0.007s
SELECT * FROM review WHERE clicker_id=28 ORDER BY id; -- 0.007s
如您所见,我需要在 ORDER BY
中添加一个虚拟 ID 以加快速度。我试图理解为什么。
运行 EXPLAIN
在他们身上:
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY done DESC LIMIT 4;
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY id, done DESC LIMIT 4;
EXPLAIN SELECT * FROM review WHERE clicker_id=28 LIMIT 4;
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY id;
给出这个:
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY done DESC LIMIT 4
Limit (cost=0.44..249.76 rows=4 width=56)
-> Index Scan using review_done on review (cost=0.44..913081.13 rows=14649 width=56)
Filter: (clicker_id = 28)
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY id, done DESC LIMIT 4
Limit (cost=11970.75..11970.76 rows=4 width=56)
-> Sort (cost=11970.75..12007.37 rows=14649 width=56)
Sort Key: id, done DESC
-> Index Scan using review_clicker_id on review (cost=0.44..11751.01 rows=14649 width=56)
Index Cond: (clicker_id = 28)
EXPLAIN SELECT * FROM review WHERE clicker_id=28 LIMIT 4
Limit (cost=0.44..3.65 rows=4 width=56)
-> Index Scan using review_clicker_id on review (cost=0.44..11751.01 rows=14649 width=56)
Index Cond: (clicker_id = 28)
EXPLAIN SELECT * FROM review WHERE clicker_id=28 ORDER BY id
Sort (cost=12764.61..12801.24 rows=14649 width=56)
Sort Key: id
-> Index Scan using review_clicker_id on review (cost=0.44..11751.01 rows=14649 width=56)
Index Cond: (clicker_id = 28)
我不是 SQL 专家,但我认为 Postgres 期望查询比实际更快,因此使用了一种实际上不合适的方法来获取数据,对吗?
数据库:
-
review
table:- 包含 22+ 百万行。
- 给定用户将获得 7 066 行顶部。
- 测试中的那个(id 28)当时有288
- 有这样的结构:
- id: bigint 自动递增 [nextval('public.review_id_seq')]
- 类型:review_type NULL
- 迭代:smallint NULL
- 重复:smallint NULL
- 由于:timestamptz NULL
- 完成:timestamptz NULL
- 添加:timestamptz NULL
- clicker_id: bigint NULL
- monologue_id: bigint NULL
- 有这些指标:
- UNIQUE 类型,clicker_id,monologue_id,迭代
- 索引clicker_id
- INDEX 完成,到期,monologue_id
- 索引号
- INDEX 完成 DESC
- 索引类型
- 包含 22+ 百万行。
其他详细信息:
环境:
- 这些查询 运行 正在使用 Postgres 9.6.14 进行开发。
- 运行 生产环境中的查询(Heroku Postgres,版本 9.6.16)差异不大,但仍然不是很大:缓慢的查询可能需要 600 毫秒。
可变速度:
- 有时,相同的查询(完全相同,或不同的 clicker_id)运行 会快很多(不到 1 秒),但我不明白为什么。我需要它们 始终 快速。
- 如果我对拥有 288 行的用户使用
LIMIT 288
,那么它会快得多(< 1 秒),但是如果我对拥有 7066 行的用户使用相同的方式,那么它又会变得非常慢.
在我想到使用虚拟机 ORDER BY
之前,我尝试了这些:
- 正在重新导入数据库。
analyze review;
- 将 done 的索引设置为 DESC(以前设置为 default/ASC。)[当时的挑战是有 no proper way to check if/when the index is done rebuilding。]
None 有帮助。
问题:
我的问题本身已经解决了,但是我还是不满意:
- 这个 "pattern" 是否有一个 名称,其中包括添加一个虚拟
ORDER BY
以加快速度? - 以后如何发现此类问题? (这花了很长时间才弄清楚。)除非我遗漏了什么,否则
EXPLAIN
没那么有用:- 对于慢速查询,成本低得令人误解,而对于快速变体,成本高得令人误解。
- 备选方案:是否有另一种方法来处理这个问题?(因为这个解决方案感觉像是 hack。)
谢谢!
类似问题:
- PostgreSQL query very slow with limit 1 几乎是同一个问题,除了他的查询在 LIMIT 1 时很慢,而在 LIMIT 3 或更高时很好。然后当然数据不一样了。
这里的根本问题是所谓的提前中止查询计划。这是来自 pgsql-hackers 的一个线程,描述了类似的东西:
https://www.postgresql.org/message-id/541A2335.3060100%40agliodbs.com
从那里引用,这就是为什么当存在 ORDER BY done DESC 时,规划器使用通常非常慢的索引扫描:
As usual, PostgreSQL is dramatically undercounting n_distinct: it shows chapters.user_id at 146,000 and the ratio of to_user_id:from_user_id as being 1:105 (as opposed to 1:6, which is about the real ratio). This means that PostgreSQL thinks it can find the 20 rows within the first 2% of the index ... whereas it actually needs to scan 50% of the index to find them.
在您的情况下,规划器认为如果它只是开始遍历按 done desc 排序的行(IOW,使用 review_done 索引),它将找到 4 行 clicker_id=28迅速地。由于需要按 "done" 降序返回行,因此它认为这将节省一个排序步骤并且比检索答题器 28 的所有行然后排序更快。考虑到行的真实世界分布,通常情况并非如此,需要它在找到 4 行之前跳过大量行,clicker=28。
一种更通用的处理方法是使用 CTE(在 9.6 中,它仍然是一个优化栅栏 - 这在 PG 12 中发生了变化,仅供参考)来获取没有顺序的行,然后在外部查询中添加 ORDER BY。鉴于为用户获取所有行,对它们进行排序,然后返回您需要的行数对于您的数据集来说是完全合理的(即使是 7k 行点击器也不应该成为问题),您可以防止计划者相信提前中止如果在 CTE 中没有 ORDER BY 或 LIMIT,计划将是最快的,为您提供类似以下内容的查询:
WITH clicker_rows as (SELECT * FROM review WHERE clicker_id=28)
select * From clicker_rows ORDER BY done DESC LIMIT 4;
这应该是可靠的快速,同时仍然遵守您想要的 ORDER BY 和 LIMIT。不过,我不确定这种模式是否有名称。