Aula 17: Functions — funções customizadas no MySQL
Na engenharia de dados, a capacidade de encapsular lógica de negócio reutilizável diretamente no banco de dados é um diferencial competitivo que separa ambientes maduros de implementações amadoras. As functions (funções customizadas) no MySQL representam exatamente essa capacidade: rotinas armazenadas que recebem parâmetros, executam processamento interno e retornam um único valor escalar — seja para compor uma cláusula SELECT, integrar-se a um WHERE ou até mesmo alimentar uma trigger. Diferentemente das stored procedures, que estudamos na aula anterior, as functions são projetadas para serem invocadas como parte de expressões SQL, o que as torna ideais para cálculos, formatações, validações e transformações de dados que precisam ser executadas com consistência em todo o ecossistema da aplicação.
Por que isso importa tanto no dia a dia de um profissional de TI ou DBA? Imagine um cenário onde dez aplicações diferentes consultam a mesma base de dados e todas precisam calcular o mesmo indicador financeiro, como o valor presente líquido de um contrato ou a idade exata de um cliente com base na data de nascimento. Sem functions, cada equipe implementaria essa lógica em sua própria camada de aplicação — em Python, Java, PHP, Node.js — introduzindo o risco real de divergências de resultado por diferenças de arredondamento, interpretação de regras de negócio ou simples erro humano. Com uma function armazenada no MySQL, a lógica é escrita uma única vez, validada exaustivamente e consumida por todas as aplicações exatamente da mesma forma. Na JRT Technology Solutions, implementamos dezenas de funções customizadas em ambientes críticos de nossos clientes justamente para eliminar esse tipo de inconsistência e centralizar a governança dos dados.
Nesta aula, você transcenderá o uso básico de funções nativas como NOW(), CONCAT() ou DATEDIFF() e passará a criar suas próprias functions com a sintaxe CREATE FUNCTION. Abordaremos desde a estrutura fundamental da declaração — incluindo a cláusula RETURNS, os modificadores DETERMINISTIC e NOT DETERMINISTIC, e o corpo com BEGIN…END — até tópicos avançados como tratamento de exceções com DECLARE HANDLER, variáveis locais, cursores dentro de funções e as implicações de segurança relacionadas ao log_bin_trust_function_creators. Tudo com exemplos reais que você poderá executar imediatamente no seu ambiente de testes.
Ao final desta aula, você terá plena capacidade de projetar, codificar, depurar e implantar funções customizadas em produção, compreendendo não apenas o “como” sintático, mas também o “quando” e o “porquê” estratégico de utilizá-las. Se você acompanhou as aulas anteriores com dedicação, especialmente a Aula 15 sobre Stored Procedures e a Aula 12 sobre Variáveis e Estruturas de Controle, perceberá que esta aula funciona como a peça que faltava para completar seu domínio sobre a programação server-side no MySQL. Vamos colocar a mão no código.
O que você vai aprender nesta aula
- Compreender o conceito de stored functions e diferenciá-las das stored procedures em propósito, sintaxe e uso
- Dominar a sintaxe completa do comando CREATE FUNCTION, incluindo cada cláusula obrigatória e opcional
- Trabalhar com os modificadores DETERMINISTIC, NOT DETERMINISTIC, CONTAINS SQL, READS SQL DATA e MODIFIES SQL DATA
- Declarar variáveis locais com DECLARE, utilizar estruturas condicionais (IF…THEN…ELSE) e laços (WHILE, REPEAT) dentro de funções
- Tratar erros e exceções com DECLARE HANDLER para garantir robustez em produção
- Implementar funções práticas: cálculo de idade, formatação de documentos, validação de dados e transformação de strings
- Diagnosticar e corrigir os erros mais frequentes ao criar e executar functions no MySQL
- Aplicar boas práticas de nomenclatura, documentação e versionamento de funções em ambientes corporativos
Pré-requisitos e Ambiente
Para acompanhar esta aula com aproveitamento máximo, você precisará de um servidor MySQL na versão 8.0 ou superior em execução — embora a maioria dos conceitos se aplique também ao MySQL 5.7, utilizaremos recursos específicos da versão 8 que facilitam o desenvolvimento, como a melhoria na detecção de erros sintáticos e o suporte aprimorado a CTEs que podem ser combinadas com funções. O ambiente pode ser Linux (Ubuntu 22.04/24.04 LTS, Rocky Linux 9, CentOS Stream 9) ou Windows 10/11 com MySQL instalado via instalador oficial ou MySQL Installer. Você precisará de privilégios de CREATE ROUTINE e EXECUTE no banco de dados onde trabalhará — se estiver utilizando o usuário root ou um usuário com grant ALL PRIVILEGES, estará adequadamente configurado. Recomendamos também ter uma ferramenta cliente como MySQL Workbench, DBeaver ou o próprio mysql CLI, pois escrever funções diretamente no terminal exige atenção extra com delimitadores.
Como esta é uma aula de nível intermediário, assumimos que você já concluiu as aulas anteriores do curso e está familiarizado com: criação de bancos de dados e tabelas (CREATE DATABASE, CREATE TABLE), tipos de dados do MySQL (INT, VARCHAR, DATE, DECIMAL), comandos básicos de manipulação (INSERT, UPDATE, DELETE, SELECT), conceito de stored procedures (Aula 15) e fundamentos de variáveis e estruturas de controle (Aula 12). Se algum desses tópicos ainda não estiver consolidado, sugerimos revisitar as aulas correspondentes antes de prosseguir — a curva de aprendizado aqui será significativamente mais suave com esses pré-requisitos bem assentados. Prepare seu ambiente, abra seu cliente SQL favorito e conecte-se ao servidor. Vamos começar criando um banco de dados de trabalho específico para esta aula.
-- Conecte-se ao MySQL com privilégios administrativos
mysql -u root -p
-- Crie o banco de dados para a aula
CREATE DATABASE IF NOT EXISTS aula_functions CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Selecione o banco de dados
USE aula_functions;
-- Verifique a versão do servidor para confirmar compatibilidade
SELECT VERSION();
mysql> CREATE DATABASE IF NOT EXISTS aula_functions CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Query OK, 1 row affected (0.01 sec)
mysql> USE aula_functions;
Database changed
mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 8.0.36 |
+-----------+
1 row in set (0.00 sec)
Conceito Fundamental: O que são Functions e como diferem de Stored Procedures
Uma function (função armazenada ou stored function) é um objeto de schema que encapsula um bloco de código SQL com a finalidade explícita de retornar um único valor ao chamador. Diferentemente das stored procedures — que podem retornar múltiplos resultsets, modificar dados, gerenciar transações e até mesmo não retornar nada — as functions possuem um contrato mais restrito e, justamente por isso, podem ser utilizadas em contextos onde procedures não são permitidas: dentro de uma cláusula SELECT, como parte de uma expressão em WHERE, na definição de uma coluna virtual ou até mesmo como valor default de um campo. Essa versatilidade posicional é o que torna as funções tão poderosas: você pode escrever SELECT nome, calcular_idade(data_nascimento) FROM clientes e obter o resultado como se calcular_idade() fosse uma função nativa do MySQL.
Para ilustrar a diferença com um exemplo concreto: se você precisa registrar um log de auditoria sempre que um salário for atualizado, uma procedure é a escolha natural, pois ela pode executar o UPDATE e em seguida um INSERT na tabela de log, sem retornar valor algum. Por outro lado, se você precisa calcular o valor do imposto de renda com base em uma faixa salarial e usar esse valor em diversas queries de relatórios diferentes, uma function é o instrumento correto — ela receberá o salário como parâmetro, processará as alíquotas progressivas e retornará o imposto calculado, podendo ser invocada em qualquer SELECT como parte da projeção de colunas. Na JRT Technology Solutions, orientamos nossos clientes a adotarem functions para lógicas de cálculo determinístico que precisam ser consumidas por múltiplas aplicações, enquanto reservamos procedures para operações transacionais complexas que envolvem múltiplas etapas e podem ou não retornar dados.
Outra diferença crucial está no tratamento de parâmetros: functions aceitam apenas parâmetros de entrada (IN implícito — você não especifica a direção, pois todos os parâmetros são de entrada), enquanto procedures suportam parâmetros IN, OUT e INOUT. Isso reforça o paradigma funcional: a função recebe insumos, processa e devolve um resultado, sem efeitos colaterais nos argumentos recebidos. Essa pureza conceitual, embora não estritamente aplicada pelo MySQL (que permite que uma função modifique dados, desde que declarada com MODIFIES SQL DATA), deve ser perseguida como boa prática: funções que alteram estado do banco são consideradas más práticas e podem gerar comportamentos inesperados quando executadas dentro de queries maiores.
A tabela a seguir sintetiza as diferenças fundamentais que você deve ter em mente ao decidir entre uma function e uma procedure:
| Característica | Function | Stored Procedure |
|---|---|---|
| Propósito principal | Retornar um valor escalar | Executar um conjunto de operações |
| Uso em SELECT | Sim — como expressão | Não — requer CALL |
| Parâmetros OUT/INOUT | Não suportados | Suportados |
| Retorno múltiplo | Não — apenas um valor | Sim — múltiplos resultsets |
| Transações internas | Não recomendado | Sim — COMMIT/ROLLBACK |
| Chamada | Como parte de expressão SQL | CALL nome_procedure() |
| Modificador SQL | Obrigatório (DETERMINISTIC, etc.) | Opcional |
Sintaxe Completa do CREATE FUNCTION
O comando CREATE FUNCTION segue uma estrutura bem definida que você precisa memorizar, pois qualquer desvio resultará em erro sintático. A forma geral é a seguinte — e recomendo que você a mantenha à mão como referência rápida durante seus desenvolvimentos:
CREATE
[DEFINER = usuario]
FUNCTION nome_da_funcao (param1 TIPO, param2 TIPO, ...)
RETURNS TIPO_DE_RETORNO
[DETERMINISTIC | NOT DETERMINISTIC]
[CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA]
[SQL SECURITY {DEFINER | INVOKER}]
[COMMENT 'descricao da funcao']
corpo_da_funcao
Vamos detalhar cada componente dessa estrutura para que você compreenda não apenas a sintaxe, mas a semântica por trás de cada cláusula. O DEFINER (opcional) especifica qual usuário será considerado o “dono” da função para fins de verificação de privilégios — se omitido, o usuário que executa o CREATE FUNCTION torna-se o definer. O nome_da_funcao deve ser único dentro do banco de dados (schema) e seguir as regras de identificadores do MySQL: até 64 caracteres, podendo conter letras, números, underscore e cifrão. Os parâmetros são declarados com nome e tipo, separados por vírgulas — todos são implicitamente de entrada. A cláusula RETURNS (com S no final — cuidado para não escrever RETURN) define o tipo de dado que a função devolverá: pode ser qualquer tipo válido do MySQL, incluindo INT, VARCHAR(n), DECIMAL(p,s), DATE, DATETIME, TEXT e até mesmo JSON.
Os modificadores DETERMINISTIC e NOT DETERMINISTIC informam ao otimizador do MySQL se a função sempre retorna o mesmo resultado para os mesmos parâmetros de entrada. Uma função determinística — como calcular_dobro(x) que retorna x * 2 — permite que o MySQL otimize chamadas repetidas dentro de uma mesma query, armazenando em cache o resultado e reutilizando-o. Já funções não determinísticas — como uma que retorna NOW() ou consulta uma tabela cujo conteúdo muda frequentemente — devem ser declaradas com NOT DETERMINISTIC. A escolha correta afeta diretamente a performance de queries complexas e a replicação binária. Em nossos projetos na JRT Technology Solutions, sempre auditamos as funções de produção para garantir que a flag de determinismo esteja corretamente configurada, pois já presenciamos cenários onde uma função marcada como DETERMINISTIC mas que na prática acessava dados variáveis causou inconsistências em replicação.
As cláusulas CONTAINS SQL, NO SQL, READS SQL DATA e MODIFIES SQL DATA são obrigatórias em algumas configurações de segurança e servem como documentação do comportamento da função. CONTAINS SQL indica que a função executa comandos SQL sem ler ou modificar dados (por exemplo, apenas manipula variáveis com SET). NO SQL declara que a função não executa SQL algum. READS SQL DATA permite leitura de tabelas (SELECT). MODIFIES SQL DATA autoriza operações de escrita (INSERT, UPDATE, DELETE) — que, como mencionamos, devem ser evitadas em funções sempre que possível. O SQL SECURITY define se a função será executada com os privilégios do DEFINER (comportamento padrão) ou do INVOKER (usuário que chamou a função) — um controle importantíssimo em ambientes multi-tenant.
Criando sua Primeira Function: Calculadora de Idade
Nada melhor do que um exemplo prático completo para fixar a teoria. Vamos criar uma função chamada calcular_idade que recebe uma data de nascimento (DATE) e retorna a idade em anos como um INT. Esta é uma das funções mais solicitadas em ambientes corporativos e servirá como veículo para demonstrarmos cada aspecto da sintaxe. Antes de criar a função, vamos preparar uma tabela de exemplo para podermos testá-la em consultas reais:
-- Tabela de clientes para testar a função
CREATE TABLE clientes (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
data_nascimento DATE NOT NULL,
email VARCHAR(150)
) ENGINE=InnoDB;
-- Inserindo dados de exemplo
INSERT INTO clientes (nome, data_nascimento, email) VALUES
('Ana Silva', '1990-05-14', 'ana.silva@email.com'),
('Carlos Oliveira', '1985-11-23', 'carlos.oliveira@email.com'),
('Mariana Santos', '2000-01-07', 'mariana.santos@email.com'),
('Roberto Almeida', '1978-09-30', 'roberto.almeida@email.com'),
('Juliana Costa', '1995-03-18', NULL);
-- Verificar os dados inseridos
SELECT * FROM clientes;
+----+------------------+-----------------+---------------------------+
| id | nome | data_nascimento | email |
+----+------------------+-----------------+---------------------------+
| 1 | Ana Silva | 1990-05-14 | ana.silva@email.com |
| 2 | Carlos Oliveira | 1985-11-23 | carlos.oliveira@email.com |
| 3 | Mariana Santos | 2000-01-07 | mariana.santos@email.com |
| 4 | Roberto Almeida | 1978-09-30 | roberto.almeida@email.com |
| 5 | Juliana Costa | 1995-03-18 | NULL |
+----+------------------+-----------------+---------------------------+
5 rows in set (0.00 sec)
Agora sim, vamos à criação da função. Observe atentamente cada linha e o uso do comando DELIMITER — essencial para que o MySQL interprete o bloco BEGIN…END corretamente sem se confundir com os ponto-e-vírgulas internos:
-- Alterar o delimitador para evitar conflito com ; dentro do BEGIN...END
DELIMITER //
CREATE FUNCTION calcular_idade(data_nasc DATE)
RETURNS INT
DETERMINISTIC
READS SQL DATA
COMMENT 'Calcula a idade em anos com base na data de nascimento'
BEGIN
DECLARE idade INT;
-- Calcula a diferença em anos considerando mês e dia atuais
SET idade = TIMESTAMPDIFF(YEAR, data_nasc, CURDATE());
-- Ajusta se ainda não fez aniversário no ano corrente
IF DATE_ADD(data_nasc, INTERVAL idade YEAR) > CURDATE() THEN
SET idade = idade - 1;
END IF;
RETURN idade;
END //
-- Restaurar o delimitador padrão
DELIMITER ;
Query OK, 0 rows affected (0.02 sec)
Vamos dissecar este código linha por linha, pois há nuances importantes. A instrução DELIMITER // redefine temporariamente o caractere que o MySQL interpreta como “fim de comando”. Sem essa alteração, o primeiro ; encontrado dentro do BEGIN…END encerraria prematuramente a definição da função, gerando um erro de sintaxe. Dentro do corpo, declaramos uma variável local idade do tipo INT usando DECLARE — lembre-se de que declarações de variáveis devem vir antes de qualquer outro comando executável dentro do bloco. A função TIMESTAMPDIFF(YEAR, data_nasc, CURDATE()) retorna a diferença em anos entre as duas datas, mas de forma “truncada”: se a pessoa nasceu em 01/12/1990 e hoje é 24/07/2026, ela retornará 35, pois já passou da data. O ajuste condicional no IF verifica se somando a idade calculada à data de nascimento obtemos uma data futura — caso positivo, subtraímos 1 para corrigir o valor. Finalmente, RETURN idade entrega o valor escalar prometido pela assinatura RETURNS INT.
Uma vez criada, a função pode ser utilizada imediatamente em qualquer query. Vamos testá-la contra nossa tabela de clientes para verificar o resultado:
-- Utilizando a função em uma consulta SELECT
SELECT
nome,
data_nascimento,
calcular_idade(data_nascimento) AS idade_atual
FROM clientes
ORDER BY idade_atual DESC;
-- Também pode ser usada em cláusula WHERE
SELECT nome, data_nascimento
FROM clientes
WHERE calcular_idade(data_nascimento) >= 35;
+------------------+-----------------+-------------+
| nome | data_nascimento | idade_atual |
+------------------+-----------------+-------------+
| Roberto Almeida | 1978-09-30 | 47 |
| Carlos Oliveira | 1985-11-23 | 40 |
| Ana Silva | 1990-05-14 | 36 |
| Juliana Costa | 1995-03-18 | 31 |
| Mariana Santos | 2000-01-07 | 26 |
+------------------+-----------------+-------------+
5 rows in set (0.00 sec)
+------------------+-----------------+
| nome | data_nascimento |
+------------------+-----------------+
| Ana Silva | 1990-05-14 |
| Carlos Oliveira | 1985-11-23 |
| Roberto Almeida | 1978-09-30 |
+------------------+-----------------+
3 rows in set (0.00 sec)
Functions com Lógica Condicional Complexa: Formatador de CPF
Elevemos a complexidade com um exemplo do mundo real que utilizamos frequentemente na JRT Technology Solutions para projetos de migração e limpeza de dados. Criaremos uma função chamada formatar_cpf que recebe uma string contendo um CPF (que pode estar em diversos formatos: puro, com pontos e traço, com espaços, etc.) e retorna o CPF formatado no padrão XXX.XXX.XXX-XX ou NULL se o valor fornecido for inválido. Esta função demonstrará manipulação avançada de strings, uso da função REGEXP_REPLACE do MySQL 8, validações aninhadas com IF…THEN…ELSEIF…ELSE e a importância de retornar um valor seguro mesmo diante de entradas inesperadas.
DELIMITER $$
CREATE FUNCTION formatar_cpf(cpf_bruto VARCHAR(20))
RETURNS VARCHAR(14)
DETERMINISTIC
CONTAINS SQL
COMMENT 'Formata um CPF removendo caracteres não numéricos e aplicando a máscara XXX.XXX.XXX-XX'
BEGIN
DECLARE cpf_limpo VARCHAR(11);
DECLARE cpf_formatado VARCHAR(14);
-- Remove qualquer caractere que não seja dígito
SET cpf_limpo = REGEXP_REPLACE(cpf_bruto, '[^0-9]', '');
-- Se a string resultante não tiver exatamente 11 dígitos, retorna NULL (inválido)
IF CHAR_LENGTH(cpf_limpo) != 11 THEN
RETURN NULL;
END IF;
-- Aplica a máscara: XXX.XXX.XXX-XX
SET cpf_formatado = CONCAT(
SUBSTRING(cpf_limpo, 1, 3), '.',
SUBSTRING(cpf_limpo, 4, 3), '.',
SUBSTRING(cpf_limpo, 7, 3), '-',
SUBSTRING(cpf_limpo, 10, 2)
);
RETURN cpf_formatado;
END $$
DELIMITER ;
Analisemos as técnicas empregadas neste código. REGEXP_REPLACE(cpf_bruto, ‘[^0-9]’, ”) é uma função poderosa introduzida no MySQL 8.0 que utiliza expressões regulares para substituição. O padrão ‘[^0-9]’ significa “qualquer caractere que NÃO seja um dígito de 0 a 9” — a negação é dada pelo circunflexo dentro dos colchetes. Esses caracteres são substituídos por string vazia, efetivamente removendo pontos, traços, espaços e qualquer outra “sujeira” do CPF. O resultado é armazenado na variável cpf_limpo. Em seguida, a validação de comprimento com CHAR_LENGTH (que conta caracteres, não bytes) garante que entradas como “123” ou textos sem sentido retornem NULL de forma elegante, sem quebrar a query. A formatação final utiliza CONCAT e SUBSTRING para fatiar a string limpa em quatro grupos e intercalar os separadores.
Para testar a robustez da função, vamos submetê-la a uma variedade de formatos de entrada — este tipo de teste de estresse é exatamente o que fazemos em nossos projetos na JRT Technology Solutions antes de homologar qualquer função para produção:
-- Testes com diferentes formatos de entrada
SELECT
formatar_cpf('12345678909') AS caso1_puro,
formatar_cpf('123.456.789-09') AS caso2_formatado,
formatar_cpf('123 456 789 09') AS caso3_espacos,
formatar_cpf('123.456.789/09') AS caso4_barra,
formatar_cpf('abc123def456ghi789jkl09') AS caso5_misturado,
formatar_cpf('12345') AS caso6_incompleto,
formatar_cpf(NULL) AS caso7_nulo;
+-------------+-----------------+----------------+--------------+-----------------+------------------+------------+
| caso1_puro | caso2_formatado | caso3_espacos | caso4_barra | caso5_misturado | caso6_incompleto | caso7_nulo |
+-------------+-----------------+----------------+--------------+-----------------+------------------+------------+
| 123.456.789-09 | 123.456.789-09 | 123.456.789-09 | 123.456.789-09 | 123.456.789-09 | NULL | NULL |
+-------------+-----------------+----------------+--------------+-----------------+------------------+------------+
1 row in set (0.00 sec)
Trabalhando com Variáveis Locais, Cursores e Tratamento de Erros
Funções não precisam se limitar a cálculos simples — elas podem interagir com o banco de dados de forma controlada. Vamos criar uma função mais sofisticada: obter_status_cliente, que recebe um id de cliente e retorna uma string descritiva com seu status baseado em regras de negócio que consultam outras tabelas. Para este exemplo, precisaremos de uma tabela adicional de pedidos e demonstraremos o uso de cursor para iterar sobre múltiplas linhas, DECLARE HANDLER para tratamento de exceções e variáveis de controle de fluxo:
-- Criação da tabela de pedidos para o exemplo
CREATE TABLE pedidos (
id INT AUTO_INCREMENT PRIMARY KEY,
cliente_id INT NOT NULL,
data_pedido DATE NOT NULL,
valor DECIMAL(10,2) NOT NULL,
status ENUM('PENDENTE','APROVADO','ENTREGUE','CANCELADO') NOT NULL,
FOREIGN KEY (cliente_id) REFERENCES clientes(id)
) ENGINE=InnoDB;
-- Inserção de pedidos de exemplo
INSERT INTO pedidos (cliente_id, data_pedido, valor, status) VALUES
(1, '2026-01-15', 1500.00, 'ENTREGUE'),
(1, '2026-06-20', 2300.00, 'APROVADO'),
(2, '2025-12-01', 500.00, 'ENTREGUE'),
(2, '2026-02-10', 1200.00, 'CANCELADO'),
(3, '2026-07-10', 3200.00, 'PENDENTE'),
(4, '2026-03-05', 800.00, 'ENTREGUE'),
(4, '2026-05-18', 1800.00, 'ENTREGUE'),
(4, '2026-07-01', 950.00, 'APROVADO');
-- Conferir os dados
SELECT * FROM pedidos;
+----+------------+-------------+---------+-----------+
| id | cliente_id | data_pedido | valor | status |
+----+------------+-------------+---------+-----------+
| 1 | 1 | 2026-01-15 | 1500.00 | ENTREGUE |
| 2 | 1 | 2026-06-20 | 2300.00 | APROVADO |
| 3 | 2 | 2025-12-01 | 500.00 | ENTREGUE |
| 4 | 2 | 2026-02-10 | 1200.00 | CANCELADO |
| 5 | 3 | 2026-07-10 | 3200.00 | PENDENTE |
| 6 | 4 | 2026-03-05 | 800.00 | ENTREGUE |
| 7 | 4 | 2026-05-18 | 1800.00 | ENTREGUE |
| 8 | 4 | 2026-07-01 | 950.00 | APROVADO |
+----+------------+-------------+---------+-----------+
8 rows in set (0.00 sec)
Agora implementaremos a função que, para um dado cliente, calcula o total gasto em pedidos ENTREGUE, conta quantos pedidos estão pendentes e retorna uma classificação textual. Utilizaremos cursor para percorrer os pedidos do cliente e um handler para capturar a condição de “nenhum pedido encontrado”:
DELIMITER $$
CREATE FUNCTION obter_status_cliente(p_cliente_id INT)
RETURNS VARCHAR(100)
READS SQL DATA
NOT DETERMINISTIC
COMMENT 'Retorna status do cliente baseado em seu histórico de pedidos'
BEGIN
DECLARE v_total_entregue DECIMAL(10,2) DEFAULT 0;
DECLARE v_qtd_pendentes INT DEFAULT 0;
DECLARE v_status_pedido VARCHAR(20);
DECLARE v_valor_pedido DECIMAL(10,2);
DECLARE v_finalizado INT DEFAULT 0;
DECLARE v_status_final VARCHAR(100);
-- Cursor para iterar sobre os pedidos do cliente
DECLARE cur_pedidos CURSOR FOR
SELECT status, valor
FROM pedidos
WHERE cliente_id = p_cliente_id;
-- Handler: quando não houver mais linhas, define v_finalizado = 1
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_finalizado = 1;
-- Verificação inicial: cliente existe?
IF NOT EXISTS (SELECT 1 FROM clientes WHERE id = p_cliente_id) THEN
RETURN 'ERRO: Cliente não encontrado';
END IF;
-- Abrir o cursor e iterar
OPEN cur_pedidos;
loop_pedidos: LOOP
FETCH cur_pedidos INTO v_status_pedido, v_valor_pedido;
-- Se o handler foi acionado (NOT FOUND), sair do loop
IF v_finalizado = 1 THEN
LEAVE loop_pedidos;
END IF;
-- Acumular totais conforme o status
IF v_status_pedido = 'ENTREGUE' THEN
SET v_total_entregue = v_total_entregue + v_valor_pedido;
ELSEIF v_status_pedido = 'PENDENTE' THEN
SET v_qtd_pendentes = v_qtd_pendentes + 1;
END IF;
END LOOP loop_pedidos;
CLOSE cur_pedidos;
-- Construir a string de status baseada nos valores acumulados
IF v_total_entregue = 0 AND v_qtd_pendentes = 0 THEN
SET v_status_final = 'SEM HISTÓRICO';
ELSEIF v_total_entregue >= 2000 THEN
SET v_status_final = CONCAT('VIP - Total entregue: R$ ', FORMAT(v_total_entregue, 2));
ELSE
SET v_status_final = CONCAT('REGULAR - Total entregue: R$ ', FORMAT(v_total_entregue, 2));
END IF;
IF v_qtd_pendentes > 0 THEN
SET v_status_final = CONCAT(v_status_final, ' | Pendências: ', v_qtd_pendentes);
END IF;
RETURN v_status_final;
END $$
DELIMITER ;
Este código introduz vários conceitos avançados simultaneamente. O DECLARE CURSOR define uma consulta que será percorrida linha a linha — diferentemente de um SELECT comum, o cursor permite processamento procedural sobre cada registro retornado. O DECLARE CONTINUE HANDLER FOR NOT FOUND é um tratador de exceção que captura o sinal de “fim do resultset”: quando um FETCH não encontra mais linhas, ao invés de lançar um erro, o handler define a variável v_finalizado para 1, permitindo que o loop seja encerrado graciosamente pelo comando LEAVE. A estrutura LOOP…END LOOP cria um laço infinito controlado manualmente — uma alternativa ao WHILE que oferece maior flexibilidade no ponto de saída. Observe também que utilizamos NOT DETERMINISTIC porque a função consulta tabelas cujos dados podem mudar entre chamadas.
Vamos executar testes para verificar o comportamento da função em diferentes cenários:
-- Testes da função obter_status_cliente
SELECT
id,
nome,
obter_status_cliente(id) AS status_cliente
FROM clientes
ORDER BY id;
-- Teste com cliente inexistente
SELECT obter_status_cliente(999) AS cliente_inexistente;
+----+------------------+---------------------------------------------------+
| id | nome | status_cliente |
+----+------------------+---------------------------------------------------+
| 1 | Ana Silva | VIP - Total entregue: R$ 1,500.00 |
| 2 | Carlos Oliveira | REGULAR - Total entregue: R$ 500.00 |
| 3 | Mariana Santos | REGULAR - Total entregue: R$ 0.00 | Pendências: 1 |
| 4 | Roberto Almeida | VIP - Total entregue: R$ 2,600.00 |
| 5 | Juliana Costa | SEM HISTÓRICO |
+----+------------------+---------------------------------------------------+
5 rows in set (0.01 sec)
+---------------------------+
| cliente_inexistente |
+---------------------------+
| ERRO: Cliente não encontrado |
+---------------------------+
1 row in set (0.00 sec)
Gerenciando Functions: ALTER, DROP e SHOW
Diferentemente das tabelas, que podem ser alteradas com ALTER TABLE, as functions no MySQL não suportam um comando ALTER FUNCTION para modificar seu corpo. Para alterar uma função existente, você precisa removê-la com DROP FUNCTION e recriá-la com as modificações desejadas. Este é um ponto importante de planejamento: em ambientes de produção, recomendamos versionar todas as funções em scripts SQL armazenados em repositório Git e aplicar as alterações de forma controlada. Na JRT Technology Solutions, utilizamos pipelines de CI/CD que executam DROP FUNCTION IF EXISTS seguido de CREATE FUNCTION com a nova definição, garantindo atomicidade e rastreabilidade das mudanças. Vamos ver os comandos de gerenciamento na prática:
-- Listar todas as functions do banco de dados atual
SHOW FUNCTION STATUS WHERE Db = 'aula_functions'\G
-- Exibir o código-fonte de uma function específica
SHOW CREATE FUNCTION calcular_idade\G
-- Remover uma function (DROP)
DROP FUNCTION IF EXISTS formatar_cpf;
-- Recriar a function com alterações (exemplo: incluindo validação adicional)
DELIMITER $$
CREATE FUNCTION formatar_cpf(cpf_bruto VARCHAR(20))
RETURNS VARCHAR(14)
DETERMINISTIC
CONTAINS SQL
COMMENT 'Formata um CPF removendo caracteres não numéricos e aplicando máscara | Versão 2.0'
BEGIN
DECLARE cpf_limpo VARCHAR(11);
DECLARE cpf_formatado VARCHAR(14);
-- Tratar NULL explicitamente
IF cpf_bruto IS NULL THEN
RETURN NULL;
END IF;
SET cpf_limpo = REGEXP_REPLACE(cpf_bruto, '[^0-9]', '');
IF CHAR_LENGTH(cpf_limpo) != 11 THEN
RETURN NULL;
END IF;
-- Validação extra: não permite CPF com todos dígitos iguais (ex: 111.111.111-11)
IF cpf_limpo REGEXP '^(0{11}|1{11}|2{11}|3{11}|4{11}|5{11}|6{11}|7{11}|8{11}|9{11})$' THEN
RETURN NULL;
END IF;
SET cpf_formatado = CONCAT(
SUBSTRING(cpf_limpo, 1, 3), '.',
SUBSTRING(cpf_limpo, 4, 3), '.',
SUBSTRING(cpf_limpo, 7, 3), '-',
SUBSTRING(cpf_limpo, 10, 2)
);
RETURN cpf_formatado;
END $$
DELIMITER ;
*************************** 1. row ***************************
Db: aula_functions
Name: calcular_idade
Type: FUNCTION
Definer: root@localhost
Modified: 2026-07-24 10:15:32
Created: 2026-07-24 10:15:32
Security_type: DEFINER
Comment: Calcula a idade em anos com base na data de nascimento
character_set_client: utf8mb4
collation_connection: utf8mb4_unicode_ci
Database Collation: utf8mb4_unicode_ci
*************************** 2. row ***************************
Db: aula_functions
Name: obter_status_cliente
Type: FUNCTION
Definer: root@localhost
Modified: 2026-07-24 10:45:00
Created: 2026-07-24 10:45:00
Security_type: DEFINER
Comment: Retorna status do cliente baseado em seu histórico de pedidos
character_set_client: utf8mb4
collation_connection: utf8mb4_unicode_ci
Database Collation: utf8mb4_unicode_ci
Query OK, 0 rows affected (0.01 sec)
Query OK, 0 rows affected (0.02 sec)
Para ambientes que exigem documentação e auditoria rigorosas, você pode consultar a tabela de metadados information_schema.ROUTINES, que oferece uma visão completa de todas as rotinas (functions e procedures) do servidor. Esta consulta nos ajuda a mapear rapidamente todo o inventário de funções de um schema — algo que fazemos rotineiramente em projetos de migração na JRT Technology Solutions:
-- Consulta avançada de metadados de functions
SELECT
ROUTINE_NAME AS nome_funcao,
ROUTINE_TYPE AS tipo,
DATA_TYPE AS tipo_retorno,
CHARACTER_MAXIMUM_LENGTH AS comprimento_max,
IS_DETERMINISTIC AS deterministica,
SQL_DATA_ACCESS AS acesso_dados,
SECURITY_TYPE AS seguranca,
CREATED AS criada_em,
LAST_ALTERED AS ultima_alteracao
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'aula_functions'
AND ROUTINE_TYPE = 'FUNCTION'
ORDER BY ROUTINE_NAME;
+----------------------+----------+--------------+----------------+----------------+---------------+---------------+---------------------+---------------------+
| nome_funcao | tipo | tipo_retorno | comprimento_max | deterministica | acesso_dados | seguranca | criada_em | ultima_alteracao |
+----------------------+----------+--------------+----------------+----------------+---------------+---------------+---------------------+---------------------+
| calcular_idade | FUNCTION | int | NULL | YES | READS SQL DATA| DEFINER | 2026-07-24 10:15:32 | 2026-07-24 10:15:32 |
| formatar_cpf | FUNCTION | varchar | 14 | YES | CONTAINS SQL | DEFINER | 2026-07-24 11:00:00 | 2026-07-24 11:00:00 |
| obter_status_cliente | FUNCTION | varchar | 100 | NO | READS SQL DATA| DEFINER | 2026-07-24 10:45:00 | 2026-07-24 10:45:00 |
+----------------------+----------+--------------+----------------+----------------+---------------+---------------+---------------------+---------------------+
3 rows in set (0.00 sec)
Verificando a Instalação / Testando a Configuração
Após criar suas funções, é fundamental executar uma bateria de verificações para garantir que tudo está operando conforme esperado. Esta seção é obrigatória em nosso fluxo de validação — na JRT Technology Solutions, nenhum deploy é concluído sem que todos os testes de verificação sejam executados e documentados. Vamos seguir um checklist sistemático:
-- 1. CONFIRMAR QUE TODAS AS FUNCTIONS ESTÃO LISTADAS
SELECT COUNT(*) AS total_functions
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'aula_functions'
AND ROUTINE_TYPE = 'FUNCTION';
-- 2. TESTAR O RETORNO DE TIPOS ESPERADOS
SELECT
calcular_idade('2010-06-15') AS teste_idade,
formatar_cpf('11122233344') AS teste_cpf,
obter_status_cliente(1) AS teste_status;
-- 3. TESTAR COMPORTAMENTO COM VALORES NULOS E INVÁLIDOS
SELECT
calcular_idade(NULL) AS idade_nula,
formatar_cpf('123') AS cpf_invalido,
obter_status_cliente(999) AS cliente_inexistente;
-- 4. VERIFICAR PERMISSÕES E DEFINER
SELECT
ROUTINE_NAME,
DEFINER,
SECURITY_TYPE
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'aula_functions'
AND ROUTINE_TYPE = 'FUNCTION';
+-----------------+
| total_functions |
+-----------------+
| 3 |
+-----------------+
1 row in set (0.00 sec)
+-------------+----------------+---------------------------------------------+
| teste_idade | teste_cpf | teste_status |
+-------------+----------------+---------------------------------------------+
| 16 | 111.222.333-44 | VIP - Total entregue: R$ 1,500.00 |
+-------------+----------------+---------------------------------------------+
1 row in set (0.00 sec)
+------------+----------------------+---------------------------+
| idade_nula | cpf_invalido | cliente_inexistente |
+------------+----------------------+---------------------------+
| NULL | NULL | ERRO: Cliente não encontrado |
+------------+----------------------+---------------------------+
1 row in set (0.00 sec)
+----------------------+----------------+---------------+
| ROUTINE_NAME | DEFINER | SECURITY_TYPE |
+----------------------+----------------+---------------+
| calcular_idade | root@localhost | DEFINER |
| formatar_cpf | root@localhost | DEFINER |
| obter_status_cl
Quer aprender na prática com especialistas?
A JRT Technology Solutions oferece treinamentos e implementação de MySQL para equipes corporativas.