Use EXPLAIN e EXPLAIN ANALYZE para inspecionar como o Hologres executa uma consulta SQL. Quando uma consulta apresenta baixo desempenho ou retorna resultados inesperados, esses comandos exibem o plano de execução para que você identifique gargalos e ajuste o SQL ou o design das tabelas.
Os planos de execução no Hologres V1.3.4x e versões posteriores são mais claros e legíveis. Este documento baseia-se na versão V1.3.4x. Atualize sua instância para a V1.3.4x ou superior para acompanhar os exemplos.
Como funciona
Toda instrução SQL passa por duas etapas:
O otimizador de consultas (QO) analisa a consulta e gera um plano de execução.
O mecanismo de consulta (QE) recebe esse plano, executa-o e retorna os resultados.
O plano de execução descreve os operadores que o QE utiliza, a ordem do fluxo de dados entre eles e o custo estimado de cada etapa. Um plano bem elaborado consome menos recursos e retorna resultados mais rapidamente. Por isso, compreender os planos de execução é fundamental para o ajuste de SQL.
O Hologres é compatível com PostgreSQL e utiliza a mesma sintaxe de EXPLAIN / EXPLAIN ANALYZE:
|
Comando |
O que exibe |
|
|
O plano estimado pelo QO. A consulta não é executada. |
|
|
O plano de tempo de execução real. A consulta é executada e exibe medições reais de tempo, contagem de linhas e memória. |
Use EXPLAIN para uma estimativa rápida. Use EXPLAIN ANALYZE para diagnósticos precisos.
EXPLAIN
Sintaxe
EXPLAIN <sql>;
Exemplo
O exemplo a seguir utiliza uma consulta TPC-H apenas para fins ilustrativos e não representa resultados oficiais de benchmark do TPC-H.
EXPLAIN SELECT
l_returnflag,
l_linestatus,
sum(l_quantity) AS sum_qty,
sum(l_extendedprice) AS sum_base_price,
sum(l_extendedprice * (1 - l_discount)) AS sum_disc_price,
sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) AS sum_charge,
avg(l_quantity) AS avg_qty,
avg(l_extendedprice) AS avg_price,
avg(l_discount) AS avg_disc,
count(*) AS count_order
FROM
lineitem
WHERE
l_shipdate <= date '1998-12-01' - interval '120' day
GROUP BY
l_returnflag,
l_linestatus
ORDER BY
l_returnflag,
l_linestatus;
Saída:
QUERY PLAN
Sort (cost=0.00..7795.30 rows=3 width=80)
Sort Key: l_returnflag, l_linestatus
-> Gather (cost=0.00..7795.27 rows=3 width=80)
-> Project (cost=0.00..7795.27 rows=3 width=80)
-> Project (cost=0.00..7794.27 rows=3 width=104)
-> Final HashAggregate (cost=0.00..7793.27 rows=3 width=76)
Group Key: l_returnflag, l_linestatus
-> Redistribution (cost=0.00..7792.95 rows=1881 width=76)
Hash Key: l_returnflag, l_linestatus
-> Partial HashAggregate (cost=0.00..7792.89 rows=1881 width=76)
Group Key: l_returnflag, l_linestatus
-> Local Gather (cost=0.00..7791.81 rows=44412 width=76)
-> Decode (cost=0.00..7791.80 rows=44412 width=76)
-> Partial HashAggregate (cost=0.00..7791.70 rows=44412 width=76)
Group Key: l_returnflag, l_linestatus
-> Project (cost=0.00..3550.73 rows=584421302 width=33)
-> Project (cost=0.00..2585.43 rows=584421302 width=33)
-> Index Scan using Clustering_index on lineitem (cost=0.00..261.36 rows=584421302 width=25)
Segment Filter: (l_shipdate <= '1998-08-03 00:00:00+08'::timestamp with time zone)
Cluster Filter: (l_shipdate <= '1998-08-03 00:00:00+08'::timestamp with time zone)
Leitura da saída
Leia o plano de baixo para cima. Cada -> representa um nó. Os dados fluem para cima, dos nós folha (fontes de dados reais) até o nó raiz (saída final).
Cada nó exibe três estimativas do otimizador:
|
Campo |
Descrição |
|
|
Tempo de execução estimado do operador, exibido como |
|
|
Número estimado de linhas de saída, baseado nas estatísticas da tabela. Para operações de varredura, a estimativa padrão é |
|
|
Largura média estimada da linha em bytes. Valores maiores indicam linhas de saída mais largas. |
EXPLAIN ANALYZE
Sintaxe
EXPLAIN ANALYZE <sql>;
Exemplo
EXPLAIN ANALYZE SELECT
l_returnflag,
l_linestatus,
sum(l_quantity) AS sum_qty,
sum(l_extendedprice) AS sum_base_price,
sum(l_extendedprice * (1 - l_discount)) AS sum_disc_price,
sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) AS sum_charge,
avg(l_quantity) AS avg_qty,
avg(l_extendedprice) AS avg_price,
avg(l_discount) AS avg_disc,
count(*) AS count_order
FROM
lineitem
WHERE
l_shipdate <= date '1998-12-01' - interval '120' day
GROUP BY
l_returnflag,
l_linestatus
ORDER BY
l_returnflag,
l_linestatus;
Saída:
QUERY PLAN
Sort (cost=0.00..7795.30 rows=3 width=80)
Sort Key: l_returnflag, l_linestatus
[id=21 dop=1 time=2427/2427/2427ms rows=4(4/4/4) mem=3/3/3KB open=2427/2427/2427ms get_next=0/0/0ms]
-> Gather (cost=0.00..7795.27 rows=3 width=80)
[20:1 id=100003 dop=1 time=2426/2426/2426ms rows=4(4/4/4) mem=1/1/1KB open=0/0/0ms get_next=2426/2426/2426ms]
-> Project (cost=0.00..7795.27 rows=3 width=80)
[id=19 dop=20 time=2427/2426/2425ms rows=4(1/0/0) mem=87/87/87KB open=2427/2425/2425ms get_next=1/0/0ms]
-> Project (cost=0.00..7794.27 rows=0 width=104)
-> Final HashAggregate (cost=0.00..7793.27 rows=3 width=76)
Group Key: l_returnflag, l_linestatus
[id=16 dop=20 time=2427/2425/2424ms rows=4(1/0/0) mem=574/570/569KB open=2427/2425/2424ms get_next=1/0/0ms]
-> Redistribution (cost=0.00..7792.95 rows=1881 width=76)
Hash Key: l_returnflag, l_linestatus
[20:20 id=100002 dop=20 time=2427/2424/2423ms rows=80(20/4/0) mem=3528/1172/584B open=1/0/0ms get_next=2426/2424/2423ms]
-> Partial HashAggregate (cost=0.00..7792.89 rows=1881 width=76)
Group Key: l_returnflag, l_linestatus
[id=12 dop=20 time=2428/2357/2256ms rows=80(4/4/4) mem=574/574/574KB open=2428/2357/2256ms get_next=1/0/0ms]
-> Local Gather (cost=0.00..7791.81 rows=44412 width=76)
[id=11 dop=20 time=2427/2356/2255ms rows=936(52/46/44) mem=7/6/6KB open=0/0/0ms get_next=2427/2356/2255ms pull_dop=9/9/9]
-> Decode (cost=0.00..7791.80 rows=44412 width=76)
[id=8 dop=234 time=2435/1484/5ms rows=936(4/4/4) mem=0/0/0B open=2435/1484/5ms get_next=4/0/0ms]
-> Partial HashAggregate (cost=0.00..7791.70 rows=44412 width=76)
Group Key: l_returnflag, l_linestatus
[id=5 dop=234 time=2435/1484/3ms rows=936(4/4/4) mem=313/312/168KB open=2435/1484/3ms get_next=0/0/0ms]
-> Project (cost=0.00..3550.73 rows=584421302 width=33)
[id=4 dop=234 time=2145/1281/2ms rows=585075720(4222846/2500323/3500) mem=142/141/69KB open=10/1/0ms get_next=2145/1280/2ms]
-> Project (cost=0.00..2585.43 rows=584421302 width=33)
[id=3 dop=234 time=582/322/2ms rows=585075720(4222846/2500323/3500) mem=142/142/69KB open=10/1/0ms get_next=582/320/2ms]
-> Index Scan using Clustering_index on lineitem (cost=0.00..261.36 rows=584421302 width=25)
Segment Filter: (l_shipdate <= '1998-08-03 00:00:00+08'::timestamp with time zone)
Cluster Filter: (l_shipdate <= '1998-08-03 00:00:00+08'::timestamp with time zone)
[id=2 dop=234 time=259/125/1ms rows=585075720(4222846/2500323/3500) mem=1418/886/81KB open=10/1/0ms get_next=253/124/0ms]
ADVICE:
[node id : 1000xxx] distribution key miss match! table lineitem defined distribution keys : l_orderkey; request distribution columns : l_returnflag, l_linestatus;
shuffle data skew in different shards! max rows is 20, min rows is 0
Query id:[300200511xxxx]
======================cost======================
Total cost:[2505] ms
Optimizer cost:[47] ms
Init gangs cost:[4] ms
Build gang desc table cost:[2] ms
Start query cost:[18] ms
- Wait schema cost:[0] ms
- Lock query cost:[0] ms
- Create dataset reader cost:[0] ms
- Create split reader cost:[0] ms
Get the first block cost:[2434] ms
Get result cost:[2434] ms
====================resource====================
Memory: 921(244/230/217) MB, straggler worker id: 72969760xxx
CPU time: 149772(38159/37443/36736) ms, straggler worker id: 72969760xxx
Physical read bytes: 3345(839/836/834) MB, straggler worker id: 72969760xxx
Read bytes: 41787(10451/10446/10444) MB, straggler worker id: 72969760xxx
DAG instance count: 41(11/10/10), straggler worker id: 72969760xxx
Fragment instance count: 275(70/68/67), straggler worker id: 72969760xxx
Leitura da saída
A saída divide-se em quatro seções: QUERY PLAN, ADVICE, Cost e Resource.
QUERY PLAN
Leia o plano de baixo para cima, assim como no EXPLAIN. Cada nó apresenta duas linhas:
Estimativas do otimizador
(cost=... rows=... width=...): Possuem o mesmo significado que noEXPLAIN.Medições reais de tempo de execução
[id=... dop=... time=... rows=... mem=... open=... get_next=...]: Valores reais coletados durante a execução.
Os campos de medição de tempo de execução são:
|
Campo |
Formato |
Descrição |
|
|
|
Proporção entre concorrência de entrada e saída. |
|
|
Inteiro |
Identificador único do nó operador. |
|
|
Inteiro |
Grau de paralelismo (dop) — o paralelismo real durante a execução. Corresponde à contagem de shards da instância. Para Local Gather, indica o número de arquivos verificados. |
|
|
|
Tempo de execução real dividido em duas fases: |
|
|
|
Linhas geradas pelo operador. Uma grande diferença entre máximo e mínimo indica distorção de dados. |
|
|
|
Memória consumida durante a execução do operador. Não é cumulativo — o valor de cada operador é independente. |
O valor detimede um operador inclui o tempo acumulado de seus operadores filhos. Para isolar o custo de um único operador, subtraia o tempo do filho. Os valores derowsememsão por operador e não cumulativos.
ADVICE
A seção ADVICE lista sugestões automáticas de ajuste derivadas da execução atual:
Chaves de distribuição, chaves de clustering ou índices bitmap ausentes ou incompatíveis (por exemplo,
Table xxx misses bitmap index)Estatísticas de tabela desatualizadas (
Table xxx Miss Stats! please run 'analyze xxx')Distorção de dados (
shuffle data skew in different shards! max rows is 20, min rows is 0)
Essas sugestões baseiam-se exclusivamente na consulta atual. Analise-as considerando sua carga de trabalho específica antes de aplicar alterações.
Detalhamento de custos
A seção de custos mostra onde o tempo da consulta foi gasto:
|
Fase |
Descrição |
|
Total cost |
Tempo total da consulta de ponta a ponta (ms). |
|
Optimizer cost |
Tempo que o QO gasta gerando o plano de execução. |
|
Build gang desc table cost |
Tempo para converter o plano do QO nas estruturas de dados usadas pelo mecanismo de execução. |
|
Init gangs cost |
Tempo para pré-processar o plano e enviar solicitações ao mecanismo de execução. |
|
Start query cost |
Inicialização após Init gangs — abrange alinhamento de esquema, bloqueio e configuração. Detalhado abaixo: |
|
— Wait schema cost |
Tempo para o mecanismo de armazenamento (SE) e o frontend (FE) alinharem as versões do esquema. Valores altos frequentemente indicam operações frequentes de linguagem de definição de dados (DDL) em tabelas pai particionadas, o que pode retardar gravações e consultas. |
|
— Lock query cost |
Tempo gasto adquirindo bloqueios de consulta. Valores altos indicam contenção de bloqueio. |
|
— Create dataset reader cost |
Tempo para criar leitores de dados de índice. Valores altos podem indicar falhas de cache. |
|
— Create split reader cost |
Tempo para abrir arquivos. Valores altos sugerem falhas no cache de metadados e alta sobrecarga de I/O. |
|
Get the first block cost |
Tempo desde a conclusão da fase Start query até o retorno do primeiro lote de registros. Para operadores como Hash Agg que precisam de todos os dados downstream antes de produzir saída, este valor aproxima-se muito do custo total de Get result. |
|
Get result cost |
Tempo desde a conclusão da fase Start query até o retorno de todos os resultados. |
Consumo de recursos
As métricas de recursos usam o formato total(max/avg/min), onde cada valor cobre o worker com maior consumo, a média por worker e o worker com menor consumo.
|
Métrica |
Descrição |
|
Memory |
Memória total consumida, além de máx/méd/mín por worker. |
|
CPU time |
Tempo total de CPU em todos os núcleos (ms, aproximado). Reflete a complexidade geral da consulta. |
|
Physical read bytes |
Dados lidos do disco. Ocorre quando os resultados não estão em cache. |
|
Read bytes |
Total de dados lidos, incluindo leituras físicas e acertos de cache. |
|
Affected rows |
Linhas afetadas. Exibido apenas para instruções DML. |
|
DAG instance count |
Número de instâncias de grafo acíclico dirigido (DAG). Valores maiores indicam mais paralelismo e complexidade. |
|
Fragment instance count |
Número de instâncias de fragmento. Valores maiores indicam mais segmentos de plano de execução e arquivos. |
|
straggler_worker_id |
ID do nó worker com o consumo máximo de recursos para cada métrica. |
Referência de operadores
A tabela abaixo lista todos os operadores e suas funções. Para detalhes e orientações de ajuste, consulte as seções seguintes.
|
Operador |
Categoria |
Função |
|
Seq Scan |
Varredura |
Varredura completa da tabela — nenhum índice utilizado. |
|
Seq Scan on Partitioned Table |
Varredura |
Examina uma tabela particionada; mostra quantas partições foram selecionadas. |
|
Index Scan (Clustering_index) |
Varredura |
Varredura de armazenamento colunar que atinge um índice (segmento, clustering ou bitmap). |
|
Index Seek (pk_index) |
Varredura |
Varredura de armazenamento de linhas usando um índice de chave primária. |
|
Foreign Table Type |
Varredura |
Indica a origem de uma tabela externa: MaxCompute, OSS ou Hologres. |
|
Filter |
Filtro |
Aplica uma condição WHERE que não atingiu nenhum índice. |
|
Segment Filter |
Filtro |
Condição correspondeu a um índice de segmento (Event Time Column). |
|
Cluster Filter |
Filtro |
Condição correspondeu a um índice de clustering. |
|
Bitmap Filter |
Filtro |
Condição correspondeu a um índice bitmap. |
|
Join Filter |
Filtro |
Filtragem adicional aplicada após uma junção. |
|
Hash Join (variantes) |
Junção |
Une duas tabelas usando uma tabela hash construída a partir da tabela menor. |
|
Nested Loop |
Junção |
Itera a tabela interna para cada linha externa. Custo elevado para grandes conjuntos de dados. |
|
Merge Join |
Junção |
Une duas entradas pré-classificadas nas chaves de junção. |
|
Cross Join |
Junção |
Junção não equitativa otimizada (V3.0+) que carrega a tabela pequena na memória. |
|
Materialize |
Junção |
Armazena a tabela interna em buffer para um Nested Loop. |
|
Broadcast |
Distribuição |
Copia uma tabela pequena para todos os shards para realizar uma junção. |
|
Redistribution |
Distribuição |
Redistribui dados entre shards usando distribuição hash ou aleatória. |
|
Local Gather |
Mesclagem |
Consolida dados de múltiplos arquivos dentro de um único shard. |
|
Gather |
Mesclagem |
Consolida dados de múltiplos shards no resultado final. |
|
Partial HashAggregate |
Agregação |
Realiza agregação dentro de arquivos e shards. |
|
Final HashAggregate |
Agregação |
Combina agregações parciais entre shards. |
|
GroupAggregate |
Agregação |
Agrega dados pré-classificados sem usar hash. |
|
Sort |
Outros |
Ordena linhas por uma ou mais chaves. |
|
Limit |
Outros |
Limita o número de linhas de saída. |
|
Append |
Outros |
Mescla resultados de subconsultas UNION ALL. |
|
Decode |
Outros |
Decodifica ou codifica dados do tipo texto para acelerar a computação. |
|
ExecuteExternalSQL |
Outros |
Indica que uma função ou operador recorreu ao PQE em vez do HQE. |
|
Exchange |
Outros |
Transfere dados dentro de um shard. Nenhuma ação necessária. |
|
Forward |
Outros |
Transfere dados entre HQE e PQE ou SQE. |
|
Project |
Outros |
Mapeia colunas entre uma subconsulta e sua consulta externa. Nenhuma ação necessária. |
Operadores de varredura
Seq scan
Um Seq Scan lê toda a tabela sem utilizar qualquer índice.
EXPLAIN SELECT * FROM public.holo_lineitem_100g;

