Vamos criar duas tabelas de teste em um banco de dados PostgreSQL 13:
CREATE TABLE foo (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
value int NOT NULL
);
CREATE TABLE bar (
id bigint PRIMARY KEY,
category_id bigint NOT NULL,
foo_id bigint REFERENCES foo (id),
value int
);
CREATE INDEX bar_category_id_ix ON bar (category_id);
e desative autovacuum
para estas tabelas:
ALTER TABLE foo SET (autovacuum_enabled = false);
ALTER TABLE bar SET (autovacuum_enabled = false);
Insira 500.000 (meio milhão) registros em foo
, transfira-os bar
e analise as tabelas:
INSERT INTO foo (value) SELECT * FROM generate_series(1, 500000);
ANALYZE foo;
INSERT INTO bar (id, category_id, foo_id, value) SELECT id, 1, id, value FROM foo WHERE value <= 500000;
ANALYZE bar;
Opcionalmente, assegure-se de que apenas ANALYZE
(não autovacuum
) foi executado nestas tabelas:
SELECT relname, last_autovacuum, last_vacuum, last_autoanalyze, last_analyze FROM pg_stat_user_tables WHERE relname IN ('foo', 'bar');
Insira outro pedaço de 500.000 registros (mas não execute ANALYZE
):
INSERT INTO foo (value) SELECT * FROM generate_series(500001, 1000000);
INSERT INTO bar (id, category_id, foo_id, value) SELECT id, 2, id, value FROM foo WHERE value > 500000;
Como não executamos ANALYZE
as estatísticas da tabela está desatualizada, está relacionada ao estágio em que foo
e bar
continha meio milhão de registros. Agora vamos verificar os planos de consulta:
EXPLAIN SELECT * FROM bar
JOIN foo ON bar.foo_id = foo.id
WHERE category_id = 2;
----
Nested Loop (cost=0.85..12.89 rows=1 width=40)
-> Index Scan using bar_category_id_ix on bar (cost=0.42..4.44 rows=1 width=28)
Index Cond: (category_id = 2)
-> Index Scan using foo_pkey on foo (cost=0.42..8.44 rows=1 width=12)
Index Cond: (id = bar.foo_id)
e
EXPLAIN SELECT * FROM bar
JOIN foo ON bar.foo_id = foo.id;
---
Hash Join (cost=32789.00..71320.29 rows=999864 width=40)
Hash Cond: (bar.foo_id = foo.id)
-> Seq Scan on bar (cost=0.00..17351.64 rows=999864 width=28)
-> Hash (cost=15406.00..15406.00 rows=1000000 width=12)
-> Seq Scan on foo (cost=0.00..15406.00 rows=1000000 width=12)
Entendo que o 1º plano de consulta estimou erroneamente apenas 1 linha ( rows=1
) para condição category_id = 2
porque a estatística está desatualizada (o ANALYZE
foi realizado antes de inserir registros com category_id = 2
). (1) Mas então, como o 2º plano de consulta chegou a uma boa estimativa ( rows=999864
) para condição bar.foo_id = foo.id
?
Também se executarmos:
EXPLAIN SELECT * FROM bar
JOIN foo ON bar.foo_id = foo.id
WHERE category_id = 1;
----
Hash Join (cost=32789.00..73819.95 rows=999864 width=40)
Hash Cond: (bar.foo_id = foo.id)
-> Seq Scan on bar (cost=0.00..19851.30 rows=999864 width=28)
Filter: (category_id = 1)
-> Hash (cost=15406.00..15406.00 rows=1000000 width=12)
-> Seq Scan on foo (cost=0.00..15406.00 rows=1000000 width=12)
(2) Por que o planejador estima 999864 linhas para condição category_id = 1
? As estatísticas devem mostrar cerca de 500.000 linhas satisfazendo isso?
NOTA: Cheguei a essas questões porque empiricamente observei que condições contendo apenas colunas de chave primária produzirão um melhor plano de consulta mesmo que a tabela não tenha sido analisada, mas não encontrei nada sobre esse comportamento na documentação oficial do PostgreSQL.
A "mágica" que você observou está nesse detalhe do planejador de consultas. Citando o manual:
Os valores
pg_class
sãoreltuples
erelpages
- o número de linhas ativas e páginas de dados no disco.Como o tamanho físico terá aproximadamente dobrado, o Postgres esperará aproximadamente esse número de linhas, o que explica a estimativa bastante precisa para a varredura sequencial.
As frequências de valor em
pg_statistic
estão desatualizadas e não podem ser dimensionadas tão trivialmente quanto a contagem de linhas. Você precisaria correrANALYZE
para isso. Explica a estimativa para a varredura de índice.Não vejo como o PK desempenharia algum papel especial nisso.