如何通过另一个表中的字段获得更快的FTS4查询结果?

cha*_*cus 14 sqlite fts3 fts4

背景

我正在对存储在SQLite中的一组电子邮件进行全文搜索,利用其出色的内置FTS4引擎.虽然不完全符合我的预期,但我的查询性能相当差.让我们来看看.

代表架构

我将提供一些有关代码的简化示例,并在适用的地方提供完整代码的链接.

我们有一个MessageTable存储有关电子邮件消息的数据(完整版本分布在几个文件,这里,这里这里):

CREATE TABLE MessageTable (
    id INTEGER PRIMARY KEY,
    internaldate_time_t INTEGER
);
CREATE INDEX MessageTableInternalDateTimeTIndex
    ON MessageTable(internaldate_time_t);
Run Code Online (Sandbox Code Playgroud)

可搜索的文本被添加到名为MessageSearchTable(此处为完整版)的FTS4表中:

CREATE VIRTUAL TABLE MessageSearchTable USING fts4(
    id INTEGER PRIMARY KEY,
    body
);
Run Code Online (Sandbox Code Playgroud)

id搜索表作为一个外键消息表.

我将把它作为练习让读者在这些表格中插入数据(我当然不能透露我的私人电子邮件).我每张桌子的记录不到26k.

问题查询

当我们检索搜索结果时,我们需要按顺序对它们进行排序,internaldate_time_t这样我们才能获取最新的几个结果.这是一个示例搜索查询(此处为完整版):

SELECT id
FROM MessageSearchTable
JOIN MessageTable USING (id)
WHERE MessageSearchTable MATCH 'a'
ORDER BY internaldate_time_t DESC
LIMIT 10 OFFSET 0
Run Code Online (Sandbox Code Playgroud)

在我的机器上,通过我的电子邮件,运行大约150毫秒,通过以下方式测量:

time sqlite3 test.db <<<"..." > /dev/null
Run Code Online (Sandbox Code Playgroud)

150毫秒不是查询的野兽,但对于简单的FTS查找和索引顺序,它是缓慢的.如果我省略ORDER BY,它会在10毫秒内完成,例如.还要记住,实际的查询还有一个子选择,所以通常会进行更多的工作:查询的完整版本在大约600毫秒内运行,这是在野兽领域,ORDER BY在这种情况下省略削减500毫秒的时间.

如果我打开内部的统计信息sqlite3并运行查询,我会注意到该行:

Sort Operations:                     1
Run Code Online (Sandbox Code Playgroud)

如果我对这些统计数据文档的解释是正确的,看起来查询完全是使用了MessageTableInternalDateTimeTIndex.查询的完整版本也有一行:

Fullscan Steps:                      25824
Run Code Online (Sandbox Code Playgroud)

听起来像是在某个地方走过桌子,但是现在让我们忽略它.

我发现了什么

所以让我们继续优化一下.我可以将查询重新排列为子选择并强制SQLite使用带有INDEXED BY扩展名的索引:

SELECT id
FROM MessageTable
INDEXED BY MessageTableInternalDateTimeTIndex
WHERE id IN (
    SELECT id
    FROM MessageSearchTable
    WHERE MessageSearchTable MATCH 'a'
)
ORDER BY internaldate_time_t DESC
LIMIT 10 OFFSET 0
Run Code Online (Sandbox Code Playgroud)

瞧,运行时间已经下降到大约100毫秒(在查询的完整版本中为300毫秒,运行时间减少了50%),并且没有报告排序操作.请注意,只是重新组织这样的查询但不强制使用索引INDEXED BY,仍然有一个排序操作(尽管我们仍然已经奇怪地消磨了几毫秒),所以看起来SQLite确实忽略了我们的索引,除非我们强制它.

我还尝试了一些其他的事情,看看他们是否有所作为,但他们没有:

  • 明确使得指数DESC作为描述在这里,有和无INDEXED BY
  • id在索引中明确添加列,有和没有internaldate_time_t有序DESC,有和没有INDEXED BY
  • 可能还有其他一些我现在还记不起来的事情

问题

这里100毫秒似乎仍然非常缓慢,似乎它应该是一个简单的FTS查找和索引顺序.

  • 这里发生了什么?除非你强行动手,为什么它忽略了明显的指数呢?
  • 我是否在组合虚拟和常规表格中的数据方面遇到了一些限制?
  • 为什么它仍然如此相对缓慢,还有什么我可以做的,以获得由另一个表中的字段排序的FTS匹配?

谢谢!

CL.*_*CL. 6

索引对于根据索引列的值查找表行很有用.找到表行后,索引不再有用,因为在任何其他标准中查找索引中的表行效率不高.

这意味着不可能为查询中访问的每个表使用多个索引.

另请参阅文档:查询计划,查询优化程序.


您的第一个查询具有以下EXPLAIN QUERY PLAN输出:

0 0 0 SCAN TABLE MessageSearchTable VIRTUAL TABLE INDEX 4: (~0 rows)
0 1 1 SEARCH TABLE MessageTable USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
0 0 0 USE TEMP B-TREE FOR ORDER BY
Run Code Online (Sandbox Code Playgroud)

会发生什么事

  1. FTS索引用于查找所有匹配的MessageSearchTable行;
  2. 对于在1.中找到的每一行,MessageTable主键索引用于查找匹配的行;
  3. 在2.中找到的所有行都使用临时表进行排序;
  4. 返回前10行.

您的第二个查询具有以下EXPLAIN QUERY PLAN输出:

0 0 0 SCAN TABLE MessageTable USING COVERING INDEX MessageTableInternalDateTimeTIndex (~100000 rows)
0 0 0 EXECUTE LIST SUBQUERY 1
1 0 0 SCAN TABLE MessageSearchTable VIRTUAL TABLE INDEX 4: (~0 rows)
Run Code Online (Sandbox Code Playgroud)

会发生什么事

  1. FTS索引用于查找所有匹配的MessageSearchTable行;
  2. SQLite MessageTableInternalDateTimeTIndex以索引顺序遍历所有条目,并在id值为步骤1中找到的值之一时返回一行.SQLite在第十行之后停止.

在此查询中,可以使用索引进行(隐含)排序,但这只是因为没有其他索引用于查找此表中的行.以这种方式使用索引意味着SQLite必须遍历所有条目,而不是查找与其他条件匹配的少数行.

当您INDEXED BY从第二个查询中省略该子句时,您将获得以下EXPLAIN QUERY PLAN输出:

0 0 0 SEARCH TABLE MessageTable USING INTEGER PRIMARY KEY (rowid=?) (~25 rows)
0 0 0 EXECUTE LIST SUBQUERY 1
1 0 0 SCAN TABLE MessageSearchTable VIRTUAL TABLE INDEX 4: (~0 rows)
0 0 0 USE TEMP B-TREE FOR ORDER BY
Run Code Online (Sandbox Code Playgroud)

这与第一个查询基本相同,只是联接和子查询的处理方式略有不同.


使用您的表结构,实际上不可能变得更快.你正在做三个操作:

  1. 查找行MessageSearchTable;
  2. 查找相应的行MessageTable;
  3. MessageTable值排序行.

就索引而言,步骤2和3相互冲突.数据库必须选择是否使用第2步的索引(在这种情况下必须明确地进行排序)或第3步(在这种情况下必须遍历所有MessageTable条目).

您可以尝试通过使消息时间成为FTS表的一部分并仅搜索最近几天(并且如果没有获得足够的结果来增加或减少时间)来从FTS搜索返回更少的记录.