Werk je met Base64-indeling? Dan is deze site perfect voor jou! Gebruik onze handige online tool om je gegevens te coderen of te decoderen.

Base64-decodering in SQL: een complete gids

Open lang genoeg elke productiedatabase en je komt de vermomming tegen. Een avatar die binnenkwam als een muur van letters in een JSON-export. Een JWT geparkeerd in een varchar-kolom naast een gebruikers-ID. Een certificaat dat iemand besloot als string te versturen omdat het overdrachtsformaat geen binair kende. Ergens in een tabel draagt je data letters, en jouw taak is ze af te trekken zonder de database te verlaten.

In SQL heeft dat werk één bemoedigende eigenschap: zodra je weet welke decoder jouw dialect spreekt, krimpt het hele werk ineen tot één functie-aanroep. Het formaat zelf is al uitvoerlijk uitgelegd op de homepage (64 drukbare tekens, elke groep van vier staat voor drie invoerbytes, en tot twee =-tekens halen de laatste groep op), dus dit artikel slaat die uitleg over. Twee dingen om mee te nemen: base64 is een manier om bytes als tekst te verkleiden, geen slot, en decoderen is de richting waarin de data kleiner wordt (terug naar driekwart van de gecodeerde grootte), precies het tegenovergestelde van waar je opslagkolom op is berekend. Het echte verhaal is dat SQL een familie van dialecten is, en dat elk lid zijn decoder een andere naam geeft en op slechte invoer reageert met een volstrekt ander temperament. Dit artikel is de rondleiding.

De decoderopstelling

Dit is wie er dienst heeft, en hoe elk zich gedraagt wanneer de invoer afval is. De "wanneer het kapotgaat"-kolom is belangrijk, want een decoder die luidruchtig faalt in staging maar in stilte faalt in productie is precies hoe ontbrekende avatars het veld in komen:

Dialect De aanroep Wat terugkomt Wanneer het kapotgaat Sinds wanneer
MySQL 8.x / MariaDB 10.x FROM_BASE64(str) binaire string stille NULL MySQL 5.6 (2013)
PostgreSQL decode(str, 'base64') bytea luidruchtige ERROR met een hint 7.2 (2002)
SQLite (CLI 3.41+) base64(str) BLOB slaat over wat het niet kan lezen 3.41.0 (2023)
DuckDB from_base64(str) BLOB conversiefout moderne releases
ClickHouse 18.16+ base64Decode(str) String exceptie (INCORRECT_DATA) 18.16.0 (2018)
SQL Server 2025+ BASE64_DECODE(str) varbinary Msg 9803, drie states 2025
Oracle UTL_ENCODE.BASE64_DECODE(raw) RAW PL/SQL-exceptie 9i-tijdperk
Snowflake BASE64_DECODE_BINARY(str) BINARY fout, of NULL met de TRY_-variant actuele releases

Let op de vorm van de tabel: de functienaam is nooit het moeilijke deel. Het moeilijke deel is de "wanneer het kapotgaat"-kolom, want die kolom beslist of je rapport in stilte rijen verliest of dat je batch-job stopt en om hulp vraagt.

MySQL en MariaDB: de decoder die zijn schouders ophaalt

Beide servers delen het paar TO_BASE64() / FROM_BASE64(). De decoder neemt een string en geeft een binaire string terug: een reeks bytes zonder bijbehorende tekenset. Een NULL erin geeft een NULL eraf, en hier is het eerste ding om te onthouden: alles anders dat geen geldige base64 is, is ook een NULL, zonder waarschuwing. De decoder haalt zijn schouders op, en je query blijft vrolijk doorgaan.

SELECT FROM_BASE64('aGVsbG8=') AS restored;
SELECT HEX(FROM_BASE64('aGVsbG8=')) AS as_hex;
SELECT CONVERT(FROM_BASE64('aGVsbG8gd29ybGQ=') USING utf8mb4) AS as_text;

De middelste regel verdient een toelichting, want hij legt een klassiek moment van verwarring uit. De mysql-commandoregelsclient toont binaire strings standaard in hexadecimale notatie (een instelling genaamd binary-as-hex), dus een kale SELECT FROM_BASE64('aGVsbG8=') toont 0x68656C6C6F in plaats van hello. Dat is geen bug en geen corruptie; het is de client die voorzichtig is met binaire data. Wil je letters, dan converteer je met CONVERT(... USING utf8mb4) of start je de client met --binary-as-hex=0; de HEX()-aanroep in de middelste regel is de bewuste versie van de hex die de client je standaard toont.

Nu de regels die de stomme decoder hanteert. Nadat hij witruimte negeert, moeten de overgebleven tekens een veelvoud van vier vormen, moet elk teken afkomstig zijn uit het standaard alfabet (letters, cijfers, +, / en =), en mag de opvulling alleen helemaal achteraan voorkomen:

SELECT FROM_BASE64('aGVsbG8gd29ybGQ=') AS ok;
SELECT FROM_BASE64('aGVsbG8gd29ybGQ') AS missing_padding;
SELECT FROM_BASE64('!!!') AS nonsense;

Alle drie de regels draaien zonder klachten, en regels twee en drie geven NULL terug. Ontbrekende opvulling, verkeerde lengte, onbekende tekens: dezelfde schouderophaling. Witruimte is het enige genadebewijs; regeleinden, retourtekens, tabs en spaties worden allemaal genegeerd, en dat is een zegen voor alles dat eerst door een e-mail ging. Het URL-veilige alfabet krijgt in ruil dezelfde schouderophaling: een liggende streep staat niet in de standaardtabel, dus FROM_BASE64('yv7K_g==') is een NULL, al is de lengte een net veelvoud van vier. Je moet het alfabet zelf vertalen voordat je aanroept, en de URL-veilige sectie hieronder toont hoe.

Nog een eigenschap die het weten waard is: decoder en encoder zijn een gemaakt paar. De encoder hakt zijn uitvoer in regels van 76 tekens, en de decoder eet die regeleinden voor ontbijt op. Wanneer de kolom door TO_BASE64() in dezelfde databasefamilie gevuld werd, is decoderen een perfecte rondreis. Is de kolom door iets anders gevuld, dan blijf je lezen.

PostgreSQL: de decoder die zijn stem verheft

PostgreSQL draagt base64 al ten minste sinds versie 7.2, daar in 2002, in zijn kern, wat het tot de oudste base64-machine van deze familie maakt met een slanke marge. De aanroep is decode(string, 'base64'), en het resultaat is bytea, het inheemse binaire type van de database. De begeleider encode(bytea, 'base64') gaat de andere richting in en wordt hier alleen genoemd omdat de twee één gemeenschappelijk formatteringscontract delen: de RFC 2045-stijl, met regels die bij 76 tekens worden afgebroken. De decoder negeert voor zijn part retourtekens, regeleinden, spaties en tabs overal in de invoer.

