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
- Escreva a consulta como
SELECTe valide o resultado. - Envolva em
EXECUTE BLOCKe teste com o volume real. - Decida se devolve linha (
SUSPEND) ou só executa. - Trate o caso de zero linha explicitamente.
- Declare exceção nomeada para erro de negócio.
- Troque laço por comando de conjunto quando possível.
- Crie com
CREATE OR ALTERe versione o script. - Meça o tempo da procedure com e sem índice adequado.
Checklist
-
SUSPENDsó onde precisa devolver linha. - Parâmetro com
DEFAULTtestado no driver real. - Zero linha tratado com
COUNTouROW_COUNT. -
SUMprotegido comCOALESCE. - 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: