1. Como o otimizador decide

O otimizador do DB2 é baseado em custo — ele estima o número de operações de I/O e CPU para cada estratégia de acesso possível e escolhe a de menor custo. Para isso, usa estatísticas armazenadas no catálogo DB2 geradas pelo RUNSTATS.

🦕 Analogia: o otimizador é como um GPS. Você informa o destino (a query) e ele escolhe a rota mais rápida com base no mapa atual (estatísticas). Se o mapa estiver desatualizado, ele pode te mandar por um caminho congestionado. Por isso RUNSTATS desatualizado = plano ruim.

O plano de acesso é definido em tempo de BIND — não em tempo de execução. Isso significa que:

  • Criar um índice novo não muda o plano de um programa já compilado — é preciso fazer REBIND
  • Estatísticas coletadas após o BIND não são usadas até o próximo REBIND
  • O mesmo SQL pode ter planos diferentes em subsistemas diferentes se as estatísticas divergirem

2. PLAN_TABLE — onde o EXPLAIN grava

O EXPLAIN não mostra o plano na tela — ele insere linhas em uma tabela chamada PLAN_TABLE no schema do usuário. Você precisa criar essa tabela uma vez antes de usar o EXPLAIN.

SQLCriando a PLAN_TABLE (DDL padrão IBM)
CREATE TABLE USERID.PLAN_TABLE (
    QUERYNO          INTEGER       NOT NULL,
    QBLOCKNO         SMALLINT      NOT NULL,
    APPLNAME         CHAR(8)       NOT NULL,
    PROGNAME         CHAR(8)       NOT NULL,
    PLANNO           SMALLINT      NOT NULL,
    METHOD           SMALLINT      NOT NULL,
    CREATOR          CHAR(8)       NOT NULL,
    TNAME            CHAR(18)      NOT NULL,
    TABNO            SMALLINT      NOT NULL,
    ACCESSTYPE       CHAR(2)       NOT NULL,
    MATCHCOLS        SMALLINT      NOT NULL,
    ACCESSCREATOR    CHAR(8)       NOT NULL,
    ACCESSNAME       CHAR(18)      NOT NULL,
    INDEXONLY        CHAR(1)       NOT NULL,
    SORTN_UNIQ       CHAR(1)       NOT NULL,
    SORTN_JOIN       CHAR(1)       NOT NULL,
    SORTN_ORDERBY    CHAR(1)       NOT NULL,
    SORTN_GROUPBY    CHAR(1)       NOT NULL,
    SORTC_UNIQ       CHAR(1)       NOT NULL,
    SORTC_JOIN       CHAR(1)       NOT NULL,
    SORTC_ORDERBY    CHAR(1)       NOT NULL,
    SORTC_GROUPBY    CHAR(1)       NOT NULL,
    TSLOCKMODE       CHAR(3)       NOT NULL,
    TIMESTAMP        CHAR(16)      NOT NULL,
    REMARKS          VARCHAR(254)  NOT NULL,
    PREFETCH         CHAR(1)       NOT NULL,
    COLUMN_FN_EVAL   CHAR(1)       NOT NULL,
    MIXOPSEQ         SMALLINT      NOT NULL,
    VERSION          VARCHAR(64)   NOT NULL,
    COLLID           CHAR(18)      NOT NULL,
    ACCESS_DEGREE    SMALLINT,
    JOIN_DEGREE      SMALLINT,
    SORTC_PGROUP_ID  SMALLINT,
    SORTN_PGROUP_ID  SMALLINT,
    PARALLELISM_MODE CHAR(1),
    MERGE_JOIN_COLS  SMALLINT,
    CORRELATION_NAME CHAR(18),
    PAGE_RANGE       CHAR(1)       NOT NULL,
    JOIN_TYPE        CHAR(1)       NOT NULL,
    GROUP_MEMBER     CHAR(8)       NOT NULL,
    IBM_SERVICE_DATA VARCHAR(254)
);
✅ Na prática, na maioria dos ambientes corporativos a PLAN_TABLE já existe no seu schema. Se não existir, o DBA pode criá-la. No SPUFI ou IBM Data Studio o EXPLAIN visual não exige criação manual.