SELECT decode('aGVsbG8gd29ybGQ=', 'base64') AS bytes;
SELECT length(decode('aGVsbG8gd29ybGQ=', 'base64')) AS byte_count;
SELECT convert_from(decode('aMOpbGxv', 'base64'), 'UTF8') AS text;

De derde regel is de waarnaar je steeds zult grijpen: convert_from() zet het bytea om naar tekst in een benannte codering, en dat is de tekensetstap die de binaire data nodig heeft (meer over dat onderwerp later in zijn eigen sectie). aMOpbGxv komt terug als héllo, accentteken en al.

Waar PostgreSQL zich van het veld onderscheidt, is de "wanneer het kapotgaat"-kolom. Ongeldige invoer is een harde fout, en de foutmelding vertelt je precies welke regel is geschonden:

  • een teken buiten het alfabet: ERROR: invalid symbol "!" found while decoding base64 sequence
  • een opvulteken in het midden van de string: ERROR: unexpected "=" while decoding base64 sequence
  • afgekapte invoer of ontbrekende opvulling: ERROR: invalid base64 end sequence, met de hint Input data is missing padding, is truncated, or is otherwise corrupted.
  • een URL-veilige liggende streep: ERROR: invalid symbol "_" found while decoding base64 sequence

Voor een dataputjob is die stem een feature. De query faalt, je ziet de rij, je repareert de bron. De prijs is dat één vergiftigde rij onder de miljoen de hele batch stopt, dus in productiepipelines filteren mensen vaak eerst voor met een regex voordat ze decode() aanroepen. En een kleine noot over de weergave: psql toont bytea als hex met \x-prefix, dus \x68656c6c6f is hetzelfde "hello" dat de MySQL-client toont als 0x68656C6C6F. Twee dialecten, twee hex-dialecten.

SQL Server: de late aankomst

Hier is de verrassing van de hele familie. SQL Server bracht BASE64_DECODE() uit in versie 2025, algemeen beschikbaar in november 2025. Daarvoor had de populairste database in het enterprise-land zesendertig jaar lang geen ingebouwde base64-decoder, en de folklore zat vol met omwegen. De moderne functie is een nette: hij neemt een varchar(n)- of varchar(max)-expressie en geeft een varbinary terug (een varchar(n)-expressie wordt afgebeeld op varbinary(8000), en een varchar(max)-expressie op varbinary(max)), met NULL dat er ongehinderd doorheen gaat.

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;

Die derde regel is een écht lekker detail: de decoder accepteert beide RFC 4648-alfabets, het standaard alfabet met + en / en het URL-veilige alfabet met - en _, en de opvulling is optioneel. Hij negeert ook de vier witruimtekens (regeleinde, retourteken, tab, spatie). Wanneer het toch kapotgaat, is de fout Msg 9803, Level 16 met de tekst Invalid data for type "Base64Decode", en de State-waarde vertelt je welke regel je hebt geraakt: state 20 voor een teken dat in geen enkel alfabet staat, state 21 voor tekens die allemaal geldig zijn maar in een vorm zijn gerangschikt die base64 niet kan maken, en state 23 voor opvulling die te vaak of te vroeg verschijnt.

Lig je vast op een pre-2025-versie, dan leent de klassieke omweg het XML-type, dat base64 al begreep sinds de XML Schema-tijden:

SELECT CAST(N'' AS XML)
  .value('xs:base64Binary("aGVsbG8=")', 'VARBINARY(MAX)') AS legacy;

De XML-machine base64-decodeert de constante en geeft de bytes terug. Het werkt, en het is wat een generatie SQL Server-ontwikkelaars gebruikte. Het heeft ook scherpe randen: het base64Binary-type is strikt over de vorm, dus een MIME-geraakte string met regeleinden erin wordt niet geparsed, en je betaalt de prijs van de XML-machine voor een klus die nu één functie natieverricht. Behandel het als het museumstuk dat het geworden is.

SQLite: het dialect zonder decoder

SQLite is de uitzondering, en begrijpen waarom vertelt je hoe je het gebruikt. De kernbibliotheek is een kleine, in te bedden engine, en base64 staat niet in zijn standaard functielijst. Houdt een kolom base64, dan moet de decoder uit één van vier plekken komen: de commandoregel-shell, een laadbare extensie, een aangepaste functie die door de hostapplicatie is geregistreerd, of puur SQL. Hier volgt elk van de vier.

De CLI. Vanaf versie 3.41.0 (februari 2023) bevat de sqlite3-commandoregel-shell een base64()-functie. Hij decodeert een tekstargument naar een BLOB, wat hem perfect maakt voor verkennend werk rechtstreeks vanuit een terminal:

$ sqlite3 app.db "SELECT hex(base64('aGVsbG8gd29ybGQ='));"
68656C6C6F20776F726C64

Twee temperamenten om te kennen. Ten eerste is het soepel: tekens die het niet herkent, worden overgeslagen in plaats van gemeld, dus base64('!!!') geeft een lege BLOB terug in plaats van een fout. Fijn voor nieuwsgierigheid, gevaarlijk voor een audit, want "leeg" en "ontbrekend" zien er in de uitvoer hetzelfde uit. Ten tweede is de functie vormveranderend; een BLOB-argument wordt gecodeerd naar tekst (met regels van 72 tekens), terwijl een tekstargument wordt gedecodeerd naar een BLOB. Zelfde naam, twee banen, gekozen door het type van het argument. Geen andere decoder in deze familie doet dat, dus lees je invoertype twee keer.

Puur SQL. De kernbibliotheek heeft geen base64, maar wel recursieve CTE's, rekenkunde en (sinds 3.41.0) unhex(), en dat is genoeg om een echte decoder te bouwen in een paar dozijn regels. Het recept: een alfabettabel van 64 rijen, de invoer in stukjes van vier tekens gehakt, elk stukje omgezet in een 24-bits getal, dat getal opgesplitst in drie bytes, en de bytes verzameld als hex voordat unhex() ze naar een BLOB zet. Hier is hij, werkend op een tabelkolom:

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;

