需要使用 Base64 格式吗?那么本网站正好适合您!使用我们的在线工具对数据进行编码或解码,便捷好用。

SQL 中的 Base64 解码:完整指南

在任何生产数据库里待得足够久,你总会遇见这种伪装。一个在 JSON 导出文件里化成一整墙字母的头像。一枚停在 varchar 列里、紧挨着用户 ID 的 JWT。一份因为传输格式没有二进制类型、被某人决定以字符串形式发出的证书。在某个表的某处,你的数据穿着字母外衣,而你的任务就是不离开数据库就把它们脱下来。

在 SQL 里,这份工作有一个非常让人安心的特性:一旦你知道自己的方言讲哪种解码器,整份活就缩小为一次函数调用。格式本身在首页已经详细解释过了(64 个可打印字符,每四个一组顶替三个输入字节,最后一组最多用两个 = 号做填充),所以本文跳过那堂课。有两件事要随身带走:base64 是一种把字节打扮成文本的方式,而不是一把锁;解码是数据变得更小的方向(缩回编码后大小的四分之三),这恰好与你为存储列预留空间时的设想相反。真正的故事在于,SQL 是一个方言家族,每个成员都叫自己的解码器一个不同的名字,面对坏输入时也会表现出完全不同的脾气。这篇文章就是这次巡游。

解码器阵容

下面是值班名册,以及每个解码器在输入是垃圾时的表现。"坏在哪"这一列很重要,因为一个在预发布环境里大声报错、到了生产环境却悄悄失效的解码器,正是头像离奇失踪的常见原因:

方言 调用 返回什么 坏在哪 从何时起
MySQL 8.x / MariaDB 10.x FROM_BASE64(str) 二进制字符串 静默的 NULL MySQL 5.6(2013)
PostgreSQL decode(str, 'base64') bytea 响亮的 ERROR,附带提示 7.2(2002)
SQLite(CLI 3.41+) base64(str) BLOB 读不了的直接跳过 3.41.0(2023)
DuckDB from_base64(str) BLOB 转换错误 较新版本
ClickHouse 18.16+ base64Decode(str) String 抛出异常(INCORRECT_DATA 18.16.0(2018)
SQL Server 2025+ BASE64_DECODE(str) varbinary Msg 9803,三种状态 2025
Oracle UTL_ENCODE.BASE64_DECODE(raw) RAW PL/SQL 异常 9i 年代
Snowflake BASE64_DECODE_BINARY(str) BINARY 报错,或用 TRY_ 变体时返回 NULL 当前版本

注意这张表的形状:函数名从来不是难点。难点是"坏在哪"这一列,因为正是这一列决定了你的报表是悄悄丢行,还是你的批处理任务停下来喊救命。

MySQL 与 MariaDB:只会耸肩的解码器

这两个服务器共享 TO_BASE64() / FROM_BASE64() 这对搭档。解码器接收一个字符串,还你一个二进制字符串:一串不带任何字符集的字节。NULL 进,NULL 出,而这是第一个要背下来的要点:任何不是合法 base64 的输入,得到的也是 NULL,且没有任何警告。解码器耸耸肩,你的查询就开开心心地继续往下跑。

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

中间那行值得点评,因为它解释了一个经典的困惑时刻。mysql 命令行客户端默认用十六进制记法打印二进制字符串(一个名叫 binary-as-hex 的设置),所以一条裸的 SELECT FROM_BASE64('aGVsbG8=') 显示的是 0x68656C6C6F 而不是 hello。这不是 bug,也不是数据损坏,而是客户端对二进制数据保持谨慎。如果你想要字母,就用 CONVERT(... USING utf8mb4) 转换,或者用 --binary-as-hex=0 启动客户端;中间那行里的 HEX() 调用,就是客户端默认展示给你的那种十六进制的刻意版本。

接下来是这个沉默解码器执行的规则。忽略空白之后,剩余字符的个数必须是四的倍数,每个字符都必须来自标准字母表(字母、数字、+/=),而填充只能出现在最末尾:

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

三行全部毫无怨言地执行完毕,第二行和第三行返回 NULL。缺填充、长度不对、出现外星字符:同一个耸肩。空白是唯一被宽容对待的对象;换行符、回车符、制表符和空格统统被忽略,这对任何先坐过邮件班机的数据都是一份仁慈。而 URL-safe 字母表得到的回报也是同一个耸肩:下划线不在标准表里,所以即便长度是四的整倍数,FROM_BASE64('yv7K_g==') 得到的也是 NULL。你必须在调用之前自己翻译字母表,下面的 URL-safe 一节会演示怎么做。

还有一个值得知道的特性:解码器和编码器是一对搭档。编码器把输出折成每行 76 个字符,而解码器把这些换行当早餐吃掉。如果某列是同一个数据库家族里的 TO_BASE64() 填进去的,解码就是一次完美的往返。如果是别的东西填进去的,请继续往下看。

PostgreSQL:会大声说话的解码器

PostgreSQL 从至少 7.2 版(2002 年)起就在核心里内置了 base64,这让它以微弱优势成为这个家族里最年长的 base64 机械装置。调用方式是 decode(string, 'base64'),结果是 bytea,也就是数据库原生的二进制类型。它的搭档 encode(bytea, 'base64') 走的是反方向,这里提到它只是因为两者共享同一份格式约定:RFC 2045 风格,每 76 个字符折一次行。至于解码器,它忽略输入中任何位置的回车符、换行符、空格和制表符。

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

第三行是你几乎天天要伸手去拿的那行:convert_from() 把 bytea 按指定编码转成文本,它是二进制数据必需的那道字符集工序(后面会有专门的一节细说)。aMOpbGxv 会原样地以 héllo 的形式回来,重音符号一个不少。

PostgreSQL 与全场拉开差距的地方,正是"坏在哪"那一列。非法输入是硬错误,而且错误消息会告诉你到底违反了哪条规则:

  • 字母表之外的字符:ERROR: invalid symbol "!" found while decoding base64 sequence
  • 填充号出现在字符串中间:ERROR: unexpected "=" while decoding base64 sequence
  • 输入被截断或缺少填充:ERROR: invalid base64 end sequence,并附带提示Input data is missing padding, is truncated, or is otherwise corrupted.
  • URL-safe 的下划线:ERROR: invalid symbol "_" found while decoding base64 sequence

对数据清洗工作来说,这种嗓门是特性而非缺陷。查询失败,你看见了那一行,你去修源头。代价是一百万行里一颗中毒的行就会停住整批处理,所以生产管道里人们常常先跑一道正则预过滤,再调用 decode()。还有一个小小的显示备注:psql 把 bytea 打印成带 \x 前缀的十六进制,所以 \x68656c6c6f 就是 MySQL 客户端显示为 0x68656C6C6F 的同一个 "hello"。两种方言,两种十六进制方言。

SQL Server:迟到者

这就是整个家族里最大的意外。SQL Server 在 2025 版才发布 BASE64_DECODE(),2025 年 11 月正式 GA。在那之前,企业世界里最流行的数据库整整三十六年都没有内置 base64 解码器,变通方案的民间传说堆积如山。现代版的函数很干净:它接收一个 varchar(n)varchar(max) 表达式,返回一个 varbinaryvarchar(n) 表达式映射到 varbinary(8000)varchar(max) 表达式映射到 varbinary(max)),NULL 直接原样通过。

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;

