Você tem que lidar com o formato Base64? Então esse site é perfeito para você! Use o nosso ferramenta online super útil para codificar ou decodificar os seus dados.

Decodificação Base64 em SQL: um guia completo

Abra qualquer banco de dados de produção por tempo suficiente e você vai se deparar com o disfarce. Um avatar que chegou como uma parede de letras dentro de uma exportação JSON. Um JWT estacionado numa coluna varchar ao lado de um ID de usuário. Um certificado que alguém decidiu enviar como string porque o formato de transferência não tinha binário. Em algum lugar de uma tabela, seus dados estão vestindo letras, e seu trabalho é tirá-las sem sair do banco de dados.

Em SQL, esse trabalho tem uma propriedade muito reconfortante: uma vez que você sabe qual decodificador o seu dialeto fala, o trabalho inteiro se reduz a uma única chamada de função. O formato em si já está explicado em detalhe na página inicial (64 caracteres imprimíveis, cada grupo de quatro representando três bytes de entrada, até dois sinais = preenchendo o grupo final), então este artigo pula a palestra. Duas coisas para levar com você: o base64 é uma forma de vestir bytes como texto, não um cadeado, e decodificar é a direção em que os dados ficam menores (de volta a três quartos do tamanho codificado), que é exatamente o oposto do que sua coluna de armazenamento foi dimensionada para aguentar. A história real é que o SQL é uma família de dialetos, e cada membro chama o seu decodificador por um nome diferente e reage a entradas ruins com um temperamento completamente distinto. Este artigo é o passeio.

O elenco dos decodificadores

Aqui está quem está de plantão, e como cada um se comporta quando a entrada é lixo. A coluna "quando quebra" importa, porque um decodificador que falha alto no staging e em silêncio em produção é exatamente o caminho pelo qual avatares perdidos chegam ao ambiente real:

Dialeto A chamada O que volta Quando quebra Desde quando
MySQL 8.x / MariaDB 10.x FROM_BASE64(str) string binária NULL silenciosa MySQL 5.6 (2013)
PostgreSQL decode(str, 'base64') bytea ERROR alto, com dica 7.2 (2002)
SQLite (CLI 3.41+) base64(str) BLOB pula o que não consegue ler 3.41.0 (2023)
DuckDB from_base64(str) BLOB erro de conversão versões recentes
ClickHouse 18.16+ base64Decode(str) String exceção (INCORRECT_DATA) 18.16.0 (2018)
SQL Server 2025+ BASE64_DECODE(str) varbinary Msg 9803, três estados 2025
Oracle UTL_ENCODE.BASE64_DECODE(raw) RAW exceção PL/SQL era 9i
Snowflake BASE64_DECODE_BINARY(str) BINARY erro, ou NULL com a variante TRY_ versões atuais

Repare na forma da tabela: o nome da função nunca é a parte difícil. A parte difícil é a coluna "quando quebra", porque é ela que decide se o seu relatório perde linhas em silêncio ou se o seu job em lote para e pede ajuda.

MySQL e MariaDB: o decodificador que dá de ombros

Os dois servidores compartilham o par TO_BASE64() / FROM_BASE64(). O decodificador recebe uma string e devolve uma string binária: uma sequência de bytes sem nenhum conjunto de caracteres anexado. Um NULL que entra dá um NULL que sai, e aqui está a primeira coisa para memorizar: tudo o mais que não seja base64 válido também é um NULL, sem nenhum aviso. O decodificador dá de ombros, e a sua consulta segue andando tranquilamente.

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

A linha do meio merece um comentário, porque ela explica um momento clássico de confusão. O cliente de linha de comando mysql imprime strings binárias em notação hexadecimal por padrão (uma configuração chamada binary-as-hex), então um simples SELECT FROM_BASE64('aGVsbG8=') mostra 0x68656C6C6F em vez de hello. Isso não é um bug nem corrupção; é o cliente sendo cauteloso com dados binários. Se você quer letras, converta com CONVERT(... USING utf8mb4) ou inicie o cliente com --binary-as-hex=0; a chamada HEX() na linha do meio é a versão deliberada do hex que o cliente mostra por padrão.

Agora as regras que o decodificador silencioso aplica. Depois de ignorar espaços em branco, os caracteres restantes devem formar um múltiplo de quatro, cada caractere deve vir do alfabeto padrão (letras, dígitos, +, / e =), e o preenchimento só pode aparecer bem no final:

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

As três linhas rodam sem reclamar, e as linhas dois e três devolvem NULL. Preenchimento faltando, tamanho errado, caracteres estrangeiros: o mesmo gesto de dar de ombros. Espaço em branco é a única indulgência; quebras de linha, retornos de carro, tabulações e espaços são todos ignorados, o que é uma bênção para qualquer coisa que tenha passado antes por um e-mail. O alfabeto URL-safe, por outro lado, recebe o gesto de ombros em troca: um sublinhado não está na tabela padrão, então FROM_BASE64('yv7K_g==') é um NULL mesmo que o tamanho seja um múltiplo limpo de quatro. Você precisa traduzir o alfabeto por sua conta antes de chamar, e a seção de URL-safe abaixo mostra como.

Um traço a mais que vale conhecer: o decodificador e o codificador são um par feito um para o outro. O codificador quebra a saída dele em linhas de 76 caracteres, e o decodificador come essas quebras de linha no café da manhã. Se uma coluna foi preenchida por TO_BASE64() nesta mesma família de bancos de dados, decodificar é uma ida e volta perfeita. Se foi preenchida por outra coisa, continue lendo.

PostgreSQL: o decodificador que levanta a voz

O PostgreSQL carrega base64 no núcleo desde pelo menos a versão 7.2, lá em 2002, o que o torna o mecanismo de base64 mais antigo desta família por uma margem curta. A chamada é decode(string, 'base64'), e o resultado é bytea, o tipo binário nativo do banco de dados. O companheiro encode(bytea, 'base64') vai na direção oposta e é mencionado aqui só porque os dois compartilham um único contrato de formatação: o estilo RFC 2045, com linhas quebradas em 76 caracteres. O decodificador, por sua vez, ignora retornos de carro, quebras de linha, espaços e tabulações em qualquer lugar da entrada.

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