Draai hem tegen een tabel met een b64-kolom en je krijgt een BLOB per rij, geen extensies, geen applicatiecode. De rekenkunde is gewoon base64 in een getalenvest: elk van de vier tekens draagt zes bits bij, de twee middelste tekens spannen zich over een bytegrens heen, en de laagste twee bits van het laatste teken worden weggegooid. Het is de traagste optie op deze pagina (een recursieve tocht plus een opzoeking per stukje), dus behoud hem voor kleine ladingen en eenmalige archeologie. Voor een langlopende applicatie is het eerlijke antwoord de derde optie: registreer een eenregelige aangepaste functie vanuit de hosttaal (de Python-module sqlite3 doet het in twee regels met create_function() en de standaard base64-module) en laat de engine hem oproepen alsof hij inheems is. De vierde optie, laadbare extensies zoals de sqlean-familie, bestaat ook, maar dat betekent een andere enginebuild installeren, wat de meeste teams liever vermijden.

DuckDB: strikt, klein, meningsvormend

DuckDB is een analytische database met een echt binair type, BLOB, en een nette familie blob-functies eromheen. De decoder is from_base64(string), en hij staat er naast zijn vriendjes to_base64(), hex(), md5() en sha256() op dezelfde referentiepagina, waar de meeste DuckDB-gebruikers hem voor het eerst tegenkomen.

SELECT from_base64('aGVsbG8gd29ybGQ=') AS bytes;
SELECT decode(from_base64('aMOpbGxv')) AS text;
SELECT hex(from_base64('AAEC')) AS padding_optional;

De derde regel toont een vriendelijker regel dan je zou verwachten: wanneer de lengte een veelvoud van vier is, is ontbrekende opvulling geen probleem, AAEC decodeert zonder moeite naar de bytes 00 01 02. De strengheid laat zich zien het moment dat de vorm fout is. DuckDB wil een lengte die een veelvoud van vier is, punt, en de conversiefout zegt precies dat:

SELECT from_base64('YWJ');
-- Conversion Error: Could not decode string "YWJ" as base64: length must be a multiple of 4

Nog twee meningen om te respecteren. Ten eerste spreekt de decoder van DuckDB alleen het standaard alfabet; een liggende streep is geen teken dat het herkent, dus URL-veilige tokens moeten worden vertaald vóór ze aankomen (het recept staat in de URL-veilige sectie). Ten tweede heeft het geen enkele tolerantie voor witruimte. Een MIME-omgebogen e-mailbijlage met haar regeleinden van 76 tekens erin faalt, en de oplossing is een replace() over regeleinden en retourtekens vóór de aanroep. En omdat er geen try_-variant is die de klap zachter maakt, is het verzachte patroon een voorkontrole in dezelfde 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 eerst, decoder daarna: de query geeft NULL terug voor alles dat onmogelijk te decoderen is, en de decoder ziet alleen maar goed gevormde invoer.

ClickHouse: de kolomdecoder

ClickHouse heeft geen apart binair type; zijn String is zonder morren binairveilig, wat betekent dat decoderen "naar een string" de hele klus is en er geen conversiestep volgt. De functie bestaat sinds versie 18.16.0 (2018) onder de naam base64Decode(), en hij houdt een MySQL-stijl alias, FROM_BASE64(), zodat geporte queries niet herschreven hoeven te worden.

SELECT base64Decode('aGVsbG8gd29ybGQ=') AS text;
SELECT tryBase64Decode('definitely not base64') AS gentle;
SELECT base64URLDecode('aHR0cHM6Ly9jbGlja2hvdXNlLmNvbQ') AS url;

De tweede regel is de huishoudstijl van ClickHouse in actie. De engine houdt van zijn try-prefix: tryBase64Decode() slaakt de mislukking en geeft een lege string terug, terwijl kale base64Decode() een exceptie gooit met de code INCORRECT_DATA en een melding die de overtredende waarde noemt. Kies de kale vorm wanneer een slechte rij de pipeline moet stoppen en de try-vorm wanneer het rapport moet doorgaan, en kies het bewust, niet per ongeluk.

Twee versienoten, omdat ClickHouse snel beweegt. Vóór 26.7 werd witruimte in de invoer geweigerd; vanaf 26.7 worden spatie, tab, line feed, retourteken en form feed allemaal genegeerd, en dat is het gedrag dat je wilt voor alles dat met een e-mail of een teksteditor in aanraking is gekomen. En de moderne decoder verwacht behoorlijke opvulling op zijn groepen van vier tekens, dus een token dat onderweg zijn equals-tekens kwijt is, wordt een exceptie in plaats van een best mogelijke poging. Wanneer een query die in 2023 werkte in 2026 gaat gooien, kijk dan eerst naar de serverversie voordat je de data de schuld geeft.

Oracle: RAW of niets

De base64-machine van Oracle woont in het UTL_ENCODE PL/SQL-pakket, en het heeft een eigen karakter: het neemt RAW en geeft RAW terug, niets anders. Geen tekst erin, geen tekst eraf. VARCHAR2 is tekendata met een tekenset; RAW is kale bytes; en het pakket weigert anders te doen. Het werkende patroon is dus een drie-laags sandwich: cast naar raw, decodeer, cast terug naar tekst:

SELECT UTL_RAW.CAST_TO_VARCHAR2(
         UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW('aGVsbG8gd29ybGQ='))
       ) AS restored
FROM DUAL;

Elke stap verdient zijn plek. UTL_RAW.CAST_TO_RAW() interpreert de bytes van de tekst opnieuw als raw (in de tekenset van de database, die bij een moderne installatie doorgaans AL32UTF8 is, zodat je UTF-8-invoer zonder verwerking doorkomt). UTL_ENCODE.BASE64_DECODE() doet het échte werk. En UTL_RAW.CAST_TO_VARCHAR2() interpreert de resultaatbytes opnieuw als tekst in diezelfde databasetekenset. Sla een laag over en je krijgt een type-mismatchfout, wat Oracle zijn werk van expliciet zijn doet.

Ongeldige invoer roept een PL/SQL-exceptie op in plaats van een stille NULL, dus een batch-decodering thuishoort in een exceptiebehandelaar die de overtredende rij logt. Het pakket draagt ook een heel museum aan zusterdecoders: MIME-headerdecodering, quoted-printable, uudecode, text encoding, allemaal uit hetzelfde tijdperk. Je gebruikt vooral het base64-paar, maar de buren verklaren waarom het pakket zo is georganiseerd: Oracle wilde één thuis voor "data in een transportkostuum".

Eén grootteval om te kennen vóór je begint. In puur SQL is een RAW-waarde gekapt op 2000 bytes, dus een base64-waarde die decodeert naar meer dan zo'n 1500 raw bytes kan met één enkel SELECT-statement helemaal niet gedecodeerd worden. Grotere ladingen hebben een PL/SQL-loop nodig die de BLOB in blokken van 2000 (of minder) bytes afloopt, elk stukje decodeert en de resultaten weer aan elkaar naait. Het is old-school, maar het is het standaard Oracle-antwoord, en het is één van die plekken waar het typesysteem uit de jaren negentig van de taal je queries uit de jaren twintig nog steeds vorm geeft.

Snowflake: neem je eigen alfabet mee

Snowflake scheidt zijn binaire type (BINARY) van zijn teksttypes, en het geeft je de meest configureerbare decoder van deze familie. Het werkpaard is BASE64_DECODE_BINARY(input), die BINARY teruggeeft, en het optionele tweede argument is een korte string die het alfabet opnieuw definieert:

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;

Lees dat alfabetargument zorgvuldig, want het is positiegebonden. Er zijn tot drie tekens toegestaan: de eerste twee overschrijven alfabetposities 62 en 63 (de standaardwaarden zijn + en /), en het derde overschrijft het opvulteken (standaard =). Om "gebruik het URL-veilige alfabet" te zeggen, geef je '-_' door. Om "URL-veilig alfabet maar vul op met % in plaats daarvan" te zeggen, moet je alle drie de tekens doorgeven, '-_%', al is het enige dat je écht wilt veranderen het opvulteken. Laat tekens weg en je behoudt de standaardwaarden; je kunt geen positie overslaan en de volgende vullen.

Twee begeleiders maken het gezelschap compleet. BASE64_DECODE_STRING() doet de decodering en de tekstconversie in één aanroep, zodat je de TO_VARCHAR() kunt overslaan wanneer de lading tekst is. En de TRY_-varianten, TRY_BASE64_DECODE_BINARY() en TRY_BASE64_DECODE_STRING(), geven bij een slechte waarde NULL terug in plaats van een fout op te roepen, wat de Snowflake-versie is van de try-vorm van ClickHouse.

Van bytes naar tekst: de tekensetstap

Decoderen geeft je bytes. Is de lading een document, een naam, een JSON-fragment, dan verschuldigd je hem nog één stap: een interpretatie als tekst in een benoemde tekenset. Hier komt "het is gedecodeerd maar ziet er verkeerd uit" vandaan, want een reeks bytes wordt pas woorden zodra je zegt in welke taal van bytes je leest. De tabel is kort en het onthouden waard:

Dialect Van bytes naar tekst Ongeldige reeksen
MySQL / MariaDB CONVERT(bin USING utf8mb4) hergeïnterpreteerd; afval erin, afval eraf
PostgreSQL convert_from(bytes, 'UTF8') roept een fout op
SQL Server CAST(bin AS VARCHAR) verliesrijk, afhankelijk van de collatie
Oracle UTL_RAW.CAST_TO_VARCHAR2(raw) hergeïnterpreteerd in de databasetekenset
DuckDB decode(blob) conversiefout
ClickHouse niet nodig; String is de tekst n/v
Snowflake TO_VARCHAR(bin, 'UTF-8') roept een fout op
SQLite CAST(blob AS TEXT) geen enkele validatie

Het verschil is grof, en dat is opzet. PostgreSQL en DuckDB valideren en weigeren, wat je dowaartse code beschermt tegen mojibake. MySQL en Oracle herinterpreteren in stilte, wat snel is maar betekent dat de database je niet kan redden van een Latin-1-lading die in een UTF-8-wereld arriveert. SQLite kijkt er niet eens naartoe, want in SQLite is een TEXT-waarde gewoon bytes met een label. De praktische regel: beslis de tekenset vóór dat je decodeert, schrijf hem in de query als een letterlijke waarde, en test met een lading die een niet-ASCII-teken bevat (het klassieke aMOpbGxv voor héllo is een goede canaria, want het breekt anders in elke verkeerde tekenset). Voor écht binaire ladingen sla deze sectie helemaal over en laat de bytes bytes blijven.

JWTs: drie punten base64 in een kolom

JSON Web Tokens zijn de meest voorkomende base64 die je in een database zult aantreffen, omdat authenticatiegebeurtenissen samen met hun tokens worden gelogd. Een JWT is drie puntgescheiden stukken: een header, een payload en een handtekening. De eerste twee zijn JSON-objecten, verpakt als base64, en hier is de draai die mensen te pakken neemt: JWTs gebruiken het URL-veilige alfabet zonder opvulling, niet de standaard vorm met opvulling. Een / zou een nieuw padsegment starten op plaatsen waar tokens vaak reizen, een + zou in een query string als spatie gelezen worden, en de opvullende equals-tekens waren pure ceremonie, dus de specificatie (RFC 7515 en RFC 7519) schakelde over op - en _ en liet de opvulling vallen.

Decoderen van een token in SQL is daarom een vierstapendans: splitsen op de punten, de URL-veilige tekens terugwisselen naar het standaard alfabet, de opvulling herstellen, decoderen en het JSON parseren. PostgreSQL, met zijn JSONB-type, is een comfortabele plek om het te doen:

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;

Het resultaat is een JSONB-waarde die je kunt opvragen als elke andere kolom, en voor het token hierboven komt het terug als {"iat": 1516239022, "sub": "1234567890", "name": "Dev User"}. Een enkel claim uit het resultaat halen is dan gewoon claims->>'sub' in een vervolgquery. Het herstellen van de opvulling is de CASE-expressie: een base64url-string waarvan de lengte twee tekens te kort is bij een veelvoud van vier, heeft twee equals-tekens nodig, drie tekens te kort er één, en een exact veelvoud geen.

Ga nog een stap verder en je kunt in SQL zelfs een HS256-handtekening verifiëren, met de pgcrypto-extensie van PostgreSQL voor de HMAC (schakel hem een keer in met CREATE EXTENSION IF NOT EXISTS pgcrypto; als hij nog niet staat). Bereken de handtekening opnieuw over header.payload met het gedeelde geheim, formatteer hem op dezelfde base64url-manier en vergelijk:

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;

De hmac()-aanroep produceert de digest, encode(..., 'base64') pakt hem in, en de drie string-operaties geven hem de URL-veilige vorm zonder opvulling die het token draagt. Voor het token en het geheim hierboven is het antwoord een vrolijke t. Houd de kanttekeningen wel bij je: dit werkt alleen voor HMAC-algoritmen (HS256, HS384, HS512), het steekt een gedeeld geheim in een database-statement, en het is gebouwd voor rapportage, auditing en debugging. Alles wat écht toegang reguleert zou in de applicatielaag moeten verifiëren met een échte JWT-bibliotheek.

Data-URLs: de afbeelding binnen een string

Het data-URL-formaat (RFC 2397) is de manier van het web om een bestand in een link in te lijnen: data:image/png;base64, gevolgd door de base64 van het bestand. Browsers plakken ze vanuit het klembord, single-page apps verwerken er kleine afbeeldingen in, en elke stroom komt uiteindelijk in een database-kolom terecht als een lange tekstwaarde. Het formaat is data:{media type}[;{parameters}][;base64],{data}, en het enige deel dat voor het decoderen uitmaakt is alles na de eerste komma, want daar begint de base64-lading.

SELECT uri,
       CAST(FROM_BASE64(SUBSTRING(uri, LOCATE(',', uri) + 1)) AS BINARY) AS png_bytes
FROM uploads
WHERE uri LIKE 'data:image/png;base64,%';

Dat is de hele klus in MySQL: de komma vinden, er langs gaan, decoderen, en je houdt de beeldbytes vast in een binaire expressie die je in een BLOB-kolom kunt opslaan of in een hash voor deduplicatie. Andere dialecten wisselen de functies (SUBSTR() en INSTR() in de meeste, substring() en position() in de overige), maar de vorm is identiek.

Drie waarschuwingen. Ten eerste is niet elke data-URL base64; een data-URL zonder de ;base64-marker draagt in plaats daarvan percent-gecodeerde tekst, en die naar een base64-decoder voeden is een fout waar de LIKE-filter hierboven voor bedoeld is. Ten tweede is het mediatype in de prefix een claim, geen feit; dezelfde string kan image/png zeggen en een JPEG bevatten. Als de inhoud er toe doet, controleer dan de magische bytes van het gedecodeerde resultaat (PNG begint met 89 50 4E 47, JPEG met FF D8). Ten derde zijn data-URLs groot. Een foto van 4 megapixels wordt een string van ruwweg 5,5 megabyte, en dat is een discussie over kolomgrootte en geheugen, niet over string-functies.

URL-veilige Base64: het alfabet dat reist

Sectie 5 van RFC 4648 definieerde een tweede alfabet voor base64, omdat het oorspronkelijke alfabet twee tekens heeft met taken in URL-syntaxis. Het plus-teken is de manier waarop query parameters waarden optellen, de slash is de manier waarop paden worden gescheiden, en het equals-teken van de opvulling wordt percent-gecodeerd in het moment dat het een query string tegenkomt. De URL-veilige variant wisselt + in voor - en / in voor _ (beide onschuldig in URLs), en de JWT-specificatie laat er bovenop de opvulling helemaal uit. Het resultaat reist door links, padsegmenten, bestandsnamen en fragmentidentifiers zonder één percent-teken.

Je zult het in een database vooral tegenkomen omdat tokens en links werden bewaard, niet omdat de data daar is geboren. Hier is wie het natiever kan verwerken en wie de twee minuten handleiding nodig heeft:

Dialect Inheemse URL-veilige decodering Notities
SQL Server 2025+ BASE64_DECODE() accepteert beide alfabets geen enkele vertaling nodig
ClickHouse 24.6+ base64URLDecode() accepteert + en / nog steeds ook
Snowflake BASE64_DECODE_BINARY(s, '-_') alfabet als positioneel argument
MySQL / MariaDB geen vertaal de tekens, verwacht NULL bij falen
PostgreSQL geen vertaal de tekens, verwacht een fout bij falen
Oracle geen vertaal de tekens vóór de RAW-cast
DuckDB geen (wijst de liggende streep af) vertaal de tekens, houd de lengte een veelvoud van 4
SQLite CLI geen vertaal de tekens; de decoder slaat over wat hij niet kent

De handleiding is twee REPLACE()-aanroepen plus het herstellen van de opvulling, en het is op elk dialect hetzelfde. In PostgreSQL leest het zo:

SELECT convert_from(
         decode(replace(replace('aGVsbG8', '-', '+'), '_', '/')
           || CASE MOD(LENGTH('aGVsbG8'), 4)
                 WHEN 2 THEN '=='
                 WHEN 3 THEN '='
                 ELSE '' END,
           'base64'),
         'UTF8') AS text;

Wissel - terug naar +, wissel _ terug naar /, voeg de ontbrekende opvulling toe op basis van de lengte modulo vier, en vanaf daar neemt de standaard decoder over. De invoer aGVsbG8 (de URL-veilige vorm zonder opvulling van "hello") komt terug als het woord zelf. De twee fouten die blijven voorkomen, zijn de die de CASE-expressie voorkomt: de opvulling vergeten, waardoor strenge decoders een lengte weigeren die geen veelvoud van vier is, en de tekenvertaling overslaan, waardoor een decoder die het URL-veilige alfabet niet kent op de liggende streep vastloopt. Schrijf de vertaling één keer, als herbruikbare functie in je database, en het hele probleem stopt met terugkeren.

Bestanden, blobs en grote dingen

Decoderen is de manier waarop bestanden uit kolommen komen, en elk dialect heeft een iets andere uitgang. In DuckDB is de rondreis twee statements: één om een bestand in een BLOB te lezen en één om gedecodeerde bytes weer naar buiten te schrijven:

SELECT filename, octet_length(content) AS size
 FROM read_blob('/data/uploads/*.png');

De leeszijde: read_blob() is een tafelfunctie die een bestandsnaam, een lijst namen of een glob-patroon accepteert en per bestand een filename- en een content-kolom teruggeeft. De schrijfzijde is een statement op zich: COPY in het BLOB-formaat schrijft kale bytes, geen quotes, geen escaping, precies wat een gedecodeerde lading wil.

COPY (SELECT from_base64(b64) FROM attachments WHERE id = 42)
 TO '/data/restored/cat.png' (FORMAT BLOB);

De uitgang van PostgreSQL is de large object-API. Een large object is een serverkantse binaire blok-opslag die via een OID wordt aangesproken, en lo_export() schrijft er één naar een bestand op de database-server. Het vereist supergebruikersrechten of de pg_write_server_files-machtiging, en de bestemming moet een pad zijn dat het serverproces kan schrijven, dus in de praktijk is het een taak voor onderhoudsscripts in plaats van applicatiecode:

SELECT lo_export(12345, '/tmp/attachments/cat.png');

MySQL heeft alleen de restrictieve SELECT ... INTO DUMPFILE-uitgang (één rij, serverkantse pad, FILE-machtiging), en SQL Server heeft helemaal geen puur-SQL-bestandsschrijver (naar schijf schrijven is een taak van de client of agent, via zijn exportgereedschap), en dat is een eerlijk ontwerp: de database bewaart de bytes, de applicatie beslist waar het bestand thuishoort. SQLite zit aan het andere eind van het spectrum, waar de applicatie de host is en een BLOB-kolom in één aanroep van de hosttaal rechtstreeks naar schijf kan worden geschreven.

Dan zijn er nog de plafonds, die meer verschillen dan je zou verwachten van databases die allemaal doen alsof ze gelijk zijn:

Dialect Binair type Praktisch plafond
PostgreSQL bytea 1 GB per waarde
MySQL / MariaDB BLOB-familie max_allowed_packet (standaard 64 MB in MySQL 8)
SQL Server varbinary(max) 2 GB per waarde
Oracle RAW / BLOB RAW: 2000 bytes in SQL, BLOB: 4 GB met PL/SQL-blokkering
SQLite BLOB wat het bestand en het geheugen toelaten
DuckDB BLOB zeer groot; geheugen en schijf beslissen
ClickHouse String kolomgrootte is virtueel, rijen zijn de eenheid
Snowflake BINARY standaard 8 MB per waarde (kale BINARY-kolommen); tot 64 MB met een expliciete BINARY(N)

De MySQL-rij verdient een verhaal, want het is de die mensen in productie verbaast. max_allowed_packet kap de grootte van één pakket tussen client en server, en een base64-string is deel van dat pakket. Een foto van 50 megabyte, gecodeerd naar base64, is een string van zo'n 67 megabyte, groter dan de standaard van 64 megabyte, en het resultaat is geen fout die je in de query kunt lezen: het is een afgekapt of NULL-waarde die eruitziet als datacorruptie. Verhuis je grote bestanden door een MySQL-kolom, dan controleer dan die limiet vóór je begint, en onthoud dat de gecodeerde vorm, niet de kale bytes, ertegen meetelt.

E-mail-omwikkelingen en MIME-regels

Elke base64 die het e-mailsysteem heeft overleefd, draagt een souvenir: regeleinden. MIME, het stel normen dat e-mail binaire bijlagen laat dragen (RFC 2045, sectie 6.8), wikkelt base64-uitvoer om bij 76 tekens en eindigt de regels met een retourteken en een regeleinde. De omwikkeling bestaat omdat het oude e-mailnetwerk geen regels langer dan dat kon vertrouwen, en het formaat is sindsdien uit gewoonte doorgevoerd. Een bijlage in een database-kolom is dan vaak een base64-string met om de 76 tekens een regeleinde, en de relatie van je decoder met die regeleinden beslist of de klus één statement is of twee.

Decoder Eet de omwikkeling op? Zo niet
MySQL / MariaDB FROM_BASE64() ja -
PostgreSQL decode() ja -
SQL Server BASE64_DECODE() ja -
SQLite CLI base64() ja -
ClickHouse 26.7+ ja -
ClickHouse vóór 26.7 nee verwijder eerst witruimte
DuckDB from_base64() nee verwijder eerst witruimte
Oracle UTL_ENCODE.BASE64_DECODE() nee verwijder witruimte in de PL/SQL-laag

De "eerst verwijderen"-oplossing is één expressie, en hij is altijd veilig, want witruimte hoort niet bij het base64-alfabet: een legitieme lading kan geen spatie, tab of regeleinde bevatten, dus het verwijderen ervan kan geen informatie vernietigen. In PostgreSQL is het idioom een enkele regexp_replace():

SELECT decode(regexp_replace(attachment_b64, '\s', '', 'g'), 'base64')
FROM email_attachments;

Elk witruimte-teken, regeleinden inbegrepen, verdwijnt, en de decoder ziet één schone ononderbroken string. Draai dit in DuckDB (met zijn replace() over de twee regeleinde-tekens) of in een pre-26.7 ClickHouse, en de omwikkelde bijlage decodeert exact als de ongewikkelde.

API-ladingen, configs en auth-headers

Stap terug van de individuele functies en er verschijnt een patroon: base64 in een database-kolom is bijna altijd één van drie dingen. Een veld binnen een JSON-document (een afbeelding, een certificaat, een bestand dat een API besloot in te lijnen). Een configuratiewaarde (een geheim of een credentieel dat een tool liever in base64 heeft, omdat base64 op één regel van een YAML-bestand past zonder quotes, zonder regeleinden en zonder backslashes). Of een authenticatie-artefact (een Basic auth-header, een bewaard token, een sessie-blob). Hier is elk met zijn decoderingsvorm.

JSON-velden. Het JSON arriveerde als tekst, het veld is een string, en de base64 verstekt zich erin. Extracteer het veld met de JSON-functie van je dialect, en decodeer daarna. In MySQL is de hele keten één expressie:

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 doet hetzelfde met JSONB, waar het veld als tekst uitkomt met de ->>-operator en decode() overneemt. De JSON_TYPE-wacht op de laatste regel is belangrijker dan hij lijkt: hij houdt de decoder uit de buurt van rijen waar het veld een getal is, een genest object of ontbreekt, en in MySQL zouden die rijen anders een stille NULL bijdragen aan je telling van "hoeveel events een afbeelding hadden".

Authenticatie-headers. Een Basic auth-header is de letterlijke string Basic gevolgd door de base64 van username:password. Het decoderen in SQL is een substring en een split, en precies daarom doen mensen het (meestal om te controleren welke gebruikers welke endpoints raakten, niet om het wachtwoord te verifiëren, dat de database nooit in de open text moet zien):

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) schilt de Basic -prefix af, de decoder herstelt de oorspronkelijke tekst, en de twee SUBSTRING_INDEX()-aanroepen splitsen hem bij de dubbele punt, eerste deel voor de gebruiker, laatste deel voor het geheim. In PostgreSQL gebruikt dezelfde query substring() en split_part().

Configuratiewaarden. De decoderingsrichting hier is de auditklus: iemand bewaarde een geheim als base64 in een config-tabel (een gewoonte geërfd van Kubernetes, waar geheimen in rust base64 zijn), en je wilt zien wat er écht in zit, of je bouwt de export die een nieuwe omgeving gaat consumeren. De vorm is één SELECT per waarde, en de tekensetstap geldt als de waarde tekst is:

SELECT name,
       CONVERT(FROM_BASE64(value) USING utf8mb4) AS plaintext
FROM app_config
WHERE name LIKE '%_secret%';

Behandel dat resultaat met de zorg die het verdient. Je hebt zojuist bewaarde geheimen omgezet naar zichtbare query-uitvoer; zorg dat het account dat de query draait de rechten heeft die het zou moeten, dat het resultaat niet in een log wordt gekopieerd, en dat de base64-in-config-gewoonte een tweede blik krijgt. Base64 is een vervoer, geen kluis, en een audit-query is het moment waarop dat zichtbaar wordt.

De valkuilen die bijten

