sqlpostgresqlto_hexbitwise

to_hex в PostgreSQL: как перевести число в шестнадцатеричный вид

Функция to_hex(int) превращает число в hex-строку для цветов, битовых масок и отладки флагов; как сделать обратное преобразование и чем отличаются MySQL и ClickHouse.

10 мин чтенияСправочникsql · postgresql · to_hex · bitwise · mysql · clickhouse

to_hex — это функция PostgreSQL, которая берёт целое число и возвращает его шестнадцатеричное представление строкой.

Например:

SELECT to_hex(255) AS result;

Результат:

result
ff

Шестнадцатеричный формат часто называют просто hex. Он использует цифры от 0 до 9 и буквы от a до f.

В обычной десятичной системе после 9 идёт 10.
В шестнадцатеричной после 9 идут a, b, c, d, e, f, а уже потом 10.

Такой формат удобен там, где число по смыслу похоже не просто на «количество», а на набор байтов или битов:

  • цвет в формате RGB;
  • маска прав доступа;
  • набор флагов;
  • технический идентификатор;
  • дамп значения для отладки;
  • часть протокола или бинарного формата.

Например, число 16711680 в обычном виде выглядит не очень понятно. А в hex это ff0000, то есть красный цвет в формате RGB.

SELECT to_hex(16711680) AS color_hex;

Результат:

color_hex
ff0000

to_hex не делает число «лучше» и не меняет его смысл. Он просто показывает то же значение в другой системе счисления — часто более удобной для программиста.

Базовый синтаксис

В PostgreSQL функция принимает целое число и возвращает текст:

SELECT to_hex(255) AS result;

Ещё несколько примеров:

SELECT to_hex(4096) AS result;

Результат:

result
1000
SELECT to_hex(16777215) AS result;

Результат:

result
ffffff

Важные свойства результата:

  • буквы возвращаются в нижнем регистре;
  • префикса 0x нет;
  • ведущие нули не добавляются;
  • результат имеет тип text.

То есть PostgreSQL вернёт ff, а не 0xff, не FF и не 000000ff.

Если нужен префикс 0x, добавьте его сами:

SELECT '0x' || to_hex(255) AS result;

Результат:

result
0xff

Какие типы принимает to_hex

to_hex работает с целыми типами:

  • integer;
  • bigint.

Простые числовые литералы обычно подходят:

SELECT to_hex(255) AS result;

Но если значение имеет тип numeric, PostgreSQL не будет автоматически угадывать, как его правильно превратить в hex.

Например, так можно получить ошибку:

SELECT to_hex(255.0);

Лучше привести значение явно:

SELECT to_hex(255.0::int) AS result;

То же самое касается значений из колонок.

Допустим, зарплата хранится как numeric:

SELECT
  id,
  to_hex(salary::int) AS salary_hex
FROM employees;

Но здесь нужно понимать смысл: приведение numeric к int может округлить или обрезать значение в зависимости от ситуации и привести к ошибке, если число не помещается в integer.

Поэтому to_hex лучше применять там, где исходное значение действительно целое по смыслу.

Фиксированная ширина через lpad

to_hex не добавляет ведущие нули.

SELECT to_hex(15) AS result;

Результат:

result
f

Но для байта обычно хотят видеть две hex-цифры: 0f.

Для этого удобно использовать lpad.

SELECT lpad(to_hex(15), 2, '0') AS result;

Результат:

result
0f

lpad дополняет строку слева до нужной длины.

Шаблон такой:

SELECT lpad(to_hex(value), width, '0') AS hex_value
FROM some_table;

Где width — нужная длина hex-строки.

Например:

  • 2 символа — один байт;
  • 6 символов — цвет RGB;
  • 8 символов — 32-битное значение;
  • 16 символов — 64-битное значение.

Пример: цвет в формате RGB

Один из самых понятных примеров — цвет.

В вебе цвет часто записывают так:

#ff0000

Это красный цвет:

  • ff — красный канал;
  • 00 — зелёный канал;
  • 00 — синий канал.

Если цвет хранится в базе как целое число, его можно красиво вывести через to_hex.

SELECT
  id,
  '#' || lpad(to_hex(theme_color), 6, '0') AS css_color
FROM users;

Если theme_color равен 16711680, результат будет:

css_color
#ff0000

Почему нужен lpad до 6 символов?

Потому что для RGB нужно ровно шесть hex-цифр: по две на каждый канал.

Например, число 255 — это синий цвет #0000ff, если воспринимать число как RGB.

Без lpad получится просто ff, и это уже не полноценный CSS-цвет.

SELECT
  '#' || lpad(to_hex(255), 6, '0') AS css_color;

Результат:

css_color
#0000ff

Пример: маска прав доступа