A terceira linha é aquela que você vai buscar constantemente: convert_from() transforma o bytea em texto numa codificação nomeada, e é o passo do conjunto de caracteres que os dados binários precisam (mais sobre isso na própria seção adiante). aMOpbGxv volta como héllo, acento e tudo.

Onde o PostgreSQL se separa do resto da família é na coluna "quando quebra". Entrada inválida é um erro duro, e a mensagem de erro diz exatamente qual regra foi violada:

  • um caractere fora do alfabeto: ERROR: invalid symbol "!" found while decoding base64 sequence
  • um sinal de preenchimento no meio da string: ERROR: unexpected "=" while decoding base64 sequence
  • entrada truncada ou preenchimento faltando: ERROR: invalid base64 end sequence, com a dica Input data is missing padding, is truncated, or is otherwise corrupted.
  • um sublinhado URL-safe: ERROR: invalid symbol "_" found while decoding base64 sequence

Para um trabalho de limpeza de dados, essa voz é um recurso. A consulta falha, você vê a linha, você corrige a fonte. A troca é que uma linha envenenada em um milhão para o lote inteiro, então em pipelines de produção as pessoas frequentemente pré-filtram com uma regex antes de chamar decode(). E uma nota rápida de exibição: o psql imprime bytea como hex com prefixo \x, então \x68656c6c6f é o mesmo "hello" que o cliente do MySQL mostra como 0x68656C6C6F. Dois dialetos, dois dialetos de hex.

SQL Server: o que chegou atrasado

Aqui está a surpresa de toda a família. O SQL Server lançou o BASE64_DECODE() na versão 2025, disponível em geral em novembro de 2025. Antes disso, o banco de dados mais popular no mundo corporativo passou trinta e seis anos sem nenhum decodificador base64 embutido, e o folklore era denso de soluções alternativas. A função moderna é uma coisa limpa: ela recebe uma expressão varchar(n) ou varchar(max) e devolve um varbinary (uma expressão varchar(n) vira varbinary(8000), e uma expressão varchar(max) vira varbinary(max)), com NULL passando reto.

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;

A terceira linha é um toque genuinamente bom: o decodificador aceita os dois alfabetos do RFC 4648, o padrão com + e / e o URL-safe com - e _, e o preenchimento é opcional. Ele também ignora os quatro caracteres de espaço em branco (quebra de linha, retorno de carro, tabulação, espaço). Quando ele quebra, o erro é Msg 9803, Level 16 com o texto Invalid data for type "Base64Decode", e o valor de State diz qual regra você atingiu: estado 20 para um caractere que não está em nenhum dos alfabetos, estado 21 para caracteres que são todos válidos mas dispostos numa forma que o base64 não consegue fazer, e estado 23 para preenchimento que aparece com frequência demais ou cedo demais.

Se você está preso numa versão anterior a 2025, a solução alternativa clássica empresta o tipo XML, que entende base64 desde os tempos do XML Schema:

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

O mecanismo XML decodifica a constante em base64 e devolve os bytes. Funciona, e foi o que uma geração de desenvolvedores de SQL Server usou. Também tem arestas: o tipo base64Binary é estrito quanto à forma, então uma string com quebras MIME e linhas quebradas no interior não será analisada, e você está pagando o preço do mecanismo XML por um trabalho que uma única função agora faz nativamente. Trate-a como a peça de museu que se tornou.

SQLite: o dialeto sem decodificador

O SQLite é o fora da curva, e entender por quê diz como usá-lo. A biblioteca núcleo é um motor pequeno e embutível, e base64 não está na sua lista padrão de funções. Se uma coluna guarda base64, o decodificador tem que vir de um de quatro lugares: o shell de linha de comando, uma extensão carregável, uma função custom registrada pelo aplicativo hospedeiro, ou SQL puro. Aqui está cada um.

O CLI. A partir da versão 3.41.0 (fevereiro de 2023), o shell de linha de comando sqlite3 traz uma função base64(). Ela decodifica um argumento de texto para um BLOB, o que a torna perfeita para trabalho exploratório direto de um terminal:

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

Dois temperamentos para saber. Primeiro, ela é tolerante: caracteres que ela não reconhece são pulados em vez de relatados, então base64('!!!') devolve um BLOB vazio em vez de um erro. Ótimo para curiosidade, perigoso para auditoria, porque "vazio" e "faltando" se parecem no resultado. Segundo, a função é um camaleão; um argumento BLOB é codificado para texto (com linhas de 72 caracteres), enquanto um argumento de texto é decodificado para um BLOB. O mesmo nome, dois trabalhos, escolhidos pelo tipo do argumento. Nenhum outro decodificador desta família faz isso, então leia o tipo da sua entrada duas vezes.

SQL puro. A biblioteca núcleo não tem base64, mas tem CTEs recursivas, aritmética e (desde a 3.41.0) unhex(), o que é suficiente para construir um decodificador de verdade em algumas dezenas de linhas. A receita: uma tabela de alfabeto com 64 linhas, a entrada cortada em pedaços de quatro caracteres, cada pedaço virando um número de 24 bits, esse número dividido em três bytes, e os bytes coletados como hex antes de unhex() transformá-los num BLOB. Aqui está, funcionando numa coluna de tabela:

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;

Rode contra uma tabela com uma coluna b64 e você ganha um BLOB por linha, sem extensões, sem código de aplicação. A aritmética é base64 puro de roupa inteira: cada um dos quatro caracteres contribui seis bits, os dois caracteres do meio atravessam um limite de byte, e os dois bits baixos do último caractere são descartados. É a opção mais lenta desta página (uma passagem recursiva mais uma consulta por pedaço), então guarde-a para payloads pequenos e arqueologia pontual. Para um aplicativo de longa duração, a resposta honesta é a terceira opção: registrar uma função custom de uma linha a partir da linguagem hospedeira (o módulo sqlite3 do Python faz isso em duas linhas com create_function() e o módulo padrão base64) e deixar o motor chamá-la como uma nativa. A quarta opção, extensões carregáveis como a família sqlean, também existe, mas significa instalar um build diferente do motor, o que a maioria das equipes prefere evitar.

