← Voltar ao blog

Procedure no Firebird: parâmetros, retorno e armadilhas

CREATE OR ALTER, parâmetro com DEFAULT, SUSPEND e EXIT e SELECT INTO sem linha: o guia prático de procedure no Firebird, com exemplos e armadilhas.


Procedure no Firebird é um bloco PSQL salvo no banco, que pode ser chamado como comando (EXECUTE PROCEDURE) ou usado como tabela (SELECT ... FROM). A diferença está no SUSPEND: sem ele, a procedure executa e termina; com ele, devolve linha por linha.

O básico se aprende em uma tarde. O que causa incidente em produção é outra coisa: SELECT ... INTO sem resultado, parâmetro opcional que o driver não envia e laço que transforma uma atualização de dez mil linhas em dez mil comandos.


Como criar uma procedure no Firebird?

Com CREATE OR ALTER PROCEDURE, que cria ou substitui sem precisar de DROP — sempre que a assinatura de parâmetros não muda.

CREATE OR ALTER PROCEDURE SP_TOTAL_CLIENTE (
  P_IDCLIENTE INTEGER
)
RETURNS (
  R_QTD   INTEGER,
  R_TOTAL NUMERIC(15,2)
)
AS
BEGIN
  SELECT COUNT(*), SUM(VALOR)
    FROM LANCAMENTOS
   WHERE IDCLIENTE = :P_IDCLIENTE
    INTO :R_QTD, :R_TOTAL;

  SUSPEND;
END

SUSPEND devolve uma linha e continua a execução; EXIT encerra. Uma procedure sem SUSPEND é executável, não selecionável.

Qual a diferença entre procedure executável e selecionável?

Tipo Como chamar Devolve
Executável EXECUTE PROCEDURE SP_X(1) nada (só efeito)
Selecionável SELECT * FROM SP_X(1) uma linha por SUSPEND
Com RETURNS e sem SUSPEND EXECUTE PROCEDURE valores em RETURNING_VALUES
-- selecionável: trata como tabela, aceita WHERE e JOIN
SELECT *
FROM SP_TOTAL_CLIENTE(42);
-- executável: sem SUSPEND, só efeito
EXECUTE PROCEDURE SP_MARCA_INATIVOS(30);

Chamar uma procedure selecionável com EXECUTE PROCEDURE devolve erro; chamar uma executável com SELECT também. Se o objetivo é usar no relatório, SELECT ... FROM é o caminho — assim a procedure entra em join com outras tabelas.

Como declarar parâmetro opcional?

Parâmetro com valor padrão é aceito na definição:

CREATE OR ALTER PROCEDURE SP_LISTA_LANCAMENTOS (
  P_IDCLIENTE INTEGER,
  P_INICIO    DATE = '2026-01-01',
  P_ATIVO     SMALLINT = 1
)
RETURNS (
  ID      INTEGER,
  DATA    DATE,
  VALOR   NUMERIC(15,2)
)
AS
BEGIN
  FOR SELECT ID, DATA, VALOR
        FROM LANCAMENTOS
       WHERE IDCLIENTE = :P_IDCLIENTE
         AND DATA >= :P_INICIO
         AND ATIVO = :P_ATIVO
        INTO :ID, :DATA, :VALOR
  DO
    SUSPEND;
END

O ponto que gera erro: o driver precisa omitir o argumento para o DEFAULT valer. Driver que monta a lista de parâmetros por posição acaba enviando NULL no lugar — e NULL não ativa o valor padrão. Se a aplicação depende disso, teste com o driver real antes de confiar.

Por que SELECT ... INTO sem linha é perigoso?

Porque não levanta erro. Uma consulta de linha única que não encontra nada deixa a variável com o valor anterior e a procedure segue como se tivesse encontrado.

-- armadilha: se não houver lançamento, R_TOTAL mantém o valor que já estava
SELECT SUM(VALOR)
  FROM LANCAMENTOS
 WHERE IDCLIENTE = :P_IDCLIENTE
  INTO :R_TOTAL;

A forma segura é contar antes ou inicializar a variável e tratar o nulo:

R_QTD = 0;
R_TOTAL = 0;

SELECT COUNT(*), COALESCE(SUM(VALOR), 0)
  FROM LANCAMENTOS
 WHERE IDCLIENTE = :P_IDCLIENTE
  INTO :R_QTD, :R_TOTAL;

IF (R_QTD = 0) THEN
BEGIN
  R_TOTAL = 0;
END

COUNT(*) sempre devolve uma linha, então essa seleção é segura. Já SUM de conjunto vazio devolve NULL — daí o COALESCE.

Como tratar erro e devolver mensagem?

O Firebird permite declarar exceção nomeada, que vira mensagem legível para a aplicação. Crie a exceção antes da procedure que a usa.