3. Executando o EXPLAIN

SQLEXPLAIN básico — grava o plano na PLAN_TABLE
-- QUERYNO identifica o resultado na PLAN_TABLE
EXPLAIN PLAN SET QUERYNO = 1 FOR
SELECT C.NOME, SUM(T.VL_TRANSACAO) AS TOTAL
  FROM CLIENTES C
  JOIN TRANSACOES T ON C.COD_CLIENTE = T.COD_CLIENTE
 WHERE T.DT_TRANSACAO BETWEEN '2026-06-01' AND '2026-06-30'
 GROUP BY C.NOME
 ORDER BY TOTAL DESC
FETCH FIRST 10 ROWS ONLY;
SQLConsultando o resultado do EXPLAIN
SELECT QUERYNO
     , QBLOCKNO
     , PLANNO
     , TNAME
     , ACCESSTYPE
     , MATCHCOLS
     , ACCESSNAME
     , INDEXONLY
     , PREFETCH
     , SORTN_JOIN
     , SORTC_JOIN
     , SORTN_ORDERBY
     , SORTC_ORDERBY
  FROM USERID.PLAN_TABLE
 WHERE QUERYNO = 1
 ORDER BY PLANNO;

4. ACCESSTYPE — o tipo de acesso

É a coluna mais importante do EXPLAIN. Indica como o DB2 vai acessar cada tabela do plano.

ACCESSTYPESignificadoPerformance
IIndex scan — acessa dados via índiceBoa
I1One-fetch index scan — retorna no máximo 1 linhaExcelente
NIndex scan sem acesso à tabela (index only)Excelente
RTablespace scan — lê a tabela inteiraRuim para tabelas grandes
MMulti-index scan — combina vários índices com list prefetchVariável
MXMultiple index ORing — une resultados de índices diferentesVariável
⚠️ ACCESSTYPE = 'R' em tabela grande é sinal de alerta. Significa que o DB2 vai ler todas as páginas do tablespace. Para uma tabela de 100 milhões de linhas isso pode representar horas de I/O. Verifique se falta índice ou se o predicado impede seu uso.

5. Colunas-chave do PLAN_TABLE

ColunaO que indicaO que observar
TNAMENome da tabela acessada neste passoQual tabela e em qual ordem
ACCESSTYPETipo de acesso (I, R, N...)Preferir I ou N; evitar R em tabelas grandes
MATCHCOLSNº de colunas do índice usadas como matchingQuanto maior, melhor a filtragem
ACCESSNAMENome do índice usadoConfirmar que é o índice esperado
INDEXONLY'Y' se a query é respondida só pelo índice'Y' é o ideal — sem acesso à tabela
PREFETCHTipo de leitura antecipada ativaS ou L = bom; branco = I/O individual
METHODMétodo de JOIN (0=primeira tabela, 1=nested loop, 2=merge scan, 4=hybrid)Entender como tabelas são combinadas
TSLOCKMODEModo de lock no tablespaceIS/IX=compartilhado; S=exclusivo de leitura

6. Lendo um plano completo

Veja um exemplo real de resultado do EXPLAIN para a query da seção 3:

Resultado PLAN_TABLESaída simplificada do SELECT na PLAN_TABLE
QUERYNO PLANNO  TNAME        ACCESSTYPE  MATCHCOLS  ACCESSNAME    INDEXONLY  PREFETCH  SORTC_JOIN  SORTC_ORDERBY
------- ------  -----------  ----------  ---------  ------------  ---------  --------  ----------  -------------
      1      1  TRANSACOES   I                   1  XTRANS01      N          S         N           N
      1      2  CLIENTES     I                   1  XCLIEN01      N          L         Y           N
      1      3  (sort)       -                   -  -             -          -         -           Y

