Percebi que o desempenho de uma consulta envolvendo uma coluna jsonb variou significativamente entre as execuções de VACUUM ANALYZE durante o teste. Eu recebo planos de execução completamente diferentes aparentemente aleatoriamente depois de analisar a tabela.
Estou usando o Postgres 9.6 aqui. A configuração para meus testes é a seguinte, estou inserindo uma única chave "x" na coluna jsonb "params" com valores entre 1 e 6, sendo 1 o valor mais raro e 6 o mais comum. Eu também tenho uma coluna int regular "single_param" que contém a mesma distribuição de valores para comparação.:
CREATE TABLE test_data (
id serial,
single_param int,
params jsonb
);
INSERT INTO test_data
SELECT
generate_series(1, 1000000) AS id,
floor(log(random() * 9999999 + 1)) AS single_param,
json_build_object(
'x', floor(log(random() * 9999999 + 1))
) AS params;
CREATE INDEX idx_test_btree ON test_data (cast(test_data.params->>'x' AS int));
CREATE INDEX idx_test_gin ON test_data USING GIN (params);
CREATE INDEX ON test_data(id)
CREATE INDEX ON test_data(single_param)
A consulta que estou testando é uma consulta típica para paginar resultados, estou classificando por id e limitando a saída às primeiras 50 linhas.
SELECT * FROM test_data where (params->>'x')::int = 1 ORDER BY id DESC LIMIT 50;
Eu recebo uma das duas saídas de análise de explicação aleatoriamente após a execução VACUUM ANALYZE
:
Limit (cost=0.42..836.59 rows=50 width=33) (actual time=39.679..410.292 rows=10 loops=1)
-> Index Scan Backward using test_data_id_idx on test_data (cost=0.42..44317.43 rows=2650 width=33) (actual time=39.678..410.283 rows=10 loops=1)
Filter: (((params ->> 'x'::text))::integer = 1)
Rows Removed by Filter: 999990"
Planning time: 0.106 ms
Execution time: 410.314 ms
ou
Limit (cost=8.45..8.46 rows=1 width=33) (actual time=0.032..0.034 rows=10 loops=1)
-> Sort (cost=8.45..8.46 rows=1 width=33) (actual time=0.032..0.032 rows=10 loops=1)
Sort Key: id DESC
Sort Method: quicksort Memory: 25kB
-> Index Scan using idx_test_btree on test_data (cost=0.42..8.44 rows=1 width=33) (actual time=0.007..0.016 rows=10 loops=1)
Index Cond: (((params ->> 'x'::text))::integer = 1)
Planning time: 0.320 ms
Execution time: 0.052 ms
A diferença é que a estimativa para o número de colunas correspondentes à cláusula where é diferente entre os dois planos. Na primeira a estimativa é de 2650 linhas, na segunda 1 linha enquanto o número real é de 10 linhas.
A seguinte versão da consulta que pode potencialmente usar o índice GIN parece usar uma estimativa padrão para a coluna json de 1%, o que também resulta no plano de consulta incorreto como acima:
SELECT * FROM test_data where params @> '{"x": 1}' ORDER BY id DESC LIMIT 50;
Minha suposição original era que o Postgres não teria nenhuma estatística na coluna jsonb e sempre usaria uma estimativa como faz para a consulta usando o @>
operador. Mas para a consulta que é escrita poder usar o índice btree que criei, ela usa estimativas diferentes. Às vezes, esses são bons o suficiente, e às vezes são ruins.
De onde vêm essas estimativas? Eu acho que eles são algum tipo de estatística que o Postgres cria com o índice. Para estatísticas de coluna existe a opção de coletar estatísticas mais precisas, existe algo assim para essas estatísticas aqui? Ou alguma outra maneira de fazer com que o Postgres escolha o melhor plano no meu caso?