提高 sqlite3 中的`GROUP BY` 查询性能

Fak*_*ame 7 sqlite performance optimization query-performance

我有一个使用 sqlite3 作为数据库的小网络应用程序(数据库相当小)。

现在,我正在使用以下查询生成一些要显示的内容:

SELECT dbId,
        dlState,
        retreivalTime,
        seriesName,
        <snip irrelevant columns>
        FROM DataItems
        GROUP BY seriesName
        ORDER BY retreivalTime DESC
        LIMIT ?
        OFFSET ?;
Run Code Online (Sandbox Code Playgroud)

其中limit通常为 ~200,并且offset为 0(它们驱动分页机制)。

无论如何,现在,这个查询完全扼杀了我的表现。在具有约 67K 行的表上执行大约需要 800 毫秒。

我在seriesName和上都有索引retreivalTime

sqlite> SELECT name FROM sqlite_master WHERE type='index' ORDER BY name;
<snip irrelevant indexes>
DataItems_seriesName_index
DataItems_time_index           // This is the index on retreivalTime. Yeah, it's poorly named
Run Code Online (Sandbox Code Playgroud)

但是,EXPLAIN QUERY PLAN似乎表明它们没有被使用:

sqlite> EXPLAIN QUERY PLAN SELECT dbId, 
                                  dlState, 
                                  retreivalTime, 
                                  seriesName 
                                  FROM 
                                      DataItems 
                                  GROUP BY 
                                      seriesName 
                                  ORDER BY 
                                      retreivalTime 
                                  DESC LIMIT 200 OFFSET 0;
0|0|0|SCAN TABLE DataItems
0|0|0|USE TEMP B-TREE FOR GROUP BY
0|0|0|USE TEMP B-TREE FOR ORDER BY
Run Code Online (Sandbox Code Playgroud)

上的索引seriesNameCOLLATE NOCASE,如果这是相关的。

如果我删除GROUP BY,它会按预期运行:

sqlite> EXPLAIN QUERY PLAN SELECT dbId, dlState, retreivalTime, seriesName FROM DataItems ORDER BY retreivalTime DESC LIMIT 200 OFFSET 0;
0|0|0|SCAN TABLE DataItems USING INDEX DataItems_time_index
Run Code Online (Sandbox Code Playgroud)

基本上,我天真的假设是执行此查询的最佳方法是从 中的最新值向后走retreivalTime,每次seriesName看到的新值时,将其附加到临时列表,最后返回该值。对于OFFSET较大的情况,这会带来一些性能不佳的情况,但在此应用程序中这种情况很少发生。

如何优化此查询?如果需要,我可以提供原始查询操作。

插入性能在这里并不重要,所以如果我需要创建一个或两个额外的索引,那很好。


我目前的想法是更新一个单独的表的提交钩子,该表仅用于跟踪唯一项目,但这似乎有点过头了。

CL.*_*CL. 5

可以使用索引来优化 GROUP BY,但是如果 ORDER BY 使用不同的列,则排序不能使用索引(因为只有当数据库能够以排序顺序从表中读取行时,索引才有帮助)。

如果您在查询中使用不同的排序规则,则 COLLATE NOCASE 索引无济于事。添加“正常”索引,或者使用GROUP BY seriesName COLLATE NOCASE,如果允许的话。

使用 OFFSET 子句进行分页不是很有效,因为数据库在开始单步执行之前仍然必须对所有行进行分组和排序。最好使用滚动光标

注意:不能保证dbIddlState值来自任何特定行;SQLite 允许聚合查询中的非聚合列仅用于与 MySQL 的错误兼容性。

  • 我喜欢这句话*“错误兼容性”*。所以,SQLite 尝试兼容 MySQL 的 bug,不错! (3认同)

ype*_*eᵀᴹ 4

这里有一个建议:添加一个索引(seriesName, retreivalTime)并尝试这个查询。它不会超级快,但可能比你所拥有的更有效:

SELECT d.dbId,
       d.dlState,
       d.retreivalTime,
       d.seriesName,
        <snip irrelevant columns>
FROM DataItems AS d
  JOIN
    ( SELECT seriesName, 
             MAX(retreivalTime) AS max_retreivalTime
      FROM DataItems
      GROUP BY seriesName
      ORDER BY max_retreivalTime DESC
      LIMIT ?
      OFFSET ?
    ) AS di
    ON  di.seriesName = d.seriesName
    AND di.max_retreivalTime = d.retreivalTime
ORDER BY di.max_retreivalTime ;
Run Code Online (Sandbox Code Playgroud)

或者(变体)也使用 PK,并带有索引(seriesName, retreivalTime, dbId)和查询:

SELECT d.dbId,
       d.dlState,
       d.retreivalTime,
       d.seriesName,
        <snip irrelevant columns>
FROM DataItems AS d
  JOIN
    ( SELECT dbId
      FROM DataItems
      GROUP BY seriesName
      ORDER BY MAX(retreivalTime) DESC
      LIMIT ?
      OFFSET ?
    ) AS di
    ON  di.dbId = d.dbId
ORDER BY d.max_retreivalTime ;
Run Code Online (Sandbox Code Playgroud)

查询背后的逻辑是仅使用索引进行派生表计算(查找每个系列名称的最大值(检索时间),然后按顺序排序并执行偏移限制操作。)

然后表本身将仅用于获取要显示的 200 行。