查询计划和时间轴

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 在阶段之间传输重要数据的查询可能需要回退到基于磁盘的传输。溢出的字节统计信息传达了溢出到磁盘的数据量。根据优化算法,它对于任何给定查询都具有不确定性。

每个阶段的步骤信息

步骤表示阶段中每个工作器必须执行的细化操作,展现为一个有序操作列表。步骤经过分类,其中一些操作提供更详细的信息。查询计划中显示的操作类别包括以下各项:

步骤 说明
READ 从输入表或中间 Shuffle 读取一列或多列。 步骤详情中仅返回读取的前 16 列。
WRITE 将一列或多列写入输出表或中间结果。对于阶段的 HASH 分区输出,这还包括用作分区键的列。
COMPUTE 诸如表达式评估和 SQL 函数之类的操作。
FILTER 执行 WHERE、OMIT IF 和 HAVING 子句的运算符。
SORT Sort 或 Order-By 操作,包括列键和排序方向。
AGGREGATE 聚合操作,例如 GROUP BY 或 COUNT。
LIMIT 执行 LIMIT 子句的运算符。
JOIN JOIN 操作,其中包括所用的联接类型和列。
ANALYTIC_FUNCTION 调用窗口函数(也称为“分析函数”)。
USER_DEFINED_FUNCTION 调用用户定义的函数。

每个阶段的耗时分类

查询阶段还以相对和绝对两种形式提供阶段耗时分类。由于每个执行阶段表示由一个或多个独立工作器所执行的工作,因此会以平均耗时和最长耗时两种形式提供信息。这些时间代表一个阶段中所有工作器的平均性能以及指定分类下的长尾最慢工作器性能。平均耗时和最大耗时被进一步分为绝对表示法和相对表示法。对于基于比率的统计信息,会以任何细分中任何工作器的最长耗时所占比例形式提供数据。

Google Cloud 控制台使用相对耗时表示法呈现阶段耗时。

阶段耗时信息报告如下:

相对耗时 绝对耗时 比率分子
waitRatioAvg waitMsAvg 一般工作器等待安排的时间。
waitRatioMax waitMsMax 最慢的工作器等待安排的时间。
readRatioAvg readMsAvg 一般工作器用于读取输入数据的时间。
readRatioMax readMsMax 最慢的工作器用于读取输入数据的时间。
computeRatioAvg computeMsAvg 一般工作器用于 CPU 绑定的时间。
computeRatioMax computeMsMax 最慢的工作器用于 CPU 绑定的时间。
writeRatioAvg writeMsAvg 一般工作器用于写入输出数据的时间。
writeRatioMax writeMsMax 最慢的工作器用于写入输出数据的时间。

联合查询的说明

联合查询可让您使用 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`

点击执行详细信息,以查看查询计划:

hamlet 查询计划。

颜色指示器显示了所有阶段中所有步骤的相对耗时。

如需详细了解执行阶段的步骤,请点击 以展开该阶段的详细信息:

hamlet 查询计划步骤的详细信息。

在此示例中,任何细分中的最长耗时即为阶段 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 阶段可能表示有必要在查询的早期阶段进行过滤。

此外,时间轴信息可以帮助确定某个查询速度缓慢是因为其自身原因,还是受到了其他查询争用相同资源的影响。如果您发现活跃单元的数量在查询的整个生命周期中始终有限,但排队的工作单元数量一直很大,这可能意味着减少并发查询数量将显著缩短某些查询的总体执行时间。