Desempenho TSQL - JOIN no valor entre min e max

10

Eu tenho duas tabelas nas quais guardo:

  • um intervalo de IP - tabela de pesquisa de país
  • uma lista de solicitações provenientes de IPs diferentes

Os IPs foram armazenados como bigints para melhorar o desempenho da pesquisa.

Esta é a estrutura da tabela:

create table [dbo].[ip2country](
    [begin_ip] [varchar](15) NOT NULL,
    [end_ip] [varchar](15) NOT NULL,
    [begin_num] [bigint] NOT NULL,
    [end_num] [bigint] NOT NULL,
    [IDCountry] [int] NULL,
    constraint [PK_ip2country] PRIMARY KEY CLUSTERED 
    (
        [begin_num] ASC,
        [end_num] ASC
    )
)

create table Request(
    Id int identity primary key, 
    [Date] datetime, 
    IP bigint, 
    CategoryId int
)

Quero obter o detalhamento da solicitação por país e, portanto, faço a seguinte consulta:

select 
    ic.IDCountry,
    count(r.Id) as CountryCount
from Request r
left join ip2country ic 
  on r.IP between ic.begin_num and ic.end_num
where r.CategoryId = 1
group by ic.IDCountry

Eu tenho muitos registros nas tabelas: cerca de 200.000 IP2Countrye alguns milhões Request, portanto a consulta demora um pouco.

Observando o plano de execução, a parte mais cara é uma Busca de Índice em Cluster no índice PK_IP2Country, que é executado várias vezes (o número de linhas na Solicitação).

Além disso, algo que me parece um pouco estranho é a left join ip2country ic on r.IP between ic.begin_num and ic.end_numparte (não sei se há uma maneira melhor de executar a pesquisa).

A estrutura da tabela, alguns dados de amostra e consulta estão disponíveis no SQLFiddle: http://www.sqlfiddle.com/#!3/a463e/3 (infelizmente não acho que posso inserir muitos registros para reproduzir o problema, mas isso espero que dê uma idéia).

Como (obviamente) eu não sou especialista em otimizações / desempenho de SQL, minha pergunta é: existem formas óbvias pelas quais essa estrutura / consulta possa ser aprimorada em termos de desempenho que estou perdendo?

Cristian Lupascu
fonte
2
Um endereço IP pode mapear para vários países? Caso contrário, você pode restringir seu PK a apenas begin_num. Eu também tenho que participar com A BETWEEN B AND Cbastante frequência, e estou curioso para saber se há uma maneira de conseguir isso sem a junção tediosa do RBAR.
Jon of All Trades
11
É um pouco estranho à sua pergunta, mas eu consideraria criar begin_ipe end_ipmanter colunas calculadas, para evitar a possibilidade de o texto e os números ficarem fora de sincronia de alguma forma.
Jon of All Trades
@ w0lf: existem faixas sobrepostas ip2country (begin_num, end_num)?
ypercubeᵀᴹ
@ JonofAllTrades normalmente um IP deve pertencer a um único país, então acho que sua ideia de uma consulta como give me the first record that has a begin_num < ip in asc order of begin_num(corrija-me se estiver errado) pode ser válida e melhorar o desempenho.
Cristian Lupascu
11
@ w0lf: Minhas impressões são de que é basicamente o que o servidor está fazendo em um caso como esse, porque ele primeiro verifica begin_nume depois verifica end_numdentro desse conjunto e encontra apenas um registro.
Jon of All Trades

Respostas:

3

Você precisa de um índice adicional. No seu exemplo do Fiddle, eu adicionei:

CREATE UNIQUE INDEX ix_IP ON Request(CategoryID, IP)

Que abrange você para a tabela de solicitação e obtém uma busca de índice em vez de uma varredura de índice em cluster.

Veja como isso melhora e me avise. Acho que vai ajudar um pouco, já que a verificação desse índice é, com certeza, não é barata.

JNK
fonte
Eu não sei por que, mas os resultados parecem ser diferentes (em SQLFiddle)
Cristian Lupascu
@ w0lf: eles são diferentes (provavelmente) porque você está inserindo dados aleatórios nas tabelas.
ypercubeᵀᴹ
@ypercube certamente essa é a causa. Ultimamente, fiz tantas coisas que esqueci que os dados eram aleatórios. Desculpa.
Cristian Lupascu
2

Sempre existe a abordagem da força bruta: você pode explodir seu mapa IP. Associe uma tabela de números ao seu mapa existente para criar um registro por endereço IP. São apenas 267 mil registros com base nos dados do Fiddle, sem nenhum problema.

CREATE TABLE IPLookup
  (
  IP  BIGINT PRIMARY KEY,
  CountryID  INT
  )
INSERT INTO IPLookup (IP, CountryID)
  SELECT
    N.Number, Existing.IDCountry
  FROM
    ip2country AS Existing
    INNER JOIN Numbers AS N ON N.Number BETWEEN Existing.begin_num AND Existing.end_num

Isso tornaria as buscas mais simples e, esperançosamente, mais rápidas. Isso só faz sentido se você fizer relativamente poucas atualizações ip2country, é claro.

Espero que alguém tenha uma solução melhor!

Jon de todos os comércios
fonte
Todo o conjunto de dados produziria mais de 5 bilhões de registros, então acho que não o farei. Mas essa é uma boa idéia, no entanto; Tenho certeza de que é viável em muitos casos semelhantes. +1
Cristian Lupascu
0

Tente o seguinte:

SELECT ic.IDCountry,
        COUNT(r.Id) AS CountryCount
FROM Request r
INNER JOIN (SELECT begin_num+NUMS.N [IP], IDCountry 
            FROM ip2country
            CROSS JOIN (SELECT TOP(SELECT ABS(MAX(end_num-begin_num)) FROM ip2country) ROW_NUMBER() OVER(ORDER BY sc.name)-1 [N]
                        FROM sys.columns sc) NUMS
            WHERE begin_num+NUMS.N <= end_num) ic
ON r.IP = ic.IP
WHERE r.CategoryId = 1
GROUP BY ic.IDCountry
Vince Pergolizzi
fonte
obrigado, eu já tentou a sua abordagem, mas parece ser mais caro do que a consulta inicial
Cristian Lupascu
Quantas linhas você tem em cada tabela? Gostaria de reproduzir a escala de seu problema no meu DB e tentar resolver sem adicionar um índice :)
Vince Pergolizzi
cerca de 200.000 no IP2Country e alguns milhões (possivelmente dezenas de milhões no futuro próximo) em Request. Acho que se você resolvê-lo sem índices que você merece um "DBA do ano" title :)
Cristian Lupascu