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

Why does this simple query not use the index in postgres?

在我的 postgreSQL 数据库中,我有一个名为 "product" 的 table。在此 table 中,我有一个名为 "date_touched" 的列,类型为 timestamp。我在此列上创建了一个简单的 btree 索引。这是我的 table 的架构(我省略了不相关的列和索引定义):

                                           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)

table 有 ~300,000 行,我想从按 "date_touched" 排序的 table 中获取第 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 个结构:

  1. 平衡树和
  2. double-linked 键列表。

第一个结构允许快速键查找,第二个结构负责排序。对于更大的 tables,链接列表无法放入单个页面,因此它是链接页面的列表,其中每个页面的条目保持排序,在索引创建期间指定。

但是,认为这些页面一起位于磁盘上的想法是错误的。事实上,它们更有可能分布在不同的位置。为了根据索引的顺序读取页面,系统必须执行随机磁盘读取。与顺序访问相比,随机磁盘 IO 是昂贵的。因此,好的优化器会更喜欢 SeqScan

我强烈推荐“SQL Performance Explained” book to better understand indexes. It is also 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 不同的偏移值。


编辑: 为了避免 OFFSET 必须基于真实数据进行分页,该数据存在于 table 中并且是一部分的指数。对于这种特殊情况,以下可能是可能的:

  • 显示前 N(比如 20)个元素
  • 包括页面上显示的最大 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,比进行单个批查询 returns 所有这些记录然后迭代要耗时得多从应用程序端。