
Refactorización Completa de una Base de Datos Legacy: De la Deuda Técnica a la Arquitectura Moderna
Este artículo documenta el proceso completo de refactorización de una base de datos empresarial de 87 tablas con más de 15 años de evolución orgánica
raulanto
Desarrollador Full Stack
23 Nov 2025
31 min read
Refactorización Completa de una Base de Datos Legacy: De la Deuda Técnica a la Arquitectura Moderna
Resumen Ejecutivo
Este artículo documenta el proceso completo de refactorización de una base de datos empresarial de 87 tablas con más de 15 años de evolución orgánica. El proyecto abordó problemas críticos de escalabilidad, mantenimiento y consistencia de datos, resultando en una arquitectura moderna.
Analizaremos el diagnóstico inicial, la metodología aplicada, los principios de diseño implementados y las métricas de mejora obtenidas.
1. Contexto y Problemática
Descripción del Sistema
El sistema objeto de estudio es una aplicación de gestión de proyectos de construcción para la industria petrolera, desarrollada originalmente en 2008. A lo largo de 15 años, el sistema acumuló 87 tablas que gestionan:
- Contratos y órdenes de trabajo
- Reportes diarios de obra
- Control de avances y estimaciones
- Gestión de almacén e inventarios
- Proceso de compras y requisiciones
- Logística de embarques costa afuera
Síntomas Críticos Identificados
El sistema presentaba los siguientes problemas:
2. Diagnóstico del Sistema Legacy
2.1 Análisis Estructural
El primer paso consistió en realizar un análisis exhaustivo de la estructura de la base de datos.
Distribución de Tablas por Módulo
| Módulo | Cantidad | Porcentaje |
|---|---|---|
| Almacén e Inventarios | 22 | 25.3% |
| Bitácora de Obra | 16 | 18.4% |
| Actividades y Partidas | 13 | 14.9% |
| Compras y Requisiciones | 9 | 10.3% |
| Catálogos Maestros | 9 | 10.3% |
| Avances y Control | 7 | 8.0% |
| Proyectos y Contratos | 4 | 4.6% |
| Logística y Embarques | 4 | 4.6% |
| Sistema y Configuración | 3 | 3.4% |
| Total | 87 | 100.0% |
2.2 Problemas de Diseño Identificados
Problema 1: Llaves Primarias Compuestas Excesivas
El 68% de las tablas utilizaban llaves primarias compuestas con 3 o más campos, siendo el caso más extremo una tabla con 13 campos en su clave primaria.
Ejemplo identificado:
-- Tabla: bitacoradepersonal
PRIMARY KEY (
sContrato, -- VARCHAR(15)
dIdFecha, -- DATE
iIdDiario, -- INT
sIdPersonal, -- VARCHAR(25)
sIdPernocta, -- VARCHAR(10)
sIdPlataforma, -- VARCHAR(50)
sWbs, -- VARCHAR(100)
sNumeroActividad, -- VARCHAR(20)
sNumeroOrden, -- VARCHAR(35)
Item, -- INT
sAnexo, -- VARCHAR(10)
sClasificacionSitio -- ENUM
)
- Tamaño promedio de índice de clave primaria: 285 bytes
- Tiempo promedio de JOIN: 1,850 ms
- Fragmentación de índice: 47%
Problema 2: Uso Extensivo de ENUMs para Estados
Se identificaron 47 campos de tipo ENUM utilizados para controlar estados de flujo de trabajo.
- Imposibilidad de agregar nuevos estados sin ALTER TABLE
- Ausencia de trazabilidad de cambios de estado
- Lógica de negocio distribuida entre base de datos y aplicación
Caso de estudio:
CREATE TABLE anexo_requisicion (
sStatus ENUM(
'PENDIENTE',
'VALIDADO',
'COMPRADO',
'LIBERADO',
'RECHAZADO',
'CANCELADO',
'ASIGNADO',
'GENERO PR',
'AUTORIZADO'
)
)
- Agregar el estado 'EN_REVISION' requiere ALTER TABLE con bloqueo de tabla
- No existe registro de quién cambió el estado ni cuándo
- No hay forma de saber si una requisición estuvo en estado 'RECHAZADO' antes de ser 'AUTORIZADO'
Problema 3: Ausencia de Sistema de Auditoría
El análisis reveló que el 91% de las tablas carecían de mecanismos de auditoría. Solo existían campos aislados como:
UsuarioValido VARCHAR(40),
FechaValido DATETIME,
UsuarioAutorizo VARCHAR(40),
FechaAutorizo DATETIME
- Imposibilidad de determinar quién modificó un precio después de su captura inicial
- Sin respaldo para investigación de discrepancias en estimaciones
- Incumplimiento de requisitos de auditoría ISO 9001
Problema 4: Sobrescritura de Datos Críticos
Se identificaron 9 tablas donde datos críticos se sobrescribían sin mantener historial:
-- Tabla: actividadesxanexo
UPDATE actividadesxanexo
SET dVentaMN = 1500.00 -- Precio original: 1200.00 (PERDIDO)
WHERE sNumeroActividad = 'ACT-001'
- Precios originales de contrato: 2,847 registros
- Cantidades originales asignadas: 1,923 registros
- Valores de avance antes de correcciones: 8,341 registros
Problema 5: Inconsistencia en Tipos de Datos
El mismo concepto semántico se representaba con diferentes tipos de datos:
-- Cantidades en diferentes tablas:
dCantidad DOUBLE(13,5) -- 28 tablas
dCantidad DECIMAL(12,4) -- 19 tablas
dCantidad DECIMAL(17,4) -- 8 tablas
dCantidad FLOAT(9,3) -- 4 tablas
- Errores de redondeo en sumas acumuladas
- Discrepancias en totales de estimaciones (hasta 0.03% de diferencia)
- Inconsistencias en conversiones de unidades
2.3 Análisis de Flujos de Datos
Se mapearon los flujos principales para identificar puntos de sobrescritura:
Flujo de Reporte Diario
Día 1: INSERT bitacoradeactividades (dCantidad=10)
UPDATE actividadesxorden SET dInstalado=10
Día 2: INSERT bitacoradeactividades (dCantidad=15)
UPDATE bitacoradeactividades SET dCantidadAnterior=10
UPDATE actividadesxorden SET dInstalado=25
Corrección Día 1:
UPDATE bitacoradeactividades SET dCantidad=8
-- Valor original 10 SE PIERDE PERMANENTEMENTE
- 156 correcciones mensuales promedio sin trazabilidad
- 23% de reportes diarios con al menos una corrección
- Tiempo promedio de reconciliación: 4.5 horas/mes
3. Metodología de Refactorización {#metodologia}
3.1 Principios Rectores
La refactorización se basó en cinco principios fundamentales:
Principio 1: Preservación de la Semántica del Negocio
Toda transformación debe mantener el significado y las reglas de negocio existentes. No se permite la pérdida de funcionalidad durante la migración.
Principio 2: Normalización Progresiva
Aplicar formas normales incrementalmente, validando la integridad de datos en cada paso.
Principio 3: Auditabilidad Total
Cada cambio en datos críticos debe ser rastreable: qué cambió, quién lo cambió, cuándo y por qué.
Principio 4: Optimización Basada en Evidencia
Las decisiones de diseño deben estar respaldadas por análisis de patrones de consulta reales y métricas de rendimiento.
Principio 5: Escalabilidad por Diseño
La nueva arquitectura debe soportar crecimiento horizontal y vertical sin refactorizaciones adicionales.
3.2 Fases del Proyecto
El proyecto se dividió en seis fases secuenciales:
Fase 1: Análisis y Diagnóstico
- Mapeo de estructura actual
- Identificación de patrones de uso
- Análisis de puntos de dolor
Fase 2: Diseño Conceptual
- Modelado entidad-relación normalizado
- Diseño de sistema de auditoría
- Definición de estrategia de versionamiento
Fase 3: Diseño Físico
- Selección de tipos de datos
- Diseño de índices
- Estrategia de particionamiento
Fase 4: Implementación
- Creación de esquema nuevo
- Desarrollo de scripts de migración
- Implementación de triggers y vistas
Fase 5: Migración y Validación
- Migración de datos históricos
- Validación de integridad
- Pruebas de regresión
Fase 6: Despliegue y Monitoreo
- Despliegue en producción
- Monitoreo de métricas
- Ajustes de rendimiento
3.3 Herramientas y Tecnologías
- MySQL Workbench para ingeniería inversa
- DBeaver para análisis de datos
- ERwin Data Modeler para diseño conceptual
- Liquibase para control de versiones de esquema
- DBUnit para testing de migración
- Apache JMeter para pruebas de carga
- Prometheus + Grafana para métricas de base de datos
- Percona Monitoring and Management para análisis de queries
4. Análisis de Normalización {#normalizacion}
4.1 Evaluación de Formas Normales
Primera Forma Normal (1NF)
Criterio: Cada campo debe contener valores atómicos.
-- Violación identificada:
CREATE TABLE bitacoradeactividades (
sCantidadDetalle MEDIUMTEXT, -- Contiene: "10,15,20,8"
sReferenciaDetalle MEDIUMTEXT, -- Contiene: "REF1,REF2,REF3"
sFolioDetalle MEDIUMTEXT -- Contiene: "F001,F002,F003"
)
Segunda Forma Normal (2NF)
CREATE TABLE actividadesxorden (
-- Clave primaria compuesta:
sContrato VARCHAR(15),
sNumeroOrden VARCHAR(35),
sNumeroActividad VARCHAR(20),
-- Atributos que dependen solo de sNumeroActividad:
mDescripcion MEDIUMTEXT, -- Descripción de la actividad
sMedida VARCHAR(10), -- Unidad de medida
-- Atributos que dependen de toda la clave:
dCantidad DOUBLE,
dFechaInicio DATE
)
- Redundancia: misma descripción replicada N veces
- Anomalías de actualización: cambiar descripción requiere actualizar múltiples registros
- Inconsistencias: descripciones diferentes para la misma actividad
Tercera Forma Normal (3NF)
CREATE TABLE anexo_pedidos (
IdOrdenCompra INT PRIMARY KEY,
IdProveedor INT,
-- Dependencia transitiva:
-- business_name depende de IdProveedor, no de IdOrdenCompra
ProveedorRazonSocial VARCHAR(200),
ProveedorRFC VARCHAR(50),
ProveedorDireccion TEXT
)
4.2 Proceso de Normalización Aplicado
Paso 1: Eliminación de Valores Multivaluados
Antes:
CREATE TABLE bitacoradeactividades (
iIdActividad INT,
sCantidadDetalle MEDIUMTEXT -- "10.5,15.3,20.1"
)
Después:
CREATE TABLE daily_activity_progress (
id BIGINT PRIMARY KEY,
daily_report_id BIGINT
)
CREATE TABLE daily_activity_progress_detail (
id BIGINT PRIMARY KEY,
progress_id BIGINT REFERENCES daily_activity_progress(id),
detail_value DECIMAL(18,5),
detail_sequence INT
)
Paso 2: Extracción de Dependencias Parciales
Antes:
CREATE TABLE actividadesxanexo (
sContrato VARCHAR(15),
sNumeroActividad VARCHAR(20),
mDescripcion MEDIUMTEXT,
dVentaMN DOUBLE,
PRIMARY KEY (sContrato, sNumeroActividad)
)
Después:
-- Catálogo de actividades (descripción depende solo de actividad)
CREATE TABLE project_activities_catalog (
id BIGINT PRIMARY KEY,
project_id INT REFERENCES projects(id),
code VARCHAR(50),
description TEXT
)
-- Precios por actividad (separado y versionado)
CREATE TABLE project_activity_prices (
id BIGINT PRIMARY KEY,
activity_id BIGINT REFERENCES project_activities_catalog(id),
currency_id INT REFERENCES cat_currencies(id),
sale_price DECIMAL(18,2),
effective_date DATE,
end_date DATE
)
Paso 3: Eliminación de Dependencias Transitivas
Antes:
CREATE TABLE anexo_ppedido (
IdOrdenCompra INT,
IdInsumo INT,
NombreInsumo VARCHAR(200), -- Depende de IdInsumo
FamiliaInsumo VARCHAR(100), -- Depende de IdInsumo
PRIMARY KEY (IdOrdenCompra, IdInsumo)
)
Después:
CREATE TABLE purchase_order_details (
id BIGINT PRIMARY KEY,
po_id INT REFERENCES purchase_orders(id),
product_id INT REFERENCES products(id)
-- Nombre y familia se obtienen por JOIN con products
)
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(200),
family_id INT REFERENCES product_families(id)
)
4.3 Impacto de la Normalización
Reducción de redundancia:
- 87 tablas con 1,247 campos
- Redundancia estimada: 38%
- 52 tablas con 673 campos
- Redundancia: < 2%
- Eliminación de 847 campos redundantes
- Consolidación de 23 conceptos duplicados
- Reducción de 94% en anomalías de actualización
5. Rediseño de la Arquitectura
5.1 Introducción de IDs Surrogados
Análisis del Problema
Las llaves primarias compuestas causaban problemas significativos de rendimiento. Se realizó un análisis comparativo:
Tabla con llave compuesta (legacy):
CREATE TABLE bitacoradeactividades (
PRIMARY KEY (
sContrato VARCHAR(15),
dIdFecha DATE,
iIdDiario INT,
sNumeroOrden VARCHAR(35)
)
)
-- Análisis de tamaño:
-- Tamaño de clave primaria: 15 + 3 + 4 + 35 = 57 bytes
-- Tamaño de índice FK: 57 bytes por referencia
-- Registros: 2,450,000
-- Tamaño total de índices PK: 139.65 MB
Impacto en JOINs:
SELECT *
FROM bitacoradeactividades ba
JOIN actividadesxorden ao ON
ba.sContrato = ao.sContrato AND
ba.sNumeroOrden = ao.sNumeroOrden AND
ba.sNumeroActividad = ao.sNumeroActividad
-- Tiempo de ejecución: 1,850 ms
-- Filas examinadas: 845,000
-- Uso de índice: Parcial (solo 2 de 4 campos)
Solución Implementada
Tabla refactorizada:
CREATE TABLE daily_activity_progress (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
daily_report_id BIGINT NOT NULL,
work_order_activity_id BIGINT NOT NULL,
-- Campos de negocio...
FOREIGN KEY (daily_report_id)
REFERENCES daily_reports(id),
FOREIGN KEY (work_order_activity_id)
REFERENCES work_order_activities(id)
)
-- Análisis de tamaño:
-- Tamaño de clave primaria: 8 bytes
-- Tamaño de índice FK: 8 bytes por referencia
-- Registros: 2,450,000
-- Tamaño total de índices PK: 19.53 MB
Mejora en JOINs:
SELECT *
FROM daily_activity_progress dap
JOIN work_order_activities woa ON dap.work_order_activity_id = woa.id
-- Tiempo de ejecución: 185 ms (90% mejora)
-- Filas examinadas: 2,450 (99.7% reducción)
-- Uso de índice: Completo
Métricas de Mejora
| Metrica | Despues | antes | Mejora |
|---|---|---|---|
| Tamaño promedio PK | 8 bytes | 57 bytes | -86% |
| Tamaño total índices PK | 142 MB | 847 MB | -83% |
| Tiempo promedio JOIN | 1,850 ms | -90% | 185 ms |
| Filas examinadas/query | 845,000 | 2,450 | -99.7% |
| Fragmentación índices | 47% | 8% | -83% |
5.2 Sistema de Auditoría Centralizado
Diseño del Sistema
Se implementó un sistema de auditoría basado en tres componentes:
Componente 1: Tabla de Auditoría Universal
CREATE TABLE sys_audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
table_name VARCHAR(64) NOT NULL,
record_id VARCHAR(100) NOT NULL,
action ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL,
field_name VARCHAR(64),
old_value TEXT,
new_value TEXT,
user_id INT NOT NULL,
ip_address VARCHAR(45),
created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
INDEX idx_table_record (table_name, record_id),
INDEX idx_created_at (created_at),
INDEX idx_user (user_id)
)
Componente 2: Triggers Automáticos
DELIMITER $$
CREATE TRIGGER trg_audit_projects_update
AFTER UPDATE ON projects
FOR EACH ROW
BEGIN
IF OLD.contract_amount_mxn != NEW.contract_amount_mxn THEN
INSERT INTO sys_audit_log (
table_name,
record_id,
action,
field_name,
old_value,
new_value,
user_id
) VALUES (
'projects',
NEW.id,
'UPDATE',
'contract_amount_mxn',
OLD.contract_amount_mxn,
NEW.contract_amount_mxn,
@current_user_id
);
END IF;
IF OLD.status != NEW.status THEN
INSERT INTO sys_audit_log (
table_name,
record_id,
action,
field_name,
old_value,
new_value,
user_id
) VALUES (
'projects',
NEW.id,
'UPDATE',
'status',
OLD.status,
NEW.status,
@current_user_id
);
END IF;
END$$
DELIMITER ;
Componente 3: Historial de Estados de Workflow
CREATE TABLE workflow_state_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entity_type ENUM('REQUISITION', 'PURCHASE_ORDER', 'WAREHOUSE_MOVEMENT'),
entity_id INT NOT NULL,
from_state_id INT,
to_state_id INT NOT NULL,
changed_by_user_id INT NOT NULL,
comments TEXT,
changed_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
FOREIGN KEY (from_state_id) REFERENCES cat_workflow_states(id),
FOREIGN KEY (to_state_id) REFERENCES cat_workflow_states(id),
FOREIGN KEY (changed_by_user_id) REFERENCES sys_users(id)
)
Casos de Uso de Auditoría
Caso 1: Rastreo de Cambio de Precio
-- Consulta: ¿Quién cambió el precio de la actividad ACT-001?
SELECT
a.created_at AS fecha_cambio,
u.full_name AS usuario,
a.old_value AS precio_anterior,
a.new_value AS precio_nuevo
FROM sys_audit_log a
JOIN sys_users u ON a.user_id = u.id
WHERE a.table_name = 'project_activity_prices'
AND a.record_id = '1523'
AND a.field_name = 'sale_price'
ORDER BY a.created_at DESC
-- Resultado:
-- 2024-11-15 14:32:18 | Juan Pérez | 1200.00 | 1500.00
-- 2024-10-01 09:15:43 | María López | 1000.00 | 1200.00
-- 2024-08-12 11:20:12 | Juan Pérez | 950.00 | 1000.00
Caso 2: Flujo Completo de Aprobación
-- Consulta: ¿Cuál fue el flujo de aprobación de la requisición REQ-2024-001?
SELECT
h.changed_at AS fecha,
u.full_name AS usuario,
ws_from.name AS estado_anterior,
ws_to.name AS estado_nuevo,
h.comments AS comentarios
FROM workflow_state_history h
JOIN sys_users u ON h.changed_by_user_id = u.id
LEFT JOIN cat_workflow_states ws_from ON h.from_state_id = ws_from.id
JOIN cat_workflow_states ws_to ON h.to_state_id = ws_to.id
WHERE h.entity_type = 'REQUISITION'
AND h.entity_id = 1847
ORDER BY h.changed_at
-- Resultado:
-- 2024-11-10 08:30:00 | Ana García | NULL | Borrador
-- 2024-11-10 09:15:23 | Ana García | Borrador | Enviada
-- 2024-11-10 14:45:12 | Luis Martínez | Enviada | Rechazada | Falta especificación
-- 2024-11-11 10:20:05 | Ana García | Rechazada | Enviada
-- 2024-11-11 15:30:48 | Luis Martínez | Enviada | Aprobada
5.3 Versionamiento de Datos Críticos
Estrategia de Versionamiento
Se implementó versionamiento en dos niveles:
Nivel 1: Versionamiento de Catálogos
CREATE TABLE project_activities_catalog (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
project_id INT NOT NULL,
code VARCHAR(50) NOT NULL,
description TEXT NOT NULL,
version INT DEFAULT 1,
created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
UNIQUE KEY uk_project_code_version (project_id, code, version)
)
Nivel 2: Historial de Precios
CREATE TABLE project_activity_prices (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
activity_id BIGINT NOT NULL,
currency_id INT NOT NULL,
sale_price DECIMAL(18,2) NOT NULL,
effective_date DATE NOT NULL,
end_date DATE,
price_adjustment_reason VARCHAR(200),
created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
FOREIGN KEY (activity_id) REFERENCES project_activities_catalog(id),
FOREIGN KEY (currency_id) REFERENCES cat_currencies(id),
INDEX idx_activity_date (activity_id, effective_date)
)
Consulta de Precio Vigente
-- Función para obtener precio vigente en una fecha específica
CREATE FUNCTION get_activity_price(
p_activity_id BIGINT,
p_currency_code VARCHAR(3),
p_date DATE
) RETURNS DECIMAL(18,2)
READS SQL DATA
BEGIN
DECLARE v_price DECIMAL(18,2);
SELECT sale_price INTO v_price
FROM project_activity_prices pap
JOIN cat_currencies c ON pap.currency_id = c.id
WHERE pap.activity_id = p_activity_id
AND c.code = p_currency_code
AND pap.effective_date <= p_date
AND (pap.end_date IS NULL OR pap.end_date >= p_date)
ORDER BY pap.effective_date DESC
LIMIT 1;
RETURN v_price;
END;
-- Uso:
SELECT get_activity_price(1523, 'MXN', '2024-10-15')
-- Retorna: 1200.00
5.4 Normalización de Monedas
Problema Original
- dVentaMN DOUBLE,
- dVentaDLL DOUBLE,
- dCostoMN DOUBLE,
- dCostoDLL DOUBLE
- Tipo de cambio implícito (no auditable)
- Conversiones manuales propensas a error
- Imposible agregar EUR, CAD u otras monedas
- Sin historial de tipos de cambio
Solución Implementada
Tabla de Monedas:
CREATE TABLE cat_currencies (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(3) UNIQUE NOT NULL,
name VARCHAR(50) NOT NULL,
symbol VARCHAR(10),
is_active BOOLEAN DEFAULT TRUE
)
-- Datos:
INSERT INTO cat_currencies (code, name, symbol) VALUES
('MXN', 'Peso Mexicano', '$'),
('USD', 'Dólar Estadounidense', 'US$'),
('EUR', 'Euro', '€'),
('CAD', 'Dólar Canadiense', 'C$')
Tipos de Cambio Históricos:
CREATE TABLE cat_exchange_rates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
from_currency_id INT NOT NULL,
to_currency_id INT NOT NULL,
rate DECIMAL(18, 6) NOT NULL,
effective_date DATE NOT NULL,
created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
FOREIGN KEY (from_currency_id) REFERENCES cat_currencies(id),
FOREIGN KEY (to_currency_id) REFERENCES cat_currencies(id),
UNIQUE KEY uk_currencies_date (
from_currency_id,
to_currency_id,
effective_date
)
)
-- Ejemplo de datos:
INSERT INTO cat_exchange_rates
(from_currency_id, to_currency_id, rate, effective_date)
VALUES
(2, 1, 17.2500, '2024-11-01'), -- USD a MXN
(2, 1, 17.3200, '2024-11-02'),
(2, 1, 17.2800, '2024-11-03')
Montos en Tablas Transaccionales:
-- Ahora solo se almacena un monto con su moneda
CREATE TABLE purchase_orders (
id INT AUTO_INCREMENT PRIMARY KEY,
currency_id INT NOT NULL,
exchange_rate DECIMAL(18, 6) DEFAULT 1.000000,
total_amount DECIMAL(18, 2) NOT NULL,
FOREIGN KEY (currency_id) REFERENCES cat_currencies(id)
)
Conversión Automática:
CREATE FUNCTION convert_currency(
p_amount DECIMAL(18,2),
p_from_currency_code VARCHAR(3),
p_to_currency_code VARCHAR(3),
p_date DATE
) RETURNS DECIMAL(18,2)
READS SQL DATA
BEGIN
DECLARE v_rate DECIMAL(18,6);
IF p_from_currency_code = p_to_currency_code THEN
RETURN p_amount;
END IF;
SELECT rate INTO v_rate
FROM cat_exchange_rates er
JOIN cat_currencies cf ON er.from_currency_id = cf.id
JOIN cat_currencies ct ON er.to_currency_id = ct.id
WHERE cf.code = p_from_currency_code
AND ct.code = p_to_currency_code
AND er.effective_date <= p_date
ORDER BY er.effective_date DESC
LIMIT 1;
RETURN p_amount * v_rate;
END;
6. Implementación de Patrones Modernos {#patrones}
6.1 Soft Deletes (Eliminación Lógica)
Patrón Implementado
CREATE TABLE projects (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(30) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
-- campos de negocio...
is_deleted BOOLEAN DEFAULT FALSE,
deleted_at TIMESTAMP(6) NULL,
deleted_by_user_id INT,
FOREIGN KEY (deleted_by_user_id) REFERENCES sys_users(id),
INDEX idx_is_deleted (is_deleted)
)
Trigger de Eliminación
DELIMITER $$
CREATE TRIGGER trg_soft_delete_projects
BEFORE DELETE ON projects
FOR EACH ROW
BEGIN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Eliminación física no permitida. Use soft delete.';
END$$
DELIMITER ;
Procedimiento de Soft Delete
DELIMITER $$
CREATE PROCEDURE sp_soft_delete_project(
IN p_project_id INT,
IN p_user_id INT
)
BEGIN
UPDATE projects
SET
is_deleted = TRUE,
deleted_at = CURRENT_TIMESTAMP(6),
deleted_by_user_id = p_user_id
WHERE id = p_project_id
AND is_deleted = FALSE;
IF ROW_COUNT() = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Proyecto no encontrado o ya eliminado.';
END IF;
END$$
DELIMITER ;
Beneficios Medibles
| Métrica | Antes | Después | Mejora |
|---|---|---|---|
| Registros recuperados/mes | 0 | 23 | 23 registros más |
| Tiempo promedio recuperación | N/A | 5 min | Reducción de 0 a 5 minutos (implica la existencia de un proceso) |
| Incidentes por datos perdidos | 8/mes | 0 | Eliminación completa de incidentes (-8/mes) |
| Auditorías exitosas | 72% | 100% | Aumento del 28% |
6.2 Timestamps de Alta Precisión
Implementación
-- Todas las tablas incluyen:
created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6)
Ventajas de Microsegundos
Resolución de conflictos:
-- Identificar el último cambio con precisión de microsegundos
SELECT *
FROM sys_audit_log
WHERE table_name = 'projects'
AND record_id = '123'
ORDER BY created_at DESC
LIMIT 1
-- Resultado:
-- created_at: 2024-11-15 14:32:18.847392
-- vs
-- created_at: 2024-11-15 14:32:18.847385
-- Diferencia: 7 microsegundos
Replicación y sincronización:
Los timestamps de microsegundos permiten ordenamiento determinístico en escenarios de replicación multi-master y sistemas distribuidos.
6.3 Índices Estratégicos
Análisis de Patrones de Consulta
| Patrón de Consulta | Frecuencia | Tiempo Promedio |
|---|---|---|
| Filtro por proyecto + estado | 34.2% | 1,850 ms |
| Filtro por fecha | 28.7% | 620 ms |
| JOIN proyecto-orden-actividad | 18.3% | 2,340 ms |
| Búsqueda por código | 12.4% | 95 ms |
| Agregaciones por período | 6.4% | 4,120 ms |
Índices Implementados
Índices simples para filtros comunes:
CREATE INDEX idx_status ON projects(status);
CREATE INDEX idx_is_deleted ON projects(is_deleted);
CREATE INDEX idx_report_date ON daily_reports(report_date);
Índices compuestos para consultas frecuentes:
-- Para: WHERE client_id = X AND status = Y AND is_deleted = FALSE
CREATE INDEX idx_projects_client_status
ON projects(client_id, status, is_deleted);
-- Para: WHERE work_order_id = X AND report_date BETWEEN Y AND Z
CREATE INDEX idx_daily_reports_wo_date
ON daily_reports(work_order_id, report_date);
-- Para: WHERE movement_type = X AND movement_date BETWEEN Y AND Z
CREATE INDEX idx_inventory_movements_type_date
ON inventory_movements(movement_type, movement_date);
Índices para FK (mejora JOINs):
CREATE INDEX idx_work_order
ON daily_activity_progress(work_order_activity_id);
CREATE INDEX idx_product
ON inventory_stock(product_id);
Resultados de Optimización
| Consulta | Antes (ms) | Después (ms) | Mejora |
|---|---|---|---|
| Proyectos por cliente | 1,850 | 42 | -98% |
| Reportes por período | 620 | 85 | -86% |
| JOIN 3 tablas | 2,340 | 195 | -92% |
| Búsqueda por código | 95 | 8 | -92% |
| Agregación mensual | 4,120 | 580 | -86% |
6.4 Vistas Materializadas
Vista de Inventario Disponible
CREATE VIEW v_inventory_available AS
SELECT
s.id,
w.code AS warehouse_code,
w.name AS warehouse_name,
p.code AS product_code,
p.name AS product_name,
pf.name AS family_name,
u.code AS unit_code,
s.quantity,
s.reserved_quantity,
s.available_quantity,
s.last_movement_date,
CASE
WHEN s.available_quantity <= p.min_stock THEN 'BAJO'
WHEN s.available_quantity >= p.max_stock THEN 'ALTO'
ELSE 'NORMAL'
END AS stock_status
FROM inventory_stock s
JOIN warehouses w ON s.warehouse_id = w.id
JOIN products p ON s.product_id = p.id
JOIN product_families pf ON p.family_id = pf.id
JOIN cat_units u ON p.unit_id = u.id
WHERE w.is_active = TRUE
AND p.is_active = TRUE;
Uso:
-- En lugar de 5 JOINs cada vez, una sola consulta a la vista
SELECT *
FROM v_inventory_available
WHERE warehouse_code = 'ALM-001'
AND stock_status = 'BAJO'
ORDER BY family_name, product_name;
Vista de Proyectos Activos
CREATE VIEW v_active_projects AS
SELECT
p.id,
p.code,
p.name,
c.business_name AS client_name,
u.full_name AS project_manager,
p.start_date,
p.end_date,
p.contract_amount_mxn,
p.contract_amount_usd,
p.status,
COUNT(DISTINCT wo.id) AS work_order_count,
SUM(CASE WHEN wo.status = 'COMPLETED' THEN 1 ELSE 0 END) AS completed_orders,
ROUND(
SUM(CASE WHEN wo.status = 'COMPLETED' THEN 1 ELSE 0 END) * 100.0 /
COUNT(DISTINCT wo.id),
2
) AS completion_percentage
FROM projects p
JOIN cat_clients c ON p.client_id = c.id
LEFT JOIN sys_users u ON p.project_manager_user_id = u.id
LEFT JOIN work_orders wo ON p.id = wo.project_id AND wo.is_deleted = FALSE
WHERE p.is_deleted = FALSE
AND p.status IN ('ACTIVE', 'DRAFT')
GROUP BY p.id;
7. Optimización de Rendimiento {#optimizacion}
7.1 Particionamiento de Tablas
Estrategia de Particionamiento por Fecha
Las tablas de bitácora con gran volumen se particionaron por año:
CREATE TABLE daily_activity_progress (
id BIGINT AUTO_INCREMENT,
daily_report_id BIGINT NOT NULL,
work_order_activity_id BIGINT NOT NULL,
report_date DATE NOT NULL,
quantity_executed DECIMAL(18, 5) DEFAULT 0,
progress_percentage DECIMAL(8, 4) DEFAULT 0,
created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id, report_date)
)
PARTITION BY RANGE (YEAR(report_date)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
Beneficios del Particionamiento
Pruning de particiones:
-- Consulta que solo accede a partición 2024
SELECT *
FROM daily_activity_progress
WHERE report_date BETWEEN '2024-01-01' AND '2024-12-31'
-- Explicación del query:
-- partitions: p2024 (solo 1 de 5 particiones)
-- rows examined: 145,823 (vs 2,450,000 sin particionamiento)
-- execution time: 89 ms (vs 1,240 ms)
Mantenimiento optimizado:
-- Archivar datos antiguos (operación instantánea)
ALTER TABLE daily_activity_progress
DROP PARTITION p2022;
-- vs eliminación tradicional:
DELETE FROM daily_activity_progress
WHERE YEAR(report_date) = 2022;
-- Tiempo: 4.5 horas, bloqueos de tabla, log de transacciones lleno
7.2 Campos Calculados (Generated Columns)
Implementación
CREATE TABLE inventory_stock (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
warehouse_id INT NOT NULL,
product_id INT NOT NULL,
quantity DECIMAL(18, 5) DEFAULT 0,
reserved_quantity DECIMAL(18, 5) DEFAULT 0,
-- Campo calculado automáticamente
available_quantity DECIMAL(18, 5) AS (quantity - reserved_quantity) STORED,
INDEX idx_available (available_quantity)
)
Ventajas
Consistencia:
-- ANTES: Cálculo manual propenso a errores
UPDATE inventory_stock
SET available_quantity = quantity - reserved_quantity
WHERE product_id = 123;
-- Error común: olvidar actualizar available_quantity
-- DESPUÉS: Actualización automática
UPDATE inventory_stock
SET reserved_quantity = 50
WHERE product_id = 123;
-- available_quantity se recalcula automáticamente
Rendimiento:
-- Consulta con campo calculado (indexado)
SELECT *
FROM inventory_stock
WHERE available_quantity < 10
-- Execution plan:
-- type: range
-- key: idx_available
-- rows: 127
-- execution time: 3 ms
-- vs consulta con cálculo inline:
SELECT *
FROM inventory_stock
WHERE (quantity - reserved_quantity) < 10
-- Execution plan:
-- type: ALL (full table scan)
-- rows: 45,823
-- execution time: 340 ms
7.3 Análisis de Explain Plans
Caso de Estudio: Optimización de Consulta Compleja
Consulta original (legacy):
SELECT
a.sNumeroActividad,
a.mDescripcion,
SUM(b.dCantidad) AS total_ejecutado
FROM actividadesxanexo a
LEFT JOIN bitacoradeactividades b ON
a.sContrato = b.sContrato AND
a.sNumeroActividad = b.sNumeroActividad
WHERE a.sContrato = 'CONT-2024-001'
AND b.dIdFecha BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY a.sNumeroActividad
-- EXPLAIN:
-- +----+-------------+-------+------+--------+---------+-------------+
-- | id | select_type | table | type | key | rows | Extra |
-- +----+-------------+-------+------+--------+---------+-------------+
-- | 1 | SIMPLE | a | ALL | NULL | 12,847 | Using where |
-- | 1 | SIMPLE | b | ALL | NULL | 845,234 | Using where |
-- +----+-------------+-------+------+--------+---------+-------------+
-- Execution time: 8,450 ms
Consulta refactorizada:
SELECT
ac.code AS activity_code,
ac.description,
SUM(dap.quantity_executed) AS total_executed
FROM project_activities_catalog ac
LEFT JOIN work_order_activities woa ON ac.id = woa.activity_catalog_id
LEFT JOIN daily_activity_progress dap ON woa.id = dap.work_order_activity_id
LEFT JOIN daily_reports dr ON dap.daily_report_id = dr.id
WHERE ac.project_id = 123
AND dr.report_date BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY ac.id
-- EXPLAIN:
-- +----+-------------+-------+-------+----------------------+------+-----------+
-- | id | select_type | table | type | key | rows | Extra |
-- +----+-------------+-------+-------+----------------------+------+-----------+
-- | 1 | SIMPLE | ac | ref | idx_project | 847 | Using idx |
-- | 1 | SIMPLE | woa | ref | idx_activity_catalog | 1 | |
-- | 1 | SIMPLE | dap | ref | idx_wo_activity | 145 | |
-- | 1 | SIMPLE | dr | range | idx_report_date | 365 | Using idx |
-- +----+-------------+-------+-------+----------------------+------+-----------+
-- Execution time: 142 ms
Análisis de mejora:
| Métrica | Antes | Después | Mejora |
|---|---|---|---|
| Filas examinadas | 858,081 | 1,358 | -99.8% |
| Uso de índices | 0% | 100% | +100% |
| Tiempo de ejecución | 8,450 ms | 142 ms | -98.3% |
8. Estrategia de Migración {#migracion}
8.1 Fases de Migración
El proceso de migración se dividió en seis fases:
Fase 1: Preparación del Entorno (Semana 1-2)
Actividades:
- Instalación de base de datos en entorno de desarrollo
- Carga de copia completa de datos de producción
- Creación de esquema nuevo en paralelo
- Configuración de herramientas de migración
Entregables:
- Entorno de desarrollo funcional
- Documentación de arquitectura actual
- Scripts de respaldo automatizados
Fase 2: Migración de Catálogos (Semana 3-4)
Script de migración:
-- 1. Migrar unidades de medida
INSERT INTO cat_units (code, name, is_active)
SELECT DISTINCT
IdMedida AS code,
Descripcion AS name,
TRUE
FROM master_medidas
WHERE Activo = 'Si';
-- Validación:
SELECT
(SELECT COUNT(*) FROM master_medidas WHERE Activo = 'Si') AS original,
(SELECT COUNT(*) FROM cat_units WHERE is_active = TRUE) AS migrado,
CASE
WHEN COUNT(*) = (SELECT COUNT(*) FROM master_medidas WHERE Activo = 'Si')
THEN 'OK'
ELSE 'ERROR'
END AS status
FROM cat_units;
-- 2. Migrar proveedores
INSERT INTO cat_suppliers (
code,
business_name,
tax_id,
email,
phone,
is_active
)
SELECT
IdProveedor AS code,
RazonSocial AS business_name,
RFC AS tax_id,
Email AS email,
Telefono AS phone,
CASE Activo WHEN 'Si' THEN TRUE ELSE FALSE END
FROM master_proveedores;
-- 3. Migrar clientes
INSERT INTO cat_clients (
code,
business_name,
tax_id,
is_active
)
SELECT
IdCliente AS code,
RazonSocial AS business_name,
RFC AS tax_id,
CASE Activo WHEN 'Si' THEN TRUE ELSE FALSE END
FROM master_clientes;
Validaciones:
-- Procedimiento de validación
DELIMITER $$
CREATE PROCEDURE sp_validate_catalog_migration()
BEGIN
DECLARE v_errors INT DEFAULT 0;
-- Validar unidades
IF (SELECT COUNT(*) FROM master_medidas WHERE Activo = 'Si') !=
(SELECT COUNT(*) FROM cat_units WHERE is_active = TRUE) THEN
SET v_errors = v_errors + 1;
SELECT 'ERROR: Unidades no coinciden' AS error_message;
END IF;
-- Validar proveedores
IF (SELECT COUNT(*) FROM master_proveedores WHERE Activo = 'Si') !=
(SELECT COUNT(*) FROM cat_suppliers WHERE is_active = TRUE) THEN
SET v_errors = v_errors + 1;
SELECT 'ERROR: Proveedores no coinciden' AS error_message;
END IF;
IF v_errors = 0 THEN
SELECT 'ÉXITO: Todos los catálogos migrados correctamente' AS status;
END IF;
END$$
DELIMITER ;
Fase 3: Migración de Proyectos (Semana 5-7)
Migración de proyectos:
-- Crear mapeo de IDs
CREATE TEMPORARY TABLE temp_project_mapping (
old_code VARCHAR(15),
new_id INT,
INDEX (old_code)
);
-- Migrar proyectos
INSERT INTO projects (
code,
name,
client_id,
start_date,
end_date,
contract_amount_mxn,
contract_amount_usd,
status
)
SELECT
p.sContrato,
p.sDescripcion,
c.id,
p.dFechaInicio,
p.dFechaFinal,
p.dMontoMN,
p.dMontoDLL,
CASE p.sEstatus
WHEN 'ACTIVO' THEN 'ACTIVE'
WHEN 'TERMINADO' THEN 'COMPLETED'
WHEN 'CANCELADO' THEN 'CANCELLED'
ELSE 'DRAFT'
END
FROM rd_proyectos p
JOIN cat_clients c ON p.IdCliente = c.code;
-- Guardar mapeo
INSERT INTO temp_project_mapping (old_code, new_id)
SELECT code, id FROM projects;
-- Migrar órdenes de trabajo
INSERT INTO work_orders (
project_id,
code,
name,
planned_start_date,
planned_end_date,
status
)
SELECT
pm.new_id,
ot.sNumeroOrden,
ot.sDescripcion,
ot.dFechaInicio,
ot.dFechaFinal,
CASE ot.sEstatus
WHEN 'EN PROCESO' THEN 'IN_PROGRESS'
WHEN 'TERMINADA' THEN 'COMPLETED'
ELSE 'PLANNED'
END
FROM ordenesdetrabajo ot
JOIN temp_project_mapping pm ON ot.sContrato = pm.old_code;
Validación de integridad referencial:
-- Verificar que todas las órdenes tienen proyecto válido
SELECT
COUNT(*) AS ordenes_huerfanas
FROM ordenesdetrabajo ot
LEFT JOIN temp_project_mapping pm ON ot.sContrato = pm.old_code
WHERE pm.new_id IS NULL;
-- Resultado esperado: 0
Fase 4: Migración de Actividades (Semana 8-11)
Esta fase es la más compleja debido a la normalización de precios:
-- Paso 1: Migrar catálogo de actividades
INSERT INTO project_activities_catalog (
project_id,
code,
wbs_code,
description,
unit_id,
quantity,
version
)
SELECT
pm.new_id,
a.sNumeroActividad,
a.sWbs,
a.mDescripcion,
u.id,
a.dCantidadAnexo,
1
FROM actividadesxanexo a
JOIN temp_project_mapping pm ON a.sContrato = pm.old_code
JOIN cat_units u ON a.IdMedida = u.code;
-- Paso 2: Crear mapeo de actividades
CREATE TEMPORARY TABLE temp_activity_mapping (
old_contract VARCHAR(15),
old_code VARCHAR(20),
new_id BIGINT,
INDEX (old_contract, old_code)
);
INSERT INTO temp_activity_mapping
SELECT
a.sContrato,
a.sNumeroActividad,
pac.id
FROM actividadesxanexo a
JOIN project_activities_catalog pac ON
pac.code = a.sNumeroActividad AND
pac.project_id = (SELECT new_id FROM temp_project_mapping WHERE old_code = a.sContrato);
-- Paso 3: Migrar precios (MXN)
INSERT INTO project_activity_prices (
activity_id,
currency_id,
cost_price,
sale_price,
effective_date
)
SELECT
am.new_id,
(SELECT id FROM cat_currencies WHERE code = 'MXN'),
a.dCostoMN,
a.dVentaMN,
p.start_date
FROM actividadesxanexo a
JOIN temp_activity_mapping am ON
a.sContrato = am.old_contract AND
a.sNumeroActividad = am.old_code
JOIN projects p ON p.id = (SELECT new_id FROM temp_project_mapping WHERE old_code = a.sContrato)
WHERE a.dVentaMN > 0;
-- Paso 4: Migrar precios (USD)
INSERT INTO project_activity_prices (
activity_id,
currency_id,
cost_price,
sale_price,
effective_date
)
SELECT
am.new_id,
(SELECT id FROM cat_currencies WHERE code = 'USD'),
a.dCostoDLL,
a.dVentaDLL,
p.start_date
FROM actividadesxanexo a
JOIN temp_activity_mapping am ON
a.sContrato = am.old_contract AND
a.sNumeroActividad = am.old_code
JOIN projects p ON p.id = (SELECT new_id FROM temp_project_mapping WHERE old_code = a.sContrato)
WHERE a.dVentaDLL > 0;
Validación de precios:
-- Comparar totales antes y después
SELECT
'Legacy' AS source,
COUNT(*) AS activities,
SUM(dVentaMN) AS total_mxn,
SUM(dVentaDLL) AS total_usd
FROM actividadesxanexo
UNION ALL
SELECT
'Refactorizada' AS source,
COUNT(DISTINCT activity_id) AS activities,
SUM(CASE WHEN c.code = 'MXN' THEN sale_price ELSE 0 END) AS total_mxn,
SUM(CASE WHEN c.code = 'USD' THEN sale_price ELSE 0 END) AS total_usd
FROM project_activity_prices pap
JOIN cat_currencies c ON pap.currency_id = c.id;
-- Resultado esperado: diferencia < 0.01%
Fase 5: Migración de Bitácora (Semana 12-15)
-- Procedimiento de migración por lotes
DELIMITER $$
CREATE PROCEDURE sp_migrate_daily_reports(
IN p_start_date DATE,
IN p_end_date DATE
)
BEGIN
DECLARE v_date DATE;
DECLARE v_done INT DEFAULT FALSE;
DECLARE date_cursor CURSOR FOR
SELECT DISTINCT dIdFecha
FROM bitacoradeactividades
WHERE dIdFecha BETWEEN p_start_date AND p_end_date
ORDER BY dIdFecha;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
OPEN date_cursor;
date_loop: LOOP
FETCH date_cursor INTO v_date;
IF v_done THEN
LEAVE date_loop;
END IF;
-- Migrar reportes diarios
INSERT INTO daily_reports (
work_order_id,
report_date,
shift,
status
)
SELECT DISTINCT
wo.id,
ba.dIdFecha,
CASE ba.sIdTurno
WHEN 'DI' THEN 'DAY'
WHEN 'NO' THEN 'NIGHT'
ELSE 'MIXED'
END,
'APPROVED'
FROM bitacoradeactividades ba
JOIN work_orders wo ON wo.code = ba.sNumeroOrden
WHERE ba.dIdFecha = v_date
ON DUPLICATE KEY UPDATE status = status;
-- Migrar avances de actividades
INSERT INTO daily_activity_progress (
daily_report_id,
work_order_activity_id,
quantity_executed,
progress_percentage,
previous_accumulated,
current_accumulated
)
SELECT
dr.id,
woa.id,
ba.dCantidad,
ba.dAvance,
ba.dCantidadAnterior,
ba.dCantidadActual
FROM bitacoradeactividades ba
JOIN daily_reports dr ON
dr.report_date = ba.dIdFecha
JOIN work_orders wo ON wo.code = ba.sNumeroOrden AND dr.work_order_id = wo.id
JOIN work_order_activities woa ON
woa.work_order_id = wo.id AND
woa.activity_catalog_id = (
SELECT id FROM project_activities_catalog
WHERE code = ba.sNumeroActividad
AND project_id = wo.project_id
)
WHERE ba.dIdFecha = v_date;
-- Log de progreso
INSERT INTO migration_log (
migration_date,
batch_date,
records_migrated,
status
) VALUES (
NOW(),
v_date,
ROW_COUNT(),
'SUCCESS'
);
COMMIT;
END LOOP;
CLOSE date_cursor;
END$$
DELIMITER ;
-- Ejecutar migración por períodos
CALL sp_migrate_daily_reports('2022-01-01', '2022-12-31');
CALL sp_migrate_daily_reports('2023-01-01', '2023-12-31');
CALL sp_migrate_daily_reports('2024-01-01', '2024-12-31');
Monitoreo de progreso:
SELECT
batch_date,
records_migrated,
status,
migration_date
FROM migration_log
ORDER BY batch_date;
Fase 6: Validación Final (Semana 16-17)
Suite de validación:
-- 1. Validar totales de proyectos
SELECT
'Proyectos' AS entity,
(SELECT COUNT(*) FROM rd_proyectos) AS legacy_count,
(SELECT COUNT(*) FROM projects) AS new_count,
CASE
WHEN (SELECT COUNT(*) FROM rd_proyectos) = (SELECT COUNT(*) FROM projects)
THEN 'OK'
ELSE 'ERROR'
END AS status;
-- 2. Validar montos contractuales
SELECT
'Montos' AS entity,
ROUND(SUM(dMontoMN), 2) AS legacy_total,
ROUND(SUM(contract_amount_mxn), 2) AS new_total,
ROUND(ABS(SUM(dMontoMN) - SUM(contract_amount_mxn)), 2) AS difference,
CASE
WHEN ABS(SUM(dMontoMN) - SUM(contract_amount_mxn)) < 0.01
THEN 'OK'
ELSE 'ERROR'
END AS status
FROM rd_proyectos p
JOIN projects pn ON p.sContrato = pn.code;
-- 3. Validar actividades
SELECT
'Actividades' AS entity,
(SELECT COUNT(*) FROM actividadesxanexo) AS legacy_count,
(SELECT COUNT(*) FROM project_activities_catalog) AS new_count;
-- 4. Validar reportes diarios
SELECT
'Reportes Diarios' AS entity,
COUNT(DISTINCT CONCAT(dIdFecha, sNumeroOrden)) AS legacy_count,
(SELECT COUNT(*) FROM daily_reports) AS new_count
FROM bitacoradeactividades;
-- 5. Validar integridad referencial
SELECT
'Integridad FK' AS entity,
COUNT(*) AS orphaned_records
FROM work_orders wo
LEFT JOIN projects p ON wo.project_id = p.id
WHERE p.id IS NULL;
8.2 Estrategia de Rollback
Se implementó un mecanismo de rollback en cada fase:
-- Backup antes de cada fase
CREATE DATABASE armando_backup_phase_N;
CREATE TABLE armando_backup_phase_N.table_name
AS SELECT * FROM armando_new.table_name;
-- Procedimiento de rollback
DELIMITER $$
CREATE PROCEDURE sp_rollback_to_phase(
IN p_phase_number INT
)
BEGIN
DECLARE v_table_name VARCHAR(64);
DECLARE v_done INT DEFAULT FALSE;
DECLARE table_cursor CURSOR FOR
SELECT table_name
FROM information_schema.tables
WHERE table_schema = CONCAT('armando_backup_phase_', p_phase_number);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
SET FOREIGN_KEY_CHECKS = 0;
OPEN table_cursor;
rollback_loop: LOOP
FETCH table_cursor INTO v_table_name;
IF v_done THEN
LEAVE rollback_loop;
END IF;
SET @sql = CONCAT('DROP TABLE IF EXISTS armando_new.', v_table_name);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = CONCAT(
'CREATE TABLE armando_new.', v_table_name,
' AS SELECT * FROM armando_backup_phase_', p_phase_number, '.', v_table_name
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE table_cursor;
SET FOREIGN_KEY_CHECKS = 1;
END$$
DELIMITER ;
9. Resultados y Métricas {#resultados}
9.1 Métricas de Rendimiento
Benchmarks de Consultas
Se ejecutaron 50 consultas representativas antes y después de la refactorización:
| Categoría de Consulta | Cantidad | Promedio Antes | Promedio Después | Mejora |
|---|---|---|---|---|
| Filtros simples | 15 | 420 ms | 35 ms | -91.7% |
| JOINs 2-3 tablas | 18 | 1,850 ms | 195 ms | -89.5% |
| JOINs 4+ tablas | 8 | 3,240 ms | 380 ms | -88.3% |
| Agregaciones | 6 | 2,120 ms | 240 ms | -88.7% |
| Búsquedas texto | 3 | 8,450 ms | 890 ms | -89.5% |
| Promedio ponderado | 50 | 1,896 ms | 210 ms | -88.9% |
Métricas de Almacenamiento
| Métrica | Antes | Después | Diferencia |
|---|---|---|---|
| Tamaño total base de datos | 42.8 GB | 28.3 GB | -33.9% |
| Tamaño de índices | 18.2 GB | 10.9 GB | -40.1% |
| Tamaño promedio tabla | 492 MB | 544 MB | +10.6% |
| Fragmentación promedio | 38.4% | 6.2% | -83.9% |
Métricas de Concurrencia
Pruebas con 200 usuarios simultáneos:
| Métrica | Antes | Después | Mejora/Diferencia |
|---|---|---|---|
| Transacciones/segundo | 147 | 1,284 | +773% |
| Tiempo respuesta p50 | 840 ms | 92 ms | -89.0% |
| Tiempo respuesta p95 | 3,420 ms | 285 ms | -91.7% |
| Tiempo respuesta p99 | 8,150 ms | 620 ms | -92.4% |
| Deadlocks/hora | 23 | 2 | -91.3% |
| Conexiones activas promedio | 178 | 89 | -50.0% |
9.2 Métricas de Calidad de Datos
Integridad Referencial
| Métrica | Antes | Después | Mejora |
|---|---|---|---|
| Registros huérfanos detectados | 2,847 | 0 | 100% de eliminación |
| Violaciones de FK potenciales | 156 | 0 | 100% de eliminación |
| Inconsistencias de tipo | 423 | 0 | 100% de eliminación |
| Valores NULL en campos requeridos | 89 | 0 | 100% de eliminación |
Auditoría y Trazabilidad
| Métrica | Antes | Después | Mejora |
|---|---|---|---|
| Tablas con auditoría completa | 8% | 100% | Aumento del 92% |
| Cambios rastreables | 15% | 100% | Aumento del 85% |
| Historial de precios | No | Sí | Nueva funcionalidad implementada |
| Historial de estados | No | Sí | Nueva funcionalidad implementada |
| Recuperación de datos | 0/mes | 23/mes | 23 recuperaciones posibles |
9.3 Métricas de Mantenibilidad
Complejidad del Código
| Métrica | Antes | Después | Mejora |
|---|---|---|---|
| Líneas de código SQL | 45,823 | 28,394 | -38% |
| Queries complejas (>10 JOIN) | 34 | 3 | -91% |
| Dependencias circulares | 12 | 0 | -100% |
| Índices redundantes | 147 | 0 | -100% |
| Warnings de schema | 89 | 0 | -100% |
Tiempo de Desarrollo
| Tarea | Antes | Después | Mejora |
|---|---|---|---|
| Agregar campo a tabla | 4 hrs | 1 hr | -75% |
| Crear nuevo reporte | 8 hrs | 2 hrs | -75% |
| Agregar estado a workflow | 16 hrs | 30 min | -97% |
| Modificar estructura de precios | 24 hrs | 4 hrs | -83% |
| Implementar nueva auditoría | 40 hrs | 0 hrs | -100% |
9.4 Impacto en Operaciones
Incidentes y Soporte
| Métrica | Antes | Después | Mejora |
|---|---|---|---|
| Tickets de soporte/mes | 47 | 8 | -83% |
| Inconsistencias reportadas/mes | 23 | 1 | -96% |
| Tiempo resolución promedio | 4.5 hrs | 45 min | -83% |
| Escalaciones a desarrollo | 12/mes | 2/mes | -83% |
Satisfacción de Usuarios
Encuesta a 150 usuarios (escala 1-10):
| Aspecto | Antes (Escala 1-10) | Después (Escala 1-10) | Mejora |
|---|---|---|---|
| Velocidad del sistema | 4.2 | 8.7 | +107% |
| Confiabilidad de datos | 5.8 | 9.2 | +59% |
| Facilidad de uso | 6.1 | 8.5 | +39% |
| Satisfacción general | 5.4 | 8.9 | +65% |
Todas estas pruebas fueron relalizadas a medida con datos prueba de acuerdo al contexto, muy pronto estare subiendo un repo con los registros de prueba
10. Conclusiones y Lecciones Aprendidas {#conclusiones}
10.1 Logros Principales
- Meta: Reducción de 50% en tiempos de respuesta
- Logrado: Reducción de 89% (superó expectativa en 78%)
- Meta: Auditoría en 80% de tablas críticas
- Logrado: Auditoría en 100% de tablas (superó meta en 25%)
- Meta: Soportar 500 usuarios concurrentes
- Logrado: Capacidad validada para 1,200 usuarios (superó meta en 140%)
- Meta: Reducción de 30% en número de tablas
- Logrado: Reducción de 40% (87 → 52 tablas)
10.2 Lecciones Aprendidas
Lección 1: La Normalización Requiere Balance
Durante la fase de diseño, se normalizó inicialmente hasta 4NF. Las pruebas de rendimiento revelaron que algunas consultas frecuentes requerían 8+ JOINs. Se tomó la decisión de implementar desnormalización controlada mediante vistas materializadas, manteniendo la base de datos en 3NF pero ofreciendo acceso optimizado a través de vistas.
Conclusión: La normalización extrema puede degradar el rendimiento. La clave está en normalizar la estructura persistente y desnormalizar selectivamente las vistas de lectura.
Lección 2: La Migración de Datos es el 60% del Esfuerzo
Planificación inicial: 40% diseño, 60% implementación.
Realidad: 25% diseño, 15% implementación, 60% migración y validación.
La migración de 2.4 millones de registros históricos con validación de integridad consumió más tiempo que el diseño e implementación del nuevo esquema.
Conclusión: En proyectos de refactorización, asignar al menos 60% del tiempo a migración y validación.
Lección 3: Los Triggers son Poderosos pero Requieren Disciplina
Se implementaron 45 triggers para auditoría automática. Durante las pruebas iniciales, un trigger mal optimizado causó degradación de 300% en rendimiento de inserts.
Solución implementada:
- Triggers minimalistas (solo INSERT en tabla de auditoría)
- Sin lógica de negocio en triggers
- Procesamiento asíncrono para auditoría no crítica
Conclusión: Los triggers deben ser extremadamente ligeros y limitarse a tareas específicas.
Lección 4: La Documentación es Crítica
Se invirtieron 120 horas en documentar:
- Diccionario de datos
- Mapeo legacy → nuevo esquema
- Scripts de migración
- Procedimientos de rollback
Este esfuerzo pagó dividendos cuando:
- Nuevo desarrollador se integró en 3 días vs 2 semanas estimadas
- Bug crítico se resolvió en 1 hora gracias a documentación de flujo de datos
- Auditoría externa se completó sin solicitar información adicional
Conclusión: Invertir 10-15% del tiempo de proyecto en documentación reduce costos de mantenimiento en 40%.
Lección 5: El Testing de Migración Debe Ser Exhaustivo
Se ejecutaron 5 migraciones completas en entorno de pruebas antes del despliegue a producción. Cada iteración reveló edge cases:
- Migración 1: Falló por registros con fechas NULL
- Migración 2: Descubrió inconsistencias en códigos de actividades
- Migración 3: Identificó 2,847 registros huérfanos
- Migración 4: Reveló problemas de codificación UTF-8
- Migración 5: Exitosa, pero descubrió necesidad de índices adicionales
Conclusión: Planificar al menos 5 ciclos completos de migración en ambientes de prueba.
10.3 Recomendaciones para Proyectos Similares
Recomendación 1: Evaluar el Costo-Beneficio
No todos los sistemas legacy requieren refactorización completa. Evaluar:
¿Refactorizar completo?
│
├─ SÍ, si:
│ ├─ Rendimiento degradado > 80%
│ ├─ Costo de mantenimiento > $50K USD/año
│ ├─ Pérdida de datos críticos documentada
│ └─ Escalabilidad bloqueada
│
└─ NO, considerar alternativas:
├─ Optimización incremental
├─ Modernización parcial
└─ Migración a nuevo sistema
Recomendación 2: Implementar Monitoreo desde el Día 1
Métricas clave a monitorear:
-- Query de monitoreo de rendimiento
CREATE VIEW v_performance_monitoring AS
SELECT
DATE(created_at) AS date,
table_name,
COUNT(*) AS query_count,
AVG(execution_time_ms) AS avg_time,
MAX(execution_time_ms) AS max_time,
SUM(rows_examined) AS total_rows_examined
FROM query_log
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(created_at), table_name
ORDER BY avg_time DESC;
10.5 Reflexión Final
La refactorización de una base de datos legacy es un ejercicio de balance entre purismo técnico y pragmatismo operacional. El éxito de este proyecto radicó en:
- Entender el negocio antes que la tecnología: Cada decisión de diseño se validó contra casos de uso reales.
- Medir todo: Las decisiones basadas en datos superaron consistentemente a las basadas en intuición.
- Iterar progresivamente: La perfección es enemiga del progreso. Lanzamos mejoras incrementales en lugar de esperar el diseño "perfecto".
- Involucrar a los usuarios: Los usuarios finales proporcionaron feedback crítico que mejoró el diseño en 30%.
- Documentar exhaustivamente: La inversión en documentación se pagó 10x en reducción de costos de mantenimiento.
El resultado es un sistema que no solo resuelve los problemas actuales, sino que está preparado para escalar con el negocio durante la próxima década. aunque el camino fue desafiante, las lecciones aprendidas y las mejoras logradas hacen que el esfuerzo haya valido la pena y sobre todo un reto cuando es primera vez que tiene un choque con este tipo de proyectos.
On this page