Ещё один частый случай — флаги, упакованные в одно число.

Допустим, права пользователя хранятся битами:

Бит Значение Смысл
0 1 чтение
1 2 запись
2 4 админ

Если у пользователя есть чтение и админ-доступ, число будет 5, потому что 1 + 4 = 5.

В десятичном виде 5 читается нормально. Но когда флагов много, hex часто удобнее.

SELECT
  id,
  permissions,
  to_hex(permissions) AS permissions_hex
FROM users;

Для проверки конкретного бита используют побитовое И.

SELECT
  id,
  permissions,
  to_hex(permissions) AS permissions_hex,
  (permissions & 4) <> 0 AS has_admin
FROM users
WHERE (permissions & 4) <> 0;

Здесь выражение:

(permissions & 4) <> 0

проверяет, включён ли бит со значением 4.

Если он включён, пользователь имеет админский флаг.

Почему hex удобен для флагов

Допустим, есть число 255.

В десятичном виде это просто:

255

А в hex:

ff

Если дополнить до 8 символов:

SELECT lpad(to_hex(255), 8, '0') AS flags_hex;

Получится:

flags_hex
000000ff

Для программиста это сразу выглядит как младший байт, где выставлены все 8 бит.

Если число большое, hex-вид часто читается легче, чем длинная десятичная запись.

SELECT
  305419896 AS value_dec,
  to_hex(305419896) AS value_hex;

Результат:

value_dec value_hex
305419896 12345678

Hex-значение 12345678 хорошо показывает структуру байтов. Десятичное 305419896 такой структуры не показывает.

Отладка: вывести число в decimal, hex и bit

Когда вы разбираетесь с флагами, удобно вывести значение сразу в нескольких видах.

SELECT
  id,
  permissions AS value_dec,
  to_hex(permissions) AS value_hex,
  permissions::bit(32) AS value_bits
FROM users;

Так можно увидеть:

  • исходное десятичное число;
  • hex-представление;
  • двоичное представление.

Например:

value_dec value_hex value_bits
5 5 00000000000000000000000000000101

В двоичном виде хорошо видно, какие именно биты включены.

Если колонка имеет тип bigint, используйте ширину 64.

SELECT
  id,
  flags AS value_dec,
  to_hex(flags) AS value_hex,
  flags::bit(64) AS value_bits
FROM events;

Отрицательные числа: главная ловушка

С положительными числами всё выглядит просто:

SELECT to_hex(255) AS result;

Результат:

result
ff

Но с отрицательными числами поведение может удивить.

SELECT to_hex(-1) AS result;

Результат:

result
ffffffff

PostgreSQL не возвращает -1 в hex-виде. Он показывает представление числа в дополнительном коде.

Для начинающего это можно объяснить так: на уровне битов отрицательные целые числа хранятся не как «минус и число», а как специальный битовый шаблон. Для -1 в 32-битном целом это все единицы:

11111111111111111111111111111111

В hex это:

ffffffff

Если явно использовать bigint, ширина будет уже 64 бита:

SELECT to_hex(-1::bigint) AS result;

Результат:

result
ffffffffffffffff

То есть результат зависит от типа входного значения:

  • integer — 32 бита;
  • bigint — 64 бита.

Это особенно важно, если вы сравниваете hex-строки, делаете экспорт или разбираете бинарный формат.

Почему ширина типа важна

Посмотрим на одно и то же значение -1 в двух типах:

SELECT
  to_hex(-1::int) AS int_hex,
  to_hex(-1::bigint) AS bigint_hex;

Результат:

int_hex bigint_hex
ffffffff ffffffffffffffff

По смыслу оба значения — -1.

Но hex-строки разные, потому что разная ширина представления.

Поэтому в технических форматах важно заранее договориться:

  • это 32-битное значение или 64-битное;
  • нужны ли ведущие нули;
  • допускаются ли отрицательные числа;
  • нужно ли показывать префикс 0x;
  • в каком регистре хранить hex-строки.

Если это не зафиксировать, одинаковые по смыслу значения могут выглядеть по-разному.

Граничные значения

Полезно посмотреть на максимальное значение integer.

SELECT to_hex(2147483647) AS max_int_hex;

Результат:

max_int_hex
7fffffff

А вот следующее значение уже не помещается в integer, но помещается в bigint.

SELECT to_hex(2147483648::bigint) AS value_hex;

Результат:

value_hex
80000000

Это важный момент: большие числа лучше явно приводить к bigint, чтобы не упереться в диапазон integer.

Обратное преобразование: hex обратно в число

В PostgreSQL нет простой встроенной функции вида from_hex.

Но hex-строку можно превратить обратно в число через битовую строку.

Для 32-битного числа удобно использовать такой приём:

