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 colunasBIT(1)ouTINYINT(1). O comandoLOAD DATAavaliava o comprimento da string ou não reconhecia o valor numérico, convertendo tudo incorretamente para1(Verdadeiro) ou falhando. - A Solução: Em vez de usar funções pesadas de
SETno 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. OLOAD DATAconsegue 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
latin1parautf8mb4, 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 comUNHEX()(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 noLOAD 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çõesLOAD DATApara identificar um valor nulo legítimo. Para casos onde o campo eraNOT NULLe 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 viaLOAD 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 oLOAD DATAde cada tabela. Isso instruiu o servidor a respeitar o0literal 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 TABLEem paralelo, os metadados do banco geravam contenção de locks, causando o erro1213: 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.