Standby Active Data Guard aberto em read only não aceita escrita. Na primeira DML da aplicação vem o ORA-16000, quase sempre no meio de um relatório que todo mundo jurava ser só consulta. No Oracle 19c o DML redirection resolve isso: manda a operação para o primário e a aplicação nem fica sabendo.
Em 20 de agosto de 2026 eu fiz uma live sobre esse assunto, com a demonstração rodando ao vivo. Se você prefere ver funcionando antes de ler, o vídeo está em youtube.com/live/AjQLCz_MyTM.
O cenário
Oracle Database 19c, uma Oracle Data Guard physical standby database em Active Data Guard, aberta em READ ONLY WITH APPLY, com real-time apply rodando. A aplicação é 95% de consulta, uns poucos INSERT de log, controle de sessão ou tabela de auditoria que teimam em existir no meio do relatório.
O time de aplicação quer usar o standby para relatório, e o relatório quebra na única linha de UPDATE que ninguém tinha mapeado.
O problema
Standby aberto read only não aceita DML
-- Roda no standby (AUTO_U_US)
SQL> connect hr@AUTOUS_TNS
Enter password:
Connected.
SQL> update hr.employees set salary = salary * 1.1 where employee_id = 100;
update hr.employees set salary = salary * 1.1 where employee_id = 100
*
ERROR at line 1:
ORA-16000: database or pluggable database open for read-only access
ORA-16000 tentativa de modificar um banco ou PDB (Pluggable Database) aberto para acesso somente leitura. Vale para DML e para DDL.
Como o DML redirection funciona
Operações DML executadas num standby Active Data Guard são redirecionadas de forma transparente e executadas no primário, incluindo as DMLs que estão dentro de blocos PL/SQL. A sessão do standby fica esperando até que as alterações correspondentes sejam enviadas e aplicadas naquele standby.
A consistência de leitura é mantida durante a operação, e o standby onde a DML rodou enxerga as próprias alterações não confirmadas. As demais instâncias standby só enxergam depois do commit. O tempo de resposta passa a incluir a ida até o primário, a execução lá, o transporte do redo de volta e o apply, ou seja, um round trip completo de Data Guard em cima de cada DML.
Do lado da rede, a comunicação é uma conexão SQL*Net entre os bancos, comportamento parecido com o de um database link.

Habilitando
O ADG_REDIRECT_DML é booleano, com default false, alterável por ALTER SYSTEM e não alterável em PDB. Em Oracle Real Application Clusters (RAC), instâncias diferentes podem usar valores diferentes. Está disponível a partir do Oracle Database 19c.
No nível de sessão
-- Roda no standby, na sessão que vai executar a DML
ALTER SESSION ENABLE ADG_REDIRECT_DML;Para desligar na mesma sessão:
-- Roda no standby, na mesma sessão
ALTER SESSION DISABLE ADG_REDIRECT_DML;No nível de sistema, valendo para todas as sessões do standby:
-- Roda no standby, no nível da instância
ALTER SYSTEM SET ADG_REDIRECT_DML=TRUE SCOPE=BOTH SID='*';Os pré-requisitos que o ORA-16397
Se o redirecionamento falhar, o erro é o ORA-16397: “statement redirection from Oracle Active Data Guard standby database to primary database failed”.
- o connect string para o primário não está estabelecido
- o primário está inacessível pela rede
- há divergência de undo ou de incarnation entre standby e primário
- o current user e o logged-in user não são o mesmo
- a operação não é suportada: PL/SQL com bind variables, ou CTAS (
CREATE TABLE AS SELECT) em global temporary table (GTT) - o real-time apply não está rodando no standby Active Data Guard
Diagnóstico antes de habilitar
Verificar se standby está em modo Active Data Guard:
-- Roda no standby
SET LINESIZE 220
COLUMN name FORMAT a10
COLUMN db_unique_name FORMAT a16
COLUMN open_mode FORMAT a22
COLUMN database_role FORMAT a18
COLUMN protection_mode FORMAT a22
SELECT name,
db_unique_name,
open_mode,
database_role,
protection_mode
FROM v$database;Resultado esperado:

READ ONLY WITH APPLY é o valor que define como physical standby aberto em real-time query mode.

