小巧、快速、可靠。
三者选其二。
下一代查询计划器

1. 简介

"查询计划器"的任务是找出完成 SQL 语句的最佳算法或“查询计划”。从 SQLite 版本 3.8.0(2013-08-26)开始,查询计划器组件被重写,使其运行速度更快,并生成更好的计划。重写被称为“下一代查询计划器”或“NGQP”。

本文概述了查询规划的重要性,描述了查询规划中的一些固有问题,并概述了 NGQP 如何解决这些问题。

NGQP 几乎总是比传统查询计划器更好。但是,可能存在一些传统应用程序,它们无意中依赖于传统查询计划器中未定义和/或次优的行为,并且将这些传统应用程序升级到 NGQP 可能会导致性能下降。本文考虑了这种风险,并提供了一份清单,以降低风险并解决任何可能出现的问题。

本文重点介绍 NGQP。有关涵盖 SQLite 整个历史的更一般的 SQLite 查询计划器概述,请参阅“SQLite 查询优化器概述”和“索引的工作原理”文档。

2. 背景

对于针对具有少量索引的单个表的简单查询,通常有一个明显的最佳算法选择。但是,对于更大更复杂的查询,例如具有许多索引和子查询的多路连接,可能有数百、数千甚至数百万种合理的算法来计算结果。查询计划器的工作是从这些可能性中选择一个“最佳”的查询计划。

查询计划器使 SQL 数据库引擎变得如此有用和强大。(这对所有 SQL 数据库引擎都是如此,不仅仅是 SQLite。)查询计划器使程序员免于选择特定查询计划的苦差事,从而使程序员能够将更多精力集中在更高层次的应用程序问题上,并为最终用户提供更多价值。对于查询计划选择显而易见的简单查询,这很方便,但并不十分重要。但是,随着应用程序、模式和查询变得越来越复杂,一个聪明的查询计划器可以极大地加快和简化应用程序开发工作。能够告诉数据库引擎需要什么内容,然后让数据库引擎找出检索这些内容的最佳方式,这具有惊人的力量。

编写一个好的查询计划器更多的是艺术而不是科学。查询计划器必须使用不完整的信息工作。它无法确定任何特定计划需要多长时间,除非实际运行该计划。因此,在比较两个或多个计划以确定哪个“最佳”时,查询计划器必须进行一些猜测和假设,而这些猜测和假设有时会出错。一个好的查询计划器能够找到正确的解决方案,以至于应用程序程序员很少需要参与其中。

2.1. SQLite 中的查询规划

SQLite 使用嵌套循环来计算连接,每个表在连接中都有一个循环。(WHERE 子句中的 IN 和 OR 运算符可能会插入额外的循环。SQLite 也考虑了这些,但为了简单起见,我们将在本文中忽略它们。)可以在每个循环中使用一个或多个索引来加快搜索速度,或者循环可以是“全表扫描”,它读取表中的每一行。因此,查询规划分解为两个子任务:

  1. 选择各种循环的嵌套顺序
  2. 为每个循环选择好的索引

选择嵌套顺序通常是更具挑战性的问题。一旦建立了连接的嵌套顺序,每个循环的索引选择通常就很明显了。

2.2. SQLite 查询计划器稳定性保证

启用查询计划器稳定性保证 (QPSG) 时,SQLite 始终会为任何给定的 SQL 语句选择相同的查询计划,只要:

  1. 数据库模式没有以添加或删除索引等重大方式更改;
  2. ANALYZE 命令未重新运行;
  3. 使用相同版本的 SQLite。

QPSG 默认情况下是禁用的。它可以通过使用 SQLITE_ENABLE_QPSG 编译时选项在编译时启用,或通过调用 sqlite3_db_config(db,SQLITE_DBCONFIG_ENABLE_QPSG,1,0) 在运行时启用。

QPSG 意味着,如果所有查询在测试期间都高效运行,并且应用程序没有更改模式,那么 SQLite 不会突然决定开始使用不同的查询计划,这可能会在应用程序发布给用户后导致性能问题。如果您的应用程序在实验室中运行良好,那么它在部署后将继续以相同的方式运行。

