当 Postgres 坚持执行顺序扫描时,如何强制它使用索引?
9 回答
假设您询问的是在许多数据库中发现的常见“索引提示”功能,PostgreSQL 不提供这样的功能。这是 PostgreSQL 团队有意识的决定。可以在此处找到关于为什么以及您可以做什么的一个很好的概述。原因基本上是它是一种性能黑客,随着数据的变化,它往往会在以后导致更多问题,而 PostgreSQL 的优化器可以根据统计数据重新评估计划。换句话说,今天可能是一个好的查询计划可能不会一直是一个好的查询计划,并且索引提示会一直强制执行特定的查询计划。
作为一个非常钝的锤子,对测试很有用,可以使用enable_seqscan
和enable_indexscan
参数。看:
这些不适合正在进行的生产使用。如果您在选择查询计划时遇到问题,您应该查看用于跟踪查询性能问题的文档。不要只是设置enable_
参数然后走开。
除非您有充分的理由使用索引,否则 Postgres 可能会做出正确的选择。为什么?
- 对于小型表,执行顺序扫描会更快。
- 当数据类型不正确匹配时,Postgres 不使用索引,您可能需要包含适当的强制转换。
- 您的规划器设置可能会导致问题。
另请参阅此旧新闻组帖子。
可能是使用的唯一正当理由
set enable_seqscan=false
当您正在编写查询并希望快速查看查询计划实际上是什么时,如果表中有大量数据。或者当然,如果您需要快速确认您的查询没有使用索引,仅仅是因为数据集太小。
TL;博士
运行以下三个命令,查看问题是否解决:
ANALYZE;
SET random_page_cost = 1.0;
SET effective_cache_size = 'X GB'; # replace X with total RAM size minus 2 GB
请继续阅读以了解更多详细信息和背景信息。
第 1 步:分析表
作为解决问题的简单第一次尝试,以ANALYZE;
数据库超级用户身份运行命令以更新所有表统计信息。从文档中:
查询计划器使用这些统计信息来帮助确定最有效的查询执行计划。
第 2 步:设置正确的随机页面成本
索引扫描需要非顺序的磁盘页面获取。PostgreSQL 使用random_page_cost
配置参数来估计这种非顺序提取相对于顺序提取的成本。从文档中:
减少这个值 [...] 将导致系统更喜欢索引扫描;提高它会使索引扫描看起来相对更昂贵。
默认值为4.0
,因此假设与顺序提取相比,平均成本因子为 4,同时考虑到缓存效果。但是,如果您的数据库存储在SSD 驱动器上,那么您应该根据文档实际设置random_page_cost
为:1.1
与顺序驱动器(例如固态驱动器)相比,具有较低随机读取成本的存储也可以用较低的 值
random_page_cost
(例如 )更好地建模1.1
。
此外,如果索引大部分(甚至完全)缓存在 RAM 中,那么索引扫描将始终比磁盘服务的顺序扫描快得多。然而,查询规划器不知道索引的哪些部分已经被缓存,因此可能会做出错误的决定。
如果您的数据库索引被频繁使用,并且系统有足够的 RAM,那么索引最终可能会被缓存。在这种情况下,random_page_cost
可以将其设置为1.0
,甚至设置为以下值,1.0
以积极地倾向于使用索引扫描(尽管文档建议不要这样做)。您将不得不尝试不同的值,看看哪些对您有用。
作为旁注,您还可以考虑使用pg_prewarm扩展将索引显式缓存到 RAM 中。
你可以random_page_cost
这样设置:
SET random_page_cost = 1.0;
步骤 3:设置正确的缓存大小
在具有 8 GB 或更多 GB RAM 的系统上,您应该将effective_cache_size
配置参数设置为 PostgreSQL 通常可用于数据缓存的内存量。从文档中:
较高的值使其更有可能使用索引扫描,较低的值使其更有可能使用顺序扫描。
请注意,此参数不会更改 PostgreSQL 实际分配的内存量,而仅用于计算成本估算。一个合理的值(至少在专用数据库服务器上)是总 RAM 大小减去 2 GB。默认值为4 GB
。
你可以effective_cache_size
这样设置:
SET effective_cache_size = '14 GB'; # e.g. on a dedicated server with 16 GB RAM
第 4 步:永久修复问题
您可能希望永久使用ALTER SYSTEM SET ...
或ALTER DATABASE db_name SET ...
设置新的配置参数值(全局或每个数据库)。有关设置参数的详细信息,请参阅文档。
第 5 步:其他资源
如果它仍然不起作用,那么您可能还想看看这个关于服务器调优的 PostgreSQL Wiki 页面。
有时 PostgreSQL 无法为特定条件做出最佳索引选择。举个例子,假设有一个交易表有几百万行,其中任何一天都有几百行,并且该表有四个索引:transaction_id、client_id、date 和 description。您要运行以下查询:
SELECT client_id, SUM(amount)
FROM transactions
WHERE date >= 'yesterday'::timestamp AND date < 'today'::timestamp AND
description = 'Refund'
GROUP BY client_id
PostgreSQL 可能会选择使用索引 transactions_description_idx 而不是 transactions_date_idx,这可能导致查询需要几分钟而不是不到一秒。如果是这种情况,您可以通过捏造这样的条件来强制使用日期索引:
SELECT client_id, SUM(amount)
FROM transactions
WHERE date >= 'yesterday'::timestamp AND date < 'today'::timestamp AND
description||'' = 'Refund'
GROUP BY client_id
这个问题本身是非常无效的。强制(例如通过 enable_seqscan=off)是非常糟糕的主意。检查它是否会更快可能很有用,但生产代码不应该使用这样的技巧。
相反 - 对您的查询进行解释分析,阅读它,并找出 PostgreSQL 选择错误(在您看来)计划的原因。
网上有一些工具可以帮助阅读解释分析输出 - 其中之一是explain.depesz.com - 由我编写。
另一种选择是加入freenode irc 网络上的#postgresql 频道,并与那里的人交谈以帮助您 - 因为优化查询不是“问一个问题,得到一个快乐的答案”的问题。它更像是一次对话,有很多东西要检查,很多东西要学习。
PostgreSQL 需要注意的一件事;您期望使用索引但未使用索引的地方是 VACUUM ANALYZE 表。
VACUUM ANALYZE schema.table;
这会更新计划程序使用的统计信息,以确定执行查询的最有效方式。这可能会导致索引被使用。
有一个技巧可以推动 postgres 更喜欢 seqscanOFFSET 0
在子查询中添加 a
当您只需要 n 个第一个/最后一个元素时,这对于优化链接大/巨大表的请求非常方便。
假设您正在查找涉及具有 100k(或更多)条目的多个表的第一个/最后 20 个元素,当您要查找的内容在前 100 个或 1000 个中时,没有必要构建/链接所有数据的所有查询条目。例如,在这种情况下,执行顺序扫描的速度要快 10 倍以上。
索引只能在某些情况下使用。
- 例如,值的类型适合列的类型。
- 在与值进行比较之前,您没有对列进行操作。
给定一个包含 3 列的客户表,所有列都有 3 个索引。
create table customer(id numeric(10), age int, phone varchar(200))
例如,数据库可能会尝试使用索引 idx_age 而不是使用电话号码。
您可以通过执行年龄操作来破坏索引年龄的使用:
select * from customer where phone = '1235' and age+1 = 24
(虽然您正在寻找 23 岁)
这当然是一个非常简单的例子,postgres 的智能可能足以做出正确的选择。但有时除了欺骗系统之外别无他法。
另一个例子是
select * from customer where phone = '1235' and age::varchar = '23'
但这可能比上述选项更昂贵。
不幸的是,您不能像在 MSSQL 或 Sybase 中那样将索引的名称设置到查询中。
select * from customer (index idx_phone) where phone = '1235' and age = 23.
这将有助于避免此类问题。
显然,在某些情况下,可以通过重复两次类似的条件来暗示 Postgre 使用索引。
我观察到的具体情况是使用 PostGISgin
索引和ST_Within谓词,如下所示:
select *
from address
natural join city
natural join restaurant
where st_within(address.location, restaurant.delivery_area)
and restaurant.delivery_area ~ address.location
请注意,第一个谓词st_within(address.location, restaurant.delivery_area)
由 PostGIS 自动分解为,(restaurant.delivery_area ~ address.location) AND _st_contains(restaurant.delivery_area, address.location)
因此添加第二个谓词restaurant.delivery_area ~ address.location
是完全多余的。尽管如此,第二个谓词说服了计划者address.location
在我需要的特定情况下使用空间索引,将运行时间提高了 8 倍。