Confirme também que o apply está andando, e não apenas que o processo de recovery existe:
-- Roda no standby
COLUMN name FORMAT a6
COLUMN role FORMAT a24
COLUMN action FORMAT a14
SELECT name,
role,
action,
sequence#
FROM v$dataguard_process
WHERE role LIKE '%recovery%'
OR role = 'managed recovery';ACTION em APPLYING_LOG é apply processando. WAIT_FOR_LOG e WAIT_FOR_GAP pedem investigação antes de qualquer teste.

Do lado do primário, o modo de recovery do destino confirma o real-time apply:
-- Roda no primário
COLUMN destination FORMAT a30
COLUMN recovery_mode FORMAT a36
SELECT dest_id,
destination,
status,
recovery_mode
FROM v$archive_dest_status
WHERE status != 'INACTIVE';O valor esperado em RECOVERY_MODE é MANAGED REAL TIME APPLY ou MANAGED REAL TIME APPLY WITH QUERY.

E confirme o valor corrente do parâmetro:
-- Roda no standby
SHOW PARAMETER adg_redirect_dml—

Gap de replicação
Redirecionamento com gap acumulado é sessão de aplicação esperando redo que ainda nem chegou. Para olhar o gap uso o dg.sql, da família g_gold, que mostra sequência recebida, sequência aplicada, diferença em logs e em minutos, e o papel da instância:
Carregando o script direto do GitHub...
Roteiro de lab para testar o DML redirection
Esse é o roteiro que uso no lab e que dá para repetir em qualquer Oracle Database 19c com Active Data Guard. O lab tem dois sites: primário BR e standby US, o desenho clássico de disaster recovery. Os DB_UNIQUE_NAME são AUTO_U_BR no primário e AUTO_U_US no standby, e as conexões usam os aliases de TNS AUTOBR_TNS e AUTOUS_TNS.
Schema de laboratório uso o HR, sample schema da Oracle, e a tabela EMPLOYEES.

Pré-requisitos
- Oracle Database 19c ou superior, com a opção Oracle Active Data Guard
- Oracle Data Guard physical standby database aberta em
READ ONLY WITH APPLY, com real-time apply ativo - Schema
HR(sample schema da Oracle) já criado no primário e com a conta aberta - O
HRprecisa deCREATE SESSIONe dos privilégios de DML naHR.EMPLOYEES, que ele já tem por ser dono da tabela - A senha do
HRem mãos, porque a conexão tem que ser nominal - Serviço TNS resolvendo primário e standby a partir da sua estação, com autenticação por usuário e senha
Importante: o redirecionamento exige que o current user e o logged-in user sejam o mesmo, então nada de
sqlplus / as sysdba
Passo 0: confirmar Active Data Guard
Confirme no standby que ele está em Active Data Guard. A consulta abaixo devolve o veredito pronto, sem interpretar o OPEN_MODE de cabeça:
-- Roda no standby
SET LINESIZE 220
COLUMN db_unique_name FORMAT a16
COLUMN database_role FORMAT a18
COLUMN open_mode FORMAT a22
COLUMN situacao FORMAT a34
SELECT db_unique_name,
database_role,
open_mode,
CASE
WHEN database_role = 'PHYSICAL STANDBY' AND open_mode = 'READ ONLY WITH APPLY'
THEN 'OK: Active Data Guard ativo'
WHEN database_role = 'PHYSICAL STANDBY' AND open_mode = 'READ ONLY'
THEN 'ATENCAO: aberto, mas sem apply'
WHEN database_role = 'PHYSICAL STANDBY' AND open_mode = 'MOUNTED'
THEN 'NAO: standby apenas montado'
WHEN database_role = 'PRIMARY'
THEN 'Este banco e o PRIMARIO'
ELSE 'Verificar: ' || database_role || ' / ' || open_mode
END AS situacao
FROM v$database;Resultado esperado para seguir com o roteiro:

Passo 1: conferir o schema HR no primário
-- Roda no primário (AUTO_U_BR), conectado como HR
CONNECT hr@AUTOBR_TNS
SELECT table_name
FROM all_tables
WHERE owner = 'HR'
ORDER BY table_name;O teste usa a HR.EMPLOYEES, a mesma tabela dos exemplos da documentação. Nada é criado, e no fim as linhas do teste são apagadas.
Resultado esperado:


