PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Unindo 2 grandes tabelas postgres usando int8range não dimensionando bem


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 de netblock_details para cada intervalo de routing_details . Você disse que existem várias entradas em netblock_details para cada entrada em routing_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 pelo routing_details intervalos e para cada intervalo encontre o melhor (menor) intervalo de cobertura de netblock_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.