Приходится иметь дело с форматом Base64? Тогда этот сайт идеально вам подойдет! Воспользуйтесь нашим невероятно удобным онлайн-инструментом для кодирования или декодирования ваших данных.

Декодирование Base64 в SQL: полное руководство

Откройте любую боевую базу данных подольше - и вы встретите маскировку. Аватар, приехавший сплошной стеной букв внутри JSON-экспорта. JWT, припаркованный в колонке varchar рядом с ID пользователя. Сертификат, который кто-то решил отправить строкой, потому что в формате передачи не было бинарного. Где-то в таблице ваши данные одеты в буквы, а ваша задача - снять их, не выходя из базы данных.

В 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 ошибка, либо NULL в варианте TRY_ актуальные релизы

Обратите внимание на форму таблицы: имя функции - никогда не сложная часть. Сложная часть - колонка «когда ломается», потому что именно она решает, потеряет ли ваш отчёт строки молча, или пакетная задача остановится и попросит помощи.

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. Это ни баг, ни порча данных; это осторожность клиента перед бинарными данными. Если вам нужны буквы, преобразуйте через 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-безопасный алфавит, напротив, получает пожатие плечами в ответ: подчёркивание в стандартной таблице отсутствует, так что FROM_BASE64('yv7K_g==') - это NULL, даже при чистой кратной четырём длине. Алфавит придётся перевести самому перед вызовом, а как именно - покажет раздел про URL-безопасный ниже.

Ещё одна черта, которую стоит знать: декодер и энкодер - пара, собранная на заводе. Энкодер режет свой вывод на строки по 76 символов, а декодер поглощает эти переносы на завтрак. Если колонку заполнял TO_BASE64() в этой же семье баз данных, декодирование - идеальный круговой путь. Если заполнил кто-то другой - читайте дальше.

PostgreSQL: декодер, который повышает голос

PostgreSQL носит base64 в своём ядре по крайней мере с версии 7.2, ещё 2002 года, что делает его старейшим 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, с подсказкой Входные данные не имеют заполнителя, оборваны или иначе повреждены.
  • URL-безопасное подчёркивание: ERROR: invalid symbol "_" found while decoding base64 sequence

Для работы по чистке данных этот голос - черта, а не изъян. Запрос падает, вы видите строку, вы чините источник. Цена за это: одна отравленная строка из миллиона останавливает всю партию, поэтому в производственных конвейерах перед вызовом decode() часто делают предварительную фильтрацию регулярным выражением. И небольшая заметка про отображение: psql печатает bytea как шестнадцатеричное с префиксом \x, так что \x68656c6c6f - это то же самое «hello», которое клиент MySQL показывает как 0x68656C6C6F. Два диалекта, два шестнадцатеричных диалекта.

SQL Server: опоздавший

Вот сюрприз всей семьи. SQL Server отгрузил BASE64_DECODE() в версии 2025, общедоступной с ноября 2025 года. До этого самая популярная база данных в корпоративном мире тридцать шесть лет обходилась без встроенного 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;

Третья строка - действительно приятная деталь: декодер принимает оба алфавита RFC 4648, стандартный с + и / и URL-безопасный с - и _, а заполнитель необязателен. Он также игнорирует четыре пробельных символа (перенос строки, возврат каретки, табуляция, пробел). Когда же он ломается, ошибка - Msg 9803, Level 16 с текстом Некорректные данные для типа «Base64Decode», а значение State говорит, в какое правило вы попали: состояние 20 - символ не из ни одного из алфавитов, состояние 21 - все символы валидны, но расположены так, что base64 из них не собрать, и состояние 23 - заполнитель, который встречается слишком часто или слишком рано.

Если вы застряли на версии до 2025 года, классический обходной путь берёт напрокат тип XML, который понимает base64 ещё со времён XML Schema:

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

XML-движок декодирует константу из base64 и возвращает байты. Это работает, и именно этим пользовалось целое поколение разработчиков SQL Server. Но тут тоже есть края: тип base64Binary строг к форме, так что MIME-обёрнутая строка с переносами внутри не распарсится, а вы платите цену XML-механизма за работу, которую теперь нативно делает одна функция. Относитесь к нему как к музейному экспонату, которым он стал.

SQLite: диалект без декодера