-- Roda no primário, conectado como HR
SELECT COUNT(*) AS total FROM hr.employees;Anote o ponto de partida da EMPLOYEES, para comparar depois:

As linhas do teste usam employee_id de 1000 para cima, faixa que o sample schema não ocupa (ele vai até 206), o que deixa a limpeza do passo 10 como um DELETE por faixa.
Passo 2: confirmar que os objetos chegaram no standby
-- Roda no standby (AUTO_U_US)
CONNECT hr@AUTOUS_TNS
SELECT COUNT(*) AS total FROM hr.employees;O total tem que bater com o do primário. Se não bater, o apply está atrasado: volte para o dg.sql antes de seguir.

Passo 3: reproduzir o ORA-16000
Ainda sem habilitar nada, tente escrever:
-- Roda no standby
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1000, 'Teste', 'Redirect', 'TREDIR1000', SYSDATE, 'IT_PROG');Resultado esperado:

Passo 4: habilitar na sessão e repetir
-- Roda no standby, mesma sessão
ALTER SESSION ENABLE ADG_REDIRECT_DML;
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1000, 'Teste', 'Redirect', 'TREDIR1000', SYSDATE, 'IT_PROG');
COMMIT;Resultado esperado:

É a mesma sequência que Hemant Chitale publicou no teste dele e que Rodrigo Jorge reproduziu com outro objeto: ORA-16000 antes, sucesso depois do ALTER SESSION.
Passo 5: confirmar os dois lados
A linha tem que existir no primário, que foi quem executou de fato, e ter voltado ao standby pelo redo.
-- Roda no primário
SELECT employee_id, first_name, last_name, email, job_id
FROM hr.employees
WHERE employee_id >= 1000
ORDER BY employee_id;-- Roda no standby
SELECT employee_id, first_name, last_name, email, job_id
FROM hr.employees
WHERE employee_id >= 1000
ORDER BY employee_id;A linha 1000 nos dois lados fecha o caminho completo: redirecionamento, execução no primário, transporte do redo e apply.


Passo 6: medir o preço do round trip
Agora o custo. Um INSERT sozinho não serve de medida, porque a primeira execução paga parse e abertura de conexão. Rode dez, nos dois lugares, na mesma janela, e compare o total.
-- Roda no primário (AUTO_U_BR), conectado como HR
SET TIMING ON
TIMING START lote_primario
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1001, 'Timing', 'Primario', 'TTIM1001', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1002, 'Timing', 'Primario', 'TTIM1002', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1003, 'Timing', 'Primario', 'TTIM1003', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1004, 'Timing', 'Primario', 'TTIM1004', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1005, 'Timing', 'Primario', 'TTIM1005', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1006, 'Timing', 'Primario', 'TTIM1006', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1007, 'Timing', 'Primario', 'TTIM1007', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1008, 'Timing', 'Primario', 'TTIM1008', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1009, 'Timing', 'Primario', 'TTIM1009', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1010, 'Timing', 'Primario', 'TTIM1010', SYSDATE, 'IT_PROG');
COMMIT;
TIMING STOP
SET TIMING OFF-- Roda no standby (AUTO_U_US), com o redirecionamento habilitado na sessão
ALTER SESSION ENABLE ADG_REDIRECT_DML;
SET TIMING ON
TIMING START lote_standby
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1011, 'Timing', 'Standby', 'TTIM1011', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1012, 'Timing', 'Standby', 'TTIM1012', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1013, 'Timing', 'Standby', 'TTIM1013', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1014, 'Timing', 'Standby', 'TTIM1014', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1015, 'Timing', 'Standby', 'TTIM1015', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1016, 'Timing', 'Standby', 'TTIM1016', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1017, 'Timing', 'Standby', 'TTIM1017', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1018, 'Timing', 'Standby', 'TTIM1018', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1019, 'Timing', 'Standby', 'TTIM1019', SYSDATE, 'IT_PROG');
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1020, 'Timing', 'Standby', 'TTIM1020', SYSDATE, 'IT_PROG');
COMMIT;
TIMING STOP
SET TIMING OFF
ALTER SESSION DISABLE ADG_REDIRECT_DML;São dez comandos separados de propósito. Cada um paga um round trip completo, que é o que a aplicação real faz. Um INSERT único com dez linhas mediria uma transação, não dez. O TIMING ON imprime o Elapsed de cada comando e o par TIMING START / TIMING STOP fecha o total do lote, então dá para olhar o custo agregado e, na variação entre os dez, se o ambiente está estável.
A diferença entre os dois totais é o custo do desenho: rede até o primário, execução lá, transporte do redo e apply no standby. Esse delta vale para o seu ambiente e só para ele, então meça no seu antes de prometer prazo para alguém.