客户端/服务器 SQL 数据库引擎通常不会做出此保证。在客户端/服务器 SQL 数据库引擎中,服务器会跟踪有关表大小和索引质量的统计信息,并且查询计划器使用这些统计信息来帮助选择最佳计划。随着数据库中添加、删除或更改内容,统计信息会不断变化,并可能导致查询计划器开始为某个特定查询使用不同的查询计划。通常,新计划将更适合数据不断变化的结构。但有时,新查询计划会导致性能下降。对于客户端/服务器数据库引擎,通常会有一个数据库管理员 (DBA) 在手,以处理这些罕见的问题。但是,DBA 无法修复嵌入式数据库(如 SQLite)中的问题,因此 SQLite 非常小心地确保计划在部署后不会意外更改。

需要注意的是,更改 SQLite 版本可能会导致查询计划发生变化。相同版本的 SQLite 始终会选择相同的查询计划,但是如果您重新链接应用程序以使用不同版本的 SQLite,那么查询计划可能会发生变化。在极少数情况下,SQLite 版本更改可能会导致性能下降。这是您应该考虑将应用程序静态链接到 SQLite,而不是使用系统范围的 SQLite 共享库(该库可能会在您不知情或不受控制的情况下更改)的原因之一。

另请参阅

3. 一个困难的案例

"TPC-H Q8" 是 交易处理性能委员会 中的一个测试查询。SQLite 3.7.17 及更早版本中的查询计划器没有为 TPC-H Q8 选择好的计划。并且已经确定,无论对传统查询计划器进行多少调整都无法解决这个问题。为了找到 TPC-H Q8 查询的良好解决方案,并继续改进 SQLite 查询计划器的质量,有必要重新设计查询计划器。本节试图解释为什么需要重新设计,以及 NGQP 如何不同并解决 TPC-H Q8 问题。

3.1. 查询细节

TPC-H Q8 是一个八路连接。如上所述,查询计划器的主要任务是找出八个循环的最佳嵌套顺序,以最大限度地减少完成连接所需的工作量。下图显示了 TPC-H Q8 情况下的此问题的简化模型。

S L O C N1 R P N2 6.00 2.08 9.17 2.30 2.77 4.03 2.64 5.30 2.08 6.40 1.79 3.47 2.64 6.01 *  5.52 *  9.47 *  16.40 *  13.87 *  12.56 *  5.52 *  3.56 *  7.71

在图中,查询 FROM 子句中的 8 个表中的每一个都由一个带有 FROM 子句项标签的大圆圈标识:N2、S、L、P、O、C、N1 和 R。图中的弧线代表计算每个项的估计成本,假设弧线的起点在外部循环中。例如,运行 S 循环作为 L 的内部循环的成本为 2.30,而运行 S 循环作为 L 的外部循环的成本为 9.17。

这里的“成本”是对数的。对于嵌套循环,工作量是相乘的,而不是相加的。但通常的做法是将带有加权的图视为加权,因此该图显示了各种成本的对数。该图显示了 S 在 L 内部运行的成本优势约为 6.87,但这转化为当 S 循环在 L 循环内部运行而不是在 L 循环外部运行时,查询运行速度快约 963 倍。

从带有“*”的小圆圈发出的箭头表示在没有依赖关系的情况下运行每个循环的成本。最外层循环必须使用此 * 成本。内部循环可以选择使用 * 成本或假设其他项之一在外部循环中的成本,以获得最佳结果。可以将 * 成本视为一种简写符号,表示多个弧线,每个弧线来自图中的其他节点。因此,该图是“完整的”,这意味着在图中每对节点之间都有弧线(一些显式,一些隐式)。

找到最佳查询计划的问题等同于在图中找到一条最小成本路径,该路径正好访问每个节点一次。

(旁注:上面 TPC-H Q8 图中的成本估计是通过 SQLite 3.7.16 中的查询计划器计算的,并使用自然对数进行转换。)

3.2. 复杂性

上面对查询计划器问题的描述是一个简化。成本是估计的。我们无法知道运行循环的真实成本,除非我们实际运行循环。SQLite 根据 WHERE 子句中找到的索引和约束的可用性,对运行循环的成本进行猜测。这些猜测通常相当准确,但有时也会出错。使用 ANALYZE 命令收集有关数据库的更多统计信息有时可以使 SQLite 能够对成本做出更好的猜测。