Tabelas particionadas exibem o operador Seq Scan on Partitioned Table e informam "Partitions selected: x out of y":
EXPLAIN SELECT * FROM public.hologres_parent;

Tabelas externas incluem um rótulo Foreign Table Type que identifica a origem (MaxCompute, OSS ou Hologres):
EXPLAIN SELECT * FROM public.odps_lineitem_100;

Index Scan e Index Seek
O Hologres usa operadores de índice diferentes dependendo do formato de armazenamento da tabela:
-
Index Scan (Clustering_index): Usado para tabelas de armazenamento colunar. Aparece quando uma consulta corresponde a um índice. O plano mostra suboperadores que identificam qual índice foi usado: Segment Filter (índice de segmento), Cluster Filter (índice de clustering) ou Bitmap Filter (índice bitmap). Para mais informações, consulte Princípios do armazenamento colunar. Exemplo 1: Consulta atinge o índice de clustering.
BEGIN; CREATE TABLE column_test ( "id" bigint not null , "name" text not null , "age" bigint not null ); CALL set_table_property('column_test', 'orientation', 'column'); CALL set_table_property('column_test', 'distribution_key', 'id'); CALL set_table_property('column_test', 'clustering_key', 'id'); COMMIT; INSERT INTO column_test VALUES(1,'tom',10),(2,'tony',11),(3,'tony',12); EXPLAIN SELECT * FROM column_test WHERE id>2;Exemplo 2: Consulta em uma coluna não indexada — nenhum Clustering_index no plano.
EXPLAIN SELECT * FROM column_test WHERE age>10;

-
Index Seek (pk_index): Usado para tabelas de armazenamento de linhas com índices de chave primária. Aparece quando uma consulta de chave primária não utiliza Fixed Plan. Para mais informações, consulte Princípios do armazenamento de linhas.
BEGIN; CREATE TABLE row_test_1 ( id bigint not null, name text not null, class text , PRIMARY KEY (id) ); CALL set_table_property('row_test_1', 'orientation', 'row'); CALL set_table_property('row_test_1', 'clustering_key', 'name'); COMMIT; INSERT INTO row_test_1 VALUES ('1','qqq','3'),('2','aaa','4'),('3','zzz','5'); BEGIN; CREATE TABLE row_test_2 ( id bigint not null, name text not null, class text , PRIMARY KEY (id) ); CALL set_table_property('row_test_2', 'orientation', 'row'); CALL set_table_property('row_test_2', 'clustering_key', 'name'); COMMIT; INSERT INTO row_test_2 VALUES ('1','qqq','3'),('2','aaa','4'),('3','zzz','5'); --primary key index EXPLAIN SELECT * FROM (SELECT id FROM row_test_1 WHERE id = 1) t1 JOIN row_test_2 t2 ON t1.id = t2.id;
Operadores de filtro
Operadores de filtro aparecem como nós filhos de operadores de varredura e indicam se uma condição correspondeu a um índice.
Filter
Um Filter simples significa que a condição não correspondeu a nenhum índice. Verifique a configuração de índices da tabela e adicione índices apropriados para melhorar o desempenho.
Se o plano mostrar One-Time Filter: false , o conjunto de resultados está vazio.
BEGIN;
CREATE TABLE clustering_index_test (
"id" bigint not null ,
"name" text not null ,
"age" bigint not null
);
CALL set_table_property('clustering_index_test', 'orientation', 'column');
CALL set_table_property('clustering_index_test', 'distribution_key', 'id');
CALL set_table_property('clustering_index_test', 'clustering_key', 'age');
COMMIT;
INSERT INTO clustering_index_test VALUES (1,'tom',10),(2,'tony',11),(3,'tony',12);
EXPLAIN SELECT * FROM clustering_index_test WHERE id>2;

Segment Filter
A consulta correspondeu a um índice de segmento (Event Time Column). Aparece junto com Index Scan. Consulte Event Time Column (chave de segmento).
Cluster Filter
A consulta correspondeu a um índice de clustering. Consulte Chave de clustering.
Bitmap Filter
A consulta correspondeu a um índice bitmap. Consulte Índice bitmap.
Join Filter
Aplica filtragem adicional após uma operação de junção.
Decode
O operador Decode decodifica ou codifica texto e tipos de dados similares para acelerar a computação.
Local Gather e Gather
No Hologres, os dados são armazenados como arquivos dentro de shards. O Local Gather consolida dados de múltiplos arquivos dentro de um único shard. O Gather consolida dados de múltiplos shards no resultado final.
EXPLAIN SELECT * FROM public.lineitem;

Redistribution
O operador Redistribution redistribui dados entre shards usando distribuição hash ou aleatória. Geralmente aparece nestas situações:
Consultas
JOIN,COUNT DISTINCTouGROUP BYonde as chaves de distribuição estão ausentes ou incompatíveis — os dados precisam ser redistribuídos entre shards em vez de unidos localmente. Em junções de múltiplas tabelas, Redistribution significa que a junção local não foi usada, o que degrada o desempenho.Chaves de
JOINouGROUP BYque usam expressões (como conversões de tipo) que alteram o tipo original do campo, impedindo junções locais.
Exemplo 1: Chaves de distribuição incompatíveis causam Redistribution.
BEGIN;
CREATE TABLE tbl1(
a int not null,
b text not null
);
CALL set_table_property('tbl1', 'distribution_key', 'a');
CREATE TABLE tbl2(
c int not null,
d text not null
);
CALL set_table_property('tbl2', 'distribution_key', 'd');
COMMIT;
EXPLAIN SELECT * FROM tbl1 JOIN tbl2 ON tbl1.a=tbl2.c;
A condição de junção é tbl1.a=tbl2.c, mas as chaves de distribuição são a e d — uma incompatibilidade que força a redistribuição de dados.

