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

时间:2016-01-01 08:26:04

标签: sql postgresql b-tree

在我的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

1 个答案:

答案 0 :(得分:1)

在这里使用SeqScan是一件非常好的事情。你的OFFSET 100000对IndexScan来说不是一件好事。

一点理论

Btree索引包含2个结构:

  1. 平衡树和
  2. 双键链接列表。
  3. 第一个结构允许快速键查找,第二个结构负责排序。对于较大的表,链表不能放入单个页面,因此它是链接页面的列表,其中每个页面的条目保持在索引创建期间指定的排序。

    但是,想到这些页面一起坐在磁盘上是错误的。事实上,它们更有可能分布在不同的地方。并且为了根据索引的顺序读取页面,系统必须执行随机磁盘读取。与顺序访问相比,随机磁盘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,必须对实际数据进行分页,该数据存在于表中并且是索引的一部分。对于这种特殊情况,可能有以下几种情况:

    • 首先显示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,比执行返回所有这些记录的单个批处理查询要花费更多时间,然后从应用程序端进行迭代。