成本由多个数字组成,而不是像图中显示的那样只有一个数字。SQLite 为每个循环计算了几个不同的估计成本,这些成本在不同的时间适用。例如,有一个“设置”成本,该成本只在查询开始时发生一次。设置成本是计算没有索引表的 查询时索引 的成本。然后是运行循环的每个步骤的成本。最后,是对循环生成的行的数量的估计,这是估计内部循环成本所需的信息。如果查询有 ORDER BY 子句,则排序成本可能发挥作用。

在一般查询中,依赖项不必位于单个循环上,因此依赖项矩阵可能无法表示为图。例如,WHERE 子句约束之一可能是 S.a=L.b+P.c,这意味着 S 循环必须是 L 和 P 的内循环。这种依赖关系无法绘制为图,因为弧线无法同时从两个或多个节点开始。

如果查询包含 ORDER BY 子句或 GROUP BY 子句,或者如果查询使用 DISTINCT 关键字,那么选择通过图的路径,使行自然地按排序顺序出现是有利的,这样就不需要单独的排序步骤。自动消除 ORDER BY 子句可以带来很大的性能差异,因此这是在完整实现中需要考虑的另一个因素。

在 TPC-H Q8 查询中,设置成本都可以忽略不计,所有依赖项都在各个节点之间,并且没有 ORDER BY、GROUP BY 或 DISTINCT 子句。因此,对于 TPC-H Q8,上面的图是需要计算内容的合理表示。一般情况下,涉及许多额外的复杂性,为了清晰起见,本文余下的部分将忽略这些复杂性。

3.3. 寻找最佳查询计划

3.8.0 版(2013-08-26)之前,SQLite 在搜索最佳查询计划时始终使用“最近邻”或“NN”启发式方法。NN 启发式方法对图进行单次遍历,始终选择成本最低的弧作为下一步。NN 启发式方法在大多数情况下效果很好。并且 NN 速度很快,因此 SQLite 能够快速找到即使对于大型 64 路联接,也能找到良好的计划。相比之下,其他 SQL 数据库引擎执行更广泛的搜索,往往会在联接表数量超过 10 或 15 时陷入困境。

不幸的是,NN 为 TPC-H Q8 计算的查询计划不是最佳的。使用 NN 计算的计划是 R-N1-N2-S-C-O-L-P,成本为 36.92。前一句中的符号表示 R 表在外部循环中运行,N1 在下一个内部循环中运行,N2 在第三个循环中运行,依此类推,直到 P 在最内层循环中运行。通过图的最短路径(通过穷举搜索找到)是 P-L-O-C-N1-R-S-N2,成本为 27.38。差异可能看起来并不大,但请记住,成本是对数的,因此最短路径的速度几乎是使用 NN 启发式方法找到的路径的 750 倍。

解决此问题的一种方法是将 SQLite 更改为对最佳路径进行穷举搜索。但穷举搜索需要与 K!成正比的时间(其中 K 是联接中的表数),因此当您超过 10 路联接时,运行 sqlite3_prepare() 的时间变得非常长。

3.4. N 个最近邻或“N3”启发式方法

NGQP 使用一种新的启发式方法来寻找通过图的最佳路径:“N 个最近邻”(以下简称“N3”)。使用 N3,算法不是在每一步只选择一个最近邻,而是跟踪每一步的 N 个最佳路径,其中 N 是某个小的整数。

假设 N=4。那么对于 TPC-H Q8 图,第一步找到访问图中任何单个节点的四条最短路径

R(成本:3.56)
N1(成本:5.52)
N2(成本:5.52)
P(成本:7.71)

第二步找到访问两个节点的四条最短路径,从上一步的四条路径之一开始。在两个或多个路径等效的情况下(它们具有相同的访问节点集,但顺序可能不同),只保留第一个和成本最低的路径。我们有

R-N1(成本:7.03)
R-N2(成本:9.08)
N2-N1(成本:11.04)
R-P {成本:11.27}

第三步从四条最短的双节点路径开始,找到四条最短的三节点路径