Elke valkuil op deze lijst is er één die in minstens één codebase een halve dag heeft gekost, en elk van hen is specifiek voor de manier waarop SQL-dialecten base64 verwerken, niet voor base64 zelf.

  • De stille NULL. MySQL en MariaDB decoderen slechte invoer naar NULL zonder klacht. In een rapport dat join't op de gedecodeerde waarde verdwijnen die rijen simpelweg, en het verschil tussen "0 rijen" en "0 rijen omdat er 14 vergiftigd waren" is onzichtbaar totdat iemand vraagt waarom de telling niet klopt. Is je decoder het stille type, dan tel je NULLs bewust.
  • De veelvoud-van-vier-regel, ongelijk toegepast. Een string waarvan de lengte geen veelvoud van vier is, is geen base64, maar de dialecten zijn het niet eens over wat je moet doen: PostgreSQL roept een fout op, DuckDB roept een conversiefout op, ClickHouse gooit een exceptie, MySQL geeft NULL terug, en de SQLite CLI decodeert in stilte wat het kan. Hetzelfde databestand levert vijf verschillende uitkomsten op op vijf databases, en daarom is "het werkte in Postgres" geen test.
  • De alfabet-mismatch. Een URL-veilig token (JWT, link, bestandsnaam) die naar een decoder met standaard alfabet gevoerd wordt: SQL Server accepteert het, base64URLDecode() van ClickHouse accepteert het, Snowflake accepteert het met het juiste argument, en de rest geeft of NULL terug, roept een fout op of, in het geval van de SQLite CLI, gooit de liggende streep in stilte weg en geeft je de verkeerde bytes. Het geval van de verkeerde bytes is het ongemakkelijke, want het resultaat lijkt aannemelijk.
  • De MIME-omwikkeling. Omwikkelde invoer naar een decoder die geen regeleinden eet (DuckDB, pre-26.7 ClickHouse, Oracle) faalt, en het falen lijkt vaak op "de laatste 76 tekens zijn afval" in plaats van "er zit een regeleinde in", omdat de fout wijst naar het teken na de breuk.
  • De weergavetruc. De mysql-client toont binair als hex, psql toont bytea als \x-hex, Snowflake toont BINARY als hex, en Oracle toont RAW als hex. Vier clients, vier hex-notaties, één zeer menselijke fout van het concluderen dat de data is beschadigd omdat het scherm getallen toont. Converteer altijd expliciet voordat je het resultaat met je ogen leest.
  • Opvulling op de verkeerde plek. Een equals-teken is alleen legaal aan het einde, er één of twee. Een string als YQ==BQ== is twee geldige groepen in één kostuum, en de strenge decoders wijzen hem af terwijl de soepele hem decoderen naar iets wat niemand heeft gevraagd. Zie je ooit opvulling in het midden van een bewaarde waarde, dan is de encoder die hem schreef kapot, en het repareren van de data is een eenmalige klus.
  • De tekenset-verrassing. Het decoderen slaagt, de tekst komt terug, en de accenten zijn verkeerd. De bytes waren prima; de interpretatie niet. Dit is de CONVERT(... USING latin1) die utf8mb4 had moeten zijn, de CAST(bin AS VARCHAR) die liep onder een collatie die ongeldige reeksen slikt, de CAST(blob AS TEXT) in SQLite die nooit controleert. Speld de tekenset vast als een letterlijke waarde in de query en test met een geaccentueerde canaria.
  • De plafonds. De 2000-byte RAW-limiet van Oracle in SQL-statements, max_allowed_packet van MySQL dat de gecodeerde grootte belaste, het 1-GB bytea-plafond van PostgreSQL, de 8-MB standaard BINARY-lengte van Snowflake. Elk is gedocumenteerd, elk ontdekt in productie, en elk is een groottecontrole die je had kunnen schrijven vóór de data groot was.
  • Het vertrouwen in de gedecodeerde bytes. Base64 kan alles dragen, inclusief een string vol quotes. Decoderen is niet onschadelijk maken. Wat je ook doet met de gedecodeerde tekst (vergelijk hem, log hem, koppel hem samen in een ander statement), hij heeft nog steeds de gebruikelijke bescherming nodig, en een geparametriseerde query blijft een geparametriseerde query na een base64-rondreis.

Zo blijf je aan de juiste kant

  • Beslis eerst het type, niet eerst de functie. Is de lading binair of tekst? Binair gaat naar BLOB/bytea/varbinary en blijft daar. Tekst gaat door de tekensetstap met een expliciete codering. De helft van alle base64-pijn in SQL is een binaire lading die in een tekstkolom is beland (of andersom) en nu wordt geïnterpreteerd.
  • Valideer voordat je decodeert, of decodeer verzacht. Een regex over het alfabet plus een lengte-modulo-vier-check kost niets en verandert een batch-stoppende fout in een NULL die je kunt tellen. Waar het dialect een try-vorm aanbiedt (tryBase64Decode van ClickHouse, TRY_BASE64_DECODE_BINARY van Snowflake), gebruik die voor rapportage en behoud de strenge vorm voor pipelines die niet mogen gokken.
  • Controleer de versie van het dialect, niet alleen van de database. ClickHouse 26.7 veranderde de witruimtebehandeling, SQL Server 2025 is de eerste release met de functie helemaal, de SQLite CLI heeft 3.41 nodig, en de opvulling-verwachtingen van ClickHouse werden in de tijd strenger. "Het is ClickHouse" is geen specificatie; "het is ClickHouse 24.8" wel.
  • Documenteer het alfabet van elke kolom. Een kolom die zowel standaard als URL-veilige base64 kan bevatten, is een kolom die de volgende ontwikkelaar zal verwarren. Komt de data uit JWTs, zeg dat dan in de schema-commentaar; komt ze uit MIME-bijlagen, zeg ook dat. Het decoderkeuze is een eigenschap van de kolom, niet van de query.
  • Bewaar bytes, codeer aan de rand. Bestuur je het schema, dan wint een BLOB-kolom plus codering in de API-laag het van een base64-tekstkolom voor opslag, voor indexering en voor elke toekomstige query. Base64 in de kolom is een compatibiliteitsbelasting, en belastingen betaal je het liefst één keer, aan de grens.
  • Rondreis maken met een canaria. Vóór je een nieuwe decoderoute vertrouwt, stuur een bekende lading door encode en decode in dezelfde database en vergelijk. De canaria moet een niet-ASCII-teken bevatten (om de tekensetstap te doorlopen), een lengte die een opvulstaart laat (om de opvulregels te doorlopen) en, voor URL-veilige routes, ergens een - of _ (om de alfabetvertaling te doorlopen).
  • Houd geheimen uit de query-tekst. JWT-verificatie met pgcrypto steekt een gedeeld geheim in het statement; config-audits leggen open text-geheimen in het resultaat. Beide zijn legitieme klussen, maar ze verdienen een beperkt account, een schoon log en een review, niet een productie-verbindingsreeks en een SELECT * INTO OUTFILE.

