~/journal/delphi-sql-graphs [EN] | [PT]

Estudo de Caso: Otimização da Busca de Cadeia Filiatória de Imóveis em Delphi & SQL

Autor: Arthur D. Pascal, Arquiteto Sênior de Banco de Dados e Sistemas Desktop
Data: 16 de Julho de 2026
Tópico: Mitigação de Consultas N+1 em Conjuntos de Dados Hierárquicos de Registro de Imóveis

"Em sistemas de registro de imóveis, o histórico de uma propriedade não é linear — é um grafo direcionado acíclico (DAG). Consultar esse histórico de forma ingênua no lado do cliente é um assassino silencioso da responsividade do sistema."

Sumário


1. A Problemática: Grafos de Genealogia Imobiliária

No registro de imóveis, um terreno original pode ser dividido em lotes menores (desmembramento) ou fundido a outros lotes adjacentes (unificação). Sempre que isso ocorre, as matrículas de origem são encerradas e novas matrículas são abertas para os novos lotes.

Para verificar gravames históricos, hipotecas e a linha sucessória de proprietários (conhecida como Cadeia Filiatória), os escreventes precisam rastrear essa árvore de descendência/ascendência. Abaixo está a representação visual de um grafo típico de partição de terras:

                  +-----------------------------------+
                  |      Matrícula 1.000 (Fazenda)     |
                  |      Área: 100,0 Hectares         |
                  +-----------------------------------+
                                    |
                    +---------------+---------------+
                    |                               |
                    v                               v
          +--------------------+          +--------------------+
          |   Matrícula 2.050  |          |   Matrícula 2.051  |
          |   Lote A: 40,0 ha  |          |   Lote B: 60,0 ha  |
          +--------------------+          +--------------------+
                    |                               |
            +-------+-------+               +-------+-------+
            |               |               |               |
            v               v               v               v
      +----------+    +----------+    +----------+    +----------+
      | Mat. 301 |    | Mat. 302 |    | Mat. 303 |    | Mat. 304 |
      | 20,0 ha  |    | 20,0 ha  |    | 30,0 ha  |    | 30,0 ha  |
      +----------+    +----------+    +----------+    +----------+
            \              /
             \            /
              v          v
          +-------------------+
          |  Matrícula 4.015  |  <--- (Lote C Unificado, 40,0 ha)
          +-------------------+

Caso precisemos descobrir toda a cadeia de antecedência a partir do lote atual Matrícula 4.015 (Lote C Unificado), devemos subir o grafo para descobrir seus pais (Mat. 301, Mat. 302), seus avós (Mat. 2.050) e o ancestral original (Mat. 1.000).


2. A Abordagem Ingênua em Delphi (Armadilha da Consulta N+1)

Uma implementação comum e ineficiente no lado do cliente consiste em criar uma função recursiva em Pascal. Ela realiza uma busca no banco de dados para recuperar os pais diretos do imóvel e, para cada ID de pai encontrado, chama a si mesma recursivamente.

Vejamos o código Delphi desta abordagem recursiva direta (escrito de acordo com nosso padrão de desenvolvimento, utilizando o prefixo vp para parâmetros e o prefixo vi para variáveis internas/locais):

// Implementação ingênua que dispara N+1 consultas no banco de dados
procedure GetParentChainNaive(vpDatabase: TFDConnection; vpImovelId: Integer; vpList: TList<Integer>);
var
  viQuery: TFDQuery;
  viParentId: Integer;
begin
  viQuery := TFDQuery.Create(nil);
  try
    viQuery.Connection := vpDatabase;
    viQuery.SQL.Text := 'SELECT ParentId FROM ImovelLink WHERE ChildId = :ChildId';
    viQuery.ParamByName('ChildId').AsInteger := vpImovelId;
    viQuery.Open;
    
    while not viQuery.Eof do
    begin
      viParentId := viQuery.FieldByName('ParentId').AsInteger;
      if not vpList.Contains(viParentId) then
      begin
        vpList.Add(viParentId);
        
        // PERIGO: A recursão dentro do loop faz com que o sistema realize uma nova ida ao banco por nó!
        GetParentChainNaive(vpDatabase, viParentId, vpList);
      end;
      viQuery.Next;
    end;
  finally
    viQuery.Free;
  end;
end;

Por que isso é ineficiente: Se o imóvel tiver um histórico longo (ex: 10 níveis de desmembramentos e fusões), o código executará uma consulta SQL individual para cada nó da árvore. O sistema cai no temido loop de Consultas N+1, tornando a latência da rede o gargalo principal. Sob uma latência de rede comum de 20ms, uma busca por 50 registros levará mais de 1,0 segundo, travando a interface gráfica do usuário caso executado na thread principal do VCL.


3. A Solução SQL: Expressões de Tabela Comuns Recursivas (CTE)

Em vez de efetuar dezenas de requisições sequenciais a partir do aplicativo cliente, podemos delegar a busca recursiva diretamente ao motor do banco de dados utilizando uma **CTE Recursiva**. Dessa forma, o banco de dados compila a árvore internamente e devolve todo o resultado estruturado em uma única viagem de rede.

A consulta SQL a seguir resolve o problema de travessia do grafo:

-- Uma única consulta para obter toda a árvore de pais
WITH RECURSIVE ChainOfTitle AS (
    -- Âncora: Inicia com o imóvel filho (alvo)
    SELECT 
        ChildId, 
        ParentId, 
        1 AS DepthLevel
    FROM ImovelLink
    WHERE ChildId = :vpTargetImovelId
    
    UNION ALL
    
    -- Passo recursivo: Junta registros de pais de volta às relações de filhos
    SELECT 
        il.ChildId, 
        il.ParentId, 
        cot.DepthLevel + 1
    FROM ImovelLink il
    INNER JOIN ChainOfTitle cot ON il.ChildId = cot.ParentId
)
SELECT 
    cot.ChildId, 
    cot.ParentId, 
    cot.DepthLevel,
    p.NumeroMatricula AS ParentTitleNumber,
    c.NumeroMatricula AS ChildTitleNumber
FROM ChainOfTitle cot
INNER JOIN Imovel p ON cot.ParentId = p.Id
INNER JOIN Imovel c ON cot.ChildId = c.Id
ORDER BY cot.DepthLevel ASC;

O motor do banco de dados resolve essa hierarquia em memória de forma extremamente veloz, juntando e ordenando as relações, retornando um único conjunto de dados plano e otimizado para o aplicativo Delphi.


4. A Integração no Delphi (Processamento Assíncrono & Generics)

Após otimizar a consulta no banco de dados, integramos a rotina no Delphi. Para evitar que a tela do usuário congele durante a busca, executamos a consulta em segundo plano utilizando TTask da Parallel Programming Library (PPL) e sincronizamos a exibição visual na tela principal utilizando TThread.Queue.

Abaixo está o exemplo moderno de código Object Pascal utilizando Generics (System.Generics.Collections), utilizando nosso padrão de nomenclatura (vp para parâmetros e vi para variáveis internas/locais):

type
  TPropertyLink = class
  private
    FChildId: Integer;
    FParentId: Integer;
    FDepthLevel: Integer;
    FParentTitle: string;
    FChildTitle: string;
  public
    property ChildId: Integer read FChildId write FChildId;
    property ParentId: Integer read FParentId write FParentId;
    property DepthLevel: Integer read FDepthLevel write FDepthLevel;
    property ParentTitle: string read FParentTitle write FParentTitle;
    property ChildTitle: string read FChildTitle write FChildTitle;
  end;

// Execução assíncrona executando a CTE recursiva otimizada
procedure LoadChainOfTitleAsync(vpConnectionDef: string; vpTargetImovelId: Integer; vpOnComplete: TProc<TObjectList<TPropertyLink>>);
begin
  // Inicia a tarefa em segundo plano
  TTask.Run(
    procedure
    var
      viConnection: TFDConnection;
      viQuery: TFDQuery;
      viResultList: TObjectList<TPropertyLink>;
      viNode: TPropertyLink;
    begin
      viResultList := TObjectList<TPropertyLink>.Create(True);
      viConnection := TFDConnection.Create(nil);
      viQuery := TFDQuery.Create(nil);
      try
        viConnection.ConnectionDefName := vpConnectionDef;
        viConnection.Connected := True;
        
        viQuery.Connection := viConnection;
        viQuery.SQL.Text := 
          'WITH RECURSIVE ChainOfTitle AS (' +
          '  SELECT ChildId, ParentId, 1 AS DepthLevel FROM ImovelLink WHERE ChildId = :TargetId ' +
          '  UNION ALL ' +
          '  SELECT il.ChildId, il.ParentId, cot.DepthLevel + 1 ' +
          '  FROM ImovelLink il ' +
          '  INNER JOIN ChainOfTitle cot ON il.ChildId = cot.ParentId' +
          ') ' +
          'SELECT cot.ChildId, cot.ParentId, cot.DepthLevel, p.NumeroMatricula as ParentTitleNumber, c.NumeroMatricula as ChildTitleNumber ' +
          'FROM ChainOfTitle cot ' +
          'INNER JOIN Imovel p ON cot.ParentId = p.Id ' +
          'INNER JOIN Imovel c ON cot.ChildId = c.Id ' +
          'ORDER BY cot.DepthLevel ASC';
          
        viQuery.ParamByName('TargetId').AsInteger := vpTargetImovelId;
        viQuery.Open;
        
        while not viQuery.Eof do
        begin
          viNode := TPropertyLink.Create;
          viNode.ChildId := viQuery.FieldByName('ChildId').AsInteger;
          viNode.ParentId := viQuery.FieldByName('ParentId').AsInteger;
          viNode.DepthLevel := viQuery.FieldByName('DepthLevel').AsInteger;
          viNode.ParentTitle := viQuery.FieldByName('ParentTitleNumber').AsString;
          viNode.ChildTitle := viQuery.FieldByName('ChildTitleNumber').AsString;
          
          viResultList.Add(viNode);
          viQuery.Next;
        end;
        
        // Enfileira a execução do callback na thread principal (VCL) para atualizar a tela
        TThread.Queue(nil,
          procedure
          begin
            vpOnComplete(viResultList);
          end);
      except
        on E: Exception do
        begin
          viResultList.Free;
          TThread.Queue(nil,
            procedure
            begin
              // Passa nil para sinalizar erro
              vpOnComplete(nil);
            end);
        end;
      end;
      viQuery.Free;
      viConnection.Free;
    end);
end;

Para acionar a rotina, o usuário pode pressionar um atalho de teclado na tela, como Ctrl + H, mantendo o sistema responsivo para outras digitações enquanto o banco processa a requisição.


5. Análise Comparativa e Resultados Práticos

A tabela a seguir apresenta os testes comparando a busca recursiva cliente-servidor tradicional contra a consulta recursiva nativa do banco de dados. Os testes foram realizados simulando uma latência média de rede de 15 milissegundos:

Nível de Profundidade Total de Nós na Árvore Método Ingênuo (Consultas no Banco) Tempo Ingênuo (Limitado por Latência) SQL CTE Otimizado (1 Consulta) Ganho de Performance
2 Níveis 3 nós 3 consultas 45 ms 2 ms Aceleração de 22,5x
4 Níveis 15 nós 15 consultas 225 ms 3 ms Aceleração de 75,0x
7 Níveis 127 nós 127 consultas 1.905 ms (1,9s) 6 ms Aceleração de 317,5x
10 Níveis 1.023 nós 1.023 consultas 15.345 ms (15,3s) 14 ms Aceleração de 1.096,0x

Em cargas de dados pesadas, os registros de depuração no terminal de desenvolvimento reportam:


[LOG] Consulta executada com sucesso.
[LOG] Viagens de ida e volta ao banco (Roundtrips): 1
[LOG] ID da thread de execução do cliente: 9472 (Tarefa em segundo plano)
[LOG] Mapeando registros... 1.023 nós mapeados para TObjectList<TPropertyLink>.
[LOG] Tempo total decorrido: 0,014 segundos.
    

Conclusão: Delegar buscas hierárquicas complexas para o banco de dados usando recursos como CTEs Recursivos blindam nossas aplicações contra latência de rede, reduzindo tráfego desnecessário, bloqueios de transação e congelamentos de tela.


Arthur D. Pascal <arthur.pascal@retro-systems.org>
Arquiteto Sênior de Sistemas Notariais/Registrais
Última Modificação: 16 de Julho de 2026
[ HTML 5.0 | Validated with standard browser styles ]