Interpretando linha a linha:

  • PLANNO 1 — TRANSACOES: acesso via índice XTRANS01, 1 matching column (DT_TRANSACAO no range), prefetch sequencial ativo. Bom — a data estava no clustering index.
  • PLANNO 2 — CLIENTES: nested loop sobre as linhas de TRANSACOES, acessa CLIENTES via índice XCLIEN01, 1 matching column (COD_CLIENTE). Prefetch de lista ativo. SORTC_JOIN='Y' significa que o DB2 ordenou o resultado de CLIENTES para o join — custo extra mas esperado.
  • PLANNO 3 — sort: operação de ORDER BY final (SORTC_ORDERBY='Y'). Inevitável neste caso pois ORDER BY é sobre o SUM calculado.
🦕 Como ler a ordem: o PLANNO menor é a tabela "guia" do join (a que dirige o acesso). O DB2 pega cada linha da tabela 1, usa os valores para acessar a tabela 2, e assim por diante. Tabelas com menos linhas depois dos filtros devem aparecer primeiro — isso reduz o número de iterações do loop.

7. INDEX ONLY — o acesso mais eficiente

INDEXONLY = 'Y' significa que todas as colunas que a query precisa estão no índice — o DB2 nunca precisa acessar as páginas de dados da tabela. É o cenário de melhor performance possível para leituras.

SQLExemplo de query que pode resultar em INDEXONLY = Y
-- Índice em (COD_AGENCIA, DT_TRANSACAO, VL_TRANSACAO)
-- Query usa apenas essas três colunas → sem acesso à tabela
SELECT COD_AGENCIA
     , COUNT(*)
     , SUM(VL_TRANSACAO)
  FROM TRANSACOES
 WHERE COD_AGENCIA = '001'
   AND DT_TRANSACAO BETWEEN '2026-06-01' AND '2026-06-30'
 GROUP BY COD_AGENCIA;
Técnica de covering index: inclua no índice colunas frequentemente selecionadas junto com as colunas de filtro. Se o SELECT sempre retorna COD_AGENCIA + VL_TRANSACAO para queries filtradas por DT_TRANSACAO, criar um índice em (DT_TRANSACAO, COD_AGENCIA, VL_TRANSACAO) pode resultar em INDEXONLY = Y e eliminar todos os acessos à tabela.

8. PREFETCH — leitura antecipada

Prefetch é a capacidade do DB2 de requisitar páginas ao gerenciador de buffer antes de precisar delas, eliminando tempo de espera de I/O.

ValorTipoQuando ocorre
SSequential prefetchTablespace scan ou clustering index scan — lê páginas contíguas em lote
LList prefetchNon-clustering index — monta lista de RIDs, ordena e faz I/Os agrupados
DDynamic prefetchDB2 detecta padrão sequencial em tempo de execução e ativa prefetch
' 'Sem prefetchAcessos aleatórios pontuais — cada página lida individualmente
🟣 List prefetch e cluster ratio: o list prefetch funciona coletando todos os RIDs (Row IDs) do índice, ordenando-os por página física e lendo em lote. É muito eficiente quando o cluster ratio do índice é alto. Com cluster ratio baixo, os RIDs apontam para páginas espalhadas — o list prefetch perde eficiência e pode ser desativado automaticamente pelo DB2.

9. Operações de SORT

Colunas de SORT na PLAN_TABLE usam o prefixo N (new table — tabela sendo acessada) ou C (composite — resultado do join até o momento).

ColunaQuando é 'Y'Custo
SORTN_JOINSort na tabela nova antes do joinMédio
SORTC_JOINSort no resultado composto para o joinMédio
SORTN_ORDERBYSort na tabela nova para ORDER BYBaixo a médio
SORTC_ORDERBYSort no resultado composto para ORDER BYMédio a alto
SORTN_GROUPBYSort na tabela nova para GROUP BYMédio
SORTC_GROUPBYSort no resultado composto para GROUP BYMédio a alto
SORTN_UNIQSort para eliminar duplicatas (DISTINCT)Médio

