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

时间:2013-08-24 00:21:48

标签: sqlite fts3 fts4

背景

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

代表架构

我将提供一些有关代码的简化示例,并提供完整代码的链接。

我们有MessageTable存储有关电子邮件的数据(完整版分布在多个文件hereherehere):< / p>

CREATE TABLE MessageTable (
    id INTEGER PRIMARY KEY,
    internaldate_time_t INTEGER
);
CREATE INDEX MessageTableInternalDateTimeTIndex
    ON MessageTable(internaldate_time_t);

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

CREATE VIRTUAL TABLE MessageSearchTable USING fts4(
    id INTEGER PRIMARY KEY,
    body
);

搜索表中的id充当消息表的外键。

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

问题查询

当我们检索搜索结果时,我们需要按internaldate_time_t降序排序,以便我们只能获取最新的几个结果。这是一个示例搜索查询(完整版here):

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

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

time sqlite3 test.db <<<"..." > /dev/null

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

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

sqlite3

如果我对docs about those stats的解释是正确的,看起来查询完全是使用Sort Operations: 1 跳过的。查询的完整版本也包含以下行:

MessageTableInternalDateTimeTIndex

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

我发现了什么

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

Fullscan Steps:                      25824

请注意,运行时间已降至约100毫秒(查询完整版300毫秒,运行时间减少50%),并且没有报告排序操作。请注意,只是重新组织这样的查询但不强制使用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 的索引,仍然有一个排序操作(尽管我们仍然已经奇怪地消磨了几毫秒),所以似乎SQLite确实忽略了我们的指数,除非我们强迫它。

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

  • 明确制定索引INDEXED BY,如here所述,有无DESC
  • 在带有和不带INDEXED BY订购id的情况下明确添加internaldate_time_t列,包括和不包含DESC
  • 可能还有其他一些我现在记不住的事了

问题

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

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

谢谢!

1 个答案:

答案 0 :(得分:6)

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

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

另请参阅文档:Query PlanningQuery Optimizer


您的第一个查询有以下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

会发生什么

  1. FTS索引用于查找所有匹配的MessageSearchTable行;
  2. 对于1.中找到的每一行,MessageTable主键索引用于查找匹配的行;
  3. 在2.中找到的所有行都使用临时表进行排序;
  4. 返回前10行。
  5. 您的第二个查询具有以下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)
    

    会发生什么

    1. FTS索引用于查找所有匹配的MessageSearchTable行;
    2. SQLite按索引顺序浏览MessageTableInternalDateTimeTIndex中的所有条目,并在id值为步骤1中找到的值之一时返回一行。 SQLite在第十行之后停止。
    3. 在此查询中,可以使用索引进行(隐含)排序,但这只是因为没有其他索引用于查找此表中的行。 以这种方式使用索引意味着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
      

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


      使用您的表结构,实际上不可能变得更快。 您正在进行三项操作:

      1. MessageSearchTable;
      2. 中查找行
      3. MessageTable;
      4. 中查找相应的行
      5. MessageTable值对行进行排序。
      6. 就索引而言,步骤2和3相互冲突。 数据库必须选择是否使用第2步的索引(在这种情况下必须明确地进行排序)或第3步(在这种情况下必须通过所有MessageTable条目)。

        您可以尝试从FTS搜索中返回较少的记录,方法是将消息时间作为FTS表的一部分,并仅搜索最近几天(如果没有获得足够的结果,则会增加或减少时间)。