Decodificación Base64 en SQL: una guía completa
Abre cualquier base de datos de producción y quédate mirándola suficiente tiempo, y te encontrarás con el disfraz. Un avatar que llegó como un muro de letras dentro de una exportación JSON. Un JWT aparcado en una columna varchar junto a un ID de usuario. Un certificado que alguien decidió enviar como cadena porque el formato de transferencia no tenía binario. En algún punto de una tabla, tus datos llevan letras puestas, y tu trabajo es quitárselas sin salir de la base de datos.
En SQL, ese trabajo tiene una propiedad muy reconfortante: en cuanto sabes qué decodificador habla tu dialecto, todo el trabajo se reduce a una única llamada a una función. El formato en sí ya se explica en detalle en la página de inicio (64 caracteres imprimibles, cada grupo de cuatro representando tres bytes de entrada, hasta dos signos = de padding en el grupo final), así que este artículo se salta la clase. Dos cosas para llevarte contigo: base64 es una forma de vestir bytes como texto, no una cerradura, y decodificar es la dirección en la que los datos se hacen más pequeños (vuelven a tres cuartas partes de su tamaño codificado), que es justo lo contrario de la dirección para la que se dimensionó tu columna de almacenamiento. La historia de verdad es que SQL es una familia de dialectos, y cada miembro llama a su decodificador por un nombre distinto y reacciona a una entrada mala con un temperamento completamente diferente. Este artículo es el recorrido.
El plantel de decodificadores
Estos son los que están de guardia, y cómo se comporta cada uno cuando la entrada es basura. La columna "cuando se rompe" importa, porque un decodificador que falla a gritos en staging y en silencio en producción es el camino por el que los avatares faltantes llegan al mundo real:
| Dialecto | La llamada | Lo que devuelve | Cuando se rompe | Desde cuándo |
|---|---|---|---|---|
| MySQL 8.x / MariaDB 10.x | FROM_BASE64(str) |
cadena binaria | NULL silenciosa |
MySQL 5.6 (2013) |
| PostgreSQL | decode(str, 'base64') |
bytea |
un ERROR ruidoso con pista |
7.2 (2002) |
| SQLite (CLI 3.41+) | base64(str) |
BLOB |
omite lo que no puede leer | 3.41.0 (2023) |
| DuckDB | from_base64(str) |
BLOB |
error de conversión | versiones modernas |
| ClickHouse 18.16+ | base64Decode(str) |
String |
excepción (INCORRECT_DATA) |
18.16.0 (2018) |
| SQL Server 2025+ | BASE64_DECODE(str) |
varbinary |
Msg 9803, tres estados | 2025 |
| Oracle | UTL_ENCODE.BASE64_DECODE(raw) |
RAW |
excepción de PL/SQL | era 9i |
| Snowflake | BASE64_DECODE_BINARY(str) |
BINARY |
error, o NULL con la variante TRY_ |
versiones actuales |
Fíjate en la forma de la tabla: el nombre de la función nunca es la parte difícil. La parte difícil es la columna "cuando se rompe", porque esa columna decide si tu informe pierde filas en silencio o si tu tarea por lotes se detiene y pide ayuda.
MySQL y MariaDB: el decodificador que se encoge de hombros
Los dos servidores comparten el par TO_BASE64() / FROM_BASE64(). El decodificador toma una cadena y devuelve una cadena binaria: una secuencia de bytes sin ningún conjunto de caracteres adherido. Una NULL de entrada da una NULL de salida, y aquí va lo primero que hay que memorizar: cualquier otra cosa que no sea base64 válido también es una NULL, sin ningún aviso. El decodificador se encoge de hombros, y tu consulta sigue avanzando feliz.
SELECT FROM_BASE64('aGVsbG8=') AS restored;
SELECT HEX(FROM_BASE64('aGVsbG8=')) AS as_hex;
SELECT CONVERT(FROM_BASE64('aGVsbG8gd29ybGQ=') USING utf8mb4) AS as_text;
La línea del medio merece un comentario, porque explica un clásico momento de confusión. El cliente de línea de comandos mysql imprime las cadenas binarias en notación hexadecimal por defecto (un ajuste llamado binary-as-hex), así que un SELECT FROM_BASE64('aGVsbG8=') plano muestra 0x68656C6C6F en vez de hello. No es un bug ni una corrupción; es el cliente siendo cauteloso con los datos binarios. Si quieres letras, convierte con CONVERT(... USING utf8mb4) o arranca el cliente con --binary-as-hex=0; la llamada HEX() de la línea del medio es la versión deliberada del hex que el cliente te muestra por defecto.
Ahora las reglas que aplica el decodificador silencioso. Después de ignorar los espacios en blanco, los caracteres restantes deben formar un múltiplo de cuatro, cada carácter debe provenir del alfabeto estándar (letras, dígitos, +, / y =), y el padding solo puede aparecer al final, y nada más:
SELECT FROM_BASE64('aGVsbG8gd29ybGQ=') AS ok;
SELECT FROM_BASE64('aGVsbG8gd29ybGQ') AS missing_padding;
SELECT FROM_BASE64('!!!') AS nonsense;
Las tres líneas se ejecutan sin quejas, y las líneas dos y tres devuelven NULL. Padding que falta, longitud incorrecta, caracteres extraterrestres: el mismo encogimiento de hombros. Los espacios en blanco son la única indulgencia; saltos de línea, retornos de carro, tabuladores y espacios se ignoran todos, lo cual es una misericordia para todo lo que primero pasó por un correo. El alfabeto URL-safe, en cambio, recibe el encogimiento de hombros a cambio: un guion bajo no está en la tabla estándar, así que FROM_BASE64('yv7K_g==') es una NULL aunque la longitud sea un múltiplo de cuatro sin resto. Tienes que traducir el alfabeto tú mismo antes de llamar, y la sección de URL-safe de abajo muestra cómo.
Un rasgo más que conviene conocer: el decodificador y el codificador son un par a juego. El codificador parte su salida en líneas de 76 caracteres, y el decodificador se come esos saltos de línea para el desayuno. Si una columna fue rellenada por TO_BASE64() en esta misma familia de bases de datos, decodificar es un viaje de ida y vuelta perfecto. Si fue rellenada por otra cosa, sigue leyendo.
PostgreSQL: el decodificador que alza la voz
PostgreSQL lleva base64 en su núcleo desde al menos la versión 7.2, allá por 2002, lo que lo convierte en el mecanismo base64 más antiguo de esta familia por un margen corto. La llamada es decode(string, 'base64'), y el resultado es bytea, el tipo binario nativo de la base de datos. El compañero encode(bytea, 'base64') va en la otra dirección y se menciona aquí solo porque los dos comparten un mismo contrato de formato: el estilo RFC 2045, con líneas rotas a los 76 caracteres. El decodificador, por su parte, ignora retornos de carro, saltos de línea, espacios y tabuladores donde sea en la entrada.
SELECT decode('aGVsbG8gd29ybGQ=', 'base64') AS bytes;
SELECT length(decode('aGVsbG8gd29ybGQ=', 'base64')) AS byte_count;
SELECT convert_from(decode('aMOpbGxv', 'base64'), 'UTF8') AS text;
La tercera línea es la que vas a usar constantemente: convert_from() convierte el bytea en texto con una codificación nombrada, y es el paso del conjunto de caracteres que los datos binarios necesitan (más sobre eso en su propia sección más adelante). aMOpbGxv vuelve como héllo, con su carácter acentuado y todo.
Donde PostgreSQL se separa del resto es en la columna "cuando se rompe". Una entrada inválida es un error duro, y el mensaje de error te dice exactamente qué regla se rompió:
- un carácter fuera del alfabeto:
ERROR: invalid symbol "!" found while decoding base64 sequence - un signo de padding en medio de la cadena:
ERROR: unexpected "=" while decoding base64 sequence - entrada truncada o padding que falta:
ERROR: invalid base64 end sequence, con la pista Input data is missing padding, is truncated, or is otherwise corrupted. - un guion bajo URL-safe:
ERROR: invalid symbol "_" found while decoding base64 sequence
Para un trabajo de limpieza de datos, esa voz es una ventaja. La consulta falla, ves la fila, arreglas la fuente. El contrapunto es que una fila envenenada entre un millón detiene todo el lote, así que en los pipelines de producción la gente suele prefiltrar con una regex antes de llamar a decode(). Y una pequeña nota de visualización: psql imprime el bytea como hex con prefijo \x, así que \x68656c6c6f es el mismo "hello" que el cliente de MySQL muestra como 0x68656C6C6F. Dos dialectos, dos dialectos hex.
SQL Server: el que llega tarde
Aquí está la sorpresa de toda la familia. SQL Server lanzó BASE64_DECODE() en la versión 2025, disponible generalmente en noviembre de 2025. Antes de eso, la base de datos más popular en el mundo empresarial no tenía ningún decodificador base64 integrado durante treinta y seis años, y el folklore estaba espeso de soluciones alternativas. La función moderna es limpia: toma una expresión varchar(n) o varchar(max) y devuelve un varbinary (una expresión varchar(n) se mapea a varbinary(8000), y una expresión varchar(max) se mapea a varbinary(max)), con NULL pasando directo.
SELECT BASE64_DECODE('aGVsbG8gd29ybGQ=') AS bytes;
SELECT CONVERT(VARCHAR(100), BASE64_DECODE('aGVsbG8gd29ybGQ=')) AS text;
SELECT BASE64_DECODE('yv7K_g') AS url_safe_also_works;
Esa tercera línea es un toque genuinamente amable: el decodificador acepta ambos alfabetos del RFC 4648, el estándar con + y / y el URL-safe con - y _, y el padding es opcional. También ignora los cuatro caracteres de espacio en blanco (salto de línea, retorno de carro, tabulador, espacio). Cuando sí se rompe, el error es Msg 9803, Level 16 con el texto Invalid data for type "Base64Decode", y el valor de State te dice qué regla tocaste: estado 20 para un carácter que no está en ninguno de los dos alfabetos, estado 21 para caracteres que son todos válidos pero están dispuestos en una forma que base64 no puede producir, y estado 23 para un padding que aparece demasiado a menudo o demasiado pronto.
Si estás atascado en una versión anterior a 2025, la solución alternativa clásica presta el tipo XML, que entiende base64 desde los días de XML Schema:
SELECT CAST(N'' AS XML)
.value('xs:base64Binary("aGVsbG8=")', 'VARBINARY(MAX)') AS legacy;
El motor XML decodifica en base64 la constante y devuelve los bytes. Funciona, y fue lo que usó una generación de desarrolladores de SQL Server. También tiene bordes: el tipo base64Binary es estricto con la forma, así que una cadena envuelta en MIME con saltos de línea dentro no se parseará, y estás pagando el precio de la maquinaria XML por un trabajo que una sola función ahora hace de forma nativa. Trátalo como la pieza de museo en la que se ha convertido.
SQLite: el dialecto sin decodificador
SQLite es el bicho raro, y entender por qué te dice cómo usarlo. La biblioteca central es un motor pequeño y embebible, y base64 no está en su lista de funciones estándar. Si una columna guarda base64, el decodificador tiene que venir de uno de cuatro sitios: la shell de línea de comandos, una extensión cargable, una función personalizada registrada por la aplicación anfitriona, o SQL puro. Estos son, uno por uno.
La CLI. A partir de la versión 3.41.0 (febrero de 2023), la shell de línea de comandos sqlite3 incluye una función base64(). Decodifica un argumento de texto en un BLOB, lo cual la hace perfecta para trabajo exploratorio directo desde una terminal:
$ sqlite3 app.db "SELECT hex(base64('aGVsbG8gd29ybGQ='));"
68656C6C6F20776F726C64
Dos temperamentos que hay que conocer. Primero, es indulgente: los caracteres que no reconoce se omiten en vez de reportarse, así que base64('!!!') devuelve un BLOB vacío en lugar de un error. Genial para la curiosidad, peligroso para la auditoría, porque "vacío" y "faltante" se ven igual en la salida. Segundo, la función cambia de forma: un argumento BLOB se codifica a texto (con líneas de 72 caracteres), mientras que un argumento de texto se decodifica a un BLOB. El mismo nombre, dos trabajos, elegidos por el tipo del argumento. Ningún otro decodificador de esta familia hace eso, así que lee el tipo de tu entrada dos veces.
SQL puro. La biblioteca central no tiene base64, pero tiene CTE recursivas, aritmética y (desde 3.41.0) unhex(), que es suficiente para construir un decodificador de verdad en un par de decenas de líneas. La receta: una tabla de alfabeto de 64 filas, la entrada cortada en trozos de cuatro caracteres, cada trozo convertido en un número de 24 bits, ese número partido en tres bytes, y los bytes recogidos como hex antes de que unhex() los convierta en un BLOB. Aquí lo tienes, funcionando sobre una columna de tabla:
WITH RECURSIVE
b64(c, v) AS (
SELECT 'A', 0 UNION ALL SELECT 'B', 1 UNION ALL SELECT 'C', 2
UNION ALL SELECT 'D', 3 UNION ALL SELECT 'E', 4 UNION ALL SELECT 'F', 5
UNION ALL SELECT 'G', 6 UNION ALL SELECT 'H', 7 UNION ALL SELECT 'I', 8
UNION ALL SELECT 'J', 9 UNION ALL SELECT 'K', 10 UNION ALL SELECT 'L', 11
UNION ALL SELECT 'M', 12 UNION ALL SELECT 'N', 13 UNION ALL SELECT 'O', 14
UNION ALL SELECT 'P', 15 UNION ALL SELECT 'Q', 16 UNION ALL SELECT 'R', 17
UNION ALL SELECT 'S', 18 UNION ALL SELECT 'T', 19 UNION ALL SELECT 'U', 20
UNION ALL SELECT 'V', 21 UNION ALL SELECT 'W', 22 UNION ALL SELECT 'X', 23
UNION ALL SELECT 'Y', 24 UNION ALL SELECT 'Z', 25 UNION ALL SELECT 'a', 26
UNION ALL SELECT 'b', 27 UNION ALL SELECT 'c', 28 UNION ALL SELECT 'd', 29
UNION ALL SELECT 'e', 30 UNION ALL SELECT 'f', 31 UNION ALL SELECT 'g', 32
UNION ALL SELECT 'h', 33 UNION ALL SELECT 'i', 34 UNION ALL SELECT 'j', 35
UNION ALL SELECT 'k', 36 UNION ALL SELECT 'l', 37 UNION ALL SELECT 'm', 38
UNION ALL SELECT 'n', 39 UNION ALL SELECT 'o', 40 UNION ALL SELECT 'p', 41
UNION ALL SELECT 'q', 42 UNION ALL SELECT 'r', 43 UNION ALL SELECT 's', 44
UNION ALL SELECT 't', 45 UNION ALL SELECT 'u', 46 UNION ALL SELECT 'v', 47
UNION ALL SELECT 'w', 48 UNION ALL SELECT 'x', 49 UNION ALL SELECT 'y', 50
UNION ALL SELECT 'z', 51 UNION ALL SELECT '0', 52 UNION ALL SELECT '1', 53
UNION ALL SELECT '2', 54 UNION ALL SELECT '3', 55 UNION ALL SELECT '4', 56
UNION ALL SELECT '5', 57 UNION ALL SELECT '6', 58 UNION ALL SELECT '7', 59
UNION ALL SELECT '8', 60 UNION ALL SELECT '9', 61 UNION ALL SELECT '+', 62
UNION ALL SELECT '/', 63
),
chunks AS (
SELECT name, b64, (LENGTH(b64) + 3) / 4 AS n
FROM payload
),
seq(name, n, i) AS (
SELECT name, n, 1 FROM chunks
UNION ALL
SELECT name, n, i + 1 FROM seq WHERE i < n
),
vals AS (
SELECT s.name, s.i AS chunk_no, s.n,
COALESCE((SELECT v FROM b64 WHERE c = substr(ch.b64, (s.i - 1) * 4 + 1, 1)), -1) AS v1,
COALESCE((SELECT v FROM b64 WHERE c = substr(ch.b64, (s.i - 1) * 4 + 2, 1)), -1) AS v2,
COALESCE((SELECT v FROM b64 WHERE c = substr(ch.b64, (s.i - 1) * 4 + 3, 1)), -1) AS v3,
COALESCE((SELECT v FROM b64 WHERE c = substr(ch.b64, (s.i - 1) * 4 + 4, 1)), -1) AS v4
FROM seq s
JOIN chunks ch ON ch.name = s.name
),
hexes AS (
SELECT name, chunk_no, n,
(CASE WHEN v1 < 0 THEN 0 ELSE v1 END) * 262144 +
(CASE WHEN v2 < 0 THEN 0 ELSE v2 END) * 4096 +
(CASE WHEN v3 < 0 THEN 0 ELSE v3 END) * 64 +
(CASE WHEN v4 < 0 THEN 0 ELSE v4 END) AS v24,
CASE WHEN v2 >= 0 OR v3 >= 0 THEN 1 ELSE 0 END +
CASE WHEN v3 >= 0 OR v4 >= 0 THEN 1 ELSE 0 END +
CASE WHEN v4 >= 0 THEN 1 ELSE 0 END AS n_bytes
FROM vals
),
acc(name, n, i, hx) AS (
SELECT h.name, h.n, 1,
(CASE WHEN h.n_bytes >= 1 THEN printf('%02X', h.v24 / 65536) ELSE '' END) ||
(CASE WHEN h.n_bytes >= 2 THEN printf('%02X', (h.v24 / 256) % 256) ELSE '' END) ||
(CASE WHEN h.n_bytes >= 3 THEN printf('%02X', h.v24 % 256) ELSE '' END)
FROM hexes h
WHERE h.chunk_no = 1
UNION ALL
SELECT a.name, a.n, a.i + 1,
a.hx || (
SELECT (CASE WHEN h.n_bytes >= 1 THEN printf('%02X', h.v24 / 65536) ELSE '' END) ||
(CASE WHEN h.n_bytes >= 2 THEN printf('%02X', (h.v24 / 256) % 256) ELSE '' END) ||
(CASE WHEN h.n_bytes >= 3 THEN printf('%02X', h.v24 % 256) ELSE '' END)
FROM hexes h
WHERE h.name = a.name AND h.chunk_no = a.i + 1
)
FROM acc a
WHERE a.i < a.n
)
SELECT name, unhex(hx) AS restored
FROM acc
WHERE i = n;
Ejecútalo contra una tabla con una columna b64 y obtienes un BLOB por fila, sin extensiones, sin código de aplicación. La aritmética es base64 plano con ropa de números enteros: cada uno de los cuatro caracteres aporta seis bits, los dos caracteres del medio cruzan un límite de byte, y los dos bits bajos del último carácter se descartan. Es la opción más lenta de esta página (un paso recursivo más una búsqueda por trozo), así que resérvala para payloads pequeños y arqueología puntual. Para una aplicación de larga duración, la respuesta honesta es la tercera opción: registra una función personalizada de una línea desde el lenguaje anfitrión (el módulo sqlite3 de Python lo hace en dos líneas con create_function() y el módulo estándar base64) y deja que el motor la llame como si fuera nativa. La cuarta opción, extensiones cargables como la familia sqlean, también existe, pero significa instalar una build diferente del motor, que es lo que la mayoría de los equipos prefiere evitar.
DuckDB: estricto, pequeño y con opiniones
DuckDB es una base de datos analítica con un tipo binario de verdad, BLOB, y una familia ordenada de funciones de blob a su alrededor. El decodificador es from_base64(string), y está sentado junto a sus amigos to_base64(), hex(), md5() y sha256() en la misma página de referencia, que es donde la mayoría de los usuarios de DuckDB se topan con él por primera vez.
SELECT from_base64('aGVsbG8gd29ybGQ=') AS bytes;
SELECT decode(from_base64('aMOpbGxv')) AS text;
SELECT hex(from_base64('AAEC')) AS padding_optional;
La tercera línea muestra una regla más amable de lo que esperarías: cuando la longitud es un múltiplo de cuatro, el padding que falta no es problema, AAEC se decodifica sin problema en los bytes 00 01 02. La estrictez aparece en el momento en que la forma es incorrecta. DuckDB quiere una longitud que sea un múltiplo de cuatro, y punto, y el error de conversión dice exactamente eso:
SELECT from_base64('YWJ');
-- Conversion Error: Could not decode string "YWJ" as base64: length must be a multiple of 4
Dos opiniones más que respetar. Primero, el decodificador de DuckDB solo habla el alfabeto estándar; un guion bajo no es un carácter que reconozca, así que los tokens URL-safe deben traducirse antes de llegar (la receta está en la sección de URL-safe). Segundo, no tiene ninguna tolerancia con los espacios en blanco. Un adjunto de correo envuelto en MIME con sus saltos de línea de 76 caracteres dentro fallará, y la solución es un replace() sobre saltos de línea y retornos de carro antes de la llamada. Y como no hay una variante try_ que amortigüe el golpe, el patrón amable es una comprobación previa en la misma consulta:
SELECT CASE
WHEN b64 ~ '^[A-Za-z0-9+/]*={0,2}$'
AND MOD(LENGTH(b64), 4) = 0
THEN from_base64(b64)
END AS maybe_bytes
FROM attachments;
Regex primero, decodificador segundo: la consulta devuelve NULL para cualquier cosa que no se pueda decodificar, y el decodificador solo ve entrada bien formada.
ClickHouse: el decodificador de columnas
ClickHouse no tiene un tipo binario por separado; su String es feliz siendo seguro para binarios, lo que significa que decodificar "a una cadena" es todo el trabajo y no sigue ningún paso de conversión. La función anda por aquí desde la versión 18.16.0 (2018) con el nombre base64Decode(), y mantiene un alias al estilo MySQL, FROM_BASE64(), de modo que las consultas portadas no necesitan reescribirse.
SELECT base64Decode('aGVsbG8gd29ybGQ=') AS text;
SELECT tryBase64Decode('definitely not base64') AS gentle;
SELECT base64URLDecode('aHR0cHM6Ly9jbGlja2hvdXNlLmNvbQ') AS url;
La segunda línea es el estilo de la casa de ClickHouse en acción. Al motor le encanta su prefijo try: tryBase64Decode() se traga el fallo y devuelve una cadena vacía, mientras que el base64Decode() plano lanza una excepción con el código INCORRECT_DATA y un mensaje que nombra al valor culpable. Elige la forma plana cuando una fila mala debe detener el pipeline y la forma try cuando el informe debe seguir adelante, y elígelo deliberadamente, no por accidente.
Dos notas de versión, porque ClickHouse avanza rápido. Antes de 26.7, los espacios en blanco en la entrada se rechazaban; a partir de 26.7, espacio, tabulador, salto de línea, retorno de carro y salto de formulario se ignoran todos, que es el comportamiento que quieres para cualquier cosa que haya tocado un correo o un editor de texto. Y el decodificador moderno espera un padding correcto en sus grupos de cuatro caracteres, así que un token que perdió sus signos de igual por el camino será una excepción y no un mejor esfuerzo. Si una consulta que funcionaba en 2023 empieza a lanzar errores en 2026, mira la versión del servidor antes de culpar a los datos.
Oracle: RAW o nada
La maquinaria base64 de Oracle vive en el paquete PL/SQL UTL_ENCODE, y tiene una personalidad propia: toma RAW y devuelve RAW, nada más. No hay texto de entrada, no hay texto de salida. VARCHAR2 es datos de caracteres con un conjunto de caracteres; RAW son bytes en bruto; y el paquete se niega a fingir lo contrario. Así que el patrón que funciona es un sándwich de tres capas: cast a raw, decodificar, cast de vuelta a texto:
SELECT UTL_RAW.CAST_TO_VARCHAR2(
UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW('aGVsbG8gd29ybGQ='))
) AS restored
FROM DUAL;
Cada paso gana su sitio. UTL_RAW.CAST_TO_RAW() reinterpreta los bytes del texto como raw (en el conjunto de caracteres de la base de datos, que en un despliegue moderno suele ser AL32UTF8, así que tu entrada UTF-8 viaja tal cual). UTL_ENCODE.BASE64_DECODE() hace el trabajo de verdad. Y UTL_RAW.CAST_TO_VARCHAR2() reinterpreta los bytes del resultado como texto en ese mismo conjunto de caracteres de la base de datos. Salta cualquier capa y te llevas un error de tipo no coincidente, que es Oracle haciendo su trabajo de ser explícito.
Una entrada inválida lanza una excepción de PL/SQL en vez de una NULL callada, así que una decodificación por lotes debería vivir dentro de un manejador de excepciones que registre la fila culpable. El paquete también arrastra todo un museo de decodificadores hermanos: decodificación de cabeceras MIME, quoted-printable, uudecode, codificación de texto, todos de la misma época. Sobre todo usarás el par base64, pero los vecinos explican por qué el paquete está organizado como está: Oracle quería un único hogar para "datos con disfraz de transporte".
Una trampa de tamaño que hay que conocer antes de empezar. En SQL plano, un valor RAW está limitado a 2000 bytes, así que un valor base64 que se decodifica a más de unos 1500 bytes raw no se puede decodificar con una sola sentencia SELECT, ni siquiera. Los payloads más grandes necesitan un bucle PL/SQL que recorra el BLOB en trozos de 2000 (o menos) bytes, decodifique cada pieza y vuelva a coser los resultados. Es de la vieja escuela, pero es la respuesta estándar de Oracle, y es uno de esos sitios donde el sistema de tipos de los años 90 del lenguaje todavía da forma a tus consultas de los años 2020.
Snowflake: trae tu propio alfabeto
Snowflake separa su tipo binario (BINARY) de sus tipos de texto, y te da el decodificador más configurable de esta familia. El caballo de batalla es BASE64_DECODE_BINARY(input), que devuelve BINARY, y el segundo argumento opcional es una cadena corta que redefine el alfabeto:
SELECT BASE64_DECODE_BINARY('aGVsbG8gd29ybGQ=') AS bytes;
SELECT TO_VARCHAR(BASE64_DECODE_BINARY('aMOpbGxv'), 'UTF-8') AS text;
SELECT TO_VARCHAR(BASE64_DECODE_BINARY('aHR0cHM6Ly9jbGlja2hvdXNlLmNvbQ', '-_'), 'UTF-8') AS url_safe;
Lee ese argumento de alfabeto con cuidado, porque es posicional. Se permiten hasta tres caracteres: los dos primeros sobrescriben las posiciones 62 y 63 del alfabeto (los valores por defecto son + y /), y el tercero sobrescribe el carácter de padding (por defecto =). Para decir "usa el alfabeto URL-safe" pasas '-_'. Para decir "alfabeto URL-safe pero con padding de % en vez" debes pasar los tres caracteres, '-_%', aunque lo único que de verdad quieres cambiar sea el carácter de padding. Omite caracteres y conservas los valores por defecto; no puedes saltarte una posición y rellenar la siguiente.
Dos compañeros completan el set. BASE64_DECODE_STRING() hace la decodificación y la conversión a texto en una sola llamada, así que puedes saltarte el TO_VARCHAR() cuando el payload es texto. Y las variantes TRY_, TRY_BASE64_DECODE_BINARY() y TRY_BASE64_DECODE_STRING(), devuelven NULL ante un valor malo en vez de lanzar un error, que es la versión de Snowflake de la forma try de ClickHouse.
De bytes a texto: el paso del conjunto de caracteres
Decodificar te entrega bytes. Si el payload es un documento, un nombre, un fragmento JSON, le debes un paso más: una interpretación como texto en un conjunto de caracteres nombrado. De aquí viene lo de "se decodificó pero se ve raro", porque una secuencia de bytes solo se convierte en palabras cuando dices en qué idioma de bytes estás leyendo. La tabla es corta y merece la pena memorizarla:
| Dialecto | De bytes a texto | Secuencias inválidas |
|---|---|---|
| MySQL / MariaDB | CONVERT(bin USING utf8mb4) |
reinterpretada; basura entra, basura sale |
| PostgreSQL | convert_from(bytes, 'UTF8') |
lanza un error |
| SQL Server | CAST(bin AS VARCHAR) |
con pérdidas, según la colación |
| Oracle | UTL_RAW.CAST_TO_VARCHAR2(raw) |
reinterpretada en el conjunto de caracteres de la base de datos |
| DuckDB | decode(blob) |
error de conversión |
| ClickHouse | no hace falta; String es el texto |
n/d |
| Snowflake | TO_VARCHAR(bin, 'UTF-8') |
lanza un error |
| SQLite | CAST(blob AS TEXT) |
sin validación alguna |
La dispersión es amplia a propósito. PostgreSQL y DuckDB validan y se niegan, lo que protege tu código aguas abajo del mojibake. MySQL y Oracle reinterpretan en silencio, que es rápido pero significa que la base de datos no puede salvarte de un payload Latin-1 que llega a un mundo UTF-8. SQLite ni siquiera mira, porque en SQLite un valor TEXT son solo bytes con una etiqueta. La regla práctica: decide el conjunto de caracteres antes de decodificar, escríbelo en la consulta como literal, y prueba con un payload que contenga un carácter no ASCII (el clásico aMOpbGxv para héllo es un buen canario, porque se rompe de forma distinta en cada conjunto de caracteres equivocado). Para payloads binarios de verdad, sáltate esta sección entera y deja los bytes como bytes.
JWTs: tres puntos de base64 en una columna
Los JSON Web Tokens son el base64 más común que encontrarás reposado en una base de datos, porque los eventos de autenticación se registran con sus tokens. Un JWT es tres piezas separadas por puntos: una cabecera, un payload y una firma. Las dos primeras son objetos JSON empaquetados como base64, y aquí está el giro que atrapa a la gente: los JWT usan el alfabeto URL-safe sin padding, no la forma estándar con padding. Una / empezaría un nuevo segmento de ruta donde los tokens viajan a menudo, una + se leería como un espacio en una cadena de consulta, y los signos de igual de padding serían pura ceremonia, así que la especificación (RFC 7515 y RFC 7519) pasó a - y _ y se quitó el padding de encima.
Decodificar un token en SQL es, por tanto, una danza de cuatro pasos: dividir por los puntos, cambiar los caracteres URL-safe de vuelta al alfabeto estándar, restaurar el padding, decodificar y parsear el JSON. PostgreSQL, con su tipo JSONB, es un sitio cómodo para hacerlo:
WITH parts AS (
SELECT split_part('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRldiBVc2VyIiwiaWF0IjoxNTE2MjM5MDIyfQ.pDDL2Ljz7cK7vo9Ne4sMFMec1JMhasDUWa82vl-fYsw', '.', 1) AS header_b64,
split_part('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRldiBVc2VyIiwiaWF0IjoxNTE2MjM5MDIyfQ.pDDL2Ljz7cK7vo9Ne4sMFMec1JMhasDUWa82vl-fYsw', '.', 2) AS payload_b64
)
SELECT convert_from(
decode(replace(replace(payload_b64, '-', '+'), '_', '/')
|| CASE MOD(LENGTH(payload_b64), 4)
WHEN 2 THEN '=='
WHEN 3 THEN '='
ELSE '' END,
'base64'),
'UTF8')::jsonb AS claims
FROM parts;
El resultado es un valor JSONB que puedes consultar como cualquier otra columna, y para el token de arriba vuelve como {"iat": 1516239022, "sub": "1234567890", "name": "Dev User"}. Sacar un claim suelto del resultado es entonces simplemente claims->>'sub' en una consulta de seguimiento. La restauración del padding es la expresión CASE: una cadena base64url cuya longitud va dos corta de un múltiplo de cuatro necesita dos signos de igual, tres corta necesita uno, y un múltiplo exacto no necesita ninguno.
Da un paso más y puedes incluso verificar una firma HS256 en SQL, usando la extensión pgcrypto de PostgreSQL para el HMAC (actívala una vez con CREATE EXTENSION IF NOT EXISTS pgcrypto; si no está instalada). Recalcula la firma sobre header.payload con el secreto compartido, formátala de la misma forma base64url y compara:
WITH parts AS (
SELECT split_part('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRldiBVc2VyIiwiaWF0IjoxNTE2MjM5MDIyfQ.pDDL2Ljz7cK7vo9Ne4sMFMec1JMhasDUWa82vl-fYsw', '.', 1) AS header_b64,
split_part('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRldiBVc2VyIiwiaWF0IjoxNTE2MjM5MDIyfQ.pDDL2Ljz7cK7vo9Ne4sMFMec1JMhasDUWa82vl-fYsw', '.', 2) AS payload_b64
)
SELECT rtrim(replace(replace(
encode(hmac((header_b64 || '.' || payload_b64)::bytea,
'sql-secret-key'::bytea, 'sha256'), 'base64'),
'+', '-'),
'/', '_'),
'=') = 'pDDL2Ljz7cK7vo9Ne4sMFMec1JMhasDUWa82vl-fYsw' AS valid
FROM parts;
La llamada hmac() produce el digest, encode(..., 'base64') lo empaqueta, y las tres operaciones de cadena le dan la forma URL-safe sin padding que el token lleva. Para el token y el secreto de arriba, la respuesta es un t sonriente. Aunque llévate las advertencias: esto solo funciona con algoritmos HMAC (HS256, HS384, HS512), mete un secreto compartido dentro de una sentencia de base de datos, y está hecho para informes, auditorías y depuración. Cualquier cosa que de verdad conceda o niegue acceso debería verificar en la capa de aplicación con una librería JWT de verdad.
Data URLs: la imagen dentro de una cadena
El formato data URL (RFC 2397) es la forma que tiene la web de incrustar un archivo dentro de un enlace: data:image/png;base64, seguido del base64 del archivo. Los navegadores los pegan desde el portapapeles, las aplicaciones de página única meten imágenes pequeñas en ellos, y todos y cada uno de esos flujos acaba aterrizando en una columna de base de datos como un valor de texto largo. El formato es data:{media type}[;{parameters}][;base64],{data}, y la única parte que importa para decodificar es todo lo que va después de la primera coma, porque ahí es donde empieza el payload base64.
SELECT uri,
CAST(FROM_BASE64(SUBSTRING(uri, LOCATE(',', uri) + 1)) AS BINARY) AS png_bytes
FROM uploads
WHERE uri LIKE 'data:image/png;base64,%';
Eso es todo el trabajo en MySQL: encontrar la coma, saltarla, decodificar, y tienes los bytes de la imagen en una expresión binaria que puedes guardar en una columna BLOB o pasar por hash para deduplicación. Otros dialectos intercambian las funciones (SUBSTR() y INSTR() en la mayoría, substring() y position() en otros) pero la forma es idéntica.
Tres advertencias. Primero, no toda data URL es base64; una data URL sin el marcador ;base64 lleva texto con percent-encoding en su lugar, y meterla en un decodificador base64 es el error que el filtro LIKE de arriba está ahí para prevenir. Segundo, el media type del prefijo es una declaración, no un hecho; la misma cadena puede decir image/png y contener un JPEG. Si el contenido importa, comprueba los bytes mágicos del resultado decodificado (PNG empieza por 89 50 4E 47, JPEG por FF D8). Tercero, las data URLs son grandes. Una foto de 4 megapíxeles se convierte en una cadena de aproximadamente 5,5 megabytes, que es una conversación de tamaño de columna y de memoria, no de funciones de cadena.
Base64 URL-safe: el alfabeto que viaja
La sección 5 del RFC 4648 definió un segundo alfabeto para base64 porque el original tiene dos caracteres con trabajo en la sintaxis de URLs. El signo más es como los parámetros de consulta suman valores, la barra es como se separan las rutas, y el signo de igual del padding se percent-encodea en el momento en que se topa con una cadena de consulta. La variante URL-safe cambia + por - y / por _ (ambos inocuos en URLs), y la especificación JWT, por encima de eso, se quita el padding por completo. El resultado viaja por enlaces, segmentos de ruta, nombres de archivo e identificadores de fragmento sin un solo signo de porcentaje.
Lo conocerás en una base de datos sobre todo porque se guardaron tokens y enlaces, no porque los datos nacieron ahí. Estos son los que lo manejan de forma nativa y los que necesitan el manual de dos minutos:
| Dialecto | Decodificación URL-safe nativa | Notas |
|---|---|---|
| SQL Server 2025+ | BASE64_DECODE() acepta ambos alfabetos |
no hace falta ninguna traducción |
| ClickHouse 24.6+ | base64URLDecode() |
también acepta + y / |
| Snowflake | BASE64_DECODE_BINARY(s, '-_') |
el alfabeto como argumento posicional |
| MySQL / MariaDB | ninguna | traduce los caracteres, espera NULL ante el fallo |
| PostgreSQL | ninguna | traduce los caracteres, espera un error ante el fallo |
| Oracle | ninguna | traduce los caracteres antes del cast a RAW |
| DuckDB | ninguna (rechaza el guion bajo) | traduce los caracteres, mantén la longitud múltiplo de 4 |
| SQLite CLI | ninguna | traduce los caracteres; el decodificador omite lo que no conoce |
El manual son dos llamadas a REPLACE() más la restauración del padding, y es el mismo en todos los dialectos. En PostgreSQL se lee así:
SELECT convert_from(
decode(replace(replace('aGVsbG8', '-', '+'), '_', '/')
|| CASE MOD(LENGTH('aGVsbG8'), 4)
WHEN 2 THEN '=='
WHEN 3 THEN '='
ELSE '' END,
'base64'),
'UTF8') AS text;
Cambia - de vuelta a +, cambia _ de vuelta a /, añade el padding que falta según la longitud módulo cuatro, y el decodificador estándar toma el relevo desde ahí. La entrada aGVsbG8 (la forma URL-safe sin padding de "hello") vuelve como la propia palabra. Los dos errores que se repiten son los que la expresión CASE previene: olvidar el padding, que hace que los decodificadores estrictos rechacen una longitud que no es múltiplo de cuatro, y saltarse la traducción de caracteres, que hace que un decodificador que no conoce el alfabeto URL-safe se atragante con el guion bajo. Escribe la traducción una vez, como una función reutilizable en tu base de datos, y el problema entero deja de repetirse.
Archivos, blobs y cosas grandes
Decodificar es como sacan los archivos de las columnas, y cada dialecto tiene una puerta de salida ligeramente distinta. En DuckDB el viaje de ida y vuelta son dos sentencias, una para leer un archivo en un BLOB y otra para escribir de vuelta los bytes decodificados:
SELECT filename, octet_length(content) AS size
FROM read_blob('/data/uploads/*.png');
El lado de la lectura: read_blob() es una función de tabla que acepta un nombre de archivo, una lista de nombres o un patrón glob y devuelve una columna filename y una columna content por archivo. El lado de la escritura es su propia sentencia: COPY con el formato BLOB escribe bytes en bruto, sin comillas, sin escapes, exactamente lo que un payload decodificado quiere.
COPY (SELECT from_base64(b64) FROM attachments WHERE id = 42)
TO '/data/restored/cat.png' (FORMAT BLOB);
La puerta de salida de PostgreSQL es la API de objetos grandes. Un objeto grande es un almacén de trozos binarios del lado del servidor, direccionado por un OID, y lo_export() escribe uno en un archivo del servidor de la base de datos. Requiere derechos de superusuario o el privilegio pg_write_server_files, y el destino debe ser una ruta a la que el proceso del servidor pueda escribir, así que en la práctica es un trabajo para scripts de mantenimiento y no para código de aplicación:
SELECT lo_export(12345, '/tmp/attachments/cat.png');
MySQL solo tiene la vía de escape restrictiva SELECT ... INTO DUMPFILE (una sola fila, ruta del lado del servidor, privilegio FILE), y SQL Server no tiene ningún escritor de archivos en SQL plano (escribir en disco es trabajo del cliente o de un agente, vía sus herramientas de exportación), que es un diseño justo: la base de datos guarda los bytes, la aplicación decide dónde vive el archivo. SQLite se sienta en el otro extremo del espectro, donde la aplicación es el anfitrión y una columna BLOB puede escribirse directamente en disco con una sola llamada del lenguaje anfitrión.
Y luego están los techos, que difieren más de lo que esperarías de bases de datos que todas hacen como si fueran iguales:
| Dialecto | Tipo binario | Techo práctico |
|---|---|---|
| PostgreSQL | bytea |
1 GB por valor |
| MySQL / MariaDB | familia BLOB | max_allowed_packet (64 MB por defecto en MySQL 8) |
| SQL Server | varbinary(max) |
2 GB por valor |
| Oracle | RAW / BLOB |
RAW: 2000 bytes en SQL, BLOB: 4 GB con troceado PL/SQL |
| SQLite | BLOB |
lo que permitan el archivo y la memoria |
| DuckDB | BLOB |
muy grande; lo deciden memoria y disco |
| ClickHouse | String |
el tamaño de columna es virtual, la fila es la unidad |
| Snowflake | BINARY |
8 MB por valor por defecto (columnas BINARY planas); hasta 64 MB con un BINARY(N) explícito |
La fila de MySQL merece una historia, porque es la que sorprende a la gente en producción. max_allowed_packet limita el tamaño de un único paquete entre cliente y servidor, y una cadena base64 es parte de ese paquete. Una foto de 50 megabytes codificada a base64 es una cadena de unos 67 megabytes, que es mayor que el valor por defecto de 64 megabytes, y el resultado no es un error que puedas leer en la consulta: es un valor truncado o NULL que parece una corrupción de datos. Si estás moviendo archivos grandes por una columna de MySQL, comprueba ese límite antes de empezar, y recuerda que lo que cuenta contra él es la forma codificada, no los bytes en bruto.
Envolturas de correo y líneas MIME
Cualquier base64 que haya sobrevivido al sistema de correo lleva un recuerdo: saltos de línea. MIME, el conjunto de estándares que deja que el correo lleve adjuntos binarios (RFC 2045, sección 6.8), envuelve la salida base64 a los 76 caracteres y termina las líneas con un retorno de carro y un salto de línea. El envoltorio existe porque la vieja red de correo no podía fiarse de líneas más largas que esa, y el formato se ha llevado arrastrando por inercia desde entonces. Así que un adjunto guardado en una columna de base de datos es con frecuencia una cadena base64 con un salto de línea cada 76 caracteres, y la relación de tu decodificador con esos saltos de línea decide si el trabajo es una sentencia o dos.
| Decodificador | ¿Se come el envoltorio? | Si no |
|---|---|---|
MySQL / MariaDB FROM_BASE64() |
sí | - |
PostgreSQL decode() |
sí | - |
SQL Server BASE64_DECODE() |
sí | - |
SQLite CLI base64() |
sí | - |
| ClickHouse 26.7+ | sí | - |
| ClickHouse anterior a 26.7 | no | quita los espacios en blanco primero |
DuckDB from_base64() |
no | quita los espacios en blanco primero |
Oracle UTL_ENCODE.BASE64_DECODE() |
no | quita los espacios en blanco en la capa PL/SQL |
La solución de "quitar primero" es una expresión, y es siempre segura, porque los espacios en blanco no son parte del alfabeto base64: ningún payload legítimo puede contener un espacio, un tabulador o un salto de línea, así que quitarlos no puede destruir información. En PostgreSQL la forma idiomática es una sola llamada a regexp_replace():
SELECT decode(regexp_replace(attachment_b64, '\s', '', 'g'), 'base64')
FROM email_attachments;
Cada carácter de espacio en blanco, saltos de línea incluidos, se va, y el decodificador ve una cadena limpia y continua. Corre esto en DuckDB (con su replace() sobre los dos caracteres de salto de línea) o en un ClickHouse anterior a 26.7, y el adjunto envuelto se decodifica exactamente igual que el no envuelto.
Payloads de API, configuraciones y cabeceras de autenticación
Dá un paso atrás desde las funciones individuales y aparece un patrón: el base64 en una columna de base de datos es casi siempre una de tres cosas. Un campo dentro de un documento JSON (una imagen, un certificado, un archivo que una API decidió incrustar). Un valor de configuración (un secreto o una credencial que alguna herramienta prefiere en base64, porque base64 cabe en una línea de un archivo YAML sin comillas, sin saltos de línea y sin barras invertidas). O un artefacto de autenticación (una cabecera Basic auth, un token guardado, un blob de sesión). Estos son, cada uno con su forma de decodificación.
Campos JSON. El JSON llegó como texto, el campo es una cadena, y el base64 se esconde dentro. Extrae el campo con la función JSON de tu dialecto y luego decodifica. En MySQL toda la cadena es una sola expresión:
SELECT event_id,
CAST(FROM_BASE64(JSON_UNQUOTE(JSON_EXTRACT(payload, '$.image'))) AS BINARY) AS image_bytes
FROM api_events
WHERE JSON_TYPE(JSON_EXTRACT(payload, '$.image')) = 'STRING';
PostgreSQL hace lo mismo con JSONB, donde el campo sale como texto con el operador ->> y decode() toma el relevo. La guarda JSON_TYPE de la última línea importa más de lo que parece: mantiene alejado al decodificador de las filas donde el campo es un número, un objeto anidado o está faltando, y en MySQL esas filas, de lo contrario, aportarían una NULL silenciosa a tu recuento de "cuántos eventos llevaban una imagen".
Cabeceras de autenticación. Una cabecera Basic auth es la cadena literal Basic seguida del base64 de username:password. Decodificarla en SQL es un substring y un split, que es exactamente por qué la gente lo hace (sobre todo para auditar qué usuarios tocaron qué endpoints, no para verificar la contraseña, que la base de datos nunca debería ver en claro):
SELECT request_id,
SUBSTRING_INDEX(CAST(FROM_BASE64(SUBSTRING(header_value, 7)) AS CHAR), ':', 1) AS username,
SUBSTRING_INDEX(CAST(FROM_BASE64(SUBSTRING(header_value, 7)) AS CHAR), ':', -1) AS secret
FROM http_log
WHERE header_name = 'Authorization'
AND header_value LIKE 'Basic %';
SUBSTRING(header_value, 7) pela el prefijo Basic , el decodificador restaura el texto original, y las dos llamadas a SUBSTRING_INDEX() lo cortan en el dos puntos, primera parte para el usuario, última parte para el secreto. En PostgreSQL la misma consulta usa substring() y split_part().
Valores de configuración. La dirección de decodificar aquí es el trabajo de auditoría: alguien guardó un secreto como base64 en una tabla de configuración (un hábito heredado de Kubernetes, donde los valores de secreto son base64 en reposo), y quieres ver lo que hay realmente dentro, o estás construyendo la exportación que un entorno nuevo va a consumir. La forma es un SELECT por valor, y el paso del conjunto de caracteres se aplica si el valor es texto:
SELECT name,
CONVERT(FROM_BASE64(value) USING utf8mb4) AS plaintext
FROM app_config
WHERE name LIKE '%_secret%';
Trata ese resultado con el cuidado que se merece. Acabas de convertir secretos guardados en salida de consulta visible; asegúrate de que la cuenta que ejecuta la consulta tiene los derechos que debería, de que el resultado no se copia a un log, y de que el hábito de base64-en-configuración recibe una segunda mirada. Base64 es un transporte, no una bóveda, y una consulta de auditoría es el momento en que eso se hace evidente.
Las trampas que muerden
Cada trampa de esta lista es una que se ha comido una tarde en al menos un codebase, y cada una es específica de la forma en que los dialectos de SQL manejan base64 y no de base64 en sí.
- La NULL silenciosa. MySQL y MariaDB decodifican una entrada mala a
NULLsin queja. En un informe que une sobre el valor decodificado, esas filas simplemente desaparecen, y la diferencia entre "0 filas" y "0 filas porque 14 estaban envenenadas" es invisible hasta que alguien pregunta por qué el recuento no cuadra. Si tu decodificador es del tipo callado, cuenta tus NULLs a propósito. - La regla del múltiplo de cuatro, aplicada a medias. Una cadena cuya longitud no es múltiplo de cuatro no es base64, pero los dialectos discrepan sobre qué hacer: PostgreSQL lanza un error, DuckDB lanza un error de conversión, ClickHouse lanza una excepción, MySQL devuelve
NULL, y la CLI de SQLite decodifica en silencio lo que puede. El mismo archivo de datos produce cinco resultados distintos en cinco bases de datos, que es por qué "funcionó en Postgres" no es una prueba. - El desajuste de alfabeto. Un token URL-safe (JWT, enlace, nombre de archivo) metido en un decodificador de alfabeto estándar: SQL Server lo acepta, el
base64URLDecode()de ClickHouse lo acepta, Snowflake lo acepta con el argumento correcto, y todos los demás o devuelvenNULL, o lanzan un error o, en el caso de la CLI de SQLite, tiran el guion bajo en silencio y te entregan los bytes equivocados. El caso de los bytes equivocados es el feo, porque el resultado parece plausible. - El envoltorio MIME. Una entrada envuelta metida en un decodificador que no se come los saltos de línea (DuckDB, ClickHouse anterior a 26.7, Oracle) falla, y el fallo a menudo se parece a "los últimos 76 caracteres son basura" en vez de "hay un salto de línea por aquí", porque el error apunta al carácter después del corte.
- El truco de visualización. El cliente mysql imprime binario como hex, psql imprime bytea como hex
\x, Snowflake imprime BINARY como hex, y Oracle imprime RAW como hex. Cuatro clientes, cuatro notaciones hex, un error muy humano de concluir que los datos están corruptos porque la pantalla muestra números. Convierte siempre de forma explícita antes de leer el resultado con los ojos. - Padding en el sitio equivocado. Un signo de igual solo es legal al final, uno o dos. Una cadena como
YQ==BQ==son dos grupos válidos con un solo disfraz, y los decodificadores estrictos lo rechazan mientras los indulgentes lo decodifican a algo que nadie pidió. Si algún día ves padding en medio de un valor guardado, el codificador que lo escribió está roto, y arreglar los datos es un trabajo puntual. - La sorpresa del conjunto de caracteres. La decodificación tiene éxito, el texto vuelve, y los acentos están mal. Los bytes estaban bien; la interpretación no. Este es el
CONVERT(... USING latin1)que debería haber sidoutf8mb4, elCAST(bin AS VARCHAR)que corrió bajo una colación que traga secuencias inválidas, elCAST(blob AS TEXT)de SQLite que nunca comprueba. Fija el conjunto de caracteres como literal en la consulta y prueba con un canario acentuado. - Los techos. El límite de RAW de 2000 bytes de Oracle en sentencias SQL, el
max_allowed_packetde MySQL cobrando al tamaño codificado, el techo de bytea de 1 GB de PostgreSQL, la longitud BINARY por defecto de 8 MB de Snowflake. Cada uno está documentado, cada uno se descubre en producción, y cada uno es una comprobación de tamaño que podrías haber escrito antes de que los datos fueran grandes. - Confiar en los bytes decodificados. Base64 puede llevar cualquier cosa, incluida una cadena llena de comillas. Decodificar no es sanitizar. Lo que hagas con el texto decodificado (compararlo, registrarlo en un log, concatenarlo a otra sentencia) sigue necesitando las protecciones de siempre, y una consulta parametrizada sigue siendo una consulta parametrizada después de un viaje de ida y vuelta base64.
Cómo mantenerse del lado correcto
- Decide el tipo primero, no la función primero. ¿El payload es binario o texto? El binario va a BLOB/bytea/varbinary y se queda ahí. El texto pasa por el paso del conjunto de caracteres con una codificación explícita. La mitad de todo el dolor base64 en SQL es un payload binario que se metió por error en una columna de texto (o viceversa) y ahora está siendo interpretado.
- Valida antes de decodificar, o decodifica con suavidad. Una regex sobre el alfabeto más una comprobación de longitud módulo cuatro no cuesta nada y convierte un error que detiene el lote en una
NULLque puedes contar. Donde el dialecto ofrece una forma try (eltryBase64Decodede ClickHouse, elTRY_BASE64_DECODE_BINARYde Snowflake), úsala para informes y guarda la forma estricta para los pipelines que no deben adivinar. - Comprueba la versión del dialecto, no solo de la base de datos. ClickHouse 26.7 cambió el manejo de espacios en blanco, SQL Server 2025 es el primer lanzamiento con la función, la CLI de SQLite necesita 3.41, y las expectativas de padding de ClickHouse se han endurecido con el tiempo. "Es ClickHouse" no es una especificación; "es ClickHouse 24.8" sí lo es.
- Documenta el alfabeto de cada columna. Una columna que puede contener base64 estándar y URL-safe es una columna que confundirá al siguiente desarrollador. Si los datos vienen de JWTs, dilo en el comentario del esquema; si vienen de adjuntos MIME, dilo también. La elección del decodificador es una propiedad de la columna, no de la consulta.
- Guarda bytes, codifica en el borde. Si controlas el esquema, una columna BLOB más codificación en la capa de API gana a una columna de texto base64 para el almacenamiento, para el indexado y para cualquier consulta futura. El base64 en la columna es un impuesto de compatibilidad, y los impuestos es mejor pagarlos una vez, en la frontera.
- Recorrido de ida y vuelta con un canario. Antes de fiarte de una nueva ruta de decodificación, pasa un payload conocido por codificar y decodificar en la misma base de datos y compara. El canario debe contener un carácter no ASCII (para ejercitar el paso del conjunto de caracteres), una longitud que deja una cola de padding (para ejercitar las reglas de padding) y, para rutas URL-safe, un
-o un_en algún sitio (para ejercitar la traducción de alfabeto). - Mantén los secretos fuera del texto de la consulta. Verificar JWTs con pgcrypto mete un secreto compartido en la sentencia; las auditorías de configuración meten secretos en claro en el resultado. Ambos son trabajos legítimos, pero se merecen una cuenta restringida, un log limpio y una revisión, no una cadena de conexión de producción y un
SELECT * INTO OUTFILE.
Breve historia del desempaquetado en SQL
El formato base64 en sí es más antiguo que la parte útil de internet. Fue estandarizado para MIME a mediados de los 90 (RFC 2045, sección 6.8, que dejó obsoleto el RFC 1521, la especificación de cuerpo de mensaje MIME de 1993 que llevaba la codificación), y el nombre es solo un recuento: el alfabeto tiene 64 caracteres. La variante URL-safe llegó con el RFC 4648 en 2006, y la especificación JWT de 2015 convirtió a esa variante en la que de verdad ves en las columnas de tokens. Pero las bases de datos se encontraron con el formato cada una a su propio ritmo, y el ritmo te dice algo del alma de cada una.
2002. PostgreSQL 7.2 ya lista base64 como un formato de primera clase de encode() y decode() - contemporáneo del UTL_ENCODE de Oracle en la era 9i - y el soporte base64 más antiguo de esta familia por un margen corto. Una base de datos con un tipo binario de verdad y un argumento de formato llegó temprano, porque la respuesta estaba a un valor de enum de distancia.
Principios de los 2000. El paquete UTL_ENCODE de Oracle aparece en la era 9i, llevando base64 junto a funciones de cabecera MIME, quoted-printable y uudecode. Entra RAW y sale RAW, que es muy de Oracle, y ha mantenido esa forma durante un cuarto de siglo.
2013. MySQL 5.6 añade TO_BASE64() y FROM_BASE64(), y MariaDB 10.0 lleva ambos al fork. El par codifica con líneas de 76 caracteres y decodifica con tolerancia a espacios en blanco, un set a juego que no ha cambiado en una docena de versiones mayores.
2018. ClickHouse 18.16 lanza base64Decode() con su alias al estilo MySQL, porque el mundo columnar estaba importando cargas de trabajo que ya llevaban base64 en sus esquemas de log.
2023. SQLite 3.41.0 añade base64() y su hermana base85 a la shell de línea de comandos como funciones definidas por la aplicación. La biblioteca central, fiel a su forma, no recibe nada; la shell, que es donde los humanos de verdad meten el dedo en las bases de datos SQLite, recibe la herramienta.
2025. SQL Server 2025, disponible generalmente en noviembre de 2025, añade BASE64_DECODE() y BASE64_ENCODE() a T-SQL después de una ausencia de treinta y seis años. Las notas de lanzamiento las tratan como una función modesta; la comunidad las trata como un rescate.
El patrón es limpio una vez que lo ves. Las bases de datos con un tipo binario genuino y un argumento de formato (PostgreSQL, y a su manera Oracle) recibieron base64 el día en que la necesidad fue evidente. Las demás (MySQL, SQL Server) lo trataron como una comodidad de cadena y lo planificaron en consecuencia. Y el motor embebible (SQLite) sigue considerándolo trabajo de la aplicación anfitriona, con la CLI como excepción amable.
Cosas que te harán sonreír
- SQL Server pasó de 1989 a 2025 sin un decodificador base64, y la respuesta de la comunidad fue una función XML llamada
xs:base64Binary()dentro de unCAST(N'' AS XML). Una generación entera de consultas empresariales decodificó tokens a través del parser XML, porque el parser XML entendía base64 desde 2001 y el motor SQL no. - El
base64()de la CLI de SQLite es el único camaleón de esta familia: pásale un BLOB y codifica, pásale texto y decodifica. La función cambia de trabajo según el tipo de su argumento, que es un pequeño acto de telepatía SQL y una trampa genuina para el desprevenido. - El codificador de PostgreSQL envuelve a los 76 caracteres exactamente como el estándar MIME de 1996, excepto que termina las líneas con un salto de línea suelto en vez del retorno de carro y salto de línea del estándar. Veinte años después de la especificación, un carácter menos. El decodificador ignora ambos, así que la rebelión es invisible a menos que hagas un diff de la salida.
- En el cliente
mysql,SELECT FROM_BASE64('aGVsbG8=')imprime0x68656C6C6F. No porque los datos sean hex, y no porque haya algo mal, sino porque el cliente decidió, en tu nombre, que las cadenas binarias se deben mostrar como hex. El ajuste se llamabinary-as-hex, y ha convencido a miles de desarrolladores de que su decodificador está roto. - El tipo RAW a nivel SQL de Oracle está limitado a 2000 bytes, así que un certificado de 3 kilobytes ni siquiera se puede pegar en una sentencia SQL como literal RAW. La decodificación tiene que pasar por PL/SQL, en trozos, con un bucle. El límite data de los 90; el bucle sigue siendo la respuesta recomendada.
- Snowflake muestra los valores
BINARYcomo hex en todos los conjuntos de resultados, así que una decodificación perfectamente exitosa de "hello" llega a tu pantalla como68656C6C6F. Dos dialectos, dos visualizaciones hex, una misma sensación de inquietud. - ClickHouse mantiene el alias
FROM_BASE64()junto a subase64Decode()nativo, un pequeño gesto de cortesía hacia los refugiados de MySQL que llegaron con consultas que, de lo contrario, no habrían funcionado. - Toda la familia comparte un hecho callado: base64 es un impuesto del 33 por ciento en la salida y un reembolso del 25 por ciento en la entrada, y ninguno de los ocho decodificadores de aquí te lo va a decir sin que lo preguntes. El formato es un disfraz; el armario es gratis; la sastrería es de lo que trata este artículo.
Sigue adelante
Este artículo ha tratado de quitarse el disfraz: la función de cada dialecto, su temperamento, y los payloads (JWTs, data URLs, correo envuelto, campos JSON, valores de configuración, cabeceras de autenticación) que lo llevan. La otra dirección es un animal propio, con su propio set de sorpresas: qué codificadores envuelven su salida a los 76 caracteres y cuáles no, cómo producir la forma URL-safe sin padding que los tokens esperan, la aritmética de tamaño que decide el ancho de tu columna, y qué significa la brecha de 36 años de SQL Server para quien todavía está en una versión más antigua. Todo eso, desde TO_BASE64() hasta BASE64_ENCODE(), está cubierto a fondo en el artículo relacionado de codificación Base64 para SQL, enlazado desde esta página. Decodifica aquí, codifica allí, y todo el viaje de ida y vuelta cabe en una sola tarde.
Última actualización: 2026-09-08
Artículo relacionado: Codificación Base64 en SQL: una guía completa