Se Redistribution aparecer, verifique se as chaves de distribuição estão definidas corretamente. Consulte Chave de distribuição.
Exemplo 2: Uma chave de junção com expressão que altera o tipo impede junções locais.

Evite expressões em chaves de junção para prevenir isso.
Operadores de junção
Hash Join
Um Hash Join constrói uma tabela hash na memória a partir da tabela menor e depois a sonda linha por linha com dados da tabela maior.
|
Tipo |
Descrição |
|
Hash Left Join |
Retorna todas as linhas da tabela esquerda, com nulos para colunas não correspondentes da tabela direita. |
|
Hash Right Join |
Retorna todas as linhas da tabela direita, com nulos para colunas não correspondentes da tabela esquerda. |
|
Hash Inner Join |
Retorna apenas linhas que satisfazem a condição de junção. |
|
Hash Full Join |
Retorna todas as linhas de ambas as tabelas, com nulos no lado não correspondente. |
|
Hash Anti Join |
Retorna linhas da tabela condutora que não têm correspondência — usado para |
|
Hash Semi Join |
Retorna uma linha por correspondência da tabela condutora — usado para |
Ao analisar um Hash Join, verifique também:
hash cond: A condição de junção, por exemplohash cond (tmp.a=tmp1.b).hash key: A chave usada para cálculo hash entre shards, tipicamente a chave deGROUP BY.
A tabela menor deve ser a tabela hash. Para verificar, procure a tabela rotulada como hash no plano — lendo de baixo para cima, ela aparece como o ramo inferior. Usar a tabela maior como tabela hash consome memória excessiva.
Ajuste: atualize estatísticas
Estatísticas desatualizadas fazem o QO julgar mal os tamanhos das tabelas. Por exemplo, estatísticas mostram rows=1000 para uma tabela que realmente tem 1 milhão de linhas, fazendo com que a tabela errada seja selecionada como tabela hash.
BEGIN ;
CREATE TABLE public.hash_join_test_1 (
a integer not null,
b text not null
);
CALL set_table_property('public.hash_join_test_1', 'distribution_key', 'a');
CREATE TABLE public.hash_join_test_2 (
c integer not null,
d text not null
);
CALL set_table_property('public.hash_join_test_2', 'distribution_key', 'c');
COMMIT ;
INSERT INTO hash_join_test_1 SELECT i, i+1 FROM generate_series(1, 10000) AS s(i);
INSERT INTO hash_join_test_2 SELECT i, i::text FROM generate_series(10, 1000000) AS s(i);
EXPLAIN SELECT * FROM hash_join_test_1 tbl1 JOIN hash_join_test_2 tbl2 ON tbl1.a=tbl2.c;
O plano usa incorretamente a tabela maior hash_join_test_2 como tabela hash:

Após atualizar as estatísticas, o plano seleciona corretamente a tabela menor:
ANALYZE hash_join_test_1;
ANALYZE hash_join_test_2;

Ajuste: ajuste a ordem de junção para consultas complexas
Atualizar estatísticas resolve a maioria dos problemas de ordem de junção. Para junções complexas com cinco ou mais tabelas, o QO pode gastar um tempo significativo encontrando o plano ideal. Use o parâmetro Grand Unified Configuration (GUC) optimizer_join_order para controlar isso:
SET optimizer_join_order = '<value>';
|
Valor |
Descrição |
|
|
Avalia todas as permutações de ordem de junção. Produz planos ideais, mas aumenta a sobrecarga do otimizador para muitas tabelas. |
|
|
Usa a ordem de junção conforme escrita no SQL. Reduz a sobrecarga do QO para junções com tabelas pequenas (menos de 100 milhões de linhas). Não defina isso no nível do banco de dados — afeta todas as junções. |
|
|
Usa um algoritmo guloso para encontrar uma boa ordem de junção com sobrecarga moderada do QO. |
Nested Loop Join e Materialize
Um operador Nested Loop lê a tabela externa linha por linha e pesquisa a tabela interna para cada correspondência — essencialmente um produto cartesiano. A tabela interna tipicamente mostra um operador Materialize no plano.
BEGIN;
CREATE TABLE public.nestedloop_test_1 (
a integer not null,
b integer not null
);
CALL set_table_property('public.nestedloop_test_1', 'distribution_key', 'a');
CREATE TABLE public.nestedloop_test_2 (
c integer not null,
d text not null
);
CALL set_table_property('public.nestedloop_test_2', 'distribution_key', 'c');
COMMIT;
INSERT INTO nestedloop_test_1 SELECT i, i+1 FROM generate_series(1, 10000) AS s(i);
INSERT INTO nestedloop_test_2 SELECT i, i::text FROM generate_series(10, 1000000) AS s(i);
EXPLAIN SELECT * FROM nestedloop_test_1 tbl1,nestedloop_test_2 tbl2 WHERE tbl1.a>tbl2.c;

