BigQuery tem três camadas de recurso Terraform: dataset (container lógico), table (dados) e reservation (slots de compute). IAM é aplicado no nível do dataset ou da tabela individualmente.

Dataset

resource "google_bigquery_dataset" "raw" {
  dataset_id  = "${replace(local.prefixo, "-", "_")}_raw"
  location    = var.regiao
  description = "Dados brutos ingeridos sem transformação"
 
  labels = local.labels
 
  delete_contents_on_destroy = var.ambiente == "dev" ? true : false
 
  default_table_expiration_ms = var.ambiente == "dev" ? 2592000000 : null  # 30d em dev
 
  depends_on = [google_project_service.apis]
}

dataset_id aceita apenas letras, números e underscores. Hífens no prefixo precisam ser substituídos com replace.

delete_contents_on_destroy: quando false, o terraform destroy falha se o dataset tiver tabelas. É um guard contra destruição acidental. Use true em dev para facilitar o ciclo de testes e false em prod.

default_table_expiration_ms: tabelas criadas no dataset expiram automaticamente após N milissegundos. Útil em dev para limpeza automática; nunca use em prod para dados persistentes.

Tabela com schema

resource "google_bigquery_table" "pedidos" {
  dataset_id = google_bigquery_dataset.raw.dataset_id
  table_id   = "pedidos"
  labels     = local.labels
 
  schema = file("${path.module}/schemas/pedidos.json")
 
  time_partitioning {
    type  = "DAY"         # DAY, HOUR, MONTH, YEAR
    field = "data_pedido" # coluna DATE ou TIMESTAMP; null = _PARTITIONTIME (ingest time)
  }
 
  clustering = ["regiao", "status"]  # até 4 colunas; ordem importa para pruning
 
  deletion_protection = var.ambiente == "prod" ? true : false
}
// schemas/pedidos.json
[
  { "name": "id",          "type": "STRING",    "mode": "REQUIRED" },
  { "name": "data_pedido", "type": "TIMESTAMP", "mode": "REQUIRED" },
  { "name": "regiao",      "type": "STRING",    "mode": "NULLABLE" },
  { "name": "status",      "type": "STRING",    "mode": "NULLABLE" },
  { "name": "valor",       "type": "NUMERIC",   "mode": "NULLABLE" }
]

Tipos de coluna

TipoEquivalente SQLObservação
STRINGVARCHARUTF-8, sem limite de tamanho
INTEGERBIGINT64 bits com sinal
FLOATFLOAT64Ponto flutuante; use NUMERIC para finanças
NUMERICDECIMAL(29,9)Precisão fixa; ideal para valores monetários
BIGNUMERICDECIMAL(76,38)Para cálculos de alta precisão
BOOLEANBOOL
TIMESTAMPTIMESTAMP WITH TIME ZONEUTC internamente
DATEDATESem hora
RECORDSTRUCTTipo aninhado (objeto JSON)

Modos: REQUIRED (NOT NULL), NULLABLE (padrão), REPEATED (equivalente a array).

time_partitioning.type

ValorPartição porQuando usar
HOURHoraStreams de alta frequência (Pub/Sub, Dataflow)
DAYDiaMaioria dos casos (ingestão batch, eventos diários)
MONTHMêsTabelas consultadas sempre com filtro mensal
YEARAnoHistórico com acesso muito esparso

Sempre filtre pela coluna de partição no WHERE. Queries sem filtro de partição varrem a tabela inteira, gerando custo e lentidão proporcionais ao volume total.

deletion_protection

Impede que terraform destroy delete a tabela. Para destruir intencionalmente, é necessário setar deletion_protection = false, aplicar, e então destruir. Use true em prod como padrão.

IAM no dataset

resource "google_bigquery_dataset_iam_binding" "readers" {
  dataset_id = google_bigquery_dataset.raw.dataset_id
  role       = "roles/bigquery.dataViewer"
 
  members = [
    "serviceAccount:${google_service_account.pipeline.email}",
    "group:[email protected]",
  ]
}

Roles principais do BigQuery:

RoleO que permite
roles/bigquery.dataViewerSELECT em tabelas do dataset
roles/bigquery.dataEditorSELECT + INSERT + UPDATE + DELETE
roles/bigquery.dataOwnerControle total do dataset (incluindo delete)
roles/bigquery.jobUserExecutar jobs (queries) no projeto
roles/bigquery.userjobUser + acesso a metadados do projeto

jobUser e dataViewer são complementares e precisam ser concedidos separadamente. Uma SA que lê dados de outro projeto precisa de dataViewer no projeto do dataset e jobUser no projeto que executa a query.

Reserva de slots (Capacity Edition)

Em prod com carga previsível, comprometer slots via reservation reduz custo em relação ao on-demand.

# Commitment: compra slots por um período (lock-in com desconto)
resource "google_bigquery_capacity_commitment" "slots" {
  count = var.ambiente == "prod" ? 1 : 0
 
  location   = var.regiao
  plan       = "MONTHLY"    # FLEX (60s), MONTHLY (30d), ANNUAL (365d)
  slot_count = 100
  edition    = var.bigquery_edition  # STANDARD, ENTERPRISE, ENTERPRISE_PLUS
}
 
# Reservation: agrupa slots para alocar a projetos específicos
resource "google_bigquery_reservation" "principal" {
  count = var.ambiente == "prod" ? 1 : 0
 
  name          = "${local.prefixo}-reserva"
  location      = var.regiao
  slot_capacity = 100
  edition       = var.bigquery_edition
}
 
# Assignment: vincula a reservation a um projeto
resource "google_bigquery_reservation_assignment" "projeto" {
  count = var.ambiente == "prod" ? 1 : 0
 
  assignee    = "projects/${var.projeto_id}"
  job_type    = "QUERY"   # QUERY, PIPELINE (Dataflow), ML_EXTERNAL
  reservation = google_bigquery_reservation.principal[0].id
}

Edições e diferenças

EdiçãoDestaquesMínimo de slots
STANDARDSem BI Engine incluso, sem CMEK multi-region100
ENTERPRISEBI Engine incluso, CMEK, time travel até 7d100
ENTERPRISE_PLUSTudo do Enterprise + failsafe (14d de recuperação)100

Planos de commitment

PlanoLock-inDesconto aproximado vs on-demand
FLEX60 segundosNenhum
MONTHLY30 dias~20%
ANNUAL365 dias~40%

Em dev nunca use commitment. Em prod com carga estável, ANNUAL paga em ~7 meses o custo equivalente em on-demand.

Habilitando as APIs necessárias

resource "google_project_service" "bigquery_apis" {
  for_each = toset([
    "bigquery.googleapis.com",
    "bigqueryreservation.googleapis.com",   # para slots e reservations
    "bigqueryconnection.googleapis.com",    # para conexões externas (Cloud SQL, GCS via BQML)
    "bigquerystorage.googleapis.com",       # para Storage Read API (leitura rápida via gRPC)
  ])
 
  service            = each.value
  disable_on_destroy = false
}

Quotas e limites relevantes

  • Queries simultâneas por projeto (on-demand): 300
  • Queries simultâneas com reservation: limitado pelos slots contratados (1 slot sustenta ~1 query simples; queries complexas usam centenas)
  • DML (UPDATE/DELETE/MERGE) por tabela por dia: 1.500 para tabelas particionadas, 48 para não-particionadas
  • Ingestão via streaming: 1 GB/s por projeto; 10 MB por linha; 50.000 linhas por segundo por tabela
  • CREATE TABLE por dia por projeto: 10.000
  • Datasets por projeto: 10.000
  • Tabelas por dataset: 1.000.000 (listagem fica lenta acima de alguns milhares)
  • Colunas por tabela: 10.000
  • Retenção de metadados (INFORMATION_SCHEMA.JOBS): 180 dias

Tabelas não-particionadas com DML intenso atingem o limite de 48 operações/dia rapidamente. Toda tabela que recebe UPDATE, DELETE ou MERGE recorrente deve ser particionada.

Conexões

Ver também: gcp-bigquery | gcp-bigquery-cobranca | gcp-bigquery-otimizacao | terraform-gcp-ambientes | terraform-gcp-buckets | terraform-gcp-looker | terraform-cloud-gcp