SQLite - чужак в этой компании, и понимание причины говорит о том, как его использовать. Ядро - это маленький встраиваемый движок, и base64 в его стандартном списке функций отсутствует. Если в колонке лежит base64, декодер придётся взять откуда-то из четырёх мест: командная оболочка, загружаемое расширение, пользовательская функция, зарегистрированная хост-приложением, либо чистый SQL. Разберём каждое.

CLI. Начиная с версии 3.41.0 (февраль 2023 года), командная оболочка sqlite3 поставляется с функцией 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 в целочисленной одежде: каждый из четырёх символов вносит шесть бит, два средних пересекают байтовую границу, а два младших бита последнего символа выбрасываются. Это самый медленный вариант на странице (рекурсивный проход плюс поиск на каждый кусок), так что держите его для малых полезных нагрузок и разовой археологии. Для долгоживущего приложения честный ответ - третий вариант: зарегистрировать однострочную пользовательскую функцию из хост-языка (модуль sqlite3 в Python делает это за две строки через 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-безопасные токены придётся перевести до того, как они прибудут (рецепт - в разделе про URL-безопасный). Второе: у него нет никакой терпимости к пробельным символам. MIME-обёрнутое вложение с его переносами строк на 76 символах внутри упадёт, и исправление - 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 или ничего

Base64-механизм Oracle живёт в PL/SQL-пакете UTL_ENCODE, и у него собственный характер: он принимает 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, кодирование текста - всё из той же эпохи. Вы будете пользоваться в основном base64-парой, но соседи объясняют, почему пакет устроен так, как устроен: Oracle хотел один дом для «данных в транспортной одежде».

Одна ловушка с размерами, о которой стоит знать до начала. В простом SQL значение RAW ограничено 2000 байтами, так что base64-значение, декодирующееся больше примерно в 1500 RAW-байтов, нельзя декодировать одним SELECT в одиночку. Большие пелоды требуют цикла PL/SQL, который идёт по BLOB кусками по 2000 (или меньше) байтов, декодирует каждый кусок и сшивает результаты обратно. Это старая школа, но это стандартный ответ 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-безопасный алфавит», передаёте '-_'. Чтобы сказать «URL-безопасный алфавит, но заполняй символом %», нужно передать все три символа, '-_%', хотя на самом деле вы хотите сменить только символ заполнителя. Опустите символы - и получите значения по умолчанию; пропустить позицию и заполнить следующую нельзя.

Два спутника доводят комплект до конца. BASE64_DECODE_STRING() делает и декодирование, и преобразование в текст за один вызов, так что TO_VARCHAR() можно пропустить, когда пелод - текст. А варианты TRY_, TRY_BASE64_DECODE_BINARY() и TRY_BASE64_DECODE_STRING(), возвращают NULL на плохом значении вместо того, чтобы поднимать ошибку, - это снейкфлоковская версия try-формы ClickHouse.

От байтов к тексту: шаг кодировки

Декодирование передаёт вам байты. Если пелод - это документ, имя, фрагмент 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 Tokens - самый частый base64, который вы найдёте, сидящим в базе данных, потому что события аутентификации логируются вместе со своими токенами. JWT - это три части, разделённые точками: заголовок, пелод и подпись. Первые две - JSON-объекты, упакованные в base64, и вот поворот, на котором спотыкаются: JWT используют URL-безопасный алфавит без заполнителя, а не стандартную форму с заполнителем. / начинал бы новый сегмент пути там, где токены часто путешествуют, + прочли бы как пробел в строке запроса, а знаки равенства заполнителя были бы чистой формальностью, поэтому спецификация (RFC 7515 и RFC 7519) перешла на - и _ и сбросила заполнитель.

Поэтому декодирование токена в SQL - это танец из четырёх шагов: разрезать по точкам, вернуть URL-безопасные символы обратно в стандартный алфавит, восстановить заполнитель, декодировать и распарсить 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"}. Вытащить из результата отдельный claim - это просто claims->>'sub' в следующем запросе. Восстановление заполнителя - это выражение CASE: base64url-строке, чья длина на два меньше кратной четырём, нужны два знака равенства, на три меньше - один, а точной кратной - ни одного.

Сделайте ещё один шаг - и можно даже проверить HS256-подпись в SQL, используя расширение 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-безопасную форму без заполнителя, которую несёт токен. Для токена и секрета выше ответ - весёлый t. Но держите с собой оговорки: это работает только для HMAC-алгоритмов (HS256, HS384, HS512), кладёт общий секрет внутрь выражения базы данных и сделан для отчётности, аудита и отладки. Всё, что по-настоящему контролирует доступ, должно проверять подпись в прикладном слое настоящей JWT-библиотекой.

Data URL: изображение внутри строки

Формат data URL (RFC 2397) - это способ веба встроить файл прямо в ссылку: 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; data URL без маркера ;base64 несёт вместо него процентно-кодированный текст, и кормить им base64-декодер - ошибка, которую предотвращает LIKE-фильтр выше. Второе: тип медиа в префиксе - это заявление, а не факт; одна и та же строка может говорить image/png и содержать JPEG. Если содержимое важно, проверьте магические байты декодированного результата (PNG начинается с 89 50 4E 47, JPEG - с FF D8). Третье: data URL - большие. Фото на 4 мегапикселя становится строкой примерно на 5.5 мегабайта, а это разговор о размере колонки и памяти, а не о строковых функциях.

URL-безопасный Base64: алфавит, который путешествует

Раздел 5 RFC 4648 определил второй алфавит для base64, потому что у исходного два символа имеют дела в URL-синтаксисе. Знак плюса - так параметры запроса добавляют значения, слеш - так разделяются пути, а знак равенства заполнителя процентно-кодируется в тот самый миг, как встречается со строкой запроса. URL-безопасный вариант меняет + на - и / на _ (оба безвредны в URL), а спецификация JWT сверху сбрасывает заполнитель вообще. Результат путешествует по ссылкам, сегментам пути, именам файлов и идентификаторам фрагментов без единого процентного знака.

В базе данных вы столкнётесь с ним в основном потому, что хранили токены и ссылки, а не потому, что данные там родились. Вот кто справляется с ним нативно, а кому нужен двухминутный ручной режим:

Диалект Нативное URL-безопасное декодирование Примечания
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() плюс восстановление заполнителя, и он одинаков во всех диалектах. В 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 (URL-безопасная форма «hello» без заполнителя) возвращается самим этим словом. Две ошибки, которые случаются постоянно, - именно то, чему мешает выражение CASE: забывчивость о заполнителе, из-за которой строгие декодеры отбраковывают длину, не кратную четырём, и пропуск перевода символов, из-за которого декодер, не знающий URL-безопасного алфавита, давится на подчёркивании. Напишите перевод один раз, как переиспользуемую функцию в вашей базе, - и вся проблема перестанет повторяться.

Файлы, BLOB и большие вещи

Декодирование - это способ, которым файлы выезжают из колонок, и у каждого диалекта выходная дверь немного другая. В DuckDB круговой путь - это два выражения: одно читает файл в BLOB, другое записывает декодированные байты обратно:

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

Сторона чтения: read_blob() - табличная функция, которая принимает имя файла, список имён или glob-шаблон и выдаёт колонки filename и content на каждый файл. Сторона записи - самостоятельное выражение: COPY с форматом BLOB пишет сырые байты, без кавычек, без экранирования, ровно то, чего хочет декодированная пелод.

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 ГБ на значение
MySQL / MariaDB семейство BLOB max_allowed_packet (по умолчанию 64 MB в MySQL 8)
SQL Server varbinary(max) 2 ГБ на значение
Oracle RAW / BLOB RAW: 2000 байт в SQL, BLOB: 4 ГБ с чанкингом PL/SQL
SQLite BLOB что позволяют файл и память
DuckDB BLOB очень большие; решают память и диск
ClickHouse String размер колонки виртуальный, строки - единица
Snowflake BINARY по умолчанию 8 MB на значение (просто колонки BINARY); до 64 MB с явным BINARY(N)

Строка MySQL заслуживает отдельного рассказа, потому что именно она сюрпризирует людей на прод-сервере. max_allowed_packet ограничивает размер одного пакета между клиентом и сервером, а base64-строка - часть этого пакета. Фото на 50 мегабайт, закодированное в base64, - это строка примерно на 67 мегабайт, что больше 64-мегабайтного значения по умолчанию, и результат не ошибка, которую можно прочитать в запросе: это обрезанное значение или NULL, выглядящее как порча данных. Если вы гоняете большие файлы через колонку MySQL, проверьте это ограничение до начала и помните: против него считается закодированная форма, а не сырые байты.

Почтовые обёртки и MIME-строки

Любой base64, переживший почтовую систему, привозит сувенир: переносы строк. MIME, набор стандартов, позволяющий почте нести бинарные вложения (RFC 2045, раздел 6.8), оборачивает base64-вывод на 76 символах и завершает строки возвратом каретки и переводом строки. Обёртка существует, потому что старая почтовая сеть не доверяла строкам длиннее этого, и формат с тех пор тащится дальше по привычке. Так что вложение, сохранённое в колонке базы, чаще всего - это base64-строка с переносом каждые 76 символов, и отношение вашего декодера к этим переносам решает, работа это одно выражение или два.

Декодер Съедает обёртку? Если нет
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() по двум символам переноса) или в ClickHouse до 26.7, и обёрнутое вложение декодируется ровно как необёрнутое.

