Pular para conteúdo

Fase 3: Carga de Dados no Cloud SQL (MySQL 8.0)

A fase de carga (Load) exigiu o transporte dos dados que foram extraídos, higienizados e salvos em arquivos (Parquet/TSV) no Google Cloud Storage (Camada Bronze) para o banco de dados de destino no Cloud SQL (MySQL 8.0). O grande desafio desta fase foi garantir a performance massiva de inserção e, simultaneamente, preservar 100% da integridade de tipos de dados complexos (binários, datas inválidas e booleanos).

🛠️ Técnicas e Arquitetura Utilizadas

1. Arquitetura "Schema First, Data, then Constraints" Para otimizar a performance, a carga foi dividida em etapas rigorosas. Primeiro, as tabelas foram criadas "nuas" (sem chaves estrangeiras ou índices secundários). Inserir dados em tabelas sem constraints é ordens de grandeza mais rápido, pois o banco não precisa validar a integridade ou atualizar múltiplos índices a cada linha. As constraints (FKs, UKs) só foram aplicadas após a carga dos dados, atuando como uma validação de integridade em massa.

2. Ingestão em Massa (Bulk Loading) O método inicial DataFrame.to_sql() provou-se extremamente lento por converter os dados em milhares de INSERTs individuais. A técnica foi substituída pelo uso do comando LOAD DATA LOCAL INFILE e, para máxima performance, evoluiu para LOAD DATA FROM GOOGLE CLOUD STORAGE. Essa última técnica permite que o Cloud SQL puxe os arquivos TSV/CSV diretamente do bucket do GCS através da rede interna de alta velocidade do Google, eliminando a máquina cliente como gargalo de rede.

3. Pipeline em Memória com Polars e Apache Arrow Para transformar os Parquets em TSV compatíveis com o MySQL sem tocar no disco, utilizou-se o Polars. Os dados eram lidos em lotes (chunks) do formato Arrow, transformados em um buffer de memória (io.StringIO ou no sistema de arquivos virtual /dev/shm) e enviados diretamente via stream para o banco de dados.

4. Orquestração Paralela com Gerenciamento de Estado (SQLite) A carga das centenas de tabelas foi paralelizada utilizando ThreadPoolExecutor. Para garantir resiliência e permitir que o processo fosse retomado de onde parou em caso de falhas (idempotência), adotou-se o PipelineStateManager com um banco de dados SQLite para controlar os status (PENDING, RUNNING, SUCCESS, FAILED) de cada tabela e evitar condições de corrida (race conditions).


🚧 Problemas Enfrentados e 💡 Soluções Aplicadas

Durante a carga de dados legados no MySQL 8.0 estrito, dezenas de incompatibilidades técnicas vieram à tona.

Problema 1: Conversão Falha de Booleanos e Tipos BIT(1)

  • O Problema: O MySQL tentava inferir strings como "false" ou dados binários "b'\x00'" extraídos do Parquet e inseri-los em colunas BIT(1) ou TINYINT(1). O comando LOAD DATA avaliava o comprimento da string ou não reconhecia o valor numérico, convertendo tudo incorretamente para 1 (Verdadeiro) ou falhando.
  • A Solução: Em vez de usar funções pesadas de SET no MySQL, a limpeza foi empurrada para o Polars no Python. Os valores booleanos e bytes foram explicitamente convertidos em strings literais de escape \0 (Falso) e \1 (Verdadeiro) dentro do arquivo TSV gerado. O LOAD DATA consegue interpretar essas sequências nativamente como bytes, garantindo uma carga direta e ultrarrápida.

Problema 2: Dados Binários, PesSenhaCripto e Expansão de Encoding

  • O Problema: Colunas de senhas em sistemas antigos misturavam bytes brutos com texto. Ao migrar de latin1 para utf8mb4, ocorria uma "expansão de encoding" onde caracteres acentuados passavam a ocupar 2 bytes. O MySQL truncava esses dados no destino (Data truncated for column), corrompendo as senhas. Além disso, variáveis de ambiente no script de carga com espaços (ex: @c_Codigo Aluno) geravam erros de sintaxe (Erro 1064).
  • A Solução: Dados sensíveis/binários foram extraídos utilizando a função HEX() da origem e armazenados como texto seguro no Parquet. Na carga, o script gerava instruções com UNHEX() (ex: SET PesSenhaCripto = UNHEX(@c_PesSenhaCripto)). Adicionalmente, foi implementada uma sanitização que substituía espaços nos nomes das variáveis do MySQL por sublinhados (@c_Codigo_Aluno) para prevenir quebras de sintaxe no LOAD DATA.

Problema 3: "Data Zero" (0000-00-00) vs. Valores NULL

  • O Problema: Colunas de data/hora permitiam valores '0000-00-00' na origem, que são inválidos no MySQL 8.0 (Strict Mode). Além disso, o Pandas/Polars convertia valores vazios para strings vazias "" no TSV, que o banco falhava ao inserir, gerando erros de tipo de data ou inteiro incorreto.
  • A Solução: Ao gerar os arquivos TSV com o Polars, utilizou-se a propriedade explícita null_value='\\N' (na_rep no Pandas). Essa é a notação exata exigida pelo MySQL durante operações LOAD DATA para identificar um valor nulo legítimo. Para casos onde o campo era NOT NULL e continha "data zero", optou-se por aplicar um valor sentinela seguro, como '1900-01-01', diretamente no processamento SQL ou na camada bronze.

Problema 4: O AUTO_INCREMENT Ignorando o Valor 0

  • O Problema: Em tabelas legadas, algumas chaves primárias (PK) possuíam o valor explícito 0. Ao carregar isso via LOAD DATA, o comportamento padrão do MySQL ignorava o zero e gerava um novo ID automático na sequência (ex: 1), corrompendo as relações das chaves.
  • A Solução: O script Python foi modificado para enviar o comando SET SESSION sql_mode = CONCAT(@@sql_mode, ',NO_AUTO_VALUE_ON_ZERO') antes de rodar o LOAD DATA de cada tabela. Isso instruiu o servidor a respeitar o 0 literal como um valor de dado real.

Problema 5: Restrições de Segurança do LOCAL INFILE no GCP

  • O Problema: Durante a execução, o servidor disparava a falha Error 1017: Can't find file 'b'DUMMY'.
  • A Solução: O Cloud SQL possui bloqueios de segurança contra injeção de arquivos locais. Foi necessário acessar o Console do GCP e habilitar explicitamente a flag de banco de dados local_infile = On, autorizando a instância a aceitar arquivos de clientes via conexão remota.

Problema 6: Deadlocks na Aplicação de Constraints

  • O Problema: Na etapa final, ao orquestrar a recriação das Foreign Keys (Constraints) com múltiplos workers rodando comandos ALTER TABLE em paralelo, os metadados do banco geravam contenção de locks, causando o erro 1213: Deadlock found when trying to get lock.
  • A Solução: Operações DDL de constraints foram configuradas para rodar de forma puramente sequencial (--workers 1), evitando disputas pelas tabelas e permitindo que o MySQL fizesse o bloqueio referencial tabela por tabela, finalizando as dependências com sucesso e integridade.

Conclusão da Fase: O sucesso da carga ponta a ponta no Cloud SQL dependeu fundamentalmente de entender o ecossistema interno do MySQL 8.0, contornando a leniência da versão 5.1 de origem. A centralização de conversões pesadas na extração/em memória (\N, \0, HEX) aliada ao gerenciamento implacável de estado (SQLite) garantiu que 100% dos dados fossem importados de forma performática, atômica e validável.