Base64 形式を扱う必要がありますか?それならこのサイトが最適です!データをエンコードまたはデコードするために便利なオンラインツールをご利用ください。

SQL での Base64 デコード:完全ガイド

本番のデータベースを十分に長く開き続けていれば、必ず偽装と出会う。JSON エクスポートの中で文字の壁として届いてきたアバター。ユーザー ID の隣にある varchar カラムに駐められた JWT。転送フォーマットにバイナリがなかったため、誰かが文字列で送ることにした証明書。どこかのテーブルの奥で、あなたのデータは文字の衣装をまとっている。あなたの仕事は、データベースの外に出ずにその衣装を脱がせることだ。

SQL では、その仕事にはひとつの非常に安心できる性質がある。自分の方言がどのデコーダーを話せるのか分かれば、仕事全体が 1 回の関数呼び出しに縮まるからだ。フォーマット自体はすでにトップページで詳しく説明してあるので(印字可能な 64 文字、4 文字のグループごとに 3 バイトの入りに立ち代わり、最後のグループには最大 2 つの = がパディングとして付く)、この記事はその講義を飛ばす。持って行ってほしいのは 2 つのこと:base64 はバイトにテキストの衣装を着せる方法であって、ロックではない。そしてデコードはデータが小さくなる方向(エンコード済みサイズの 3/4 に戻る)であり、これは保存カラムのサイズ設計とちょうど真逆のことだ。本当の物語は、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、3 つの状態 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 コマンドラインクライエントは、デフォルトでバイナリ文字列を 16 進表記で表示する(この設定は binary-as-hex という名前で呼ばれる)。だから素の SELECT FROM_BASE64('aGVsbG8=') は hello ではなく 0x68656C6C6F を表示する。これはバグでも破損でもなく、クライエントがバイナリデータに対して慎重になっているだけだ。文字が見たいなら、CONVERT(... USING utf8mb4) で変換するか、クライエントを --binary-as-hex=0 で起動すればいい。真ん中の行の HEX() 呼び出しは、クライエントがデフォルトで表示するあの 16 進の、意図的なバージョンだ。

さて、この無音のデコーダーが適用するルールだ。空白を無視した後の残り文字は、4 の倍数の長さでなければならない。すべての文字が標準アルファベット(英数字、+、/、=)から来ていなければならない。そしてパディングは最後の端にしか現れてはいけない:

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

3 行とも何の文句もなく実行され、2 行目と 3 行目は NULL を返す。パディング不足、長さ違い、謎の文字:返事は同じ肩すくめだ。唯一の許しは空白で、改行、キャリッジリターン、タブ、スペースはすべて無視される。メールを一度でも経由したデータにとっては、これこそ救いだ。一方、URL-safe アルファベットには肩すくめが返ってくる。アンダースコアは標準テーブルに載っていないので、FROM_BASE64('yv7K_g==') は長さがきれいに 4 の倍数であっても NULL になる。呼び出す前にアルファベットを自分で翻訳しなければならない。やり方は下の URL-safe セクションに書いてある。

知っておく価値がある特徴をもう一つ:デコーダーとエンコーダーは対でできている。エンコーダーは出力を 76 文字の行に折り返し、デコーダーはあの改行を朝飯前に食べる。カラムがこの同じデータベースファミリーの TO_BASE64() で埋められたものなら、デコードは完璧なラウンドトリップだ。別の何かが埋めたのなら、読み続けてほしい。

PostgreSQL:声を上げるデコーダー

PostgreSQL は少なくとも 2002 年のバージョン 7.2 から、コアに 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;

3 行目が、何度も手を伸ばすことになる一行だ: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、ヒント付き 入力データにパディングがなかったり、切り詰められていたり、その他の方法で破損しています。
  • URL-safe なアンダースコア:ERROR: invalid symbol "_" found while decoding base64 sequence

データクリーニングの仕事では、その声がまさに機能だ。クエリが失敗し、あなたは問題の行を見て、ソースを直す。代償は、百万行の中のたった 1 行の汚染でバッチ全体が止まることで、本番のパイプラインでは decode() を呼ぶ前に正規式で事前フィルタをかけるのがよくあることだ。そして表示に関する小さな注記:psql は bytea を \x 接頭辞付きの 16 進数で表示するので、\x68656c6c6f は MySQL クライエントが 0x68656C6C6F と表示するのと同じ「hello」だ。方言が 2 つ、16 進の方言も 2 つ。

SQL Server:遅れてきたメンバー

ファミリー全体の驚きがここにある。SQL Server はバージョン 2025 で BASE64_DECODE() を出荷し、2025 年 11 月に一般提供された。それまで、エンタープライズ界で最も人気のあるデータベースは 36 年間、内蔵の base64 デコーダーを持たず、伝説レベルの定番ネタは回避策で満ちていた。現代的な関数はきれいだ:varchar(n) または varchar(max) の式を受け取り、varbinary を返す(varchar(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;

3 行目が本当に気持ちよい所だ:デコーダーは RFC 4648 のアルファベットを両方受け入れる。+ と / の標準アルファベットと、- と _ の URL-safe アルファベットで、パディングは任意だ。4 つの空白文字(改行、キャリッジリターン、タブ、スペース)も無視する。壊れる時は、メッセージが 型「Base64Decode」のデータが無効です の Msg 9803, Level 16 となり、State の値がどのルールに当たったかを教えてくれる:どちらのアルファベットにもない文字なら state 20、すべての文字は有効でも base64 では作り出せない並び方なら state 21、パディングが頻出しすぎたり早すぎたりすれば state 23。

2025 年以前のバージョンに縛られているなら、定番の回避策は XML 型を借りるものだ。XML 型は、XML Schema の時代から base64 を理解している:

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

XML エンジンが定数を base64 デコードしてバイトを返してくれる。動くし、SQL Server の開発者たちの一代がそれで使ってきたものだ。ただ、端も持つ:base64Binary 型は形に対して厳格なので、中に改行を含む MIME ラップ済みの文字列はパースできない。しかも今や 1 つの関数がネイティブでこなす仕事に、XML 機構の料金を払っている。博物館の展示品として扱うのが正解だ。

SQLite:デコーダーを持たない方言

SQLite はファミリーの中で浮いている存在で、なぜかを理解すれば使い方も分かる。コアライブラリは小さな埋め込み可能なエンジンで、base64 は標準関数リストに載っていない。カラムに base64 が入っているなら、デコーダーは 4 つの場所のどれかから来なければならない:コマンドラインシェル、ロード可能な拡張機能、ホストアプリケーションが登録したカスタム関数、あるいは純粋な SQL。それぞれ見ていこう。

CLI。バージョン 3.41.0(2023 年 2 月)から、sqlite3 コマンドラインシェルには base64() 関数が同梱される。テキストの引数を BLOB にデコードするので、ターミナルからそのまま探索的な作業をするのにぴったりの関数だ:

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

知っておくべき性格はふたつ。第一に、寛大だ:認識できない文字は報告されるのではなくスキップされるので、base64('!!!') はエラーではなく空の BLOB を返す。好奇心には最高だが監査には危ない。出力では「空」と「欠落」は同じに見えてしまうから。第二に、この関数は変幻自在だ:BLOB の引数はエンコードされてテキストになる(72 文字の行で)、テキストの引数はデコードされて BLOB になる。名前はひとつ、仕事は 2 つ、引数の型で選ぶ。このファミリーの中で他のどのデコーダーもそうはしないので、入力の型は 2 回読んで確認しろ。

純粋な SQL。コアライブラリには base64 はないが、再帰 CTE と四則演算、そして(3.41.0 から)unhex() がある。これで数十行で本物のデコーダーが組める。レシピはこうだ:64 行のアルファベットテーブル、入力を 4 文字チャンクに切り分け、各チャンクを 24 ビットの数値に変え、その数を 3 バイトに分割し、バイトを 16 進として集めてから 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 カラムを持つテーブルに対して実行すれば、拡張機能もアプリケーションコードもなしに、1 行ごとに BLOB が得られる。計算は整数衣装に身を包んだ素の base64 だ:4 文字の各々が 6 ビットを寄与し、真ん中の 2 文字はバイト境界をまたぎ、最後の文字の下位 2 ビットは捨てられる。これはこのページの中で最も遅い選択肢(再帰パス 1 回とチャンクごとの 1 回のルックアップ)なので、小さなペイロードや一度きりの考古学に取っておくのが吉。長期稼働するアプリケーションでは、正直な答えは 3 つ目の選択肢だ:ホスト言語から 1 行のカスタム関数を登録し(Python の sqlite3 モジュールなら create_function() と標準の base64 モジュールで 2 行)、エンジンをネイティブ関数のように呼ばせる。4 つ目の選択肢、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;

3 行目が、想像以上に寛容なルールを示している:長さが 4 の倍数なら、パディングの欠落は問題にならず、AAEC はバイト 00 01 02 に普通にデコードされる。厳格さは、形が崩れた瞬間に顔を出す。DuckDB が求めるのは 4 の倍数の長さ、それだけだ。変換エラーも正確にそれを言う:

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

尊重すべき意見を 2 つ追加する。第一に、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;

2 行目が、ClickHouse のハウススタイルの現れだ。エンジンは try 接頭辞を好む:tryBase64Decode() は失敗を飲み込んで空文字列を返し、素の base64Decode() はコード INCORRECT_DATA と問題の値の名前を載せたメッセージで例外を投げる。悪い行がパイプラインを止めるべきなら素の形を、レポートを続行させるべきなら try 形を、と意図的に選ぶのだ。偶然で選んではいけない。

ClickHouse は動きが速いので、バージョンの注記を 2 つ。26.7 以前は入力の空白は拒否されていた。26.7 からは、スペース、タブ、改行、キャリッジリターン、フォームフィードはすべて無視される。メールやテキストエディタを一度でも触ったものには、これが欲しい挙動だ。また、現代的なデコーダーは 4 文字グループに正しいパディングを期待する。入りの途中でイコール記号を失ったトークンは、最善の試みではなく例外になる。2023 年に動いていたクエリが 2026 年に例外を投げ始めたなら、データを責める前にサーバーのバージョンを見ろ。

Oracle: RAW か、何もしない

Oracle の base64 機構は UTL_ENCODE PL/SQL パッケージの中に住み、それなりの個性を持つ:RAW を受け取り、RAW を返す。それ以外は何も。テキストは入ってこないし、出ていかない。VARCHAR2 はキャラクターセットを持つ文字データ、RAW は素のバイト。パッケージはそれ以上を装うことを拒否する。だから実動するパターンは 3 層のサンドイッチで、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 が明快であるという仕事を果たしているだけだ。

無効な入力は静かな NULL ではなく PL/SQL 例外を投げるので、バッチデコードは問題の行をログに記録する例外ハンドラの中に置くべきだ。パッケージには兄弟デコーダーの博物館まるごと乗っている:MIME ヘッダーのデコード、quoted-printable、uudecode、テキストエンコーディング、すべて同じ時代からのものだ。大半は base64 のペアを使うことになるだろうが、隣人たちはパッケージがなぜそのようになっているかを説明している:Oracle は「転送の衣装を纏ったデータ」にひとつの住処を欲しかったのだ。

始める前に知っておくべきサイズ罠がある。素の SQL では、RAW 値は 2000 バイトで頭打ちなので、約 1500 バイトを超える RAW にデコードされる base64 値は、単一の SELECT 文では到底デコードできない。より大きなペイロードには、BLOB を 2000 バイト(それ以下)のチャンク単位で歩を進め、各チャンクをデコードして、結果をまた縫い合わせる PL/SQL ループが必要だ。古風だが、これが Oracle の標準的な答えであり、言語の 1990 年代の型システムがあなたの 2020 年代のクエリを今も形づくっている場所のひとつだ。

Snowflake:アルファベットは持ち込み制

Snowflake はバイナリ型(BINARY)をテキスト型から分離し、このファミリーで最も設定可能なデコーダーを与えてくれる。主力は BASE64_DECODE_BINARY(input) で BINARY を返し、任意の第 2 引数はアルファベットを再定義する短い文字列だ:

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;

そのアルファベット引数をよく読んでほしい。位置指定だからだ。許可されるのは最大 3 文字:最初の 2 文字がアルファベット位置 62 と 63 を上書きする(デフォルトは + と /)、3 文字目がパディング文字を上書きする(デフォルト =)。「URL-safe アルファベットを使え」と言うには '-_' を渡す。「URL-safe アルファベットだが、パディングは代わりに % で」と言うには、実際に変えたいのがパディング文字だけであっても、3 文字全部の '-_%' を渡さなければならない。文字を省略すればデフォルトが残り、位置を飛ばして次の位置だけ埋めることはできない。

仲間の 2 つが一式を完成させる。BASE64_DECODE_STRING() はデコードとテキスト変換を 1 回の呼び出しで行うので、ペイロードがテキストなら 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 は黙って再解釈する。速いが、UTF-8 の世界に Latin-1 のペイロードがやってくる場合に、データベースが救ってくれるわけではない。SQLite は見ることすらしない。SQLite において TEXT 値とは、タグ付きのバイトにすぎないのだから。実務的なルール:キャラクターセットをデコード以前に決め、クエリにリテラルとして書き、非 ASCII 文字を含むペイロードでテストすること(定番の héllo 用の aMOpbGxv は良いカナリアだ。誤ったキャラクターセットごとに、壊れ方が異なるため)。本当にバイナリなペイロードなら、このセクションは丸ごとスキップして、バイトはバイトのままにしておく。

JWT:カラムの中の base64 の 3 つのドット

JSON Web Token は、データベースの中に見つける base64 の中で最も一般的なものだ。認証イベントはトークンごとログに記録されるから。JWT はドットで区切った 3 部構成:ヘッダー、ペイロード、署名。最初の 2 つは base64 に詰めた JSON オブジェクトで、ここからが人を翻弄するひねり:JWT が使うのは パディングのない URL-safe アルファベットで、標準のパディング付き形式ではない。/ はトークンが行き来する場所で新しいパスセグメントの始まりになり、+ はクエリ文字列でスペースと読まれ、パディングのイコール記号は純粋な儀式にすぎない。だから仕様(RFC 7515 と RFC 7519)は - と _ に切り替え、パディングを捨てた。

したがって、SQL でトークンをデコードするのは 4 ステップのダンスだ:ドットで分割し、URL-safe 文字を標準アルファベットに戻し、パディングを復元し、デコードして JSON をパースする。JSONB 型を持つ PostgreSQL は、それをやるのに快適な場所:

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"} として戻ってくる。結果から 1 つのクレームを引き出すのは、後続のクエリで claims->>'sub' とするだけでいい。パディングの復元が CASE 表現だ:base64url 文字列の長さが 4 の倍数に 2 文字足りないならイコール記号 2 つ、3 文字足りないなら 1 つ、ちょうど倍数なら不要。

もう一歩進めば、SQL で HS256 署名の検証すらできる。HMAC には PostgreSQL の pgcrypto 拡張機能を使う(まだ入っていないなら 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') がそれを詰め、3 つの文字列操作がトークンが持つパディングなし URL-safe 形に整形し直す。上のトークンとシークレットなら、答えは陽気な t だ。ただし注意書きは持って行ってほしい:これは HMAC アルゴリズム(HS256, HS384, HS512)でのみ動き、共有シークレットをデータベース文の中に置き、レポート・監査・デバッグのために作られている。実際にアクセスを門番するものは、本物の JWT ライブラリでアプリケーションレイヤーで検証すべきだ。

Data URL:文字列の奥にいる画像

Data URL フォーマット(RFC 2397)は、ウェブがファイルをリンクにインライン埋め込む方法だ:data:image/png;base64, の後に、ファイルの base64 が続く。ブラウザはクリップボードからペーストし、シングルページアプリは小さな画像をそこへ埋め込み、これらの流れはすべて最終的にデータベースのカラムに長いテキスト値として着地する。フォーマットは data:{メディアタイプ}[;{パラメータ}][;base64],{データ} で、デコードに重要なのは最初のコンマ以降のすべてだけで、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())で、形は完全に同じだ。

警告を 3 つ。第一に、すべての Data URL が base64 であるとは限らない。;base64 マーカーのない Data URL は代わりにパーセントエンコードされたテキストを運び、それを base64 デコーダーに食わせるのは間違いで、上の LIKE フィルタがそれを防ぐためにある。第二に、プレフィックスのメディアタイプは主張であって事実ではない。同じ文字列が image/png と謳いながら JPEG を含んでいることがある。中身が重要なら、デコード結果のマジックバイトをチェックせよ(PNG は 89 50 4E 47 で始まる、JPEG は FF D8 で)。第三に、Data URL は大きい。4 メガピクセルの写真は約 5.5 メガバイトの文字列になり、それはカラムサイズとメモリの話であって、文字列関数の話ではない。

URL-safe Base64:旅するアルファベット

RFC 4648 の 5 節は、base64 に 2 つ目のアルファベットを定義した。理由は、元のアルファベットに URL 構文で別の仕事を持っている 2 つの文字があるからだ。プラス記号はクエリパラメータが値を足す方法、スラッシュはパスを区切る方法で、パディングのイコール記号はクエリ文字列に会う瞬間にパーセントエンコードされる。URL-safe 変形は + を - に、/ を _ に差し替える(URL ではどちらも無害)、さらに JWT の仕様がパディングを丸ごと捨てる。その結果、パーセント記号ひとつなしに、リンク、パスセグメント、ファイル名、フラグメント識別子のすべてを旅できる。

データベースでそれに出会うのは、大半がデータがそこで生まれたからではなく、トークンやリンクが保存されたからだ。ネイティブで扱えるのは誰で、2 分の手作業マニュアルが必要なのは誰か:

方言 ネイティブな URL-safe デコード 備考
SQL Server 2025+ BASE64_DECODE() が両アルファベットを受け入れる 翻訳はまったく不要
ClickHouse 24.6+ base64URLDecode() + と / も引き続き受け入れる
Snowflake BASE64_DECODE_BINARY(s, '-_') アルファベットは位置引数
MySQL / MariaDB なし 文字を翻訳し、失敗時は NULL を想定
PostgreSQL なし 文字を翻訳し、失敗時はエラーを想定
Oracle なし RAW キャストの前に文字を翻訳
DuckDB なし(アンダースコアを拒否) 文字を翻訳し、長さを 4 の倍数に保つ
SQLite CLI なし 文字を翻訳。デコーダーは知らないものはスキップする

マニュアルは REPLACE() 呼び出し 2 つとパディング復元、で、どの方言でも同じだ。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;

- を + に戻し、_ を / に戻し、長さの 4 による剰余に基づいて欠けたパディングを足して、あとは標準デコーダーが引き継ぐ。入力 aGVsbG8(「hello」のパディングなし URL-safe 形)は、その言葉そのものとして戻ってくる。起き続ける 2 つの間違いは、CASE 表現が防いでいるものだ:パディングを忘れること(4 の倍数でなければ厳しいデコーダーが長さを拒否する)、そして文字の翻訳をスキップすること(URL-safe アルファベットを知らないデコーダーがアンダースコアで詰む)。翻訳を 1 回だけ、データベースの中で再利用可能な関数として書いておけば、この問題は二度と繰り返されなくなる。

ファイル、BLOB、そして大きいものたち

デコードは、ファイルがカラムの外に出る方法で、各方言の出口ドアはそれぞれ微妙に違う。DuckDB ではラウンドトリップは 2 つの文:ファイルを BLOB に読み込むものと、デコードしたバイトを書き戻すもの:

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

読み込み側:read_blob() はテーブル関数で、ファイル名、名のリスト、またはワイルドカードパターンを受け取り、ファイルごとに filename と content のカラムを返す。書き出し側は別の文:BLOB フォーマットの COPY は素のバイトを書き、引用符もエスケープもなし。デコードしたペイロードが求めるものはまさにそれだ。

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

PostgreSQL の出口ドアは Large Object API だ。Large Object は OID でアドレス付けされるサーバーサイドのバイナリチャンクストアで、lo_export() がそれをデータベースサーバー上のファイルに書き出す。スーパーユーザー権限または pg_write_server_files 権限が必要で、宛先はサーバープロセスが書き込めるパスでなければならない。だから実務的にはアプリケーションコードではなく、メンテナンススクリプトの仕事だ:

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

MySQL には制限の厳しい SELECT ... INTO DUMPFILE という脱出口だけがある(1 行のみ、サーバーサイドのパス、FILE 権限)。SQL Server には素の SQL のファイル書き手はまったくない(ディスクへの書き出しはクライエントまたはエージェントの仕事で、そのエクスポートツールを介す)。これは公平な設計だ:データベースはバイトを保管し、アプリケーションがファイルの行き先を決める。SQLite はスペクトルの反対側にいて、アプリケーションこそがホストであり、BLOB カラムはホスト言語の 1 回の呼び出しでそのままディスクに書ける。

それから天井がある。どれも似ているふりをするデータベースなのに、想像以上に差がある:

方言 バイナリ型 実用上の天井
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 はクライエントとサーバー間の 1 つのパケットのサイズに上限を設け、base64 文字列はそのパケットの一部だ。50 メガバイトの写真が base64 にエンコードされると約 67 メガバイトの文字列になり、64 メガバイトのデフォルトより大きい。結果は、クエリの中で読めるエラーではなく、切り詰められた値や NULL の値で、データ破損のように見える。大きなファイルを MySQL のカラムを通じて動かすなら、始める前にその上限を確認し、そこに対してカウントされるのは生バイトではなくエンコード後の形だということを覚えておこう。

メールの折り返しと MIME 行

メールシステムを生き延びた base64 は、必ずお土産を持っている:改行だ。MIME(メールがバイナリ添付を運べるようにする規格の集まり、RFC 2045 6.8 節)は base64 の出力を 76 文字で折り返し、行をキャリッジリターンと改行で終わらせる。この折り返しがあるのは、古いメールネットワークはそれより長い行を信用できなかったからで、あの頃からこのフォーマットは慣習で持ち込まれ続けた。つまり、データベースのカラムに保存された添付ファイルは、76 文字ごとに改行が入った base64 文字列であることが多い。あなたのデコーダーがあの改行とどのような関係を持つかで、この仕事は 1 つの文なのか 2 つの文なのかが決まる。

デコーダー 折り返しを食べる? 食べない場合
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 レイヤーで空白を除去

「先に除去する」修正は 1 つの式で、常に安全だ。空白は base64 のアルファベットの一部ではないから:正当なペイロードにスペース、タブ、改行が入ることはなく、それらを除去しても情報は壊れない。PostgreSQL での言い慣れは regexp_replace() 1 つ:

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

すべての空白文字、改行もろとも去り、デコーダーが見るのは 1 つのきれいな連続した文字列だけになる。これを DuckDB(2 つの改行文字に対する replace() を使う)や 26.7 以前の ClickHouse で実行すれば、ラップ済みの添付ファイルもラップなしのものとまったく同じようにデコードされる。

API ペイロード、設定、認証ヘッダー

個々の関数から一歩下がると、パターンが見えてくる:データベースのカラムの中の base64 は、ほぼ常に 3 つのもののどれかだ。JSON ドキュメントの中のフィールド(画像、証明書、API がインラインにすると決めたファイル)。設定値(あるツールが base64 を好むシークレットや認証情報。base64 は、引用符も改行もバックスラッシュも不要で YAML ファイルの 1 行に収まるから)。あるいは認証の産物(Basic 認証ヘッダー、保存されたトークン、セッションの blob)。それぞれにデコードの形を見ていこう。

JSON フィールド。JSON はテキストとして届き、フィールドは文字列で、base64 はその中に隠れている。自分の方言の JSON 関数でフィールドを取り出して、デコードする。MySQL ではチェーン全体が 1 つの式:

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 プレフィックスを剥ぎ取り、デコーダーが元のテキストを復元し、2 つの SUBSTRING_INDEX() 呼び出しがコロンで分割する。前半がユーザー、後半がシークレット。PostgreSQL では同じクエリが substring() と split_part() を使う。

設定値。ここでのデコード方向は監査の仕事:誰かが設定テーブルにシークレットを base64 で保存した(Kubernetes から受け継いだ癖で、そこではシークレット値は保存時に base64)。中身に何が実際にあるかを見たい、あるいは新しい環境が消費するエクスポートを作っている。形は値ごとに 1 つの SELECT で、値がテキストならキャラクターセット手順が適用される:

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

その結果には、それに見合う扱いをせよ。あなたはたった今、保存されたシークレットを見えるクエリ結果に変えたばかりだ。クエリを実行するアカウントが持つべき権限を持っていること、結果がログにコピーされないこと、設定の中の base64 という癖に目を向け直すことを確かめよ。base64 は転送であって金庫ではない。監査クエリこそ、それが明白になる瞬間だ。

噛みつく落とし穴たち

このリストのどの落とし穴も、少なくともひとつのコードベースで少なくともひとつの午後を奪い、どれも base64 自体ではなく SQL 方言の base64 扱い方に特有のものだ。

  • 無音の NULL。MySQL と MariaDB は悪い入力を何の文句もなく NULL にデコードする。デコード値で結合するレポートでは、そうした行はただ消え、「0 行」と「14 行が汚染されていたから 0 行」の差は、誰かがカウントが合わない理由を問い質すまで見えない。デコーダーが静かなタイプなら、NULL の数は意図して数えよ。
  • 4 の倍数ルール、不平等な適用。長さが 4 の倍数でない文字列は base64 ではないが、どうするかは方言で意見が割れる:PostgreSQL はエラーを上げ、DuckDB は変換エラーを上げ、ClickHouse は例外を投げ、MySQL は NULL を返し、SQLite CLI は静かにできる限りをデコードする。同じデータファイルが 5 つのデータベースで 5 つの違う結果を生む。だから「Postgres では動いた」はテストにならない。
  • アルファベット不一致。URL-safe トークン(JWT、リンク、ファイル名)を標準アルファベット用のデコーダーに食わせる:SQL Server は受け入れ、ClickHouse の base64URLDecode() は受け入れ、Snowflake は正しい引数で受け入れ、それ以外の全員が NULL を返すかエラーを上げるか、SQLite CLI のように静かにアンダースコアを落として、違うバイトを渡してくる。違うバイトの場合が悪質なのは、結果がもっともらしく見えるから。
  • MIME ラップ。改行を食べないデコーダー(DuckDB、26.7 以前の ClickHouse、Oracle)にラップ済みの入力を出すと失敗する。しかも失敗は「ここに改行がある」よりも「最後の 76 文字がゴミ」のように見えることが多い。エラーが指し示すのは、折り返しの後ろの文字だからだ。
  • 表示のトリック。mysql クライエントはバイナリを 16 進で表示し、psql は bytea を \x 16 進で表示し、Snowflake は BINARY を 16 進で表示し、Oracle は RAW を 16 進で表示する。クライエント 4 つ、16 進表記 4 種、そしてひとつの非常に人間的な間違い:画面に数字が表示されているからデータが壊れたと結論づけること。目で結果を読む前に、必ず明示的に変換せよ。
  • 場所違いのパディング。イコール記号が合法なのは末端だけで、1 つまたは 2 つまで。YQ==BQ== のような文字列は、ひとつのコスチュームを 2 つの正当なグループが着たもので、厳しいデコーダーは拒否し、寛容なものは誰も求めなかった何かにデコードする。保存された値の真ん中にパディングを見かけたら、書いたエンコーダーが壊れている。データを直すのは一度きりの仕事だ。
  • キャラクターセットの驚き。デコードは成功し、テキストは戻ってくるが、アクセントがおかしい。バイトは正しかった。解釈が間違っていたのだ。これは 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 の苦しみの半分は、テキストカラムに迷い込んだバイナリペイロード(またはその逆)が、今まさに解釈されつつあることからだ。
  • デコード前に検証するか、やさしくデコードする。アルファベットに対する正規式と、長さの 4 による剰余チェックは無料で、バッチを止めさせるエラーを数えられる NULL に変えてくれる。方言に try 形があるなら(ClickHouse の tryBase64Decode、Snowflake の TRY_BASE64_DECODE_BINARY)、レポートにはそれを使い、推測してはならないパイプラインには厳しい形を留める。
  • 検証するのはデータベースだけでなく、方言のバージョンだ。ClickHouse 26.7 は空白の扱いを変え、SQL Server 2025 は関数がある最初のリリースで、SQLite CLI には 3.41 が必要、ClickHouse のパディング期待も時間とともに厳しくなった。「ClickHouse である」は仕様ではない。「ClickHouse 24.8 である」が仕様だ。
  • 各カラムのアルファベットをドキュメント化せよ。標準も URL-safe も保持できるカラムは、次の開発者を困惑させるカラムになる。データが JWT から来たらスキーマのコメントに書き、MIME 添付から来たらそれも書く。デコーダーの選択はクエリの性質ではなく、カラムの性質だ。
  • バイトを保存し、端でエンコードする。スキーマを支配できるなら、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 変形は 2006 年の RFC 4648 と一緒に現れ、2015 年の JWT 仕様はその変形を、トークンカラムで実際に目にするものにした。しかし各データベースは独自のスケジュールでこのフォーマットと出会った。スケジュールは、それぞれの中身について何かを語っている。

2002 年。PostgreSQL 7.2 はすでに base64 を encode() と decode() の第一級フォーマットとして載せていた - Oracle の 9i 時代における UTL_ENCODE と同時代で、僅差でこのファミリーで最も古い base64 サポートだ。本物のバイナリ型とフォーマット引数を持つデータベースは早く着いた。答えは列挙型の値が 1 つ先にあったから。

2000 年代初頭。Oracle の UTL_ENCODE パッケージは 9i 時代に現れ、MIME ヘッダー、quoted-printable、uuecode の関数の隣に base64 を乗せていた。RAW が入って RAW が出る。とても Oracle であり、あの形は四半世紀変わらない。

2013 年。MySQL 5.6 が TO_BASE64() と FROM_BASE64() を追加し、MariaDB 10.0 はフォークに両方を持ち込んだ。このペアは 76 文字の行でエンコードし、空白を許容してデコードする。12 のメジャーバージョンにわたって変わっていない対だ。

2018 年。ClickHouse 18.16 が base64Decode() とその MySQL スタイルの別名を出した。カラム型の世界は、ログスキーマにすでに base64 を持ったワークロードを取り込んでいたから。

2023 年。SQLite 3.41.0 が base64() とその base85 の兄弟をコマンドラインシェルにアプリケーション定義関数として追加した。コアライブラリはいつものように何ももらわない。ツールをもらうのはシェルで、人間が実際に SQLite データベースをいじるのはそこだからだ。

2025 年。2025 年 11 月に一般提供された SQL Server 2025 が、36 年の不在のあとに BASE64_DECODE() と BASE64_ENCODE() を T-SQL に追加した。リリースノートはそれを控えめな機能として扱い、コミュニティは救援として扱った。

パターンは、見ればシンプルだ。本物のバイナリ型とフォーマット引数を持つデータベース(PostgreSQL、そしてそれぞれのやり方で Oracle)は、必要性が明白になった日に base64 を得た。残りは(MySQL、SQL Server)それを文字列の便宜機能として扱い、それに合わせてスケジュールした。そして埋め込みエンジン(SQLite)は今もそれをホストアプリケーションの仕事と考えている。CLI は友好的な例外として。

思わず微笑むものたち

  • SQL Server は 1989 年から 2025 年まで base64 デコーダーなしで過ごし、コミュニティの答えは CAST(N'' AS XML) の中の XML 関数 xs:base64Binary() だった。エンタープライズクエリ的一世代が XML パーサーを通じてトークンをデコードしてきた。XML パーサーは 2001 年から base64 を理解していたのに、SQL エンジンは理解していなかったから。
  • SQLite CLI の base64() はこのファミリーで唯一の変身芸人:BLOB を渡せばエンコードし、テキストを渡せばデコードする。関数は引数の型に基づいて仕事を替え、それは SQL による小さなテレパシーであり、不意打ちを食らう人々には本物の罠だ。
  • PostgreSQL のエンコーダーは 1996 年の MIME 規格と同様にちょうど 76 文字で折り返すが、行の終わりは規格のキャリッジリターンと改行ではなく、単独の改行で終える。規格から 20 年経って、1 文字少ない。デコーダーは両方を無視するので、出力の差分を比較しない限りこの反乱は目に見えない。
  • mysql クライエントでは、SELECT FROM_BASE64('aGVsbG8=') は 0x68656C6C6F を表示する。データが 16 進だからでも、何かがおかしいからでもなく、クライエントがあなたの代わりに、バイナリ文字列は 16 進で表示すべきだと決めたから。その設定の名前は binary-as-hex で、何千人もの開発者に自分のデコーダーが壊れていると信じ込ませている。
  • Oracle の SQL レベルの RAW 型は 2000 バイトで頭打ちなので、3 キロバイトの証明書は SQL 文に RAW リテラルとして貼り付けられたら、それが無理。デコードはチャンク単位で、ループで、PL/SQL において行わなければならない。制限は 1990 年代のもので、ループは今も推奨される答えだ。
  • Snowflake は BINARY 値をすべての結果セットで 16 進で表示するので、「hello」の完全に成功したデコードがあなたの画面に届くのは 68656C6C6F という形になる。方言が 2 つ、16 進表示が 2 つ、不穏さはまったく同じ 1 つ。
  • ClickHouse はネイティブの base64Decode() の隣に別名 FROM_BASE64() を残している。さもないと動かないクエリを持ってやってきた MySQL の難民たちへの小さな思いやりだ。
  • ファミリー全体がひとつの静かな事実を共有している:base64 は外に出るとき 33 パーセントの税金で、入るとき 25 パーセントの還付金で、ここにある 8 つのデコーダーのどれにも、聞かれなければそれを教えてくれるものはいない。フォーマットはコスチュームで、衣装室は無料、仕立てこそこの記事が扱っていることだ。

つづいて

この記事では、偽装を脱がせることを扱ってきた:各方言の関数、その性格、そしてそれを纏うペイロード(JWT、Data URL、ラップされたメール、JSON フィールド、設定値、認証ヘッダー)。もう一方の方向は別の生き物で、自分だけの驚きのセットを持っている:どのエンコーダーが出力を 76 文字で折り返し、どのエンコーダーがそうしないか、トークンが期待するパディングなし URL-safe 形をどう作るか、カラム幅を決めるサイズの計算、そして SQL Server の 36 年の空白が古いバージョンにいる誰かにとって何を意味するか。それらすべて、TO_BASE64() から BASE64_ENCODE() まで、は関連する SQL 向け Base64 エンコードの記事で詳しく扱われていて、このページからリンクされている。ここはデコード、あそこはエンコード。ラウンドトリップ全体は、ひとつの午前に収まる。

最終更新: 2026-10-09

関連記事: SQL での Base64 エンコード:完全ガイド