Los martes nos enfocamos en resolver problemas reales. Hoy analizamos los cuellos de botella de base de datos más comunes que pueden arruinar la performance de una aplicación y las técnicas sistemáticas para identificarlos y resolverlos.
Problema #1: Queries N+1 - El Asesino Silencioso
Síntomas:
-
Aplicación lenta en listados
-
Número excesivo de queries en logs
-
Performance que empeora exponencialmente con más datos
Identificación:
-- Lo que vemos en logs (señal de alarma)
SELECT * FROM posts ORDER BY created_at DESC LIMIT 10;
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
-- ... se repite para cada post
Herramientas de detección:
// En desarrollo - contador de queries
let queryCount = 0;
const originalQuery = db.query;
db.query = function(...args) {
queryCount++;
console.log(`Query #${queryCount}:`, args[0]);
return originalQuery.apply(this, args);
};
Soluciones según el ORM:
Eloquent (Laravel):
// ❌ Problema N+1
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // Query por cada post
}
// ✅ Solución con Eager Loading
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // Una sola query adicional
}
Sequelize (Node.js):
// ❌ Problema
const posts = await Post.findAll();
for (const post of posts) {
const user = await post.getUser(); // N queries
}
// ✅ Solución
const posts = await Post.findAll({
include: [{ model: User }]
});
Django ORM:
# ❌ Problema
posts = Post.objects.all()
for post in posts:
print(post.user.name) # Query por cada post
# ✅ Solución
posts = Post.objects.select_related('user').all()
for post in posts:
print(post.user.name) # Una sola query con JOIN
Problema #2: Índices Faltantes o Mal Optimizados
Señales de alerta:
-
Queries que tardan segundos en ejecutarse
-
Full table scans en EXPLAIN plans
-
CPU de base de datos constantemente alto
Debugging sistemático:
PostgreSQL:
-- Identificar queries lentas
SELECT query, mean_time, calls, total_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
-- Analizar plan de ejecución
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM users
WHERE email = 'user@example.com'
AND status = 'active';
MySQL:
-- Habilitar slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- Analizar query específica
EXPLAIN FORMAT=JSON
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at > '2025-01-01';
Estrategias de indexación:
Índices compuestos estratégicos:
-- Para WHERE + ORDER BY
CREATE INDEX idx_posts_status_created
ON posts (status, created_at DESC);
-- Para JOIN + filtros
CREATE INDEX idx_orders_user_status
ON orders (user_id, status, created_at);
-- Para búsquedas de texto (PostgreSQL)
CREATE INDEX idx_posts_title_gin
ON posts USING gin(to_tsvector('english', title));
Monitoreo de uso de índices:
-- PostgreSQL: Índices no utilizados
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND schemaname = 'public';
-- MySQL: Índices redundantes
SELECT table_name, non_unique, index_name,
column_name, cardinality
FROM information_schema.statistics
WHERE table_schema = 'your_database'
ORDER BY table_name, index_name, seq_in_index;
Problema #3: Memory y Connection Pool Issues
Síntomas típicos:
-
“Connection pool exhausted” errors
-
Out of memory errors en base de datos
-
Timeouts intermitentes en aplicación
Diagnosticando connection leaks:
Node.js (Sequelize):
// Monitor de conexiones
const originalQuery = sequelize.connectionManager.getConnection;
sequelize.connectionManager.getConnection = function(options) {
console.log('Active connections:', this.pool.size);
console.log('Available connections:', this.pool.available);
return originalQuery.call(this, options);
};
// Configuración optimizada
const sequelize = new Sequelize(database, username, password, {
pool: {
max: 20, // Máximo de conexiones
min: 0, // Mínimo de conexiones
acquire: 30000, // Tiempo máximo para obtener conexión
idle: 10000 // Tiempo antes de cerrar conexión idle
},
logging: (sql, timing) => {
if (timing > 1000) { // Log queries > 1s
console.warn(`Slow query (${timing}ms):`, sql);
}
}
});
Python (Django):
# settings.py - Configuración de pool
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'OPTIONS': {
'MAX_CONNS': 20,
'MIN_CONNS': 1,
},
'CONN_MAX_AGE': 600, # Reutilizar conexiones por 10 min
}
}
# Debugging de conexiones
import logging
logging.getLogger('django.db.backends').setLevel(logging.DEBUG)
Mejores prácticas:
Connection pooling optimizado:
// Configuración balanceada para diferentes entornos
const poolConfig = {
development: { max: 5, min: 1 },
staging: { max: 10, min: 2 },
production: { max: 20, min: 5 }
};
// Monitoring de health del pool
setInterval(() => {
const pool = sequelize.connectionManager.pool;
if (pool.size === pool.max) {
console.warn('Connection pool at maximum capacity');
// Alertar al sistema de monitoreo
}
}, 30000);
Problema #4: Queries Ineficientes y Data Fetching
Problemas comunes:
-
SELECT * en lugar de campos específicos
-
Lack of pagination en listados grandes
-
Agregaciones costosas sin optimización
Identificación de data over-fetching:
Análisis de transferencia de datos:
-- PostgreSQL: Tamaño de resultados por query
SELECT query,
calls,
total_time,
mean_time,
rows,
100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
FROM pg_stat_statements
WHERE rows > 1000 -- Queries que retornan muchas filas
ORDER BY rows DESC;
Optimizaciones específicas:
Paginación eficiente:
-- ❌ OFFSET lento en datasets grandes
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 10000; -- Muy lento
-- ✅ Cursor-based pagination
SELECT * FROM posts
WHERE created_at < '2025-01-15 10:30:00'
ORDER BY created_at DESC
LIMIT 20;
-- ✅ Keyset pagination con índice
SELECT * FROM posts
WHERE id < 12345
ORDER BY id DESC
LIMIT 20;
Agregaciones optimizadas:
-- ❌ Agregación costosa
SELECT user_id, COUNT(*) as post_count
FROM posts
GROUP BY user_id;
-- ✅ Con índice optimizado
CREATE INDEX idx_posts_user_count ON posts (user_id);
-- ✅ Materialized view para agregaciones frecuentes
CREATE MATERIALIZED VIEW user_post_counts AS
SELECT user_id, COUNT(*) as post_count
FROM posts
GROUP BY user_id;
-- Refresh periódico
REFRESH MATERIALIZED VIEW user_post_counts;
Proyección de columnas:
// ❌ Over-fetching
const users = await User.findAll(); // Trae todas las columnas
// ✅ Solo campos necesarios
const users = await User.findAll({
attributes: ['id', 'name', 'email']
});
// ✅ Exclude campos pesados
const users = await User.findAll({
attributes: { exclude: ['password_hash', 'profile_data'] }
});
Problema #5: Transacciones Mal Manejadas y Deadlocks
Síntomas de problemas transaccionales:
-
Deadlock errors esporádicos
-
Transactions que nunca terminan
-
Inconsistencia de datos bajo carga
Debugging de deadlocks:
PostgreSQL:
-- Monitoring de locks activos
SELECT pid, state, query_start, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;
-- Configurar deadlock logging
ALTER SYSTEM SET log_lock_waits = on;
ALTER SYSTEM SET deadlock_timeout = '1s';
SELECT pg_reload_conf();
MySQL:
-- Ver último deadlock
SHOW ENGINE INNODB STATUS\G
-- Monitoring de transacciones largas
SELECT trx_id, trx_started, trx_query
FROM information_schema.innodb_trx
WHERE trx_started < DATE_SUB(NOW(), INTERVAL 30 SECOND);
Mejores prácticas transaccionales:
Transacciones optimizadas:
// ❌ Transacción muy larga
const transaction = await sequelize.transaction();
try {
// Muchas operaciones que toman tiempo
const user = await User.create(userData, { transaction });
await sendEmail(user.email); // Operación externa lenta
await updateStatistics(); // Operación pesada
await transaction.commit();
} catch (error) {
await transaction.rollback();
}
// ✅ Transacción mínima y atomic
const transaction = await sequelize.transaction();
try {
// Solo operaciones DB críticas
const user = await User.create(userData, { transaction });
const order = await Order.create(orderData, { transaction });
await transaction.commit();
// Operaciones externas después del commit
await sendEmail(user.email);
await updateStatistics();
} catch (error) {
await transaction.rollback();
throw error;
}
Prevención de deadlocks:
// Orden consistente de locks para prevenir deadlocks
async function transferMoney(fromUserId, toUserId, amount) {
// Siempre lockear IDs en orden ascendente
const [firstId, secondId] = [fromUserId, toUserId].sort();
const transaction = await sequelize.transaction();
try {
const firstUser = await User.findByPk(firstId, {
transaction,
lock: transaction.LOCK.UPDATE
});
const secondUser = await User.findByPk(secondId, {
transaction,
lock: transaction.LOCK.UPDATE
});
// Lógica de transferencia
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
Herramientas de Monitoring y Debugging
Database-specific tools:
PostgreSQL:
-- Configurar pg_stat_statements
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Top queries por tiempo total
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;
MySQL:
-- Performance Schema básico
SELECT EVENT_NAME, COUNT_STAR, SUM_TIMER_WAIT
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE EVENT_NAME LIKE 'wait/io%'
ORDER BY SUM_TIMER_WAIT DESC;
APM Solutions:
// New Relic monitoring
const newrelic = require('newrelic');
// Custom metrics para DB performance
newrelic.recordMetric('Custom/Database/SlowQuery', queryTime);
newrelic.recordMetric('Custom/Database/ConnectionPoolUsage',
pool.size / pool.max * 100);
Performance Monitoring Dashboard
Métricas clave a trackear:
// Health check comprehensivo
async function databaseHealthCheck() {
const metrics = {
connectionPool: {
active: pool.size,
idle: pool.available,
usage: (pool.size / pool.max) * 100
},
queryPerformance: {
avgResponseTime: await getAvgQueryTime(),
slowQueryCount: await getSlowQueryCount(),
errorRate: await getQueryErrorRate()
},
resources: {
cpuUsage: await getDbCpuUsage(),
memoryUsage: await getDbMemoryUsage(),
diskIO: await getDiskIOStats()
}
};
// Alertas automáticas
if (metrics.connectionPool.usage > 80) {
alert('Connection pool usage high');
}
if (metrics.queryPerformance.avgResponseTime > 1000) {
alert('Average query time too high');
}
return metrics;
}
Checklist de Database Performance
Antes de deploy:
-
¿Todos los queries tienen EXPLAIN plans analizados?
-
¿Índices apropiados para WHERE y JOIN clauses?
-
¿Connection pool configurado para carga esperada?
-
¿Queries paginadas para datasets grandes?
-
¿Transacciones mínimas y atómicas?
En producción:
-
¿Monitoring de slow queries activo?
-
¿Alertas configuradas para métricas críticas?
-
¿Logs de deadlocks habilitados?
-
¿Backup y recovery procedures testeados?
Tu Experiencia con DB Debugging
¿Cuál ha sido el problema de performance más difícil que han debuggeado?
¿Qué herramientas usan para monitorear bases de datos en producción?
¿Han tenido experiencias con deadlocks o connection leaks? ¿Cómo los resolvieron?
Database performance debugging requiere paciencia y metodología sistemática. La clave está en combinar monitoring proactivo con análisis detallado cuando surgen problemas. Un 80% de los issues de performance suelen estar en un 20% de queries críticos.
Próximo martes: “Troubleshooting de APIs REST - Status codes, rate limiting y error handling” ![]()
#TroubleshootingTuesday #Database #Performance #SQL debugging #PostgreSQL #MySQL #Optimization