背景
我正在对存储在SQLite中的一组电子邮件实施全文搜索,利用其出色的内置FTS4引擎。虽然不完全符合我的预期,但我的查询性能相当差。我们来看看。
代表架构
我将提供一些有关代码的简化示例,并提供完整代码的链接。
我们有MessageTable
存储有关电子邮件的数据(完整版分布在多个文件here,here和here):< / 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查找和索引订单。
谢谢!
答案 0 :(得分:6)
索引对于根据索引列的值查找表行很有用。 找到表行后,索引不再有用,因为在任何其他标准中查找索引中的表行效率不高。
这意味着不能为查询中访问的每个表使用多个索引。
另请参阅文档:Query Planning,Query 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
会发生什么
MessageSearchTable
行; MessageTable
主键索引用于查找匹配的行; 您的第二个查询具有以下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)
会发生什么
MessageSearchTable
行; 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
与第一个查询基本相同,只是联接和子查询的处理方式略有不同。
使用您的表结构,实际上不可能变得更快。 您正在进行三项操作:
MessageSearchTable
; MessageTable
; MessageTable
值对行进行排序。就索引而言,步骤2和3相互冲突。
数据库必须选择是否使用第2步的索引(在这种情况下必须明确地进行排序)或第3步(在这种情况下必须通过所有MessageTable
条目)。
您可以尝试从FTS搜索中返回较少的记录,方法是将消息时间作为FTS表的一部分,并仅搜索最近几天(如果没有获得足够的结果,则会增加或减少时间)。