DuckDB: estrito, pequeno e opinativo

O DuckDB é um banco de dados analítico com um tipo binário de verdade, BLOB, e uma família arrumada de funções de blob ao redor dele. O decodificador é from_base64(string), e ele fica do lado dos amigos to_base64(), hex(), md5() e sha256() na mesma página de referência, que é onde a maioria dos usuários de DuckDB se depara com ele pela primeira vez.

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

A terceira linha mostra uma regra mais amigável do que você talvez esperasse: quando o tamanho é um múltiplo de quatro, o preenchimento faltando não é problema, AAEC decodifica para os bytes 00 01 02 perfeitamente bem. O rigor aparece no momento em que a forma está errada. O DuckDB quer um tamanho que seja um múltiplo de quatro, ponto final, e o erro de conversão diz exatamente isso:

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

Mais duas opiniões para respeitar. Primeira: o decodificador do DuckDB só fala o alfabeto padrão; um sublinhado não é um caractere que ele reconhece, então tokens URL-safe precisam ser traduzidos antes de chegarem (a receita está na seção de URL-safe). Segunda: ele não tem nenhuma tolerância para espaço em branco. Um anexo de e-mail com quebras MIME e suas linhas de 76 caracteres no interior vai falhar, e a correção é um replace() sobre quebras de linha e retornos de carro antes da chamada. E como não existe variante try_ para amolecer o golpe, o padrão gentil é uma verificação prévia na mesma consulta:

SELECT CASE
         WHEN b64 ~ '^[A-Za-z0-9+/]*={0,2}$'
          AND MOD(LENGTH(b64), 4) = 0
         THEN from_base64(b64)
       END AS maybe_bytes
FROM attachments;

Regex primeiro, decodificador depois: a consulta devolve NULL para qualquer coisa que não possa de jeito nenhum ser decodificada, e o decodificador só enxerga entrada bem-formada.

ClickHouse: o decodificador de coluna

O ClickHouse não tem um tipo binário separado; o String dele é felizmente seguro para binário, o que significa que decodificar "para uma string" é o trabalho inteiro e nenhum passo de conversão vem depois. A função está por aí desde a versão 18.16.0 (2018) com o nome base64Decode(), e ela mantém um alias no estilo MySQL, FROM_BASE64(), para que consultas portadas não precisem de reescrita.

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

A segunda linha é o estilo da casa do ClickHouse em ação. O motor ama seu prefixo try: tryBase64Decode() engole a falha e devolve uma string vazia, enquanto o base64Decode() puro lança uma exceção com o código INCORRECT_DATA e uma mensagem nomeando o valor culpado. Escolha a forma pura quando uma linha ruim deve parar o pipeline e a forma try quando o relatório deve seguir em frente, e escolha deliberadamente, não por acidente.

Duas notas de versão, porque o ClickHouse anda rápido. Antes da 26.7, espaço em branco na entrada era rejeitado; da 26.7 em diante, espaço, tabulação, line feed, retorno de carro e form feed são todos ignorados, que é o comportamento que você quer para qualquer coisa que tenha tocado um e-mail ou um editor de texto. E o decodificador moderno espera o preenchimento certo nos grupos de quatro caracteres, então um token que perdeu os seus sinais de igual no caminho vai virar uma exceção em vez de um esforço de boa-fé. Se uma consulta que funcionava em 2023 começar a lançar erros em 2026, olhe a versão do servidor antes de culpar os dados.

Oracle: RAW ou nada

O mecanismo de base64 do Oracle mora no pacote PL/SQL UTL_ENCODE, e ele tem uma personalidade própria: ele recebe RAW e devolve RAW, nada mais. Sem texto por dentro, sem texto por fora. VARCHAR2 é dados de caractere com um conjunto de caracteres; RAW é bytes puros; e o pacote recusa fingir o contrário. Então o padrão que funciona é um sanduíche de três camadas: cast para raw, decodificar, cast de volta para texto:

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

Cada passo ganha o seu lugar. UTL_RAW.CAST_TO_RAW() reinterpreta os bytes do texto como raw (no conjunto de caracteres do banco de dados, que numa instalação moderna costuma ser AL32UTF8, então a sua entrada UTF-8 viaja como está). UTL_ENCODE.BASE64_DECODE() faz o trabalho de verdade. E UTL_RAW.CAST_TO_VARCHAR2() reinterpreta os bytes do resultado como texto naquele mesmo conjunto de caracteres do banco. Pule qualquer camada e você ganha um erro de incompatibilidade de tipo, que é o Oracle fazendo o seu trabalho de ser explícito.

Entrada inválida levanta uma exceção PL/SQL em vez de um NULL silencioso, então uma decodificação em lote deve morar dentro de um handler de exceção que registre a linha culpada. O pacote também carrega um museu inteiro de decodificadores primos: decodificação de cabeçalho MIME, quoted-printable, uudecode, codificação de texto, todos da mesma era. Você vai usar o par base64 na maior parte do tempo, mas os vizinhos explicam por que o pacote é organizado do jeito que é: o Oracle queria uma casa única para "dados vestindo um figurino de transporte".

Uma armadilha de tamanho para saber antes de começar. Em SQL puro, um valor RAW é limitado a 2000 bytes, então um valor base64 que decodifica para mais do que uns 1500 bytes raw não pode ser decodificado com uma única instrução SELECT, nem de longe. Payloads maiores precisam de um loop PL/SQL que percorre o BLOB em pedaços de 2000 (ou menos) bytes, decodifica cada pedaço e costura os resultados de volta. É antiga escola, mas é a resposta padrão do Oracle, e é um daqueles lugares onde o sistema de tipos dos anos 1990 da linguagem ainda molda as suas consultas dos anos 2020.

Snowflake: traga seu próprio alfabeto

O Snowflake separa o seu tipo binário (BINARY) dos seus tipos de texto, e entrega o decodificador mais configurável desta família. O trabalho de cavalo é o BASE64_DECODE_BINARY(input), que devolve BINARY, e o segundo argumento opcional é uma string curta que redefine o 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;

Leia aquele argumento de alfabeto com cuidado, porque ele é posicional. Até três caracteres são permitidos: os dois primeiros sobrescrevem as posições 62 e 63 do alfabeto (os padrões são + e /), e o terceiro sobrescreve o caractere de preenchimento (padrão =). Para dizer "use o alfabeto URL-safe" você passa '-_'. Para dizer "alfabeto URL-safe mas preencha com % em vez disso" você precisa passar os três caracteres, '-_%', mesmo que a única coisa que você realmente quer mudar seja o caractere de preenchimento. Omita caracteres e você mantém os padrões; você não pode pular uma posição e preencher a próxima.

Dois companheiros completam o conjunto. BASE64_DECODE_STRING() faz a decodificação e a conversão para texto numa única chamada, então você pode pular o TO_VARCHAR() quando o payload é texto. E as variantes TRY_, TRY_BASE64_DECODE_BINARY() e TRY_BASE64_DECODE_STRING(), devolvem NULL num valor ruim em vez de levantar um erro, que é a versão do Snowflake da forma try do ClickHouse.

De bytes para texto: o passo do conjunto de caracteres

Decodificar te entrega bytes. Se o payload é um documento, um nome, um fragmento JSON, você deve a ele um passo a mais: uma interpretação como texto num conjunto de caracteres nomeado. É daí que vem o "decodificou mas parece errado", porque uma sequência de bytes só vira palavras quando você diz qual linguagem de bytes está lendo. A tabela é curta e vale a pena memorizar:

Dialeto De bytes para texto Sequências inválidas
MySQL / MariaDB CONVERT(bin USING utf8mb4) reinterpretado; lixo por dentro, lixo por fora
PostgreSQL convert_from(bytes, 'UTF8') levanta um erro
SQL Server CAST(bin AS VARCHAR) com perda, dependendo da colação
Oracle UTL_RAW.CAST_TO_VARCHAR2(raw) reinterpretado no conjunto de caracteres do banco
DuckDB decode(blob) erro de conversão
ClickHouse nada necessário; String é o texto n/d
Snowflake TO_VARCHAR(bin, 'UTF-8') levanta um erro
SQLite CAST(blob AS TEXT) nenhuma validação

A variação é larga de propósito. PostgreSQL e DuckDB validam e recusam, o que protege o seu código a jusante do mojibake. MySQL e Oracle reinterpretam em silêncio, o que é rápido mas significa que o banco não consegue te salvar de um payload Latin-1 chegando num mundo UTF-8. O SQLite nem olha, porque no SQLite um valor TEXT é apenas bytes com uma etiqueta. A regra prática: decida o conjunto de caracteres antes de decodificar, escreva-o na consulta como literal, e teste com um payload que contenha um caractere não-ASCII (o clássico aMOpbGxv para héllo é um bom canário, porque ele quebra de um jeito diferente em cada conjunto de caracteres errado). Para payloads genuinamente binários, pule esta seção inteira e mantenha os bytes como bytes.

JWTs: três pontos de base64 numa coluna

JSON Web Tokens são o base64 mais comum que você vai encontrar parado num banco de dados, porque eventos de autenticação ficam registrados com os seus tokens. Um JWT é três pedaços separados por pontos: um cabeçalho, um payload e uma assinatura. Os dois primeiros são objetos JSON empacotados como base64, e aqui está a reviravolta que pega as pessoas: JWTs usam o alfabeto URL-safe sem preenchimento, não a forma padrão com preenchimento. Um / começaria um novo segmento de caminho onde tokens frequentemente viajam, um + seria lido como espaço numa query string, e os sinais de igual de preenchimento seriam pura cerimônia, então a especificação (RFC 7515 e RFC 7519) trocou para - e _ e descartou o preenchimento.

Decodificar um token em SQL é, portanto, uma dança de quatro passos: dividir nos pontos, trocar os caracteres URL-safe de volta para o alfabeto padrão, restaurar o preenchimento, decodificar e analisar o JSON. O PostgreSQL, com o seu tipo JSONB, é um lugar confortável para fazer isso:

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;

O resultado é um valor JSONB que você pode consultar como qualquer outra coluna, e para o token acima ele volta como {"iat": 1516239022, "sub": "1234567890", "name": "Dev User"}. Puxar um único claim do resultado é então apenas claims->>'sub' numa consulta de acompanhamento. A restauração do preenchimento é a expressão CASE: uma string base64url cujo tamanho fica dois à curta de um múltiplo de quatro precisa de dois sinais de igual, três à curta precisa de um, e um múltiplo exato não precisa de nenhum.

De um passo adiante e você pode até verificar uma assinatura HS256 em SQL, usando a extensão pgcrypto do PostgreSQL para o HMAC (ative uma vez com CREATE EXTENSION IF NOT EXISTS pgcrypto; se ela ainda não estiver instalada). Recalcule a assinatura sobre header.payload com o segredo compartilhado, formate-a da mesma forma base64url, e compare:

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;

A chamada hmac() produz o digest, encode(..., 'base64') o empacota, e as três operações de string o remodelam para a forma URL-safe sem preenchimento que o token carrega. Para o token e o segredo acima, a resposta é um alegre t. Guarde os avisos com você, no entanto: isso só funciona para algoritmos HMAC (HS256, HS384, HS512), coloca um segredo compartilhado dentro de uma instrução de banco de dados, e foi feito para relatórios, auditoria e depuração. Qualquer coisa que de fato controle o acesso deve verificar na camada de aplicação com uma biblioteca JWT de verdade.

Data URLs: a imagem dentro de uma string

O formato de data URL (RFC 2397) é a forma da web de embutir um arquivo num link: data:image/png;base64, seguido do base64 do arquivo. Navegadores colam eles da área de transferência, apps de página única embutem pequenas imagens neles, e cada um desses fluxos acaba caindo numa coluna de banco de dados como um valor de texto longo. O formato é data:{media type}[;{parameters}][;base64],{data}, e a única parte que importa para decodificar é tudo o que vem depois da primeira vírgula, porque é ali que o payload base64 começa.

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

Esse é o trabalho inteiro no MySQL: achar a vírgula, passar além dela, decodificar, e você segura os bytes da imagem numa expressão binária que pode guardar numa coluna BLOB ou fazer hash para deduplicação. Outros dialetos trocam as funções (SUBSTR() e INSTR() na maioria, substring() e position() em outras) mas a forma é idêntica.

Três avisos. Primeiro, nem todo data URL é base64; um data URL sem o marcador ;base64 carrega texto percent-encoded em vez disso, e alimentá-lo com um decodificador base64 é um erro que o filtro LIKE acima está ali para impedir. Segundo, o tipo de mídia no prefixo é uma afirmação, não um fato; a mesma string pode dizer image/png e conter um JPEG. Se o conteúdo importa, confira os bytes mágicos do resultado decodificado (PNG começa com 89 50 4E 47, JPEG com FF D8). Terceiro, data URLs são grandes. Uma foto de 4 megapixels vira aproximadamente uma string de 5,5 megabytes, o que é uma conversa de tamanho de coluna e de memória, não de função de string.

Base64 URL-safe: o alfabeto que viaja

A seção 5 do RFC 4648 definiu um segundo alfabeto para base64 porque o original tem dois caracteres com funções na sintaxe de URL. O sinal de mais é como os parâmetros de query somam valores, a barra é como os caminhos são separados, e o sinal de igual do preenchimento é percent-encoded no momento em que encontra uma query string. A variante URL-safe troca + por - e / por _ (os dois inofensivos em URLs), e a especificação de JWT por cima disso ainda descarta o preenchimento inteiramente. O resultado viaja por links, segmentos de caminho, nomes de arquivo e identificadores de fragmento sem um único sinal de percent.

Você vai encontrá-lo num banco de dados principalmente porque tokens e links foram guardados, não porque os dados nasceram lá. Aqui está quem consegue lidar com isso nativamente e quem precisa do manual de dois minutos:

Dialeto Decodificação URL-safe nativa Observações
SQL Server 2025+ BASE64_DECODE() aceita os dois alfabetos nenhuma tradução necessária
ClickHouse 24.6+ base64URLDecode() ainda aceita + e / também
Snowflake BASE64_DECODE_BINARY(s, '-_') alfabeto como argumento posicional
MySQL / MariaDB nenhuma traduza os caracteres, espere NULL em caso de falha
PostgreSQL nenhuma traduza os caracteres, espere um erro em caso de falha
Oracle nenhuma traduza os caracteres antes do cast para RAW
DuckDB nenhuma (rejeita o sublinhado) traduza os caracteres, mantenha o tamanho múltiplo de 4
SQLite CLI nenhuma traduza os caracteres; o decodificador pula o que não conhece

O manual é duas chamadas REPLACE() mais a restauração do preenchimento, e é o mesmo em todos os dialetos. No PostgreSQL ele se lê assim:

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

Troque - de volta para +, troque _ de volta para /, acrescente o preenchimento faltando com base no tamanho módulo quatro, e o decodificador padrão assume a partir daí. A entrada aGVsbG8 (a forma URL-safe sem preenchimento de "hello") volta como a própria palavra. Os dois erros que insistem em acontecer são exatamente os que a expressão CASE evita: esquecer o preenchimento, o que faz decodificadores estritos rejeitarem um tamanho que não é múltiplo de quatro, e pular a tradução dos caracteres, o que faz um decodificador que não conhece o alfabeto URL-safe engasgar no sublinhado. Escreva a tradução uma vez, como uma função reutilizável no seu banco de dados, e o problema inteiro para de reincidir.

Arquivos, blobs e coisas grandes

Decodificar é como os arquivos saem das colunas, e cada dialeto tem uma porta de saída um pouco diferente. No DuckDB a ida e volta é duas instruções, uma para ler um arquivo num BLOB e outra para escrever os bytes decodificados de volta:

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

O lado de leitura: read_blob() é uma função de tabela que aceita um nome de arquivo, uma lista de nomes ou um padrão glob e devolve uma coluna filename e uma coluna content por arquivo. O lado de escrita é a sua própria instrução: COPY com o formato BLOB grava bytes crus, sem aspas, sem escapes, exatamente o que um payload decodificado quer.

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

A porta de saída do PostgreSQL é a API de large objects. Um large object é um armazém de pedaços binários no lado do servidor endereçado por um OID, e lo_export() grava um deles num arquivo no servidor do banco de dados. É preciso privilégio de superusuário ou o privilégio pg_write_server_files, e o destino deve ser um caminho para o qual o processo do servidor consegue gravar, então na prática é um trabalho para scripts de manutenção em vez de código de aplicação:

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

O MySQL só tem o escapamento restrito SELECT ... INTO DUMPFILE (linha única, caminho no lado do servidor, privilégio FILE), e o SQL Server não tem nenhum gravador de arquivos em SQL puro (gravar no disco é um trabalho do cliente ou do agent, via as suas ferramentas de exportação), o que é um desenho justo: o banco de dados guarda os bytes, a aplicação decide onde o arquivo mora. O SQLite fica na outra ponta do espectro, onde a aplicação é a hospedeira e uma coluna BLOB pode ser gravada direto no disco com uma chamada da linguagem hospedeira.

Depois vêm os tetos, que diferem mais do que você esperaria de bancos de dados que todos fingem ser iguais:

Dialeto Tipo binário Teto prático
PostgreSQL bytea 1 GB por valor
MySQL / MariaDB família BLOB max_allowed_packet (padrão de 64 MB no MySQL 8)
SQL Server varbinary(max) 2 GB por valor
Oracle RAW / BLOB RAW: 2000 bytes em SQL, BLOB: 4 GB com chunking em PL/SQL
SQLite BLOB o que o arquivo e a memória permitirem
DuckDB BLOB muito grande; memória e disco decidem
ClickHouse String o tamanho da coluna é virtual, as linhas são a unidade
Snowflake BINARY 8 MB por valor por padrão (colunas BINARY puras); até 64 MB com um BINARY(N) explícito

A linha do MySQL merece uma história, porque é ela que surpreende as pessoas em produção. max_allowed_packet limita o tamanho de um único pacote entre cliente e servidor, e uma string base64 faz parte desse pacote. Uma foto de 50 megabytes codificada em base64 é aproximadamente uma string de 67 megabytes, o que é maior que o padrão de 64 megabytes, e o resultado não é um erro que você consiga ler na consulta: é um valor truncado ou NULL que parece corrupção de dados. Se você está movendo arquivos grandes por uma coluna do MySQL, confira esse limite antes de começar, e lembre-se que é a forma codificada, não os bytes crus, que conta contra ele.

Quebras de e-mail e linhas MIME

Qualquer base64 que sobreviveu ao sistema de e-mail carrega um souvenir: quebras de linha. O MIME, o conjunto de padrões que permite que o e-mail carregue anexos binários (RFC 2045, seção 6.8), quebra a saída base64 em 76 caracteres e termina as linhas com um retorno de carro e um line feed. A quebra existe porque a rede de e-mail antiga não podia confiar em linhas mais longas do que isso, e o formato seguiu em frente por hábito desde então. Então um anexo guardado numa coluna de banco de dados é frequentemente uma string base64 com uma quebra de linha a cada 76 caracteres, e a relação do seu decodificador com essas quebras de linha decide se o trabalho é uma instrução ou duas.

Decodificador Come a quebra? Se não
MySQL / MariaDB FROM_BASE64() sim -
PostgreSQL decode() sim -
SQL Server BASE64_DECODE() sim -
SQLite CLI base64() sim -
ClickHouse 26.7+ sim -
ClickHouse antes da 26.7 não remova espaços em branco primeiro
DuckDB from_base64() não remova espaços em branco primeiro
Oracle UTL_ENCODE.BASE64_DECODE() não remova espaços em branco na camada PL/SQL

O conserto "remover primeiro" é uma única expressão, e é sempre seguro, porque espaço em branco não faz parte do alfabeto base64: nenhum payload legítimo pode conter um espaço, tabulação ou quebra de linha, então removê-los não pode destruir informação. No PostgreSQL o idioma é um único regexp_replace():

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

Todo caractere de espaço em branco, quebras de linha incluídas, vai embora, e o decodificador vê uma string limpa e contínua. Rode isso no DuckDB (com o replace() dele sobre os dois caracteres de quebra de linha) ou num ClickHouse anterior à 26.7, e o anexo quebrado decodifica exatamente como o que não foi quebrado.

API payloads, configs e cabeçalhos de autenticação

Passando um passo para trás das funções individuais, um padrão aparece: base64 numa coluna de banco de dados é quase sempre uma de três coisas. Um campo dentro de um documento JSON (uma imagem, um certificado, um arquivo que uma API decidiu embutir). Um valor de config (um segredo ou credencial que alguma ferramenta prefere em base64, porque base64 cabe numa linha de um arquivo YAML sem aspas, sem quebras de linha e sem barras invertidas). Ou um artefato de autenticação (um cabeçalho de auth Basic, um token guardado, um blob de sessão). Aqui está cada um com a sua forma de decodificação.

Campos JSON. O JSON chegou como texto, o campo é uma string, e o base64 está escondido dentro dela. Extraia o campo com a função JSON do seu dialeto, depois decodifique. No MySQL a cadeia inteira é uma expressão:

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';

O PostgreSQL faz o mesmo com JSONB, onde o campo sai como texto com o operador ->> e o decode() assume. A guarda JSON_TYPE na última linha importa mais do que parece: ela mantém o decodificador longe das linhas onde o campo é um número, um objeto aninhado ou falta, e no MySQL essas linhas de outra forma contribuiriam com um NULL silencioso para a sua contagem de "quantos eventos tinham uma imagem".

Cabeçalhos de autenticação. Um cabeçalho de auth Basic é a string literal Basic seguida do base64 de username:password. Decodificá-lo em SQL é um substring e um split, que é exatamente o motivo pelo qual as pessoas fazem isso (normalmente para auditar quais usuários atingiram quais endpoints, não para verificar a senha, que o banco de dados nunca deveria ver em texto puro):

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) descasca o prefixo Basic , o decodificador restaura o texto original, e as duas chamadas SUBSTRING_INDEX() dividem no caractere de dois-pontos, a primeira parte para o usuário, a última parte para o segredo. No PostgreSQL a mesma consulta usa substring() e split_part().

Valores de config. A direção de decodificação aqui é o trabalho de auditoria: alguém guardou um segredo como base64 numa tabela de config (um hábito herdado do Kubernetes, onde valores de segredo ficam em base64 em repouso), e você quer ver o que está de verdade lá dentro, ou está construindo a exportação que um novo ambiente vai consumir. A forma é um SELECT por valor, e o passo do conjunto de caracteres se aplica se o valor for texto:

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

Trate esse resultado com o cuidado que ele merece. Você acabou de transformar segredos guardados em saída de consulta visível; certifique-se de que a conta rodando a consulta tem os direitos que deve, de que o resultado não é copiado para um log, e de que o hábito de base64-em-config ganha uma segunda olhada. Base64 é um transporte, não um cofre, e uma consulta de auditoria é o momento em que isso fica óbvio.

As armadilhas que mordem

Toda armadilha desta lista é uma que já custou uma tarde em ao menos um codebase, e todas elas são específicas da maneira como os dialetos SQL tratam base64, não do base64 em si.

  • A NULL silenciosa. MySQL e MariaDB decodificam entrada ruim para NULL sem reclamar. Num relatório que faz join sobre o valor decodificado, essas linhas simplesmente somem, e a diferença entre "0 linhas" e "0 linhas porque 14 delas estavam envenenadas" é invisível até alguém perguntar por que a contagem não fecha. Se o seu decodificador é do tipo calado, conte as suas NULLs de propósito.
  • A regra do múltiplo de quatro, aplicada desigualmente. Uma string cujo tamanho não é múltiplo de quatro não é base64, mas os dialetos não concordam sobre o que fazer: o PostgreSQL levanta um erro, o DuckDB levanta um erro de conversão, o ClickHouse levanta uma exceção, o MySQL devolve NULL, e o CLI do SQLite decodifica em silêncio o que consegue. O mesmo arquivo de dados produz cinco resultados diferentes em cinco bancos de dados, que é por isso que "funcionou no Postgres" não é um teste.
  • A incompatibilidade de alfabeto. Um token URL-safe (JWT, link, nome de arquivo) alimentado a um decodificador de alfabeto padrão: o SQL Server aceita, o base64URLDecode() do ClickHouse aceita, o Snowflake aceita com o argumento certo, e todos os outros ou devolvem NULL, ou levantam um erro, ou, no caso do CLI do SQLite, descarta o sublinhado em silêncio e te entrega os bytes errados. O caso dos bytes errados é o mais chatinho, porque o resultado parece plausível.
  • A quebra MIME. Entrada quebrada num decodificador que não come quebras de linha (DuckDB, ClickHouse pré-26.7, Oracle) falha, e a falha muitas vezes parece "os últimos 76 caracteres são lixo" em vez de "tem uma quebra de linha aqui dentro", porque o erro aponta para o caractere depois da quebra.
  • O truque da exibição. O cliente mysql imprime binário como hex, o psql imprime bytea como hex \x, o Snowflake imprime BINARY como hex, e o Oracle imprime RAW como hex. Quatro clientes, quatro notações hex, um erro muito humano de concluir que os dados estão corrompidos porque a tela mostra números. Converta explicitamente sempre antes de ler o resultado com os olhos.
  • Preenchimento no lugar errado. O sinal de igual só é legal no final, um ou dois deles. Uma string como YQ==BQ== é dois grupos válidos vestindo um figurino só, e os decodificadores estritos rejeitam enquanto os tolerantes decodificam para uma coisa que ninguém pediu. Se você um dia vir preenchimento no meio de um valor guardado, o codificador que o escreveu está quebrado, e consertar os dados é um trabalho pontual.
  • A surpresa do conjunto de caracteres. A decodificação tem sucesso, o texto volta, e os acentos estão errados. Os bytes estavam bons; a interpretação não estava. É o CONVERT(... USING latin1) que deveria ter sido utf8mb4, o CAST(bin AS VARCHAR) que rodou sob uma colação que engole sequências inválidas, o CAST(blob AS TEXT) no SQLite que nunca confere. Fixe o conjunto de caracteres como literal na consulta e teste com um canário acentuado.
  • Os tetos. O limite de 2000 bytes de RAW do Oracle em instruções SQL, o max_allowed_packet do MySQL taxando o tamanho codificado, o teto de 1 GB de bytea do PostgreSQL, o comprimento padrão de 8 MB de BINARY do Snowflake. Cada um está documentado, cada um é descoberto em produção, e cada um é uma verificação de tamanho que você poderia ter escrito antes dos dados ficarem grandes.
  • Confiar nos bytes decodificados. Base64 pode carregar qualquer coisa, incluindo uma string cheia de aspas. Decodificar não é sanitizar. O que você fizer com o texto decodificado (compará-lo, registrá-lo num log, concatená-lo em outra instrução) ainda precisa das proteções de sempre, e uma consulta parametrizada continua sendo uma consulta parametrizada depois de uma ida e volta em base64.

Como ficar do lado certo

  • Decida o tipo primeiro, não a função primeiro. O payload é binário ou texto? Binário vai para BLOB/bytea/varbinary e fica lá. Texto passa pelo passo do conjunto de caracteres com uma codificação explícita. Metade de toda a dor base64 em SQL é um payload binário que entrou por engano numa coluna de texto (ou vice-versa) e agora está sendo interpretado.
  • Valide antes de decodificar, ou decodifique com gentileza. Uma regex sobre o alfabeto mais um teste de tamanho módulo quatro não custa nada e transforma um erro que para o lote num NULL que você pode contar. Onde o dialeto oferece uma forma try (o tryBase64Decode do ClickHouse, o TRY_BASE64_DECODE_BINARY do Snowflake), use-a para relatórios e guarde a forma estrita para pipelines que não podem adivinhar.
  • Confira a versão do dialeto, não só do banco de dados. O ClickHouse 26.7 mudou o tratamento de espaço em branco, o SQL Server 2025 é o primeiro release com a função de fato, o CLI do SQLite precisa da 3.41, e as expectativas de preenchimento do ClickHouse apertaram com o tempo. "É ClickHouse" não é uma especificação; "é ClickHouse 24.8" é.
  • Documente o alfabeto de cada coluna. Uma coluna que pode guardar base64 padrão e URL-safe é uma coluna que vai confundir o próximo desenvolvedor. Se os dados vêm de JWTs, diga isso no comentário do schema; se vêm de anexos MIME, diga também. A escolha do decodificador é uma propriedade da coluna, não da consulta.
  • Guarde bytes, codifique na borda. Se você controla o schema, uma coluna BLOB mais codificação na camada de API ganha de uma coluna de texto base64 em armazenamento, em indexação e em toda consulta futura. Base64 na coluna é um imposto de compatibilidade, e impostos são melhores pagados uma vez, na fronteira.
  • Faça uma ida e volta com um canário. Antes de confiar numa nova rota de decodificação, empurre um payload conhecido por codificação e decodificação no mesmo banco de dados e compare. O canário deve conter um caractere não-ASCII (para exercitar o passo do conjunto de caracteres), um tamanho que deixe uma cauda de preenchimento (para exercitar as regras de preenchimento) e, para rotas URL-safe, um - ou _ em algum lugar (para exercitar a tradução de alfabeto).
  • Mantenha segredos fora do texto da consulta. A verificação de JWT com pgcrypto coloca um segredo compartilhado na instrução; auditorias de config colocam segredos em texto puro no resultado. Ambos são trabalhos legítimos, mas merecem uma conta restrita, um log limpo e uma revisão, não um connection string de produção e um SELECT * INTO OUTFILE.

Uma breve história do desempacotamento em SQL

