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

标签 sql postgresql query-performance sql-execution-plan b-tree

在我的 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” book更好地理解索引。也是available on-line .

这是怎么回事?

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

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

  • 运行 EXPLAIN(分析、缓冲区)OFFSET 查询
  • SET enable_seqscan TO 'off';
  • 并再次运行 EXPLAIN(分析、缓冲),比较结果。

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

关于sql - 为什么这个简单的查询不使用 postgres 中的索引?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34554644/

相关文章:

sql - 使用 UNION 向 SELECT 语句添加参数会更改记录号

sql - 在sql中计算增量(当前行和上一行的差异)

java - 如何使用复合类型数组调用 postgres 函数

mysql - Postgres JDBC 的表名别名

mysql - 优化查询。想在子查询中不使用 max 的情况下选择最后一条记录

MySQL查询需要一段时间,如果可能需要优化

java - 如何在执行.sql文件时出现oracle错误

sql - PostgreSQL 索引性能增益/损失在短时间后生效

sql-server - 为什么不使用列存储索引

mysql - 我应该牺牲 innodb_buffer_pool_size/RAM 来为 query_cache_size 腾出空间吗?