SELECT ('x' || lpad('ff', 8, '0'))::bit(32)::int AS result;

Результат:

result
255

Почему lpad до 8 символов?

Потому что 32 бита — это 4 байта, а каждый байт записывается двумя hex-цифрами. Значит, нужно 8 hex-символов.

Ещё пример:

SELECT ('x' || lpad('1000', 8, '0'))::bit(32)::int AS result;

Результат:

result
4096

Полный путь туда-обратно:

SELECT ('x' || lpad(to_hex(48879), 8, '0'))::bit(32)::int AS result;

Результат:

result
48879

Для bigint используйте 64 бита и 16 hex-символов:

SELECT ('x' || lpad('ff', 16, '0'))::bit(64)::bigint AS result;

Результат:

result
255

Осторожно с обратным преобразованием отрицательных чисел

С положительными значениями всё спокойно.

Но если hex-строка описывает число в дополнительном коде, результат может стать отрицательным.

Например:

SELECT ('x' || 'ffffffff')::bit(32)::int AS result;

Результат:

result
-1

Это логично: ffffffff в 32-битном знаковом представлении — это -1.

А вот если взять 64 бита:

SELECT ('x' || 'ffffffffffffffff')::bit(64)::bigint AS result;

Результат:

result
-1

Поэтому при обратном преобразовании всегда задавайте правильную ширину: bit(32) или bit(64).

bytea: hex для байтов, а не для чисел

to_hex подходит для целых чисел.

Но если у вас настоящие байты, например bytea, хеш или бинарные данные, используйте другие функции:

  • encode;
  • decode.

Превратить байты в hex-строку:

SELECT encode('PG'::bytea, 'hex') AS result;

Результат:

result
5047

Превратить hex-строку обратно в байты:

SELECT decode('5047', 'hex') AS result;

Результат будет значением типа bytea.

То есть правило такое:

to_hex — для чисел.
encode и decode — для байтов.

Не путайте эти задачи.

Если вы храните хеш, бинарный ключ или сырой пакет данных, это не to_hex.

Если у вас целое число, которое удобно показать в шестнадцатеричном виде, это как раз to_hex.

Пример: хранить и показывать бинарный хеш

Допустим, в таблице есть колонка с хешем типа bytea.

CREATE TABLE files (
  id bigint,
  hash_value bytea
);

Чтобы показать хеш в читаемом hex-виде:

SELECT
  id,
  encode(hash_value, 'hex') AS hash_hex
FROM files;

А если hex пришёл из внешней системы и его нужно сохранить как байты:

INSERT INTO files (id, hash_value)
VALUES (1, decode('5047', 'hex'));

Здесь to_hex вообще не нужен, потому что мы работаем не с числом, а с массивом байтов.

to_hex и индексы

Если вы фильтруете по исходному числу, используйте число.

SELECT
  id,
  flags
FROM events
WHERE flags = 255;

Так обычный индекс по колонке flags может помочь.

Но если вы фильтруете по результату функции:

SELECT
  id,
  flags
FROM events
WHERE to_hex(flags) = 'ff';

обычный индекс по flags может не подойти, потому что условие построено уже не по числу, а по выражению.

Если такой поиск действительно нужен часто, можно создать функциональный индекс:

CREATE INDEX events_flags_hex_idx
ON events (to_hex(flags));

Но чаще правильнее сравнивать сами числа, а to_hex использовать только для вывода.

SELECT
  id,
  flags,
  to_hex(flags) AS flags_hex
FROM events
WHERE flags = 255;

Так запрос остаётся понятным и обычно лучше дружит с индексами.

Отличия от MySQL

В MySQL есть функция HEX.

Она похожа по идее, но ведёт себя иначе.

SELECT HEX(255) AS result;

Результат в MySQL:

result
FF

Главное отличие: MySQL возвращает буквы в верхнем регистре.

В PostgreSQL:

SELECT to_hex(255) AS result;

Результат:

result
ff

Если вы сравниваете hex-строки из разных систем, приводите регистр явно:

SELECT lower(hex_value) AS hex_value_normalized
FROM imported_values;

Для обратного преобразования в MySQL часто используют CONV.

SELECT CONV('ff', 16, 10) AS result;

Результат:

result
255

Для байтов в MySQL есть UNHEX.

SELECT UNHEX('5047') AS result;

Ещё одна особенность MySQL: HEX может работать не только с числами, но и со строками. В PostgreSQL to_hex так не работает: для байтов нужен encode.

Отличия от ClickHouse

В ClickHouse есть функция hex.

SELECT hex(255) AS result;

Она обычно возвращает hex в верхнем регистре.

Для обратного преобразования байтов используется unhex.

SELECT unhex('FF') AS result;

