2025, Oct 19 13:31

PostgreSQL में NumPy arrays को सहेजना: tobytes बनाम msgpack-numpy

इस गाइड में PostgreSQL में NumPy arrays स्टोर करने के दो तरीके—tobytes और msgpack-numpy—की तुलना, फायदे-नुकसान, कोड स्निपेट्स और प्रदर्शन टिप्स शामिल हैं.

PostgreSQL में NumPy arrays को स्थायी रूप से सहेजना (persist करना) सीधा-सा लगता है, जब तक कि मेटाडेटा की दीवार से टकराव न हो: कच्चे बाइट्स में shape या dtype की जानकारी नहीं होती। एक आम तरीका है NumPy के tobytes से सीरियलाइज़ करना और shape व dtype को अलग-अलग कॉलम में रखना। यह चलता है, लेकिन कई टेबल्स और कॉल साइट्स पर इसे दोहराने पर झंझट महसूस होने लगता है।

बेसलाइन: ऐरे को रॉ बाइट्स के रूप में स्टोर करना

नीचे दिया गया तरीका psycopg, numpy.tobytes और टार्गेट टेबल्स की एक व्हाइटलिस्ट का इस्तेमाल करता है। यह इन्सर्ट/अपडेट करता है, और पढ़ते समय सेव किए गए shape व dtype के साथ स्टोर किए गए बाइट्स को मिलाकर ऐरे को फिर से बनाता है।

import psycopg
from psycopg import sql
import os
import orjson
import numpy as np

pg_conn_opts = {
    'dbname': os.getenv("PG_DATABASE"),
    'user': os.getenv("PG_USERNAME"),
    'password': os.getenv("PG_PASSWORD"),
    'host': os.getenv("PG_HOST"),
    'connect_timeout': os.getenv("PG_CONNECT_TIMEOUT")
}

def persist_ndarray(arr, arr_key, target_table):
    if target_table in ['table_a', 'table_b', 'table_c']:
        with psycopg.connect(**pg_conn_opts) as cxn:
            with cxn.cursor() as cur:
                try:
                    cur.execute(sql.SQL(
                        """
                        INSERT INTO {tbl}(array_id, np_array_bytes, np_array_shape, np_array_dtype)
                        VALUES (%s, %s, %s, %s)
                        ON CONFLICT (array_id) DO UPDATE SET
                        np_array_bytes = EXCLUDED.np_array_bytes,
                        np_array_shape = EXCLUDED.np_array_shape,
                        np_array_dtype = EXCLUDED.np_array_dtype
                        """
                    ).format(tbl=sql.Identifier(target_table)),
                    [arr_key, arr.tobytes(), orjson.dumps(arr.shape), str(arr.dtype)])
                    cxn.commit()
                except:
                    print("Error while saving NumPy array to database.")
                finally:
                    cxn.close()
    else:
        print("You're attempting to write arrays to a non-approved table.")


def fetch_ndarray(arr_key, target_table):
    if target_table in ['table_a', 'table_b', 'table_c']:
        with psycopg.connect(**pg_conn_opts) as cxn:
            with cxn.cursor() as cur:
                cur.execute(sql.SQL(
                    """
                    SELECT np_array_bytes, np_array_shape, np_array_dtype
                    FROM {tbl}
                    WHERE array_id = %s
                    """
                ).format(tbl=sql.Identifier(target_table)), [arr_key])
                row = cur.fetchone()
                if row:
                    blob, shp, dt = row
                    return np.frombuffer(blob, dtype=dt).reshape(orjson.loads(shp))

यह जरूरत से ज्यादा भारी क्यों महसूस होता है

tobytes तेज है, लेकिन रॉ बाइट्स को उस ऐरे के बारे में कुछ पता नहीं होता जिनसे वे आए हैं। नतीजतन आपको shape और dtype अलग से ट्रैक करना पड़ता है, SQL के जरिए उन्हें पास करना पड़ता है, और पढ़ते समय मैन्युअली रीकंस्ट्रक्ट करना पड़ता है। यह भरोसेमंद तो है, पर बकायदा लंबाचौड़ा। हर कॉल साइट पर वही प्रक्रिया दोहरानी पड़ती है। अगर आपको कई टेबल्स में ऐरे सहेजने हैं, तो हर insert और select के साथ ओवरहेड बढ़ता जाता है।

एक सरल विकल्प: msgpack-numpy

msgpack-numpy किसी NumPy ऐरे को एक कॉम्पैक्ट ब्लॉब में सीरियलाइज़ कर सकता है, जिसमें shape और dtype पहले से शामिल रहते हैं। यानी आप केवल एक बाइनरी वैल्यू सहेजते हैं और बिना अतिरिक्त कॉलम्स के साथ जूझे उसे ठीक उसी ऐरे में वापस डिकोड कर सकते हैं। उदाहरण के लिए:

यह A.tobytes() से थोड़ा धीमा है, लेकिन shape और dtype को साथ रखकर काम को अधिक सुविधाजनक बना देता है। यह NumPy के मूल np.save() फॉर्मैट से भी छोटा और तेज है।

बड़े ऐरे के साथ परीक्षण से एक व्यावहारिक अवलोकन भी सामने आता है:

