Skynex Pide presupuesto

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)).

AccessSQLiteMySQL / MariaDBPostgreSQLSQL ServerFirebird 3+
Sí/NoINTEGER (0/1)TINYINT(1)BOOLEANBITBOOLEAN
Byte (0–255)INTEGERTINYINT UNSIGNEDSMALLINTTINYINTSMALLINT
EnteroINTEGERSMALLINTSMALLINTSMALLINTSMALLINT
Entero largoINTEGERINTINTEGERINTINTEGER
AutonuméricoINTEGER PRIMARY KEYINT AUTO_INCREMENTINTEGER GENERATED BY DEFAULT AS IDENTITYINT IDENTITY(1,1)INTEGER GENERATED BY DEFAULT AS IDENTITY
SimpleREALFLOATREALREALFLOAT
DobleREALDOUBLEDOUBLE PRECISIONFLOATDOUBLE PRECISION
MonedaNUMERIC (ver trampas)DECIMAL(19,4)NUMERIC(19,4)MONEY o DECIMAL(19,4)DECIMAL(18,4)
Decimal (p,s)NUMERICDECIMAL(p,s)NUMERIC(p,s)DECIMAL(p,s)DECIMAL(p,s), máx. 18 cifras en Firebird 3
Fecha/HoraTEXT ISO 8601DATETIME(3)TIMESTAMPDATETIME2TIMESTAMP
Texto corto (n)TEXTVARCHAR(n) utf8mb4VARCHAR(n)NVARCHAR(n)VARCHAR(n) CHARACTER SET UTF8
Texto largo (Memo)TEXTLONGTEXTTEXTNVARCHAR(MAX)BLOB SUB_TYPE TEXT
Objeto OLE / binarioBLOBLONGBLOBBYTEAVARBINARY(MAX)BLOB
Id. de réplica (GUID)TEXTCHAR(36)UUIDUNIQUEIDENTIFIERCHAR(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 ON antes de los INSERT y OFF al 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 WITH cambió entre versiones: en Firebird 3 el siguiente valor generado es n + 1 y desde Firebird 4 es n. Compruébalo con un INSERT de 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, Position o Desc son 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 subir max_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 o TRUE.

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.

Entrada Base de Access .accdb, .mdb o un .zip que la contenga
Conversor en Python Lee el tipo real de cada columna Sin drivers de Windows, fila a fila y sin cargar la base entera en memoria
  • CSVun fichero por tabla
  • Scripts SQLSQLite, MySQL/MariaDB, PostgreSQL, SQL Server y Firebird
  • Base SQLitelista para abrir
  • Informetablas, filas, tipos y avisos (sin datos)
Cómo funciona el conversor de Access: de la base original a CSV, scripts SQL y SQLite, con un informe que nunca incluye los 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.

¿Tienes una tarea que te roba horas?

Cuéntamela en dos líneas. Te respondo con preguntas concretas y, si encaja, con un presupuesto cerrado por escrito. Sin compromiso.