Después de que una inundación catastrófica destruyera nuestro almacén y nos obligara a reconstruir desde cero, decidí hacerlo bien esta vez: migrar de las hojas de cálculo a una base de datos de verdad, construir un ERP en condiciones y no volver a perder nunca el control de nuestros datos. Esta es la historia de esa migración.
Contexto: la inundación que lo cambió todo
En abril de 2024 nos mudamos a una nave industrial en Alfafar, Valencia, para escalar las operaciones. Seis meses después, en octubre de 2024, la DANA —un fenómeno meteorológico catastrófico— azotó Valencia. Nuestro almacén estaba en una de las zonas más afectadas. El agua de la riada destruyó el inventario, los equipos y meses de impulso operativo.
Lo que vino después fueron dos meses de gestión de crisis que no tenían nada que ver con el código. En las semanas posteriores a la inundación, voluntarios y militares vinieron a ayudarnos a recuperar las operaciones del almacén: inventario, equipos, limpieza y logística. Luego llegaron los partes a los seguros, los presupuestos de emergencia sin ingresos y el reseteo mental de ver todo lo que habías construido bajo el agua. Fue el periodo más duro de mi vida.
Pero también fue un reseteo. Cuando reconstruimos —y lo hicimos— tomé una decisión: esta vez lo haríamos bien. Se acabó el apaño. Se acabó el "ya está bien así". Reconstruimos el equipo de 2 a 5 empleados, y me comprometí a construir una infraestructura en condiciones: una base de datos de verdad, un ERP de verdad, documentación de verdad. La era de Google Sheets había terminado.
El punto de partida: por qué Google Sheets tenía que desaparecer
Cuando fundé la empresa, Google Sheets era la elección obvia para gestionar el inventario. Es colaborativa, visual, todo el mundo sabe usarla y es gratis. Mi equipo de almacén podía actualizar las ubicaciones del stock. Yo podía escribir fórmulas. Todos podían verlo todo en tiempo real.
El problema es que Google Sheets nunca se diseñó para ser una base de datos. Y cuando procesas miles de SKU de liquidación de Amazon repartidos entre varias cuentas de marketplace, empieza a romperse de formas difíciles de predecir:
- Límites de la API — La API de Google Sheets tiene una cuota de ~60 peticiones por minuto y usuario. Cuando tienes 5 scripts automatizados golpeando la misma hoja, te la fundes al instante
- Pesadillas de concurrencia — Dos scripts actualizando la misma fila a la vez. Un empleado editando mientras un script escribe. Recálculos de fórmulas durante importaciones de datos. La corrupción de datos era un riesgo constante
- Sin integridad referencial — Una "venta" era una fila en una pestaña. El "artículo" al que hacía referencia era una fila en otra pestaña. ¿La conexión? Una cadena de texto (el código LPN). ¿Un error de tecleo en el LPN? Venta huérfana. Sin ninguna clave foránea que te salvara
- Degradación del rendimiento — Con ~8.000 filas con fórmulas, la hoja tardaba más de 15 segundos en cargar. Mis empleados esperaban. Las operaciones se ralentizaban
- Sin capacidad de consulta — "Muéstrame todos los artículos del camión A2Z33838 que sigan en stock y no se hayan publicado en eBay" requiere una fórmula QUERY de 200 caracteres que se rompe si alguien inserta una columna
Tenía que migrar. Pero no podía parar el negocio para hacerlo.
La restricción que lo cambió todo
Esto no era una migración de proyecto paralelo de fin de semana. Era un negocio en producción con restricciones reales:
- 5 empleados que dependían de Google Sheets cada día: operarios de almacén escaneando artículos, administración procesando pedidos, logística coordinando envíos
- Pedidos entrando a diario a través de Wallapop (nuestro canal principal), cada uno con que procesarse en cuestión de horas
- Datos financieros que tenían que ser exactos al céntimo: registramos los costes por camión de Amazon (códigos A2Z) para la declaración fiscal y los informes de rentabilidad
- Sin un segundo ingeniero — Yo era el único desarrollador, Y a la vez llevaba las operaciones del negocio
No podía darle a un interruptor. Necesitaba una estrategia de migración que me permitiera avanzar de forma incremental, validar en cada paso y dar marcha atrás si algo salía mal.
El pipeline de 5 fases
Diseñé la migración como un pipeline de 5 fases, donde cada fase podía ejecutarse de forma independiente en modo dry-run o execute:
Phase 1: EXTRACT & ANALYZE
└─ Pull all data from 3 Google Sheets tabs → CSVs
└─ Generate analysis report (row counts, data types, anomalies)
Phase 2: VALIDATE
└─ Parse every row against the target PostgreSQL schema
└─ Type conversions (dates, decimals, booleans, enums)
└─ Flag invalid rows with specific error reasons
└─ Generate validation report
Phase 3: MIGRATE
└─ Load validated data into PostgreSQL
└─ Respect foreign key ordering (items → listings → sales)
└─ Dry-run mode: simulate without writing
└─ Execute mode: actual INSERT with transaction safety
Phase 4: SYNC OPERATIONAL DATA
└─ Pull "Prep de Pedidos" (order preparation) tab
└─ Match warehouse status to database records
└─ Update pick/pack states
Phase 5: CONTINUOUS SYNC
└─ Daily: Sheet → DB delta sync
└─ Handles new rows, updates, and edge cases
└─ Runs alongside the database as source of truth transitions
Las partes difíciles de las que nadie habla
1. La calidad de los datos nunca es la que crees
Google Sheets es "flexible", lo que significa que tus datos son un desastre. Esto es lo que encontré cuando intenté parsear más de 8.000 filas de inventario:
- Precios guardados como texto con símbolos de euro:
"12,50 €","12.50","12,5"y""(vacío), todos con significados distintos - Fechas en 4 formatos diferentes según quién las introdujera y en qué configuración regional tuviera el navegador
- Códigos LPN (nuestro identificador principal) con mayúsculas y minúsculas inconsistentes, espacios al final y duplicados ocasionales
- Campos de estado que habían evolucionado de forma orgánica:
"vendido","VENDIDO","Vendido (devuelto)","vendido?"
Dediqué más tiempo a escribir código de limpieza y normalización de datos que a la propia lógica de migración. Cada caso límite que arreglaba revelaba otros dos.
2. Conciliación financiera por camión
Compramos liquidaciones de Amazon por camiones completos. Cada camión tiene un código (A2Z33838, A2Z34102, etc.) y un coste. A efectos fiscales, necesito saber: por cada camión, cuánto pagamos, cuánto vendimos, qué queda en stock y cuál es el margen.
En Google Sheets, esto era una pestaña con fórmulas SUMIFS. En PostgreSQL, necesitaba reconstruirlo como una vista (v_resumen_a2z) y validar que las cifras cuadraban al céntimo.
Truckload | Sheet Total | DB Total | Diff | Status
A2Z33838 | €14,230.50 | €14,230.50| €0.00 | ✓ MATCH
A2Z34102 | €8,445.00 | €8,445.00 | €0.00 | ✓ MATCH
A2Z34567 | €11,200.75 | €11,198.25| €2.50 | ✗ INVESTIGATE
| | | | → 1 sale missing
| | | | → LPN XU-20240312
Generé cientos de estos informes de conciliación. Cada discrepancia había que investigarla. La mayoría se debían a problemas de sincronización (una venta registrada en la hoja pero aún no extraída de la API de Wallapop) o a que mis empleados hacían correcciones manuales directamente en la hoja.
3. El periodo de operación en paralelo
Durante unas 3 semanas, ejecuté ambos sistemas a la vez. Google Sheets seguía siendo la fuente de verdad para mis empleados, mientras la base de datos se iba poblando y validando en segundo plano.
Cada mañana:
- Ejecutar el script de sincronización para traer los cambios de Sheets a PostgreSQL
- Revisar el informe de conciliación en busca de discrepancias
- Investigar y arreglar cualquier incidencia (normalmente de 2 a 5 al día)
- Probar los dashboards de Retool contra datos reales
- Procesar los pedidos del día (las operaciones reales del negocio)
Cada tarde:
- Revisar qué habían cambiado los empleados en Sheets durante el día
- Ejecutar la sincronización de nuevo
- Verificar que los totales financieros seguían cuadrando
Era agotador. Básicamente estaba operando dos sistemas, haciendo mi trabajo como gestor del negocio Y depurando casos límite de la migración. Pero era la única forma de tener confianza antes del corte definitivo.
4. Los scripts de corrección
Ninguna migración es limpia. Escribí más de 15 scripts de corrección especializados para incidencias descubiertas durante o después del periodo en paralelo:
fix_listing_prices_from_sheet.py— Algunos precios de publicación en la BD venían de la API de Wallapop (que muestra el precio actual), pero la hoja tenía el precio de publicación original. Estos había que conciliarlosfix_cash_transaction_duplicates.py— Transacciones de efectivo que se introdujeron dos veces (una por el script de sincronización, otra por la interfaz de Retool tras el corte)fix_incidents_create_returned_sales.py— Devoluciones que existían como "incidencias" en Sheets pero que necesitaban registros de "venta" correspondientes en la base de datosborrar_ventas_no_cuadran_sheet.py— Ventas que existían en la BD pero no tenían correspondencia en la hoja (datos fantasma de errores tempranos en la extracción por API)fix_zero_price_sales.py— Ventas importadas con precio de €0,00 por un error en el extractor que no capturaba el campo de precio para ciertos estados de pedido
Cada script de corrección seguía el mismo patrón: diagnosticar → dry-run → revisar → ejecutar → verificar. Sin arreglos a ciegas.
5. Explicárselo al equipo
La parte más difícil no fue técnica. Fue explicarles a 4 empleados sin perfil técnico por qué estábamos cambiando su forma de trabajar.
"La hoja de cálculo funciona bien" es algo perfectamente razonable que diga un operario de almacén. Y desde su punto de vista, sí que funcionaba bien. Ellos no veían los límites de la API, los riesgos de corrupción de datos ni los scripts cayéndose a las 3 de la mañana por un recálculo de fórmulas.
Tuve que:
- Construir interfaces en Retool que fueran al menos tan fáciles como la hoja de cálculo (de lo contrario la adopción fracasaría)
- Formar a cada persona individualmente en el nuevo flujo de trabajo
- Mantener la hoja disponible como copia de seguridad de solo lectura durante las 2 primeras semanas tras el corte
- Estar atento al feedback del tipo "esto no funciona como antes" (la mayoría del cual era válido)
El corte definitivo
Tras 3 semanas de operación en paralelo, más de 200 informes de conciliación y 15 scripts de corrección, documenté el plan del corte definitivo:
CUTOVER CHECKLIST
─────────────────
[✓] All truckloads reconciled to €0.00 difference
[✓] All active listings matched between Sheet and DB
[✓] All pending orders present in both systems
[✓] Retool modules tested: orders, items, sales, invoicing
[✓] Employees trained on new workflow
[✓] Sheet set to read-only with banner: "USE RETOOL"
[✓] Automated extractors pointed to DB (not Sheet)
[✓] Backup snapshot of all table counts taken
[✓] Rollback procedure documented (Sheet restore)
El corte en sí fue anticlimático, que es exactamente lo que quieres. Cambié los extractores para que escribieran directamente en PostgreSQL, puse Google Sheets en solo lectura y le dije a mi equipo: "a partir de hoy, usad Retool para todo".
Lo que construí (el stack técnico)
La migración no eran solo scripts. Requería construir toda la infraestructura de destino:
- 16 ficheros DDL modulares que definen el esquema de PostgreSQL (tablas, vistas, vistas materializadas, funciones, triggers)
- Más de 35 ficheros de migración incremental para la evolución del esquema tras el corte
- Más de 25 modelos de datos SQLModel en Python para el backend de FastAPI
- Vistas materializadas como
mv_portalhero_feedpara la generación del feed de marketplace - Vistas como
v_resumen_a2zpara los informes financieros por camión - Funciones de trigger para el seguimiento automático de ventas y las actualizaciones de inventario
Resultados
6 meses después de la migración:
- Tiempos de consulta: De más de 15 segundos (carga de la hoja) a <50 ms (consulta a PostgreSQL)
- Concurrencia: 5 scripts automatizados + 5 empleados + dashboards de Retool, todos golpeando la BD a la vez con cero conflictos
- Integridad de datos: Claves foráneas, restricciones y triggers detectan errores que habrían corrompido la hoja en silencio
- Escala operativa: Pasamos de operar solo en Wallapop a Wallapop + eBay + PortalHero, algo que habría sido imposible con Sheets como columna vertebral
- Precisión financiera: La conciliación por camión que antes me llevaba 2 horas con SUMIFS ahora se ejecuta en 3 segundos como una vista materializada
Qué haría diferente
Si volviera a hacerlo:
- Empezar con la base de datos desde el primer día. El impuesto del "ya migraremos más adelante" es real y caro. Incluso un montaje sencillo de PostgreSQL + un frontend de ERP a medida en Retool desde el principio me habría ahorrado cientos de horas
- Automatizar las comprobaciones de conciliación en lugar de ejecutarlas a mano cada mañana. Un pipeline tipo CI que me envíe un email solo cuando se detecten discrepancias
- Construir primero la interfaz de Retool y migrar después. Las construí en paralelo, lo que significó que los empleados vieron interfaces a medio terminar durante el periodo de transición. Si la interfaz hubiera estado pulida antes del corte, la adopción habría sido más fluida
La conclusión
Migrar un sistema en producción no es un problema técnico. Es un problema operativo que requiere soluciones técnicas. El código es quizá el 30 % del trabajo. El otro 70 % es:
- Entender cada caso límite de tus datos existentes
- Generar confianza mediante la operación en paralelo y la conciliación
- Gestionar el lado humano: formación, comunicación, feedback
- Tener la disciplina de no tomar atajos cuando estás agotado y el recuento de discrepancias por fin ha bajado a cero
Hice todo esto como ingeniero en solitario llevando un negocio con 5 empleados. Sin un equipo de DBA. Sin QA dedicado. Solo ingeniería sistemática y mucho café.
El pipeline de migración es de código abierto: github.com/AspiranteD/sheets-to-postgres-migration