Spanner para usuários do Cassandra

Este documento compara o Apache Cassandra e Conceitos e práticas do Spanner. Ele pressupõe que você esteja familiarizado com Cassandra e quiser migrar aplicativos existentes ou projetar novos aplicativos enquanto usa o Spanner como banco de dados.

O Cassandra e o Spanner são bancos de dados distribuídos em grande escala criados para aplicativos que exigem alta escalabilidade e baixa latência. Embora os dois bancos de dados possam oferecer suporte a cargas de trabalho NoSQL exigentes, o Spanner oferece recursos avançados para modelagem de dados, consultas e operações transacionais. Para mais informações sobre como o Spanner atende aos critérios de banco de dados NoSQL, consulte Spanner para cargas de trabalho não relacionais.

Migrar do Cassandra para o Spanner

Para migrar do Cassandra para o Spanner, use o Cassandra to Spanner Proxy Adapter. Com essa ferramenta de código aberto, é possível migrar cargas de trabalho do Cassandra ou do DataStax Enterprise (DSE) para o Spanner sem quaisquer mudanças na lógica do aplicativo.

Principais conceitos

Esta seção compara os principais conceitos do Cassandra e do Spanner.

Terminologia

Cassandra Spanner
Cluster Instância

Um cluster do Cassandra é equivalente a um Spanner instance: um conjunto de servidores e os recursos de armazenamento. Como o Spanner é um serviço gerenciado, você não precisa configurar o hardware ou o software subjacente. Você você só precisa especificar a quantidade de nós que quer reservar para sua instância ou o escalonamento automático para escalona a instância automaticamente. Uma instância atua como um contêiner bancos de dados e topologia de replicação de dados (regional, birregional ou multirregional) é escolhido no nível da instância.
Keyspace (em inglês) Banco de dados

Um keyspace do Cassandra é equivalente a um banco de dados Spanner, que é uma coleção de tabelas e outros elementos de esquema (por exemplo, índices e papéis). Ao contrário de um keyspace, não é preciso configurar o fator de replicação. O Spanner replica automaticamente seus dados para a região designada na instância.
Tabela Tabela

No Cassandra e no Spanner, as tabelas são uma coleção de linhas identificadas por uma chave primária especificada no esquema da tabela.
Partição Divisão

Tanto o Cassandra quanto o Spanner são dimensionados por fragmentação de dados. No Cassandra, cada fragmento é chamado de partição, enquanto no Spanner cada fragmento é chamado de divisão. O Cassandra usa partições de hash, o que significa que cada linha é atribuída de forma independente a um nó de armazenamento com base em um hash da chave primária. O Spanner é fragmentado por intervalo, ou seja, as linhas que são contíguos no espaço da chave primária também são contíguos no armazenamento (exceto limites de divisão). O Spanner cuida da divisão e da mesclagem com base na carga e no armazenamento, e isso é transparente para o aplicativo. A implicação principal é que, ao contrário do Cassandra, a verificação de intervalo em um prefixo da chave primária é uma operação eficiente no Spanner.
Row Linha

No Cassandra e no Spanner, uma linha é uma coleção de colunas identificada exclusivamente por uma chave primária. Como o Cassandra, o Spanner oferece suporte a chaves primárias compostas. Ao contrário do Cassandra, o Spanner não faz distinção entre chave de partição e chave de classificação, porque os dados são e fragmentados por intervalos. O Spanner tem apenas chaves de classificação, com o particionamento gerenciado em segundo plano.
Coluna Coluna

No Cassandra e no Spanner, uma coluna é um conjunto de valores de dados que têm o mesmo tipo. Há um valor para cada linha de uma tabela. Para mais informações sobre como comparar os tipos de coluna do Cassandra com o Spanner, consulte Tipos de dados.

Arquitetura

Um cluster do Cassandra consiste em um conjunto de servidores e armazenamentos colocalizados com esses servidores. Uma função hash mapeia linhas de um espaço de chave de partição para um nó virtual (vnode). Um conjunto de vnodes é atribuído aleatoriamente a cada servidor para veicular uma parte do keyspace do cluster. O armazenamento dos vnodes é anexado localmente ao nó de exibição. Os drivers de cliente se conectam diretamente aos nós de veiculação e e lidam com o balanceamento de carga e o roteamento de consultas.

Uma instância do Spanner consiste em um conjunto de servidores em uma topologia de replicação. O Spanner fragmenta dinamicamente cada tabela em intervalos de linhas com base em Uso da CPU e do disco. Os fragmentos são atribuídos a nós de computação para veiculação. Dados são armazenados fisicamente no Colossus, o sistema de arquivos distribuído do Google, separado do os nós de computação. Os drivers do cliente se conectam aos servidores de front-end do Spanner, que executam o roteamento de solicitações e o balanceamento de carga. Para saber mais, consulte a Ciclo de vida das leituras e dos recursos do Spanner escreve (em inglês).

Em alto nível, as duas arquiteturas são escalonadas à medida que os recursos são adicionados do cluster. A separação de computação e armazenamento do Spanner permite um rebalanceamento mais rápido da carga entre nós de computação em resposta a mudanças na carga de trabalho. Ao contrário do Cassandra, os movimentos de fragmento não envolvem a movimentação de dados, já que eles permanecem no Colossus. Além disso, o particionamento baseado em intervalo do Spanner pode ser mais natural para aplicativos que esperam que os dados sejam classificados por chave de partição. O outro lado da o particionamento baseado em intervalo é aquele em que as cargas de trabalho gravam em uma extremidade do espaço da chave (por exemplo, tabelas com chave de data/hora atual) podem enfrentar uso excessivo do ponto de acesso sem e considerações sobre o design do esquema. Para mais informações sobre técnicas uso excessivo do ponto de acesso, consulte as Práticas recomendadas de criação de esquema.

Consistência

No Cassandra, é necessário especificar um nível de consistência para cada operação. Se você usar o nível de consistência do quórum, a maioria do nó de réplica deve responder ao nó coordenador para que a operação seja considerada bem-sucedida. Se você usar um nível de consistência de um, o Cassandra precisará de um único nó de réplica para responder e a operação ser considerada bem-sucedida.

O Spanner oferece consistência forte. A API Spanner não expõe réplicas ao cliente. Os clientes do Spanner interagem como se fosse um banco de dados de máquina única. Uma gravação é são sempre gravados na maioria das réplicas antes de serem confirmados pelo usuário. Todas as leituras subsequentes refletem os dados recém-gravados. Os aplicativos podem optar por ler um snapshot do banco de dados de um momento no passado, o que pode ter benefícios de desempenho em comparação a leituras fortes. Para mais informações sobre a consistência do Spanner, consulte a Visão geral das transações.

O Spanner foi criado para oferecer a consistência e a disponibilidade necessárias em aplicativos de grande escala. O Spanner oferece consistência forte em grande escala e com alto desempenho. Para casos de uso que exigem isso, o Spanner oferece suporte a leituras de snapshots que relaxam os requisitos de atualização.

Modelagem de dados

Esta seção compara os modelos de dados do Cassandra e do Spanner.

Declaração de tabela

A sintaxe de declaração de tabela é bastante semelhante no Cassandra e no Spanner. Você especifica o nome da tabela, os nomes e tipos de coluna e a chave primária que identifica exclusivamente uma linha. A principal diferença é que o Cassandra é particionado por hash e faz uma distinção entre a chave de partição e a chave de classificação, enquanto o Spanner é particionado por intervalo. Podemos pensar que o Spanner tem apenas chaves de classificação, e partições são mantidas automaticamente em segundo plano. Assim como o Cassandra, o Spanner oferece suporte a chaves primárias compostas.

Parte única da chave primária

A diferença entre Cassandra e Spanner está nos nomes dos tipos e a localização da cláusula de chave primária.

Cassandra Spanner
CREATE TABLE users (
  user_id    bigint,
  first_name text,
  last_name  text,
  PRIMARY KEY (user_id)
)
    
CREATE TABLE users (
  user_id    int64,
  first_name string(max),
  last_name  string(max),
) PRIMARY KEY (user_id)
    

Várias partes da chave primária

No Cassandra, a primeira parte da chave primária é a "chave de partição", e as partes da chave primária subsequentes são "chaves de classificação". Para o Spanner, não há uma chave de partição separada. Os dados são armazenados classificados pela chave primária composta inteira.

Cassandra Spanner
CREATE TABLE user_items (
  user_id    bigint,
  item_id    bigint,
  first_name text,
  last_name  text,
  PRIMARY KEY (user_id, item_id)
)
    
CREATE TABLE user_items (
  user_id    int64,
  item_id    int64,
  first_name string(max),
  last_name  string(max),
) PRIMARY KEY (user_id, item_id)
    

Chave de partição composta

No Cassandra, as chaves de partição podem ser compostas. Não há partição separada no Spanner. Os dados são armazenados classificados por todo o grupo de chave primária.

Cassandra Spanner
CREATE TABLE user_category_items (
  user_id     bigint,
  category_id bigint,
  item_id     bigint,
  first_name  text,
  last_name   text,
  PRIMARY KEY ((user_id, category_id), item_id)
)
    
CREATE TABLE user_category_items (
  user_id     int64,
  category_id int64,
  item_id     int64,
  first_name  string(max),
  last_name   string(max),
) PRIMARY KEY (user_id, category_id, item_id)
    

Tipos de dados

Esta seção compara os tipos de dados do Cassandra e do Spanner. Para para saber mais sobre os tipos do Spanner, consulte Tipos de dados no GoogleSQL.

Cassandra Spanner
Tipos numéricos Números inteiros padrão:

bigint (número inteiro assinado de 64 bits)
int (número inteiro assinado de 32 bits)
smallint (número inteiro assinado de 16 bits)
tinyint (número inteiro assinado de 8 bits)
int64 (número inteiro assinado de 64 bits)

O Spanner aceita um único tipo de dados de 64 bits para números inteiros assinados.
Ponto flutuante padrão:

double (ponto flutuante IEEE-754 de 64 bits)
float (ponto flutuante IEEE-754 de 32 bits)
float64 (ponto flutuante IEEE-754 de 64 bits)
float32 (ponto flutuante IEEE-754 de 32 bits)
Números de precisão variável:

varint (número inteiro de precisão variável)
decimal (número decimal de precisão variável)
Para números decimais de precisão fixa, use numeric (escala 9 de precisão 38). Caso contrário, use string com uma biblioteca de precisão de variável de camada de aplicativo.
Tipos de string text
varchar
string(max)

text e varchar armazenam e validam strings UTF-8. No Spanner, string colunas precisam especificar o comprimento máximo (não há impacto sobre storage; isso é para fins de validação).
blob bytes(max)

Para armazenar dados binários, use o tipo de dados bytes.
Tipos de data e hora date date
duration int64

O Spanner não oferece suporte a um tipo de dados de duração dedicado. Use int64 para armazenar duração de nanossegundos.
time int64

O Spanner não oferece suporte a um tipo de dados de tempo no dia dedicado. Use int64 para armazenar o deslocamento de nanossegundos em um dia.
timestamp timestamp
Tipos de contêiner Tipos definidos pelo usuário json ou proto
list array

Use array para armazenar uma lista de objetos tipados.
map json ou proto

O Spanner não oferece suporte a um tipo de mapa dedicado. Use colunas json ou proto para representar mapas. Para mais informações, consulte Armazenar mapas grandes como tabelas intercaladas.
set array

O Spanner não oferece suporte a um tipo de conjunto dedicado. Use colunas array para representar set, com o aplicativo gerenciando a exclusividade do conjunto. Para mais informações, consulte Armazenar mapas grandes como tabelas intercaladas, que também pode ser usado para armazenar conjuntos grandes.

Padrões de uso básicos

Os exemplos de código a seguir mostram a diferença entre o código do cliente do Cassandra e do Spanner em Go. Para mais informações, consulte Bibliotecas de cliente do Spanner.

Inicialização do cliente

Em clientes do Cassandra, você cria um objeto de cluster que representa o cluster subjacente do Cassandra, instancia um objeto de sessão que abstrai uma conexão com o cluster e emite consultas na sessão. No Spanner, criar um objeto cliente vinculado a um banco de dados específico e emitir solicitações de banco de dados. no objeto cliente.

Exemplo do Cassandra

Go

import "github.com/gocql/gocql"

...

cluster := gocql.NewCluster("<address>")
cluster.Keyspace = "<keyspace>"
session, err := cluster.CreateSession()
if err != nil {
  return err
}
defer session.Close()

// session.Query(...)

Exemplo do Spanner

Go

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

...

client, err := spanner.NewClient(ctx,
    fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, database))
