【问题标题】:Why does this simple query not use the index in postgres?为什么这个简单的查询不使用 postgres 中的索引?
【发布时间】:2016-04-05 21:51:32
【问题描述】:

在我的 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。我的问题是,为什么检索第 100,000 个元素需要太多时间,尽管我已经定义了一个 btree 索引?

这是我对 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】:

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

一点理论

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

  1. 平衡树和
  2. 键的双链表。

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

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

我强烈推荐“SQL Performance Explained” book 以更好地理解索引。也是available on-line

发生了什么事?

您的OFFSET 子句将导致数据库读取索引的键的链接列表(导致大量随机磁盘读取),而不是丢弃所有这些结果,直到您达到所需的偏移量。事实上,Postgres 决定在这里使用 SeqScan + Sort 这很好——这应该更快。

您可以通过以下方式检查此假设:

  • 运行 EXPLAIN (analyze, buffers) 的 big-OFFSET 查询
  • SET enable_seqscan TO 'off';
  • 并再次运行EXPLAIN (analyze, buffers),比较结果。

一般来说,最好避免使用OFFSET,因为 DBMS 并不总是在这里选择正确的方法。 (顺便说一句,您使用的是哪个版本的 PostgreSQL?) 这是 a comparison of how it performs 用于不同的偏移值。


编辑:为了避免OFFSET,必须根据实际数据进行分页,这些数据存在于表中并且是索引的一部分。对于这种特殊情况,以下可能是可能的:

  • 显示前 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,这将比执行返回所有这些记录的单个批处理查询更耗时,然后从应用程序端对其进行迭代。

【讨论】:

  • 那么我怎样才能避免你所说的偏移?有没有其他查询?还有为什么它不使用“平衡树”来查找第 n 个元素?我知道这种数据结构既可以用于键查找,也可以用于查找第 n 个元素(如果它在树的每个节点中保存了子元素的数量)。 postgresql 中是否有这种索引可以在对数时间内执行查找第 n 个元素而不是线性扫描? (感谢您的完整回答!)
  • 感谢您的示例。你知道是否有任何用于在对数时间内使用平衡树回答第 n 个元素查询的 postgresql 索引? (与键查找查询相同)
猜你喜欢
  • 2016-01-06
  • 2017-09-07
  • 1970-01-01
  • 2017-06-12
  • 2021-12-17
  • 1970-01-01
  • 1970-01-01
  • 2011-08-24
  • 1970-01-01
相关资源
最近更新 更多