Partitioned DML

Partitioned DML은 다음과 같은 유형의 일괄 업데이트와 삭제를 위해 설계되었습니다.

  • 정기적 클린업 및 가비지 컬렉션: 오래된 행을 삭제하거나 열을 NULL로 설정하는 작업 포함
  • 새 열을 기본값으로 백필: UPDATE 문을 사용하여 현재 NULL인 새 열의 값을 False로 설정하는 작업 포함

Partitioned DML은 소규모 트랜잭션 처리에 적합하지 않습니다. 일부 행에서 문을 실행하려면 식별 가능한 기본 키가 있는 트랜잭션 DML을 사용합니다. 자세한 내용은 DML 사용을 참조하세요.

블라인드 쓰기를 대량으로 커밋해야 하지만 원자적 트랜잭션이 필요하지 않은 경우 일괄 쓰기를 사용하여 Spanner 테이블을 일괄 수정할 수 있습니다. 자세한 내용은 일괄 쓰기를 사용하여 데이터 수정을 참조하세요.

Spanner 데이터베이스의 통계 테이블에서 활성 Partitioned DML 쿼리 및 진행 상황에 대한 통계를 확인할 수 있습니다. 자세한 내용은 활성 Partitioned DML 통계를 참조하세요.

DML 및 Partitioned DML

Spanner는 DML 문에 두 가지 실행 모드를 지원합니다.

  • DML은 트랜잭션 처리에 적합합니다. 자세한 내용은 DML 사용을 참조하세요.

  • Partitioned DML은 키 공간을 분할한 후 보다 작은 범위의 개별 트랜잭션으로 파티션에 대한 문을 실행하므로 동시 트랜잭션 처리에 미치는 영향을 최소화하면서 데이터베이스 전체에 대한 대규모 작업을 가능하게 합니다. 자세한 내용은 Partitioned DML 사용을 참조하세요.

다음 표에서는 두 가지 실행 모드의 주요 차이점 몇 가지를 보여줍니다.

DML Partitioned DML
WHERE 절과 일치하지 않는 행이 잠길 수 있습니다. WHERE 절과 일치하는 행만 잠깁니다.
트랜잭션 크기 제한이 적용됩니다. Spanner에서 트랜잭션 제한과 트랜잭션별 동시 실행 제한을 처리합니다.
문이 멱등성을 가질 필요가 없습니다. DML 문이 멱등성을 가져야 일관된 결과를 얻을 수 있습니다.
트랜잭션 하나에 DML 및 SQL 문이 여러 개 포함될 수 있습니다. 분할된 트랜잭션 하나에 DML 문이 하나만 포함될 수 있습니다.
문 복잡성에 대한 제한이 없습니다. 문이 완전히 분할 가능해야 합니다.
사용자가 클라이언트 코드로 읽기-쓰기 트랜잭션을 생성합니다. Spanner에서 트랜잭션을 만듭니다.

분할 가능성 및 멱등성

Partitioned DML 문이 실행될 때는 한 파티션에 있는 행에서 다른 파티션의 행에 액세스할 수 없으며 개발자가 Spanner에서 파티션을 만드는 방식을 선택할 수 없습니다. 파티션 나누기는 확장성을 보장하지만 한편으로는 Partitioned DML 문이 완전히 분할 가능해야 함을 의미하기도 합니다. 즉, Partitioned DML 문은 일련의 문이 결합된 형태로 표현될 수 있어야 하며, 이때 각 문은 테이블의 단일 행에만 액세스하고 다른 테이블에는 액세스하지 않아야 합니다. 예를 들어 여러 테이블에 액세스하거나 자체 조인을 수행하는 DML 문은 분할할 수 없습니다. DML 문을 분할할 수 없으면 Spanner에서 BadUsage 오류를 반환합니다.

다음 DML 문은 각 문을 테이블의 단일 행에 적용할 수 있으므로 완전히 분할 가능합니다.

UPDATE Singers SET LastName = NULL WHERE LastName = '';

DELETE FROM Albums WHERE MarketingBudget > 10000;

다음 DML 문은 여러 테이블에 액세스하므로 완전히 분할 가능하지 않습니다.

# Not fully partitionable
DELETE FROM Singers WHERE
SingerId NOT IN (SELECT SingerId FROM Concerts);

네트워크 수준 재시도로 인해 Spanner에서 일부 파티션에 대해 Partitioned DML 문을 여러 번 실행할 수도 있습니다. 즉 하나의 문이 하나의 행에서 두 번 이상 실행될 수 있습니다. 따라서 결과의 일관성을 위해서는 문이 멱등성을 가져야 합니다. 멱등성을 가진 문은 단일 행에 대해 여러 번 실행되어도 결과가 동일합니다.

다음 DML 문은 멱등성을 갖습니다.

UPDATE Singers SET MarketingBudget = 1000 WHERE true;

다음 DML 문은 멱등성을 갖지 않습니다.

UPDATE Singers SET MarketingBudget = 1.5 * MarketingBudget WHERE true;

행 잠금

Spanner는 행이 업데이트 후보나 삭제 후보인 경우에만 잠금을 얻습니다. 이 동작은 WHERE 절과 일치하지 않는 행에 읽기 잠금을 적용할 수 있는 DML 실행과는 다릅니다.

실행 및 트랜잭션

DML 문의 분할 여부는 실행을 위해 선택하는 클라이언트 라이브러리 메서드에 따라 다릅니다. 각 클라이언트 라이브러리는 DML 실행용 메서드와 Partitioned DML 실행용 메서드를 별개로 제공합니다.

클라이언트 라이브러리 메서드를 한 번 호출할 때 Partitioned DML 문을 하나만 실행할 수 있습니다.

