16

背景

我正在利用其出色的内置FTS4引擎对存储在 SQLite 中的电子邮件正文实施全文搜索。我得到了一些相当糟糕的查询性能,尽管并不完全符合我的预期。让我们来看看。

代表模式

我将给出一些相关代码的简化示例,并在适用的情况下提供完整代码的链接。

我们有一个MessageTable存储有关电子邮件信息的数据(完整版本分布在此处此处此处的多个文件中):

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

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

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

搜索表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

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

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

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

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

Sort Operations:                     1

如果我对有关这些统计信息的文档的解释是正确的,那么查询似乎完全跳过了使用MessageTableInternalDateTimeTIndex. 完整版的查询也有这行:

Fullscan Steps:                      25824

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

我发现了什么

因此,让我们稍微优化一下。我可以将查询重新排列为子选择并强制 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

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

我还尝试了其他一些方法,看看它们是否会有所作为,但它们没有:

  • 显式地按照此处DESC描述的方式创建索引,无论有无INDEXED BY
  • 在索引中显式添加id列,有和没有internaldate_time_t排序DESC,有和没有INDEXED BY
  • 可能还有其他几件事我现在不记得了

问题

这里的 100 毫秒似乎仍然非常慢,因为它看起来应该是一个简单的 FTS 查找和索引顺序。

  • 这里发生了什么?除非你强迫它,否则它为什么会忽略明显的索引?
  • 我在合并虚拟表和常规表中的数据时遇到了一些限制吗?
  • 为什么它仍然相对较慢,我还能做些什么来让 FTS 匹配按另一个表中的字段排序?

谢谢!

4

1 回答 1

7

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

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

另请参阅文档:查询计划查询优化器


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

您的第二个查询具有以下 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 在第十个这样的行之后停止。

在这个查询中,可以使用索引进行(隐含的)排序,但这只是因为没有其他索引用于查找该表中的行。以这种方式使用索引意味着 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. MessageTable在;中查找相应的行
  3. 按值对行进行排序MessageTable

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

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

于 2013-08-24T12:25:24.300 回答