API-пелоды, конфиги и заголовки аутентификации

Отступите от отдельных функций - и появится паттерн: base64 в колонке базы почти всегда что-то одно из трёх. Поле внутри JSON-документа (изображение, сертификат, файл, который API решил встроить). Значение конфигурации (секрет или учётные данные, которые какой-то инструмент предпочитает в base64, потому что base64 помещается на одной строке YAML-файла без кавычек, без переносов и без обратных слешей). Или артефакт аутентификации (заголовок Basic auth, сохранённый токен, сессионный 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 auth - это литеральная строка Basic с последующим base64 от username:password. Декодировать его в 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-безопасный токен (JWT, ссылка, имя файла), сунутый в декодер стандартного алфавита: SQL Server принимает его, base64URLDecode() ClickHouse принимает, Snowflake принимает с правильным аргументом, а все остальные либо возвращают NULL, либо поднимают ошибку, либо, как в случае с SQLite CLI, молча выбрасывают подчёркивание и протягивают вам неправильные байты. Случай с неправильными байтами - гадкий, потому что результат выглядит правдоподобно.
  • MIME-обёртка. Обёрнутый ввод в декодер, который не ест переносы строк (DuckDB, ClickHouse до 26.7, Oracle), падает, и падение часто выглядит как «последние 76 символов - мусор», а не «тут есть перенос строки», потому что ошибка указывает на символ после переноса.
  • Игра с отображением. Клиент mysql печатает бинарное в шестнадцатеричной нотации, psql печатает bytea как шестнадцатеричное с префиксом \x, Snowflake печатает BINARY как шестнадцатеричное, а Oracle печатает RAW как шестнадцатеричное. Четыре клиента, четыре шестнадцатеричные нотации, одна очень человеческая ошибка - решить, что данные порчены, потому что на экране цифры. Всегда преобразуйте явно, прежде чем читать результат глазами.
  • Заполнитель не там. Знак равенства законен только в конце, один или два. Строка вроде YQ==BQ== - это две валидные группы в одном костюме, и строгие декодеры отбраковывают её, тогда как снисходительные декодируют в то, о чём никто не просил. Если вы когда-нибудь увидите заполнитель посреди сохранённого значения, энкодер, который его записал, сломан, и починка данных - разовая работа.
  • Сюрприз с кодировкой. Декодирование проходит успешно, текст возвращается, и акценты неправильные. Байты были в порядке; интерпретация - нет. Это тот самый CONVERT(... USING latin1), который должен был быть utf8mb4, тот самый CAST(bin AS VARCHAR), который работал под коллацией, проглатывающей невалидные последовательности, тот самый CAST(blob AS TEXT) в SQLite, который никогда не проверяет. Пригвоздите кодировку литералом в запросе и тестируйте с акцентированным канареечным словом.
  • Потолки. 2000-байтовое ограничение RAW Oracle в SQL-выражениях, max_allowed_packet MySQL, берущее налог с закодированного размера, 1-ГБ потолок bytea PostgreSQL, 8-МБ длина BINARY Snowflake по умолчанию. Каждое задокументировано, каждое обнаружено на прод-сервере, и каждое - проверка размера, которую можно было написать до того, как данные стали большими.
  • Доверие декодированным байтам. Base64 может нести что угодно, включая строку, полную кавычек. Декодирование - не санитизация. Что бы вы ни делали с декодированным текстом (сравниваете, пишете в журнал, конкатенируете в другое выражение), ему по-прежнему нужны обычные защиты, и параметризованный запрос остаётся параметризованным запросом и после кругового пути base64.

Как остаться по правильную сторону

  • Сначала решите тип, а не функцию. Пелод бинарный или текстовый? Бинарный идёт в BLOB/bytea/varbinary и там остаётся. Текстовая проходит шаг кодировки с явной кодировкой. Половина всех base64-страданий в SQL - это бинарный пелод, забредший в текстовую колонку (или наоборот), которую теперь интерпретируют.
  • Сначала валидируйте, потом декодируйте, или декодируйте мягко. Регулярное выражение по алфавиту плюс проверка остатка длины от деления на четыре ничего не стоят и превращают ошибку, останавливающую пакетную задачу, в NULL, который можно посчитать. Где диалект предлагает try-форму (try-форма ClickHouse tryBase64Decode, TRY_BASE64_DECODE_BINARY Snowflake), используйте её для отчётности и держите строгую форму для конвейеров, которым нельзя угадывать.
  • Проверяйте версию диалекта, а не только базы. ClickHouse 26.7 изменил обработку пробельных символов, SQL Server 2025 - первый релиз с функцией вообще, SQLite CLI требует 3.41, а ожидания ClickHouse по заполнителю ужесточались со временем. «Это ClickHouse» - не спецификация; «это ClickHouse 24.8» - да.
  • Документируйте алфавит каждой колонки. Колонка, которая может держать и стандартный, и URL-безопасный base64, - это колонка, которая запутает следующего разработчика. Если данные приходят из JWT, скажите об этом в комментарии схемы; если из MIME-вложений - скажите и об этом. Выбор декодера - свойство колонки, а не запроса.
  • Храните байты, кодируйте на границе. Если вы управляете схемой, BLOB-колонка плюс кодирование в API-слое побеждает base64-текстовую колонку в хранении, в индексации и в каждом будущем запросе. Base64 в колонке - налог на совместимость, и налоги лучше платить один раз, на границе.
  • Круговой путь с канареекой. До того как доверить новый путь декодирования, прогоните известный пелод через кодирование и декодирование в одной и той же базе и сравните. Канареека должна содержать не-ASCII символ (чтобы нагрузить шаг кодировки), длину, оставляющую хвост заполнителя (чтобы нагрузить правила заполнителя) и, для URL-безопасных путей, где-нибудь - или _ (чтобы нагрузить перевод алфавита).
  • Держите секреты подальше от текста запроса. Проверка JWT через pgcrypto кладёт общий секрет в выражение; аудит конфигов кладёт секреты в открытом виде в результат. Обе - легитимные работы, но им полагается ограниченная учётная запись, чистый журнал и проверка, а не строка подключения к прод-серверу и SELECT * INTO OUTFILE.

Краткая история распаковки в SQL

Сам формат base64 старше полезной части интернета. Он был стандартизирован для MIME в середине 1990-х (RFC 2045, раздел 6.8, который отменил RFC 1521, спецификацию MIME-тела сообщения 1993 года, несшую это кодирование), а имя - просто подсчёт: в алфавите 64 символа. URL-безопасный вариант прибыл с RFC 4648 в 2006 году, а спецификация JWT в 2015-м сделала этот вариант тем, который вы реально видите в колонках токенов. Но базы данных встретили формат каждый по своему расписанию, и расписание кое-что говорит о душе каждой.

2002. PostgreSQL 7.2 уже числит base64 первым классом форматом encode() и decode() - одновременно с UTL_ENCODE Oracle в эпоху 9i, и старейшая base64-поддержка в этой семье с небольшим отрывом. База данных с настоящим бинарным типом и аргументом формата добралась туда рано, потому что ответ был на расстоянии одного значения перечисления.

Начало 2000-х. Пакет UTL_ENCODE Oracle появляется в эпоху 9i, неся base64 рядом с функциями MIME-заголовка, quoted-printable и uudecode. RAW на входе, RAW на выходе, что очень по-Oracle, и он держал эту форму четверть века.

2013. MySQL 5.6 добавляет TO_BASE64() и FROM_BASE64(), и MariaDB 10.0 уносит оба в форк. Пара кодирует строками по 76 символов и декодирует с терпимостью к пробельным символам, собранный комплект, который не менялся дюжину больших версий.

2018. ClickHouse 18.16 отгружает base64Decode() вместе с алиасом в MySQL-стиле, потому что колоночный мир импортировал нагрузки, которые уже несли base64 в своих схемах логов.

2023. SQLite 3.41.0 добавляет base64() и его base85-брата в командную оболочку как функции, определённые приложением. Ядро, верное себе, не получает ничего; оболочка, где люди на самом деле тыкают в базы SQLite, получает инструмент.

2025. SQL Server 2025, общедоступный с ноября 2025 года, добавляет BASE64_DECODE() и BASE64_ENCODE() в T-SQL после 36-летнего отсутствия. Заметки о выпуске отнеслись к ним как к скромной возможности; сообщество - как к спасению.

Паттерн чистый, стоит его увидеть. Базы с подлинным бинарным типом и аргументом формата (PostgreSQL, и по-своему Oracle) получили base64 в день, когда потребность стала очевидной. Остальные (MySQL, SQL Server) сочли его строковым удобством и запланировали соответственно. А встраиваемый движок (SQLite) по-прежнему считает это работой хост-приложения, с CLI как дружелюбным исключением.

Вещи, от которых улыбнётесь

  • SQL Server провёл 1989-2025 годы без base64-декодера, и ответом сообщества стала XML-функция xs:base64Binary() внутри CAST(N'' AS XML). Целое поколение корпоративных запросов декодировало токены через XML-парсер, потому что XML-парсер понимал base64 с 2001 года, а SQL-движок - нет.
  • base64() в SQLite CLI - единственный сменщик формы в этой семье: подайте ему BLOB - он кодирует, подайте текст - он декодирует. Функция меняет свою работу по типу аргумента, что есть маленький акт SQL-телепатии и настоящая ловушка для невнимательных.
  • Энкодер PostgreSQL оборачивает ровно на 76 символах, как MIME-стандарт 1996 года, только завершает строки одиночным переводом строки вместо возврата каретки и перевода строки из стандарта. Двадцать лет после спецификации - на один символ меньше. Декодер игнорирует и то, и другое, так что бунт незрим, если не сделать дифф вывода.
  • В клиенте mysql SELECT FROM_BASE64('aGVsbG8=') печатает 0x68656C6C6F. Не потому что данные шестнадцатеричные и не потому что что-то не так, а потому что клиент решил за вас, что бинарные строки должны отображаться в шестнадцатеричной нотации. Настройка называется binary-as-hex, и она убедила тысячи разработчиков, что их декодер сломан.
  • SQL-уровневый тип RAW в Oracle ограничен 2000 байтами, так что сертификат на 3 килобайта нельзя даже вставить в SQL-выражение как RAW-литерал. Декодирование должно происходить в PL/SQL, кусками, с циклом. Ограничению - с 1990-х; цикл до сих пор - рекомендуемый ответ.
  • Snowflake отображает значения BINARY как шестнадцатеричное в любом наборе результатов, так что полностью успешное декодирование «hello» приезжает на ваш экран как 68656C6C6F. Два диалекта, два шестнадцатеричных отображения, одно и то же чувство неловости.
  • ClickHouse держит алиас FROM_BASE64() рядом с нативным base64Decode() - маленькая вежливость по отношению к беженцам из MySQL, приехавшим с запросами, которые иначе бы не запустились.
  • Вся семья делит один тихий факт: base64 - это налог 33 процента по дороге на выход и возврат 25 процентов по дороге на вход, и ни один из восьми декодеров здесь не скажет вам об этом без вопроса. Формат - это костюм; гардероб бесплатный; пошив - то, о чём эта статья.

Двигайтесь дальше

Эта статья была про снятие маскировки: функция в каждом диалекте, её характер и пелоды (JWT, data URL, обёрнутая почта, JSON-поля, значения конфигов, заголовки аутентификации), которые её носят. Другое направление - зверь отдельный, со своим набором сюрпризов: какие энкодеры оборачивают свой вывод на 76 символах, а какие нет, как получить URL-безопасную форму без заполнителя, которую ждут токены, размерный расчёт, который определяет ширину вашей колонки, и что означает 36-летний разрыв SQL Server для всех, кто до сих пор на более старой версии. Всё это, от TO_BASE64() до BASE64_ENCODE(), подробно разобрано в связанной статье о кодировании Base64 в SQL, на которую ведёт ссылка с этой страницы. Декодируйте здесь, кодируйте там - и весь круговой путь умещается в одно полдня.

Последнее обновление: 2026-09-08

Связанная статья: Кодирование Base64 в SQL: полное руководство