Если данные собираются из PostgreSQL, MySQL и ClickHouse, заранее приведите формат к одному виду:

  • нижний или верхний регистр;
  • с префиксом 0x или без;
  • с ведущими нулями или без;
  • фиксированная ширина или короткая запись.

Иначе одинаковые значения могут не совпасть как строки.

Например:

Источник Значение
PostgreSQL ff
MySQL FF
ClickHouse FF

По смыслу это одно и то же число. Но строковое сравнение без нормализации может сказать, что значения разные.

Что зафиксировать перед экспортом hex

Если hex-значение уходит во внешний файл, API, отчёт или другую базу, лучше заранее записать правила формата.

Например:

  • используется нижний регистр;
  • префикса 0x нет;
  • цвет всегда занимает 6 символов;
  • 32-битные флаги всегда занимают 8 символов;
  • 64-битные флаги всегда занимают 16 символов;
  • отрицательные числа запрещены или описаны отдельно;
  • bytea-значения кодируются через encode;
  • числа кодируются через to_hex.

Иначе через пару месяцев может появиться набор значений вроде:

ff
FF
0xff
000000ff

Все они могут означать одно и то же число 255, но для строкового сравнения это четыре разные строки.

Частые ошибки

Первая ошибка — ждать префикс 0x.

SELECT to_hex(255) AS result;

PostgreSQL вернёт ff, а не 0xff.

Если префикс нужен:

SELECT '0x' || to_hex(255) AS result;

Вторая ошибка — забыть ведущие нули.

SELECT '#' || to_hex(255) AS css_color;

Результат будет #ff, а не #0000ff.

Правильно:

SELECT '#' || lpad(to_hex(255), 6, '0') AS css_color;

Третья ошибка — использовать to_hex для bytea.

SELECT to_hex(hash_value)
FROM files;

Если hash_value имеет тип bytea, так делать не нужно. Используйте encode.

SELECT encode(hash_value, 'hex') AS hash_hex
FROM files;

Четвёртая ошибка — не учитывать отрицательные числа.

SELECT to_hex(-1) AS result;

Результат будет ffffffff, а не -1.

Пятая ошибка — сравнивать hex-строки из разных СУБД без нормализации регистра.

SELECT lower(hex_value) AS hex_value_normalized
FROM imported_values;

Практические шаблоны

Число в hex:

SELECT to_hex(value) AS value_hex
FROM numbers;

Hex с префиксом:

SELECT '0x' || to_hex(value) AS value_hex
FROM numbers;

Один байт, всегда два символа:

SELECT lpad(to_hex(value), 2, '0') AS byte_hex
FROM numbers;

Цвет RGB, всегда шесть символов:

SELECT '#' || lpad(to_hex(theme_color), 6, '0') AS css_color
FROM users;

32-битное значение, всегда восемь символов:

SELECT lpad(to_hex(flags), 8, '0') AS flags_hex
FROM events;

64-битное значение, всегда шестнадцать символов:

SELECT lpad(to_hex(flags::bigint), 16, '0') AS flags_hex
FROM events;

Hex обратно в int:

SELECT ('x' || lpad('ff', 8, '0'))::bit(32)::int AS value_int;

Hex обратно в bigint:

SELECT ('x' || lpad('ff', 16, '0'))::bit(64)::bigint AS value_bigint;

Байты в hex:

SELECT encode(data_bytes, 'hex') AS data_hex
FROM files;

Hex обратно в байты:

SELECT decode('5047', 'hex') AS data_bytes;

Главное

to_hex в PostgreSQL переводит целое число в hex-строку.

SELECT to_hex(255) AS result;

Результат:

result
ff

Функция принимает integer и bigint.

Результат возвращается в нижнем регистре, без префикса 0x и без ведущих нулей.

Если нужна фиксированная ширина, используйте lpad.

SELECT lpad(to_hex(15), 2, '0') AS result;

Для цветов удобно делать так:

SELECT '#' || lpad(to_hex(theme_color), 6, '0') AS css_color
FROM users;

Для отрицательных чисел to_hex показывает представление в дополнительном коде. Поэтому to_hex(-1::int) вернёт ffffffff, а to_hex(-1::bigint)ffffffffffffffff.

Для байтов, хешей и bytea используйте не to_hex, а encode и decode.

SELECT encode('PG'::bytea, 'hex') AS result;

В MySQL и ClickHouse hex-функции обычно возвращают буквы в верхнем регистре, а PostgreSQL to_hex — в нижнем. При переносе данных нормализуйте регистр через lower или upper.

Перед экспортом hex-значений всегда фиксируйте формат: регистр, ширину, ведущие нули, префикс 0x, тип числа и поведение отрицательных значений. Именно на этих мелочах чаще всего ломаются сравнения, дампы и интеграции.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр