BigQuery 的诊断查询计划和耗时信息嵌入在查询作业中。 这类似于在其他数据库和分析系统中由 EXPLAIN
等语句提供的信息。 您可从 jobs.get
等方法的 API 响应中检索到这些信息。
对于长时间运行的查询,BigQuery 将定期更新这些统计信息。这些更新的发生与轮询作业状态的速率无关,但通常更新间隔不会小于 30 秒。此外,不使用执行资源的查询作业(例如试运行请求或可通过缓存结果提供的结果)将不包含额外的诊断信息,但可能存在其他统计信息。
背景
BigQuery 在执行查询作业时,会将声明式 SQL 语句转换为执行图表,而后分解为一系列查询阶段,而这些阶段本身由更细化的多组执行步骤构成。 BigQuery 会利用高度分布式的并行架构来运行这些查询,而阶段则会对许多潜在工作器可能并行执行的工作单元进行建模。各阶段使用快捷分布式 Shuffle 架构相互通信。
在查询计划中,“工作单元”和“工作器”这两个术语专门用于传达有关并行性的信息。在 BigQuery 的其他位置,您可能会遇到“槽”这个术语;该术语是查询执行的多个方面(包括计算、内存和 I/O 资源)的抽象表示。借助这种抽象核算方式,顶级作业统计信息可通过查询的 totalSlotMs
估算值提供单次查询的费用估算。
查询执行架构的另一个重要属性是它具有动态性,这意味着您可在查询运行时修改查询计划。在查询运行时引入的阶段通常用于改进所有查询工作器的数据分布。在发生这种情况的查询计划中,这些阶段通常会被标记为“重新分区阶段”。
除了查询计划之外,查询作业还会显示执行时间轴,该时间轴用于提供查询工作器内处于已完成、待处理和活跃状态的工作单元的核算信息。一个查询可能会同时经历多个具有活跃工作器的阶段,因此时间轴是用于显示查询的整体进度。
使用 Google Cloud 控制台查看信息
在 Google Cloud 控制台中,您可以通过点击执行详情按钮(在结果按钮附近)来查看已完成查询的查询计划详细信息。
查询计划信息
在 API 响应中,查询计划表示为一系列查询阶段的列表。 列表中的每个项显示每个阶段的概览统计信息、详细步骤信息和阶段耗时分类。并非所有详细信息都会显示在 Google Cloud 控制台中,但它们全部存在于 API 响应中。
阶段概览
每个阶段的概览字段可能包括以下各项:
API 字段 | 说明 |
---|---|
id |
阶段的唯一数字 ID。 |
name |
阶段的简单概括名称。阶段内的 steps 提供有关执行步骤的更多详细信息。 |
status |
阶段的执行状态。可能的状态包括:待处理、正在运行、已完成、已失败和已取消。 |
inputStages |
构成阶段的依赖关系图的一系列 ID。例如,JOIN 阶段通常需要两个依赖阶段,用于准备 JOIN 关系左右侧的数据。 |
startMs |
时间戳(以纪元毫秒为单位),表示阶段中第一个工作器开始执行的时间。 |
endMs |
时间戳(以纪元毫秒为单位),表示最后一个工作器结束执行的时间。 |
steps |
阶段中更加详细的执行步骤列表。要了解详情,请参阅下一部分。 |
recordsRead |
所有阶段工作器的阶段输入大小(即记录数)。 |
recordsWritten |
所有阶段工作器的阶段输出大小(即记录数)。 |
parallelInputs |
阶段的可并行工作单元数。它可能表示表中的列式细分数,也可能表示中间 Shuffle 的分区数,具体取决于阶段和查询。 |
completedParallelInputs |
阶段中已完成的工作单元数量。对于某些查询,要完成某个阶段,并不一定需要完成该阶段中的每一个输入。 |
shuffleOutputBytes |
表示查询阶段中写入所有工作器的总字节数。 |
shuffleOutputBytesSpilled |
在阶段之间传输重要数据的查询可能需要回退到基于磁盘的传输。溢出的字节统计信息传达了溢出到磁盘的数据量。根据优化算法,它对于任何给定查询都具有不确定性。 |
每个阶段的耗时分类
查询阶段以相对和绝对两种形式提供阶段耗时分类。由于每个执行阶段表示由一个或多个独立工作器所执行的工作,因此会以平均耗时和最长耗时两种形式提供信息。这些时间代表一个阶段中所有工作器的平均性能以及指定分类下的长尾最慢工作器性能。平均耗时和最大耗时被进一步分为绝对表示法和相对表示法。对于基于比率的统计信息,会以任何细分中任何工作器的最长耗时所占比例形式提供数据。
Google Cloud 控制台使用相对耗时表示法呈现阶段耗时。
阶段耗时信息报告如下:
相对耗时 | 绝对耗时 | 比率分子 |
---|---|---|
waitRatioAvg |
waitMsAvg |
一般工作器等待安排的时间。 |
waitRatioMax |
waitMsMax |
最慢的工作器等待安排的时间。 |
readRatioAvg |
readMsAvg |
一般工作器用于读取输入数据的时间。 |
readRatioMax |
readMsMax |
最慢的工作器用于读取输入数据的时间。 |
computeRatioAvg |
computeMsAvg |
一般工作器用于 CPU 绑定的时间。 |
computeRatioMax |
computeMsMax |
最慢的工作器用于 CPU 绑定的时间。 |
writeRatioAvg |
writeMsAvg |
一般工作器用于写入输出数据的时间。 |
writeRatioMax |
writeMsMax |
最慢的工作器用于写入输出数据的时间。 |
步骤概览
步骤包含阶段中每个工作器执行的操作,以有序操作列表的形式呈现。每个步骤操作都有一个类别,其中一些操作提供更详细的信息。查询计划中显示的操作类别包括以下各项:
步骤类别 | 说明 |
---|---|
READ |
从输入表或中间 Shuffle 读取一列或多列。步骤详情中仅返回读取的前 16 列。 |
WRITE |
将一列或多列写入输出表或中间 Shuffle。对于阶段的 HASH 分区输出,这还包括用作分区键的列。 |
COMPUTE |
表达式求值和 SQL 函数。 |
FILTER |
由 WHERE 、OMIT IF 和 HAVING 子句使用。 |
SORT |
包含列键和排序顺序的 ORDER BY 操作。 |
AGGREGATE |
为 GROUP BY 或 COUNT 等子句实现汇总。 |
LIMIT |
实现 LIMIT 子句。 |
JOIN |
为 JOIN 等子句实现联接;包括联接类型和可能的联接条件。 |
ANALYTIC_FUNCTION |
调用窗口函数(也称为“分析函数”)。 |
USER_DEFINED_FUNCTION |
对用户定义的函数的调用。 |
解读和优化步骤
以下部分介绍了如何解读查询计划中的步骤,并提供了优化查询的方法。
READ
步骤
READ
步骤表示某个阶段正在访问数据以进行处理。您可以直接从查询中引用的表或从 Shuffle 内存中读取数据。读取上一个阶段的数据时,BigQuery 会从 Shuffle 内存读取数据。使用按需槽时,扫描的数据量会影响费用;使用预留时,扫描的数据量会影响性能。
潜在的性能问题
- 对未分区的表进行大规模扫描:如果查询只需要一小部分数据,则可能表明表扫描效率不高。分区可能是一项不错的优化策略。
- 过滤比例较小的大型表的扫描:这表示过滤条件未有效减少扫描的数据量。请考虑修改过滤条件。
- Shuffle 字节溢出到磁盘:这表示系统未使用集群等优化技术有效存储数据,而集群可在集群中维护类似数据。
优化
- 有针对性地过滤:在查询中,有策略地使用
WHERE
子句,尽早滤除不相关的数据。这可以减少查询需要处理的数据量。 - 分区和聚类:BigQuery 使用表分区和聚类来高效定位特定数据片段。确保根据典型查询模式对表进行分区和分片,以最大限度地减少
READ
步骤期间扫描的数据量。 - 选择相关列:避免使用
SELECT *
语句。请改为选择特定列或使用SELECT * EXCEPT
来避免读取不必要的数据。 - 具体化视图:具体化视图可以预计算和存储常用的汇总,这可能会减少在使用这些视图的查询的
READ
步骤期间读取基表的需要。
COMPUTE
步骤
在 COMPUTE
步骤中,BigQuery 会对您的数据执行以下操作:
- 评估查询的
SELECT
、WHERE
、HAVING
和其他子句中的表达式,包括计算、比较和逻辑运算。 - 执行内置 SQL 函数和用户定义函数。
- 根据查询中的条件过滤数据行。
优化
查询计划可以揭示 COMPUTE
步骤中的瓶颈。查找需要进行大量计算或处理大量行的阶段。
- 将
COMPUTE
步骤与数据量相关联:如果某个阶段显示大量计算并处理大量数据,则可能是优化的理想对象。 - 数据偏斜:如果计算最大值显著高于计算平均值,则表示相应阶段花费了过多时间处理少量数据片段。不妨查看数据分布,看看是否存在数据偏差。
- 考虑数据类型:为列使用适当的数据类型。例如,使用整数、日期时间和时间戳(而非字符串)可以提高性能。
WRITE
步骤
WRITE
步骤会针对中间数据和最终输出执行。
- 写入到 Shuffle 内存:在多阶段查询中,
WRITE
步骤通常涉及将处理后的数据发送到另一个阶段以进行进一步处理。这对于 Shuffle 内存来说很常见,因为 Shuffle 内存会合并或汇总来自多个来源的数据。在此阶段写入的数据通常是中间结果,而不是最终输出。 - 最终输出:查询结果会写入目标表或临时表。
哈希分区
当查询计划中的某个阶段将数据写入哈希分区输出时,BigQuery 会写入输出中包含的列以及被选作分区键的列。
优化
虽然 WRITE
步骤本身可能无法直接优化,但了解其作用有助于您在更早的阶段发现潜在的瓶颈:
- 尽量减少写入的数据量:重点通过过滤和聚合优化前面的阶段,以减少在此步骤中写入的数据量。
分区:表分区对写入非常有益。如果您写入的数据仅限于特定分区,BigQuery 可以更快地执行写入操作。
如果 DML 语句包含针对表分区列的静态条件的
WHERE
子句,则 BigQuery 只会修改相关表分区。反规范化权衡:反规范化有时会导致中间
WRITE
步骤中的结果集较小。不过,这也存在一些缺点,例如存储用量增加和数据一致性问题。
JOIN
步骤
在 JOIN
步骤中,BigQuery 会合并来自两个数据源的数据。联接可以包含联接条件。联接会占用大量资源。在 BigQuery 中联接大量数据时,系统会独立对联接键进行重排,使其对齐到同一槽,以便在每个槽上本地执行联接。
JOIN
步骤的查询计划通常会显示以下详细信息:
- 联接模式:表示所使用的联接类型。每种类型都定义了结果集中包含的联接表中的行数。
- 联接列:这些列用于在数据源之间匹配行。列的选择对联接性能至关重要。
联接模式
- 广播联接:当一个表(通常是较小的表)可以放入单个工作器节点或槽的内存中时,BigQuery 可以将其广播到所有其他节点,以高效地执行联接。在步骤详情中查找
JOIN EACH WITH ALL
。 - 哈希联接:如果表很大或广播联接不适用,则可能会使用哈希联接。BigQuery 使用哈希和洗牌操作对左表和右表进行洗牌,以便匹配的键最终位于同一槽中,以执行本地联接。由于需要移动数据,因此哈希联接是一项开销较高的操作,但可以高效地跨哈希匹配行。在步骤详情中查找
JOIN EACH WITH EACH
。 - 自联接:一种 SQL 反模式,其中表会与自身联接。
- 交叉联接:一种 SQL 反模式,可能会导致严重的性能问题,因为它会生成比输入更大的数据。
- 联接偏斜:一个表中联接键的数据分布非常不均衡,可能会导致性能问题。查找查询计划中最大计算时间远远大于平均计算时间的情况。如需了解详情,请参阅高基数联接和分区偏斜。
调试
- 数据量过大:如果查询计划显示在
JOIN
步骤期间处理了大量数据,请检查联接条件和联接键。请考虑过滤或使用更具选择性的联接键。 - 数据分布偏差:分析联接键的数据分布。如果某个表非常不平衡,请探索拆分查询或预过滤等策略。
- 高基数联接:如果联接生成的行数明显多于左侧和右侧输入行数,则可能会大幅降低查询性能。避免使用会产生大量行的联接。
- 表的排序不正确:请确保您选择了适当的联接类型(例如
INNER
或LEFT
),并根据查询的要求将表从大到小排序。
优化
- 选择性联接键:对于联接键,请尽可能使用
INT64
而非STRING
。STRING
比较的速度比INT64
比较的速度慢,因为它们会比较字符串中的每个字符。整数只需进行一次比较。 - 在联接之前过滤:在联接之前对各个表应用
WHERE
子句过滤条件。这可以减少联接操作中涉及的数据量。 - 避免对联接列使用函数:避免对联接列调用函数。相反,请在提取过程中或提取后使用 ELT SQL 流水线对表数据进行标准化处理。这种方法无需动态修改联接列,从而实现更高效的联接,同时又不会影响数据完整性。
- 避免自联接:自联接通常用于计算依赖于行的关联。不过,自联接可能会使输出行数增加到原来的四倍,从而导致性能问题。请考虑使用窗口(分析)函数,而不是依赖于自联接。
- 优先处理大型表:即使 SQL 查询优化器可以确定哪个表应位于联接的哪一侧,也请适当地对联接的表进行排序。最佳做法是先放置最大的表,然后放置最小的表,接下来再按从大到小的顺序放置。
- 反规范化:在某些情况下,有策略地对表进行反规范化(添加冗余数据)可以完全消除联接。不过,这种方法会带来存储和数据一致性方面的权衡。
- 分区和聚类:通过根据联接键对表进行分区并对共置数据进行聚类,可以让 BigQuery 定位到相关的数据分区,从而显著加快联接速度。
- 优化存在数据倾斜的联接:为避免与存在数据倾斜的联接相关的性能问题,请尽早预先过滤表中的数据,或将查询拆分为两个或更多查询。
AGGREGATE
步骤
在 AGGREGATE
步骤中,BigQuery 会汇总和分组数据。
调试
- 阶段详情:检查聚合操作的输入行数和输出行数,以及重新洗牌大小,以确定汇总步骤减少了多少数据,以及是否涉及数据重新洗牌。
- Shuffle 大小:Shuffle 大小较大可能表示在汇总期间有大量数据在工作器节点之间移动。
- 检查数据分布情况:确保数据在各个分区中均匀分布。数据分布偏差可能会导致汇总步骤中的工作负载不平衡。
- 检查汇总:分析汇总子句,确认其是否必要且高效。
优化
- 重合:按
GROUP BY
、COUNT
或其他汇总子句中经常使用的列对表进行重合。 - 分区:选择与您的查询模式相符的分区策略。考虑使用提取时间分区表,以减少汇总期间扫描的数据量。
- 提前聚合:请尽可能在查询流水线中提前执行聚合。这可以减少聚合期间需要处理的数据量。
- 洗牌优化:如果洗牌是瓶颈,请探索减少洗牌次数的方法。例如,对表进行去规范化处理,或使用聚类来共置相关数据。
边缘用例
- DISTINCT 汇总:使用
DISTINCT
汇总的查询的计算开销可能很高,尤其是在大型数据集上。考虑使用APPROX_COUNT_DISTINCT
等替代方法来获取近似结果。 - 组数量过多:如果查询生成大量组,则可能会占用大量内存。在这种情况下,不妨考虑限制组的数量或使用其他汇总策略。
REPARTITION
步骤
REPARTITION
和 COALESCE
都是 BigQuery 直接应用于查询中随机排序数据的优化技术。
REPARTITION
:此操作旨在跨工作器节点重新平衡数据分布。假设在进行重排后,一个工作器节点最终获得的数据量不成比例地大。REPARTITION
步骤会更均匀地重新分配数据,从而防止任何单个工作器成为瓶颈。对于联接等计算密集型操作,这一点尤为重要。COALESCE
:如果您在重新洗牌后有许多小数据分桶,则会执行此步骤。COALESCE
步骤会将这些存储分区合并到更大的存储分区中,从而减少与管理大量小数据块相关的开销。在处理非常小的中间结果集时,这尤为有用。
如果您在查询计划中看到 REPARTITION
或 COALESCE
步骤,并不一定表示您的查询存在问题。这通常表示 BigQuery 正在主动优化数据分布,以提升性能。不过,如果您反复看到这些操作,则可能表示您的数据本身存在偏差,或者您的查询导致过多的数据重排。
优化
如需减少 REPARTITION
步骤的数量,请尝试以下操作:
- 数据分布:确保表已有效分区和分片。数据分布均匀有助于降低洗牌后出现严重不平衡的可能性。
- 查询结构:分析查询,找出数据偏差的潜在来源。例如,是否存在高度选择性的过滤器或联接,导致只有一小部分数据在单个工作器上处理?
- 联接策略:尝试使用不同的联接策略,看看它们能否带来更均衡的数据分布。
如需减少 COALESCE
步骤的数量,请尝试以下操作:
- 汇总策略:考虑在查询流水线中更早地执行汇总。这有助于减少可能导致
COALESCE
步骤的小型中间结果集的数量。 - 数据量:如果您处理的是非常小的数据集,
COALESCE
可能不是一项重大问题。
请勿过度优化。过早优化可能会使查询变得更复杂,而不会带来显著的好处。
联合查询的说明
联合查询可让您使用 EXTERNAL_QUERY
函数将查询语句发送到外部数据源。联合查询依赖于被称为 SQL 下推的优化方法,查询计划会显示下推到外部数据源的操作(如有)。例如,如果您运行以下查询:
SELECT id, name
FROM EXTERNAL_QUERY("<connection>", "SELECT * FROM company")
WHERE country_code IN ('ee', 'hu') AND name like '%TV%'
查询计划将显示以下阶段步骤:
$1:id, $2:name, $3:country_code
FROM table_for_external_query_$_0(
SELECT id, name, country_code
FROM (
/*native_query*/
SELECT * FROM company
)
WHERE in(country_code, 'ee', 'hu')
)
WHERE and(in($3, 'ee', 'hu'), like($2, '%TV%'))
$1, $2
TO __stage00_output
在此计划中,table_for_external_query_$_0(...)
代表 EXTERNAL_QUERY
函数。在括号中,您可以看到外部数据源执行的查询。根据这些信息,您可以看到:
- 外部数据源仅返回 3 个选定的列。
- 外部数据源仅返回
country_code
为'ee'
或'hu'
的行。 LIKE
运算符没有下推,并且由 BigQuery 进行计算。
为进行比较,如果没有下推,查询计划将显示以下阶段步骤:
$1:id, $2:name, $3:country_code
FROM table_for_external_query_$_0(
SELECT id, name, description, country_code, primary_address, secondary address
FROM (
/*native_query*/
SELECT * FROM company
)
)
WHERE and(in($3, 'ee', 'hu'), like($2, '%TV%'))
$1, $2
TO __stage00_output
这次,外部数据源返回 company
表中的所有列和所有行,并且 BigQuery 执行过滤。
时间轴元数据
查询时间轴用于在特定时间点报告进度,同时提供总体查询进度的快照视图。时间轴由一系列示例表示,这些示例报告了以下详细信息:
字段 | 说明 |
---|---|
elapsedMs |
自查询执行以来经过的毫秒数。 |
totalSlotMs |
查询使用的槽的累计表示(以毫秒为单位)。 |
pendingUnits |
已计划和等待执行的工作单元总数。 |
activeUnits |
工作器正在处理的活跃工作单元总数。 |
completedUnits |
在执行此查询时已完成的工作单元总数。 |
查询示例
以下查询会计算 Shakespeare 公开数据集内的行数,然后再运行第二个条件计数,将结果限制为引用“hamlet”的行数:
SELECT
COUNT(1) as rowcount,
COUNTIF(corpus = 'hamlet') as rowcount_hamlet
FROM `publicdata.samples.shakespeare`
点击执行详细信息,以查看查询计划:
颜色指示器显示了所有阶段中所有步骤的相对耗时。
如需详细了解执行阶段的步骤,请点击
以展开该阶段的详细信息:在此示例中,任何细分中的最长耗时即为阶段 01 中的单个工作器等待阶段 00 完成的时间。这是因为阶段 01 依赖于阶段 00 的输入,仅当第一个阶段将其输出写入中间 Shuffle 后才能启动阶段 01。
Error Reporting
查询作业在执行过程中可能会失败。由于计划信息会定期更新,您可以查看在执行图表中的哪个位置发生故障。在 Google Cloud 控制台中,成功或失败的阶段的名称旁边会标示对勾或感叹号图标。
如需详细了解如何解读和处理错误,请参阅问题排查指南。
API 表示示例
查询计划信息将嵌入到作业响应信息中,您可以通过调用 jobs.get
进行检索。例如,以下是返回 hamlet 查询示例的作业的 JSON 响应摘录,其中显示了查询计划和时间轴信息。
"statistics": { "creationTime": "1576544129234", "startTime": "1576544129348", "endTime": "1576544129681", "totalBytesProcessed": "2464625", "query": { "queryPlan": [ { "name": "S00: Input", "id": "0", "startMs": "1576544129436", "endMs": "1576544129465", "waitRatioAvg": 0.04, "waitMsAvg": "1", "waitRatioMax": 0.04, "waitMsMax": "1", "readRatioAvg": 0.32, "readMsAvg": "8", "readRatioMax": 0.32, "readMsMax": "8", "computeRatioAvg": 1, "computeMsAvg": "25", "computeRatioMax": 1, "computeMsMax": "25", "writeRatioAvg": 0.08, "writeMsAvg": "2", "writeRatioMax": 0.08, "writeMsMax": "2", "shuffleOutputBytes": "18", "shuffleOutputBytesSpilled": "0", "recordsRead": "164656", "recordsWritten": "1", "parallelInputs": "1", "completedParallelInputs": "1", "status": "COMPLETE", "steps": [ { "kind": "READ", "substeps": [ "$1:corpus", "FROM publicdata.samples.shakespeare" ] }, { "kind": "AGGREGATE", "substeps": [ "$20 := COUNT($30)", "$21 := COUNTIF($31)" ] }, { "kind": "COMPUTE", "substeps": [ "$30 := 1", "$31 := equal($1, 'hamlet')" ] }, { "kind": "WRITE", "substeps": [ "$20, $21", "TO __stage00_output" ] } ] }, { "name": "S01: Output", "id": "1", "startMs": "1576544129465", "endMs": "1576544129480", "inputStages": [ "0" ], "waitRatioAvg": 0.44, "waitMsAvg": "11", "waitRatioMax": 0.44, "waitMsMax": "11", "readRatioAvg": 0, "readMsAvg": "0", "readRatioMax": 0, "readMsMax": "0", "computeRatioAvg": 0.2, "computeMsAvg": "5", "computeRatioMax": 0.2, "computeMsMax": "5", "writeRatioAvg": 0.16, "writeMsAvg": "4", "writeRatioMax": 0.16, "writeMsMax": "4", "shuffleOutputBytes": "17", "shuffleOutputBytesSpilled": "0", "recordsRead": "1", "recordsWritten": "1", "parallelInputs": "1", "completedParallelInputs": "1", "status": "COMPLETE", "steps": [ { "kind": "READ", "substeps": [ "$20, $21", "FROM __stage00_output" ] }, { "kind": "AGGREGATE", "substeps": [ "$10 := SUM_OF_COUNTS($20)", "$11 := SUM_OF_COUNTS($21)" ] }, { "kind": "WRITE", "substeps": [ "$10, $11", "TO __stage01_output" ] } ] } ], "estimatedBytesProcessed": "2464625", "timeline": [ { "elapsedMs": "304", "totalSlotMs": "50", "pendingUnits": "0", "completedUnits": "2" } ], "totalPartitionsProcessed": "0", "totalBytesProcessed": "2464625", "totalBytesBilled": "10485760", "billingTier": 1, "totalSlotMs": "50", "cacheHit": false, "referencedTables": [ { "projectId": "publicdata", "datasetId": "samples", "tableId": "shakespeare" } ], "statementType": "SELECT" }, "totalSlotMs": "50" },
利用执行情况信息
BigQuery 查询计划提供有关该服务如何执行查询的信息,但该服务的托管特性限制了某些详细信息的可直接操作性。使用该服务可自动执行很多优化,这可能不同于其他需要由知识丰富的专业人员来完成调整、预配和监控的环境。
如需了解可以完善查询执行和性能的特定做法,请参阅最佳做法文档。查询计划和时间轴统计信息可以帮助您了解某些阶段是否在资源耗用上占据大头。例如,生成的输出行远超输入行的 JOIN 阶段可能表示有必要在查询的早期阶段进行过滤。
此外,时间轴信息可以帮助确定某个查询速度缓慢是因为其自身原因,还是受到了其他查询争用相同资源的影响。如果您发现活跃单元的数量在查询的整个生命周期中始终有限,但排队的工作单元数量一直很大,这可能意味着减少并发查询数量将显著缩短某些查询的总体执行时间。