将数据导出到 Spanner(反向 ETL)

本文档介绍如何设置从 BigQuery 到 Spanner 的反向提取、转换和加载(反向 ETL)工作流。为此,您可以使用 EXPORT DATA 语句将数据从 BigQuery 数据源(包括 Iceberg 表)导出到 Spanner 表。

此反向 ETL 工作流将 BigQuery 中的分析功能与 Spanner 中的低延迟时间和高吞吐量相结合。此工作流可让您向应用用户提供数据,而不会耗尽 BigQuery 的配额和限制。

准备工作

所需的角色

如需获得将 BigQuery 数据导出到 Spanner 所需的权限,请让管理员向您授予项目的以下 IAM 角色:

如需详细了解如何授予角色,请参阅管理对项目、文件夹和组织的访问权限

您也可以通过自定义角色或其他预定义角色来获取所需的权限。

限制

  • Assured Workloads 不支持此功能。

  • 以下 BigQuery 数据类型在 Spanner 中没有等效数据类型,不受支持:

Spanner 数据库方言 不支持的 BigQuery 类型
所有方言
  • STRUCT
  • GEOGRAPHY
  • DATETIME
  • RANGE
  • TIME
GoogleSQL
  • BIGNUMERIC:支持的 NUMERIC 类型不够宽。请考虑在查询中向 NUMERIC 类型添加显式类型转换。
  • 所导出行的大小上限为 1 MiB。

  • Spanner 会在导出期间强制执行参照完整性。 如果目标表是另一个表的子表 (INTERLEAVE IN PARENT),或者目标表具有外键限制条件,则在导出期间将验证外键和父键。如果导出的行写入到具有 INTERLEAVE IN PARENT 的表,但父行不存在,则导出将失败,并显示“父行缺失。Row cannot be written”错误。如果要将导出的行写入有外键限制条件的表,并且引用了不存在的键,则导出将失败并显示“违反外键限制条件”错误。导出到多个表时,建议按顺序导出,以确保在导出过程中保持引用完整性。这通常意味着先导出父表和被外键引用的表,然后再导出引用它们的表。

    如果导出目标表具有外键约束,或者属于另一个表的子表 (INTERLEAVE IN PARENT),则必须先填充父表,然后再导出子表,并且父表应包含所有相应的键。如果父表没有完整的一组相关键,导出子表的尝试将会失败。

  • BigQuery 作业(例如向 Spanner 提取数据的作业)的时长上限为 6 小时。如需了解如何优化大型提取作业,请参阅导出优化。或者,考虑将输入拆分为单独的数据块,这些数据块可以作为单独的提取作业导出。

  • 只有 BigQuery 企业版或企业 Plus 版支持导出到 Spanner。不支持 BigQuery 标准版和按需计算。

  • 您无法使用持续查询导出到具有自动生成的主键的 Spanner 表。

  • 您无法使用持续查询导出到 PostgreSQL 方言数据库中的 Spanner 表。

  • 使用持续查询导出到 Spanner 表时,请确保您选择的主键不与 BigQuery 表中单调递增的整数对应。否则可能会导致导出发生性能问题。如需了解 Spanner 中的主键以及缓解这些性能问题的方法,请参阅选择主键

使用 spanner_options 选项配置导出

您可以使用 spanner_options 选项指定目标 Spanner 数据库和表。配置以 JSON 字符串的形式表示,如以下示例所示:

EXPORT DATA OPTIONS(
   uri="https://spanner.googleapis.com/projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID",
  format='CLOUD_SPANNER',
   spanner_options = """{
      "table": "TABLE_NAME",
      "change_timestamp_column": "CHANGE_TIMESTAMP",
      "priority": "PRIORITY",
      "tag": "TAG",
   }"""
)

