【问题标题】:Postgres LIKE query triggers full table scanPostgres LIKE 查询触发全表扫描
【发布时间】:2021-02-18 19:00:09
【问题描述】:

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

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 不应该返回结果吗?

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

【问题讨论】:

  • 请问'&'有什么用?
  • & 符号只是存储在该字段中的文本的一部分,并且数据库中的某些条目包含& 符号或问号。我不认为任何人两者都有,但如果他们有,可能就不相关了。
  • 这不是真的。如果它是一个 btree 索引,你会是正确的,但它是一个 Trigram 索引。我一开始就这么说了。在 Trigram Indexes 上阅读此内容:scoutapm.com/blog/…
  • 这是 PostgreSQL 11.8。如果需要,我可能会升级,但存在后勤挑战,所以这不是一夜之间的事情。我正在考虑简单地从查询中删除任何非字母数字字符,用空格替换它们,删除双空格,然后将它们重新组合成一个字符串以运行查询。但我更愿意在做那种事情之前了解潜在的问题。
  • 不确定是不是你的情况,但这里有关于三元索引的很好解释:stackoverflow.com/a/57685243/593144

标签: postgresql indexing


【解决方案1】:

我无法在 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 根本无法从查询字符串中提取任何三元组,那么它必须返回整个表然后重新检查。这就是 '%%'、'%?%'、'%??%' 等会发生的情况。当然,重新检查所有行比首先进行 seq 扫描要慢。

【讨论】:

  • 如果查询使用索引,通常不到 2 秒,但是全表扫描需要 2 分钟多一点,并且它始终为数据库挂起磁盘 IO,导致其他查询陷入困境下。这是移动应用程序的后端,因此查询速度很重要,最好使用索引。我想知道解决方案是否是提前检查是否存在任何三元组,如果不存在,则不返回任何内容而不是进行表扫描。拦截那些破坏数据库的查询似乎需要付出很小的代价。想法?
  • “通常”是指%&%,还是在一堆其他不那么病态的东西上平均下来,从而降低了平均值?
  • 对不起,我的意思是“通常”,就像我最初的问题中的示例 1-7 和 10 中所示的索引可以使用时一样。我一直在追踪这样的性能问题,并注意到一些随机用户搜索了???,这迫使表扫描。我试图找出根本原因是什么,然后决定如何处理它。为此,似乎没有任何有效的三元组。但是 %?% 和 %???% 都没有有效的三元组对我来说没有意义,但是第一个立即返回而第二个没有。也许它使用了不同的索引。
  • 很难相信 1-3 的速度很快。 4-7和10,当然他们可以很快。您可以为其中一个显示完整的EXPLAIN (ANALYZE, BUFFERS) 吗?我同意%?%%???% 都没有有效的三元组,我无法复制它们导致不同的计划结构。你能同时显示EXPLAIN (ANALYZE, BUFFERS)吗?
  • 您是否有机会运行 pg_trgm 的自定义编译,其中有人在编译之前未定义 KEEPONLYALNUM?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-24
  • 2012-02-04
  • 2017-10-22
  • 2013-01-11
  • 2011-05-19
  • 2020-05-12
  • 1970-01-01
相关资源
最近更新 更多