पैकिंग के समय, लाख तत्वों वाले ऐरे के लिए tobytes करीब 2 गुना तेज है, और अनपैकिंग के समय बहुत ज्यादा तेज (लगभग 1000–2000 गुना)। दोनों ही तेज हैं, इसलिए फर्क तभी मायने रखता है जब ऐरे बहुत बड़े हों।

कहने का मतलब, यदि कम ओवरहेड के साथ सुविधा चाहिए, तो msgpack-numpy बेहतरीन बैठता है। बहुत बड़े ऐरे पर अगर आप शुद्ध गति के पीछे हैं, तो कच्चे tobytes का रास्ता अब भी आकर्षक है।

परिवर्तित कोड: msgpack-numpy के साथ पैकिंग और अनपैकिंग

नीचे दिया गया स्निपेट बताता है कि msgpack-numpy के जरिए स्टोर/लोड कैसे करें, जबकि डेटाबेस इंटरैक्शन का मॉडल वही रहता है। बाइट्स np_array_bytes में जाती हैं; shape और dtype पहले की तरह ही लिखे जाते हैं, भले ही पैक किए गए पेलोड में वे पहले से मौजूद हों।

import psycopg
from psycopg import sql
import os
import orjson
import numpy as np
import msgpack
import msgpack_numpy as mpn

pg_conn_opts = {
    'dbname': os.getenv("PG_DATABASE"),
    'user': os.getenv("PG_USERNAME"),
    'password': os.getenv("PG_PASSWORD"),
    'host': os.getenv("PG_HOST"),
    'connect_timeout': os.getenv("PG_CONNECT_TIMEOUT")
}

def persist_ndarray_msgpack(arr, arr_key, target_table):
    if target_table in ['table_a', 'table_b', 'table_c']:
        with psycopg.connect(**pg_conn_opts) as cxn:
            with cxn.cursor() as cur:
                try:
                    packed = msgpack.packb(arr, default=mpn.encode)
                    cur.execute(sql.SQL(
                        """
                        INSERT INTO {tbl}(array_id, np_array_bytes, np_array_shape, np_array_dtype)
                        VALUES (%s, %s, %s, %s)
                        ON CONFLICT (array_id) DO UPDATE SET
                        np_array_bytes = EXCLUDED.np_array_bytes,
                        np_array_shape = EXCLUDED.np_array_shape,
                        np_array_dtype = EXCLUDED.np_array_dtype
                        """
                    ).format(tbl=sql.Identifier(target_table)),
                    [arr_key, packed, orjson.dumps(arr.shape), str(arr.dtype)])
                    cxn.commit()
                except:
                    print("Failed to persist NumPy array via msgpack.")
                finally:
                    cxn.close()
    else:
        print("Refusing to write to unexpected table.")


def fetch_ndarray_msgpack(arr_key, target_table):
    if target_table in ['table_a', 'table_b', 'table_c']:
        with psycopg.connect(**pg_conn_opts) as cxn:
            with cxn.cursor() as cur:
                cur.execute(sql.SQL(
                    """
                    SELECT np_array_bytes, np_array_shape, np_array_dtype
                    FROM {tbl}
                    WHERE array_id = %s
                    """
                ).format(tbl=sql.Identifier(target_table)), [arr_key])
                row = cur.fetchone()
                if row:
                    packed, shp, dt = row
                    return msgpack.unpackb(packed, object_hook=mpn.decode)

इससे आपका डेटा मॉडल जस का तस रहता है और पढ़ते समय shape व dtype को समन्वित करने की जरूरत खत्म हो जाती है, क्योंकि वे पेलोड के भीतर एंबेडेड होकर वापस आते हैं। अगर स्कीमा पर आपका नियंत्रण है, तो आप इसे और सरल बनाकर केवल एक बाइनरी कॉलम भी सहेज सकते हैं।

यह क्यों मायने रखता है

कौन-सा सीरियलाइज़ेशन रास्ता चुनते हैं, यह तय करता है कि आपको कितना ‘ग्लू कोड’ संभालना पड़ेगा। रॉ बाइट्स अतिरिक्त मेटाडेटा हैंडलिंग के रूप में जटिलता को आपके एप्लिकेशन पर छोड़ देती हैं। msgpack-numpy उस जटिलता को सीरियलाइज़ेशन चरण में केंद्रीकृत कर देता है। समझौता प्रदर्शन के बारीक फर्क में है: tobytes तेज हो सकता है, खासकर बहुत बड़े ऐरे डिकोड करते समय, जबकि msgpack-numpy उपयोग-सुविधा के लिए बेहतर है और np.save से छोटा व तेज है।

मुख्य बातें

यदि आपकी प्राथमिकता कई PostgreSQL टेबल्स में साफ-सुथरी और संभालने योग्य पाइपलाइन है, तो msgpack-numpy एक व्यवहारिक विकल्प है जो आपके लिए shape और dtype साथ लाता है। अगर बैंडविड्थ सीमित है और बहुत बड़े ऐरे पर सबसे तेज़ एन्कोड/डिकोड चाहिए, तो tobytes के साथ स्पष्ट shape और dtype रखना भी पूरी तरह उचित है। किसी भी स्थिति में, टेबल एक्सेस को ज्ञात गंतव्यों तक सीमित रखें और dtype हैंडलिंग में एकरूपता बनाए रखें, ताकि जो सहेजें, वही पढ़ें।