Spanner para usuários do Cassandra

Este documento compara os conceitos e práticas do Apache Cassandra e do Spanner. Pressupo que você já conhece o Cassandra e quer migrar aplicativos existentes ou projetar novos usando o Spanner como seu 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 Adaptador de proxy do Cassandra para o Spanner. Essa ferramenta de código aberto permite migrar cargas de trabalho do Cassandra para o Spanner sem mudar a lógica do aplicativo.

Principais conceitos

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

Terminologia

Cassandra Spanner
Cluster Instância

O cluster do Cassandra é equivalente a uma instância do Spanner, uma coleção de servidores e recursos de armazenamento. Como o Spanner é um serviço gerenciado, você não precisa configurar o hardware ou o software subjacente. Você só precisa especificar a quantidade de nós que quer reservar para a instância ou escolher escalonamento automático para escalonar a instância automaticamente. Uma instância funciona como um contêiner para bancos de dados, e a topologia de replicação de dados (regional, birregional ou multirregional) é escolhida 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 funções). Ao contrário de um keyspace, você não precisa 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ções 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 a partição 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 é dividido em intervalos, o que significa que as linhas que são contíguas no espaço da chave primária também são contíguas no armazenamento (exceto nos limites de divisão). O Spanner divide e mescla os dados 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. Assim 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 divididos em intervalos. O Spanner pode ser considerado como tendo 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 serviço e processam 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 no uso da CPU e do disco. Os fragmentos são atribuídos a nós de computação para veiculação. Os dados são armazenados fisicamente no Colossus, o sistema de arquivos distribuídos do Google, separado dos nós de computação. Os drivers do cliente se conectam aos servidores front-end do Spanner, que executam o roteamento de solicitações e o balanceamento de carga. Para saber mais, consulte o artigo técnico Vida útil das leituras e gravações do Spanner.

Em um nível alto, as duas arquiteturas são dimensionadas à medida que os recursos são adicionados ao 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 lado negativo do particionamento baseado em intervalo é que as cargas de trabalho que gravam em uma extremidade do espaço da chave (por exemplo, tabelas codificadas por carimbo de data/hora atual) podem enfrentar problemas de ponto de acesso sem consideração adicional de design do esquema. Para mais informações sobre técnicas para superar pontos de acesso, consulte Práticas recomendadas de design 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 de quórum, a maioria do nó de réplica precisa responder ao nó do 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 com ele como se fosse um banco de dados de uma única máquina. Uma gravação é sempre gravada na maioria das réplicas antes de ser confirmada para o usuário. Todas as leituras subsequentes refletem os dados recém-gravados. Os aplicativos podem escolher ler um snapshot do banco de dados em um momento anterior, o que pode ter benefícios de desempenho em relação a leituras fortes. Para mais informações sobre as propriedades de 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 das colunas 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. O Spanner pode ser considerado como tendo apenas chaves de classificação, com partições 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 o Cassandra e o Spanner está nos nomes de tipo e na 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". No 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á uma chave de partição separada no Spanner. Os dados são armazenados classificados pela chave primária composta inteira.

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 mais informações 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 de 64 bits)

O Spanner oferece suporte a 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 (precisão 38 escala 9). 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, as colunas string precisam especificar o comprimento máximo. Isso não afeta o armazenamento, é apenas 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 a duração em nanossegundos.
time int64

O Spanner não oferece suporte a um tipo de dados dedicado de horário do dia. 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 digitados.
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 básicos de uso

Os exemplos de código abaixo 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, você cria um objeto cliente vinculado a um banco de dados específico e emite 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

As leituras no Spanner podem ser realizadas usando uma API de estilo chave-valor e uma API de consulta. Como usuário do Cassandra, você pode achar a API de consulta mais familiar. Uma diferença importante na API de consulta é que o Spanner exige argumentos nomeados (diferente 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. O Spanner é compatível com a DML e uma API de mutação de estilo 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

Esta seção contém informações sobre como usar 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 é usado para manipular a semântica de último escritor vence do 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 fornece uma interface 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. Crie um esquema com uma coluna de carimbo de data/hora de atualização explícita.

    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 à instrução 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. No Spanner, é possível usar mutações UPDATE condicionais 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 oferece suporte para 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 um esquema com uma coluna de carimbo de data/hora de atualização explícita

    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 tipos map que contêm uma pequena quantidade de dados no Spanner, use os tipos JSON ou PROTO, que permitem armazenar dados semiestruturados e estruturados, respectivamente. As atualizações nessas colunas exigem que o valor da coluna inteira seja gravado novamente. Se você tiver um caso de uso em que uma grande quantidade de dados for armazenada em um map do Cassandra e apenas uma pequena parte do map precisar ser atualizada, o uso de tabelas INTERLEAVED pode 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 a linha de usuário correspondente e pode ser recuperada e atualizada de maneira eficiente com a linha de usuário. É possível usar as APIs de leitura e gravação no Spanner para interagir com tabelas intercaladas. Para mais informações sobre a intercalação, consulte Criar tabelas principais e secundárias.

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 testes de unidade. O Spanner oferece um ambiente semelhante para desenvolvimento local usando o emulador do Spanner. O emulador oferece um ambiente de alta fidelidade para desenvolvimento interativo e testes de unidade. Para mais informações, consulte Emulação local do Spanner.

Linha de comando

O equivalente do Spanner ao nodetool do Cassandra é a CLI do Google Cloud. É possível realizar operações de plano de controle e de dados usando gcloud spanner. Para mais informações, consulte o guia de referência da CLI do Spanner 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 no 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.