Eu estava pensando originalmente em uma junção lateral como em outras abordagens sugeridas (por exemplo, a última consulta de Erwin Brandstetter, onde ele usa
int8
simples tipo de dados e índices simples), mas não quis escrevê-lo na resposta, porque achei que não é realmente eficiente. Quando você tenta encontrar todos os netblock
intervalos que cobrem um determinado intervalo, um índice não ajuda muito. Vou repetir a pergunta do Erwin Brandstetter aqui:
SELECT * -- only select columns you need to make it faster
FROM routing_details r
, LATERAL (
SELECT *
FROM netblock_details n
WHERE n.ip_min <= r.ip_min
AND n.ip_max >= r.ip_max
ORDER BY n.ip_max - n.ip_min
LIMIT 1
) n;
Quando você tem um índice em netblock_details, assim:
CREATE INDEX netblock_details_ip_min_max_idx ON netblock_details
(ip_min, ip_max DESC NULLS LAST);
você pode rapidamente (em
O(logN)
) encontre o ponto inicial da varredura em netblock_details
tabela - ou o máximo n.ip_min
que é menor que r.ip_min
, ou o mínimo n.ip_max
que é mais do que r.ip_max
. Mas então você tem que escanear/ler o resto do índice/tabela e para cada linha fazer a segunda parte da verificação e filtrar a maioria das linhas. Em outras palavras, esse índice ajuda a encontrar rapidamente a linha inicial que atende aos primeiros critérios de pesquisa:
n.ip_min <= r.ip_min
, mas você continuará lendo todas as linhas que atendem a esse critério e, para cada linha, execute a segunda verificação n.ip_max >= r.ip_max
. Em média (se os dados tiverem distribuição uniforme), você terá que ler metade das linhas do netblock_details
tabela. O otimizador pode optar por usar o índice para pesquisar n.ip_max >= r.ip_max
primeiro e depois aplique o segundo filtro n.ip_min <= r.ip_min
, mas você não pode usar esse índice para aplicar os dois filtros juntos. Resultado final:para cada linha de
routing_details
leremos metade das linhas de netblock_details
. 600K * 4M =2.400.000.000.000 linhas. É 2 vezes melhor que o produto cartesiano. Você pode ver este número (produto cartesiano) na saída de EXPLAIN ANALYZE
na pergunta. 592,496 * 8,221,675 = 4,871,309,550,800
Não é de admirar que suas consultas fiquem sem espaço em disco ao tentar materializar e classificar isso.
O processo geral de alto nível para chegar ao resultado final:
-
juntar duas mesas (sem encontrar a melhor combinação ainda). Na pior das hipóteses, é um produto cartesiano, na melhor das hipóteses, abrange todas as faixas denetblock_details
para cada intervalo derouting_details
. Você disse que existem várias entradas emnetblock_details
para cada entrada emrouting_details
, qualquer coisa de 3 a cerca de 10. Portanto, o resultado dessa junção pode ter ~ 6 milhões de linhas (não muito)
-
ordenar/particionar o resultado da junção pelorouting_details
intervalos e para cada intervalo encontre o melhor (menor) intervalo de cobertura denetblock_details
.
Minha ideia é reverter a consulta. Em vez de encontrar todos os intervalos de cobertura de
netblock_details
maiores para cada linha de routing_details
menores sugiro encontrar todos os intervalos menores de routing_details
menores para cada linha de netblock_details
maiores . Processo em duas etapas
Para cada linha de
netblock_details
maiores encontre todos os intervalos de routing_details
que estão dentro do netblock
variar. Eu usaria o seguinte esquema nas consultas. Adicionei a chave primária
ID
para ambas as tabelas. CREATE TABLE routing_details (
ID int
,ip_min int8
,ip_max int8
,asn text
,netblock text
);
CREATE TABLE netblock_details (
ID int
,ip_min int8
,ip_max int8
,name text
,country text
,source text
);
SELECT
netblock_details.ID AS n_ID
,netblock_details.ip_max - netblock_details.ip_min AS n_length
,r.ID AS r_ID
FROM
netblock_details
INNER JOIN LATERAL
(
SELECT routing_details.ID
FROM routing_details
WHERE
routing_details.ip_min >= netblock_details.ip_min
AND routing_details.ip_min <= netblock_details.ip_max
-- note how routing_details.ip_min is limited from both sides
-- this would make it possible to scan only (hopefully) small
-- portion of the table instead of full or half table
AND routing_details.ip_max <= netblock_details.ip_max
-- this clause ensures that the whole routing range
-- is inside the netblock range
) AS r ON true
Precisamos de índice em
routing_details
em (ip_min, ip_max)
. O principal aqui é indexar em ip_min
. Ter a segunda coluna no índice ajuda a eliminar a necessidade de pesquisar o valor de ip_max
; não ajuda na busca em árvore. Observe que a subconsulta lateral não tem
LIMIT
. Ainda não é o resultado final. Esta consulta deve retornar cerca de 6 milhões de linhas. Salve este resultado em uma tabela temporária. Adicione um índice à tabela temporária em (r_ID, n_length, n_ID)
. n_ID
é novamente apenas para remover pesquisas extras. Precisamos de um índice para evitar classificar os dados para cada r_ID
. Etapa final
Para cada linha de
routing_details
encontre o n_ID
com o menor n_length
. Agora podemos usar a junção lateral na ordem "correta". Aqui temp
tabela é resultado da etapa anterior com o índice . SELECT
routing_details.*
,t.n_ID
,netblock_details.*
FROM
routing_details
INNER JOIN LATERAL
(
SELECT temp.n_ID
FROM temp
WHERE temp.r_ID = routing_details.ID
ORDER BY temp.n_length
LIMIT 1
) AS t ON true
INNER JOIN netblock_details ON netblock_details.ID = t.n_ID
Aqui a subconsulta deve ser uma busca do índice, não uma varredura. O otimizador deve ser inteligente o suficiente para fazer apenas a busca e retornar o primeiro resultado encontrado devido ao
LIMIT 1
, então você terá 600 mil buscas de índice na tabela temporária de 6 milhões de linhas. Resposta original (vou mantê-la apenas para o diagrama de intervalos):
Você pode "enganar"?
Se entendi sua consulta corretamente, para cada
routing_details.range
você deseja encontrar um menor netblock_details.range
que cobre/é maior que routing_details.range
. Dado o exemplo a seguir, onde
r
é o intervalo de roteamento e n1, ..., n8
são intervalos de netblock, a resposta correta é n5
. |---|
n1
|------------------|
n2
|---------------|
n3
|-----|
n4
|------------------|
n5
|--------------------------------------|
n6
|---------------------------|
n7
|-----|
n8
|------------|
r
start end
n.start <= r.start AND n.end >= r.end
order by n.length
limit 1
Sua consulta que levou 14 horas mostra que a varredura de índice levou 6 ms, mas a classificação por comprimento de intervalo levou 80 ms.
Com este tipo de pesquisa não existe uma simples ordenação 1D dos dados. Você está usando
n.start
junto com n.end
e junto com n.length
. Mas, talvez seus dados não sejam tão genéricos, ou não há problema em retornar um resultado um pouco diferente. Por exemplo, se não havia problema em retornar
n6
como resultado, poderia funcionar muito mais rápido. n6
é o intervalo de cobertura que tem o maior start
:n.start <= r.start AND n.end >= r.end
order by n.start desc
limit 1
Ou você pode usar
n7
, que tem o menor end
:n.start <= r.start AND n.end >= r.end
order by n.end
limit 1
Esse tipo de pesquisa usaria o índice simples em
n.start
(ou n.end
) sem classificação extra. Uma segunda abordagem completamente diferente. Quão grandes/longos são os intervalos? Se eles forem relativamente curtos (poucos números), você pode tentar armazená-los como uma lista explícita de números inteiros, em vez de um intervalo. Por exemplo, intervalo
[5-8]
seria armazenado como 4 linhas:(5, 6, 7, 8)
. Com este modelo de armazenamento pode ser mais fácil encontrar interseções de intervalos.