Sorts são caros porque consomem memória de trabalho (work files em DSNDB07) e CPU. Para eliminar sorts de ORDER BY, o clustering index deve estar alinhado com a cláusula ORDER BY da query.

SQLEliminando sort de ORDER BY com clustering index
-- Clustering index em DT_TRANSACAO ASC
-- Esta query elimina o sort de ORDER BY — dados já estão na ordem certa
SELECT COD_CONTA, VL_TRANSACAO
  FROM TRANSACOES
 WHERE DT_TRANSACAO = '2026-06-15'
 ORDER BY DT_TRANSACAO;   -- alinhado ao clustering → SORTC_ORDERBY = N

-- Esta query FORÇA sort — ORDER BY em coluna diferente do clustering
SELECT COD_CONTA, VL_TRANSACAO
  FROM TRANSACOES
 WHERE DT_TRANSACAO = '2026-06-15'
 ORDER BY VL_TRANSACAO;   -- diferente → SORTC_ORDERBY = Y

10. Como melhorar o plano

Depois de ler o EXPLAIN, estas são as ações mais comuns para melhorar o plano de acesso:

Problema no EXPLAINCausa provávelAção
ACCESSTYPE = 'R' em tabela grandeSem índice adequado ou predicado impede usoCriar índice ou reescrever predicado
MATCHCOLS = 0 com índice existenteFunção na coluna ou coluna errada no predicadoReescrever para predicado stage 1
Muitos SORT = 'Y'ORDER BY / GROUP BY sem apoio de índiceAlinhar clustering index ou criar índice cobrindo o ORDER BY
INDEXONLY = 'N' em query simplesSELECT inclui coluna fora do índiceConsiderar covering index
PREFETCH = ' ' em scan grandeCluster ratio baixo ou acesso muito aleatórioREORG tablespace ou rever estratégia de acesso
Ordem errada das tabelas no JOINEstatísticas desatualizadasRUNSTATS + REBIND
SQLFluxo completo de investigação de performance
-- 1. Executar EXPLAIN da query problemática
EXPLAIN PLAN SET QUERYNO = 99 FOR
SELECT ... /* query suspeita */;

-- 2. Consultar resultado
SELECT PLANNO, TNAME, ACCESSTYPE, MATCHCOLS,
       ACCESSNAME, INDEXONLY, PREFETCH,
       SORTC_JOIN, SORTC_ORDERBY
  FROM USERID.PLAN_TABLE
 WHERE QUERYNO = 99
 ORDER BY PLANNO;

-- 3. Verificar quando as estatísticas foram coletadas
SELECT CREATOR, NAME, CARDF, NPAGES, PCTROWCOMP,
       STATSTIME
  FROM SYSIBM.SYSTABLESTATS
 WHERE CREATOR = 'SCHEMA1'
   AND NAME = 'TRANSACOES';

-- 4. Verificar cluster ratio dos índices
SELECT CREATOR, NAME, TBNAME, CLUSTERING,
       CLUSTERRATIO, FULLKEYCARDF, FIRSTKEYCARDF
  FROM SYSIBM.SYSINDEXES
 WHERE TBCREATOR = 'SCHEMA1'
   AND TBNAME    = 'TRANSACOES';

-- 5. Executar RUNSTATS se necessário, depois REBIND
-- 6. Rodar EXPLAIN novamente e comparar
🟣 REBIND vs BIND: o REBIND recompila o plano de acesso de um programa ou package sem precisar do source COBOL. É o que você executa após RUNSTATS ou criação de índice para que o otimizador recalcule o plano com as estatísticas novas. Em produção, sempre faça REBIND em ambiente de homologação primeiro e compare os planos — um REBIND pode tanto melhorar quanto piorar a performance dependendo do estado das estatísticas.