[英]Why does this simple query not use the index in postgres?
在我的 postgreSQL 数据库中,我有一个名为"product"
的表。 在此表中,我有一个名为"date_touched"
的列,类型为timestamp
。 我在此列上创建了一个简单的 btree 索引。 这是我的表的架构(我省略了不相关的列和索引定义):
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)
该表有 ~300,000 行,我想从按"date_touched"
排序的表中获取第 n 个元素。 当我要获取第1000个元素时,需要0.2s,但是当我要获取第100,000个元素时,大约需要6s。 我的问题是,为什么我已经定义了 btree 索引,但检索第 100,000 个元素需要花费太多时间?
这是我的explain analyze
查询,显示 postgreSQL 不使用 btree 索引而是对所有行进行排序以查找第 100,000 个元素:
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
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 个结构:
第一个结构允许快速键查找,第二个结构负责排序。 对于更大的表,链接列表无法放入单个页面,因此它是链接页面的列表,其中每个页面的条目保持顺序,在索引创建期间指定。
但是,认为这些页面一起位于磁盘上的想法是错误的。 事实上,它们更有可能分布在不同的位置。 为了根据索引的顺序读取页面,系统必须执行随机磁盘读取。 与顺序访问相比,随机磁盘 IO 是昂贵的。 因此,好的优化器会更喜欢SeqScan
。
我强烈推荐“SQL Performance Explained”一书以更好地理解索引。 它也可以在线获得。
到底是怎么回事?
您的OFFSET
子句会导致数据库读取索引的键链接列表(导致大量随机磁盘读取),而不是丢弃所有这些结果,直到您达到想要的偏移量。 事实上,Postgres 决定在这里使用SeqScan
+ Sort
是件好事——这应该会更快。
您可以通过以下方式检查此假设:
OFFSET
查询的EXPLAIN (analyze, buffers)
SET enable_seqscan TO 'off';
EXPLAIN (analyze, buffers)
,比较结果。 一般来说,最好避免使用OFFSET
,因为 DBMS 在这里并不总是选择正确的方法。 (顺便说一句,您使用的是哪个版本的 PostgreSQL?)下面是它对不同偏移值的执行情况的比较。
编辑:为了避免OFFSET
,必须将分页基于表中存在的真实数据,并且是索引的一部分。 对于这种特殊情况,可能会出现以下情况:
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,这比执行返回所有这些记录的单个批查询要耗时得多,然后从应用程序端迭代这些记录。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.