Configurei um servidor PostgreSQL FDW com a seguinte tabela, fragmentada por user_id
mais de quatro servidores:
CREATE TABLE my_big_table
(
user_id bigint NOT NULL,
serial bigint NOT NULL, -- external, incrementing only
some_object_id bigint NOT NULL,
timestamp_ns bigint NOT NULL,
object_type smallint NOT NULL,
other_type smallint NOT NULL,
data bytea
) PARTITION BY HASH (user_id) ;
CREATE SERVER shardA
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '192.168.200.11', port '5432', dbname 'postgres', fetch_size '10000');
.
.
.
CREATE SERVER shardD
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '192.168.200.14', port '5432', dbname 'postgres', fetch_size '10000');
create foreign table my_big_table_mod4_s0 partition of my_big_table
FOR VALUES WITH (MODULUS 4, REMAINDER 0) server shardA
OPTIONS (table_name 'my_big_table_mod4_s0');
.
.
.
create foreign table my_big_table_mod4_s3 partition of my_big_table
FOR VALUES WITH (MODULUS 4, REMAINDER 3) server shardD
OPTIONS (table_name 'my_big_table_mod4_s3');
Nos servidores de back-end, configurei uma tabela com vários índices, seus dados agrupados (user_id, serial)
em várias partições. No entanto, não acho que esses detalhes sejam muito relevantes para minha pergunta real.
A consulta comum no meu cluster está no padrão de:
SELECT * from my_big_table
WHERE
user_id = 12345 -- only 1 user, always! --> single foreign server.
ORDER BY serial DESC -- get 'newest' 90% of the time, 10% ASC
LIMIT 1000; -- max limit 1000, sometimes less
Para usuários com < 1000 registros: tudo bem, sem problemas.
Para usuários com > 100.000 registros, vejo o problema que leva a um desempenho ruim: explain
mostra LIMIT e a classificação acontece no FDW, não é pressionado. Por quê?
Limit (cost=927393.08..927395.58 rows=1000 width=32)
Output: my_big_table_mod4_s0.serial, my_big_table_mod4_s0.some_object_id, my_big_table_mod4_s0.timestamp_ns, my_big_table_mod4_s0.object_type, my_big_table_mod4_s0.other_type, (length(my_big_table_mod4_s0.data))
-> Sort (cost=927393.08..931177.06 rows=1513592 width=32)
Output: my_big_table_mod4_s0.serial, my_big_table_mod4_s0.some_object_id, my_big_table_mod4_s0.timestamp_ns, my_big_table_mod4_s0.object_type, my_big_table_mod4_s0.other_type, (length(my_big_table_mod4_s0.data))
Sort Key: my_big_table_mod4_s0.serial DESC
-> Foreign Scan on public.my_big_table_mod4_s0 (cost=5318.35..844404.46 rows=1513592 width=32)
Output: my_big_table_mod4_s0.serial, my_big_table_mod4_s0.some_object_id, my_big_table_mod4_s0.timestamp_ns, my_big_table_mod4_s0.object_type, my_big_table_mod4_s0.other_type, length(my_big_table_mod4_s0.data)
Remote SQL: SELECT serial, some_object_id, timestamp_ns, object_type, other_type, data FROM public.my_big_table_mod4_s0 WHERE ((user_id = 4560084))
JIT:
Functions: 3
Options: Inlining true, Optimization true, Expressions true, Deforming true
O takeaway do acima é:
- Servidor de back-end único selecionado: OK! (resolvido com isso )
Remote SQL: SELECT [...]
indica que não há ORDER BY, nem LIMIT. Problema.
Executado no servidor back-end mostra isso diretamente:
Limit (cost=1.74..821.42 rows=1000 width=32)
Output: my_big_table_mod4_s0_part123.serial, my_big_table_mod4_s0_part123.some_object_id, my_big_table_mod4_s0_part123.timestamp_ns, my_big_table_mod4_s0_part123.object_type, my_big_table_mod4_s0_part123.other_type, (length(my_big_table_mod4_s0_part123.data))
-> Append (cost=1.74..1240669.45 rows=1513592 width=32)
-> Index Scan Backward using my_big_table_mod4_s0_part123_pkey on public.my_big_table_mod4_s0_part123 (cost=0.43..290535.67 rows=355620 width=32)
Output: my_big_table_mod4_s0_part123.serial, my_big_table_mod4_s0_part123.some_object_id, my_big_table_mod4_s0_part123.timestamp_ns, my_big_table_mod4_s0_part123.object_type, my_big_table_mod4_s0_part123.other_type, length(my_big_table_mod4_s0_part123.data)
Index Cond: (my_big_table_mod4_s0_part123.user_id = 4560084)
-> Index Scan Backward using [... other partitions ...]
O que eu tentei:
- Como o FDW ainda está ativo em desenvolvimento, tentei usar uma versão mais recente: 11.4 e 12-beta2 tanto para FDW quanto para servidores backend. Nenhuma diferença observada.
- Executando ANALYZE na tabela externa (na instância FDW). Leva muito tempo; parece que está cheio de tabelas digitalizando a tabela remota? Nenhuma diferença no planejamento de consultas.
- Alterando o valor de
fetch_size
no objeto SERVER remoto. Nenhuma diferença. - Definido
use_remote_estimate=true
no objeto SERVER remoto. Nenhuma diferença. - Definido
fdw_tuple_cost=100
no objeto SERVER remoto. A classificação agora acontece no servidor remoto, mas LIMIT ainda não foi pressionado. Procurando online por outras pessoas vendo isso, mostrando apenas este post relacionado: Missed LIMIT cláusula pushdown na API FDW
Mas este tópico menciona commits corrigindo isso em 9.7 e outros enfeites, mas estou usando 11.4 e 12-beta2. Ou eu entendo isso errado?
E post: Estimativa de custo estranho para tabelas estrangeiras mostra um bom exemplo de ajuste de FDW, mas infelizmente não cobre meu problema com LIMITs.
Dando uma rápida olhada no código fonte do PostgreSQL, notei esta declaração, talvez relevante para FDW, talvez não ( source ).
Não podemos enviar subseleções contendo LIMIT/OFFSET para os trabalhadores, pois não há garantia de que a ordem das linhas seja totalmente determinística, e a aplicação de LIMIT/OFFSET levará a resultados inconsistentes no nível superior. (Em alguns casos, onde o resultado é ordenado, podemos relaxar essa restrição. Mas atualmente não parece valer a pena gastar um esforço extra para fazê-lo.)
Dando outra olhada no código-fonte, encontrei este commit promissor ( d50d172e51 ):
Isso fornece a capacidade do postgres_fdw de lidar com comandos SELECT para que 1) pule a etapa LockRows (se houver) (observe que isso é seguro, pois executa o bloqueio antecipado) e 2) reduz as restrições LIMIT e/ou OFFSET (se houver) ) para o lado remoto. Isso não lida com os casos INSERT/UPDATE/DELETE.
Ele adiciona um caso de teste de unidade exatamente para o meu caso!
-- and both ORDER BY and LIMIT can be shipped EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 === t1.c2 order by t1.c2 limit 1; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------ Foreign Scan on public.ft1 t1 Output: c1, c2, c3, c4, c5, c6, c7, c8 Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(public.===) c2)) ORDER BY c2 ASC NULLS LAST LIMIT 1::bigint
que deve fazer parte do 12-beta2, que já estou rodando...
Percebi que deveria usar índices na ordem DESC aqui, mas isso não é tão relevante agora.
Isso funciona como esperado desde a versão 12 com commit d50d172e51 , mas apenas para tabelas não particionadas (não fragmentadas).
Executando a consulta diretamente no nome da tabela estrangeira (
my_big_table_mod4_s0
), o LIMIT é pressionado corretamente.Relatado como um bug
; Não vejo uma razão técnica para que isso não funcione com o particionamento (remoção de partição) envolvido.Atualização: acontece que isso não é realmente um bug, dada a complexidade do planejador e da poda de partição em combinação com o FDW, mas mais uma solicitação de recurso. O autor original do commit mencionado acima indica que o trabalho sobre isso pode ser feito para o PostgreSQL 13 . :-)
Lição aprendida: o FDW não é realmente um roteador de consulta eficiente para todos os tipos de consultas (ainda).
A solução alternativa por enquanto com tabelas particionadas (fragmentadas) no FDW para mim é criar uma função no plpgsql para determinar o nome da tabela estrangeira com base no layout de particionamento declarativo (
mod(user_id, 4)
no meu caso). (Acredito que está ficando fora do escopo incluir isso completamente aqui.)