R-N1-N2(成本:12.55)
R-N1-C(成本:13.43)
R-N1-P(成本:14.74)
R-N2-S(成本:15.08)

依此类推。TPC-H Q8 查询中共有 8 个节点,因此该过程总共重复 8 次。在 K 路联接的一般情况下,存储要求为 O(N),计算时间为 O(K*N),这比 O(2K) 精确解快得多。

但应选择哪个 N 值?您可以尝试 N=K。这使得算法为 O(K2),这实际上仍然非常有效,因为 K 的最大值为 64,而 K 很少超过 10。但对于 TPC-H Q8 问题来说,这还不够。在 TPC-H Q8 上使用 N=8 时,N3 算法找到了解决方案 R-N1-C-O-L-S-N2-P,成本为 29.78。与 NN 相比,这是一个很大的改进,但它仍然不是最佳的。当 N 为 10 或更大时,N3 找到了 TPC-H Q8 的最佳解决方案。

NGQP 的初始实现为简单查询选择 N=1,为双路联接选择 N=5,为所有三路或更多路联接选择 N=10。选择 N 的此公式可能会在后续版本中更改。

4. 升级到 NGQP 的风险

→ 更新:此部分已过时,仅保留用于历史参考。当 NGQP 是新事物时,此部分很重要。但十年过去了,NGQP 已成功部署到数十亿台设备中,每个人都已升级,并且从未向 SQLite 开发人员报告过性能回归。升级风险已消失。此部分仅保留用于历史参考。现代读者可以跳至 查询计划清单。←

对于大多数应用程序,从旧的查询计划升级到 NGQP 几乎不需要考虑或努力。只需将旧的 SQLite 版本替换为更新的 SQLite 版本并重新编译,应用程序就会运行得更快。没有 API 更改或编译过程的修改。

但与任何查询计划更改一样,升级到 NGQP 确实存在引入性能回归的微小风险。这里的问题不是 NGQP 不正确或有错误或比旧的查询计划差。如果能够获得有关索引选择性的可靠信息,NGQP 应该始终选择一个与之前一样好或更好的计划。问题在于,一些应用程序可能使用低质量和低选择性索引,而没有运行 ANALYZE。旧的查询计划查看每个查询的更少可能的实现,因此它们可能通过愚蠢的运气偶然发现了一个好计划。另一方面,NGQP 查看了更多查询计划的可能性,它可能会选择一个理论上更好的查询计划,假设索引良好,但在实践中却会导致性能回归,因为数据形状不同。

关键点

并非所有应用程序都能满足这些条件。幸运的是,即使没有这些条件,NGQP 通常仍然可以找到好的查询计划。但是,确实会发生(很少见)性能回归的情况。

4.1. 案例研究:将 Fossil 升级到 NGQP

Fossil DVCS 是用于跟踪所有 SQLite 源代码的版本控制系统。Fossil 存储库是一个 SQLite 数据库文件。(鼓励读者将此递归作为独立的练习来思考。)Fossil 既是 SQLite 的版本控制系统,又是 SQLite 的测试平台。每当对 SQLite 进行增强时,Fossil 都是首批测试和评估这些增强功能的应用程序之一。因此,Fossil 是 NGQP 的早期采用者。

不幸的是,NGQP 导致 Fossil 的性能下降。

Fossil 提供的众多报告之一是单个分支的更改时间线,显示了该分支的进出合并。请参阅 https://www.sqlite.org/src/timeline?nd&n=200&r=trunk 查看此类报告的典型示例。生成此类报告通常只需要几毫秒。但升级到 NGQP 后,我们注意到,对于存储库的主干,此报告需要花费接近 10 秒的时间。

用于生成分支时间线的核心查询如下所示。(读者不必理解此查询的细节。后续将进行说明。)