Spanner는 Partitioned DML 문을 전체 테이블에 원자적으로 적용하지 않지만, 각 파티션에 대해서는 Partitioned DML 문을 원자적으로 적용합니다.

Partitioned DML은 커밋 또는 롤백을 지원하지 않습니다. Spanner는 DML 문을 즉시 실행하고 적용합니다.

  • 작업을 취소하면 Spanner는 실행 중인 파티션을 취소하며 나머지 파티션을 시작하지 않습니다. Spanner는 이미 실행된 파티션을 롤백하지 않습니다.
  • 문을 실행하여 오류가 발생하면 모든 파티션에 대해 실행이 중지되며 Spanner는 전체 작업의 해당 오류를 반환합니다. 오류의 예로는 데이터 유형 제약 조건 위반, UNIQUE INDEX 위반, ON DELETE NO ACTION 위반 등이 포함됩니다. 실행 실패 시점에 따라 일부 파티션에는 문이 성공적으로 실행되고 다른 파티션에는 문이 실행되지 않았을 수 있습니다.

Partitioned DML 문이 성공하면 Spanner에서 키 범위의 각 파티션에 대해 문을 최소 1회 이상 실행한 것입니다.

수정된 행의 개수

Partitioned DML 문은 수정된 행의 개수에 대한 하한값을 반환합니다. Spanner는 수정된 모든 행을 세지 못할 수도 있으므로 이 값은 수정된 행의 정확한 수가 아닐 수 있습니다.

트랜잭션 제한

Spanner에서 Partitioned DML 문을 실행하는 데 필요한 파티션과 트랜잭션을 만듭니다. 트랜잭션 제한이나 트랜잭션별 동시 실행 제한이 적용되지만 Spanner는 트랜잭션을 제한 이내로 유지하려고 합니다.

Spanner는 데이터베이스당 Partitioned DML 문을 최대 20,000개까지 동시에 실행할 수 있습니다.

지원되지 않는 기능

Spanner에서는 Partitioned DML의 일부 기능을 지원하지 않습니다.

  • INSERT는 지원되지 않습니다.
  • Google Cloud 콘솔: Google Cloud 콘솔에서는 Partitioned DML 문을 실행할 수 없습니다.
  • 쿼리 계획 및 프로파일링: Google Cloud CLI와 클라이언트 라이브러리에서는 쿼리 계획 및 프로파일링을 지원하지 않습니다.
  • 다른 테이블 또는 같은 테이블의 다른 행에서 읽는 서브 쿼리

테이블 이동, 테이블 간에 조인이 필요한 변환과 같은 복잡한 시나리오에서는 Dataflow 커넥터를 사용하는 것이 좋습니다.

Examples

다음 코드 예시에서는 Albums 테이블의 MarketingBudget 열을 업데이트합니다.

C++

ExecutePartitionedDml() 함수를 사용하여 Partitioned DML 문을 실행합니다.

void DmlPartitionedUpdate(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("UPDATE Albums SET MarketingBudget = 100000"
                            "  WHERE SingerId > 1"));
  if (!result) throw std::move(result).status();
  std::cout << "Updated at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_update]\n";
}

C#

ExecutePartitionedUpdateAsync() 메서드를 사용하여 Partitioned DML 문을 실행합니다.


using Google.Cloud.Spanner.Data;
using System;
using System.Threading.Tasks;

public class UpdateUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> UpdateUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        using var cmd = connection.CreateDmlCommand("UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) updated...");
        return rowCount;
    }
}

Go

PartitionedUpdate() 메서드를 사용하여 Partitioned DML 문을 실행합니다.


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func updateUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err
	}
	fmt.Fprintf(w, "%d record(s) updated.\n", rowCount)
	return nil
}

Java

executePartitionedUpdate() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

static void updateUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records updated.\n", rowCount);
}

Node.js

runPartitionedUpdate() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1',
  });
  console.log(`Successfully updated ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

executePartitionedUpdate() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

use Google\Cloud\Spanner\SpannerClient;

/**
 * Updates sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function update_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1'
    );

    printf('Updated %d row(s).' . PHP_EOL, $rowCount);
}

Python

execute_partitioned_dml() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml(
    "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

print("{} records updated.".format(row_ct))

Ruby

execute_partitioned_update() 메서드를 사용하여 Partitioned DML 문을 실행합니다.

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

puts "#{row_count} records updated."

다음 코드 예시에서는 SingerId 열을 기준으로 Singers 테이블에서 행을 삭제합니다.

C++

void DmlPartitionedDelete(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("DELETE FROM Singers WHERE SingerId > 10"));
  if (!result) throw std::move(result).status();
  std::cout << "Deleted at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_delete]\n";
}

C#


using Google.Cloud.Spanner.Data;
using System;
using System.Threading.Tasks;

public class DeleteUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> DeleteUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        using var cmd = connection.CreateDmlCommand("DELETE FROM Singers WHERE SingerId > 10");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) deleted...");
        return rowCount;
    }
}

Go


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func deleteUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "DELETE FROM Singers WHERE SingerId > 10"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err

	}
	fmt.Fprintf(w, "%d record(s) deleted.", rowCount)
	return nil
}

Java

static void deleteUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "DELETE FROM Singers WHERE SingerId > 10";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records deleted.\n", rowCount);
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'DELETE FROM Singers WHERE SingerId > 10',
  });
  console.log(`Successfully deleted ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Delete sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function delete_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'DELETE FROM Singers WHERE SingerId > 10'
    );

    printf('Deleted %d row(s).' . PHP_EOL, $rowCount);
}

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml("DELETE FROM Singers WHERE SingerId > 10")

print("{} record(s) deleted.".format(row_ct))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "DELETE FROM Singers WHERE SingerId > 10"
)

puts "#{row_count} records deleted."

다음 단계