为什么这个简单的查询不使用postgres中的索引?

问题描述 投票:4回答:1

在我的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个元素:

  • 第一个查询(第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
sql postgresql query-performance sql-execution-plan b-tree
1个回答
3
投票

很好,在这里使用了SeqScan。您的OFFSET 100000对IndexScan不好。

一点理论

Btree索引内部包含2个结构:

  1. 平衡树和
  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,必须将分页基于表中存在的并且是索引一部分的真实数据。对于这种特殊情况,可能会发生以下情况:
  • 显示前N(例如20)个元素
  • 包括最大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,这将比单批查询返回所有这些记录然后从应用程序侧进行迭代要耗费更多的时间。

© www.soinside.com 2019 - 2024. All rights reserved.