SELECT
     blob.rid AS blobRid,
     uuid AS uuid,
     datetime(event.mtime,'localtime') AS timestamp,
     coalesce(ecomment, comment) AS comment,
     coalesce(euser, user) AS user,
     blob.rid IN leaf AS leaf,
     bgcolor AS bgColor,
     event.type AS eventType,
     (SELECT group_concat(substr(tagname,5), ', ')
        FROM tag, tagxref
       WHERE tagname GLOB 'sym-*'
         AND tag.tagid=tagxref.tagid
         AND tagxref.rid=blob.rid
         AND tagxref.tagtype>0) AS tags,
     tagid AS tagid,
     brief AS brief,
     event.mtime AS mtime
  FROM event CROSS JOIN blob
 WHERE blob.rid=event.objid
   AND (EXISTS(SELECT 1 FROM tagxref
                WHERE tagid=11 AND tagtype>0 AND rid=blob.rid)
        OR EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=cid
                   WHERE tagid=11 AND tagtype>0 AND pid=blob.rid)
        OR EXISTS(SELECT 1 FROM plink JOIN tagxref ON rid=pid
                   WHERE tagid=11 AND tagtype>0 AND cid=blob.rid))
 ORDER BY event.mtime DESC
 LIMIT 200;

此查询并不特别复杂,但即便如此,它也替换了数百行甚至数千行过程代码。查询的要点如下:扫描 EVENT 表,查找满足以下三个条件之一的最近 200 次检入

  1. 检入具有“主干”标签。
  2. 检入有一个具有“主干”标签的子检入。
  3. 检入有一个具有“主干”标签的父检入。

第一个条件导致所有主干检入都显示出来,第二和第三条件导致合并到主干或从主干分叉的检入也包含在内。这三个条件由 WHERE 子句中三个用 OR 连接的 EXISTS 语句实现。NGQP 导致的减速是由第二和第三条件造成的。这两个条件的问题相同,因此我们将只检查第二个条件。第二个条件的子查询可以重新编写(进行了一些细微的不重要的简化),如下所示

SELECT 1
  FROM plink JOIN tagxref ON tagxref.rid=plink.cid
 WHERE tagxref.tagid=$trunk
   AND plink.pid=$ckid;

PLINK 表保存检入之间的父子关系。TAGXREF 表将标签映射到检入。供参考,这两个表的相关部分架构如下所示

CREATE TABLE plink(
  pid INTEGER REFERENCES blob,
  cid INTEGER REFERENCES blob
);
CREATE UNIQUE INDEX plink_i1 ON plink(pid,cid);

CREATE TABLE tagxref(
  tagid INTEGER REFERENCES tag,
  mtime TIMESTAMP,
  rid INTEGER REFERENCE blob,
  UNIQUE(rid, tagid)
);
CREATE INDEX tagxref_i1 ON tagxref(tagid, mtime);

此查询只有两种合理的方法。(还有许多其他可能的算法,但没有其他算法是“最佳”算法的竞争者。)

  1. 找到检入 $ckid 的所有子检入,并测试每个子检入是否具有 $trunk 标签。

  2. 找到所有具有 $trunk 标签的检入,并测试每个检入是否是 $ckid 的子检入。

直观地,我们人类理解算法 1 是最好的。每个检入可能只有很少的子检入(一个子检入是最常见的情况),并且可以对每个子检入进行对数时间测试,以查看它是否具有 $trunk 标签。实际上,算法 1 在实践中是更快的选择。但 NGQP 没有直觉。NGQP 必须使用硬数学,而算法 2 在数学上略微更好。这是因为,在没有其他信息的情况下,NGQP 必须假设索引 PLINK_I1 和 TAGXREF_I1 的质量相同,并且选择性相同。算法 2 使用 TAGXREF_I1 索引的一个字段和 PLINK_I1 索引的两个字段,而算法 1 仅使用每个索引的第一个字段。由于算法 2 使用了更多索引材料,因此 NGQP 正确地认为它是更好的算法。分数很接近,算法 2 险胜算法 1。但算法 2 确实是在这里正确的选择。

不幸的是,算法 2 比算法 1 在此应用程序中速度更慢。

问题在于索引的质量不同。检入可能只有一个子检入。因此,PLINK_I1 的第一个字段通常会将搜索范围缩小到一行。但有成千上万个检入带有“主干”标签,因此 TAGXREF_I1 的第一个字段在缩小搜索范围方面几乎没有帮助。

除非在数据库上运行了 ANALYZE 命令,否则 NGQP 无法知道 TAGXREF_I1 在此查询中几乎毫无用处。 ANALYZE 命令收集有关各种索引质量的统计信息,并将这些统计信息存储在 SQLITE_STAT1 表中。 拥有这些统计信息,NGQP 可以轻松地选择算法 1 作为最佳算法,而且差距很大。

为什么传统的查询规划器没有选择算法 2?简单来说:因为 NN 算法甚至从未考虑过算法 2。规划问题的图表如下所示

P T 4.8 4.4 *  4.9 *  5.2 without ANALYZE P T 4.4 3.8 *  3.9 *  6.1 with ANALYZE

在左侧的“无 ANALYZE”情况下,NN 算法选择循环 P(PLINK)作为外循环,因为 4.9 小于 5.2,导致路径 P-T 成为算法 1。NN 只会在每一步查看最佳选择,因此它完全忽略了 5.2+4.4 比 4.9+4.8 更便宜的事实。但 N3 算法跟踪了 2 路连接的 5 条最佳路径,因此它最终选择路径 T-P,因为它总成本略低。路径 T-P 是算法 2。

请注意,使用 ANALYZE 时,成本估算更符合实际情况,算法 1 由 NN 和 N3 都选择。

(旁注:最近两个图表中的成本估算由 NGQP 使用以 2 为底的对数和与传统查询规划器相比略有不同的成本假设计算得出。因此,这两个图表中的成本估算与 TPC-H Q8 图表中的成本估算不可直接比较。)

4.2. 解决问题

在存储库数据库上运行 ANALYZE 命令立即解决了性能问题。但是,我们希望 Fossil 能够健壮,并且始终快速工作,无论其存储库是否经过分析。出于这个原因,查询被修改为使用 CROSS JOIN 运算符而不是普通的 JOIN 运算符。SQLite 不会重新排序 CROSS JOIN 的表。这是 SQLite 的一项长期功能,专门设计用于允许有经验的程序员强制执行特定的循环嵌套顺序。一旦连接更改为 CROSS JOIN(添加了一个关键字),NGQP 就被迫选择更快的算法 1,无论是否使用 ANALYZE 收集了统计信息。

我们说算法 1 “更快”,但这并不完全正确。算法 1 在普通存储库中更快,但可以构建一个存储库,其中每次签入都在不同的唯一命名的分支上,并且所有签入都是根签入的子节点。在这种情况下,TAGXREF_I1 将比 PLINK_I1 更具选择性,算法 2 确实会成为更快的选择。但是,这种存储库在实践中极不可能出现,因此在这种情况下,使用 CROSS JOIN 语法对循环嵌套顺序进行硬编码是解决问题的合理方法。

4.3. 2017 年更新:更好的解决方案

前面的文字写于 2013 年初,在 SQLite 3.8.0 版首次发布之前。本段添加于 2021 年中。虽然之前的所有讨论仍然成立,但查询规划器已经有了很多改进,使得整个部分几乎无关紧要。

2017 年,Fossil 进行了增强,以利用新的 PRAGMA optimize 语句。每当 Fossil 要关闭与其存储库的数据库连接时,它首先运行“PRAGMA optimize”,这反过来会导致在需要时运行 ANALYZE。通常不需要 ANALYZE,因此这样做没有明显的性能损失。但偶尔会对存储库数据库中的几个表运行 ANALYZE。因此,这里描述的查询规划问题在 Fossil 中不再出现。由于定期运行 ANALYZE 以保持 sqlite_stat1 表最新,因此不再需要手动调整查询。我们已经很久没有对 Fossil 中的查询进行调整了。

因此,当前避免此类问题的建议是在关闭每个数据库连接之前简单地运行“PRAGMA optimize”。或者,如果您的应用程序是长时间运行的并且从不关闭任何数据库连接,那么每天运行一次“PRAGMA optimize”。还要考虑在任何模式更改后运行“PRAGMA optimize”。

