Decodifica Base64 in SQL: una guida completa
Apri un qualsiasi database di produzione per tempo sufficiente e incontrerai il travestimento. Un avatar arrivato come un muro di lettere dentro un export JSON. Un JWT parcheggiato in una colonna varchar accanto a un ID utente. Un certificato che qualcuno ha deciso di spedire come stringa perché il formato di trasferimento non aveva il tipo binario. In qualche punto di una tabella, i tuoi dati indossano delle lettere, e il tuo lavoro è toglierle senza uscire dal database.
In SQL, quel lavoro ha una proprietà molto rassicurante: una volta capito quale decodificatore parla il tuo dialetto, il lavoro intero si riduce a una singola chiamata di funzione. Il formato in sé è già spiegato in dettaglio sulla pagina iniziale (64 caratteri stampabili, ogni gruppo di quattro che sta al posto di tre byte in input, fino a due segni = che fanno da padding all'ultimo gruppo), quindi questo articolo salta la lezione. Due cose da portarti dietro: il base64 è un modo per travestire i byte come testo, non un lucchetto, e la decodifica è la direzione in cui i dati diventano più piccoli (tornando a tre quarti della loro dimensione codificata), che è esattamente l'opposto di quello per cui la tua colonna di archiviazione era dimensionata. La vera storia è che SQL è una famiglia di dialetti, e ogni membro chiama il suo decodificatore con un nome diverso e reagisce agli input cattivi con un temperamento completamente diverso. Questo articolo è il tour.
La squadra dei decodificatori
Ecco chi è in servizio, e come si comporta ciascuno quando l'input è spazzatura. La colonna "quando si rompe" conta, perché un decodificatore che fallisce a gran voce in staging e fallisce in silenzio in produzione è così che gli avatar mancanti finiscono sul campo:
| Dialetto | La chiamata | Cosa torna | Quando si rompe | Da quando |
|---|---|---|---|---|
| MySQL 8.x / MariaDB 10.x | FROM_BASE64(str) |
stringa binaria | NULL silenziosa |
MySQL 5.6 (2013) |
| PostgreSQL | decode(str, 'base64') |
bytea |
ERROR clamoroso con un suggerimento |
7.2 (2002) |
| SQLite (CLI 3.41+) | base64(str) |
BLOB |
salta ciò che non sa leggere | 3.41.0 (2023) |
| DuckDB | from_base64(str) |
BLOB |
errore di conversione | release moderne |
| ClickHouse 18.16+ | base64Decode(str) |
String |
eccezione (INCORRECT_DATA) |
18.16.0 (2018) |
| SQL Server 2025+ | BASE64_DECODE(str) |
varbinary |
Msg 9803, tre stati | 2025 |
| Oracle | UTL_ENCODE.BASE64_DECODE(raw) |
RAW |
eccezione PL/SQL | era 9i |
| Snowflake | BASE64_DECODE_BINARY(str) |
BINARY |
errore, oppure NULL con la variante TRY_ |
release correnti |
Nota la forma della tabella: il nome della funzione non è mai la parte difficile. La parte difficile è la colonna "quando si rompe", perché è quella colonna a decidere se il tuo report perde righe in silenzio o se il tuo lavoro in batch si ferma e chiede aiuto.
MySQL e MariaDB: il decodificatore che alza le spalle
I due server condividono la coppia TO_BASE64() / FROM_BASE64(). Il decodificatore prende una stringa e restituisce una stringa binaria: una sequenza di byte senza nessun set di caratteri allegato. Una NULL in input dà una NULL in output, ed ecco la prima cosa da memorizzare: qualsiasi altra cosa che non sia base64 valido è anch'essa una NULL, senza alcun avviso. Il decodificatore alza le spalle, e la tua query continua contenta per la sua strada.
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 riga di mezzo merita un commento, perché spiega un classico momento di confusione. Il client da riga di comando mysql stampa le stringhe binarie in notazione esadecimale di default (un'impostazione chiamata binary-as-hex), quindi un semplice SELECT FROM_BASE64('aGVsbG8=') mostra 0x68656C6C6F al posto di hello. Non è un bug e non è corruzione: è il client che è cauto con i dati binari. Se vuoi le lettere, converti con CONVERT(... USING utf8mb4) o avvia il client con --binary-as-hex=0; la chiamata HEX() della riga di mezzo è la versione deliberata dell'hex che il client ti mostra di default.
Ora le regole che applica il decodificatore silenzioso. Dopo aver ignorato gli spazi bianchi, i caratteri rimanenti devono formare un multiplo di quattro, ogni carattere deve provenire dall'alfabeto standard (lettere, cifre, +, / e =), e il padding può comparire solo alla fine:
SELECT FROM_BASE64('aGVsbG8gd29ybGQ=') AS ok;
SELECT FROM_BASE64('aGVsbG8gd29ybGQ') AS missing_padding;
SELECT FROM_BASE64('!!!') AS nonsense;
Tutte e tre le righe girano senza lamentarsi, e le righe due e tre restituiscono NULL. Padding mancante, lunghezza sbagliata, caratteri alieni: stessa alzata di spalle. Gli spazi bianchi sono l'unica indulgenza; gli a capo, i carriage return, le tab e gli spazi vengono tutti ignorati, ed è una vera grazia per qualsiasi cosa che sia passata prima da un'email. L'alfabeto URL-safe, d'altra parte, riceve l'alzata di spalle in cambio: un underscore non è nella tabella standard, quindi FROM_BASE64('yv7K_g==') è una NULL anche se la lunghezza è un bel multiplo di quattro. Devi tradurre l'alfabeto tu stesso prima della chiamata, e la sezione URL-safe qui sotto mostra come fare.
Un tratto in più che vale la pena conoscere: decodificatore e codificatore sono una coppia fatta per sé. Il codificatore spezza il suo output in righe di 76 caratteri, e il decodificatore se li mangia a colazione. Se una colonna è stata riempita da TO_BASE64() nella stessa famiglia di database, la decodifica è un perfetto viaggio di andata e ritorno. Se invece è stata riempita da qualcos'altro, continua a leggere.
PostgreSQL: il decodificatore che alza la voce
PostgreSQL ha il base64 nel suo nucleo almeno dalla versione 7.2, nel 2002, il che lo rende il meccanismo base64 più antico di questa famiglia, di poco. La chiamata è decode(string, 'base64'), e il risultato è bytea, il tipo binario nativo del database. Il compagno encode(bytea, 'base64') va nella direzione opposta e viene menzionato qui solo perché i due condividono un contratto di formattazione: lo stile RFC 2045, con righe spezzate a 76 caratteri. Il decodificatore, per la sua parte, ignora i carriage return, gli a capo, gli spazi e le tab ovunque nell'input.
SELECT decode('aGVsbG8gd29ybGQ=', 'base64') AS bytes;
SELECT length(decode('aGVsbG8gd29ybGQ=', 'base64')) AS byte_count;
SELECT convert_from(decode('aMOpbGxv', 'base64'), 'UTF8') AS text;
La terza riga è quella a cui ricorrerai in continuazione: convert_from() trasforma il bytea in testo con una codifica nominata, ed è il passaggio di set di caratteri che i dati binari necessitano (ne parleremo nella sua sezione dedicata più avanti). aMOpbGxv torna come héllo, con tanto di carattere accentato.
Il punto in cui PostgreSQL si stacca dal resto del gruppo è la colonna "quando si rompe". Un input non valido è un errore duro, e il messaggio di errore ti dice esattamente quale regola è stata violata:
- un carattere fuori dall'alfabeto:
ERROR: invalid symbol "!" found while decoding base64 sequence - un segno di padding a metà stringa:
ERROR: unexpected "=" while decoding base64 sequence - input troncato o padding mancante:
ERROR: invalid base64 end sequence, con il suggerimento Input data is missing padding, is truncated, or is otherwise corrupted. - un underscore URL-safe:
ERROR: invalid symbol "_" found while decoding base64 sequence
Per un lavoro di pulizia dati, quella voce è una funzionalità. La query fallisce, vedi la riga, aggiusti la fonte. Il rovescio della medaglia è che una riga avvelenata su un milione ferma l'intero batch, quindi nei pipeline di produzione spesso si prefiltra con una regex prima di chiamare decode(). E una piccola nota di visualizzazione: psql stampa il bytea come hex con prefisso \x, quindi \x68656c6c6f è la stessa "hello" che il client MySQL mostra come 0x68656C6C6F. Due dialetti, due dialetti dell'hex.
SQL Server: l'arrivo tardivo
Ecco la sorpresa di tutta la famiglia. SQL Server ha rilasciato BASE64_DECODE() nella versione 2025, disponibile in generale a novembre 2025. Prima di allora, il database più popolare del mondo enterprise non aveva un decodificatore base64 integrato per trentasei anni, e il folklore era fitto di workaround. La funzione moderna è una cosa pulita: prende un'espressione varchar(n) o varchar(max) e restituisce un varbinary (un'espressione varchar(n) viene mappata su varbinary(8000), e un'espressione varchar(max) viene mappata su varbinary(max)), con le NULL che passano dritto.
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;
Quella terza riga è una mossa davvero gradevole: il decodificatore accetta entrambi gli alfabeti RFC 4648, quello standard con + e / e quello URL-safe con - e _, e il padding è opzionale. Ignora anche i quattro spazi bianchi (a capo, carriage return, tab, spazio). Quando poi si rompe, l'errore è Msg 9803, Level 16 con il testo Invalid data for type "Base64Decode", e il valore di State ti dice quale regola hai colpito: stato 20 per un carattere che non è in nessuno dei due alfabeti, stato 21 per caratteri tutti validi ma disposti in una forma che il base64 non può produrre, e stato 23 per un padding che compare troppo spesso o troppo presto.
Se sei bloccato su una versione pre-2025, il workaround classico fa leva sul tipo XML, che capisce il base64 dai tempi di XML Schema:
SELECT CAST(N'' AS XML)
.value('xs:base64Binary("aGVsbG8=")', 'VARBINARY(MAX)') AS legacy;
Il motore XML decodifica la costante in base64 e restituisce i byte. Funziona, ed è quello che ha usato una generazione di sviluppatori SQL Server. Ha anche i suoi spigoli: il tipo base64Binary è rigoroso sulla forma, quindi una stringa avvolta in MIME con a capo all'interno non viene analizzata, e stai pagando il prezzo del meccanismo XML per un lavoro che una singola funzione ora fa in modo nativo. Trattalo come il pezzo da museo che è diventato.
SQLite: il dialetto senza decodificatore
SQLite è l'eccezione del gruppo, e capire il perché ti dice come usarlo. La libreria core è un motore piccolo e incorporabile, e il base64 non è nella sua lista di funzioni standard. Se una colonna contiene base64, il decodificatore deve venire da uno di quattro posti: la shell da riga di comando, un'estensione caricabile, una funzione personalizzata registrata dall'applicazione host, o SQL puro. Ecco ciascuno di questi.
La CLI. A partire dalla versione 3.41.0 (febbraio 2023), la shell da riga di comando sqlite3 offre una funzione base64(). Decodifica un argomento di testo in un BLOB, il che la rende perfetta per lavoro esplorativo direttamente da terminale:
$ sqlite3 app.db "SELECT hex(base64('aGVsbG8gd29ybGQ='));"
68656C6C6F20776F726C64
Due temperamenti da conoscere. Primo: è indulgente: i caratteri che non riconosce vengono saltati invece che segnalati, quindi base64('!!!') restituisce un BLOB vuoto invece di un errore. Ottimo per la curiosità, pericoloso per l'auditing, perché "vuoto" e "mancante" sembrano la stessa cosa nell'output. Secondo: la funzione è camaleontica; un argomento BLOB viene codificato in testo (con righe di 72 caratteri), mentre un argomento di testo viene decodificato in un BLOB. Lo stesso nome, due lavori, scelti in base al tipo dell'argomento. Nessun altro decodificatore di questa famiglia lo fa, quindi leggi il tipo del tuo input due volte.
SQL puro. La libreria core non ha il base64, ma ha CTE ricorsive, aritmetica e (da 3.41.0) unhex(), ed è abbastanza per costruire un vero decodificatore in un paio di dozzine di righe. La ricetta: una tabella dell'alfabeto a 64 righe, l'input tagliato in frammenti di quattro caratteri, ogni frammento trasformato in un numero di 24 bit, quel numero suddiviso in tre byte, e i byte raccolti come hex prima che unhex() li trasformi in un BLOB. Ecco, funzionante su una colonna di tabella:
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;
Eseguilo su una tabella con una colonna b64 e ottieni un BLOB per riga, senza estensioni, senza codice di applicazione. L'aritmetica è base64 puro in abiti da numeri interi: ciascuno dei quattro caratteri contribuisce sei bit, i due caratteri centrali attraversano un confine di byte, e i due bit bassi dell'ultimo carattere vengono scartati. È l'opzione più lenta di questa pagina (un passaggio ricorsivo più una ricerca per frammento), quindi tienila per payload piccoli e archeologia una tantum. Per un'applicazione a lungo termine, la risposta onesta è la terza opzione: registra una funzione personalizzata di una riga dal linguaggio host (il modulo sqlite3 di Python lo fa in due righe con create_function() e il modulo standard base64) e lascia che il motore la chiami come una nativa. La quarta opzione, le estensioni caricabili come la famiglia sqlean, esiste anch'essa, ma significa installare una build diversa del motore, cosa che la maggior parte dei team preferirebbe evitare.
DuckDB: rigoroso, piccolo, con le sue idee
DuckDB è un database analitico con un tipo binario vero, BLOB, e una famiglia di funzioni blob ben ordinata intorno a esso. Il decodificatore è from_base64(string), e sta seduto accanto ai suoi amici to_base64(), hex(), md5() e sha256() nella stessa pagina di riferimento, ed è lì che la maggior parte degli utenti DuckDB lo incontra per la prima volta.
SELECT from_base64('aGVsbG8gd29ybGQ=') AS bytes;
SELECT decode(from_base64('aMOpbGxv')) AS text;
SELECT hex(from_base64('AAEC')) AS padding_optional;
La terza riga mostra una regola più amichevole di quanto potresti aspettarti: quando la lunghezza è un multiplo di quattro, il padding mancante non è un problema, AAEC si decodifica nei byte 00 01 02 senza nessun problema. Il rigore viene fuori nel momento in cui la forma è sbagliata. DuckDB vuole una lunghezza che sia un multiplo di quattro, punto e basta, e l'errore di conversione dice esattamente quello:
SELECT from_base64('YWJ');
-- Conversion Error: Could not decode string "YWJ" as base64: length must be a multiple of 4
Due opinioni in più da rispettare. Prima: il decodificatore di DuckDB parla solo l'alfabeto standard; un underscore non è un carattere che riconosce, quindi i token URL-safe devono essere tradotti prima di arrivare (la ricetta è nella sezione URL-safe). Secondo: non ha nessuna tolleranza per gli spazi bianchi. Un allegato email avvolto in MIME con i suoi a capo di 76 caratteri all'interno fallirà, e la correzione è un replace() sugli a capo e i carriage return prima della chiamata. E dato che non esiste una variante try_ per ammorbidire il colpo, il modo gentile è un controllo preliminare nella stessa query:
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 prima, decodificatore dopo: la query restituisce NULL per qualsiasi cosa che non possa essere decodificata, e il decodificatore vede solo input dalla forma corretta.
ClickHouse: il decodificatore delle colonne
ClickHouse non ha un tipo binario separato; il suo String è binary-safe senza battere ciglio, il che significa che decodificare "in una stringa" è l'intero lavoro e non segue nessun passaggio di conversione. La funzione c'è dalla versione 18.16.0 (2018) col nome base64Decode(), e mantiene un alias in stile MySQL, FROM_BASE64(), quindi le query portate non richiedono nessuna riscrittura.
SELECT base64Decode('aGVsbG8gd29ybGQ=') AS text;
SELECT tryBase64Decode('definitely not base64') AS gentle;
SELECT base64URLDecode('aHR0cHM6Ly9jbGlja2hvdXNlLmNvbQ') AS url;
La seconda riga è lo stile della casa ClickHouse in azione. Il motore ama il suo prefisso try: tryBase64Decode() ingoia il fallimento e restituisce una stringa vuota, mentre il semplice base64Decode() lancia un'eccezione con il codice INCORRECT_DATA e un messaggio che nomina il valore colpevole. Scegli la forma semplice quando una riga cattiva deve fermare il pipeline e la forma try quando il report deve continuare, e fallo per scelta, non per caso.
Due note sulle versioni, perché ClickHouse corre veloce. Prima di 26.7, gli spazi bianchi nell'input venivano rifiutati; da 26.7 in poi, spazio, tab, line feed, carriage return e form feed vengono tutti ignorati, ed è il comportamento che vuoi per qualsiasi cosa che abbia toccato un'email o un editor di testo. E il decodificatore moderno si aspetta un padding corretto sui suoi gruppi di quattro caratteri, quindi un token che ha perso i suoi segni di uguale in ingresso sarà un'eccezione e non un best effort. Se una query che funzionava nel 2023 inizia a lanciare eccezioni nel 2026, guarda la versione del server prima di dare la colpa ai dati.
Oracle: RAW o niente
Il meccanismo base64 di Oracle vive nel pacchetto PL/SQL UTL_ENCODE, e ha un carattere tutto suo: prende RAW e restituisce RAW, nient'altro. Nessun testo in ingresso, nessun testo in uscita. VARCHAR2 è un dato di carattere con un set di caratteri; RAW sono byte nudi; e il pacchetto rifiuta di fingere il contrario. Quindi il modello che funziona è un panino a tre strati: cast a raw, decodifica, cast di nuovo a testo:
SELECT UTL_RAW.CAST_TO_VARCHAR2(
UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW('aGVsbG8gd29ybGQ='))
) AS restored
FROM DUAL;
Ogni passo si guadagna il suo posto. UTL_RAW.CAST_TO_RAW() riinterpreta i byte del testo come raw (nel set di caratteri del database, che per un deployment moderno di solito è AL32UTF8, quindi il tuo input UTF-8 viaggia com'è). UTL_ENCODE.BASE64_DECODE() fa il lavoro vero. E UTL_RAW.CAST_TO_VARCHAR2() riinterpreta i byte del risultato come testo nello stesso set di caratteri del database. Salta uno strato e ottieni un errore di mancata corrispondenza dei tipi, ed è Oracle che fa il suo lavoro di essere esplicito.
Un input non valido lancia un'eccezione PL/SQL invece di una NULL silenziosa, quindi una decodifica in batch dovrebbe vivere dentro un gestore di eccezioni che registra la riga colpevole nel log. Il pacchetto porta anche un intero museo di decodificatori fratelli: decodifica degli header MIME, quoted-printable, uudecode, codifica testuale, tutti della stessa era. Userai soprattutto la coppia base64, ma i vicini spiegano perché il pacchetto è organizzato come è: Oracle voleva una casa sola per "i dati che indossano un costume di trasporto".
Una trappola di misura da conoscere prima di iniziare. In SQL puro, un valore RAW è limitato a 2000 byte, quindi un valore base64 che si decodifica in più di circa 1500 byte raw non può essere decodificato con una singola istruzione SELECT, punto. I payload più grandi richiedono un ciclo PL/SQL che percorre il BLOB a blocchi di 2000 (o meno) byte, decodifica ogni pezzetto e ricuce i risultati insieme. È vecchio stile, ma è la risposta standard di Oracle, ed è uno di quei posti dove il sistema di tipi degli anni '90 del linguaggio continua a modellare le tue query degli anni 2020.
Snowflake: porta il tuo alfabeto
Snowflake tiene separato il suo tipo binario (BINARY) dai suoi tipi di testo, e ti dà il decodificatore più configurabile di questa famiglia. Il cavallo da lavoro è BASE64_DECODE_BINARY(input), che restituisce BINARY, e il secondo argomento opzionale è una stringa corta che ridefinisce l'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;
Leggi con attenzione quell'argomento alfabeto, perché è posizionale. Sono ammessi fino a tre caratteri: i primi due sovrascrivono le posizioni 62 e 63 dell'alfabeto (i default sono + e /), e il terzo sovrascrive il carattere di padding (default =). Per dire "usa l'alfabeto URL-safe" passi '-_'. Per dire "alfabeto URL-safe ma fai il padding con %" devi passare tutti e tre i caratteri, '-_%', anche se l'unica cosa che vuoi davvero cambiare è il carattere di padding. Ometti dei caratteri e mantieni i default; non puoi saltare una posizione e riempire quella successiva.
Due compagni completano il set. BASE64_DECODE_STRING() fa la decodifica e la conversione al testo in una chiamata sola, così puoi saltare il TO_VARCHAR() quando il payload è testo. E le varianti TRY_, TRY_BASE64_DECODE_BINARY() e TRY_BASE64_DECODE_STRING(), restituiscono NULL su un valore cattivo invece di lanciare un errore: è la versione Snowflake della forma try di ClickHouse.
Dai byte al testo: il passaggio del set di caratteri
La decodifica ti consegna dei byte. Se il payload è un documento, un nome, un frammento JSON, gli devi un passaggio in più: un'interpretazione come testo in un set di caratteri nominato. È da qui che nasce "si è decodificato ma sembra sbagliato", perché una sequenza di byte diventa parole solo quando dici quale lingua di byte stai leggendo. La tabella è corta e vale la pena di memorizzarla:
| Dialetto | Dai byte al testo | Sequenze non valide |
|---|---|---|
| MySQL / MariaDB | CONVERT(bin USING utf8mb4) |
riinterpretati; spazzatura in, spazzatura out |
| PostgreSQL | convert_from(bytes, 'UTF8') |
lancia un errore |
| SQL Server | CAST(bin AS VARCHAR) |
con perdita, a seconda della collazione |
| Oracle | UTL_RAW.CAST_TO_VARCHAR2(raw) |
riinterpretati nel set di caratteri del database |
| DuckDB | decode(blob) |
errore di conversione |
| ClickHouse | non serve nulla; String è il testo |
n/d |
| Snowflake | TO_VARCHAR(bin, 'UTF-8') |
lancia un errore |
| SQLite | CAST(blob AS TEXT) |
nessuna validazione |
La varianza è ampia di proposito. PostgreSQL e DuckDB validano e rifiutano, il che protegge il tuo codice a valle dai caratteri corrotti (mojibake). MySQL e Oracle reinterpretano in silenzio, il che è veloce ma significa che il database non può salvarti da un payload Latin-1 che arriva in un mondo UTF-8. SQLite non guarda nemmeno, perché in SQLite un valore TEXT è solo byte con un'etichetta. La regola pratica: decidi il set di caratteri prima di decodificare, scrivilo nella query come un letterale, e testa con un payload che contenga un carattere non ASCII (il classico aMOpbGxv per héllo è un buon canarino, perché si rompe in modo diverso in ogni set di caratteri sbagliato). Per i payload davvero binari, salta questa sezione interamente e tieni i byte come byte.
JWT: tre punti di base64 in una colonna
I JSON Web Token sono il base64 più comune che troverai seduto in un database, perché gli eventi di autenticazione vengono registrati insieme ai loro token. Un JWT è tre pezzi separati da punti: un header, un payload e una firma. I primi due sono oggetti JSON impacchettati come base64, ed ecco il colpo di scena che beffa tutti: i JWT usano l'alfabeto URL-safe senza padding, non la forma standard con padding. Un / darebbe inizio a un nuovo segmento di path dove i token viaggiano spesso, un + verrebbe letto come uno spazio in una stringa di query, e i segni di uguale del padding sarebbero pura cerimonia, quindi la specifica (RFC 7515 e RFC 7519) è passata a - e _ e ha buttato il padding.
Decodificare un token in SQL è quindi una danza a quattro passi: taglia sui punti, riporta i caratteri URL-safe all'alfabeto standard, ripristina il padding, decodifica e analizza il JSON. PostgreSQL, con il suo tipo JSONB, è un posto comodo per farlo:
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;
Il risultato è un valore JSONB che puoi interrogare come qualsiasi altra colonna, e per il token qui sopra torna come {"iat": 1516239022, "sub": "1234567890", "name": "Dev User"}. Estrarre un singolo claim dal risultato è poi solo claims->>'sub' in una query di follow-up. Il ripristino del padding è l'espressione CASE: una stringa base64url la cui lunghezza è corta di due rispetto a un multiplo di quattro ha bisogno di due segni di uguale, se è corta di tre ne ha bisogno di uno, e un multiplo esatto non ne ha bisogno.
Fai un passo in più e puoi perfino verificare una firma HS256 in SQL, usando l'estensione pgcrypto di PostgreSQL per l'HMAC (attivala una volta con CREATE EXTENSION IF NOT EXISTS pgcrypto; se non è già installata). Ricalcola la firma su header.payload con la chiave segreta condivisa, formattala nello stesso modo base64url e confronta:
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 chiamata hmac() produce il digest, encode(..., 'base64') lo impacchetta, e le tre operazioni sulle stringhe lo rimodellano nella forma URL-safe senza padding che il token porta. Per il token e il segreto qui sopra, la risposta è un allegro t. Tieni però con te le avvertenze: questo funziona solo per gli algoritmi HMAC (HS256, HS384, HS512), mette una chiave segreta condivisa dentro un'istruzione di database, ed è fatto per reporting, auditing e debug. Qualsiasi cosa che in effetti decida l'accesso dovrebbe verificare nel layer di applicazione con una vera libreria JWT.
Data URL: l'immagine dentro una stringa
Il formato data URL (RFC 2397) è il modo del web di incorporare un file in un link: data:image/png;base64, seguito dal base64 del file. I browser li incollano dagli appunti, le single-page app vi incorporano immagini piccole, e ciascuno di questi flussi alla fine atterra in una colonna di database come un lungo valore di testo. Il formato è data:{media type}[;{parameters}][;base64],{data}, e l'unica parte che conta per la decodifica è tutto ciò che viene dopo la prima virgola, perché è lì che inizia il 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,%';
Questo è l'intero lavoro in MySQL: trova la virgola, passala, decodifica, e hai i byte dell'immagine in un'espressione binaria che puoi mettere in una colonna BLOB o hashare per la deduplicazione. Gli altri dialetti scambiano le funzioni (SUBSTR() e INSTR() nella maggior parte, substring() e position() in altre) ma la forma è identica.
Tre avvertenze. Prima: non ogni data URL è base64; un data URL senza il marcatore ;base64 porta testo in percent-encoding al suo posto, e darlo in pasto a un decodificatore base64 è un errore che il filtro LIKE qui sopra c'è proprio per prevenire. Seconda: il media type nel prefisso è un'affermazione, non un fatto; la stessa stringa può dire image/png e contenere un JPEG. Se il contenuto conta, controlla i magic byte del risultato decodificato (PNG inizia con 89 50 4E 47, JPEG con FF D8). Terza: i data URL sono grandi. Una foto da 4 megapixel diventa una stringa di circa 5,5 megabyte, ed è una questione di dimensione della colonna e di memoria, non di funzioni sulle stringhe.
Base64 URL-safe: l'alfabeto che viaggia
La sezione 5 di RFC 4648 ha definito un secondo alfabeto per il base64, perché l'originale ha due caratteri con un ruolo nella sintassi URL. Il segno più è come i parametri di query aggiungono valori, la barra è come si separano i path, e il segno di uguale del padding viene codificato con percent-encoding nel momento in cui incontra una stringa di query. La variante URL-safe scambia + con - e / con _ (entrambi innocui negli URL), e la specifica JWT per di più butta via il padding del tutto. Il risultato viaggia attraverso link, segmenti di path, nomi di file e identificatori di frammento senza un singolo segno di percentuale.
Lo incontrerai in un database soprattutto perché ci sono stati salvati token e link, non perché i dati sono nati lì. Ecco chi sa gestirlo in modo nativo e chi ha bisogno del manuale da due minuti:
| Dialetto | Decodifica URL-safe nativa | Note |
|---|---|---|
| SQL Server 2025+ | BASE64_DECODE() accetta entrambi gli alfabeti |
non serve nessuna traduzione |
| ClickHouse 24.6+ | base64URLDecode() |
accetta comunque anche + e / |
| Snowflake | BASE64_DECODE_BINARY(s, '-_') |
l'alfabeto come argomento posizionale |
| MySQL / MariaDB | nessuno | tradurre i caratteri, aspettarsi NULL in caso di fallimento |
| PostgreSQL | nessuno | tradurre i caratteri, aspettarsi un errore in caso di fallimento |
| Oracle | nessuno | tradurre i caratteri prima del cast a RAW |
| DuckDB | nessuno (rifiuta l'underscore) | tradurre i caratteri, tenere la lunghezza un multiplo di 4 |
| SQLite CLI | nessuno | tradurre i caratteri; il decodificatore salta ciò che non conosce |
Il manuale sono due chiamate a REPLACE() più il ripristino del padding, ed è lo stesso in ogni dialetto. In PostgreSQL si legge così:
SELECT convert_from(
decode(replace(replace('aGVsbG8', '-', '+'), '_', '/')
|| CASE MOD(LENGTH('aGVsbG8'), 4)
WHEN 2 THEN '=='
WHEN 3 THEN '='
ELSE '' END,
'base64'),
'UTF8') AS text;
Riporta - a +, riporta _ a /, aggiungi il padding mancante in base al modulo quattro della lunghezza, e da lì il decodificatore standard subentra. L'input aGVsbG8 (la forma URL-safe senza padding di "hello") torna come la parola stessa. I due errori che continuano a succedere sono quelli che l'espressione CASE previene: dimenticare il padding, che fa rifiutare ai decodificatori rigorosi una lunghezza che non è un multiplo di quattro, e saltare la traduzione dei caratteri, che fa inceppare un decodificatore che non conosce l'alfabeto URL-safe sull'underscore. Scrivi la traduzione una volta, come funzione riutilizzabile nel tuo database, e il problema intero smette di ricomparire.
File, BLOB e cose grandi
La decodifica è il modo in cui i file escono dalle colonne, e ogni dialetto ha una porta d'uscita leggermente diversa. In DuckDB il viaggio di andata e ritorno sono due istruzioni, una per leggere un file in un BLOB e una per scrivere di nuovo i byte decodificati:
SELECT filename, octet_length(content) AS size
FROM read_blob('/data/uploads/*.png');
Il lato lettura: read_blob() è una funzione tabella che accetta un nome di file, una lista di nomi o un pattern glob e restituisce una colonna filename e una content per file. Il lato scrittura è a sé stante: COPY con il formato BLOB scrive byte grezzi, senza virgolette, senza escaping, esattamente ciò che vuole un payload decodificato.
COPY (SELECT from_base64(b64) FROM attachments WHERE id = 42)
TO '/data/restored/cat.png' (FORMAT BLOB);
La porta d'uscita di PostgreSQL è l'API dei large object. Un large object è un magazzino di blocchi binari sul lato server, indirizzato da un OID, e lo_export() ne scrive uno su un file nel server del database. Richiede i diritti di superuser o il privilegio pg_write_server_files, e la destinazione deve essere un percorso su cui il processo server può scrivere, quindi in pratica è un lavoro per script di manutenzione e non per codice di applicazione:
SELECT lo_export(12345, '/tmp/attachments/cat.png');
MySQL ha solo la via di fuga restrittiva SELECT ... INTO DUMPFILE (singola riga, percorso sul lato server, privilegio FILE), e SQL Server non ha nessun writer di file in SQL puro (scrivere su disco è un lavoro del client o di un agent, tramite i suoi strumenti di export), ed è un design equo: il database conserva i byte, l'applicazione decide dove il file deve stare. SQLite sta all'altra estremità dello spettro, dove l'applicazione è l'host e una colonna BLOB può essere scritta direttamente su disco con una singola chiamata del linguaggio host.
Poi ci sono i tetti, che differiscono più di quanto ti aspetteresti da database che fanno tutti finta di essere uguali:
| Dialetto | Tipo binario | Tetto pratico |
|---|---|---|
| PostgreSQL | bytea |
1 GB per valore |
| MySQL / MariaDB | famiglia BLOB | max_allowed_packet (default 64 MB in MySQL 8) |
| SQL Server | varbinary(max) |
2 GB per valore |
| Oracle | RAW / BLOB |
RAW: 2000 byte in SQL, BLOB: 4 GB con chunking PL/SQL |
| SQLite | BLOB |
ciò che file e memoria permettono |
| DuckDB | BLOB |
molto grande; memoria e disco decidono |
| ClickHouse | String |
la dimensione della colonna è virtuale, le righe sono l'unità |
| Snowflake | BINARY |
8 MB per valore di default (colonne BINARY standard); fino a 64 MB con un BINARY(N) esplicito |
La riga di MySQL merita una storia, perché è quella che sorprende le persone in produzione. max_allowed_packet limita la dimensione di un singolo pacchetto tra client e server, e una stringa base64 fa parte di quel pacchetto. Una foto da 50 megabyte codificata in base64 è una stringa di circa 67 megabyte, che è più grande del default di 64 megabyte, e il risultato non è un errore che puoi leggere nella query: è un valore troncato o NULL che sembra corruzione dei dati. Se stai spostando file grandi attraverso una colonna MySQL, controlla quel limite prima di iniziare, e ricorda che è la forma codificata, non i byte grezzi, a contare contro di esso.
Le avvolture email e le righe MIME
Ogni base64 che ha sopravvissuto al sistema email porta con sé un souvenir: gli a capo. MIME, l'insieme di standard che permette all'email di portare allegati binari (RFC 2045, sezione 6.8), avvolge l'output base64 a 76 caratteri e termina le righe con un carriage return e un line feed. L'avvolgimento esiste perché la vecchia rete email non poteva fidarsi di righe più lunghe, e il formato è stato portato avanti per abitudine da allora in poi. Quindi un allegato salvato in una colonna di database è spesso una stringa base64 con un a capo ogni 76 caratteri, e il rapporto del tuo decodificatore con quegli a capo decide se il lavoro è un'istruzione o due.
| Decodificatore | Mangia l'avvolgimento? | Se no |
|---|---|---|
MySQL / MariaDB FROM_BASE64() |
sì | - |
PostgreSQL decode() |
sì | - |
SQL Server BASE64_DECODE() |
sì | - |
SQLite CLI base64() |
sì | - |
| ClickHouse 26.7+ | sì | - |
| ClickHouse prima di 26.7 | no | rimuovere prima gli spazi bianchi |
DuckDB from_base64() |
no | rimuovere prima gli spazi bianchi |
Oracle UTL_ENCODE.BASE64_DECODE() |
no | rimuovere gli spazi bianchi nel layer PL/SQL |
La correzione "rimuovi prima" è una singola espressione, ed è sempre sicura, perché gli spazi bianchi non fanno parte dell'alfabeto base64: nessun payload legittimo può contenere uno spazio, una tab o un a capo, quindi rimuoverli non può distruggere informazioni. In PostgreSQL l'idioma è un singolo regexp_replace():
SELECT decode(regexp_replace(attachment_b64, '\s', '', 'g'), 'base64')
FROM email_attachments;
Ogni carattere di spazio bianco, a capo inclusi, sparisce, e il decodificatore vede una singola stringa continua e pulita. Esegui questo in DuckDB (con il suo replace() sui due caratteri di a capo) o su un ClickHouse pre-26.7, e l'allegato avvolto si decodifica esattamente come quello non avvolto.
Payload API, config e header di autenticazione
Fai un passo indietro dalle singole funzioni e appare un pattern: il base64 in una colonna di database è quasi sempre una di tre cose. Un campo dentro un documento JSON (un'immagine, un certificato, un file che un'API ha deciso di inlineare). Un valore di configurazione (un segreto o una credenziale che qualche strumento preferisce in base64, perché il base64 sta su una riga di un file YAML senza virgolette, senza a capo e senza backslash). O un artefatto di autenticazione (un header di autenticazione Basic, un token salvato, un blob di sessione). Ecco ciascuno con la sua forma di decodifica.
Campi JSON. Il JSON è arrivato come testo, il campo è una stringa, e il base64 si nasconde dentro. Estrai il campo con la funzione JSON del tuo dialetto, poi decodifica. In MySQL l'intera catena è una singola espressione:
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 fa la stessa cosa con JSONB, dove il campo esce come testo con l'operatore ->> e decode() riprende in mano le redini. La guardia JSON_TYPE dell'ultima riga conta più di quanto sembri: tiene il decodificatore lontano dalle righe dove il campo è un numero, un oggetto annidato o mancante, e in MySQL quelle righe altrimenti contribuirebbero una NULL silenziosa al tuo conteggio di "quanti eventi avevano un'immagine".
Header di autenticazione. Un header di autenticazione Basic è la stringa letterale Basic seguita dal base64 di username:password. Decodificarlo in SQL è un substring e una divisione, ed è esattamente il motivo per cui le persone lo fanno (di solito per fare auditing di quali utenti hanno colpito quali endpoint, non per verificare la password, che il database non dovrebbe mai vedere in chiaro):
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) toglie via il prefisso Basic , il decodificatore ripristina il testo originale, e le due chiamate a SUBSTRING_INDEX() lo dividono al due punti, prima parte per l'utente, ultima parte per il segreto. In PostgreSQL la stessa query usa substring() e split_part().
Valori di configurazione. La direzione decodifica qui è il lavoro di auditing: qualcuno ha salvato un segreto come base64 in una tabella di configurazione (un'abitudine ereditata da Kubernetes, dove i valori dei segreti sono base64 a riposo), e vuoi vedere cosa c'è davvero, o stai costruendo l'export che un nuovo ambiente consumerà. La forma è una SELECT per valore, e il passaggio del set di caratteri si applica se il valore è testo:
SELECT name,
CONVERT(FROM_BASE64(value) USING utf8mb4) AS plaintext
FROM app_config
WHERE name LIKE '%_secret%';
Tratta quel risultato con la cura che merita. Hai appena trasformato segreti salvati in output di query visibile; assicurati che l'account che esegue la query abbia i diritti che dovrebbe, che il risultato non venga copiato in un log, e che l'abitudine base64-nelle-configurazioni riceva una seconda occhiata. Il base64 è un trasporto, non una cassaforte, e una query di auditing è il momento in cui questo diventa evidente.
Le insidie che mordono
Ogni insidia di questa lista è una che ha rubato un pomeriggio in almeno un codebase, e ognuna è specifica del modo in cui i dialetti SQL gestiscono il base64, non del base64 in sé.
- La NULL silenziosa. MySQL e MariaDB decodificano un input cattivo in
NULLsenza lamentarsi. In un report che fa join sul valore decodificato, quelle righe semplicemente spariscono, e la differenza tra "0 righe" e "0 righe perché 14 di esse erano avvelenate" è invisibile finché qualcuno non chiede perché il conteggio non torna. Se il tuo decodificatore è della specie silenziosa, conta le tue NULL apposta. - La regola del multiplo di quattro, applicata a macchia di leopardo. Una stringa la cui lunghezza non è un multiplo di quattro non è base64, ma i dialetti non sono d'accordo su cosa fare: PostgreSQL lancia un errore, DuckDB lancia un errore di conversione, ClickHouse lancia un'eccezione, MySQL restituisce
NULL, e la CLI di SQLite decodifica silenziosamente ciò che può. Lo stesso file di dati produce cinque risultati diversi su cinque database, ed è per questo che "funzionava in Postgres" non è un test. - Il disallineamento degli alfabeti. Un token URL-safe (JWT, link, nome di file) dato in pasto a un decodificatore con alfabeto standard: SQL Server lo accetta,
base64URLDecode()di ClickHouse lo accetta, Snowflake lo accetta con l'argomento giusto, e tutti gli altri o restituisconoNULL, o lanciano un errore, o, nel caso della CLI di SQLite, scartano silenziosamente l'underscore e ti consegnano i byte sbagliati. Il caso dei byte sbagliati è quello sgradevole, perché il risultato sembra plausibile. - L'avvolgimento MIME. Un input avvolto dato a un decodificatore che non mangia gli a capo (DuckDB, ClickHouse pre-26.7, Oracle) fallisce, e il fallimento spesso sembra "gli ultimi 76 caratteri sono spazzatura" piuttosto che "c'è un a capo qui dentro", perché l'errore punta al carattere dopo il taglio.
- Il trucco della visualizzazione. Il client mysql stampa il binario come hex, psql stampa il bytea come hex con prefisso
\x, Snowflake stampa BINARY come hex, e Oracle stampa RAW come hex. Quattro client, quattro notazioni hex, un errore molto umano: concludere che i dati sono corrotti perché lo schermo mostra numeri. Converti sempre esplicitamente prima di leggere il risultato con i tuoi occhi. - Padding nel posto sbagliato. Un segno di uguale è legale solo alla fine, uno o due. Una stringa come
YQ==BQ==è due gruppi validi che indossano un unico costume, e i decodificatori rigorosi la rifiutano mentre quelli indulgenti la decodificano in qualcosa che nessuno ha chiesto. Se vedi mai padding a metà di un valore salvato, il codificatore che l'ha scritto è rotto, e sistemare i dati è un lavoro una tantum. - La sorpresa del set di caratteri. La decodifica riesce, il testo torna, e gli accenti sono sbagliati. I byte erano a posto; l'interpretazione no. Questa è la
CONVERT(... USING latin1)che avrebbe dovuto essereutf8mb4, ilCAST(bin AS VARCHAR)che è girato sotto una collazione che inghiotte le sequenze non valide, ilCAST(blob AS TEXT)in SQLite che non controlla mai. Fissa il set di caratteri come letterale nella query e testa con un canarino accentato. - I tetti. Il limite di 2000 byte per RAW di Oracle nelle istruzioni SQL, il
max_allowed_packetdi MySQL che tassa la dimensione codificata, il tetto bytea di 1 GB di PostgreSQL, la lunghezza BINARY predefinita di 8 MB di Snowflake. Ognuno è documentato, ognuno viene scoperto in produzione, e ognuno è un controllo di dimensioni che avresti potuto scrivere prima che i dati diventassero grandi. - Fidarsi dei byte decodificati. Il base64 può portare qualsiasi cosa, inclusa una stringa piena di virgolette. Decodificare non è sanificare. Qualsiasi cosa tu faccia con il testo decodificato (confrontarlo, registrarne il log, concatenarlo in un'altra istruzione) ha comunque bisogno delle protezioni di sempre, e una query parametrica dopo un viaggio di andata e ritorno base64 è ancora una query parametrica.
Come stare dalla parte giusta
- Decidi prima il tipo, non prima la funzione. Il payload è binario o è testo? Il binario va in BLOB/bytea/varbinary e ci resta. Il testo passa attraverso il passaggio del set di caratteri con una codifica esplicita. Metà di tutto il dolore base64 in SQL è un payload binario che è finito in una colonna di testo (o vice versa) e ora viene interpretato.
- Valida prima di decodificare, o decodifica con delicatezza. Una regex sull'alfabeto più un controllo del modulo quattro della lunghezza non costa nulla e trasforma un errore che ferma il batch in una
NULLche puoi contare. Dove il dialetto offre una forma try (tryBase64Decodedi ClickHouse,TRY_BASE64_DECODE_BINARYdi Snowflake), usala per il reporting e tieni la forma rigorosa per i pipeline che non devono indovinare. - Controlla la versione del dialetto, non solo del database. ClickHouse 26.7 ha cambiato la gestione degli spazi bianchi, SQL Server 2025 è la prima versione con la funzione in assoluto, la CLI di SQLite richiede 3.41, e le aspettative di padding di ClickHouse si sono strette nel tempo. "È ClickHouse" non è una specifica; "è ClickHouse 24.8" lo è.
- Documenta l'alfabeto di ogni colonna. Una colonna che può contenere sia base64 standard che URL-safe è una colonna che confonderà il prossimo sviluppatore. Se i dati vengono da JWT, dillo nel commento dello schema; se vengono da allegati MIME, dillo pure. La scelta del decodificatore è una proprietà della colonna, non della query.
- Conserva byte, codifica sul bordo. Se controlli lo schema, una colonna BLOB più la codifica nel layer API batte una colonna di testo base64 per lo storage, per l'indicizzazione e per ogni query futura. Il base64 nella colonna è una tassa di compatibilità, e le tasse si pagano meglio una volta sola, al confine.
- Metti alla prova l'andata e ritorno con un canarino. Prima di fidarti di un nuovo percorso di decodifica, fai passare un payload noto attraverso codifica e decodifica nello stesso database e confronta. Il canarino dovrebbe contenere un carattere non ASCII (per mettere alla prova il passaggio del set di caratteri), una lunghezza che lascia una coda di padding (per mettere alla prova le regole del padding) e, per i percorsi URL-safe, un
-o un_da qualche parte (per mettere alla prova la traduzione dell'alfabeto). - Tieni i segreti fuori dal testo della query. La verifica dei JWT con pgcrypto mette una chiave segreta condivisa nell'istruzione; le audit delle configurazioni mettono segreti in chiaro nel risultato. Entrambi sono lavori legittimi, ma meritano un account ristretto, un log pulito e una revisione, non una stringa di connessione di produzione e un
SELECT * INTO OUTFILE.
Una breve storia dello svestire in SQL
Il formato base64 in sé è più vecchio della parte utile di internet. È stato standardizzato per MIME a metà degli anni '90 (RFC 2045, sezione 6.8, che ha reso obsoleto RFC 1521, la specifica del corpo dei messaggi MIME del 1993 che portava la codifica), e il nome è solo un conteggio: l'alfabeto ha 64 caratteri. La variante URL-safe è arrivata con RFC 4648 nel 2006, e la specifica JWT nel 2015 ha fatto di quella variante la variante che in effetti vedi nelle colonne di token. Ma i database hanno incontrato il formato ciascuno con il suo piano, e il piano ti dice qualcosa sull'anima di ciascuno.
2002. PostgreSQL 7.2 elenca già il base64 come formato di prima classe di encode() e decode() - contemporaneo all'UTL_ENCODE di Oracle dell'era 9i, e il supporto base64 più antico di questa famiglia, di poco. Un database con un tipo binario vero e un argomento formato ci è arrivato presto, perché la risposta era a un valore enum di distanza.
Inizio anni 2000. Il pacchetto UTL_ENCODE di Oracle appare nell'era 9i, portando il base64 accanto alle funzioni per gli header MIME, il quoted-printable e l'uuecode. È RAW in ingresso e RAW in uscita, il che è molto Oracle, e ha mantenuto quella forma per un quarto di secolo.
2013. MySQL 5.6 aggiunge TO_BASE64() e FROM_BASE64(), e MariaDB 10.0 porta entrambi nel fork. La coppia codifica con righe di 76 caratteri e decodifica con tolleranza per gli spazi bianchi, un set fatto per sé che non è cambiato in una dozzina di versioni maggiori.
2018. ClickHouse 18.16 rilascia base64Decode() con il suo alias in stile MySQL, perché il mondo dei database a colonne importava carichi di lavoro che portavano già il base64 nei loro schema di log.
2023. SQLite 3.41.0 aggiunge base64() e il suo fratello base85 alla shell da riga di comando come funzioni definite dall'applicazione. La libreria core, fedele a se stessa, non riceve nulla; la shell, che è il posto dove gli umani effettivamente smanettano con i database SQLite, riceve lo strumento.
2025. SQL Server 2025, disponibile in generale a novembre 2025, aggiunge BASE64_DECODE() e BASE64_ENCODE() a T-SQL dopo un'assenza di trentasei anni. Le note di rilascio li trattano come una funzione modesta; la comunità li tratta come un salvataggio.
Il pattern diventa chiaro una volta che lo vedi. I database con un tipo binario genuino e un argomento formato (PostgreSQL, e a modo suo Oracle) hanno avuto il base64 il giorno in cui il bisogno è diventato ovvio. Gli altri (MySQL, SQL Server) lo hanno trattato come una comodità per le stringhe e lo hanno programmato di conseguenza. E il motore incorporabile (SQLite) lo considera ancora un lavoro dell'applicazione host, con la CLI come eccezione amichevole.
Cose che ti faranno sorridere
- SQL Server ha passato il periodo dal 1989 al 2025 senza un decodificatore base64, e la risposta della comunità era una funzione XML chiamata
xs:base64Binary()dentro unCAST(N'' AS XML). Un'intera generazione di query enterprise ha decodificato i token attraverso il parser XML, perché il parser XML capiva il base64 dal 2001 e il motore SQL no. base64()della CLI di SQLite è l'unico camaleonte di questa famiglia: passale un BLOB e codifica, passale testo e decodifica. La funzione cambia lavoro in base al tipo del suo argomento, il che è un piccolo atto di telepatia SQL e una trappola vera per gli sconsiderati.- Il codificatore di PostgreSQL avvolge a 76 caratteri esattamente come lo standard MIME del 1996, tranne per il fatto che termina le righe con un singolo a capo invece del carriage return e a capo dello standard. Vent'anni dopo la specifica, un carattere in meno. Il decodificatore ignora entrambi, quindi la ribellione è invisibile a meno che non fai il diff dell'output.
- Nel client
mysql,SELECT FROM_BASE64('aGVsbG8=')stampa0x68656C6C6F. Non perché i dati siano hex, e non perché ci sia qualcosa di sbagliato, ma perché il client ha deciso, in tuo nome, che le stringhe binarie devono essere visualizzate come hex. L'impostazione si chiamabinary-as-hex, e ha convinto migliaia di sviluppatori che il loro decodificatore è rotto. - Il tipo RAW a livello SQL di Oracle è limitato a 2000 byte, quindi un certificato da 3 kilobyte non può nemmeno essere incollato in un'istruzione SQL come letterale RAW. La decodifica deve succedere in PL/SQL, a blocchi, con un ciclo. Il limite risale agli anni '90; il ciclo è ancora la risposta consigliata.
- Snowflake mostra i valori
BINARYcome hex in ogni result set, quindi una decodifica perfettamente riuscita di "hello" arriva sul tuo schermo come68656C6C6F. Due dialetti, due visualizzazioni hex, un'identica sensazione di disagio. - ClickHouse mantiene l'alias
FROM_BASE64()accanto al suobase64Decode()nativo, una piccola cortesia per i rifugiati MySQL arrivati con query che altrimenti non girerebbero. - Tutta la famiglia condivide un fatto silenzioso: il base64 è una tassa del 33 percento in uscita e un rimborso del 25 percento in entrata, e nessuno degli otto decodificatori qui te lo dice senza essere interpellato. Il formato è un costume; il guardaroba è gratis; la sartoria è ciò di cui parla questo articolo.
Prosegui
Questo articolo ha parlato di togliere il travestimento: la funzione in ogni dialetto, il suo temperamento, e i payload (JWT, data URL, email avvolte, campi JSON, valori di configurazione, header di autenticazione) che lo indossano. L'altra direzione è un animale a sé, con il suo set di sorprese: quali codificatori avvolgono il loro output a 76 caratteri e quali no, come produrre la forma URL-safe senza padding che i token aspettano, i calcoli di dimensione che decidono la larghezza della tua colonna, e cosa significa il gap di 36 anni di SQL Server per chiunque sia ancora su una versione più vecchia. Tutto questo, da TO_BASE64() a BASE64_ENCODE(), è trattato in profondità nell'articolo correlato sulla codifica Base64 per SQL, collegato da questa pagina. Decodifica qui, codifica là, e l'intero viaggio di andata e ritorno sta dentro un solo pomeriggio.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in SQL: una guida completa