defer client.Close()

// client.Apply(...)

Ler dados

É possível fazer leituras no Spanner usando uma API estilo chave-valor e uma API de consulta. Como usuário do Cassandra, talvez você considere a API de consulta mais familiar. Um a principal diferença na API de consulta é que o Spanner exige argumentos nomeados, ao contrário dos argumentos posicionais ? no Cassandra. O nome de um argumento em uma consulta do Spanner precisa ter o prefixo @.

Exemplo do Cassandra

Go

stmt := `SELECT
           user_id, first_name, last_name
         FROM
           users
         WHERE
           user_id = ?`

var (
  userID    int
  firstName string
  lastName  string
)

err := session.Query(stmt, 1).Scan(&userID, &firstName, &lastName)

Exemplo do Spanner

Go

stmt := spanner.Statement{
  SQL: `SELECT
          user_id, first_name, last_name
        FROM
          users
        WHERE
          user_id = @user_id`,
  Params: map[string]any{"user_id": 1},
}

var (
  userID    int64
  firstName string
  lastName  string
)

err := client.Single().Query(ctx, stmt).Do(func(row *spanner.Row) error {
  return row.Columns(&userID, &firstName, &lastName)
})

Insira os dados

Um INSERT do Cassandra é equivalente a um INSERT OR UPDATE do Spanner. É necessário especificar a chave primária completa para uma inserção. Spanner. oferece suporte a DML e a uma API de mutação de estilo de chave-valor. A API de mutação de estilo chave-valor é recomendada para gravações triviais devido à latência mais baixa. A API Spanner DML tem mais recursos, já que oferece suporte à superfície SQL completa (incluindo o uso de expressões na instrução DML).

Exemplo do Cassandra

Go

stmt := `INSERT INTO
           users (user_id, first_name, last_name)
         VALUES
           (?, ?, ?)`
err := session.Query(stmt, 1, "John", "Doe").Exec()

Exemplo do Spanner

Go

_, err := client.Apply(ctx, []*spanner.Mutation{
  spanner.InsertOrUpdateMap(
    "users", map[string]any{
      "user_id":    1,
      "first_name": "John",
      "last_name":  "Doe",
    }
  )})

Inserir dados em lote

No Cassandra, é possível inserir várias linhas usando uma instrução de lote. No Spanner, uma operação de confirmação pode conter várias mutações. O Spanner insere essas mutações no banco de dados de forma atômica.

Exemplo do Cassandra

Go

stmt := `INSERT INTO
           users (user_id, first_name, last_name)
         VALUES
           (?, ?, ?)`
b := session.NewBatch(gocql.UnloggedBatch)
b.Entries = []gocql.BatchEntry{
  {Stmt: stmt, Args: []any{1, "John", "Doe"}},
  {Stmt: stmt, Args: []any{2, "Mary", "Poppins"}},
}
err = session.ExecuteBatch(b)

Exemplo do Spanner

Go

_, err := client.Apply(ctx, []*spanner.Mutation{
  spanner.InsertOrUpdateMap(
    "users", map[string]any{
       "user_id":    1,
       "first_name": "John",
       "last_name":  "Doe"
    },
  ),
  spanner.InsertOrUpdateMap(
    "users", map[string]any{
       "user_id":    2,
       "first_name": "Mary",
       "last_name":  "Poppins",
    },
  ),
})

Excluir dados

As exclusões do Cassandra exigem a especificação da chave primária das linhas a serem excluídas. Isso é semelhante à mutação DELETE no Spanner.

Exemplo do Cassandra

Go

stmt := `DELETE FROM
           users
         WHERE
           user_id = ?`
err := session.Query(stmt, 1).Exec()

Exemplo do Spanner

Go

_, err := client.Apply(ctx, []*spanner.Mutation{
  spanner.Delete("users", spanner.Key{1}),
})

Tópicos avançados

Nesta seção, você encontra informações sobre como usar os recursos mais avançados do Cassandra no Spanner.

Carimbo de data/hora de gravação

O Cassandra permite que as mutações especifiquem explicitamente um carimbo de data/hora de gravação para uma célula específica usando a cláusula USING TIMESTAMP. Normalmente, esse recurso é usada para manipular a semântica "last-writer-wins" da Cassandra.

O Spanner não permite que os clientes especifiquem o carimbo de data/hora de cada gravação. Cada célula é marcada internamente com o carimbo de data/hora TrueTime no momento em que o valor da célula foi confirmado. Como o Spanner oferece um conjunto de dados consistente e estritamente serializável, a maioria dos aplicativos não precisa da funcionalidade de USING TIMESTAMP.

Se você usa o USING TIMESTAMP do Cassandra para a lógica específica do aplicativo, adicione uma coluna TIMESTAMP extra ao esquema do Spanner, que pode rastrear o tempo de modificação no nível do aplicativo. As atualizações de uma linha podem ser agrupadas em uma transação de leitura e gravação. Exemplo:

Exemplo do Cassandra

Go

stmt := `INSERT INTO
           users (user_id, first_name, last_name)
         VALUES
           (?, ?, ?)
         USING TIMESTAMP
           ?`
err := session.Query(stmt, 1, "John", "Doe", ts).Exec()

Exemplo do Spanner

  1. Criar esquema com uma coluna explícita do carimbo de data/hora da atualização.

    GoogleSQL

    CREATE TABLE users (
      user_id    INT64,
      first_name STRING(MAX),
      last_name  STRING(MAX),
      update_ts  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
    ) PRIMARY KEY (user_id)
  2. Personalize a lógica para atualizar a linha e incluir um carimbo de data/hora.

    Go

    func ShouldUpdateRow(ctx context.Context, txn *spanner.ReadWriteTransaction, updateTs time.Time) (bool, error) {
      // Read the existing commit timestamp.
      row, err := txn.ReadRow(ctx, "users", spanner.Key{1}, []string{"update_ts"})
    
      // Treat non-existent row as NULL timestamp - the row should be updated.
      if spanner.ErrCode(err) == codes.NotFound {
        return true, nil
      }
    
      // Propagate unexpected errors.
      if err != nil {
        return false, err
      }
    
      // Check if the committed timestamp is newer than the update timestamp.
      var committedTs *time.Time
      err = row.Columns(&committedTs)
      if err != nil {
        return false, err
      }
      if committedTs != nil && committedTs.Before(updateTs) {
        return false, nil
      }
    
      // Committed timestamp is older than update timestamp - the row should be updated.
      return true, nil
    }
  3. Verifique a condição personalizada antes de atualizar a linha.

    Go

    _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
      // Check if the row should be updated.
      ok, err := ShouldUpdateRow(ctx, txn, time.Now())
      if err != nil {
        return err
      }
      if !ok {
        return nil
      }
    
      // Update the row.
      txn.BufferWrite([]*spanner.Mutation{
        spanner.InsertOrUpdateMap("users", map[string]any{
          "user_id":    1,
          "first_name": "John",
          "last_name":  "Doe",
          "update_ts":  spanner.CommitTimestamp,
        })})
    
      return nil
    })

Mutações condicionais

A instrução INSERT ... IF EXISTS no Cassandra é equivalente à INSERT. no Spanner. Em ambos os casos, a inserção falha se a linha já existir.

No Cassandra, também é possível criar instruções DML que especifiquem uma condição, e a instrução falhará se a condição for avaliada como falsa. Em Spanner, é possível usar UPDATE condicional em transações de leitura/gravação. Por exemplo, para atualizar uma linha somente se uma condição específica existir:

Exemplo do Cassandra

Go

stmt := `UPDATE
           users
         SET
           last_name = ?
         WHERE
           user_id = ?
         IF
           first_name = ?`
err := session.Query(stmt, 1, "Smith", "John").Exec()

Exemplo do Spanner

  1. Personalize a lógica para atualizar a linha e incluir uma condição.

    Go

    func ShouldUpdateRow(ctx context.Context, txn *spanner.ReadWriteTransaction) (bool, error) {
      row, err := txn.ReadRow(ctx, "users", spanner.Key{1}, []string{"first_name"})
      if err != nil {
        return false, err
      }
    
      var firstName *string
      err = row.Columns(&firstName)
      if err != nil {
        return false, err
      }
      if firstName != nil && firstName == "John" {
        return false, nil
      }
      return true, nil
    }
  2. Verifique a condição personalizada antes de atualizar a linha.

    Go

    _, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
      ok, err := ShouldUpdateRow(ctx, txn, time.Now())
      if err != nil {
        return err
      }
      if !ok {
        return nil
      }
    
      txn.BufferWrite([]*spanner.Mutation{
        spanner.InsertOrUpdateMap("users", map[string]any{
          "user_id":    1,
          "last_name":  "Smith",
          "update_ts":  spanner.CommitTimestamp,
        })})
    
      return nil
    })

TTL

O Cassandra é compatível com a definição de um valor de time to live (TTL) no nível da linha ou da coluna. No Spanner, o TTL é configurado no nível da linha, e você designa uma coluna nomeada como o tempo de expiração da linha. Para mais informações, consulte a Visão geral do Time to live (TTL).

Exemplo do Cassandra

Go

stmt := `INSERT INTO
           users (user_id, first_name, last_name)
         VALUES
           (?, ?, ?)
         USING TTL 86400
           ?`
err := session.Query(stmt, 1, "John", "Doe", ts).Exec()

Exemplo do Spanner

  1. Criar esquema com uma coluna explícita de carimbo de data/hora da atualização

    GoogleSQL

    CREATE TABLE users (
      user_id    INT64,
      first_name STRING(MAX),
      last_name  STRING(MAX),
      update_ts  TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
    ) PRIMARY KEY (user_id),
      ROW DELETION POLICY (OLDER_THAN(update_ts, INTERVAL 1 DAY));
  2. Insira linhas com um carimbo de data/hora de confirmação.

    Go

    _, err := client.Apply(ctx, []*spanner.Mutation{
      spanner.InsertOrUpdateMap("users", map[string]any{
                  "user_id":    1,
                  "first_name": "John",
                  "last_name":  "Doe",
                  "update_ts":  spanner.CommitTimestamp}),
    })

Armazene mapas grandes como tabelas intercaladas.

O Cassandra oferece suporte ao tipo map para armazenar pares de chave-valor ordenados. Para armazenar map que contêm uma pequena quantidade de dados no Spanner, você pode usar o JSON ou o PROTO que permitem armazenar dados estruturados e semiestruturados, respectivamente. As atualizações nessas colunas exigem a regravação de todo o valor da coluna. Se você tem um caso de uso em que uma grande quantidade de dados é armazenada em um map do Cassandra; e apenas uma pequena parte do map precisa ser atualizada, usando INTERLEAVED tabelas podem ser uma boa opção. Por exemplo, para associar uma grande quantidade de dados de chave-valor a um usuário específico:

Exemplo do Cassandra

CREATE TABLE users (
  user_id     bigint,
  attachments map<string, string>,
  PRIMARY KEY (user_id)
)

Exemplo do Spanner

CREATE TABLE users (
  user_id  INT64,
) PRIMARY KEY (user_id);

CREATE TABLE user_attachments (
  user_id        INT64,
  attachment_key STRING(MAX),
  attachment_val STRING(MAX),
) PRIMARY KEY (user_id, attachment_key);

Nesse caso, uma linha de anexos do usuário é armazenada junto com o linha de usuário e pode ser recuperada e atualizada de forma eficiente junto com a linha de usuário. É possível usar o método read-write APIs no Spanner para interagir com tabelas intercaladas. Para mais informações sobre intercalação, consulte Crie tabelas mãe e filha.

Experiência do desenvolvedor

Esta seção compara as ferramentas para desenvolvedores do Spanner e do Cassandra.

Desenvolvimento local

É possível executar o Cassandra localmente para desenvolvimento e teste de unidade. O Spanner oferece um ambiente semelhante para desenvolvimento local usando o emulador do Spanner. O emulador oferece um alto de fidelidade para desenvolvimento interativo e testes de unidade. Para mais para mais informações, consulte Emular o Spanner localmente.

Linha de comando

O equivalente no Spanner ao nodetool do Cassandra é Google Cloud CLI. É possível realizar operações de plano de controle e de dados usando gcloud spanner. Para mais informações, consulte a Guia de referência do Spanner da CLI do Google Cloud.

Se você precisar de uma interface REPL para emitir consultas ao Spanner semelhantes a cqlsh, use a ferramenta spanner-cli. Para instalar e executar spanner-cli em Go:

go install github.com/cloudspannerecosystem/spanner-cli@latest

$(go env GOPATH)/bin/spanner-cli

Para mais informações, consulte o repositório spanner-cli do GitHub.