比较 DML 和变更

数据操纵语言 (DML) 和变更是 Spanner 中的两个 API,可用于修改数据。它们各自提供类似的数据操纵特性。本页面对两种方法进行了比较。

什么是数据操纵语言 (DML)?

借助 Spanner 中的数据操纵语言 (DML),您可以使用 INSERTUPDATEDELETE 语句操控数据库表中的数据。您可以使用客户端库Google Cloud 控制台gcloud spanner 运行 DML 语句。

Spanner 提供以下两种 DML 执行实现,每种实现具有不同的属性。

  • 标准 DML - 适用于标准联机事务处理 (OLTP) 工作负载。

    如需了解详情(包括代码示例),请参阅使用 DML

  • 分区 DML - 专为批量更新和删除而设计,如以下示例所示。

    • 定期清理和垃圾回收。例如,删除旧行或将列设置为 NULL。

    • 使用默认值回填新列。例如,使用 UPDATE 语句将新列的值为 NULL 的值设置为 False。

    如需了解详情(包括代码示例),请参阅使用分区 DML

    您可以将批量写入用于大量写入操作,而无需执行不需要原子事务的读取操作。如需了解详情,请参阅使用批量写入修改数据

什么是变更?

变更表示插入、更新和删除等一系列操作,Spanner 会以原子方式应用于数据库中的不同行和表。您可以在变更中添加应用于不同行或不同表的操作。在定义一个或多个变更(其中包含一个或多个写入)之后,必须应用该更改来提交写入。每项更改均按照它们添加到更改的顺序进行应用。

如需了解详情(包括代码示例),请参阅使用变更插入、更新和删除数据

DML 与变更之间的特性比较

下表汇总了常见数据库操作和特性的 DML 和变更支持。

运维 DML 变更
插入数据 受支持 受支持
删除数据 支持 受支持
更新数据 受支持 受支持
插入或忽略数据 受支持 不受支持
读己所写 (RYW) 受支持 不受支持
插入或更新数据(更新/插入) 受支持 受支持
SQL 语法 受支持 不受支持
限制条件检查 在每个语句之后 提交时

DML 和变更对以下功能的支持有所不同:

  • 读取您的写入:读取活跃事务中未提交的结果。使用 DML 语句所做的更改对同一事务中的后续语句可见。这与使用变更不同,使用变更所做的更改在事务提交之前在任何读取(包括在同一事务中完成的读取)中都不可见。这是因为事务中的变更会在客户端(本地)缓冲,并在执行提交操作的过程中发送到服务器。因此,提交请求中的变更对同一事务中的 SQL 或 DML 语句不可见。

  • 约束检查:Spanner 会在每个 DML 语句之后检查约束。这与使用变更不同,在变更后,Spanner 会在提交之前缓冲客户端中的变更,并在提交时检查限制条件。在每个 DML 语句之后评估限制条件后,Spanner 可以保证同一事务中的后续查询返回的数据返回与架构一致的数据。

  • SQL 语法:DML 提供了一种处理数据的传统方法。您可以重复使用 SQL 技能,以便使用 DML API 更改数据。

最佳做法 - 避免在同一事务中混用 DML 和变更

如果事务在提交请求中同时包含 DML 语句和变更,则 Spanner 会在变更之前执行 DML 语句。为了避免在客户端库代码中考虑执行顺序,您应该在单个事务中使用 DML 语句或变更,但不要同时使用两者。

以下 Java 示例说明了可能会出现的意外行为。代码使用 Mutation API 将两行插入 Albums。然后,代码段调用 executeUpdate() 来更新新插入的行,并调用 executeQuery() 来读取更新后的 Albums。

static void updateMarketingBudget(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(
          new TransactionCallable<Void>() {
            @Override
            public Void run(TransactionContext transaction) throws Exception {
               transaction.buffer(
                    Mutation.newInsertBuilder("Albums")
                        .set("SingerId")
                        .to(1)
                        .set("AlbumId")
                        .to(1)
                        .set("AlbumTitle")
                        .to("Total Junk")
                        .set("MarketingBudget")
                        .to(800)
                        .build());
               transaction.buffer(
                    Mutation.newInsertBuilder("Albums")
                        .set("SingerId")
                        .to(1)
                        .set("AlbumId")
                        .to(2)
                        .set("AlbumTitle")
                        .to("Go Go Go")
                        .set("MarketingBudget")
                        .to(200)
                        .build());

                // This UPDATE will not include the Albums inserted above.
                String sql =
                  "UPDATE Albums SET MarketingBudget = MarketingBudget * 2"
                      + " WHERE SingerId = 1";
                long rowCount = transaction.executeUpdate(Statement.of(sql));
                System.out.printf("%d records updated.\n", rowCount);

                // Read a newly updated record.
                sql =
                  "SELECT SingerId, AlbumId, AlbumTitle FROM Albums"
                      + " WHERE SingerId = 1 AND MarketingBudget < 1000";
                ResultSet resultSet =
                                 transaction.executeQuery(Statement.of(sql));
                while (resultSet.next()) {
                   System.out.printf(
                        "%s %s\n",
                        resultSet.getString("FirstName"),
                        resultSet.getString("LastName"));
                }
                return null;
              }
            });
}

如果执行此代码,则会看到 0 条记录已更新。为什么?之所以发生这种情况,是因为我们使用变更所做的更改在事务提交之前对后续语句不可见。理想情况下,应仅在事务的最后缓冲写入。

后续步骤