Linguagem de manipulação de dados particionados

A linguagem de manipulação de dados particionada (DML particionada) foi concebida para os seguintes tipos de atualizações e eliminações em massa:

  • Limpeza periódica e recolha de lixo. Exemplos: eliminar linhas antigas ou definir colunas como NULL.
  • Preencher novas colunas com valores predefinidos. Um exemplo é usar uma declaração UPDATE para definir o valor de uma nova coluna como False, quando atualmente é NULL.

A DML particionada não é adequada para o processamento de transações em pequena escala. Se quiser executar uma declaração em algumas linhas, use DMLs transacionais com chaves primárias identificáveis. Para mais informações, consulte o artigo Usar DML.

Se precisar de confirmar um grande número de escritas cegas, mas não precisar de uma transação atómica, pode modificar em massa as tabelas do Spanner através da escrita em lote. Para mais informações, consulte o artigo Modifique dados através de gravações em lote.

Pode obter estatísticas sobre as consultas DML particionadas ativas e o respetivo progresso a partir das tabelas de estatísticas na sua base de dados do Spanner. Para mais informações, consulte o artigo Estatísticas de DMLs particionadas ativas.

DML e DML particionada

O Spanner suporta dois modos de execução para declarações DML:

  • DML, que é adequado para o processamento de transações. Para mais informações, consulte o artigo Usar DML.

  • DML particionada, que permite operações em grande escala em toda a base de dados com um impacto mínimo no processamento de transações simultâneas, através da partição do espaço de chaves e da execução da declaração em partições em transações separadas de âmbito mais reduzido. Para mais informações, consulte o artigo Usar DML particionada.

A tabela seguinte realça algumas das diferenças entre os dois modos de execução.

DML DML particionada
As linhas que não correspondem à cláusula WHERE podem estar bloqueadas. Apenas as linhas que correspondem à cláusula WHERE são bloqueadas.
Aplicam-se limites de tamanho das transações. O Spanner processa os limites de transações e os limites de concorrência por transação.
As declarações não têm de ser idempotentes. Uma instrução DML tem de ser idempotente para garantir resultados consistentes.
Uma transação pode incluir várias declarações DML e SQL. Uma transação particionada só pode incluir uma declaração DML.
Não existem restrições quanto à complexidade das declarações. As declarações têm de ser totalmente divisíveis.
Cria transações de leitura/escrita no seu código de cliente. O Spanner cria as transações.

Particionável e idempotente

Quando uma declaração DML particionada é executada, as linhas numa partição não têm acesso a linhas noutras partições, e não pode escolher como o Spanner cria as partições. A partição garante a escalabilidade, mas também significa que as declarações DML particionadas têm de ser totalmente particionáveis. Ou seja, a declaração DML particionada tem de ser expressa como a união de um conjunto de declarações, em que cada declaração acede a uma única linha da tabela e cada declaração não acede a outras tabelas. Por exemplo, uma declaração DML que acede a várias tabelas ou executa uma junção automática não é divisível em partições. Se a declaração DML não for particionável, o Spanner devolve o erro BadUsage.

Estas declarações DML são totalmente divisíveis, porque cada declaração pode ser aplicada a uma única linha na tabela:

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

DELETE FROM Albums WHERE MarketingBudget > 10000;

Esta declaração DML não é totalmente particionável, porque acede a várias tabelas:

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

O Spanner pode executar uma declaração DML particionada várias vezes em algumas partições devido a novas tentativas ao nível da rede. Como resultado, uma declaração pode ser executada mais do que uma vez numa linha. Por conseguinte, a declaração tem de ser idempotente para gerar resultados consistentes. Uma declaração é idempotente se a execução da mesma várias vezes numa única linha produzir o mesmo resultado.

Esta instrução DML é idempotente:

UPDATE Singers SET MarketingBudget = 1000 WHERE true;

Esta instrução DML não é idempotente:

UPDATE Singers SET MarketingBudget = 1.5 * MarketingBudget WHERE true;

Elimine linhas de tabelas principais com tabelas secundárias indexadas

