Eu tenho uma tabela geográfica que contém
- países
- localidades (cidade, vila, vila, ilha, arquipélago)
- locais (local/empresa + bairros/distrito/área), por exemplo - Big Ben ou Southwark Borough.
Para detalhes adicionais de cada tipo de local, tenho tabelas relacionadas.
Tabela 'country_details' para locais do tipo 'country' e da mesma forma para locais.
Para um local como 'Big Ben', tem referência ao id da sua localidade (ou seja, Londres), e também referência ao país (que pode ser simplesmente pelo iso_code do país)
Exemplo:
id | title | locality_id | country_iso_code |
---------------------------------------------------------|
1 | United Kingdom | null | UK |
2 | London | null | UK |
3 | Big Ben | 2 | UK |
4 | XYZ District | 2 | UK |
Cenário
Agora, como para enviar ao cliente informações sobre o Big Ben eu também gostaria de obter o nome da localidade (Londres) e do país (Reino Unido), parece que minhas únicas 2 opções são:
- CTE recursivo
- JOIN na mesma mesa.
No entanto, uma vez que temos uma tabela de dezenas de milhares de registros, que pode potencialmente crescer para muito mais (alguns milhões), além da complexidade da consulta, isso também afetará o desempenho, suponho.
Pergunta
Qual é a melhor opção para "juntar" detalhes como "Londres" e "Reino Unido"?
Ambas as opções são ruins e é melhor repensar o design do esquema?
Tabelas:
CREATE TABLE places (
id int,
type smallint, -- ['country', 'locality', 'location']
sub_type smallint, -- nullable (city, village, etc.)
-- names
title text,
-- locality
locality_name text,
locality_id
-- country
country_iso_alpha2 text, -- 'GB'
country_name text, -- 'United Kingdom'
admin_region text, -- 'England', 'Texas', .. (null for Country)
...
);
CREATE TABLE country_details(
place_id int,
place_type smallint NOT NULL CHECK (item_type=1),
iso_alpha2 text,
iso_alpha3 text,
...
PRIMARY KEY (place_id, place_type),
FOREIGN KEY (place_id, place_type) references places (place_id, place_type) ON DELETE CASCADE
);
CREATE TABLE location_details(
place_id int,
place_type smallint NOT NULL CHECK (item_type=3),
website text,
neighborhood text,
formatted_address text,
...
PRIMARY KEY (place_id, place_type),
FOREIGN KEY (place_id, place_type) references places (place_id, place_type) ON DELETE CASCADE
);
Se for um número fixo de junções e um número pequeno, então, para simplificar, eu diria que escolha a opção 2 e faça algumas auto-junções.
Se houver muita variabilidade na profundidade hierárquica dos dados, eu diria que escolha a opção nº 1 e use um CTE recursivo.
Para a solução de autojunção, alguns milhões de linhas são pequenos e, quando indexados corretamente, a diferença de algumas centenas de linhas é insignificante.
Para a solução CTE recursiva, ela ainda deve ter bastante desempenho em alguns milhões de linhas, quando indexada corretamente. Mas você pode notar uma ligeira regressão, como levar menos de um segundo para algumas centenas de linhas e levar alguns segundos para alguns milhões de linhas.
É uma árvore, então vamos construir um exemplo de árvore com 10 folhas por nível e 7 níveis, ou seja, cerca de 1,1 milhão de linhas.
Agora vamos levar uma folha, junto com todos os seus pais, até a raiz. Existem vários métodos.
Foi assim que foi feito antes COM RECURSIVO. Funciona bem:
Esta é a opção padrão. Ele não usa o caminho, então esta coluna pode ser removida, a menos que seja usada para outra coisa.
Conclusão: ambas as opções são muito rápidas, menos de 1ms. Nenhum vencedor claro. Não é surpreendente, pois tudo o que fazem é buscar um pequeno número de linhas por meio da chave primária indexada.
Não estou considerando isso porque imporia uma profundidade máxima fixa à árvore e retornaria linhas em um formato inconveniente para uma árvore (ou seja, com uma tonelada de colunas).
No entanto, no meu exemplo de árvore, todas as folhas desta árvore têm o mesmo formato. Os níveis de subdivisão que você está usando não.
Se sua profundidade for fixa (países> localidades> locais) e você tiver certeza de que nunca precisará subdividir em condados, quarteirões, sub-bairros ou outras coisas... então o método JOIN faz sentido porque o formato de linha que foi antes inconveniente agora se torna conveniente, já que você está lidando com três tipos diferentes de subdivisões, em três tabelas diferentes, e todas elas possuem colunas diferentes.
Na verdade, com o método JOIN, você pode obter o resultado completo em uma consulta. Com os outros dois, depois de obter os IDs do caminho da tabela em árvore, você terá que consultar as três tabelas de subdivisão separadamente, o que adiciona mais trabalho.
Isso será bem dimensionado, porque as linhas mais atingidas são os níveis baixos da árvore, que praticamente sempre serão armazenados em cache na RAM.