Passo 7: enxergar a sessão do standby dentro do primário
As conexões vindas do standby aparecem como conexão normal na GV$SESSION do primário, com a máquina de origem identificando o standby, conforme o teste de Fernando Simon. Serve para rastrear quem está escrevendo por esse caminho:
-- Roda no primário
COLUMN username FORMAT a20
COLUMN machine FORMAT a30
COLUMN program FORMAT a38
SELECT inst_id, sid, serial#, username, machine, program, status
FROM gv$session
WHERE username = 'HR'
ORDER BY inst_id, sid;Deixe essa query rodando numa segunda janela durante o passo 6, para ver a sessão nascer, escrever e sumir.

Passo 8: os testes de fronteira
8.1. DML com bind variable dentro de bloco PL/SQL. A causa oficial do ORA-16397 cita PL/SQL com bind variables como operação não suportada, mas essa restrição é do outro caminho: a do ADG_REDIRECT_PLSQL, que redireciona o bloco inteiro. O ADG_REDIRECT_DML redireciona a DML que está dentro do bloco, e a doc do 19c diz isso ao descrever o recurso. Bind variable no INSERT não atrapalha:
-- Roda no standby, com o redirecionamento habilitado
ALTER SESSION ENABLE ADG_REDIRECT_DML;
VARIABLE v_nome VARCHAR2(30)
EXEC :v_nome := 'ComBind';
BEGIN
INSERT INTO hr.employees (employee_id, first_name, last_name, email, hire_date, job_id)
VALUES (1031, :v_nome, 'Redirect', 'TBIND1031', SYSDATE, 'IT_PROG');
END;
/
COMMIT;
ALTER SESSION DISABLE ADG_REDIRECT_DML;O bloco completa com PL/SQL procedure successfully completed.

8.2. Bloco que nem faz DML, com %ROWTYPE. O caso publicado por Connor McDonald: a compilação do %ROWTYPE grava tipos temporários no dicionário, e isso falha no read only.
-- Roda no standby, numa sessão nova, sem nada habilitado
DECLARE
r hr.employees%ROWTYPE;
BEGIN
NULL;
END;
/Reproduzindo, sai ORA-16000 num bloco que não escreve nada. Habilite o redirecionamento de PL/SQL e repita:

-- Roda no standby, na sessão que vai executar o bloco
ALTER SESSION ENABLE ADG_REDIRECT_PLSQL;Pode não reproduzir se os tipos já estiverem no dicionário por execução anterior. É o comportamento reportado por ele, e vale a checagem no seu ambiente.

