SQL での Base64 エンコード:完全ガイド
たまに、データベースは外の世界と話し合わなければならない。そし、外の世界は必ずしもバイトを話さない。ある API はあなたのロゴを JSON 文字列の中に求めている。設定のエクスポートは、引用符もバックスラッシュも不要で YAML の 1 行に収まるシークレットを求めている。メンテナンススクリプトは、テキストしか運べないシステムをまたいでファイルを届けたいと思っている。その瞬間に、あなたのデータは文字の衣装をまとってくれる。そしてあの衣装の名前は base64 だ。
フォーマット自体は、すでにトップページで扱っている(印字可能な 64 文字、その 4 つが 3 バイトの入りに立ち代わり、最後のグループには最大 2 つの = がパディングとして付く)ので、この記事はその講義を飛ばして、機械的な作業に直行する。覚えておいてほしいのは 2 つのこと:エンコードはデータが大きくなる方向なので、カラム幅もパケットの制限もその 1 バイトずつを肌で感じる。そしてこの SQL ファミリーのエンコーダーは、あとで取り消しにくい 2 つのこと - カラムがテキストを持っている時、どのバイトを読んでいるか、そして書き出すものにどこに改行を入れるか - について意見が割れている。
エンコーダー早見表
誰が当番で、何を食べ、出力のどこで壊れるか。噛みつくのは最後の 2 列だ。招かれざる改行だらけの文字列と、アルファベットが別の文字列は、どちらも完全な base64 文字列なのに、あなたの消費者はそれでも拒否するから:
| 方言 | 呼び出し | 入力型 | 76 で折り返す? | URL-safe オプション | いつから |
|---|---|---|---|---|---|
| MySQL 8.x / MariaDB 10.x | TO_BASE64(str) |
文字列(キャラクターセットが適用される) | はい | なし | MySQL 5.6(2013) |
| PostgreSQL | encode(bytea, 'base64') |
bytea |
はい、LF のみ | なし | 7.2(2002) |
| SQLite(CLI 3.41+) | base64(blob) |
BLOB |
はい、72 で | なし | 3.41.0(2023) |
| DuckDB | to_base64(blob) |
BLOB |
いいえ | なし | 最新リリース |
| ClickHouse 18.16+ | base64Encode(x) |
何でも、String にキャスト | いいえ | base64URLEncode() |
18.16(2018) |
| SQL Server 2025+ | BASE64_ENCODE(bin [, url_safe]) |
varbinary |
いいえ | 第 2 引数 | 2025 |
| Oracle | UTL_ENCODE.BASE64_ENCODE(raw) |
RAW |
いいえ | なし | 9i 時代 |
| Snowflake | BASE64_ENCODE(binary) |
BINARY |
いいえ | なし | 現行リリース |
テーブルを左から右に読めば、仕事は 2 つの決断に落ちる。第一に、バイトがどうそこに着くか:入力型の列が、キャラクターセットの驚きが生まれる場所だ。「同じテキスト」でも、コレーションによってバイトは異なるのだから。第二に、向こう側から何が出てくるか:折り返しの列が、結果が折り返しのない 1 行なのか、76 文字ごとに改行が入った詩なのかを決め、URL-safe の列が、結果をリンクに置けるのか置けないのかを決める。
まず、どのバイトを指しているかを決める
エンコーダーが詰めるのはバイトだが、あなたのカラムに入っているのはたいてい文字で、文字は「どのバイトのアルファベットか」を言わなければバイトにならない。MySQL の TO_BASE64() は引数を接続のキャラクターセットで読む。これは便利なのだが、そうでなくなる瞬間もある:同じ 'héllo' でも、latin1 クライエントと utf8mb4 クライエントでは別の base64 として出荷される。保存されているままの正確なバイトを指すなら、先にバイナリキャストで固定せよ:
SELECT TO_BASE64('hello') AS from_text;
SELECT TO_BASE64(CAST('héllo' AS BINARY)) AS utf8_bytes;
2 行目は aMOpbGxv として戻ってくる。真ん中の 2 つの 16 進バイト C3 A9 が、UTF-8 の é の綴り方だ。PostgreSQL は最初から厳しい:encode() は bytea 以外のものを見ることを拒否するので、テキスト値は先にエンコーディングを名乗らなければならない。一方、素のバイトは 16 進リテラルとして到着できる:
SELECT encode(convert_to('héllo', 'UTF8'), 'base64') AS text_as_utf8;
SELECT encode('\x68656c6c6f'::bytea, 'base64') AS raw_bytes;
他のすべての方言にも同じアイデアへのそれぞれの入口があり、どれも「バイトを得て、それから詰める」に集約される:
| 方言 | テキストからバイトへ | エンコーダーの呼び出し |
|---|---|---|
| T-SQL | CAST('héllo' AS VARBINARY(8000)) をカラムのコレーションを介して |
BASE64_ENCODE(bin) |
| Snowflake | TO_BINARY('héllo', 'UTF-8') |
BASE64_ENCODE(binary) |
| Oracle | UTL_RAW.CAST_TO_RAW('héllo') |
UTL_ENCODE.BASE64_ENCODE(raw) |
| DuckDB | encode('héllo') で BLOB を得る |
to_base64(blob) |
| SQLite CLI | X'68656C6C6F' のような BLOB リテラル |
base64(blob) |
実務的なルールはデコード側と同じだ:エンコード前にキャラクターセットを決め、クエリにリテラルとして書き、カラムを信頼する前にアクセント付きのペイロードを 1 つ、パイプライン全体に通せ。ひとつの héllo がすべての誤ったコレーションを捕まえてくれる。そしてコストは無料だ。
次に、出てくるものを観察する
バイトが詰められれば、エンコーダーは改行問題で道が分かれる。出力を折り返すのは 3 つ:MySQL と PostgreSQL は 76 文字(メールの癖)、SQLite CLI は 72 文字。残りはどれだけ長くても折り返しのない 1 行を返す。その違いは見逃しがやすく、見つけた時には高くつく。隠れた改行を含む base64 フィールドは、JSON パーサーを半道で壊すフィールドだからだ:
SELECT LENGTH(TO_BASE64(REPEAT('x', 300))) AS wrapped,
LENGTH(REPLACE(TO_BASE64(REPEAT('x', 300)), '\n', '')) AS flat;
300 バイトの入力は 400 個の base64 文字として戻り、同じ呼び出しが 405 と測るのは、5 つの改行が同乗したから。その裏の計算は頭に入れておけるほど小さい:折り返しなしの長さは、入力長を 3 で割って切り上げ、4 を乗じたもの。エンコーダーが折り返すなら、76 文字の行の間に 1 つの改行を足す。これは折り返しなしの長さを 76 で割って切り上げて、1 引いたもの。300 バイト:折り返しなし 400、折り返し付き 405。111 バイト:折り返しなし 148、折り返し付き 149。予算より 1 つ多い改行が、VARCHAR(500) カラムが VARCHAR(480) のペイロードを静かに切り詰め始める始まり方だ。
書き留める価値のある結果を 2 つ。書き手が折り返す可能性があるなら、テキストカラムは折り返しなしの長さに少しの余裕を持たせて設計するか、書き手側で折り返しを禁止して折り返しなしの長さで設計する。そして覚えておきたいのは、結果が戦う制限は文字列の制限であって、バイトの制限ではないということ:MySQL では折り返し付きのテキストが max_allowed_packet(MySQL 8 のデフォルトは 64 MB)に対してカウントされるので、約 67 メガバイトの文字にエンコードされた 50 メガバイトの写真は、生ファイルなら収まるのにデフォルトのパケットには収まらない。
URL-safe Base64:旅するアルファベット
RFC 4648 の 5 節は、base64 に 2 つ目のアルファベットを定義した。理由は、元のアルファベットに URL 構文で別の仕事をしている 2 つの文字があるからだ。プラス記号はクエリパラメータを足し、スラッシュはパスセグメントを区切り、パディングのイコール記号はクエリ文字列に会う瞬間にパーセントエンコードされる。URL-safe 変形は + を - に、/ を _ に差し替え、さらに JWT の仕様がパディングを丸ごと捨てる。だからトークンは、パーセント記号ひとつなしに、リンク、パスセグメント、ファイル名の中に置ける。
このファミリーで、そのスイッチをネイティブで同梱しているのは 1 つの方言だけ。SQL Server 2025 の BASE64_ENCODE() は任意の第 2 引数を取り、それが ON なら、結果は - と _ を使い、パディングをスキップする:
SELECT BASE64_ENCODE(0xCAFECAFE) AS standard;
SELECT BASE64_ENCODE(0xCAFECAFE, TRUE) AS url_safe;
同じ 4 バイトは yv7K/g== と yv7K_g として戻ってくる。ClickHouse は変形を別々の関数として保持し、その URL-safe 形もパディングを落とす:
SELECT base64URLEncode('https://clickhouse.com') AS url_safe;
到着するのは aHR0cHM6Ly9jbGlja2hvdXNlLmNvbQ で、標準形の 2 つのパディング記号が刈り取られている。それ以外のどこでも、レシピは 2 つの文字翻訳とトリミング。すべてのトークンパイプラインが必要とするので、データベース関数として 1 回書く価値がある。PostgreSQL ではこう読める:
SELECT rtrim(replace(replace(
encode(convert_to('https://clickhouse.com', 'UTF8'), 'base64'),
'+', '-'),
'/', '_'),
'=') AS url_safe;
+ を - に翻訳し、/ を _ に翻訳し、末尾のパディングをトリミングして、完成。SQL Server の人への警告を 1 つ:url_safe の出力は、サーバー自身の XML と JSON の base64 デコーダーが期待するものではない。つまり、外の世界向けに URL-safe 形で詰められたカラムは、組み込み関数ではデータベースの中で解けなくなる。アルファベットを選ぶ前に、読む相手を考えておけ。
JWT:データベースからトークンを発行する
エンコーダーで作れる中で最も面白いものは JSON Web Token だ。JWT とは、つまり base64 の断片 3 つの並列にすぎないから:ヘッダーとペイロード(どちらもパディングなしの URL-safe に詰めた JSON オブジェクト)と、最初の 2 つに対して計算された署名。バッチジョブがトークンを発行する必要に迫られた時(テスト環境のシード、期限切れ API 認証情報の再生成、監査フィードの構築)、HMAC に pgcrypto を受け入れるなら(CREATE EXTENSION IF NOT EXISTS pgcrypto; で一度有効化する)、その儀式全体が PostgreSQL の 1 つのクエリに収まる:
WITH head AS (
SELECT encode(convert_to('{"alg":"HS256","typ":"JWT"}', 'UTF8'), 'base64') AS h
),
body AS (
SELECT encode(convert_to('{"sub":"1234567890","name":"Dev User"}', 'UTF8'), 'base64') AS p
),
joined AS (
SELECT rtrim(replace(replace(h, '+', '-'), '/', '_'), '=') AS h64u,
rtrim(replace(replace(p, '+', '-'), '/', '_'), '=') AS p64u
FROM head, body
)
SELECT h64u || '.' || p64u || '.' ||
rtrim(replace(replace(
encode(hmac((h64u || '.' || p64u)::bytea, 'sql-secret-key'::bytea, 'sha256'), 'base64'),
'+', '-'),
'/', '_'),
'=') AS token
FROM joined;
各ステップは、この記事がすでに示した手のうちの一つ:JSON を base64 に詰め、パディングなし URL-safe アルファベットに整形し、最初の 2 つに署名して、署名も同じように整形する。上の JSON とシークレット sql-secret-key に対して、結果は eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRldiBVc2VyIn0.7_3VdmM8vH0L2bRBNqXXQXzZIR1l8T4_S4QxPPNBEFM で、どんな HS256 検査官も受け入れるトークンになる。注意書きは、その手並みに匹敵する時間を値する:これは HMAC アルゴリズム(HS256, HS384, HS512)のみをカバーし、共有シークレットをデータベース文の中に置き、本番のトークンサービスではなくバッチと監査の仕事のために作られている。トークンをそのシークレットに対して証明する検証側は、アプリケーションレイヤー、あるいはデコード記事の署名チェックの仕事だ。
テキストカラムの中の画像とファイル
SQL でエンコードする最も一般的な理由は、テキストとして旅しなければならないファイルだ:画像を参照する代わりにインラインにする API、バイナリを運べないシステム向けのエクスポート、新しいサーバーでデータベースを再作成するシードスクリプト。DuckDB はこのラウンドトリップをほぼ自明にする。ワイルドカードパターンを受け入れるテーブル関数を通じてファイルを BLOB に読み込み、エンコーダーは届いたものを折り返しなしにしてくれるから:
SELECT filename, to_base64(content) AS b64
FROM read_blob('/data/pics/*.png');
ファイルごとに 1 行、行ごとに折り返しなしの base64 文字列。取り除く折り返しはなしだが、いつもの 1 つか 2 つのパディング記号が末尾に同乗している。結果をテキストカラムに書けば、画像はテキストを動かすどんな経路でも持ち運べる。それから、コストの話を正直に:1 メガバイトの写真は約 1.33 メガバイトの文字として到着し、それからはすべてのスキャン、ソート、インデックスエントリがその代金を払う。スキーマを支配できるなら、より良い設計は BLOB カラムと API 境界でのエンコード:実際に建物を出るバイトだけが衣装をまとえる。
HTTP、JSON、API トラフィック
ヘッダーとペイロードこそ、base64 がその静かな日常の仕事をする場所だ。Basic 認証ヘッダーはリテラルのプレフィックス Basic の後に username:password の base64 が続くもので、SQL でそれを組み立てるのは連結 1 つとエンコード 1 つ:
SELECT 'Basic ' || encode(convert_to('alice' || ':' || 's3cret', 'UTF8'), 'base64') AS header;
これが Basic YWxpY2U6czNjcmV0 を組み立てる。クライエントが送るであろうヘッダーそのものだ。それを使って統合テストが比較するフィクスチャを生成し、あるいは監査の前に保存されたヘッダーのカラムを正規化する。JSON 側では、MySQL はフィールドを詰め、ドキュメントの中に埋め込むことを 1 つの式ででき、アプリケーションコードはループの外:
SELECT JSON_OBJECT('img', TO_BASE64(CAST('hello file' AS BINARY))) AS doc;
結果は {"img": "aGVsbG8gZmlsZQ=="} で、すぐ送り出せるペイロード。同じ形は、証明書、公開鍵、API がインラインにすると決めた他のどんなファイルでも効く。あなたがトラフィックをデコードしているのではなく生産している場合に、効いてくる方向だ。
設定ファイル、シークレット、環境変数
1 つのエクスポートの癖が、専用の段落を値する。そこらじゅうにあるから:設定テーブルに base64 で保存されたシークレット。Kubernetes がこの癖を生かしてきた。そこではシークレット値は保存時に base64 で、引用符も改行もバックスラッシュも不要で YAML の 1 行に収まる。Kubernetes のパイプラインに一度でも会った社内設定システムは、全部これを受け入れた。詰める方向は、値ごとにエンコード 1 つ。バイナリキャストがキャラクターセットの仕事をして、エクスポートされたテキストが保存されているバイトそのものになる:
SELECT name, TO_BASE64(CAST(value AS BINARY)) AS for_config
FROM app_config
WHERE name LIKE '%_secret%';
57 バイトまでの各値は、折り返しなしでそのまま貼り付けられる文字列として出てくる。それより長いものは、YAML に入る前に折り返し行を剥がす REPLACE() が 1 つ必要で、新しい環境は向こう側でそれをデコードして戻す。その結果には、それに見合う扱いをせよ。あなたはたった今、保存されたシークレットのカラムを、どんな人間でも約 10 秒で読める保存されたシークレットのカラムに変えたばかりだ。プレーンテキストまでこんなに近い距離に立っている今こそ、設定の中の base64 という癖に目を向け直す時だ。base64 は転送であって金庫ではない。環境に本物のシークレットストアがあるなら、base64 カラムはそこから 1 つの移行の距離にある。
メールと 76 文字の癖
76 文字の折り返しは、このページの全データベースより古い。MIME(メールがバイナリ添付を運べるようにする規格の集まり、RFC 2045 6.8 節、1996 年)は base64 の出力を 76 文字で折り返し、各行をキャリッジリターンと改行で終わらせる。古いメールネットワークは、それより長い行を信用できなかったから。ここでは 3 つのエンコーダーが、その折り返しをデフォルトとして受け継いだ(MySQL、PostgreSQL、SQLite CLI)。メールにたどり着いたものにとっては贈り物で、たどり着かなかったものにとっては罠だ。しかも半端な形で受け継いだ:PostgreSQL は行を、MIME 規格が指定するキャリッジリターンと改行ではなく、単独の改行で終わらせる。本当のメール添付に貼り付けられるべき出力には、あと 1 工程必要だ:
WITH t AS (
SELECT replace(encode(attachment::bytea, 'base64'), chr(10), '') AS s
FROM email_outbox
)
SELECT regexp_replace(s, '(.{1,76})', '\1' || chr(13) || chr(10), 'g') AS mime_ready
FROM t;
PostgreSQL がすでに加えた改行を剥がし、それから 76 文字で再折り返しして各行の後に完全な CRLF を付ければ、文字列は 1 つの式で MIME 正しいものになる。300 バイトを通してみせれば、あなたが詰めた 400 文字は 412 になる:折り返し行 6 つ、CRLF ペア 6 つ、転送の儀式 12 文字。同じ形の問題は、76 ではなく 64 で折り返す PEM ブロックにも現れ、フィールドの真ん中で改行に会うのが嫌いな JSON パーサーを持つ API にも現れる。すべてへのルール:エンコードする前に、消費者がどの契約に署名したかを確かめよ。保存された base64 のカラムを再折り返しするのは、クエリではなく移行だから。
落とし穴:エンコーダーが嘘をつきかける場所
このリストの罠はどれも、base64 が仕掛けるものではなく、特定の方言が仕掛けるもの。そしてどれも、本番でそれを見つけたコードベースが少なくともひとつある:
- 招かれざる折り返し。MySQL と PostgreSQL はデフォルトで 76 で出力を折り返し、SQLite CLI は 72。あなたのクエリの中でそれを求めた者はいないのに。JSON の base64 フィールドに改行が入り、仕様の形式は受け入れていた消費者が、現実のものは拒否する。折り返しなしの形は改行文字に対する
REPLACE()1 つで、書き手側で適用すれば、カラムは読み手が求めるものを保存できる。 - キャラクターセットのすれ違い。バイナリキャストのない
TO_BASE64('héllo')は、接続のキャラクターセットがその文字を何だと思うかをエンコードする。そしてlatin1クライエントとutf8mb4クライエントは違うことを思っている。同じクエリテキスト、2 つの違う base64 結果。そして悪いほうは、誰にもエンコーダーまで遡られないもじれつにデコードされる。バイナリキャスト、あるいは明示的なconvert_to()が、このクエリの唯一の正直な形だ。 - BLOB のドア。DuckDB の
to_base64()はBLOBを欲しがる。テキストを渡せば暗黙のキャストをしてしまうので、UTF-8 でないエンコーディングのvarcharカラムは、静かに違うバイトをエンコードしてしまうことがある。正直な道はto_base64(encode(...))で、だからこのページの例はテキスト入力に対して常にこのペアを見せている。 - 26.7 の空白変更。ClickHouse の
base64Decode()とbase64URLDecode()は長年、入力に対して厳格だった。26.7 から、空白(スペース、タブ、改行、キャリッジリターン、フォームフィード)を拒否する代わりに無視するようになったので、ラップ済みのカラムでエラーだったスクリプトが今や静かに成功する。これはまた別の種の回帰だ。デコードを信頼する前に、サーバーのバージョンをチェックせよ。 - 6000 バイトの境界。SQL Server の
BASE64_ENCODE()は、入力が n が 6000 以下のvarbinary(n)の時はvarchar(8000)を返し、それより上是varchar(max)を返す。マッピングは値ではなく宣言サイズに鍵がかかるので、varbinary(8000)カラムは 3 バイトでもvarchar(max)を返す。一方、同じ関数のurl_safe出力は、パディング付きの標準アルファベットを期待するサーバー自身の XML と JSON の base64 デコーダーには読めない。読む相手に合わせて変形を選べ。 - 2000 バイトの RAW。Oracle では、素の SQL 文における
RAW値は 2000 バイトで頭打ちだ。エンコーダーはRAWを受け取りRAWを返すので、単一文のエンコードが受け入れられるのは約 1500 バイトの入力まで(2000 文字の出力なら収まる、それ以上は収まらない)。単一文のデコードが受け入れられるのは 2000 文字の base64 まで。大きなペイロードは PL/SQL へ移り、そこではRAW変数が 32767 バイトを持ち、1 回の呼び出しでその大半を運べる。base64 出力があの天井を超えるペイロードだけがチャンクループを必要とする。そのループは、一度も動かない 1990 年代の制限から来たものだ。 - パケットの税。MySQL が
max_allowed_packetに対してカウントするのは、生バイトではなくエンコード後の文字列だ。テーブルには余裕で収まる写真でも、33 パーセント大きくなって折り返されればパケットをオーバーしうる。失敗の形は、切り詰められた値か、データ破損のように見えるNULLだ。カラム幅と同時にこの制限も確認せよ。 - 末尾の改行。SQLite CLI の
base64()は最後の行を改行で終わらせる。行末の癖が最終行にも適用されるのだ。シェルの出力を JSON フィールドにペーストすれば、改行を 1 つ含んだ base64 文字列を発送したことになる。招かれざる折り返しが、別の帽子をかぶってやってくるのだ。 - アルファベットの前提。標準アルファベット用に作られた消費者があなたの URL-safe 出力に会う(またはその逆)、知らない文字を見せる。ほとんどのデコーダーはアンダースコアで盛大に失敗する。いくつかは、それをスキップして静かに失敗する。次の開発者はどのトークンパイプラインが行を書いたか覚えていないので、base64 カラムすべてのアルファベットをスキーマのコメントにドキュメント化せよ。
サイズが本当に効いてくる時
サイズ計算は、折り返しなしの長さにあなたのエンコーダーが足す折り返しを足したもので、比率の完全な導出はトップページでやっている。ここでやる価値があるのは、その数が「ただの気楽な知識」ではなくなる地点を巡ることだ。入力バイト数に合わせてサイズを決めた VARCHAR カラムは、ペイロードが余分な余裕を必要とするほど長くなった最初の回に、出力を静かに切り詰める。3 バイトの入力は 4 文字のコストだからだ。base64 テキストカラム上のインデックスは、税を 2 回払う:格納時に 1 回、すべての比較でまた 1 回。インデックスエントリは折り返された文字で、バイトではないから。多くの人に最初にぶつかる 2 つの壁は、MySQL の max_allowed_packet と PostgreSQL の 1 GB の bytea 天井。どちらもテキストに対してチェックされ、それは取引の大きな側だ。設計の答えは、違うエンコーダーを選ぶことではほとんどない(base64 はひとつしかない)。エンコーディングをどこで起こすかを選ぶことだ。BLOB カラム、参照用のハッシュカラム、境界でのエンコード:base64 は、それが属する場所であるトラフィックの中でだけ存在する。
セキュリティ:base64 ではないもの
base64 は暗号化ではない。声に出して言わなければならない癖は 1 つだけあって、それは先ほどの設定テーブルの方だ:base64 で保存されたシークレットとは、別のフォントで保存されたシークレットにすぎない。その変換は鍵のない双射で、地球上のすべてのプログラミング言語が 1 回の関数呼び出しで逆算できる。唯一の実質的な効果は、値を YAML の 1 行に保つことだ。脅威モデルにこのデータベースの別のユーザー、エクスポートを読む別のサービス、行を捉えたログが含まれるなら、base64 が防御に寄与するのはちょうどゼロだ。人間の目を数秒だけ欺く。だからコードレビューでは保護のように感じ、インシデントでは失敗するのだ。秘密でなければならないものは暗号化せよ。誰かが実際に秘密に保てる鍵で暗号化し、base64 に得手な仕事をさせよう:テキストしか運べない経路でバイトを運ぶこと。
各方言が折り返しを覚えた時期
リリースノートは、デコード側が語ったのと同じ物語を語り、ただ文字が行く方向が逆だ。スケジュールは、各エンジンについて何かを語っている:
2002 年。PostgreSQL 7.2 は base64 を encode() と decode() の第一級フォーマットとして載せた。Oracle の 9i 時代における UTL_ENCODE と同時代で、僅差でこのファミリーで最も古い base64 機構だ。本物のバイナリ型とフォーマット引数を持つデータベースは早く着いた。答えは列挙型の値が 1 つ先にあったから。
2000 年代初頭。Oracle の UTL_ENCODE パッケージは 9i 時代に BASE64_ENCODE() を出荷した。MIME ヘッダー、quoted-printable、uuecode の兄弟の隣に並んで。RAW が入って RAW が出る。四半世紀が経っても、パッケージは考えを改めない。
2013 年。MySQL 5.6 が対として TO_BASE64() と FROM_BASE64() を追加し、MariaDB 10.0 は両方を継承した。このペアの契約はその間一切動いていない:出ていく時は 76 文字の行、入ってくる時は空白の許容。
2018 年。ClickHouse 18.16(2018 年 12 月)が base64Encode() と base64Decode() を MySQL スタイルの別名とともに出荷した。カラム型の世界は、ログスキーマにすでに base64 を持ったワークロードを取り込んでいたから。
2023 年。SQLite 3.41.0 がコマンドラインシェルに base64() をアプリケーション定義関数として追加した。コアライブラリはいつものように何ももらわない。ツールをもらうのはシェルで、人間が実際に SQLite ファイルをいじるのはそこだから。
2025 年。2025 年 11 月に一般提供になった SQL Server 2025 が、ようやく BASE64_ENCODE() と BASE64_DECODE() を出荷した。プロダクトのローンチから 36 年、そのユーザーが XML の回避策を暗記してから 1 世代が経ったあとのことだ。
パターンは、デコード記事が締めくくったものと同じで、裏返している:本物のバイナリ型とフォーマット引数を持つエンジンは、必要性が明白になった日に base64 を得た。すべてが文字列のエンジンは、それを後回しにスケジュールしたのだ。
知っておく価値のある珍現象
- SQLite CLI の
base64()はこのファミリーの変身芸人で、エンコード方向ではその芸が最もよく見えてくる:BLOBを渡せば末尾に改行を持つ折り返し付きテキストを返し、テキストを渡せばBLOBを返す。名前はひとつ、仕事は 2 つ、引数の型で選ぶ。このファミリーで他のどのエンコーダーもそうはしない。 - ClickHouse の
base64Decode()ファミリーは 26.7 で寛大になった:入力の空白は拒否する代わりに無視されるようになったので、ラップ済みのカラムに対する同じクエリは古いサーバーでは失敗し、新しいサーバーでは静かに値を返す。デコーダーは壊れなかった。緩和しただけで、なんとそれがデバッグしにくいのだ。 - PostgreSQL は 1996 年の MIME 規格と同様にちょうど 76 文字で折り返すが、行の終わりは規格のキャリッジリターンと改行ではなく、単独の改行で終える。規格から 20 年以上経って、1 行あたり 1 文字少ない。バイトの差分を比較しない限り、この反乱は目に見えない。
mysqlクライエントでは、エンコードしたバイトは base64 テキストとして普通に印字される。だがCAST(... AS BINARY)で生のカラムを見るときの瞬間に、クライエントは 16 進表示(binary-as-hex)に切り替わり、まったく健全なhelloが画面に届くのは0x68656C6C6Fという形になる。この設定は、何千人もの開発者に自分のエンコーダーが壊れていると信じ込ませている。- Snowflake は
BINARY値をすべての結果セットで 16 進で表示するので、エンコードクエリのTO_BINARY()入力カラムは、すべてが正しく動いてもチェックサムのように読めてしまう。方言が 2 つ、16 進表示が 2 つ、不穏さはまったく同じ 1 つ。 - Oracle の SQL レベルの
RAWは 2000 バイトで頭打ちなので、3 キロバイトの証明書は SQL 文にRAWリテラルとして貼り付けられたら、それが無理だ。エンコードは PL/SQL で行わなければならない。そこではRAW変数が 32767 バイトを持つので、3 キロバイトの証明書は 1 回の呼び出しで済む。base64 出力が 32 キロバイトを超えるペイロードだけが、1990 年代に遡るチャンクループを必要とする。 - MIME base64 の 1 行は 76 文字で、それは 57 の生バイトに等しい。4 文字が 3 バイトを運ぶから。3 つのエンコーダーのデフォルトに現れる 76 という数字は、制限というより詰め密度だ:古いメール添付で見る折り返し行のすべてが、あなたのデータのちょうど 57 バイトを運んでいた。
もう一方の方向へ
この記事では、衣装を着せることを扱ってきた:どのバイトを指しているかを決める、出てくるものを観察する、読む相手に合わせてアルファベットを選ぶ、カラムが切り詰める前にサイズの計算をする。衣装を脱がせるのはまったく別の性格の作業で、ある方言は肩をすくめて無音の NULL を返し、別の方言は声を上げるようにハードエラーになる。そしてファミリーの半分は知らない、URL-safe アルファベットもある。それらすべて、FROM_BASE64() から decode() から BASE64_DECODE() まで、は関連する SQL 向け Base64 デコードの記事で、ちょうど下にあるリンクから詳しく扱われている。ここはエンコード、あそこはデコード。ラウンドトリップ全体は、ひとつの午前に収まる。
最終更新: 2026-10-09
関連記事: SQL での Base64 デコード:完全ガイド