在我的postgre数据库中,我有一个名为'product'的表。在此表中,我有一个名为date_touched
的列,其类型为timestamp。我在这个专栏上创建了一个简单的postgre 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.2秒时,但是当我想获得第100,000个元素时,需要大约6秒。我的问题是为什么在定义btree索引时检索第100,000个元素需要花费太多时间?
这是我对explain analyze
的查询,显示postgres不使用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
答案 0 :(得分:1)
在这里使用SeqScan是一件非常好的事情。你的OFFSET 100000
对IndexScan来说不是一件好事。
一点理论
Btree索引包含2个结构:
第一个结构允许快速键查找,第二个结构负责排序。对于较大的表,链表不能放入单个页面,因此它是链接页面的列表,其中每个页面的条目保持在索引创建期间指定的排序。
但是,想到这些页面一起坐在磁盘上是错误的。事实上,它们更有可能分布在不同的地方。并且为了根据索引的顺序读取页面,系统必须执行随机磁盘读取。与顺序访问相比,随机磁盘IO价格昂贵。因此,优秀的优化器会更喜欢SeqScan
。
我强烈建议“SQL Performance Explained” book更好地了解索引。它也是available on-line。
发生了什么事?
您的OFFSET
子句会导致数据库读取索引的链接列表(导致大量随机磁盘读取),而不是丢弃所有这些结果,直到您达到所需的偏移量。事实上,Postgres决定在这里使用SeqScan
+ Sort
是好的 - 这个应该更快。
您可以通过以下方式检查此假设:
EXPLAIN (analyze, buffers)
查询<{li>的OFFSET
SET enable_seqscan TO 'off';
EXPLAIN (analyze, buffers)
,比较结果。一般来说,最好避免使用OFFSET
,因为DBMS并不总是选择正确的方法。 (顺便说一下,你正在使用哪个版本的PostgreSQL?)
对于不同的偏移值,这里是a comparison of how it performs。
编辑:为了避免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,比执行返回所有这些记录的单个批处理查询要花费更多时间,然后从应用程序端进行迭代。