2025, Oct 30 22:16
Совместимость HASHBYTES SHA‑256 и BIGINT между SQL Server и PySpark
Разбираем, почему CAST(HASHBYTES('SHA2_256') AS BIGINT) в SQL Server даёт другое число, чем в PySpark, и показываем, как получить идентичный 64‑битный ключ.
При переносе ETL-логики из Synapse SQL в PySpark часто нужно воспроизвести существующий идентификатор строки, построенный с помощью HASHBYTES. Само значение SHA‑256 может совпадать побайтно между SQL и Python, но производный BIGINT расходится. В этом материале подробно показано, почему так происходит и как получить совместимое до бита 64‑битное целое.
Подготовка: как ключ формируется в SQL
В SQL идентификатор создаётся так: хешируется конкатенация значений столбцов с разделителем «|», после чего хеш приводится к BIGINT:
SELECT CAST(HASHBYTES('SHA2_256', CONCAT_WS('|', [col1], [col2], ...)) AS BIGINT) AS EtlHashKey
FROM sample_table;
Для простого примера, col1 = abc и col2 = 123, ожидается конкретное отрицательное значение BIGINT в SQL. При тех же входных данных в Python может получиться другой 64‑битный результат, даже если шестнадцатеричные дайджесты SHA‑256 совпадают. Именно это расхождение и является проблемой.
Воспроизведение проблемы в Python
Пример ниже показывает, что даже при, казалось бы, корректном подходе получается другой BIGINT по сравнению с SQL. Используются те же кодировка utf‑8 и обработка null, что и у CONCAT_WS, а шестнадцатеричный SHA‑256 совпадает с результатом HASHBYTES в SQL.
import hashlib
import struct
def calc_row_key(*parts):
merged = '|'.join(['' if p is None else str(p) for p in parts])
digest = hashlib.sha256(merged.encode('utf-8')).digest()
key64 = struct.unpack('<q', digest[:8])[0]
return key64
Код выдаёт корректное знаковое 64‑битное число, но оно не совпадает с результатом SQL CAST(HASHBYTES(...) AS BIGINT).
Почему числа расходятся
SHA‑256 возвращает 32 байта. Несоответствие возникает из‑за того, как эти 32 байта сводятся к 8 и как затем эти 8 байт интерпретируются. SQL Server берёт правые 8 байт 32‑байтового хеша и трактует их как BIGINT. А приведённый выше Python‑код берёт первые 8 байт и распаковывает их в порядке little‑endian.
Если выбрать правильные 8 байт и порядок байтов, итоговое 64‑битное число совпадёт с поведением CAST в SQL Server. Сырые значения хеша и так совпадали; отличались лишь выбранные 8 байт и способ их чтения.
Решение: повторить семантику CAST в SQL
Чтобы воспроизвести CAST(HASHBYTES('SHA2_256', ...) AS BIGINT), возьмите последние 8 байт дайджеста SHA‑256 и распакуйте их как знаковое 64‑битное целое в порядке big‑endian.
import hashlib
import struct
def build_etl_key(*fields):
joined = '|'.join(['' if f is None else str(f) for f in fields])
hbytes = hashlib.sha256(joined.encode('utf-8')).digest()
etl_key = struct.unpack('>q', hbytes[-8:])[0]
return etl_key
Эта правка выравнивает результат Python с BIGINT, полученным в SQL Server из HASHBYTES. Ключевые детали — правые 8 байт и интерпретация в формате big‑endian.
Почему это важно понимать
Сведение 32‑байтового дайджеста SHA‑256 к 8‑байтовому целому отбрасывает три четверти информации. Из‑за этого вероятность коллизий выше, чем при использовании полного бинарного значения. Такая практика сохраняется в некоторых конвейерах по историческим причинам или из‑за ограничений схемы, но важно осознавать компромисс. Если есть возможность хранить полный хеш как бинарные данные, шаг преобразования отпадает, а вероятность коллизий минимизируется относительно исходного алгоритма.
Итоги на практике
Если вам нужно получить тот же BIGINT, что и в SQL Server при CAST(HASHBYTES('SHA2_256', ...) AS BIGINT), вычислите SHA‑256, возьмите правые 8 байт и распакуйте их как знаковое 64‑битное значение в порядке big‑endian. Помните, что BIGINT хранит лишь часть сведений из хеша, поэтому, когда это возможно, предпочтительнее сохранять полный 32‑байтовый дайджест, чтобы лучше сохранять свойства уникальности.