Connection Pooling em Node.js
Connection pooling é uma técnica fundamental para otimizar o uso de conexões com bancos de dados. Em vez de criar uma nova conexão a cada requisição (operação custosa), mantemos um conjunto de conexões reutilizáveis. Isso reduz latência, economiza recursos e melhora a throughput da aplicação.
No Node.js, a maioria das bibliotecas de database já implementa pooling nativamente. Vamos explorar com PostgreSQL usando pg:
const { Pool } = require('pg');
const pool = new Pool({
user: 'usuario',
password: 'senha',
host: 'localhost',
port: 5432,
database: 'meu_banco',
max: 20, // máximo de conexões no pool
idleTimeoutMillis: 30000, // fecha conexões ociosas
connectionTimeoutMillis: 2000,
});
// Usar a conexão
pool.query('SELECT * FROM usuarios WHERE id = $1', [1], (err, res) => {
if (err) console.error(err);
else console.log(res.rows);
});
// Fechar o pool quando a app encerrar
pool.end();
Configuração Avançada
A chave está em encontrar o equilíbrio entre max (número máximo de conexões) e consumo de memória. Para aplicações de médio porte, 20-50 conexões costumam ser suficientes. Se sua aplicação está em produção com alta concorrência, monitore métricas: quantas conexões estão ativas vs ociosas. Use pool.totalCount e pool.availableCount para debug.
setInterval(() => {
console.log(`Ativas: ${pool.totalCount}, Disponíveis: ${pool.availableCount}`);
}, 5000);
Query Optimization
Otimizar queries é onde a maioria ganha performance exponencial. A diferença entre uma query ruim e uma boa pode ser de 100x ou mais. Foque em três pilares: índices, evitar N+1 queries, e prepared statements.
Índices e Explain
Sempre use EXPLAIN ANALYZE antes de considerar uma query pronta para produção. Isso mostra o plano de execução e se está usando índices corretamente.
EXPLAIN ANALYZE SELECT * FROM usuarios WHERE email = 'teste@example.com';
-- Criar índice se necessário
CREATE INDEX idx_usuarios_email ON usuarios(email);
Em Node.js, pratique isso antes de fazer o deploy:
async function analisarQuery() {
const resultado = await pool.query(`
EXPLAIN ANALYZE
SELECT u.id, u.nome, COUNT(p.id) as posts
FROM usuarios u
LEFT JOIN posts p ON u.id = p.usuario_id
GROUP BY u.id
`);
console.log(resultado.rows);
}
Evitar o Problema N+1
Este é o erro mais comum. Você faz uma query que retorna N linhas, depois faz N queries adicionais em um loop. Solução: use JOINs ou batch loading.
❌ Errado — N+1 queries:
async function buscarUsuariosComPostsErrado() {
const usuarios = await pool.query('SELECT * FROM usuarios LIMIT 10');
for (let user of usuarios.rows) {
const posts = await pool.query(
'SELECT * FROM posts WHERE usuario_id = $1',
[user.id]
); // 10 queries adicionais!
user.posts = posts.rows;
}
return usuarios.rows;
}
✅ Correto — Uma única query com JOIN:
async function buscarUsuariosComPostsCerto() {
const resultado = await pool.query(`
SELECT
u.id, u.nome, u.email,
json_agg(json_build_object('id', p.id, 'titulo', p.titulo)) as posts
FROM usuarios u
LEFT JOIN posts p ON u.id = p.usuario_id
GROUP BY u.id
LIMIT 10
`);
return resultado.rows;
}
Isso reduz 10+ queries para exatamente 1. Performance dramática.
Prepared Statements e Parametrização
Sempre use placeholders ($1, $2) ao invés de concatenar strings. Isso previne SQL injection e permite que o banco reutilize planos de execução compilados.
// ✅ Correto e seguro
await pool.query(
'SELECT * FROM usuarios WHERE email = $1 AND ativo = $2',
['teste@example.com', true]
);
// ❌ Perigoso - nunca faça isso
const email = req.body.email;
await pool.query(`SELECT * FROM usuarios WHERE email = '${email}'`);
Monitoramento e Boas Práticas
Aplicações em produção precisam de observabilidade. Implemente métricas de performance de queries: tempo de execução, quantidade de linhas retornadas, e uso do pool.
const { performance } = require('perf_hooks');
async function queryComMonitoramento(sql, params) {
const inicio = performance.now();
try {
const resultado = await pool.query(sql, params);
const duracao = performance.now() - inicio;
console.log(`[QUERY] ${duracao.toFixed(2)}ms - ${sql.substring(0, 50)}`);
return resultado;
} catch (erro) {
console.error(`[ERRO] Query falhou após ${(performance.now() - inicio).toFixed(2)}ms`);
throw erro;
}
}
Checklist de Otimização
- Use índices nas colunas de filtro (WHERE, JOIN, ORDER BY)
- Selecione apenas as colunas necessárias (evite SELECT *)
- Aggregate dados no banco (GROUP BY, SUM) em vez de na aplicação
- Cache resultados que mudam pouco frequentemente
- Use transactions (
BEGIN/COMMIT) para múltiplas operações relacionadas - Configure timeouts:
statement_timeoutno PostgreSQL para queries que travam
Conclusão
Connection pooling e query optimization são competências essenciais para desenvolvedores Node.js que lidam com dados. Comece dominando EXPLAIN ANALYZE e eliminando N+1 queries—esses dois pontos podem resolver 80% dos problemas de performance. Em segundo lugar, configure seu pool conscientemente: nem muito pequeno (sufoca requisições) nem muito grande (desperdiça memória). Por fim, meça tudo: implemente logging e monitoramento desde o início, porque otimização sem métricas é apenas adivinhação.