# Benchmark go-migrate — 2,000,000 filas **Tabla**: `Cartografia.MANZANA` **Fecha**: 2026-05-29 **Entorno**: Docker local (MSSQL 2022 Developer / PostgreSQL 16 + PostGIS) --- ## Resultado final — 5 pasadas cada dirección | Métrica | MSSQL → PostgreSQL | PostgreSQL → MSSQL | |---|---|---| | **Promedio** | **8.37s** | **16.77s** | | **Mediana** | 8.16s | 16.33s | | **Mínimo** | 7.75s | 16.03s | | **Máximo** | 9.17s | 18.46s | | **Desv. estándar** | 0.56s | 1.01s | | **Throughput promedio** | **~238,892 filas/seg** | **~119,261 filas/seg** | | **Factor** | 1x | **~2x más lento** | --- ## Evolución del tuning PG → MSSQL | Etapa | Config | Tiempo | Throughput | Δ | |---|---|---|---|---| | Corrida 1 — original | conservadora | 236.8s | ~8,446 /seg | baseline | | Corrida 2 — igualada | mismos parámetros | 21.94s | ~91,148 /seg | +10.8x | | Tuning A | 4ext/8load 50k | 17.37s | ~115,200 /seg | +1.27x | | Tuning C | 16 loaders | 17.26s | ~115,900 /seg | +1.28x | | **Tuning D — óptimo** | **8ext/8load 50k** | **~16.77s** | **~119,261 /seg** | **+1.37x** | | Tablock + 8 loaders | lock exclusivo serial | ~44s | ~45,000 /seg | ❌ regresión | | Tablock + 1 loader | minimal logging | ~47s | ~42,000 /seg | ❌ regresión | --- ## Configuración óptima — `config-reverse.yaml` ```yaml max_parallel_workers: 4 defaults: batches_per_partition: 4 max_extractors: 8 # ← mayor lever de mejora extractor_batch_size: 25000 extractor_queue_size: 32 max_transformers: 8 transformer_batch_size: 50000 transformer_queue_size: 32 max_loaders: 8 loader_batch_size: 50000 # sweet spot — 75k y 100k peores ``` --- ## Análisis de la brecha final (~2x) La diferencia residual entre ambas direcciones es estructural y está en el protocolo de escritura: | Protocolo | Mecanismo | Overhead | |---|---|---| | `pgx.CopyFrom` (→ PG) | PostgreSQL COPY protocol — streaming binario sin SQL | mínimo | | `mssql.CopyIn` (→ MSSQL) | BCP protocol — row-by-row dentro de un bulk statement | mayor por fila | `mssql.CopyIn` itera fila a fila via `stmt.ExecContext(row...)` antes del flush final, lo que introduce overhead por fila independientemente del batch size. `pgx.CopyFrom` hace streaming puro. --- ## Hallazgos sobre Tablock `Tablock: true` en `mssql.BulkOptions` resultó contraproducente en ambos escenarios: - **Con 8 loaders paralelos**: cada loader compite por un lock exclusivo de tabla → serialización completa (~44s) - **Con 1 loader + batch enorme**: sin contención de locks, pero overhead de log + gestión de la lock exclusiva superó el beneficio de minimal logging (~47s) **Conclusión**: para este patrón de carga (múltiples loaders concurrentes), `Tablock: false` (default) es siempre mejor.