第三行是一个真正贴心的细节:解码器两种 RFC 4648 字母表都收,既有带 +/ 的标准字母表,也有带 -_ 的 URL-safe 字母表,而且填充可给可不给。它同样忽略四个空白字符(换行、回车、制表符、空格)。而当它真的坏掉时,错误是 Msg 9803, Level 16,文案为Invalid data for type "Base64Decode"State 值会告诉你撞上了哪条规则:字符不属于任何一种字母表是 state 20,字符全部合法但排列成了 base64 造不出的形状是 state 21,填充出现得太频繁或太早则是 state 23。

如果你被困在 2025 之前的版本上,经典的变通方案借用了 XML 类型,它从 XML Schema 年代起就认识 base64:

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

XML 引擎对那个常量做 base64 解码,把字节还给你。它能用,而且是一整代 SQL Server 开发者的老朋友。但它也有毛边:base64Binary 类型对形状很挑剔,所以带着内部换行的 MIME 折行字符串解析不了;而且现在一个原生函数就能干的活,你却要付上 XML 机械装置的开销。把它当成它已经变成的博物馆展品吧。

SQLite:没有解码器的方言

SQLite 是全场唯一的例外,而理解它为什么这样,就告诉你该怎么用它。核心库是一个小而可嵌入的引擎,它的标准函数列表里没有 base64。如果一列里装着 base64,解码器必须来自四个地方之一:命令行 shell、可加载扩展、宿主应用注册的自定义函数,或者纯 SQL。下面逐一介绍。

命令行 shell。从 3.41.0 版(2023 年 2 月)起,sqlite3 命令行 shell 自带一个 base64() 函数。它把文本参数解码成 BLOB,非常适合直接在终端里做探索性工作:

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

有两个脾气体格要清楚。第一,它宽松:不认识的字符会被跳过而不是报告,所以 base64('!!!') 返回一个空 BLOB 而不是错误。满足好奇心很香,做审计很危险,因为输出里"空"和"缺失"看起来一模一样。第二,这个函数会变身:BLOB 参数会被编码成文本(每行 72 个字符),而文本参数会被解码成 BLOB。同一个名字,两份工作,由参数的类型决定干哪份。这个家族里没有第二个解码器这样干,所以输入类型要检查两遍。

纯 SQL。核心库没有 base64,但它有递归 CTE、算术运算,以及(3.41.0 起)unhex(),这几样足以用几十行代码搭出一个真解码器。配方是:一张 64 行的字母表,输入切成四字符一块,每块变成一个 24 位数字,数字拆成三个字节,字节先收集成十六进制,再由 unhex() 变成 BLOB。下面是它作用于一个表列的样子:

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;

对着一张有 b64 列的表运行它,每行就得到一个 BLOB,不用扩展,不用应用代码。里面的算术就是穿了整数外衣的普通 base64:四个字符各贡献 6 个比特,中间两个字符跨在一个字节边界上,最后一个字符的低 2 个比特被丢弃。它是这一页里最慢的方案(一次递归遍历,外加每块一次查表),所以把它留给小载荷和一次性的考古挖掘。对长期运行的应用来说,诚实的答案是第三个选项:从宿主语言注册一个一行代码的自定义函数(Python 的 sqlite3 模块用 create_function() 加标准 base64 模块两行就搞定),让引擎像调用原生函数一样调用它。第四个选项,sqlean 家族这样的可加载扩展,也确实存在,但那意味着安装一个不同的引擎构建,大多数团队会宁可避免。

DuckDB:严格、小巧、有主见

DuckDB 是一个分析型数据库,有真正的二进制类型 BLOB,以及围绕它一整家族利落的 blob 函数。解码器是 from_base64(string),和它的朋友们 to_base64()hex()md5()sha256() 排在同一个参考页上,大多数 DuckDB 用户就是在那里第一次见到它的。

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

第三行展示了一条可能比你想的更友好的规则:当长度是四的倍数时,缺了填充也没关系,AAEC 好好地解码成字节 00 01 02。严格的一面在形状不对的瞬间就会露面。DuckDB 要求长度必须是四的倍数,句号,转换错误说的也正是这件事:

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

还有两条需要尊重的主见。第一,DuckDB 的解码器只说标准字母表;下划线不是它认识的字符,所以 URL-safe 令牌必须在抵达之前先翻译(配方在 URL-safe 一节)。第二,它对空白零容忍。一封带着 76 字符折行的 MIME 邮件附件会直接失败,修复办法是在调用前用 replace() 把换行符和回车符清掉。而且因为没有 try_ 变体来缓冲这一击,温和的写法是在同一条查询里先做预检:

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;

先正则,后解码器:查询对任何不可能解码的东西都返回 NULL,解码器永远只见到形状正确的输入。

ClickHouse:列式解码器

ClickHouse 没有单独的二进制类型;它的 String 天生二进制安全,这意味着"解码进字符串"就是全部工作,后面不再跟任何转换步骤。这个函数从 18.16.0 版(2018 年)起就叫 base64Decode(),并且保留了一个 MySQL 风格的别名 FROM_BASE64(),所以移植来的查询不用改写。

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

第二行就是 ClickHouse 的招牌风格。这个引擎钟爱它的 try 前缀:tryBase64Decode() 吞掉失败并返回空字符串,而普通的 base64Decode() 会抛出带 INCORRECT_DATA 错误码的异常,消息里还点名了肇事的那个值。当坏行应该让管道停下来时用普通形式,当报表应该继续走下去时用 try 形式,而且要有意识地选,别靠运气。

两条版本备注,因为 ClickHouse 跑得快。26.7 之前,输入里的空白会被拒绝;26.7 起,空格、制表符、换行符、回车符和换页符全都被忽略,这才是任何碰过邮件或文本编辑器的数据所需要的行为。而且现代解码器期望四字符分组带上规整的填充,所以一个在入库路上丢了等号的令牌会招来异常,而不是尽力而为。如果一个 2023 年能跑的查询在 2026 年开始抛异常,先查服务器版本,再怀疑数据。

Oracle:要么 RAW,要么什么都没有

Oracle 的 base64 机械装置住在 UTL_ENCODE PL/SQL 包里,它自有性格:只收 RAW,只还 RAW,别无其他。文本进不来,文本出不去。VARCHAR2 是带字符集的字符数据;RAW 是裸字节;这个包拒绝装出别的样子。所以能用的模式是一个三层三明治:转成 raw、解码、再转回文本:

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

每一步都挣得自己的位置。UTL_RAW.CAST_TO_RAW() 把文本的字节重新解释为 raw(按数据库字符集,对现代部署来说通常是 AL32UTF8,所以你的 UTF-8 输入原样通行)。UTL_ENCODE.BASE64_DECODE() 干实际的活。UTL_RAW.CAST_TO_VARCHAR2() 再把结果字节按同一个数据库字符集重新解释为文本。跳过任何一层都会得到类型不匹配错误,这正是 Oracle 在认真执行"把话说明白"这项工作。

非法输入会抛出 PL/SQL 异常,而不是安静的 NULL,所以批量解码应该住在一个会记录肇事行的异常处理器里。这个包还带着一整个同代解码器博物馆:MIME 头解码、quoted-printable、uudecode、text 编码,全都来自同一个年代。你大部分时候只会用 base64 这对搭档,但邻居们解释了包为什么这样组织:Oracle 想给"穿着传输外衣的数据"建一个家。

动手前先知道一个尺寸陷阱。在普通 SQL 里,RAW 值上限是 2000 字节,所以一条解码后超过约 1500 个 raw 字节的 base64 值,根本无法用单条 SELECT 语句解码。更大的载荷需要 PL/SQL 循环,每次 2000(或更少)字节地走查 BLOB,逐块解码,再把结果拼回。这是老派做法,但它是 Oracle 的标准答案,也是那种 1990 年代的类型系统至今仍在你 2020 年代的查询里留下指纹的地方。

Snowflake:自带字母表

Snowflake 把二进制类型(BINARY)和文本类型分开,并给你这个家族里可配置程度最高的解码器。主力是 BASE64_DECODE_BINARY(input),它返回 BINARY,可选的第二个参数是一个重新定义字母表的短字符串:

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;

仔细读那个字母表参数,因为它是按位置的。最多允许三个字符:前两个覆盖字母表第 62 和 63 位(默认是 +/),第三个覆盖填充字符(默认 =)。要说"用 URL-safe 字母表",传 '-_'。要说"URL-safe 字母表但用 % 做填充",就必须把三个字符全传上,'-_%',哪怕你真正想改的只有填充字符。省略掉的字符保持默认;你不能跳过某一位去填下一位。

两个同伴补齐了全家。BASE64_DECODE_STRING() 一次调用里同时完成解码和文本转换,载荷是文本时你就可以跳过 TO_VARCHAR()。还有 TRY_ 变体 TRY_BASE64_DECODE_BINARY()TRY_BASE64_DECODE_STRING(),遇到坏值返回 NULL 而不是抛错,这就是 Snowflake 版的 ClickHouse try 形式。

从字节到文本:字符集这道工序

解码交给你的是字节。如果载荷是文档、名字或 JSON 片段,你还欠它最后一步:按一个指名道姓的字符集把它解释成文本。"解码成功但看起来不对"就是从这里来的,因为一串字节只有在你说出按哪种字节语言来读之后才会变成词语。这张表很短,值得背下来:

方言 字节转文本 非法序列
MySQL / MariaDB CONVERT(bin USING utf8mb4) 直接重解释;垃圾进,垃圾出
PostgreSQL convert_from(bytes, 'UTF8') 抛错
SQL Server CAST(bin AS VARCHAR) 有损,取决于排序规则
Oracle UTL_RAW.CAST_TO_VARCHAR2(raw) 按数据库字符集重解释
DuckDB decode(blob) 转换错误
ClickHouse 不需要;String 本身就是文本 不适用
Snowflake TO_VARCHAR(bin, 'UTF-8') 抛错
SQLite CAST(blob AS TEXT) 完全不校验

这张表的跨度大得刻意。PostgreSQL 和 DuckDB 会校验并拒收,这保护你的下游代码远离乱码。MySQL 和 Oracle 默默重解释,快是快,但意味着 Latin-1 载荷闯进 UTF-8 世界时数据库救不了你。SQLite 连看都不看,因为在 SQLite 里 TEXT 值就是带标签的字节。实用规则:在解码之前就定好字符集,以字面量写进查询,并用一个包含非 ASCII 字符的载荷测试(经典的 aMOpbGxv 对应 héllo 是只用好金丝雀,因为它在每一种错误的字符集里坏法都不一样)。对真正的二进制载荷,直接跳过这一节,让字节保持字节。

JWT:一列里三个点隔开的 Base64

JSON Web Token 是你在数据库里最常发现的 base64,因为认证事件总是连带令牌一起被记录。一个 JWT 是三段用点分隔的东西:头、载荷、签名。前两段是打包成 base64 的 JSON 对象,而这里有个坑住过人的反转:JWT 用的是不带填充的 URL-safe 字母表,不是标准的带填充形式。/ 会在令牌经常穿行的路径里开启新的路径段,+ 在查询字符串里会被读成空格,填充等号则纯属仪式,所以规范(RFC 7515 和 RFC 7519)换成了 -_,并把填充整个丢掉。

因此在 SQL 里解码一个令牌是一支四步舞:按点拆分、把 URL-safe 字符换回标准字母表、补回填充、解码并解析 JSON。PostgreSQL 有 JSONB 类型,是干这件事的舒适之地:

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;

结果是一个 JSONB 值,你可以像查询其他任何列一样查询它;对上面那个令牌,它回来时是 {"iat": 1516239022, "sub": "1234567890", "name": "Dev User"}。从结果里取出单个声明,后续查询里就是 claims->>'sub'。填充恢复工作由那个 CASE 表达式完成:长度比四的倍数少 2 的 base64url 字符串需要两个等号,少 3 的需要一个,正好是倍数则一个都不用。

再走一步,你甚至可以在 SQL 里验证 HS256 签名,用 PostgreSQL 的 pgcrypto 扩展做 HMAC(如果还没装,先执行一次 CREATE EXTENSION IF NOT EXISTS pgcrypto; 启用)。用共享密钥对 header.payload 重新计算签名,按同样的 base64url 方式格式化,然后比较:

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;

hmac() 调用产生摘要,encode(..., 'base64') 把它打包,三个字符串操作把它塑成令牌所携带的那种无填充 URL-safe 形状。对上文的令牌和密钥,答案是一个欢快的 t。不过把注意事项带在身上:这只对 HMAC 算法(HS256、HS384、HS512)有效,它把共享密钥放进了数据库语句里,而且是为报表、审计和调试而生的。任何真正把关访问的东西,都应该在应用层用真正的 JWT 库来验证。

Data URL:字符串里的图像

data URL 格式(RFC 2397)是 Web 把文件内联进链接的方式:data:image/png;base64, 后面跟着文件的 base64。浏览器从剪贴板粘贴它们,单页应用把小图像嵌在里面,而这些流程每一个最终都会以一个长文本值的身份落进数据库列里。格式是 data:{media type}[;{parameters}][;base64],{data},对解码而言唯一要紧的是第一个逗号之后的全部内容,因为 base64 载荷从那里开始。

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

在 MySQL 里这就是全部工作:找到逗号,跳过它,解码,然后一个可以存进 BLOB 列或拿去算哈希去重的二进制表达式里就握着图像字节了。其他方言只是换函数(大多数用 SUBSTR()INSTR(),另一些用 substring()position()),但形状一模一样。

三条警告。第一,不是每个 data URL 都是 base64;没有 ;base64 标记的 data URL 携带的是百分号编码的文本,把它喂给 base64 解码器是个错误,上面那个 LIKE 过滤器就是用来防这个的。第二,前缀里的媒体类型是声明,不是事实;同一个字符串可以说自己是 image/png 却装着 JPEG。内容要紧的话,检查解码结果的魔数(PNG 以 89 50 4E 47 开头,JPEG 以 FF D8 开头)。第三,data URL 很大。一张 400 万像素的照片变成大约 5.5 MB 的字符串,这是列尺寸和内存的对话,不是字符串函数的对话。

URL-safe Base64:会旅行的字母表

RFC 4648 的第 5 节为 base64 定义了第二个字母表,因为原始字母表里有两个字符在 URL 语法里另有职务。加号是查询参数给值加值的方式,斜杠是路径分隔的方式,填充等号一碰到查询字符串就会立刻被百分号编码。URL-safe 变体把 + 换成 -/ 换成 _(两者在 URL 里都无害),而 JWT 规范在此基础上又把填充整个丢掉。结果就是它能穿过链接、路径段、文件名和片段标识符,全程没有一个百分号。

你在数据库里遇到它,多半是因为存了令牌和链接,而不是数据在那里出生。下面是谁能原生处理、谁需要这份两分钟手工教程:

方言 原生 URL-safe 解码 备注
SQL Server 2025+ BASE64_DECODE() 两种字母表都收 完全不需要翻译
ClickHouse 24.6+ base64URLDecode() 也照样收 +/
Snowflake BASE64_DECODE_BINARY(s, '-_') 字母表是按位置传参
MySQL / MariaDB 没有 翻译字符,失败时得到 NULL
PostgreSQL 没有 翻译字符,失败时报错
Oracle 没有 在 RAW 转换之前翻译字符
DuckDB 没有(会拒绝下划线) 翻译字符,保持长度是四的倍数
SQLite CLI 没有 翻译字符;解码器会跳过它不认识的东西

手工教程是两个 REPLACE() 调用加上填充恢复,在每个方言上都是同一套。在 PostgreSQL 里它长这样:

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

- 换回 +,把 _ 换回 /,按长度对四取模补上缺失的填充,标准解码器从那里接手。输入 aGVsbG8("hello" 的无填充 URL-safe 形式)会作为那个单词本身回来。反复发生的两个错误正是 CASE 表达式所防的:忘了填充,让严格解码器拒绝一个不是四的倍数的长度;跳过了字符翻译,让不认识 URL-safe 字母表的解码器卡在下划线上。把翻译写成数据库里一个可复用的函数,写一次,整个问题就不再复发。

文件、BLOB 和大块头

解码就是文件从列里出来的方式,而每个方言的出口都略有不同。在 DuckDB 里,往返只需两条语句,一条把文件读进 BLOB,一条把解码后的字节写回去:

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

读取这一边:read_blob() 是一个表函数,接受单个文件名、一串文件名或一个 glob 模式,每个文件还你一列 filename 和一列 content。写入那一边是它自己的语句:COPYBLOB 格式写裸字节,不加引号、不做转义,正是解码载荷想要的东西。

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

PostgreSQL 的出口是大对象 API。大对象是按 OID 寻址的服务器端二进制块存储,lo_export() 把它写到数据库服务器上的一个文件里。它需要超级用户权限或 pg_write_server_files 权限,而且目标必须是服务器进程能写入的路径,所以实践中这是维护脚本的活,而不是应用代码的活:

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

MySQL 只有一个受限的 SELECT ... INTO DUMPFILE 逃生舱(单行、服务器端路径、FILE 权限),SQL Server 则根本没有普通 SQL 文件写入器(写盘是客户端或代理的活,经由它的导出工具),这是一个合理的设计:数据库存字节,应用决定文件该放哪。SQLite 在光谱的另一端,在那里应用就是宿主,一个 BLOB 列可以用宿主语言一次调用直接写到磁盘。

然后是天花板,它们之间的差异比你想象的这些假装彼此相同的数据库要大得多:

方言 二进制类型 实际天花板
PostgreSQL bytea 每个值 1 GB
MySQL / MariaDB BLOB 家族 max_allowed_packet(MySQL 8 默认 64 MB)
SQL Server varbinary(max) 每个值 2 GB
Oracle RAW / BLOB RAW:SQL 里 2000 字节,BLOB:PL/SQL 分块可到 4 GB
SQLite BLOB 文件和内存允许多少就是多少
DuckDB BLOB 非常大;由内存和磁盘说了算
ClickHouse String 列尺寸是虚拟的,行才是单位
Snowflake BINARY 默认每个值 8 MB(普通 BINARY 列);显式 BINARY(N) 可到 64 MB

MySQL 那一行值得一讲,因为它是生产环境里最让人意外的。max_allowed_packet 限制客户端与服务器之间单个数据包的大小,而 base64 字符串是这个数据包的一部分。一张 50 MB 的照片编码成 base64 后约是 67 MB 的字符串,比 64 MB 的默认值还大,而且结果不是你能在查询里读到的错误:它是一个被截断或为 NULL 的值,看起来像数据损坏。如果你正通过 MySQL 列搬大文件,动手前先检查这个限制,并记住对它计数的是编码后的形式,而不是裸字节。

邮件折行与 MIME 行

凡是活过邮件系统的 base64,都带着一件纪念品:换行。MIME,那套让邮件能携带二进制附件的标准(RFC 2045,第 6.8 节),把 base64 输出在 76 个字符处折行,行尾是回车加换行。折行存在的原因,是老的邮件网络不敢信任更长的行,而此后出于习惯,这个格式一直被沿用。所以存进数据库列的附件,经常是一个每 76 个字符就带一个换行的 base64 字符串,而你的解码器与这些换行的相处方式,决定了这份活是一条语句还是两条。

解码器 吃折行吗? 不吃的话
MySQL / MariaDB FROM_BASE64() -
PostgreSQL decode() -
SQL Server BASE64_DECODE() -
SQLite CLI base64() -
ClickHouse 26.7+ -
ClickHouse 26.7 之前 不吃 先去掉空白
DuckDB from_base64() 不吃 先去掉空白
Oracle UTL_ENCODE.BASE64_DECODE() 不吃 在 PL/SQL 层去掉空白

"先去空白"的修复只是一个表达式,而且永远安全,因为空白不是 base64 字母表的一部分:合法的载荷不可能包含空格、制表符或换行,所以删掉它们不会毁掉任何信息。在 PostgreSQL 里,惯用法是单个 regexp_replace()

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

所有空白字符,包括换行,统统消失,解码器看到的是一整条干净的连续字符串。在 DuckDB(对两个换行字符跑它的 replace())或 26.7 之前的 ClickHouse 里跑这个,折行的附件就会和没折行的一样被解码。

API 载荷、配置与认证头

从单个函数退后一步,一个模式浮现出来:数据库列里的 base64 几乎总是三样东西之一。JSON 文档里的一个字段(一张图像、一份证书、一个 API 决定内联的文件)。一个配置值(某个工具偏好用 base64 存放的密钥或凭据,因为 base64 能塞进 YAML 文件的一行里,无引号、无换行、无反斜杠)。或者一个认证产物(Basic 认证头、存储的令牌、会话 blob)。下面逐一给出各自的解码形状。

JSON 字段。JSON 以文本形式到达,字段是字符串,base64 藏在里面。用你方言的 JSON 函数取出字段,然后解码。在 MySQL 里,整条链就是一个表达式:

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 用 JSONB 做同样的事,字段用 ->> 操作符以文本形式出来,decode() 接手。最后一行的 JSON_TYPE 守卫比看上去更重要:它把解码器挡在那些字段是数字、嵌套对象或干脆缺失的行外面,否则在 MySQL 里这些行会默默给你的"有多少事件带图像"计数贡献一个 NULL

认证头。Basic 认证头是字面量前缀 Basic 加上 username:password 的 base64。在 SQL 里解码它就是取子串加拆分,这正是人们做这件事的原因(通常是审计哪些用户敲了哪些端点,而不是验证密码,密码绝不应该以明文形式被数据库看到):

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) 剥掉 Basic 前缀,解码器还原出原文,两个 SUBSTRING_INDEX() 调用在冒号处拆分,前半段给用户,后半段给密钥。在 PostgreSQL 里同一条查询用 substring()split_part()

配置值。这里的解码方向就是审计工作:有人把密钥以 base64 存进了配置表(一个从 Kubernetes 继承来的习惯,那里的密钥值落盘时就是 base64),你想看看里面到底是什么,或者你在搭一个新环境要消费的导出。形状是每个值一条 SELECT,如果值是文本,字符集那道工序也适用:

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

用配得上这份结果的谨慎对待它。你刚刚把存起来的密钥变成了可见的查询输出;确认跑查询的账号只该有的权限、结果不会被复制进日志、配置里存 base64 的习惯值得再审视一遍。Base64 是传输,不是保险箱,而审计查询就是这件事变得显而易见的那一刻。

那些会咬人的陷阱

这份清单里的每个陷阱,都至少在某个代码库里吃掉过一下午,而且每一个都是 SQL 方言处理 base64 的方式特有的,而不是 base64 本身的问题。

  • 静默的 NULL。MySQL 和 MariaDB 把坏输入解码成 NULL,一声不吭。在一个按解码值连接的报表里,那些行就此消失,"0 行"和"0 行,因为 14 行是中毒的"之间的差别,直到有人问起为什么总数对不上才看得见。如果你的解码器是安静派,就有意地去数你的 NULL。
  • 四的倍数规则,执行得不均匀。长度不是四的倍数的字符串不是 base64,但各方言对接下来怎么办意见不一:PostgreSQL 抛错,DuckDB 抛转换错误,ClickHouse 抛异常,MySQL 返回 NULL,SQLite CLI 则安安静静地解码它能解码的部分。同一个数据文件在五个数据库上产生五种结果,这就是为什么"在 Postgres 里能跑"不算测试。
  • 字母表不匹配。一个 URL-safe 令牌(JWT、链接、文件名)喂给标准字母表的解码器:SQL Server 收下,ClickHouse 的 base64URLDecode() 收下,Snowflake 用对的参数收下,而其他人要么返回 NULL,要么抛错,要么像 SQLite CLI 那样悄悄丢掉下划线、把错误的字节交给你。错误字节那一种才阴险,因为结果看起来很合理。
  • MIME 折行。折行输入喂给不吃换行的解码器(DuckDB、26.7 之前的 ClickHouse、Oracle)会失败,而失败的样子常常像"最后 76 个字符是垃圾",而不是"这里藏着一个换行符",因为错误指向的是断行之后的那个字符。
  • 显示把戏。mysql 客户端把二进制打印成十六进制,psql 把 bytea 打印成 \x 十六进制,Snowflake 把 BINARY 打印成十六进制,Oracle 把 RAW 打印成十六进制。四个客户端,四种十六进制记法,一个非常人性化的错误:因为屏幕上显示的是数字,就断定数据损坏了。用眼睛读结果之前,永远先显式转换。
  • 填充放错了地方。等号只在末尾合法,一个或两个。像 YQ==BQ== 这样的字符串是两个合法分组套了一件马甲,严格解码器拒收,宽松解码器把它解码成没人要的东西。如果你在存储值中间看到填充,写它的编码器就是坏的,修数据是一次性的活。
  • 字符集惊吓。解码成功,文本回来了,重音符号却是错的。字节没问题,解释错了。这该是 utf8mb4 却用了 CONVERT(... USING latin1),这该显式却在一个会吞掉非法序列的排序规则下跑了 CAST(bin AS VARCHAR),这该校验却在 SQLite 里跑了从不检查的 CAST(blob AS TEXT)。把字符集以字面量钉死在查询里,并用一个带重音的金丝雀测试。
  • 天花板。Oracle SQL 语句里 2000 字节的 RAW 上限,MySQL 对编码后大小收税的 max_allowed_packet,PostgreSQL 1 GB 的 bytea 天花板,Snowflake 默认 8 MB 的 BINARY 长度。每一个都有文档,每一个都是在生产里撞见的,每一个都是数据变大之前本可以写下的尺寸检查。
  • 信任解码出来的字节。Base64 能装任何东西,包括一串引号。解码不是消毒。你对解码文本做的任何事(比较、记日志、拼接进另一条语句)都仍需要常规防护,而参数化查询在 base64 往返之后依然是参数化查询。

如何站在正确的一边

  • 先定类型,而不是先定函数。载荷是二进制还是文本?二进制去 BLOB/bytea/varbinary 并待在那里。文本走字符集工序,用显式编码。SQL 里一半的 base64 痛苦,是一个二进制载荷误入了文本列(或者反过来),现在正被人解释。
  • 解码前先校验,或者温和地解码。对字母表跑一遍正则,加上长度对四取模的检查,成本为零,能把停批处理的错误变成一个你能计数的 NULL。方言提供 try 形式时(ClickHouse 的 tryBase64Decode、Snowflake 的 TRY_BASE64_DECODE_BINARY),报表用它,严格形式留给绝不能猜的管道。
  • 版本检查要查到方言,而不只是数据库。ClickHouse 26.7 改变了空白处理,SQL Server 2025 才是第一次提供这个函数的版本,SQLite CLI 需要 3.41,ClickHouse 对填充的期望也随时间收紧。"它是 ClickHouse"不是规格说明;"它是 ClickHouse 24.8"才是。
  • 记录每一列的字母表。一列里既可能存标准 base64 又可能存 URL-safe base64,这一列迟早会让下一个开发者困惑。数据来自 JWT,就在 schema 注释里说明;来自 MIME 附件,也说明。解码器的选择是列的属性,不是查询的属性。
  • 存字节,在边缘编码。如果你掌控 schema,一个 BLOB 列加上 API 层的编码,在存储、索引和此后每一条查询上,都胜过 base64 文本列。列里的 base64 是兼容性税,而税最好在边界上只交一次。
  • 用金丝雀做往返。信任一条新解码路径之前,把已知载荷在同一个数据库里走一遍编码和解码再比较。金丝雀应包含一个非 ASCII 字符(演练字符集工序)、一个会留下填充尾巴的长度(演练填充规则),对 URL-safe 路径,某处还应有一个 -_(演练字母表翻译)。
  • 别把密钥放进查询文本。用 pgcrypto 做 JWT 验证会把共享密钥写进语句;配置审计会把明文密钥放进结果。两者都是正当的工作,但配得上的是受限账号、干净的日志和一次评审,而不是一条生产连接字符串加一个 SELECT * INTO OUTFILE

SQL 解包简史

base64 格式本身比互联网的有用部分还要老。它在 1990 年代中期为 MIME 标准化(RFC 2045 第 6.8 节,它取代了 1993 年承载该编码的 MIME 消息体规范 RFC 1521),而这个名字只是个数:字母表有 64 个字符。URL-safe 变体随 RFC 4648 在 2006 年到来,2015 年的 JWT 规范让这个变体成为你在令牌列里真正见到的那个。但数据库各自按自己的日程遇到了这个格式,而日程透露出每一个引擎的脾性。