O formato base64 em si é mais velho que a parte útil da internet. Ele foi padronizado para MIME no meio dos anos 1990 (RFC 2045, seção 6.8, que tornou obsoleto o RFC 1521, a especificação de corpo de mensagem MIME de 1993 que carregava a codificação), e o nome é apenas uma contagem: o alfabeto tem 64 caracteres. A variante URL-safe chegou com o RFC 4648 em 2006, e a especificação de JWT em 2015 fez dessa variante a que você de fato vê em colunas de token. Mas cada banco de dados se encontrou com o formato no seu próprio cronograma, e o cronograma diz algo sobre a alma de cada um.

2002. O PostgreSQL 7.2 já lista o base64 como formato de primeira classe do encode() e do decode() - contemporâneo do UTL_ENCODE do Oracle na era 9i, e o suporte a base64 mais antigo desta família por uma margem curta. Um banco de dados com um tipo binário de verdade e um argumento de formato chegou cedo, porque a resposta estava a um valor de enum de distância.

Princípio dos anos 2000. O pacote UTL_ENCODE do Oracle aparece na era 9i, carregando base64 ao lado de funções de cabeçalho MIME, quoted-printable e uudecode. É RAW por dentro e RAW por fora, o que é muito Oracle, e ele manteve essa forma por um quarto de século.

2013. O MySQL 5.6 adiciona TO_BASE64() e FROM_BASE64(), e o MariaDB 10.0 carrega os dois para o fork. O par codifica com linhas de 76 caracteres e decodifica com tolerância a espaço em branco, um conjunto feito um para o outro que não mudou em uma dezena de versões principais.

2018. O ClickHouse 18.16 lança o base64Decode() com o seu alias no estilo MySQL, porque o mundo colunar estava importando workloads que já carregavam base64 nos schemas de log deles.

2023. O SQLite 3.41.0 adiciona base64() e o primo base85 dele ao shell de linha de comando como funções definidas pela aplicação. A biblioteca núcleo, fiel ao seu jeito, não ganha nada; o shell, onde os humanos de fato cutucam bancos SQLite, ganha a ferramenta.

2025. O SQL Server 2025, disponível em geral em novembro de 2025, adiciona BASE64_DECODE() e BASE64_ENCODE() ao T-SQL depois de uma ausência de trinta e seis anos. As notas de release o tratam como um recurso modesto; a comunidade o trata como um resgate.

O padrão fica limpo uma vez que você o vê. Bancos de dados com um tipo binário genuíno e um argumento de formato (PostgreSQL, e de um jeito o Oracle) ganharam base64 no dia em que a necessidade ficou óbvia. Os demais (MySQL, SQL Server) o trataram como uma conveniência de string e agendaram assim. E o motor embutível (SQLite) ainda considera isso um trabalho do aplicativo hospedeiro, com o CLI como uma exceção amigável.

Coisas que vão fazer você sorrir

  • O SQL Server passou de 1989 a 2025 sem um decodificador base64, e a resposta da comunidade foi uma função XML chamada xs:base64Binary() dentro de um CAST(N'' AS XML). Uma geração inteira de consultas corporativas decodificou tokens através do parser XML, porque o parser XML entendia base64 desde 2001 e o motor SQL não entendia.
  • O base64() do CLI do SQLite é o único camaleão desta família: passe um BLOB e ele codifica, passe texto e ele decodifica. A função muda o seu trabalho com base no tipo do argumento, que é um pequeno ato de telepatia SQL e uma armadilha genuína para os distraídos.
  • O codificador do PostgreSQL quebra em 76 caracteres exatamente como o padrão MIME de 1996, exceto que ele termina as linhas com uma quebra de linha solta em vez do retorno de carro e quebra de linha do padrão. Vinte anos depois da especificação, um caractere a menos. O decodificador ignora os dois, então a revolução é invisível a menos que você faça diff da saída.
  • No cliente mysql, SELECT FROM_BASE64('aGVsbG8=') imprime 0x68656C6C6F. Não porque os dados sejam hex, e não porque algo esteja errado, mas porque o cliente decidiu, em seu nome, que strings binárias deveriam ser exibidas como hex. A configuração se chama binary-as-hex, e ela já convenceu milhares de desenvolvedores de que o decodificador deles está quebrado.
  • O tipo RAW em nível de SQL do Oracle é limitado a 2000 bytes, então um certificado de 3 quilobytes não pode nem ser colado numa instrução SQL como literal RAW. A decodificação tem que acontecer em PL/SQL, em pedaços, com um loop. O limite data dos anos 1990; o loop continua sendo a resposta recomendada.
  • O Snowflake exibe valores BINARY como hex em todos os conjuntos de resultado, então uma decodificação perfeitamente bem-sucedida de "hello" chega na sua tela como 68656C6C6F. Dois dialetos, duas exibições hex, uma sensação de desconfiança idêntica.
  • O ClickHouse mantém o alias FROM_BASE64() do lado do nativo base64Decode(), uma pequena cortesia para os refugiados do MySQL que chegaram com consultas que de outra forma não rodariam.
  • A família inteira compartilha um fato silencioso: base64 é um imposto de 33 por cento na saída e um reembolso de 25 por cento na entrada, e nenhum dos oito decodificadores aqui vai te contar isso sem ser perguntado. O formato é um figurino; o guarda-roupa é grátis; a alfaiataria é o que este artigo trata.

Continue por aí

Este artigo tem sido sobre tirar o disfarce: a função em cada dialeto, o temperamento dela, e os payloads (JWTs, data URLs, e-mails quebrados, campos JSON, valores de config, cabeçalhos de auth) que o vestem. A outra direção é um animal à parte, com o seu próprio conjunto de surpresas: quais codificadores quebram a saída em 76 caracteres e quais não quebram, como produzir a forma URL-safe sem preenchimento que os tokens esperam, a conta de tamanho que decide a largura da sua coluna, e o que a lacuna de 36 anos do SQL Server significa para quem ainda está numa versão mais antiga. Tudo isso, do TO_BASE64() ao BASE64_ENCODE(), está coberto em detalhe no artigo relacionado de codificação Base64 para SQL, linkado a partir desta página. Decodifique aqui, codifique lá, e a ida e volta inteira cabe dentro de uma tarde.

Última atualização: 2026-09-08

Artigo relacionado: Codificação Base64 em SQL: um guia completo