8.3. SELECT ... FOR UPDATE. Bloqueio de linha é padrão comum em aplicação, e a pergunta é se ele atravessa:
-- Roda no standby, com o redirecionamento habilitado
ALTER SESSION ENABLE ADG_REDIRECT_DML;
SELECT employee_id, last_name
FROM hr.employees
WHERE employee_id = 100
FOR UPDATE;
ALTER SESSION DISABLE ADG_REDIRECT_DML;No meu lab, num Oracle Database 19c versão 19.3.0.0.0, esse comando não falhou. Ele devolveu a linha normalmente:
EMPLOYEE_ID LAST_NAME
----------- -------------------------
100 King
1 row selected.O FOR UPDATE não aparece na lista de restrições da doc, então esse é o comportamento esperado, e o teste confirma para este release update.
A linha voltou sem erro, mas o output não mostra se o lock foi mesmo tomado no primário. Para fechar, abra uma segunda sessão no primário e tente atualizar a mesma linha enquanto a primeira continua sem commit:
-- Roda no primário (AUTO_U_BR), numa segunda sessão, com a sessão do standby ainda aberta
UPDATE hr.employees SET salary = salary WHERE employee_id = 100;Se essa sessão travar esperando, o lock atravessou. Se ela passar na hora, o SELECT devolveu a linha sem bloquear nada no primário, que é bem diferente do que a aplicação espera.
8.4. GTT junto com operação remota (opcional). A doc proíbe transação distribuída na instância Active Data Guard que envolva alteração de objeto local. Reproduzir exige uma GTT e um database link a partir do standby.
Passo 9: testar o nível de sistema e voltar atrás
Para ver a diferença entre sessão e instância, use SCOPE=MEMORY no lab, assim um restart limpa a sujeira:
-- Roda no standby
ALTER SYSTEM SET ADG_REDIRECT_DML=TRUE SCOPE=MEMORY SID='*';Atenção: esse comando vale para todas as sessões da instância, inclusive as que já estão conectadas, e autoriza também o DDL de recompilação automática de PL/SQL saindo do standby.
Abra uma sessão nova, sem executar ALTER SESSION, e insira a linha 1033 do mesmo jeito: ela passa. Desligue só na sessão e repita:
-- Roda no standby, na mesma sessão
ALTER SESSION DISABLE ADG_REDIRECT_DML;Volta o ORA-16000 mesmo com o parâmetro em TRUE na instância, que é a precedência da sessão sobre o sistema funcionando na prática.
-- Roda no standby
ALTER SYSTEM SET ADG_REDIRECT_DML=FALSE SCOPE=MEMORY SID='*';Passo 1: criar a tabela de teste no schema HR, no primário
Atenção: apague só as linhas do teste, pela faixa de
employee_id. Nada deDELETE FROM hr.employeessem cláusula, e nada deDROP USER hr. O sample schema continua inteiro.
-- Roda no primário, conectado como HR
DELETE FROM hr.employees WHERE employee_id >= 1000;
COMMIT;
SELECT COUNT(*) AS total FROM hr.employees;O total tem que voltar ao número anotado no passo 1. Confirme no standby que as linhas sumiram depois do apply.
As 4 restrições documentadas
As quatro estão no capítulo 10 do Data Guard Concepts and Administration do Oracle Database 19c, na seção que descreve o redirecionamento automático de DML.
1. Volume de DML impacta o primário
A doc pede para evitar muitas operações DML no standby Active Data Guard, porque elas rodam de verdade no primário (Data Guard Concepts and Administration 19c, cap. 10). O recurso foi desenhado para aplicações read-mostly que eventualmente executam DML, não para dividir carga de escrita. Se a aplicação escreve a cada requisição, o standby vira um proxy caro do primário, com latência de rede somada à latência de apply.
2. Transações Oracle XA não são suportadas
XA é a interface do padrão X/Open Distributed Transaction Processing, que permite a uma aplicação tratar como uma transação única alterações espalhadas por recursos diferentes. Quem coordena é o transaction manager, que fica fora do banco, normalmente num servidor de aplicação ou monitor transacional. Cada recurso participante é um resource manager, e o Oracle Database é um deles. O transaction manager roda um commit em duas fases: primeiro pergunta a cada resource manager se ele consegue commitar, depois manda todo mundo commitar ou tudo voltar atrás. É assim que uma transação que grava no banco e publica numa fila de mensagens vira tudo ou nada (Developing Applications with Oracle XA, 19c).
Operações DML dentro de transações Oracle XA não funcionam em standby Active Data Guard (Data Guard Concepts and Administration 19c, cap. 10). Se a aplicação passa por WebLogic, JBoss, Tuxedo ou qualquer coordenador de transação distribuída, esse caminho está fechado.
3. Transações distribuídas com objeto local
Transação distribuída na instância Active Data Guard não é permitida quando envolve alteração de objeto local (Data Guard Concepts and Administration 19c, cap. 10). O exemplo da própria doc: você não consegue commitar uma transação que modifica uma global temporary table na instância Active Data Guard e também atualiza uma tabela remota por database link.
Commite ou dê rollback nas DMLs pendentes em GTT antes de emitir a operação remota, ou o contrário. Escritas implícitas em GTT feitas por operações como EXPLAIN PLAN entram nessa conta.
4. Temporary undo não suporta LOB temporário
Alteração em GTT não gera redo, mas o undo gerado por essa alteração gera. Como redo não é permitido em banco read only, o Active Data Guard usa temporary undo, que grava o undo no temporary tablespace.
No standby Active Data Guard o temporary undo é sempre habilitado por default, e o TEMP_UNDO_ENABLED não tem efeito lá. O recurso exige COMPATIBLE em 12.0.0 ou superior e não suporta BLOBs nem CLOBs temporários em instância Active Data Guard. Tudo isso na seção de global temporary tables do mesmo capítulo 10.
PL/SQL no standby: o parâmetro não é o mesmo
Bloco PL/SQL de nível superior tem outro comando:
-- Roda no standby, na sessão que vai executar o bloco
ALTER SESSION ENABLE ADG_REDIRECT_PLSQL;Duas condições documentadas: só funciona em nível de sessão, e o bloco não pode conter bind variables. A restrição de bind variable vale aqui e só aqui, no redirecionamento do bloco inteiro, não no da DML que está dentro dele.
Connor McDonald publicou um caso incômodo: um bloco que não faz DML nenhuma, apenas declara uma variável %ROWTYPE, falha com ORA-16000 no Active Data Guard, porque a compilação do %ROWTYPE grava tipos temporários no dicionário. Com o ADG_REDIRECT_PLSQL habilitado, o bloco roda.
-- Roda no standby
DECLARE
tst_row dual%ROWTYPE;
BEGIN
NULL;
END;
/Recompilação automática de PL/SQL inválido
Objeto PL/SQL fica inválido quando um objeto dependente é alterado. A partir do 19c, objeto PL/SQL invalidado executado num standby pode ser recompilado automaticamente quando o ADG_REDIRECT_DML está em TRUE. O DDL correspondente é redirecionado e executado no primário, e a sessão do standby espera a conclusão.
O parâmetro em nível de sistema, portanto, não liga só DML redirection: ele autoriza também DDL de recompilação saindo do standby em direção ao primário. Vale lembrar disso antes de colocar TRUE no spfile.
Troubleshooting
Entram aqui só os casos com registro público ou mensagem oficial, nada de erro que eu imaginei que pudesse acontecer.
ORA-16000 mesmo com o parâmetro no spfile
Sintoma: a DML falha com ORA-16000 apesar de SHOW PARAMETER adg_redirect_dml mostrar TRUE.
Causa: a configuração de sessão sobrepõe a de sistema. Uma sessão que executou ALTER SESSION DISABLE ADG_REDIRECT_DML, ou um pool que faz isso no login trigger, ignora o spfile.
Solução: habilitar explicitamente na sessão.
-- Roda no standby, na sessão que vai executar a DML
ALTER SESSION ENABLE ADG_REDIRECT_DML;ORA-16397 conectando com autenticação de sistema operacional
Sintoma: ORA-16397 ao rodar a DML numa sessão aberta com sqlplus / as sysdba no standby.
Causa: a causa oficial do ORA-16397 inclui o current user e o logged-in user não serem o mesmo. Fernando Simon reportou exatamente esse comportamento e a solução que funcionou.
Solução: conectar com usuário e senha nominais, não por autenticação do sistema operacional.
# Roda na estação ou no servidor do standby
sqlplus hr@AUTOUS_TNSORA-16000 em consulta por database link no standby
Sintoma: query remota via database link executada no standby retorna ORA-16000.
Causa: também reportado por David Kurtz nos testes com PeopleSoft.
Solução: manter as consultas por database link no primário.
ORA-02049 e ORA-02063 apontando para ADGREDIRECT
Sintoma: a sessão do standby recebe timeout de transação distribuída, com a segunda linha do erro referenciando ADGREDIRECT.
Causa: lock no primário segurando a transação redirecionada. Fernando Simon registrou esse comportamento no teste dele.
Solução: tratar a contenção no primário como qualquer enq: TX - row lock contention. O comportamento é útil, porque a sessão do standby não fica presa para sempre.
Licenciamento
A tabela de opções do Licensing Information do Oracle Database 19c lista “Active Data Guard DML Redirection” entre os recursos incluídos na opção Oracle Active Data Guard. O recurso não vem com Oracle Data Guard puro, ele exige a opção licenciada.
Atenção: habilitar o
ADG_REDIRECT_DMLnum ambiente sem a opção Oracle Active Data Guard licenciada expõe a empresa em auditoria. Todo dimensionamento e uso de licença Oracle deve ser analisado pelo especialista de licenciamento da sua organização antes de habilitar o recurso em produção.
Boas práticas
- Habilite por sessão, não no spfile. O redirecionamento fica onde o time de aplicação sabe que existe.
- No spfile, o
TRUEautoriza junto a recompilação automática de PL/SQL, que é DDL saindo do standby. - Real-time apply e gap conferidos antes de liberar o recurso para a aplicação.
- Antes de apontar o relatório para o standby, mapeie no código onde estão as DMLs, os
FOR UPDATEe os database links. - Teste switchover com o recurso ligado. Os papéis invertem e o parâmetro fica onde estava.
- O tempo de resposta da DML redirecionada merece o medição que a do primário, porque o caminho é maior.
- Privilégio de objeto no primário passa a ser o controle, já que o standby não é mais uma barreira de escrita.
Riscos e avisos
Toda DML redirecionada consome recurso no primário, e quem vê a lentidão é o time de lá, não o do standby. Uma aplicação que “só escreve um pouquinho” multiplicada por centenas de sessões deixa de ser pouquinho.
Do lado do standby, a conta chega como tempo de resposta. A sessão espera o redo da própria transação voltar e ser aplicado, então gap de apply, rede ruim ou primário sob carga viram lentidão direta na tela do usuário.
Ligar ADG_REDIRECT_DML=TRUE no spfile pega todas as sessões daquela instância, inclusive as que já estão conectadas, e autoriza junto o DDL de recompilação de PL/SQL indo para o primário. Em RAC, valores diferentes por instância fazem o comportamento mudar conforme o nó em que a sessão caiu, o que é ruim de diagnosticar quando ninguém sabe que o parâmetro existe.
Referências oficiais
- ADG_REDIRECT_DML, Oracle Database 19c Reference (doc oficial): tipo, default, escopo de alteração, comportamento em PDB e em RAC, comandos de sessão.
- ADG_REDIRECT_DML, Oracle Database 21c Reference (doc oficial): confirma que a definição do parâmetro se mantém nas versões seguintes ao 19c.
- Managing Physical and Snapshot Standby Databases, Oracle 19c (doc oficial): funcionamento do redirecionamento, restrições, GTT e temporary undo,
ADG_REDIRECT_PLSQL, recompilação automática de PL/SQL. - ORA-16397 (doc oficial): causa e ação da falha de redirecionamento.
- ORA-16000 (doc oficial): erro de tentativa de modificação em banco read only.
- V$DATABASE (doc oficial): valores de
OPEN_MODE,DATABASE_ROLEePROTECTION_MODE. - V$DATAGUARD_PROCESS (doc oficial): colunas e valores de
ROLEeACTION. - V$ARCHIVE_DEST_STATUS (doc oficial): valores de
RECOVERY_MODE. - Database Licensing Information User Manual 19c (doc oficial): “Active Data Guard DML Redirection” na lista de recursos da opção Oracle Active Data Guard.
O que está no post ou saiu da doc oficial acima, ou é comportamento observado em teste e vem identificado com o nome de quem testou. Fontes: Hemant Chitale, Fernando Simon, Rodrigo Jorge, David Kurtz e Connor McDonald.
Conclusão
O DML redirection resolve o ORA-16000 no standby dentro do escopo para o qual foi feito: aplicação read-mostly com DML ocasional. Fora dele, vira latência na aplicação e carga extra no primário.
Antes de habilitar, valide real-time apply, mapeie XA, FOR UPDATE e database links no código, decida entre sessão e spfile com consciência do DDL de recompilação, e revise privilégios de objeto no primário.
Se quiser ver o roteiro rodando em vez de ler, a live de 20 de agosto está em youtube.com/live/AjQLCz_MyTM.


