Importei uma cópia do banco de dados ip2location_db11 lite , que contém 3.319.097 linhas, e estou procurando otimizar uma consulta de intervalo numérico, onde os valores baixo e alto estão em colunas separadas da tabela ( ip_from
, ip_to
).
Importando o banco de dados:
CREATE TABLE ip2location_db11
(
ip_from bigint NOT NULL, -- First IP address in netblock.
ip_to bigint NOT NULL, -- Last IP address in netblock.
country_code character(2) NOT NULL, -- Two-character country code based on ISO 3166.
country_name character varying(64) NOT NULL, -- Country name based on ISO 3166.
region_name character varying(128) NOT NULL, -- Region or state name.
city_name character varying(128) NOT NULL, -- City name.
latitude real NOT NULL, -- City latitude. Default to capital city latitude if city is unknown.
longitude real NOT NULL, -- City longitude. Default to capital city longitude if city is unknown.
zip_code character varying(30) NOT NULL, -- ZIP/Postal code.
time_zone character varying(8) NOT NULL, -- UTC time zone (with DST supported).
CONSTRAINT ip2location_db11_pkey PRIMARY KEY (ip_from, ip_to)
);
\copy ip2location_db11 FROM 'IP2LOCATION-LITE-DB11.CSV' WITH CSV QUOTE AS '"';
Minha primeira tentativa de indexação ingênua foi criar índices separados em cada uma dessas colunas, o que resultou em uma varredura sequencial com tempos de consulta de 400ms:
account=> CREATE INDEX ip_from_db11_idx ON ip2location_db11 (ip_from);
account=> CREATE INDEX ip_to_db11_idx ON ip2location_db11 (ip_to);
account=> EXPLAIN ANALYZE VERBOSE SELECT * FROM ip2location_db11 WHERE 2538629520 BETWEEN ip_from AND ip_to;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
Seq Scan on public.ip2location_db11 (cost=0.00..48930.99 rows=43111 width=842) (actual time=286.714..401.805 rows=1 loops=1)
Output: ip_from, ip_to, country_code, country_name, region_name, city_name, latitude, longitude, zip_code, time_zone
Filter: (('2538629520'::bigint >= ip2location_db11.ip_from) AND ('2538629520'::bigint <= ip2location_db11.ip_to))
Rows Removed by Filter: 3319096
Planning time: 0.155 ms
Execution time: 401.834 ms
(6 rows)
account=> \d ip2location_db11
Table "public.ip2location_db11"
Column | Type | Modifiers
--------------+------------------------+-----------
ip_from | bigint | not null
ip_to | bigint | not null
country_code | character(2) | not null
country_name | character varying(64) | not null
region_name | character varying(128) | not null
city_name | character varying(128) | not null
latitude | real | not null
longitude | real | not null
zip_code | character varying(30) | not null
time_zone | character varying(8) | not null
Indexes:
"ip2location_db11_pkey" PRIMARY KEY, btree (ip_from, ip_to)
"ip_from_db11_idx" btree (ip_from)
"ip_to_db11_idx" btree (ip_to)
Minha segunda tentativa foi criar um índice btree de várias colunas, que resultou em uma verificação de índice com tempos de consulta de 290ms:
account=> CREATE INDEX ip_range_db11_idx ON ip2location_db11 (ip_from,ip_to);
account=> EXPLAIN ANALYZE VERBOSE SELECT * FROM ip2location_db11 WHERE 2538629520 BETWEEN ip_from AND ip_to;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
Index Scan using ip_to_db11_idx on public.ip2location_db11 (cost=0.43..51334.91 rows=756866 width=69) (actual time=1.109..289.143 rows=1 loops=1)
Output: ip_from, ip_to, country_code, country_name, region_name, city_name, latitude, longitude, zip_code, time_zone
Index Cond: ('2538629520'::bigint <= ip2location_db11.ip_to)
Filter: ('2538629520'::bigint >= ip2location_db11.ip_from)
Rows Removed by Filter: 1160706
Planning time: 0.324 ms
Execution time: 289.172 ms
(7 rows)
n4l_account=> \d ip2location_db11
Table "public.ip2location_db11"
Column | Type | Modifiers
--------------+------------------------+-----------
ip_from | bigint | not null
ip_to | bigint | not null
country_code | character(2) | not null
country_name | character varying(64) | not null
region_name | character varying(128) | not null
city_name | character varying(128) | not null
latitude | real | not null
longitude | real | not null
zip_code | character varying(30) | not null
time_zone | character varying(8) | not null
Indexes:
"ip2location_db11_pkey" PRIMARY KEY, btree (ip_from, ip_to)
"ip_from_db11_idx" btree (ip_from)
"ip_range_db11_idx" btree (ip_from, ip_to)
"ip_to_db11_idx" btree (ip_to)
Atualização : Conforme solicitado nos comentários, refazer a consulta acima. O tempo das primeiras 15 consultas após a recriação da tabela (165ms, 65ms, 86ms, 83ms, 86ms, 64ms, 85ms, 811ms, 868ms, 845ms, 810ms, 781ms, 797ms, 890ms, 806ms):
account=> EXPLAIN (ANALYZE, VERBOSE, BUFFERS, TIMING) SELECT * FROM ip2location_db11 WHERE 2538629520 BETWEEN ip_from AND ip_to;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.ip2location_db11 (cost=28200.29..76843.12 rows=368789 width=842) (actual time=64.866..64.866 rows=1 loops=1)
Output: ip_from, ip_to, country_code, country_name, region_name, city_name, latitude, longitude, zip_code, time_zone
Recheck Cond: (('2538629520'::bigint >= ip2location_db11.ip_from) AND ('2538629520'::bigint <= ip2location_db11.ip_to))
Heap Blocks: exact=1
Buffers: shared hit=8273
-> Bitmap Index Scan on ip_range_db11_idx (cost=0.00..28108.09 rows=368789 width=0) (actual time=64.859..64.859 rows=1 loops=1)
Index Cond: (('2538629520'::bigint >= ip2location_db11.ip_from) AND ('2538629520'::bigint <= ip2location_db11.ip_to))
Buffers: shared hit=8272
Planning time: 0.099 ms
Execution time: 64.907 ms
(10 rows)
account=> EXPLAIN (ANALYZE, VERBOSE, BUFFERS, TIMING) SELECT * FROM ip2location_db11 WHERE 2538629520 BETWEEN ip_from AND ip_to;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------
Seq Scan on public.ip2location_db11 (cost=0.00..92906.18 rows=754776 width=69) (actual time=577.234..811.757 rows=1 loops=1)
Output: ip_from, ip_to, country_code, country_name, region_name, city_name, latitude, longitude, zip_code, time_zone
Filter: (('2538629520'::bigint >= ip2location_db11.ip_from) AND ('2538629520'::bigint <= ip2location_db11.ip_to))
Rows Removed by Filter: 3319096
Buffers: shared hit=33 read=43078
Planning time: 0.667 ms
Execution time: 811.783 ms
(7 rows)
Linhas de amostra do arquivo CSV importado:
"0","16777215","-","-","-","-","0.000000","0.000000","-","-"
"16777216","16777471","AU","Australia","Queensland","Brisbane","-27.467940","153.028090","4000","+10:00"
"16777472","16778239","CN","China","Fujian","Fuzhou","26.061390","119.306110","350004","+08:00"
Existe uma maneira melhor de indexar esta tabela que melhoraria a consulta ou existe uma consulta mais eficiente que me daria o mesmo resultado?
Esta é uma solução um pouco diferente daquelas já oferecidas que envolviam o uso de índices espaciais para fazer alguns truques.
Em vez disso, vale a pena lembrar que, com endereços IP, você não pode ter intervalos sobrepostos. Isso
A -> B
não pode se cruzarX -> Y
de forma alguma. Sabendo disso, você pode alterar umSELECT
pouco sua consulta e aproveitar essa característica. Ao tirar proveito dessa característica, você não precisa ter nenhuma indexação "inteligente". Na verdade, você só precisa indexar suaip_from
coluna.Anteriormente, a consulta que estava sendo analisada era:
Vamos supor que o intervalo que
2538629520
se enquadra seja2538629512
e2538629537
.A partir disso, podemos supor que o próximo
ip_from
valor é2538629538
. Na verdade, não precisamos nos preocupar com nenhum registro acima desseip_from
valor. Na verdade, tudo o que realmente importa é o intervalo ondeip_from
é igual2538629512
a .Sabendo desse fato, nossa consulta realmente se torna (em inglês):
Como nunca temos intervalos sobrepostos,
ip_from
issoip_to
é verdade e nos permite escrever a consulta como:Volte para a indexação para aproveitar tudo isso. Na verdade, tudo o que estamos vendo é ip_from e estamos fazendo comparações de inteiros. O MIN(ip_from) faz com que o PostgreSQL encontre o primeiro registro disponível. Isso é bom porque podemos buscar isso e não nos preocuparmos com nenhum outro registro.
Tudo o que realmente precisamos é de um índice como:
CREATE UNIQUE INDEX CONCURRENTLY ix_ip2location_ipFrom ON public.ip2location(ip_from)
Podemos tornar o índice único porque não teremos registros sobrepostos. Eu mesmo tornaria essa coluna a chave primária.
Com este índice e esta consulta, o plano de explicação é:
Para dar uma ideia de melhoria no desempenho da consulta com essa abordagem, testei isso no meu Raspberry Pi. A abordagem original levou aproximadamente 4 segundos. Essa abordagem leva aproximadamente 120ms. A grande vitória é da fila individual procura versos alguns scans. A consulta original sofreria EXTREMAMENTE com valores de intervalo baixos, pois mais da tabela precisa ser considerada nos resultados. Essa consulta exibirá um desempenho consistente em todo o intervalo de valores.
Espero que isso ajude e minha explicação faça sentido para todos vocês.
Graças a um comentário, tenho uma solução que reduziu o tempo de consulta para 0,073ms usando um índice espacial gist e ajustando a consulta de acordo:
Citações:
http://www.siafoo.net/article/53#comment_288
http://www.pgsql.cz/index.php/PostgreSQL_SQL_Tricks#Fast_interval_.28of_time_or_ip_addresses.29_searching_with_spatial_indexes
ip4r
Primeiro, construa adicione a extensão (melhores instruções) no Github.
Vamos começar com quase a mesma coisa que você tinha antes, crie os tipos de ip como
ip4
alternativa. Não faça nada aPRIMARY KEY
e não adicione índices nos tipos. Vamos mudar a tabela após o carregamento.Agora vamos atualizá-los para um
ip4r
Agora vamos indexá-lo
E questione sobre isso,