在我的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” book以更好地理解索引。也是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,表示不同的偏移值。
EDIT:
为了避免出现OFFSET
,必须将分页基于表中存在的并且是索引一部分的真实数据。对于这种特殊情况,可能会发生以下情况:date_touched
显示在页面上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,这将比单批查询返回所有这些记录然后从应用程序侧进行迭代要耗费更多的时间。