Para reduzir o custo do Nested Loop:
Mantenha o conjunto de resultados externo pequeno para limitar quantas vezes a tabela interna é verificada.
Evite junções não equitativas sempre que possível — elas tipicamente produzem planos Nested Loop.
Cross Join
A partir da V3.0, o operador Cross Join otimiza cenários de junção não equitativa envolvendo uma tabela pequena. Ao contrário do Nested Loop, que redefine o estado interno para cada linha externa, o Cross Join carrega toda a tabela pequena na memória e transmite a tabela grande através dela. Isso é significativamente mais rápido, mas usa mais memória.

Para desativar o Cross Join:
-- Disable for the current session
SET hg_experimental_enable_cross_join_rewrite = off;
-- Disable at the database level (takes effect for new connections)
ALTER database <database name> hg_experimental_enable_cross_join_rewrite = off;
Merge Join
Um Merge Join requer que suas entradas estejam pré-classificadas nas chaves de junção. O Hologres pode selecionar Merge Join para consultas onde os dados de entrada já estão ordenados.
Broadcast
O operador Broadcast copia uma tabela para todos os shards e é usado em cenários de Broadcast Join onde uma tabela pequena é unida a uma tabela grande. O QO compara o custo de Broadcast com Redistribution e escolhe a opção mais barata.
Broadcast é econômico quando a tabela é pequena e a instância tem poucos shards (por exemplo, 5 shards).
BEGIN;
CREATE TABLE broadcast_test_1 (
f1 int,
f2 int);
CALL set_table_property('broadcast_test_1','distribution_key','f2');
CREATE TABLE broadcast_test_2 (
f1 int,
f2 int);
COMMIT;
INSERT INTO broadcast_test_1 SELECT i AS f1, i AS f2 FROM generate_series(1, 30)i;
INSERT INTO broadcast_test_2 SELECT i AS f1, i AS f2 FROM generate_series(1, 30000)i;
ANALYZE broadcast_test_1;
ANALYZE broadcast_test_2;
EXPLAIN SELECT * FROM broadcast_test_1 t1, broadcast_test_2 t2 WHERE t1.f1=t2.f1;

Se Broadcast aparecer para uma tabela que não é pequena, estatísticas desatualizadas são a causa provável (por exemplo, estatísticas mostram 1.000 linhas, mas a tabela real tem 1 milhão de linhas). Execute ANALYZE <tablename> para atualizá-las.
Shard prune e Shards selected
-
Shard prune: Mostra como o QO seleciona shards relevantes. O QO escolhe o método apropriado automaticamente.
lazily: Shards são marcados por ID primeiro e selecionados durante a computação.eagerly: Apenas shards correspondentes são selecionados imediatamente; shards irrelevantes são ignorados.
Shards selected: O número de shards usados. Por exemplo, 1 out of 20 significa que 1 shard foi selecionado de um total de 20.
ExecuteExternalSQL
O Hologres possui três componentes de mecanismo de consulta: Hologres Query Engine (HQE), PostgreSQL Query Engine (PQE) e Shard Query Engine (SQE). O HQE é o mecanismo proprietário. Quando o HQE não suporta uma função ou operador, o PQE assume o processamento — com custo de desempenho. O operador ExecuteExternalSQL em um plano marca a execução via PQE.
Exemplo 1: A conversão ::timestamp é tratada pelo PQE.
CREATE TABLE pqe_test(a text);
INSERT INTO pqe_test VALUES ('2023-01-28 16:25:19.082698+08');
EXPLAIN SELECT a::timestamp FROM pqe_test;

Exemplo 2: Reescrever ::timestamp como to_timestamp usa o HQE em vez disso — nenhum ExecuteExternalSQL no plano.
EXPLAIN SELECT to_timestamp(a,'YYYY-MM-DD HH24:MI:SS') FROM pqe_test;

Identifique funções que acionam o PQE nos planos de execução e reescreva-as para usar equivalentes compatíveis com HQE. Consulte Otimizar desempenho de consultas para exemplos comuns de reescrita.
O Hologres transfere mais operações do PQE para o HQE a cada versão. Algumas funções podem usar automaticamente o HQE após uma atualização. Consulte Notas de versão de funções .
Operadores de agregação
O Hologres usa HashAggregate para a maioria das agregações. O HashAggregate distribui dados entre shards para agregação paralela e depois mescla os resultados com Gather.
Para grandes conjuntos de dados, o Hologres usa HashAggregate multiestágio:
Partial HashAggregate: Agrega dentro de arquivos e shards.
Final HashAggregate: Combina resultados parciais entre shards.
GroupAggregate é usado quando os dados estão pré-classificados nas chaves de GROUP BY.
EXPLAIN SELECT
sum(l_extendedprice * l_discount) AS revenue
FROM
lineitem
WHERE
l_shipdate >= date '1996-01-01'
AND l_shipdate < date '1996-01-01' + interval '1' year
AND l_discount BETWEEN 0.02 - 0.01 AND 0.02 + 0.01
AND l_quantity < 24;

O QO escolhe automaticamente HashAggregate de estágio único ou multiestágio com base no volume de dados. Se EXPLAIN ANALYZE mostrar tempo alto para um operador Aggregate, mas o QO escolheu apenas agregação no nível de shard, force a agregação multiestágio:
SET optimizer_force_multistage_agg = on;
Sort
O operador Sort ordena linhas em ordem crescente (ASC) ou decrescente (DESC), tipicamente a partir de cláusulas ORDER BY.
EXPLAIN SELECT l_shipdate FROM public.lineitem ORDER BY l_shipdate;

Operações de classificação grandes consomem recursos significativos. Evite classificar grandes conjuntos de dados sempre que possível.
Limit
O operador Limit restringe o número de linhas de saída. Ele controla apenas a saída final, não quantas linhas são verificadas. Verifique se o Limit foi enviado para o nó Seq Scan para entender o volume real de varredura.
EXPLAIN SELECT * FROM public.lineitem limit 1;

Observações sobre Limit:
Nem todos os operadores Limit são enviados para níveis inferiores. Adicione condições de filtro para reduzir as linhas verificadas.
Evite valores LIMIT muito grandes (centenas de milhares ou mais) — eles aumentam o tempo de varredura mesmo quando enviados para níveis inferiores.
Append
O operador Append mescla resultados de subconsultas UNION ALL.
Exchange
O operador Exchange transfere dados dentro de um shard. Nenhuma ação é necessária.
Forward
O operador Forward transfere dados entre HQE e PQE ou SQE. Aparece em planos que usam combinações HQE+PQE ou HQE+SQE.
Project
O operador Project representa o mapeamento de colunas entre uma subconsulta e sua consulta externa. Nenhuma ação é necessária.
Problemas comuns de desempenho
Use esta seção como referência rápida quando identificar um sintoma específico no plano de execução.
|
Sintoma |
Causa provável |
Ação recomendada |
|
|
Estatísticas desatualizadas |
Execute |
|
Redistribution em uma junção |
Incompatibilidade de chave de distribuição ou expressão que altera tipo na chave de junção |
Alinhe chaves de distribuição com chaves de junção; evite expressões em chaves de junção. Consulte Chave de distribuição. |
|
Tabela grande como tabela hash em Hash Join |
Estatísticas desatualizadas |
Execute |
|
Tempo alto de Aggregate com apenas agregação no nível de shard |
Volume de dados muito grande para agregação de estágio único |
Defina |
|
ExecuteExternalSQL aparece |
Função ou operador não suportado pelo HQE, recorrendo ao PQE |
Reescreva a expressão para usar uma função compatível com HQE. Consulte Otimizar desempenho de consultas. |
|
Nested Loop em tabelas grandes |
Condição de junção não equitativa |
Reescreva para junção equitativa quando possível; mantenha o conjunto de resultados externo pequeno |
|
Tempo |
Construção lenta da tabela hash — tabela interna grande ou nós filhos lentos |
Verifique estatísticas e tempos dos operadores filhos; considere a ordem de junção |
|
Grande diferença entre máx e mín em |
Distorção de dados entre shards |
Verifique a seção ADVICE; ajuste chaves de distribuição |
|
Wait schema cost alto |
DDL frequente em tabelas pai particionadas |
Reduza a frequência de DDL |
|
Lock query cost alto |
Contenção de bloqueio |
Investigue consultas concorrentes |
Próximos passos
Para visualizar planos de execução no HoloWeb, consulte Visualizar planos de execução.