2002 年。PostgreSQL 7.2 已经把 base64 列为 encode()decode() 的一等格式 - 与 9i 年代 Oracle 的 UTL_ENCODE 同期,以微弱优势成为这个家族里最古老的 base64 支持。一个拥有真正二进制类型和格式参数的数据库早早到位,因为答案只隔一个枚举值。

2000 年代初。Oracle 的 UTL_ENCODE 包出现在 9i 年代,在 MIME 头、quoted-printable 和 uudecode 函数旁边带着 base64。它是 RAW 进、RAW 出,非常 Oracle,并且保持这个形状已经一个世纪的四分之一。

2013 年。MySQL 5.6 加入 TO_BASE64()FROM_BASE64(),MariaDB 10.0 把两者都带进了分叉。这对搭档用 76 字符的行编码、以空白宽容解码,是一套在十几个主版本里纹丝不动的组合。

2018 年。ClickHouse 18.16 带着它的 MySQL 风格别名发布 base64Decode(),因为列式世界正在导入那些日志 schema 里已经带着 base64 的工作负载。

2023 年。SQLite 3.41.0 把 base64() 和它的 base85 兄弟作为应用定义函数加进命令行 shell。核心库一如既往地什么都没得到;shell,人类真正动手戳 SQLite 数据库的地方,得到了工具。

2025 年。SQL Server 2025,2025 年 11 月 GA,在缺席三十六年之后向 T-SQL 添加了 BASE64_DECODE()BASE64_ENCODE()。发布说明把它们当成一个不温不火的功能;社区把它们当成一次救援。

模式一旦看见就很干净。拥有真正二进制类型和格式参数的数据库(PostgreSQL,以及以其自身方式的 Oracle)在需求显而易见的那天就得到了 base64。剩下的(MySQL、SQL Server)把它当作字符串便利功能,排期也照此办理。而那个可嵌入引擎(SQLite)至今仍认为这是宿主应用的事,CLI 只是个友善的例外。

让你会心一笑的小事

  • SQL Server 从 1989 年到 2025 年一直过着没有 base64 解码器的日子,社区的回应是 CAST(N'' AS XML) 里一个叫 xs:base64Binary() 的 XML 函数。整整一代企业查询通过 XML 解析器解码令牌,因为 XML 解析器从 2001 年起就认识 base64,而 SQL 引擎不认识。
  • SQLite CLI 的 base64() 是这个家族里唯一的变形者:喂它 BLOB 它编码,喂它文本它解码。函数根据参数类型换工作,这是一点小号的 SQL 心灵感应,也是对不留心之人的真陷阱。
  • PostgreSQL 的编码器像 1996 年的 MIME 标准一样在 76 字符处折行,只是它用单个换行符结束每一行,而不是标准的回车加换行。规范颁布二十年后,每行少一个字符。解码器两种都忽略,所以这场叛乱肉眼不可见,除非你去 diff 输出。
  • mysql 客户端里,SELECT FROM_BASE64('aGVsbG8=') 打印出 0x68656C6C6F。不是因为数据是十六进制,也不是因为哪里出了错,而是客户端替你做了决定:二进制字符串应该显示为十六进制。这个设置叫 binary-as-hex,它已经让成千上万的开发者相信自己的解码器坏了。
  • Oracle SQL 层的 RAW 类型上限是 2000 字节,所以一份 3 KB 的证书甚至没法作为 RAW 字面量贴进 SQL 语句。解码必须在 PL/SQL 里分块、带循环地进行。这个上限来自 1990 年代;那个循环至今仍是推荐答案。
  • Snowflake 在每个结果集里都把 BINARY 值显示为十六进制,所以一次对 "hello" 完全成功的解码,到达你屏幕上时是 68656C6C6F。两种方言,两种十六进制显示,一种如出一辙的不安感。
  • ClickHouse 在它原生的 base64Decode() 旁边保留着别名 FROM_BASE64(),这是对 MySQL 难民的一点小小体贴,他们带着不这样改就跑不动的查询走了进来。
  • 整个家族共享一个安静的常识:base64 出门时收 33 的税,进门时退 25 的款,而这里八个解码器没有一个会主动告诉你这件事。格式是戏服;衣柜免费;量体裁衣才是这篇文章的主题。

继续前进

这篇文章一直在讲如何脱掉伪装:每个方言里的函数、它的脾气,以及穿着它的载荷(JWT、data URL、折行的邮件、JSON 字段、配置值、认证头)。另一个方向是另一种生物,自带一套惊喜:哪些编码器把输出折在 76 字符、哪些不折,如何产出令牌期望的那种 URL-safe 无填充形式,决定你列宽的尺寸算术,以及 SQL Server 的 36 年空窗对任何还停在老版本上的人意味着什么。这一切,从 TO_BASE64()BASE64_ENCODE(),都在关联的 SQL Base64 编码文章里深入展开,链接就在本页。在这里解码,在那里编码,整个往返一个下午就装得下。

最后更新: 2026-09-08

相关文章: SQL 中的 Base64 编码:完整指南