替换以下内容:

  • PROJECT_ID:您的 Google Cloud 项目的名称。
  • INSTANCE_ID:您的数据库实例的名称。
  • DATABASE_ID:您的数据库的名称。
  • TABLE_NAME:现有目标表的名称。
  • CHANGE_TIMESTAMP:目标 Spanner 表中 TIMESTAMP 类型列的名称。此选项在导出期间用于跟踪最新行更新的时间戳。指定此选项后,导出操作会先读取 Spanner 表中的相应行,以确保仅写入最新的行更新。建议您在运行持续导出时指定 TIMESTAMP 类型列,其中对具有相同主键的行的更改顺序很重要。
  • PRIORITY(可选):写入请求的优先级。允许使用的值:LOWMEDIUMHIGH。默认值:MEDIUM
  • TAG(可选):请求标记,可帮助识别 Spanner 监控中的导出器流量。默认值:bq_export

导出查询要求

如需将查询结果导出到 Spanner,结果必须满足以下要求:

  • 结果集中的所有列必须存在于目标表中,并且列的类型必须匹配或可转换
  • 结果集必须包含目标表的所有 NOT NULL 列。
  • 列值不得超过 Spanner 表中的数据大小限制
  • 必须先将任何不受支持的列类型转换为某种受支持的类型,然后再导出到 Spanner。

类型转换

为了方便使用,Spanner 导出器会自动应用以下类型转换:

BigQuery 类型 Spanner 类型
BIGNUMERIC NUMERIC(仅限 PostgreSQL 方言)
FLOAT64 FLOAT32
BYTES PROTO
INT64 ENUM

导出数据

您可以使用 EXPORT DATA 语句将数据从 BigQuery 表导出到 Spanner 表。

以下示例从名为 mydataset.table1 的表中导出选定的字段:

EXPORT DATA OPTIONS (
  uri="https://spanner.googleapis.com/projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID",
  format='CLOUD_SPANNER',
  spanner_options="""{ "table": "TABLE_NAME" }"""
)
AS SELECT * FROM mydataset.table1;

替换以下内容:

  • PROJECT_ID:您的 Google Cloud 项目的名称
  • INSTANCE_ID:您的数据库实例的名称
  • DATABASE_ID:您的数据库的名称
  • TABLE_NAME:现有目标表的名称

导出具有相同 rowkey 值的多个结果

当您导出包含具有相同 rowkey 值的多个行的结果时,写入 Spanner的值最终将位于同一个 Spanner行中。导出生成的 Spanner 行集中将仅包含单个匹配的 BigQuery 行(无法保证是哪一个)。

持续导出

如需持续处理导出查询,请参阅创建持续查询,获取相关说明和示例代码

导出优化

如需优化将记录从 BigQuery 导出到 Spanner 的过程,您可以尝试以下操作:

  • 在 Spanner 目标实例中增加节点数量。在导出操作的早期阶段,增加实例中的节点数量可能不会立即增加导出吞吐量。由于 Spanner 执行基于负载的拆分,因此可能会出现短暂延迟。使用基于负载的拆分时,导出吞吐量会增长并稳定下来。使用 EXPORT DATA 语句可批量处理数据,从而优化对 Spanner 的写入操作。如需了解详情,请参阅性能概览

  • spanner_options 中指定 HIGH 优先级。 如果您的 Spanner 实例已启用自动扩缩,设置 HIGH 优先级有助于确保 CPU 利用率达到触发扩缩所需的阈值。这样,自动扩缩器便可根据导出负载添加计算资源,从而提高整体导出吞吐量。

    以下示例展示了设置为 HIGH 优先级的 Spanner 导出命令:

    EXPORT DATA OPTIONS (
      uri="https://spanner.googleapis.com/projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID",
      format='CLOUD_SPANNER',
      spanner_options="""{ "table": "TABLE_NAME", "priority": "HIGH" }"""
    )
  • 避免对查询结果进行排序。如果结果集包含所有主键列,则导出器会自动对目标表的主键进行排序,以简化写入并最大限度地减少争用。

    如果目标表的主键包含生成的列,请将生成的列的表达式添加到查询中,以确保导出的数据正确排序和批处理。

    例如,在以下 Spanner 架构中,SaleYearSaleMonth 是构成 Spanner 主键开头的生成列:

    CREATE TABLE Sales (
      SaleId STRING(36) NOT NULL,
      ProductId INT64 NOT NULL,
      SaleTimestamp TIMESTAMP NOT NULL,
      Amount FLOAT64,
      -- Generated columns
      SaleYear INT64 AS (EXTRACT(YEAR FROM SaleTimestamp)) STORED,
      SaleMonth INT64 AS (EXTRACT(MONTH FROM SaleTimestamp)) STORED,
    ) PRIMARY KEY (SaleYear, SaleMonth, SaleId);

    将数据从 BigQuery 导出到主键中使用了生成列的 Spanner 表时,建议(但并非必须)在 EXPORT DATA 查询中包含这些生成列的表达式。这样一来,BigQuery 就可以正确地预先对数据进行排序,这对于高效地将数据分批写入 Spanner 至关重要。EXPORT DATA 语句中生成列的值不会在 Spanner 中提交,因为它们是由 Spanner 自动生成的,但它们可用于优化导出。

    以下示例将数据导出到主键使用生成列的 Spanner Sales 表。为了优化写入性能,该查询包含与生成的 SaleYearSaleMonth 列相匹配的 EXTRACT 表达式,以便 BigQuery 在导出之前对数据进行预排序:

    EXPORT DATA OPTIONS (
      uri="https://spanner.googleapis.com/projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID",
      format='CLOUD_SPANNER',
      spanner_options="""{ "table": "Sales" }"""
    )
    AS SELECT
      s.SaleId,
      s.ProductId,
      s.SaleTimestamp,
      s.Amount,
      -- Add expressions that match the generated columns in the Spanner PK
      EXTRACT(YEAR FROM s.SaleTimestamp) AS SaleYear,
      EXTRACT(MONTH FROM s.SaleTimestamp) AS SaleMonth
    FROM my_dataset.sales_export AS s;
  • 为防止作业长时间运行,请按分区导出数据。使用分区键(例如查询中的时间戳)对 BigQuery 数据进行分片:

    EXPORT DATA OPTIONS (
      uri="https://spanner.googleapis.com/projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID",
      format='CLOUD_SPANNER',
      spanner_options="""{ "table": "TABLE_NAME", "priority": "MEDIUM" }"""
    )
    AS SELECT *
    FROM 'mydataset.table1' d
    WHERE
    d.timestamp >= TIMESTAMP '2025-08-28T00:00:00Z' AND
    d.timestamp < TIMESTAMP '2025-08-29T00:00:00Z';

    这样一来,查询便可在 6 小时的作业运行时内完成。如需详细了解这些限制,请参阅查询作业限制

  • 为了提高数据加载性能,请在要导入数据的 Spanner 表中舍弃索引。然后,在导入完成后重新创建该 StatefulSet。

  • 我们建议您先使用一个 Spanner 节点(1,000 个处理器单元)和最少的 BigQuery slot 预留。例如,100 个槽,或者 0 个基准槽(但有自动扩缩功能)。对于小于 100 GB 的导出,此配置通常会在 6 小时的作业限制时间内完成。对于超过 100 GB 的导出,请根据需要增加 Spanner 节点和 BigQuery slot 预留,以提高吞吐量。吞吐量会随着节点数量的增加而增加,每个节点大约增加 5 MiB/s。

价格

当您使用 EXPORT DATA 语句将数据导出到 Spanner 时,您需要按照 BigQuery 容量计算价格付费。

如需使用持续查询将数据持续导出到 Spanner,您必须拥有 BigQuery 企业版或企业 Plus 版的槽预留,以及使用 CONTINUOUS 作业类型的预留分配

如果 BigQuery 导出到 Spanner 的数据跨越区域边界,则会按数据提取费率收费。如需了解详情,请参阅 BigQuery 价格。 为避免产生数据传输费用,请确保 BigQuery 导出作业在 Spanner 默认主节点所在的同一区域中运行。持续查询导出不支持跨区域边界的导出。

导出数据后,如果您将数据存储在 Spanner 中,则需要为此付费。如需了解详情,请参阅 Spanner 价格