为什么这个简单的查询不使用 postgres 中的索引?
Why does this simple query not use the index in postgres?
在我的 postgreSQL 数据库中,我有一个名为 "product"
的 table。在此 table 中,我有一个名为 "date_touched"
的列,类型为 timestamp
。我在此列上创建了一个简单的 btree 索引。这是我的 table 的架构(我省略了不相关的列和索引定义):
Table "public.product"
Column | Type | Modifiers
---------------------------+--------------------------+-------------------
id | integer | not null default nextval('product_id_seq'::regclass)
date_touched | timestamp with time zone | not null
Indexes:
"product_pkey" PRIMARY KEY, btree (id)
"product_date_touched_59b16cfb121e9f06_uniq" btree (date_touched)
table 有 ~300,000 行,我想从按 "date_touched"
排序的 table 中获取第 n 个元素。当我要获取第1000个元素时,需要0.2s,但是当我要获取第100,000个元素时,大约需要6s。我的问题是,为什么我已经定义了 btree 索引,但检索第 100,000 个元素需要花费太多时间?
这是我使用 explain analyze
的查询,显示 postgreSQL 不使用 btree 索引而是对所有行进行排序以查找第 100,000 个元素:
- 第一个查询(第 100 个元素):
explain analyze
SELECT product.id
FROM product
ORDER BY product.date_touched ASC
LIMIT 1
OFFSET 1000;
QUERY PLAN
-----------------------------------------------------------------------------------------------------
Limit (cost=3035.26..3038.29 rows=1 width=12) (actual time=160.208..160.209 rows=1 loops=1)
-> Index Scan using product_date_touched_59b16cfb121e9f06_uniq on product (cost=0.42..1000880.59 rows=329797 width=12) (actual time=16.651..159.766 rows=1001 loops=1)
Total runtime: 160.395 ms
- 第二个查询(第 100,000 个元素):
explain analyze
SELECT product.id
FROM product
ORDER BY product.date_touched ASC
LIMIT 1
OFFSET 100000;
QUERY PLAN
------------------------------------------------------------------------------------------------------
Limit (cost=106392.87..106392.88 rows=1 width=12) (actual time=6621.947..6621.950 rows=1 loops=1)
-> Sort (cost=106142.87..106967.37 rows=329797 width=12) (actual time=6381.174..6568.802 rows=100001 loops=1)
Sort Key: date_touched
Sort Method: external merge Disk: 8376kB
-> Seq Scan on product (cost=0.00..64637.97 rows=329797 width=12) (actual time=1.357..4184.115 rows=329613 loops=1)
Total runtime: 6629.903 ms
这里用到了SeqScan,真是太好了。您的 OFFSET 100000
对 IndexScan 来说不是一件好事。
一点理论
Btree 索引内部包含 2 个结构:
- 平衡树和
- double-linked 键列表。
第一个结构允许快速键查找,第二个结构负责排序。对于更大的 tables,链接列表无法放入单个页面,因此它是链接页面的列表,其中每个页面的条目保持排序,在索引创建期间指定。
但是,认为这些页面一起位于磁盘上的想法是错误的。事实上,它们更有可能分布在不同的位置。为了根据索引的顺序读取页面,系统必须执行随机磁盘读取。与顺序访问相比,随机磁盘 IO 是昂贵的。因此,好的优化器会更喜欢 SeqScan
。
我强烈推荐“SQL Performance Explained” book to better understand indexes. It is also available on-line。
这是怎么回事?
您的 OFFSET
子句将导致数据库读取索引的键链接列表(导致大量随机磁盘读取),而不是丢弃所有这些结果,直到您达到想要的偏移量。事实上,Postgres 决定在这里使用 SeqScan
+ Sort
是件好事——这个 应该 更快。
您可以通过以下方式检查此假设:
- 运行宁
EXPLAIN (analyze, buffers)
你的大OFFSET
查询
- 比
SET enable_seqscan TO 'off';
再次- 和运行
EXPLAIN (analyze, buffers)
,比较结果。
一般来说,最好避免使用 OFFSET
,因为 DBMS 在这里并不总是选择正确的方法。 (顺便说一句,您使用的是哪个版本的 PostgreSQL?)
这是 a comparison of how it performs 不同的偏移值。
编辑: 为了避免 OFFSET
必须基于真实数据进行分页,该数据存在于 table 中并且是一部分的指数。对于这种特殊情况,以下可能是可能的:
- 显示前 N(比如 20)个元素
- 包括页面上显示的最大
date_touched
所有“下一步”链接。您可以在应用程序端计算此值。对“Previous”链接执行类似的操作,除了对这些链接包含最小 date_touch
。
- 在服务器端你会得到限制值。因此,对于“下一个”案例,您可以这样查询:
SELECT id
FROM product
WHERE date_touched > $max_date_seen_on_the_page
ORDER BY date_touched ASC
LIMIT 20;
此查询充分利用了索引。
当然,您可以根据需要调整此示例。我使用了分页,因为它是 OFFSET
.
的典型案例
还有一点要注意 — 多次查询 1 行,每次查询的偏移量增加 1,比进行单个批查询 returns 所有这些记录然后迭代要耗时得多从应用程序端。
在我的 postgreSQL 数据库中,我有一个名为 "product"
的 table。在此 table 中,我有一个名为 "date_touched"
的列,类型为 timestamp
。我在此列上创建了一个简单的 btree 索引。这是我的 table 的架构(我省略了不相关的列和索引定义):
Table "public.product"
Column | Type | Modifiers
---------------------------+--------------------------+-------------------
id | integer | not null default nextval('product_id_seq'::regclass)
date_touched | timestamp with time zone | not null
Indexes:
"product_pkey" PRIMARY KEY, btree (id)
"product_date_touched_59b16cfb121e9f06_uniq" btree (date_touched)
table 有 ~300,000 行,我想从按 "date_touched"
排序的 table 中获取第 n 个元素。当我要获取第1000个元素时,需要0.2s,但是当我要获取第100,000个元素时,大约需要6s。我的问题是,为什么我已经定义了 btree 索引,但检索第 100,000 个元素需要花费太多时间?
这是我使用 explain analyze
的查询,显示 postgreSQL 不使用 btree 索引而是对所有行进行排序以查找第 100,000 个元素:
- 第一个查询(第 100 个元素):
explain analyze
SELECT product.id
FROM product
ORDER BY product.date_touched ASC
LIMIT 1
OFFSET 1000;
QUERY PLAN
-----------------------------------------------------------------------------------------------------
Limit (cost=3035.26..3038.29 rows=1 width=12) (actual time=160.208..160.209 rows=1 loops=1)
-> Index Scan using product_date_touched_59b16cfb121e9f06_uniq on product (cost=0.42..1000880.59 rows=329797 width=12) (actual time=16.651..159.766 rows=1001 loops=1)
Total runtime: 160.395 ms
- 第二个查询(第 100,000 个元素):
explain analyze
SELECT product.id
FROM product
ORDER BY product.date_touched ASC
LIMIT 1
OFFSET 100000;
QUERY PLAN
------------------------------------------------------------------------------------------------------
Limit (cost=106392.87..106392.88 rows=1 width=12) (actual time=6621.947..6621.950 rows=1 loops=1)
-> Sort (cost=106142.87..106967.37 rows=329797 width=12) (actual time=6381.174..6568.802 rows=100001 loops=1)
Sort Key: date_touched
Sort Method: external merge Disk: 8376kB
-> Seq Scan on product (cost=0.00..64637.97 rows=329797 width=12) (actual time=1.357..4184.115 rows=329613 loops=1)
Total runtime: 6629.903 ms
这里用到了SeqScan,真是太好了。您的 OFFSET 100000
对 IndexScan 来说不是一件好事。
一点理论
Btree 索引内部包含 2 个结构:
- 平衡树和
- double-linked 键列表。
第一个结构允许快速键查找,第二个结构负责排序。对于更大的 tables,链接列表无法放入单个页面,因此它是链接页面的列表,其中每个页面的条目保持排序,在索引创建期间指定。
但是,认为这些页面一起位于磁盘上的想法是错误的。事实上,它们更有可能分布在不同的位置。为了根据索引的顺序读取页面,系统必须执行随机磁盘读取。与顺序访问相比,随机磁盘 IO 是昂贵的。因此,好的优化器会更喜欢 SeqScan
。
我强烈推荐“SQL Performance Explained” book to better understand indexes. It is also available on-line。
这是怎么回事?
您的 OFFSET
子句将导致数据库读取索引的键链接列表(导致大量随机磁盘读取),而不是丢弃所有这些结果,直到您达到想要的偏移量。事实上,Postgres 决定在这里使用 SeqScan
+ Sort
是件好事——这个 应该 更快。
您可以通过以下方式检查此假设:
- 运行宁
EXPLAIN (analyze, buffers)
你的大OFFSET
查询 - 比
SET enable_seqscan TO 'off';
再次 - 和运行
EXPLAIN (analyze, buffers)
,比较结果。
一般来说,最好避免使用 OFFSET
,因为 DBMS 在这里并不总是选择正确的方法。 (顺便说一句,您使用的是哪个版本的 PostgreSQL?)
这是 a comparison of how it performs 不同的偏移值。
编辑: 为了避免 OFFSET
必须基于真实数据进行分页,该数据存在于 table 中并且是一部分的指数。对于这种特殊情况,以下可能是可能的:
- 显示前 N(比如 20)个元素
- 包括页面上显示的最大
date_touched
所有“下一步”链接。您可以在应用程序端计算此值。对“Previous”链接执行类似的操作,除了对这些链接包含最小date_touch
。 - 在服务器端你会得到限制值。因此,对于“下一个”案例,您可以这样查询:
SELECT id
FROM product
WHERE date_touched > $max_date_seen_on_the_page
ORDER BY date_touched ASC
LIMIT 20;
此查询充分利用了索引。
当然,您可以根据需要调整此示例。我使用了分页,因为它是 OFFSET
.
还有一点要注意 — 多次查询 1 行,每次查询的偏移量增加 1,比进行单个批查询 returns 所有这些记录然后迭代要耗时得多从应用程序端。