2025, Oct 30 22:32

SQL HASHBYTES से बने SHA-256 को PySpark BIGINT से बिट-टू-बिट मिलाएँ

Synapse SQL से PySpark माइग्रेशन में SHA-256/HASHBYTES से बने पहचानकर्ता का BIGINT रूप Python में क्यों बदलता है, और सही स्लाइस व एंडियननेस से उसे कैसे सटीक मैच करें।

Synapse SQL से PySpark में ETL लॉजिक माइग्रेट करते समय, HASHBYTES से बने मौजूदा रो पहचानकर्ता का प्रतिरूप बनाना आम है। कच्चा SHA-256 मान SQL और Python में बाइट-टू-बाइट मेल खा सकता है, फिर भी उससे निकला BIGINT अलग हो सकता है। यह गाइड साफ तौर पर बताती है कि ऐसा क्यों होता है और बिट-टू-बिट संगत 64-बिट पूर्णांक कैसे प्राप्त करें।

सेटअप: SQL में कुंजी कैसे बनती है

SQL में, पहचानकर्ता पाइप सेपरेटर के साथ कॉलम मानों को जोड़कर उनका हैश बनाकर और फिर उस हैश को BIGINT में CAST करके तैयार किया जाता है:

SELECT CAST(HASHBYTES('SHA2_256', CONCAT_WS('|', [col1], [col2], ...)) AS BIGINT) AS EtlHashKey
FROM sample_table;

col1 = abc और col2 = 123 जैसे सरल इनपुट पर, SQL का अपेक्षित परिणाम एक विशिष्ट नकारात्मक BIGINT होता है। वही इनपुट Python में, भले ही SHA-256 के हेक्स डाइजेस्ट समान हों, एक अलग 64-बिट पूर्णांक दे सकता है। यही असंगति मूल समस्या है।

Python में समस्या दोहराना

नीचे दिया गया Python स्निपेट एक दिखने में सही तरीका दिखाता है, जो फिर भी SQL से अलग BIGINT देता है। यह CONCAT_WS के समान utf-8 एन्कोडिंग और null हैंडलिंग का उपयोग करता है, और SHA-256 का हेक्स SQL के HASHBYTES आउटपुट से मेल खाता है।

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 32-बाइट हैश के सबसे दाएँ 8 बाइट्स लेता है और उन्हें BIGINT के रूप में व्याख्यायित करता है। ऊपर दिया Python कोड पहले 8 बाइट्स लेता है और उन्हें लिटिल-एंडियन के रूप में अनपैक करता है।

सही स्लाइस और बाइट ऑर्डर लागू करने पर प्राप्त 64-बिट पूर्णांक SQL Server के CAST व्यवहार से मेल खा जाता है। कच्चे हैश मान तो शुरू से ही समान थे; फर्क सिर्फ इतना था कि कौन-से 8 बाइट चुने गए और उन्हें किस क्रम से पढ़ा गया।

उपाय: SQL के CAST सेमान्टिक्स से मेल करें

CAST(HASHBYTES('SHA2_256', ...) AS BIGINT) को दोहराने के लिए, SHA-256 डाइजेस्ट के अंतिम 8 बाइट लें और उन्हें बिग-एंडियन क्रम में साइन किए हुए 64-बिट पूर्णांक के रूप में अनपैक करें।

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 के परिणाम को HASHBYTES से निकले SQL Server के BIGINT से संरेखित कर देता है। अहम बातें हैं: सबसे दाएँ 8 बाइट और उनका बिग-एंडियन के रूप में पढ़ना।

यह जानना क्यों जरूरी है

32-बाइट SHA-256 डाइजेस्ट को 8-बाइट पूर्णांक में घटाने से कुल जानकारी का तीन-चौथाई हिस्सा छोड़ना पड़ता है। इस कमी से, पूरे बाइनरी के मुकाबले, टकराव की संभावना बढ़ जाती है। कुछ पाइपलाइनों में यह तरीका विरासत या स्कीमा कारणों से चलता है, लेकिन इसका समझौता जानना जरूरी है। यदि पूरा हैश बाइनरी रूप में संग्रहीत किया जा सकता है, तो रूपांतरण चरण खत्म हो जाता है और मूल एल्गोरिदम की तुलना में टकराव की संभावना न्यूनतम रहती है।

व्यावहारिक निष्कर्ष

यदि आपको ठीक वही BIGINT चाहिए जो SQL Server CAST(HASHBYTES('SHA2_256', ...) AS BIGINT) के लिए देता है, तो SHA-256 निकालें, सबसे दाएँ 8 बाइट लें और उन्हें साइन किए हुए 64-बिट बिग-एंडियन मान के रूप में अनपैक करें। याद रखें, BIGINT हैश की जानकारी का केवल एक उपसमुच्चय पकड़ता है; इसलिए यदि आपका सिस्टम अनुमति देता है, तो विशिष्टता बनाए रखने के लिए पूरा 32-बाइट डाइजेस्ट ही संग्रहीत करें।

यह लेख StackOverflow के प्रश्न (लेखक: Santosh Reddy Kommidi) और Charlieface के उत्तर पर आधारित है।