5. 避免或解决查询规划器问题的清单

  1. 不要惊慌!查询规划器选择劣质计划的情况实际上非常罕见。您的应用程序不太可能遇到任何问题。如果您没有性能问题,则无需担心这些问题。

  2. 创建适当的索引。大多数 SQL 性能问题并非源于查询规划器问题,而是由于缺少适当的索引。确保有索引可以帮助所有大型查询。大多数性能问题可以通过一两个 CREATE INDEX 命令解决,而无需更改应用程序代码。

  3. 避免创建低质量索引。. 低质量索引(出于本清单的目的)是指表中有多于 10 或 20 行具有相同索引最左侧列的值的索引。特别是,避免使用布尔值或“枚举”列作为索引的最左侧列。

    本文档前一部分描述的 Fossil 性能问题是因为 TAGXREF 表中有多达一万个条目在 TAGXREF_I1 索引的最左侧列(TAGID 列)中具有相同的值。

  4. 如果您必须使用低质量索引,请务必运行 ANALYZE只要查询规划器知道索引质量低下,低质量索引就不会混淆查询规划器。查询规划器知道这一点的方式是通过 SQLITE_STAT1 表的内容,该表由 ANALYZE 命令计算得出。

    当然,ANALYZE 只有在您的数据库中首先有大量内容时才能有效地工作。在创建您期望积累大量数据的数据库时,您可以运行命令“ANALYZE sqlite_schema”来创建 SQLITE_STAT1 表,然后使用普通的 INSERT 语句预填充 sqlite_stat1 表,其内容描述了您的应用程序的典型数据库,例如在实验室中对已填充的模板数据库运行 ANALYZE 后提取的内容。或者,您也可以在关闭数据库连接之前运行“PRAGMA optimize”,以便在需要时自动运行 ANALYZE 以使 sqlite_stat1 表保持最新。

  5. 对您的代码进行检测。添加逻辑,让您能够快速轻松地知道哪些查询花费了太多时间。然后只针对这些特定的查询进行处理。


    2024 年更新:查询规划器经过多年的改进,您永远不需要使用下面描述的任何技巧。下面描述的功能仍然可用,是为了向后兼容。但您不应该使用它们。如果您确实发现查询计划不佳的情况,请在 SQLite 论坛 上向 SQLite 开发人员报告,以便他们尝试解决问题。换句话说

    请停止阅读!

    为了鼓励您停止阅读,本清单的其余部分现在已变灰。


  6. 使用 unlikely()likelihood() SQL 函数。SQLite 通常假设 WHERE 子句中无法被索引使用的项有很高的概率为真。如果这个假设不正确,可能会导致查询计划不佳。 unlikely()likelihood() SQL 函数可用于为查询规划器提供有关可能不为真的 WHERE 子句项的提示,从而帮助查询规划器选择最佳计划。

  7. 使用 CROSS JOIN 语法对可能在未分析的数据库中使用低质量索引的查询强制执行特定的循环嵌套顺序。SQLite 对 CROSS JOIN 运算符进行特殊处理,强制左侧的表相对于右侧的表成为外循环。

  8. 使用一元“+”运算符来取消资格 WHERE 子句项。如果查询规划器坚持为特定查询选择低质量索引,而有更高质量的索引可用,则 谨慎使用一元“+”运算符 在 WHERE 子句中可以强制查询规划器避开低质量索引。如果可能,请避免使用此技巧,特别是在应用程序开发周期的早期避免使用此技巧。请注意,如果涉及 类型亲和性,则在相等表达式中添加一元“+”运算符可能会更改该表达式的结果。

  9. 使用 INDEXED BY 语法强制对问题查询选择特定索引。与前面两点一样,如果可能,请避免此步骤,特别是在开发的早期阶段避免这样做,因为这显然是过早优化。

6. 总结

SQLite 中的查询规划器通常在为运行您的 SQL 语句选择快速算法方面做得非常出色。这对于传统的查询规划器来说是正确的,对于新的 NGQP 来说更是如此。可能偶尔会出现由于信息不完整,查询规划器选择了次优计划的情况。使用 NGQP 比使用传统查询规划器更少发生这种情况,但仍然可能发生。只有在这些罕见的情况下,应用程序开发人员才需要参与进来,帮助查询规划器做出正确的事情。在一般情况下,NGQP 只是 SQLite 的一项新增强功能,它使应用程序运行速度稍快,并且不需要开发人员进行任何新的思考或操作。

此页面最后修改于 2024-05-10 14:30:36 UTC