SET no MySQL funcionou no Workbench e sumiu na aplicação: sessão, global e pool
Entenda por que SET time_zone e SET sql_mode valem só para uma conexão, como o pool de conexões espalha ou perde esse estado e onde configurar o que precisa valer sempre.
A clínica reclama que a consulta marcada para quinta às 22h30 aparece na agenda de sexta, à 1h30. Você abre o Workbench, roda SET time_zone = '-03:00', consulta a tabela e tudo parece certo. Fecha o chamado. No dia seguinte, o mesmo erro, porque a aplicação nunca rodou o seu SET: ele valeu só para a conexão do seu Workbench.
A resposta curta: SET variavel = valor sem escopo muda só a sessão atual, ou seja, aquela conexão, até ela fechar. A aplicação usa outras conexões, que nascem com os valores globais do servidor. O que precisa valer para todas as conexões vai para o arquivo de configuração do servidor ou para um comando executado ao abrir cada conexão, num lugar só do código, com teste. SET espalhado no meio das telas é imprevisível.
Por que o ajuste desaparece
O MySQL tem variáveis de sistema em dois escopos. A global é o padrão do servidor; a de sessão é a cópia que cada conexão recebe ao nascer e pode alterar para si. Os comandos fazem coisas diferentes:
SET time_zone = ...ouSET SESSION time_zone = ...: altera só a conexão atual.SET GLOBAL time_zone = ...: altera o padrão para conexões novas. As já abertas, inclusive as do pool da aplicação, continuam com o valor antigo. E o valor se perde quando o servidor reinicia.SET PERSIST(MySQL 8.0 em diante): altera o global e grava para sobreviver ao reinício. Não existe no MariaDB.- Arquivo de configuração (
my.cnfoumy.ini): vale desde a subida do servidor e fica versionado junto com a infraestrutura.
O pool de conexões complica tudo. A aplicação abre, por exemplo, dez conexões e as reaproveita entre requisições. Se uma tela roda SET time_zone numa conexão, as próximas requisições que pegarem aquela conexão herdam o ajuste; as que pegarem as outras nove, não. O resultado é o pior tipo de bug: às vezes certo, às vezes errado. Alguns drivers limpam o estado da sessão ao reaproveitar a conexão, outros não, então o comportamento muda até quando você troca de biblioteca.
Como o fuso da sessão muda o dado gravado
NOW() devolve a hora no fuso da sessão, e colunas TIMESTAMP são convertidas do fuso da sessão para UTC ao gravar e de volta ao ler. Colunas DATETIME não convertem nada: guardam a data como veio. Na clínica, a coluna era TIMESTAMP e o servidor estava em UTC. No Workbench, com a sessão em -03:00, a consulta aparecia às 22h30 de quinta; na aplicação, com a sessão herdando UTC, a mesma linha voltava como 1h30 de sexta, e a tela exibia sem converter. Misturar os dois tipos de coluna com sessões em fusos diferentes piora o quadro: parte das linhas desloca, parte não.
Como configurar o que precisa valer sempre
- Descubra os valores atuais. Compare global e sessão numa conexão nova, sem nenhum
SETmanual. - Decida a regra. Para fuso, a recomendação mais segura é gravar em UTC e converter só na exibição.
- Configure no servidor. Ponha a regra no arquivo de configuração e reinicie em janela de manutenção.
- Se não controla o servidor, configure ao abrir a conexão, num único ponto do código.
- Remova os
SETespalhados pelas telas e consultas. - Escreva um teste que pega conexões novas do pool e confere os valores.
- Documente a regra no README ou na infraestrutura como código, para o próximo ambiente nascer igual.
Exemplo: diagnóstico e correção na clínica
O diagnóstico, rodado numa conexão recém-aberta:
SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone;
-- +--------------------+---------------------+--------------------+
-- | @@global.time_zone | @@session.time_zone | @@system_time_zone |
-- | SYSTEM | SYSTEM | UTC |
-- +--------------------+---------------------+--------------------+
SELECT @@global.sql_mode;
A correção no servidor, no arquivo de configuração:
[mysqld]
default-time-zone = '+00:00'
sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'
O deslocamento numérico '+00:00' funciona sempre. Nomes como 'America/Sao_Paulo' só funcionam se as tabelas de fuso do MySQL tiverem sido carregadas no servidor; sem elas, o servidor recusa o valor.
Se a aplicação roda num banco gerenciado em que você não edita o arquivo, configure ao abrir cada conexão. Em Node com mysql2, o pool emite o evento connection uma vez para cada conexão física criada:
// db.js
const mysql = require('mysql2');
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'clinica',
connectionLimit: 10
});
pool.on('connection', (conn) => {
conn.query("SET time_zone = '+00:00'");
});
module.exports = pool.promise();
E o teste que pega duas conexões ao mesmo tempo (para forçar conexões diferentes) e confere:
// fuso.test.js
const db = require('./db');
async function main() {
const [a, b] = await Promise.all([db.getConnection(), db.getConnection()]);
try {
for (const conn of [a, b]) {
const [linhas] = await conn.query('SELECT @@session.time_zone AS tz');
if (linhas[0].tz !== '+00:00') throw new Error('fuso inesperado: ' + linhas[0].tz);
}
console.log('ok: todas as conexões em UTC');
} finally {
a.release();
b.release();
await db.end();
}
}
main().catch((e) => { console.error(e); process.exit(1); });
Outros drivers têm mecanismos equivalentes, como um comando inicial na configuração da conexão. Use o que o seu driver documenta e mantenha esse ponto único. Para o lado da aplicação, com o fuso de quem lê, veja grave o instante em UTC e mostre no fuso de quem lê e, em PHP, por que o pedido nasce ontem.
Variáveis de sessão que causam mais estrago
O fuso é o caso mais visível, mas não o único. sql_mode alterado por sessão faz uma conexão aceitar data zerada e outra rejeitar. SET NAMES diferente entre conexões grava acento quebrado em parte das linhas. E SET foreign_key_checks = 0, usado numa importação e esquecido numa conexão do pool, deixa a aplicação gravar registros órfãos até a conexão ser descartada. Comandos assim só devem existir em scripts de manutenção com conexão própria, que fecha no fim.
Erros comuns
- Corrigir no cliente SQL e fechar o chamado. O ajuste valeu só para a sua conexão; a aplicação continua errada.
- Usar
SET GLOBALe achar que resolveu. As conexões já abertas no pool seguem com o valor antigo, e tudo volta no próximo reinício do servidor. SETno meio do código de uma tela. Uma conexão do pool fica diferente das outras e o bug aparece de forma intermitente.- Usar fuso por nome sem as tabelas de fuso carregadas. A configuração é recusada e o servidor pode nem subir com ela no arquivo.
- Desligar checagem de chave estrangeira numa conexão compartilhada. Registros órfãos entram em silêncio.
- Não documentar. O próximo servidor nasce com outro padrão e o bug volta na migração.
Checklist
- Você comparou
@@globale@@sessionnuma conexão nova, semSETmanual. - Fuso e
sql_modeestão no arquivo de configuração do servidor ou num único ponto de abertura de conexão. - Não há
SETde variável de sessão espalhado pelo código de telas. - Um teste automatizado confere os valores em mais de uma conexão do pool.
- Scripts de manutenção que mexem em variáveis usam conexão própria.
- A regra está documentada junto da infraestrutura.
Abra uma conexão nova com o usuário da aplicação, sem rodar nenhum SET, e grave uma data: ela sai no dia e na hora certos?
Referência: http://www.tecposter.com/fix-errors-for-mysql/