繁体   English   中英

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

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

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

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

一点理论

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

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

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

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

我强烈推荐“SQL Performance Explained”一书以更好地理解索引。 它也可以在线获得

到底是怎么回事?

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

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

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

一般来说,最好避免使用OFFSET ,因为 DBMS 在这里并不总是选择正确的方法。 (顺便说一句,您使用的是哪个版本的 PostgreSQL?)下面是它对不同偏移值的执行情况的比较


编辑:为了避免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,这比执行返回所有这些记录的单个批查询要耗时得多,然后从应用程序端迭代这些记录。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM