首页 > 解决方案 > Postgres LIKE 查询触发全表扫描

问题描述

我有一个 Postgres 查询,我们在其中设置了几个索引,其中一个在我们有 GIN 索引的文本字段上。我基于pg_trgm 文档对此的理解是,它仅适用于搜索字符串由字母数字文本组成的情况。测试证明了这一点,并在具有数千万条记录的数据库中,执行以下操作非常有效:

SELECT * FROM my_table WHERE target_field LIKE '%foo%'

我在不同的地方读到,在三元组搜索中,任何不是字母数字字符串的东西都被视为单独的单词,所以类似下面的内容也很有效:

SELECT * FROM my_table WHERE target_field LIKE '%foo & bar%'

然而,有人运行了一个搜索,实际上只是连续三个问号,它触发了全表扫描。出于某种原因,当查询中单独使用多个与号或问号时,它们的处理方式与放置在实际字母数字字符旁边或中间的单个符号不同。

我所做的研究暗示这可能是某些数据库驱动程序处理问号的方式,有时将其解释为需要提供的参数,但随后会因为找不到参数并触发表扫描而感到困惑. 我真的不相信这是事实。我可能倾向于相信它会抛出错误而不是完成查询,但无论如何运行它似乎是一个设计缺陷。

更有意义的是问号不是字母数字字符,因此它的处理方式不同。在某些技术中,常见的符号如 & 被认为是字母数字,但我认为 Postgres 不是这种情况。事实上,文档建议非字母数字字符在基于 GIN 的索引中被视为单词边界。

奇怪的是我可以搜索%foo & bar%,这似乎工作正常。我什至可以搜索%&%它并快速返回,尽管不是我想要的结果。但是,如果我将(例如)其中三个像这样: 放在一起%&&&%,它会触发全表扫描。

在运行各种实验之后,这是我所看到的:

  1. %%: 使用索引
  2. %&%: 使用索引
  3. %?%: 使用索引
  4. %foo & bar%: 使用索引
  5. %foo ? bar%: 使用索引
  6. %foo && bar%: 使用索引
  7. %foo ?? bar%: 使用索引
  8. %&&%: 触发全表扫描
  9. %??%: 触发全表扫描
  10. %foo&bar%: 使用索引,但不返回结果

我认为所有这些都是有意义的,直到你到达#8 和#9。如果 & 是一个单词边界,#10 不应该返回结果吗?

任何人都可以解释为什么多个连续的标点符号与单个标点符号的处理方式不同?

标签: postgresqlindexing

解决方案


我无法在 v11 中在充满 md5 哈希的表上重现这一点:我得到了前 3 个模式的 seq 扫描(全表扫描)。

如果我通过设置 enable_seqscan=false 来强制他们使用索引,那么我就会让它使用索引,但它实际上比进行 seq 扫描要慢。所以它在那里做出了正确的选择。你呢?原则上,您不应该强迫它在实际上较慢时使用索引。

看到它认为它将为所有这些示例返回的估计行数会很有趣。

事实上,文档建议非字母数字字符在基于 GIN 的索引中被视为单词边界。

GIN 中的 G 代表“广义”。你不能对笼统的东西做出这样的笼统陈述。他们甚至根本不需要对文本进行操作。但是在您的情况下,您使用的是 LIKE 运算符,而 LIKE 运算符不关心单词边界。任何声称支持 LIKE 运算符的 GIN 索引都必须为 LIKE 运算符返回正确的结果。如果它不能做到这一点,那么它声称支持它就是一个错误。

pg_trgm 确实对待 & 和 ? 提取三元组时与空白相同,但如果此决定有义务将 LIKE 与效果隔离开。它通过两种方法做到这一点。一是它返回“MAYBE”结果,这意味着必须重新检查它报告的所有元组,以查看它们是否真正满足 LIKE。所以 '%foo&bar%' 和 '%foo&bar%' 将返回相同的元组集合到堆扫描,但堆扫描会重新检查它们,因此最终返回不同的集合给用户,这取决于哪些在重新检查。第二件事是,如果 pg_trgm 根本无法从查询字符串中提取任何三元组,那么它必须返回整个表然后重新检查。这就是 '%%'、'%?%'、'%??%' 等会发生的情况。


推荐阅读