Een korte geschiedenis van uitpakken in SQL

Het base64-formaat zelf is ouder dan het nuttige deel van het internet. Het werd in de midden jaren negentig gestandaardiseerd voor MIME (RFC 2045, sectie 6.8, die RFC 1521 verouderde, de MIME-medelijvclichaam-specificatie van 1993 die de codering droeg), en de naam is gewoon een telling: het alfabet heeft 64 tekens. De URL-veilige variant arriveerde met RFC 4648 in 2006, en de JWT-specificatie van 2015 maakte van die variant de die je écht ziet in token-kolommen. Maar de databases ontmoetten het formaat elk op hun eigen schema, en het schema zegt iets over de ziel van elk.

2002. PostgreSQL 7.2 noemt base64 al als eerste-klasse formaat van encode() en decode() - gelijktijdig met UTL_ENCODE van Oracle uit het 9i-tijdperk, en de oudste base64-ondersteuning van deze familie met een slanke marge. Een database met een echt binair type en een formaatargument kwam er vroeg, omdat het antwoord één enum-waarde ver weg was.

Begin 2000s. Het UTL_ENCODE-pakket van Oracle verschijnt in het 9i-tijdperk, met base64 naast MIME-header, quoted-printable en uuecode-functies. Het is RAW erin en RAW eraf, wat zeer Oracle is, en het heeft die vorm een kwarteeuw behouden.

2013. MySQL 5.6 voegt TO_BASE64() en FROM_BASE64() toe, en MariaDB 10.0 neemt beide mee in de fork. Het paar codeert met regels van 76 tekens en decodeert met witruimtetolerantie, een gematcht stel dat in een dozijn grote versies niet is veranderd.

2018. ClickHouse 18.16 brengt base64Decode() met zijn MySQL-stijl alias, omdat de kolomaire wereld workloads importeerde die base64 al in hun log-schemas droegen.

2023. SQLite 3.41.0 voegt base64() en zijn base85-zus toe aan de commandoregel-shell als applicatie-gedefinieerde functies. De kernbibliotheek krijgt, trouw aan de vorm, niets; de shell, waar mensen écht aan SQLite-databases prikkelen, krijgt het gereedschap.

2025. SQL Server 2025, algemeen beschikbaar in november 2025, voegt BASE64_DECODE() en BASE64_ENCODE() toe aan T-SQL na een afwezigheid van zesendertig jaar. De release notes behandelen ze als een bescheiden functie; de gemeenschap behandelt ze als een redding.

Het patroon is helder zodra je het ziet. Databases met een écht binair type en een formaatargument (PostgreSQL, en op zijn manier Oracle) kregen base64 de dag dat de behoefte duidelijk was. De rest (MySQL, SQL Server) behandelde het als een string-handigheid en plande het dienovereenkomstig in. En de in te bedden engine (SQLite) beschouwt het nog steeds als het werk van de hostapplicatie, met de CLI als vriendelijke uitzondering.

Dingen die je een glimlach bezorgen

  • SQL Server bracht 1989 tot 2025 door zonder base64-decoder, en het antwoord van de gemeenschap was een XML-functie genaamd xs:base64Binary() binnen een CAST(N'' AS XML). Hele generaties enterprise-queries decodeerden tokens door de XML-parser, omdat de XML-parser base64 al sinds 2001 begreep en de SQL-engine niet.
  • De base64() van de SQLite CLI is de enige vormveranderaar in deze familie: geef hem een BLOB en hij codeert, geef hem tekst en hij decodeert. De functie wisselt van baan op basis van het type van zijn argument, wat een kleine daad van SQL-telepathie is en een échte val voor de onvoorzichtige.
  • De encoder van PostgreSQL wikkelt om bij 76 tekens, exact zoals de MIME-norm van 1996, behalve dat hij de regels eindigt met een eenzame regeleinde in plaats van de retourteken-en-regeleinde van de norm. Twintig jaar na de specificatie, één teken minder. De decoder negeert beide, dus de opstand is onzichtbaar tenzij je de uitvoer diff't.
  • In de mysql-client toont SELECT FROM_BASE64('aGVsbG8=') 0x68656C6C6F. Niet omdat de data hex is, en niet omdat er iets mis is, maar omdat de client, in jouw plaats, besloot dat binaire strings als hex getoond moeten worden. De instelling heet binary-as-hex, en hij heeft duizenden ontwikkelaars ervan overtuigd dat hun decoder kapot is.
  • Het SQL-niveau RAW-type van Oracle is gekapt op 2000 bytes, dus een certificaat van 3 kilobyte past zelfs niet in een SQL-statement als RAW-literal. De decodering moet in PL/SQL gebeuren, in blokken, met een loop. De limiet dateert uit de jaren negentig; de loop is nog steeds het aanbevolen antwoord.
  • Snowflake toont BINARY-waarden als hex in elke resultaatset, dus een perfect geslaagde decodering van "hello" arriveert op je scherm als 68656C6C6F. Twee dialecten, twee hex-weergaven, één identiek gevoel van ongemak.
  • ClickHouse houdt de alias FROM_BASE64() naast zijn inheemse base64Decode(), een kleine beleefdheid voor de MySQL-refugees die arriveerden met queries die anders niet zouden draaien.
  • De hele familie deelt één stil feit: base64 is een belasting van 33 procent op weg naar buiten en een restitutie van 25 procent op weg naar binnen, en geen van de acht decoders hier zal je dat vertellen zonder gevraagd te worden. Het formaat is een kostuum; de garderobe is gratis; het maatwerk is waar dit artikel over gaat.

Ga verder

Dit artikel ging over het aftrekken van de vermomming: de functie in elk dialect, zijn temperament, en de ladingen (JWTs, data-URLs, omwikkelde e-mail, JSON-velden, config-waarden, auth-headers) die hem dragen. De andere richting is een dier op zich, met zijn eigen stel verrassingen: welke encoders hun uitvoer omwikken bij 76 tekens en welke niet, hoe je de URL-veilige vorm zonder opvulling maakt die tokens verwachten, de groottemath die je kolombreedte beslist, en wat de 36-jaargap van SQL Server betekent voor wie nog op een oudere versie zit. Alles samen, van TO_BASE64() tot BASE64_ENCODE(), staat gedetailleerd in het gerelateerde artikel over Base64-coderen in SQL, gelinkt vanaf deze pagina. Decodeer hier, codeer daar, en de hele rondreis past in één namiddag.

Laatst bijgewerkt: 2026-10-06

Gerelateerd artikel: Base64-codering in SQL: een complete gids