SQLite: cambiar tipo de columna sin perder datos
En pocas palabras: No existe forma directa: SQLite carece de ALTER TABLE … ALTER COLUMN para cambiar tipos, así que hay que reconstruir la tabla en cuatro pasos (crear, copiar, borrar, renombrar). Hecho sin precaución, ese rebuild borra filas hijas por ON DELETE CASCADE y elimina triggers e índices sin avisar.
SQLite no tiene un comando para cambiar el tipo de una columna con ALTER TABLE: la única vía es reconstruir la tabla completa en cuatro sentencias SQL (crear, copiar, borrar, renombrar). Un análisis publicado el 1 de octubre de 2026 en dev.to muestra que, hecho sin cuidado, ese rebuild borra filas hijas, triggers e índices sin lanzar ningún error.
El table rebuild es el procedimiento que documenta la propia página de ALTER TABLE de SQLite para modificar el tipo, la nullability o las claves de una columna cuando ALTER TABLE no alcanza: crear una tabla nueva con la definición correcta, copiar las filas, borrar la vieja y renombrar la nueva en su lugar. Lo usan ORMs, scripts de migración manuales y cualquier herramienta que toque el esquema de una base SQLite en producción. Lo que cambia entre “funciona” y “borró datos en silencio” no es el procedimiento en sí, sino el orden exacto de esos pasos.
En este artículo:
- En 30 segundos
- ¿Por qué SQLite no permite cambiar el tipo de una columna con ALTER TABLE?
- ¿Qué borra un rebuild de tabla mal hecho en SQLite?
- ¿Por qué PRAGMA foreign_keys = OFF no funciona si se pone dentro de BEGIN?
- ¿Cómo cambiar el tipo de columna en SQLite sin perder datos?
- Ejemplo hipotético: aplicá el mismo checklist a otro esquema
- Criterios para decidir qué tan cuidadoso ser con un rebuild
- ¿Qué cambios de columna puede hacer SQLite sin reconstruir la tabla desde la versión 3.53.0?
- Checklist antes de reconstruir una tabla en SQLite
- Errores comunes al reconstruir una tabla en SQLite
- Preguntas Frecuentes
- Conclusión
- Fuentes
En 30 segundos
- SQLite no tiene ALTER TABLE … ALTER COLUMN para cambiar tipos: hace falta reconstruir la tabla en 4 pasos (crear, copiar, borrar, renombrar).
- Si ponés PRAGMA foreign_keys = OFF dentro de la transacción, no tiene efecto: el DROP TABLE dispara el ON DELETE CASCADE y borra las filas hijas.
- El rebuild mal hecho también borra triggers e índices, porque DROP TABLE se lleva todo lo que estaba atado a la tabla vieja.
- Un valor como ‘n/a’ se copia como TEXT en una columna REAL sin ningún error, porque la afinidad de tipo en SQLite es una preferencia, no una restricción.
- Desde la versión 3.53.0, lanzada el 9 de abril de 2026, SQLite puede agregar o sacar NOT NULL y CHECK sin reconstruir la tabla.
¿Por qué SQLite no permite cambiar el tipo de una columna con ALTER TABLE?
SQLite no incluye un comando que cambie el tipo de dato de una columna porque su diseño nunca contempló reescribir el storage en el lugar: cualquier cambio que afecte cómo se guardan los valores exige crear una tabla nueva. Según la documentación oficial de ALTER TABLE, el comando soporta apenas cuatro operaciones directas, y cambiar un tipo no es una de ellas.
Ponele que tenés una tabla orders con una columna total declarada como TEXT porque alguien la cargó mal al arrancar el proyecto, y ahora necesitás que sea REAL para sumar y ordenar sin hacer cast en cada query. No hay atajo: ALTER TABLE renombra, agrega y borra columnas, pero el tipo queda fuera de su alcance.
| Cambio | ¿In place? | Desde versión |
|---|---|---|
| Renombrar tabla o columna | Sí | 3.25.0 (columnas) |
| Agregar columna | Sí, con restricciones en defaults y constraints | 3.2.0 |
| Borrar columna | Sí, salvo que esté en PK, unique, index, FK, check, vista o trigger | 3.35.0 |
| Poner o sacar NOT NULL | Sí | 3.53.0 |
| Agregar o sacar CHECK con nombre | Sí | 3.53.0 |
| Cambiar el tipo de una columna | No, rebuild | – |
| Agregar o cambiar FK, PK o unique | No, rebuild | – |

¿Qué borra un rebuild de tabla mal hecho en SQLite?
Un rebuild mal escrito borra tres cosas sin avisar: las filas de las tablas hijas que cuelgan vía ON DELETE CASCADE, todos los triggers e índices atados a la tabla vieja, y los valores que no convierten quedan guardados como texto en la columna numérica nueva, sin ningún error.
El ejemplo de la fuente usa una tabla orders con tres filas, una tabla hija order_items con tres registros, un índice, un trigger orders_touch y una vista big_orders. Corriendo el rebuild de la forma más obvia (BEGIN, PRAGMA foreign_keys = OFF, CREATE TABLE nueva, INSERT, DROP, RENAME, COMMIT), el resultado de SELECT count(*) FROM order_items después es 0. Las tres filas hijas desaparecieron (spoiler: las tres sobreviven si el PRAGMA va antes del BEGIN).
Después del rebuild, sqlite_schema ya no lista ni el índice orders_updated_at_idx ni el trigger orders_touch. El índice faltante se nota en una query que de golpe anda lenta. El trigger faltante se nota en un updated_at que dejó de actualizarse y nadie entiende por qué.
Y después está el caso del valor ‘n/a’. La columna nueva es REAL, pero REAL en SQLite es una afinidad de tipo, no una restricción dura: según la documentación de tipos de SQLite, un texto que no es un número bien formado “se guarda como TEXT”. El copy no falla, 150.0 y 19.9 quedan como números, ‘n/a’ queda como texto, y la vista big_orders sigue mostrando esa fila como un pedido grande porque SQLite ordena cualquier texto por encima de cualquier número. Una tabla declarada STRICT, en cambio, hubiera rechazado el valor en vez de guardarlo como texto sin aviso.
¿Por qué PRAGMA foreign_keys = OFF no funciona si se pone dentro de BEGIN?
Porque SQLite no permite cambiar el enforcement de foreign keys en medio de una transacción multi-sentencia. Según la documentación oficial de foreign keys, intentarlo “no devuelve error, simplemente no tiene efecto”. Si ejecutás el PRAGMA después de BEGIN, la base sigue chequeando las claves foráneas igual que antes.
¿Y cómo se nota eso en la práctica? Corriendo PRAGMA foreign_keys; adentro de la misma transacción, que sigue devolviendo 1 aunque un renglón arriba hayas pedido apagarlo. Sin el cascade activo, el mismo DROP TABLE falla con FOREIGN KEY constraint failed en vez de borrar nada en silencio, que es en realidad la forma más segura de enterarte del problema antes de perder datos.
La solución es simple: el PRAGMA tiene que correr antes de BEGIN, no después. Es una línea que cambia de orden y listo, pero si no la sabés, el error no te avisa. No hay excepción, no hay warning, el rebuild simplemente hace lo que no querías.
¿Cómo cambiar el tipo de columna en SQLite sin perder datos?
Siguiendo el procedimiento de 12 pasos que documenta ALTER TABLE: apagar foreign keys antes de empezar la transacción, guardar la definición de triggers e índices, crear la tabla nueva, copiar los datos, verificar que convirtieron bien, recién ahí borrar la vieja, renombrar, recrear lo que se perdió y chequear las foreign keys antes del commit. Son muchos pasos, sí, pero cada uno existe porque alguien se comió un problema real antes.
Aplicado al ejemplo de orders, el orden correcto queda así:
- PRAGMA foreign_keys = OFF antes de BEGIN. Así el DROP TABLE no dispara ningún cascade sobre order_items.
- Guardar triggers e índices. Un SELECT type, name, sql FROM sqlite_schema WHERE tbl_name = ‘orders’ AND type <> ‘table’ te da el texto exacto para recrearlos después.
- Crear la tabla nueva con el tipo correcto y copiar las filas con INSERT INTO … SELECT.
- Chequear con typeof() antes de dropear nada. Un SELECT id, total FROM orders__new WHERE typeof(total) <> ‘real’ te muestra qué filas no convirtieron (en el ejemplo, la fila con ‘n/a’). Si devuelve algo, hacés ROLLBACK y arreglás esos datos antes de seguir.
- Recién ahí DROP TABLE y RENAME. Y después recrear el índice y el trigger con las sentencias guardadas en el paso dos.
- PRAGMA foreign_key_check antes del COMMIT. Lista cualquier fila que rompa una clave foránea mientras el enforcement todavía está apagado.
Un detalle que se pasa por alto: si pegás todo ese script en un solo bloque y lo corrés de una, nunca te vas a detener en el chequeo de typeof(). Hay que correrlo paso a paso, no como un script monolítico.
Ejemplo hipotético: aplicá el mismo checklist a otro esquema
Ejemplo hipotético (no corrido, solo para razonar el caso): imaginá un sistema de turnos médicos con una tabla pacientes donde la columna telefono quedó declarada TEXT porque en altas viejas se guardaron números con guiones y prefijos tipo “+54 11…”. El equipo quiere normalizarla a INTEGER para validar el formato desde la aplicación. Sobre esa tabla cuelga turnos, con FOREIGN KEY (paciente_id) REFERENCES pacientes(id) ON DELETE CASCADE, hay un índice sobre telefono para búsquedas rápidas y un trigger que actualiza ultima_modificacion cada vez que se edita un paciente.
El mismo razonamiento que vale para orders se traslada sin cambios: si alguien corre el rebuild con el PRAGMA foreign_keys = OFF adentro del BEGIN, el DROP TABLE de pacientes cascadea sobre turnos y vacía los turnos agendados de esos pacientes. El índice sobre telefono desaparece y las búsquedas por teléfono pasan a recorrer la tabla entera sin que nadie note por qué el sistema se puso lento. Y cualquier paciente con un teléfono cargado como “sin datos”, con espacios o con un prefijo mal tipeado queda silenciosamente como TEXT en la columna nueva, sin error ni aviso, listo para romper el primer filtro numérico que alguien escriba sobre esa columna.
El checklist no cambia porque cambie el dominio del problema: PRAGMA antes del BEGIN, guardar índice y trigger desde sqlite_schema, correr el typeof() antes del DROP, foreign_key_check antes del COMMIT. Lo único que varía entre un sistema de pedidos y uno de turnos médicos es qué tan caro sale no haberlo hecho así.
Criterios para decidir qué tan cuidadoso ser con un rebuild
No todos los rebuilds pesan lo mismo. Antes de decidir si alcanza con el procedimiento básico o conviene extremar precauciones, sirve mirar estas señales:
| Situación | Qué priorizar |
|---|---|
| La tabla no tiene triggers, índices propios ni FKs que apunten a ella | El rebuild de 4 pasos alcanza; el riesgo de pérdida silenciosa es bajo |
| Otras tablas tienen ON DELETE CASCADE hacia esta tabla | PRAGMA foreign_keys = OFF antes de BEGIN es obligatorio, no opcional |
| La columna acumula datos históricos cargados a mano o por sistemas viejos | Correr el chequeo typeof() antes del DROP: ahí aparecen los valores que no convirtieron |
| Solo necesitás agregar o sacar NOT NULL o CHECK | Confirmar la versión real de SQLite con sqlite3 –version: desde 3.53.0 puede no hacer falta rebuild |
| La tabla es grande o está en producción | Probar el rebuild completo contra una copia del archivo .db antes de tocar la base real |
¿Qué cambios de columna puede hacer SQLite sin reconstruir la tabla desde la versión 3.53.0?
Desde SQLite 3.53.0, lanzada el 9 de abril de 2026 según la documentación oficial de ALTER TABLE, se puede poner o sacar NOT NULL y agregar o quitar un CHECK con nombre usando ALTER TABLE … ALTER COLUMN y ALTER TABLE … ADD CONSTRAINT, sin pasar por el rebuild completo. Las dos operaciones chequean las filas existentes antes de aplicarse.
El dato concreto: SET NOT NULL sobre una columna que tiene algún NULL falla con constraint failed, y agregar CHECK (total > 0) a una tabla con algún valor negativo falla de la misma forma. No es magia, simplemente valida antes de comprometerse. Eso sí: cualquier versión anterior a 3.53.0, incluida la que trae empaquetada el driver de tu proyecto (Python, Node, lo que sea), sigue necesitando el rebuild completo para esos dos casos.
Acá entra un dato raro que vale la pena marcar. El análisis de dev.to corrió todas las pruebas con el shell sqlite3 que viene con macOS, el build propio de Apple, y ese binario reporta versión 3.54.0 aunque la última release publicada en sqlite.org es 3.53.4. ¿Afecta esto a quien desarrolla? Puede, si asumís funcionalidad disponible según el número de versión sin confirmarlo con sqlite3 --version en el entorno real donde corre tu app.
Checklist antes de reconstruir una tabla en SQLite
Antes de tocar una tabla con datos que te importan, conviene repasar esto:
- Tu versión de SQLite. En 3.53.0 o más nueva, NOT NULL y CHECK no necesitan rebuild.
- PRAGMA foreign_keys = OFF antes de BEGIN, nunca después.
- Triggers e índices de la tabla, guardados desde sqlite_schema antes del DROP.
- Un chequeo typeof() sobre la tabla nueva antes de borrar la vieja.
- PRAGMA foreign_key_check antes del COMMIT, no después.
Errores comunes al reconstruir una tabla en SQLite
El primero, el más común: poner PRAGMA foreign_keys = OFF dentro de BEGIN. Parece prolijo porque queda todo junto en la transacción, pero no tiene ningún efecto y el DROP TABLE termina cascadeando sobre las tablas hijas.
El segundo: dropear la tabla sin guardar antes la definición de sus triggers e índices. DROP TABLE se los lleva puestos, y como no tira ningún warning, el problema aparece recién cuando alguien nota que una query quedó lenta o que un campo dejó de actualizarse solo.
El tercero: confiar en que declarar una columna REAL rechaza automáticamente valores que no son números. No lo hace. La afinidad de tipo en SQLite es una preferencia, no una validación, y un texto como ‘n/a’ se copia tal cual sin frenar nada. Si necesitás esa garantía, la única forma es declarar la tabla como STRICT.
El cuarto, más de proceso que de sintaxis: correr el script completo de una sola pasada en vez de ejecutarlo paso a paso. El chequeo con typeof() existe justamente para frenar antes del DROP, pero si pegás todo el bloque junto, nunca llegás a leer ese resultado a tiempo.
Preguntas Frecuentes
¿Cómo se cambia el tipo de una columna en SQLite?
Creando una tabla nueva con el tipo correcto, copiando las filas de la vieja, borrando la original y renombrando la nueva en su lugar: son los cuatro pasos mínimos del table rebuild que documenta la página oficial de ALTER TABLE. No existe un comando que lo haga en una sola sentencia.
¿Por qué SQLite no permite ALTER COLUMN para cambiar el tipo de dato?
Porque SQLite guarda el esquema como texto plano en sqlite_schema y solo soporta reescribirlo para renombrar tabla o columna, agregar columna, borrar columna y, desde la versión 3.53.0 del 9 de abril de 2026, poner o sacar NOT NULL y CHECK. Cambiar un tipo de dato, una primary key o una foreign key sigue sin tener soporte directo y exige el rebuild completo.
¿Qué pasa con las foreign keys al reconstruir una tabla en SQLite?
Si PRAGMA foreign_keys = OFF se ejecuta dentro de la transacción, no tiene ningún efecto, y el DROP TABLE dispara las acciones ON DELETE CASCADE sobre las tablas hijas, borrando sus filas. El PRAGMA tiene que correr antes del BEGIN para que el borrado de la tabla vieja no arrastre nada.
¿Cómo evitar que se borren los triggers e índices al modificar una tabla en SQLite?
Guardando su definición con una consulta tipo SELECT type, name, sql FROM sqlite_schema WHERE tbl_name = ‘orders’ antes del DROP TABLE, y recreándolos con CREATE INDEX y CREATE TRIGGER después de renombrar la tabla nueva. DROP TABLE borra cualquier índice o trigger atado a esa tabla y nada los repone automáticamente.
¿Qué hace PRAGMA foreign_keys = OFF dentro de una transacción en SQLite?
Nada: la documentación oficial de SQLite dice textualmente que cambiar el enforcement de foreign keys en medio de una transacción multi-sentencia “no devuelve error, simplemente no tiene efecto”. Por eso ese PRAGMA tiene que ejecutarse antes de BEGIN, no después.
Conclusión
El table rebuild en SQLite no es complicado, pero es fácil pifiarla si lo escribís de memoria copiando el PRAGMA foreign_keys = OFF después del BEGIN, como aparece en un montón de ejemplos sueltos dando vueltas. Como criterio práctico, antes de aplicar cualquier rebuild en una base que te importa, probalo primero contra una copia del archivo .db (alcanza con duplicar el archivo) y corré ahí el PRAGMA foreign_key_check y el chequeo typeof() antes de tocar la base real.
Si tu SQLite ya es 3.53.0 o más nuevo, fijate primero si el cambio que necesitás (NOT NULL o CHECK) se puede hacer sin rebuild, porque te ahorrás todo el lío de triggers e índices. Y confirmá la versión real con sqlite3 --version antes de asumir nada: el ejemplo del build de Apple en macOS, que reporta 3.54.0 contra el 3.53.4 oficial de sqlite.org, deja claro que no todas las instalaciones responden igual al mismo número.






