Bases de datos · Migraciones
Pasar una base de datos Access a SQL sin perder tipos
Exportar las tablas de Access a otro motor parece cuestión de un clic, hasta que las fechas llegan como texto, los importes pierden céntimos y los autonuméricos empiezan de nuevo en 1. Esta guía resume lo que hay que vigilar, con las equivalencias de tipos para los motores más habituales.
La regla de oro: los tipos salen de la definición de la tabla
El error más común es dejar que la herramienta de exportación «adivine» el tipo mirando los datos, o directamente crear todas las columnas como texto. En una migración real que revisé, la herramienta anterior dejaba 92 de 133 columnas como VARCHAR(500): las fechas ya no se podían ordenar, los importes no se podían sumar y las claves dejaban de ser números.
Access guarda en cada tabla el tipo exacto de cada columna (Entero largo, Moneda, Fecha/Hora, Sí/No…), su tamaño y si es autonumérica. Esa definición es la que hay que traducir, columna a columna, al motor de destino.
Cómo leer la estructura de Access
En Windows, con el controlador de Access
Con el controlador ACE de Microsoft y pyodbc puedes listar los tipos de cada columna. El controlador tiene que ser de la misma arquitectura que tu Python (64 bits con 64 bits); si no coinciden, no aparecerá en la lista de controladores.
import pyodbc
cn = pyodbc.connect(r"DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};DBQ=C:\datos\socios.accdb")
cur = cn.cursor()
for col in cur.columns(table="Socios"):
print(col.column_name, col.type_name, col.column_size, col.decimal_digits)
Verás nombres de tipo como COUNTER (autonumérico), VARCHAR, LONGCHAR (texto largo), DATETIME, CURRENCY, BIT, BYTE, SMALLINT, INTEGER, REAL, DOUBLE, DECIMAL, LONGBINARY y GUID.
En Linux o macOS, sin Windows
mdbtools lee ficheros .mdb y .accdb sin controladores:
mdb-tables -1 socios.accdb # una tabla por línea
mdb-schema socios.accdb postgres > esquema.sql # CREATE TABLE para PostgreSQL
mdb-export -T "%Y-%m-%d %H:%M:%S" socios.accdb Socios > socios.csv # en versiones antiguas, -D
El esquema que genera es un buen punto de partida, pero revísalo con la tabla de abajo antes de usarlo. Si prefieres hacerlo todo en Python, la librería access-parser lee la definición de las tablas; para bases grandes o con tablas modificadas muchas veces conviene comprobar sus resultados, porque tiene fallos con algunos tipos y con filas antiguas.
Tabla de equivalencias
Una propuesta probada para los tipos de Access más comunes. Los textos de longitud fija conservan la longitud declarada en Access (por ejemplo, VARCHAR(50)).
| Access | SQLite | MySQL / MariaDB | PostgreSQL | SQL Server | Firebird 3+ |
|---|---|---|---|---|---|
| Sí/No | INTEGER (0/1) | TINYINT(1) | BOOLEAN | BIT | BOOLEAN |
| Byte (0–255) | INTEGER | TINYINT UNSIGNED | SMALLINT | TINYINT | SMALLINT |
| Entero | INTEGER | SMALLINT | SMALLINT | SMALLINT | SMALLINT |
| Entero largo | INTEGER | INT | INTEGER | INT | INTEGER |
| Autonumérico | INTEGER PRIMARY KEY | INT AUTO_INCREMENT | INTEGER GENERATED BY DEFAULT AS IDENTITY | INT IDENTITY(1,1) | INTEGER GENERATED BY DEFAULT AS IDENTITY |
| Simple | REAL | FLOAT | REAL | REAL | FLOAT |
| Doble | REAL | DOUBLE | DOUBLE PRECISION | FLOAT | DOUBLE PRECISION |
| Moneda | NUMERIC (ver trampas) | DECIMAL(19,4) | NUMERIC(19,4) | MONEY o DECIMAL(19,4) | DECIMAL(18,4) |
| Decimal (p,s) | NUMERIC | DECIMAL(p,s) | NUMERIC(p,s) | DECIMAL(p,s) | DECIMAL(p,s), máx. 18 cifras en Firebird 3 |
| Fecha/Hora | TEXT ISO 8601 | DATETIME(3) | TIMESTAMP | DATETIME2 | TIMESTAMP |
| Texto corto (n) | TEXT | VARCHAR(n) utf8mb4 | VARCHAR(n) | NVARCHAR(n) | VARCHAR(n) CHARACTER SET UTF8 |
| Texto largo (Memo) | TEXT | LONGTEXT | TEXT | NVARCHAR(MAX) | BLOB SUB_TYPE TEXT |
| Objeto OLE / binario | BLOB | LONGBLOB | BYTEA | VARBINARY(MAX) | BLOB |
| Id. de réplica (GUID) | TEXT | CHAR(36) | UUID | UNIQUEIDENTIFIER | CHAR(36) |
Dos apuntes: el Byte de Access no tiene signo (0 a 255), así que en MySQL necesita UNSIGNED o un SMALLINT; y el Hipervínculo de Access es en realidad un texto largo con partes separadas por #.
Las trampas que rompen una migración
1. Fechas anteriores a 1900
Access guarda las fechas como un número de días desde el 30 de diciembre de 1899, con la hora en la parte decimal. Antes de esa fecha el número es negativo y aquí está la trampa: la parte decimal es la hora y siempre cuenta hacia delante. El valor −1,25 no es el 28 de diciembre a las 18:00, sino el 29 de diciembre de 1899 a las 6:00. Si conviertes con una suma directa, todas las fechas antiguas con hora se desplazan:
from datetime import datetime, timedelta
ORIGEN = datetime(1899, 12, 30)
def fecha_access(valor: float) -> datetime:
dias = int(valor) # trunca hacia cero: -1.25 -> -1
ms = round(abs(valor - dias) * 86_400_000) # la hora siempre cuenta hacia delante
return ORIGEN + timedelta(days=dias, milliseconds=ms)
Si en la base hay fechas de nacimiento o de alta antiguas, compruébalas una a una después de migrar. Y conserva los milisegundos si existen: en MySQL hace falta DATETIME(3), porque DATETIME los descarta.
2. Moneda: no pases por coma flotante
El tipo Moneda de Access es un entero de 64 bits dividido entre 10 000: es exacto, con cuatro decimales. Si por el camino pasa por float (al dividir o, sobre todo, al sumar), aparecen valores como 0.30000000000000004 y los totales dejan de cuadrar al céntimo. Léelo como entero y divide con Decimal, o usa el valor Decimal que ya devuelve el controlador.
En SQLite no hay un tipo decimal exacto: NUMERIC guarda los importes como número entero o de coma flotante, lo que basta para importes normales (hasta unas 15 cifras significativas). Si necesitas exactitud absoluta, guarda el importe en diezmilésimas como INTEGER. En Firebird, DECIMAL(18,4) se almacena como un entero de 64 bits y cabe todo el rango de Moneda.
3. Simple: el 0,1 que se convierte en 0,100000001
Una columna Simple es un número de coma flotante de 32 bits. Al leerla, Python la amplía a 64 bits y el 0,1 aparece como 0.10000000149011612. Si el destino es REAL de 32 bits, escribe el texto más corto que represente el mismo valor:
import struct
def float32_corto(x: float) -> str:
"""El texto más corto que, leído como float32, da exactamente el mismo valor."""
objetivo = struct.pack("<f", x)
for cifras in range(6, 10):
texto = f"{x:.{cifras}g}"
if struct.pack("<f", float(texto)) == objetivo:
return texto
return repr(x)
4. Autonuméricos que vuelven a empezar en 1
Al copiar las filas hay que insertar los identificadores originales (otras tablas dependen de ellos) y, después, dejar el contador por encima del último valor. Cada motor lo hace a su manera:
- SQLite y MySQL/MariaDB continúan solos a partir del valor más alto insertado.
- SQL Server necesita permiso para insertar el valor:
SET IDENTITY_INSERT dbo.socios ONantes de losINSERTyOFFal terminar. El contador se ajusta solo al valor más alto. - PostgreSQL no ajusta la secuencia: hay que hacerlo al final.
- Firebird tampoco, y además el efecto de
RESTART WITHcambió entre versiones: en Firebird 3 el siguiente valor generado es n + 1 y desde Firebird 4 es n. Compruébalo con unINSERTde prueba.
-- PostgreSQL: deja la secuencia en el último id usado
SELECT setval(pg_get_serial_sequence('socios', 'id'), (SELECT max(id) FROM socios));
-- Firebird 4 o posterior: el siguiente id será 1235
ALTER TABLE SOCIOS ALTER COLUMN ID RESTART WITH 1235;
5. Nombres con espacios, tildes y palabras reservadas
En Access es normal tener columnas como «Fecha de alta», «Año» o «Nº socio». Escribe los nombres siempre entre comillas del motor ("…" en SQLite, PostgreSQL, Firebird y SQL Server; `…` en MySQL) y ten en cuenta que:
- PostgreSQL pasa a minúsculas los nombres sin comillas y Firebird, a mayúsculas: decide un criterio y aplícalo en todo el script.
- Firebird 3 limita los nombres a 31 bytes (una tilde ocupa dos); Firebird 4 admite 63 caracteres y PostgreSQL, 63 bytes.
- Palabras como
Order,User,PositionoDescson reservadas en algunos motores: con comillas no rompen nada, pero conviene listarlas en un informe.
6. Textos: escapes y tamaño de las sentencias
- MySQL interpreta la barra invertida dentro de las cadenas: hay que escaparla (salvo con el modo
NO_BACKSLASH_ESCAPES). Con binarios grandes, puede que tengas que subirmax_allowed_packet. - SQL Server necesita el prefijo
N'…'para no perder caracteres fuera del juego de caracteres de la base. - Firebird no admite varias filas en un mismo
VALUES: una sentencia por fila. En Firebird 3 cada sentencia está limitada a 64 KB, así que los textos largos o los binarios necesitan otro camino; Firebird 4 y 5 admiten sentencias mucho más grandes. - Sí/No: si exportas desde Access a texto, «Sí» puede salir como
-1(en Access, Verdadero vale −1). Conviértelo a 1 oTRUE.
Lo que no viaja solo
Una migración de tablas no trae consigo la lógica de la base. Haz una lista y decide qué recrear:
- Consultas guardadas (en el destino serán vistas o consultas de la aplicación).
- Relaciones e integridad referencial (claves ajenas).
- Índices secundarios, valores por defecto y reglas de validación.
- Campos calculados (llegan con el valor guardado, no con la fórmula) y campos multivalor o de datos adjuntos.
Cómo comprobar que no se ha perdido nada
No des una migración por buena porque «no ha dado error». Lanza la misma consulta en Access y en el destino, tabla por tabla, y compara:
SELECT COUNT(*) AS filas,
SUM(importe) AS total,
MIN(fecha_alta) AS primera,
MAX(fecha_alta) AS ultima
FROM socios;
El número de filas detecta registros perdidos; la suma de importes, céntimos que se han ido por el camino; las fechas mínima y máxima, fechas mal convertidas. Para las tablas importantes, compara también algunos registros concretos campo a campo, sobre todo los más antiguos y los que tienen textos largos o caracteres especiales.
Si la base tiene datos personales
Las bases de Access suelen guardar datos de clientes, socios o pacientes, a veces con cuentas bancarias. Si encargas la migración a alguien, esa persona trata los datos por tu cuenta y necesitáis un contrato de encargo de tratamiento (artículo 28 del RGPD). Y en cualquier caso: trabaja con la menor cantidad de copias posible y bórralas al terminar.
En el conversor de Access que desarrollé, por ejemplo, el programa nunca escribe valores en la consola ni en el informe, y la base subida y todos los resultados se borran automáticamente a las 2 horas, sin pasar por las copias de seguridad.
- CSVun fichero por tabla
- Scripts SQLSQLite, MySQL/MariaDB, PostgreSQL, SQL Server y Firebird
- Base SQLitelista para abrir
- Informetablas, filas, tipos y avisos (sin datos)
¿Te ayudo con la tuya?
Si tienes una base de Access que hay que sacar a otro sistema sin perder nada, cuéntame qué tiene (número de tablas, tamaño y a dónde quieres llevarla) y te digo cómo lo haría y cuánto costaría.