C-tree Avançado – Otimização, Travas de Registro e a Madrugada da Reindexação
Um mergulho técnico nos segredos do C-tree, detalhando otimizações de índice, o gerenciamento de concorrência com travas de registro e o ritual noturno de reindexação para manter a performance do sistema.
Olá novamente, pessoal!
No nosso último papo, eu dei uma visão geral de como era trabalhar com o C-tree, o nosso motor de banco de dados ISAM (leia também o artigo "O Mundo Antes do SQL – Uma Viagem ao C-tree e aos Bancos de Dados ISAM"). Mostrei como a gente definia a estrutura dos dados no código e como "navegávamos" pelos registros. Agora, para aqueles que, como eu, gostam de fuçar no motor e entender cada detalhe, vamos abrir o capô de verdade.
Vou contar os segredos, as otimizações que a gente fazia para "espremer" cada pingo de performance, as batalhas que travávamos para controlar o acesso de múltiplos usuários e, claro, o nosso ritual de fim de semana: a temida e necessária reindexação dos arquivos.
1. Otimizando Cada Byte: Tipos de Chave e Compressão
Hoje, com terabytes de armazenamento, a gente não pensa muito nisso, mas nos anos 90, cada byte em disco era precioso. E uma das belezas do C-tree era o nível de controle que ele nos dava para otimizar o espaço usado pelos arquivos de índice (.idx). Isso era feito através da escolha do "tipo de chave" (keytyp) na hora de definir um índice.
O manual do C-tree descrevia vários tipos, mas dois eram particularmente geniais para economizar espaço:
- Compressão de Caracteres Iniciais (tipo 4): Imagine um cadastro de clientes com milhares de nomes como "SILVA, JOÃO", "SILVA, JOSÉ", "SILVA, MARIA". Em vez de gravar "SILVA, " repetidamente no arquivo de índice, essa opção permitia que o C-tree armazenasse algo como: "SILVA, JOÃO", 7"JOSÉ", 7"MARIA". O número 7 indicava que os 7 primeiros caracteres eram iguais ao do registro anterior. Uma economia brutal!
- Compressão de Espaços Finais (tipo 8): Nossos campos de nome tinham 35 caracteres, mas nem todo mundo tem um nome comprido. Para não gastar 35 bytes no índice para um nome como "ANA LUIZA", essa opção removia os espaços em branco do final e guardava apenas o nome, mais um byte para dizer o tamanho.
Pode parecer exagero, mas em uma base com milhões de registros, essas otimizações podiam reduzir o tamanho de um arquivo de índice pela metade, o que significava buscas mais rápidas, já que o cabeçote do HD tinha menos dados para ler.
2. Destrinchando a Planta Baixa: A Estrutura IDX
E onde a gente definia tudo isso? No nosso arquivo de cabeçalho. Vejam um exemplo mais detalhado da definição de um dos nossos índices no arquivo de definição:
Snippet de Código 1:
IIDX _FicIdx[] =
{
// ... outros índices aqui ...
{
47, /* ikeylen: Comprimento total da chave (Nome + Codigo) */
0, /* ikeytyp: 0 = Chave de tamanho fixo, sem compressão */
1, /* ikeydup: 1 = permite duplicacao da chave */
0, /* inulkey: 0 = nao permitida chave nula */
20, /* iempchr: caracter vazio- SPC */
2, /* inumseg: 2 = numero de segmentos da chave */
&_FicSeg[4], /* apontador para os segmentos que formam a chave */
""
},
// ... outros índices aqui ...
};
Cada campo era uma decisão de engenharia:
- ikeylen: O tamanho total da chave em bytes.
- ikeytyp: O tipo de chave que discutimos acima. Neste caso, 0 significava uma chave simples de tamanho fixo.
- ikeydup: 1 significava que podíamos ter chaves duplicadas (várias fichas para o mesmo paciente). O C-tree adicionava um sufixo único de 4 bytes para diferenciar os registros.
- inumseg: A chave podia ser formada por um ou mais "pedaços" do registro. O índice de paciente, por exemplo, usava o nome do paciente (Npac) e o código da ficha (codf) como segmentos para garantir uma ordenação única.
Era um trabalho de artesão. Nós "desenhávamos" o índice no código para obter a performance exata que precisávamos para cada tipo de consulta.
3. A Dança da Concorrência: Travas de Nó vs. Travas de Registro
A parte que realmente separava uma aplicação robusta de uma que corrompia dados era o gerenciamento de acesso multiusuário. O manual do C-tree era claro sobre essa divisão de responsabilidades:
- Travas de Nó (Node Locks): O C-tree cuidava automaticamente de travar os "nós" internos dos arquivos de índice (.idx) durante uma atualização. O algoritmo era tão bom que era garantido como "deadlock-free" (à prova de impasse), o que era incrível.
- Travas de Registro (Data Record Locks): Aqui a responsabilidade era nossa. O manual dizia, em outras palavras: "Eu cuido do meu índice, você cuida do seu dado".
Isso significava que, no nosso código C, antes de permitir que um usuário alterasse uma ficha, precisávamos implementar um protocolo de travamento. Era a tal "dança" que eu mencionei no artigo anterior, onde a gente usava funções como LKISAM para adquirir e liberar os "cadeados" sobre os registros.
E o que acontecia se uma recepcionista tentasse abrir um atendimento que uma faturista estava salvando naquele exato instante? O C-tree nos retornava o erro ITIM_ERR (160). O manual descrevia essa situação perfeitamente: uma interferência multiusuário. O nosso código precisava ser inteligente para tratar esse erro. Ele não podia simplesmente dar uma mensagem de "erro" para o usuário. A rotina tinha que entender que ITIM_ERR significava "tente de novo em um instante", e o programa entrava em um loop rápido, tentando reler o registro até conseguir acessá-lo.
4. O Ritual da Madrugada: Desfragmentando e Reindexando com CTRBLD
Por fim, a tarefa mais brutal e mais importante: a manutenção da base de dados. Com meses de trabalho, os arquivos ficavam "velhos" de duas formas:
- Fragmentação Física: O arquivo de dados (.dat), ao crescer, era espalhado pelo sistema operacional em pedaços não contíguos no disco. Para gerar um relatório sequencial, o cabeçote do HD precisava pular loucamente pelo disco, tornando a operação lentíssima.
- Fragmentação Lógica: Os arquivos de índice (.idx) ficavam desbalanceados e cheios de buracos com milhares de inclusões e exclusões, o que diminuía a velocidade das buscas.
A solução era um ritual que, dependendo do tamanho do cliente, era feito a cada um, três ou seis meses. E era um processo de duas etapas:
- Etapa 1: Desfragmentar o .dat. Não havia uma ferramenta mágica para isso. Nossa solução era manual: copiávamos o arquivo de dados inteiro para um novo nome (COPY FICHAS.DAT FICHAS.NEW). Isso forçava o sistema operacional a alocar um novo bloco de espaço, o mais contínuo possível, para o novo arquivo. O arquivo antigo era mantido como um backup de emergência.
- Etapa 2: A "Maldita" Reindexação com CTRBLD. Com o arquivo de dados agora otimizado, era a hora de usar o utilitário CTRBLD.EXE (C-tree Rebuild). Apontávamos ele para o novo FICHAS.NEW, e o trabalho começava. O programa lia cada registro do arquivo de dados, um por um, e reconstruía do zero todos os arquivos de índice. Para um cliente grande, isso ficava a noite inteira rodando. E 'rodando' significava que o sistema inteiro tinha que ser derrubado. A operação era agendada para uma madrugada de fim de semana, e a equipe ficava de plantão, pois se o processo falhasse no meio, a segunda-feira seria um caos. Era o som do HD "cantando" sem parar, um sinal de que a manutenção pesada estava em andamento.
Hoje, com um Oracle, se uma consulta está lenta, o DBA vai lá, analisa o 'plano de execução' e ajusta um parâmetro. Na nossa época, não tinha plano de execução. O 'plano' éramos nós. Nossa otimização era essa operação braçal, noturna, de reconstruir a base de dados inteira, bloco por bloco.
Conclusão
Dominar o C-tree nesse nível nos transformou. Não éramos apenas programadores de aplicação; éramos administradores de banco de dados, arquitetos de dados e especialistas em performance, tudo ao mesmo tempo. Era um trabalho que exigia um conhecimento profundo e uma disciplina enorme, mas era esse controle granular que nos permitia construir sistemas com uma velocidade que, para a época, parecia mágica. E essa, meus amigos, é uma lição que carrego até hoje.