Quando usa uma declaração DML particionada para eliminar linhas numa tabela principal, a operação pode falhar com o erro: The transaction contains too many mutations. Isto ocorre se a tabela principal tiver tabelas secundárias intercaladas que contenham um índice global. As mutações nas linhas da tabela secundária em si não são contabilizadas para o limite de mutações da transação. No entanto, as mutações correspondentes às entradas do índice são contabilizadas. Se um grande número de entradas de índice da tabela secundária for afetado, a transação pode exceder o limite de mutação.

Para evitar este erro, elimine as linhas em duas declarações DML particionadas separadas:

  1. Executar uma eliminação particionada nas tabelas secundárias.
  2. Executar uma eliminação particionada na tabela principal.

Este processo de dois passos ajuda a manter a contagem de mutações dentro dos limites permitidos para cada transação. Em alternativa, pode eliminar o índice global na tabela secundária antes de eliminar as linhas principais.

Bloqueio de linhas

O Spanner adquire um bloqueio apenas se uma linha for candidata a atualização ou eliminação. Este comportamento é diferente da execução de DML, que pode bloquear a leitura de linhas que não correspondem à cláusula WHERE.

Execução e transações

Se uma declaração DML é particionada ou não, depende do método da biblioteca do cliente que escolher para execução. Cada biblioteca de cliente fornece métodos separados para execução de DML e execução de DML particionada.

Só pode executar uma declaração DML particionada numa chamada ao método da biblioteca do cliente.

O Spanner não aplica as declarações DML particionadas de forma atómica em toda a tabela. No entanto, o Spanner aplica declarações DML particionadas de forma atómica em cada partição.

O DML particionado não suporta a confirmação nem a reversão. O Spanner executa e aplica a declaração DML imediatamente.

  • Se cancelar a operação, o Spanner cancela as partições em execução e não inicia as partições restantes. O Spanner não reverte nenhuma partição que já tenha sido executada.
  • Se a execução da declaração causar um erro, a execução é interrompida em todas as partições e o Spanner devolve esse erro para toda a operação. Alguns exemplos de erros são violações de restrições de tipo de dados, violações de UNIQUE INDEX e violações de ON DELETE NO ACTION. Consoante o momento em que a execução falhou, a declaração pode ter sido executada com êxito em algumas partições e pode nunca ter sido executada noutras partições.

Se a declaração DML particionada for bem-sucedida, o Spanner executou a declaração, pelo menos, uma vez em cada partição do intervalo de chaves.

Contagem de linhas modificadas

Uma declaração DML particionada devolve um limite inferior no número de linhas modificadas. Pode não ser uma contagem exata do número de linhas modificadas, porque não há garantia de que o Spanner conte todas as linhas modificadas.

Limites de transação

O Spanner cria as partições e as transações de que precisa para executar uma declaração DML particionada. Aplicam-se limites de transação ou limites de concorrência por transação, mas o Spanner tenta manter as transações dentro dos limites.

O Spanner permite um máximo de 20 000 declarações DML particionadas simultâneas por base de dados.

Funcionalidades não suportadas

O Spanner não suporta algumas funcionalidades para DML particionada:

  • O INSERT não é suportado.
  • Google Cloud consola: não pode executar declarações DML particionadas na Google Cloud consola.
  • Planos de consulta e criação de perfis: a CLI gcloud e as bibliotecas de cliente não suportam planos de consulta nem criação de perfis.
  • Subconsultas que leem de outra tabela ou de uma linha diferente da mesma tabela.

Para cenários complexos, como mover uma tabela ou transformações que requerem junções entre tabelas, pondere usar o conetor do Dataflow.

Exemplos

O exemplo de código seguinte atualiza a coluna MarketingBudget da tabela Albums.

C++

Use a função ExecutePartitionedDml() para executar uma declaração DML particionada.

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#

Usa o método ExecutePartitionedUpdateAsync() para executar uma declaração DML particionada.


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

Usa o método PartitionedUpdate() para executar uma declaração DML particionada.


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

Usa o método executePartitionedUpdate() para executar uma declaração DML particionada.

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

Usa o método runPartitionedUpdate() para executar uma declaração DML particionada.

// 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

Usa o método executePartitionedUpdate() para executar uma declaração DML particionada.

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

Usa o método execute_partitioned_dml() para executar uma declaração DML particionada.

# 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

Usa o método execute_partitioned_update() para executar uma declaração DML particionada.

# 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."

O exemplo de código seguinte elimina linhas da tabela Singers com base na coluna SingerId.

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."

O que se segue?