CREATE EXCEPTION E_ESTOQUE_INSUFICIENTE 'Saldo insuficiente para a baixa';
CREATE EXCEPTION E_QUANTIDADE_INVALIDA  'Quantidade deve ser maior que zero';
CREATE OR ALTER PROCEDURE SP_BAIXA_ESTOQUE (
  P_IDPRODUTO INTEGER,
  P_QTD       INTEGER
)
RETURNS (
  R_SALDO INTEGER
)
AS
BEGIN
  IF (P_QTD <= 0) THEN
    EXCEPTION E_QUANTIDADE_INVALIDA;

  UPDATE ESTOQUE
     SET SALDO = SALDO - :P_QTD
   WHERE IDPRODUTO = :P_IDPRODUTO
     AND SALDO >= :P_QTD;

  IF (ROW_COUNT = 0) THEN
    EXCEPTION E_ESTOQUE_INSUFICIENTE;

  SELECT SALDO FROM ESTOQUE WHERE IDPRODUTO = :P_IDPRODUTO INTO :R_SALDO;
  SUSPEND;
END

ROW_COUNT depois do UPDATE diz quantas linhas foram afetadas — é o teste correto para "não encontrei nada para atualizar". Para capturar violação de chave, use WHEN SQLCODE -803.

BEGIN
  INSERT INTO CLIENTES (CPF, NOME) VALUES (:P_CPF, :P_NOME);
WHEN SQLCODE -803 DO
BEGIN
  EXCEPTION E_CPF_DUPLICADO;
END

Quando não usar procedure?

Quando a lógica é de conjunto. Procedure com FOR SELECT ... DO UPDATE linha a linha é mais lenta e mais difícil de manter que um UPDATE único.

-- lento: um comando por linha
FOR SELECT ID, SALDO FROM ESTOQUE INTO :ID, :S
DO
  UPDATE ESTOQUE SET SALDO = SALDO * 1.1 WHERE ID = :ID;

-- melhor: um comando para o conjunto
UPDATE ESTOQUE SET SALDO = SALDO * 1.1 WHERE ATIVO = 1;

Use procedure para: encapsular regra usada por várias aplicações, executar operação com transação controlada no servidor, ou devolver conjunto que exige lógica procedural. Evite para: relatório que um SELECT resolve, cálculo que cabe em CASE, e "organizar" código que ninguém mais lê.

Como testar sem criar a procedure?

Com EXECUTE BLOCK, que roda o mesmo PSQL de forma anônima:

EXECUTE BLOCK (
  P_IDCLIENTE INTEGER = 42
)
RETURNS (
  R_TOTAL NUMERIC(15,2)
)
AS
BEGIN
  SELECT COALESCE(SUM(VALOR), 0)
    FROM LANCAMENTOS
   WHERE IDCLIENTE = :P_IDCLIENTE
    INTO :R_TOTAL;
  SUSPEND;
END

É o jeito mais rápido de validar a lógica antes de gravar a procedure no banco — e de reproduzir o comportamento em homologação.

Passo a passo para escrever uma procedure útil

  1. Escreva a consulta como SELECT e valide o resultado.
  2. Envolva em EXECUTE BLOCK e teste com o volume real.
  3. Decida se devolve linha (SUSPEND) ou só executa.
  4. Trate o caso de zero linha explicitamente.
  5. Declare exceção nomeada para erro de negócio.
  6. Troque laço por comando de conjunto quando possível.
  7. Crie com CREATE OR ALTER e versione o script.
  8. Meça o tempo da procedure com e sem índice adequado.

Checklist

  • SUSPEND só onde precisa devolver linha.
  • Parâmetro com DEFAULT testado no driver real.
  • Zero linha tratado com COUNT ou ROW_COUNT.
  • SUM protegido com COALESCE.
  • Exceção nomeada para erro de negócio.
  • Laço linha a linha substituído por comando de conjunto.
  • Script versionado com CREATE OR ALTER.
  • Tempo medido com volume de produção.

Perguntas frequentes

Qual a diferença entre SUSPEND e EXIT no Firebird?

SUSPEND devolve a linha atual e mantém a procedure em execução, permitindo chamar como tabela. EXIT encerra a procedure e é usado quando ela só executa, sem devolver conjunto.

Posso chamar procedure dentro de outra procedure?

Sim, tanto com EXECUTE PROCEDURE quanto em SELECT ... FROM. O custo é o mesmo de uma chamada externa; para lógica interna repetida, avalie usar um bloco ou uma view em vez de aninhar procedures.

Como descobrir a procedure que está lenta?

Verifique o plano das consultas internas com SET PLANONLY ON e confira as estatísticas por statement nas tabelas MON$ durante a execução. Laço em tabela grande costuma ser a causa, não a procedure em si.

Conclusão

Procedure no Firebird é simples de escrever e fácil de errar nos detalhes: SUSPEND define se devolve linha, DEFAULT depende do driver, SELECT ... INTO vazio não avisa e laço linha a linha custa caro. Tratando esses quatro pontos, a procedure vira peça confiável do banco em vez de caixa-preta.

Se a lógica da procedure é basicamente uma consulta, o Sqlify gera o SQL a partir da descrição em português — com filtros, junções e agregações explicadas — e você evita escrever PSQL para algo que um SELECT resolve.

Leia também: