# Esquema físico do banco `dfimoveis.sqlite3` > **Atualização de 22/07/2026:** as seções históricas deste documento descrevem o schema v1. A migração aditiva v2 descrita na seção final prevalece para consultas atuais. ## 1. Escopo e identificação do snapshot Este documento descreve o esquema observado e revalidado em 21 de julho de 2026. Ele não altera o modelo conceitual de `docs/DATA_MODEL.md` e não atribui significado não comprovado às colunas. | Item | Valor | |---|---| | Caminho | `dfimoveis_data/dfimoveis.sqlite3` | | Tamanho | 9.478.144 bytes | | SHA-256 | `bfbbdd09e88f76afd917ebd981b848b9321aeb5fc0602f712645d33e1a5ea76c` | | Data de referência | `2026-07-21` | | Codificação | UTF-8 | | `journal_mode` persistido | WAL | | Páginas | 2.314 páginas de 4.096 bytes; `freelist_count = 2` | | `user_version` / `application_id` | 0 / 0 | | Método de abertura | URI `mode=ro`, `PRAGMA query_only = ON` e transação de leitura | Como este arquivo é atualizado pelo scraper, não foi usado `immutable=1`. `PRAGMA quick_check` retornou `ok` e `PRAGMA foreign_key_check` não retornou violações. Nenhum sidecar foi removido. ## 2. Visão geral O banco possui cinco tabelas da aplicação e a tabela interna `sqlite_sequence`. Não há views nem triggers. Os quatro índices existentes são autoíndices criados por chaves primárias ou restrições `UNIQUE`. ```mermaid erDiagram listings ||--|{ listing_history : "listing_id" listings ||--|{ listing_searches : "listing_id" listings ||--|{ photos : "listing_id" crawl_runs { INTEGER id PK TEXT started_at TEXT finished_at TEXT status TEXT stats_json } listings { TEXT listing_id PK TEXT url REAL price_brl REAL area_m2 TEXT first_seen_at TEXT last_seen_at TEXT content_hash } listing_history { INTEGER id PK TEXT listing_id FK TEXT captured_at TEXT content_hash TEXT data_json } listing_searches { TEXT listing_id PK,FK TEXT search_url PK TEXT first_seen_at TEXT last_seen_at } photos { TEXT listing_id PK,FK TEXT source_url PK INTEGER ordinal TEXT sha256 TEXT local_path } ``` `crawl_runs` não possui relacionamento físico com anúncios ou snapshots. Portanto, a execução que gerou uma linha só pode ser inferida pelos horários, não recuperada por uma chave. ### Contagens e cardinalidades observadas | Tabela | Linhas | Interpretação física | |---|---:|---| | `crawl_runs` | 20 | Execuções do coletor | | `listings` | 161 | Identidades de anúncios e seu estado corrente | | `listing_history` | 562 | Estados distintos dos anúncios por hash | | `listing_searches` | 161 | Vínculos entre anúncio e URL de busca | | `photos` | 7.363 | Mídias associadas diretamente aos anúncios | | `sqlite_sequence` | 2 | Sequências internas de tabelas `AUTOINCREMENT` | No snapshot avaliado: - cada anúncio está ligado a exatamente uma URL de busca; - cada anúncio possui de 1 a 4 registros em `listing_history`; - cada anúncio possui de 5 a 79 linhas em `photos`; - não há órfãos nas três relações com `listings`; - o hash corrente de cada anúncio possui registro correspondente no histórico; - grande parte dos snapshots adicionais reflete instabilidade de campos contaminados, sobretudo `creci`, e não mudança imobiliária real. ## 3. Dicionário de tabelas ### 3.1 `crawl_runs` Representa execuções técnicas do coletor. Não registra fonte, versão do scraper, configuração, texto de erro ou relacionamento direto com os itens coletados. | Coluna | Tipo | Nulo? | Chave/default | Interpretação | |---|---|---:|---|---| | `id` | INTEGER | Não | PK, `AUTOINCREMENT` | Identificador da execução; a PK inteira atribui um valor quando omitido | | `started_at` | TEXT | Não | — | Início em ISO 8601, observado em UTC | | `finished_at` | TEXT | Sim | — | Término em ISO 8601 | | `status` | TEXT | Não | — | Estado textual da execução; `ok` e `failed` foram observados | | `stats_json` | TEXT | Não | `'{}'` | Contadores técnicos em JSON | As estruturas JSON observadas são válidas. Além das chaves originais, execuções recentes também registram `photos_skipped`. ### 3.2 `listings` Representa a identidade do anúncio no portal e uma cópia mutável de seu estado mais recente. Não representa um imóvel físico deduplicado. | Coluna | Tipo | Nulo? | Chave/default | Interpretação e ressalvas | |---|---|---:|---|---| | `listing_id` | TEXT | Sim no DDL; não observado | PK | Identificador do anúncio no portal; todos os valores atuais têm 6–7 dígitos. Por peculiaridade do SQLite, PK textual sem `NOT NULL` explícito merece validação na aplicação | | `url` | TEXT | Não | — | URL do anúncio | | `title` | TEXT | Sim | — | Título extraído | | `description` | TEXT | Sim | — | Descrição curta extraída; não é o HTML bruto completo | | `transaction_type` | TEXT | Sim | — | Tipo da transação; somente `venda` no snapshot | | `property_type` | TEXT | Sim | — | Tipo anunciado; `apartamento` ou `casa` | | `price_brl` | REAL | Sim | — | Preço anunciado em reais; sujeito a erros de escala | | `condominium_brl` | REAL | Sim | — | Condomínio anunciado; periodicidade não está formalizada | | `iptu_brl` | REAL | Sim | — | IPTU anunciado; periodicidade não está formalizada | | `area_m2` | REAL | Sim | — | Uma única área em m², sem indicar se é útil, privativa, construída, total ou terreno | | `bedrooms` | INTEGER | Sim | — | Quartos anunciados, não a planta original comprovada | | `suites` | INTEGER | Sim | — | Suítes anunciadas | | `parking_spaces` | INTEGER | Sim | — | Vagas anunciadas | | `address` | TEXT | Sim | — | Texto de endereço; fortemente contaminado pela página na coleta atual | | `neighborhood` | TEXT | Sim | — | Bairro; fortemente contaminado pela página na coleta atual | | `city` | TEXT | Sim | — | Cidade; 100% nula atualmente | | `state` | TEXT | Sim | — | UF; 100% nula atualmente | | `advertiser_name` | TEXT | Sim | — | Nome do anunciante; 100% nulo atualmente | | `advertiser_code` | TEXT | Sim | — | Código do anunciante; parte relevante dos valores está contaminada | | `creci` | TEXT | Sim | — | Registro do anunciante; parte relevante dos valores está contaminada | | `latitude` | REAL | Sim | — | Latitude; 100% nula atualmente | | `longitude` | REAL | Sim | — | Longitude; 100% nula atualmente | | `published_at` | TEXT | Sim | — | Data publicada pelo portal; 100% nula atualmente | | `first_seen_at` | TEXT | Não | — | Primeira observação do anúncio | | `last_seen_at` | TEXT | Não | — | Última observação do anúncio | | `inactive_at` | TEXT | Sim | — | Momento de inativação inferida pelo coletor; 100% nulo atualmente | | `content_hash` | TEXT | Sim | — | SHA-256 do conteúdo normalizado usado para detectar mudança | | `raw_html_path` | TEXT | Sim | — | Caminho relativo para o HTML bruto do anúncio | | `extra_json` | TEXT | Não | `'{}'` | JSON adicional; contém apenas `json_ld`, um array vazio nos 156 anúncios | Exemplo não sensível de identidade: o anúncio `1026522` aponta para uma URL sob `www.dfimoveis.com.br` e para um HTML em `raw/listing/1026522/...html.gz`. ### 3.3 `listing_history` Representa estados distintos de um anúncio, não cada tentativa ou observação do coletor. | Coluna | Tipo | Nulo? | Chave/default | Interpretação | |---|---|---:|---|---| | `id` | INTEGER | Não | PK, `AUTOINCREMENT` | Identificador do estado histórico; a PK inteira atribui um valor quando omitido | | `listing_id` | TEXT | Não | FK | Anúncio ao qual o estado pertence | | `captured_at` | TEXT | Não | — | Momento em que o estado foi capturado | | `content_hash` | TEXT | Não | `UNIQUE` com `listing_id` | Hash que impede repetir um estado idêntico para o anúncio | | `data_json` | TEXT | Não | — | Snapshot JSON dos principais campos extraídos | `data_json` é JSON válido em todas as 156 linhas. Ele contém os campos correntes de conteúdo, mas não contém `first_seen_at`, `last_seen_at`, `inactive_at`, `raw_html_path` ou uma referência à execução. Como a restrição é `UNIQUE(listing_id, content_hash)`, observações repetidas sem mudança não geram novos snapshots. ### 3.4 `listing_searches` Tabela de associação entre anúncio e consulta de busca que o encontrou. | Coluna | Tipo | Nulo? | Chave/default | Interpretação | |---|---|---:|---|---| | `listing_id` | TEXT | Não | PK parcial, FK | Anúncio | | `search_url` | TEXT | Não | PK parcial | URL/configuração da busca | | `first_seen_at` | TEXT | Não | — | Primeira observação nessa busca | | `last_seen_at` | TEXT | Não | — | Última observação nessa busca | A chave primária composta é `(listing_id, search_url)`. Há duas URLs distintas no banco, uma para apartamentos de três quartos no Cruzeiro Novo e outra para casas anunciadas com três ou quatro quartos no Jardins Mangueiral. ### 3.5 `photos` Representa arquivos de mídia associados diretamente ao anúncio. A tabela não preserva associação por snapshot. | Coluna | Tipo | Nulo? | Chave/default | Interpretação | |---|---|---:|---|---| | `listing_id` | TEXT | Não | PK parcial, FK | Anúncio | | `ordinal` | INTEGER | Não | — | Ordem da imagem no anúncio | | `source_url` | TEXT | Não | PK parcial | URL de origem da imagem | | `sha256` | TEXT | Sim | — | Hash do arquivo baixado | | `mime_type` | TEXT | Sim | — | Tipo MIME detectado | | `bytes` | INTEGER | Sim | — | Tamanho do arquivo | | `local_path` | TEXT | Sim | — | Caminho relativo sob `dfimoveis_data/` | | `first_seen_at` | TEXT | Não | — | Primeira observação da mídia | | `last_seen_at` | TEXT | Não | — | Última observação da mídia | A chave primária é `(listing_id, source_url)`, e não `(listing_id, ordinal)`. Não há ordinais repetidos dentro de um anúncio na amostra atual. Exemplo de caminho: `photos/1026522/001_9d7f11205f0a4a3f.jpg`. ### 3.6 `sqlite_sequence` Tabela interna do SQLite que mantém as sequências `AUTOINCREMENT` de `crawl_runs` e `listing_history`. Não é entidade do domínio. ## 4. Chaves, índices e integridade referencial | Índice | Tabela | Origem | Colunas lógicas | |---|---|---|---| | `sqlite_autoindex_listings_1` | `listings` | PK | `listing_id` | | `sqlite_autoindex_listing_history_1` | `listing_history` | `UNIQUE` | `listing_id`, `content_hash` | | `sqlite_autoindex_listing_searches_1` | `listing_searches` | PK | `listing_id`, `search_url` | | `sqlite_autoindex_photos_1` | `photos` | PK | `listing_id`, `source_url` | As FKs de `listing_history`, `listing_searches` e `photos` apontam para `listings(listing_id)`, com `NO ACTION` para atualização e exclusão. Não há índices explícitos por data, preço, região ou hash de foto. Com o volume atual isso não é um problema operacional; se a base crescer, índices analíticos devem ser criados apenas em uma camada derivada ou recomendados para uma migração aprovada, nunca adicionados ao banco original durante análise. ## 5. Correspondência com o modelo conceitual | Conceito desejado | Implementação física | Avaliação | |---|---|---| | Fonte | Ausente; DF Imóveis é implícito nas URLs | Não permite múltiplas fontes com identidade segura | | Execução de coleta | `crawl_runs` | Parcial; faltam fonte, configuração, versão e relação com itens | | Anúncio | `listings` | Presente; estado corrente e identidade estão na mesma linha | | Observação histórica | `listing_history` | Parcial; guarda somente conteúdo alterado, sem `run_id` | | Imóvel físico candidato | Ausente | Nenhuma deduplicação física é persistida | | Localização normalizada | Colunas em `listings` | Insuficiente e atualmente contaminada | | Mídia | `photos` | Parcial; vinculada ao anúncio, não ao snapshot | | Origem por consulta | `listing_searches` | Presente como relação anúncio–URL de busca | ## 6. Decisões de interpretação e ambiguidades 1. `listing_id` identifica um **anúncio no portal**, não um imóvel físico. 2. Uma linha de `listing_history` é um **estado de conteúdo distinto**, não prova de que o anúncio foi observado somente uma vez. Observações idênticas são condensadas em `last_seen_at`. 3. O banco ainda não contém histórico suficiente para liquidez: há quatro dias efetivos de observação e a maioria das transições é ruído de campos contaminados. 4. Anúncios diferentes com preço, área ou fotos semelhantes permanecem anúncios distintos. Não há chave de endereço/unidade nem tabela de candidatos físicos. 5. `area_m2` não deve ser usada para preço por m² até que o conceito de área seja validado por tipologia e fonte. 6. `bedrooms` é a quantidade anunciada. Ela não comprova a planta original de três quartos exigida para Jardins Mangueiral. 7. Datas com sufixo `+00:00` são tratadas como UTC. A cobertura observada vai de 13 a 21/07/2026. 8. Os campos textuais contaminados devem ser reextraídos do HTML bruto em uma camada derivada; os valores atuais não devem ser silenciosamente normalizados como se fossem corretos. ## 7. Problemas conhecidos do snapshot - 150 de 161 endereços têm mais de 300 caracteres; 159 de 161 bairros têm mais de 100 caracteres. - `city`, `state`, `latitude`, `longitude`, `published_at` e `advertiser_name` estão integralmente ausentes. - 99 códigos de anunciante e 157 valores de CRECI são longos demais para o significado esperado, indicando extração contaminada. - Há um preço de R$ 570.000.000 e uma área de 0,11 m², ambos candidatos fortes a erro de extração/escala. - O JSON-LD reservado em `extra_json` é um array vazio em todos os 161 anúncios e não oferece uma fonte estruturada alternativa no snapshot. - As 7.363 linhas de foto têm hash e caminho local preenchidos; isso não garante que toda imagem pertença ao imóvel. - Hashes de foto idênticos aparecem em muitos anúncios; isso demonstra a presença de ativos genéricos e impede usar igualdade de hash como prova isolada de imóvel duplicado. - Das 401 transições históricas, 398 alteram `creci`; somente três anúncios apresentam mudança real de preço. O histórico precisa de normalização antes de análises temporais. - Não há como ligar formalmente anúncio/snapshot a `crawl_runs`, nem distinguir por chave a fonte se uma segunda fonte for incorporada. --- ## 12. Schema v2 — 22/07/2026 A migração foi autorizada explicitamente, executada após backup consistente e não removeu linhas ou colunas antigas. Contagens preservadas na migração: | Tabela legada | Linhas | |---|---:| | `listings` | 162 | | `listing_searches` | 162 | | `listing_history` | 637 | | `photos` | 7.457 | | `crawl_runs` | 21 | Novas colunas: - `listings`: `address_normalized`, `neighborhood_normalized`, `creci_normalized`, `raw_content_hash`, `stable_content_hash`; - `photos`: `discovery_source`, `is_primary_gallery`, `review_status`; - `crawl_runs`: `searches_json`, `config_json`, `scope_complete`. Novas tabelas: - `availability_observations`: evidência datada e sua fonte, sem converter selo visual em prova registral; - `data_quality_issues`: quarentenas reversíveis, com severidade, evidência e estado; - `listing_change_events`: eventos materiais produzidos pelo hash estável nas futuras coletas. Novas visões: - `v_primary_photos`; - `v_open_data_quality_issues`; - `v_listing_current_availability`. Auditoria inicial: - 3.521 fotos pertencem à galeria do próprio anúncio; - 3.286 registros legados apontam para `/fotos//` e vieram de recomendações do portal; - 650 registros restantes são ativos visuais do site ou URLs sem proprietário de anúncio; - os 3.936 registros não primários foram preservados e marcados, não apagados; - 15 anúncios foram normalizados para o CRECI `26903`; - `PRAGMA quick_check` retornou `ok` após a migração.