Postgres في التقنية المالية

لماذا PostgreSQL هي مصدر الحقيقة الافتراضي لدفاتر الحسابات والمحافظ والمدفوعات، وأين تنكسر، وكيف تشغّلها بما يرضي الجهات الرقابية في مصر والسعودية والإمارات.

· · تحديث · 33 دقائق قراءة

غلاف طباعي داكن يحمل عنوان Postgres in Fintech مع شارة زرقاء لتصنيف الخلفية والبيانات
في هذا المقال

لماذا يهم Postgres في التقنية المالية

كل منتج في التقنية المالية هو، تحت واجهة التطبيق، وعدٌ بشأن الأرقام. المحفظة تعد بأن الرصيد الظاهر على الشاشة هو المال الذي يستطيع العميل إنفاقه فعلًا. وشركة الإقراض تعد بأن جدول الأقساط يساوي قيمة العقد. وبوابة الدفع تعد بأن التاجر حصل على أمواله مرة واحدة بالضبط عن كل عملية تفويض تم تحصيلها. قاعدة البيانات هي المكان الذي تُحفظ فيه هذه الوعود أو تُكسر، وبالنسبة لمعظم الشركات التي تبني اليوم في مصر والسعودية والإمارات، قاعدة البيانات هذه هي PostgreSQL.

أقول «معظم» عن قصد؛ فهناك شركات تقنية مالية تعمل على MySQL وOracle وقواعد المستندات وقواعد SQL الموزعة. لكن حين أجلس مع فريق مؤسس لاختيار مصدر الحقيقة في منتج جديد، تكون Postgres هي الخيار الافتراضي، وعلى كل بديل آخر أن يدافع عن نفسه ليحل محلها. هذا المقال عن سبب ذلك، وعن الحالات التي يتوقف فيها الخيار الافتراضي عن كونه الصواب، وعما يتطلبه تشغيل Postgres في شركة مالية خاضعة للرقابة لا في مشروع جانبي.

قاعدة البيانات هي المنتج

في تطبيق للمحتوى، قاعدة البيانات مجرد أنابيب خلفية. أما في التقنية المالية فقاعدة البيانات هي المنتج نفسه: الرصيد استعلام، وكشف الحساب استعلام، والتقرير الرقابي استعلام. ولذلك تتغير الأسئلة. تتوقف عن السؤال أولًا «كم طلبًا في الثانية؟» وتبدأ بالسؤال: «هل يمكن لتحويلين متزامنين أن يتركا الدفاتر غير متوازنة؟» و«هل يستطيع أحد تعديل التاريخ؟» و«هل أستطيع أن أثبت للمدقق ما كان عليه الرصيد في الساعة 23:59 من آخر يوم في الربع؟».

تجيب Postgres عن هذه الأسئلة بخصائص قديمة وموثقة جيدًا ومملة: معاملات حقيقية بمستوى عزل قابل للتسلسل، وقيود يفرضها المحرك لا التطبيق، ومشغّلات (triggers) لا تستطيع خدمة مهملة تجاوزها، وسجل كتابة مسبقة (WAL) يمنحك الاستعادة إلى نقطة زمنية محددة، ونموذج صلاحيات دقيق بما يكفي لمنع واجهتك البرمجية نفسها من قراءة صفوف مستأجر آخر.

الصحة ميزة يستطيع المنظّم قراءتها

مسؤول الامتثال في البنك السعودي الشريك أو المفتش القادم من البنك المركزي المصري لن يقرأ كودك المكتوب بلغة Go. سيقرأ مخطط قاعدة بياناتك وضوابط الوصول وسجلات التدقيق، أو على الأقل الوثائق التي تصفها. دفتر الحسابات الذي تعيش ثوابته في قيود CHECK ومشغّلات أسهل في الشرح من دفتر تعيش ثوابته في خدمة «تستدعي دائمًا» الدالة الصحيحة. تصبح قاعدة البيانات هي المكان الذي تُكتب فيه القواعد بصيغة يستطيع المهندس والمدقق معًا التحقق منها.

«مملة» هنا مديح

في المدفوعات، أثمن خاصية في أي تقنية هي ألا يحدث شيء مفاجئ في الثالثة فجرًا.

تصدر Postgres إصدارًا رئيسيًا كل عام، وصيغة التخزين على القرص مستقرة، وأنماط الفشل فيها مفهرسة ومعروفة، وحين يحدث خطأ ما تكون الإجابة غالبًا على بعد بحث واحد. بالنسبة لقائد تقني في شركة تقنية مالية، هذه القابلية للتنبؤ تساوي أكثر من أي اختبار أداء، ومعظم ما يلي هو عن الحصول عليها عمدًا لا بالحظ.

أين يُستخدم

أمثلة علنية

تتميز Postgres بين قواعد البيانات بكمية ما كُتب علنًا عن استخدامها في الإنتاج. مقالات Instagram الهندسية المبكرة وصفت تقسيم البيانات (sharding) على PostgreSQL وتوليد المعرّفات داخل دوال PL/pgSQL بينما كانت الشركة تنمو بملايين المستخدمين شهريًا. وشغّلت Skype خلفيتها على Postgres وقدّمت للعالم PgBouncer وPL/Proxy في الطريق. أما مقال Stripe عن دفتر حساباتها الداخلي فلا يقول الكثير عن التخزين، لكنه يستحق القراءة من أجل التصميم: قيد مزدوج، وعدم قابلية للتعديل، وتسوية مستمرة مقابل الأنظمة الخارجية.

على جانب المنصات، أقوى إشارة هي أين يضع مزودو السحابة أموالهم: Amazon Aurora PostgreSQL وRDS، وGoogle Cloud SQL وAlloyDB، وAzure Database for PostgreSQL، وSupabase وNeon، كلها رهانات على أن السوق يريد Postgres مُدارة، وعدد منها يعمل الآن في مناطق خليجية. وعلى الطرف الآخر تقف TigerBeetle، قاعدة بيانات مبنية فقط لمحاسبة القيد المزدوج بمعدلات معاملات عالية جدًا؛ وهي أوضح تعبير عما يبدو عليه مخزن متخصص في دفاتر الحسابات.

داخل منظومة تقنية مالية نموذجية

في محفظة إلكترونية أو منتج «اشترِ الآن وادفع لاحقًا» أو بوابة دفع مبنية في المنطقة، تحتفظ Postgres عادةً بدفتر الحسابات (الحسابات وقيود اليومية وسطور القيود والأرصدة المشتقة منها)، وبرسم العملاء (الهويات وحالة اعرف عميلك والحدود والموافقات والأجهزة)، وبالحالة التشغيلية للمدفوعات (التفويضات والتحصيلات والاستردادات ودفعات التسوية ونتائج المطابقة)، وبالإعدادات التي يجب أن تبقى متسقة مع حركة الأموال مثل الرسوم وأسعار الصرف السارية. أما ما لا تحتفظ به عادة فهو تدفق النقرات، وسيل الأحداث الخام القادم من معالج البطاقات، وتجميعات لوحات المعلومات عبر سنوات من التاريخ؛ فهذه تعيش في تخزين الكائنات أو طابور رسائل أو قاعدة بيانات تحليلية تتغذى من Postgres، لا بديلًا عنها.

في منطقتنا تحديدًا

ثلاثة أمور تشكّل طريقة استخدام Postgres في مصر والسعودية والإمارات. أنظمة الترخيص (البنك المركزي المصري، والبنك المركزي السعودي SAMA، ومصرف الإمارات المركزي CBUAE، ومنظّمو المناطق الحرة) تدفع الفرق إلى إبقاء البيانات المالية الأساسية داخل البلد أو المنطقة، ما يجعل توفر Postgres مُدارة في مناطق خليجية عاملًا حاسمًا. ومواهب إدارة قواعد البيانات نادرة، لذا تتفوق النشرات الصغيرة المفهومة جيدًا على النشرات الذكية المعقدة. والفرق الصغيرة التي تتحرك بسرعة هي الأكثر استفادة من قاعدة بيانات تفرض الثوابت نيابة عنها.

نقاط القوة

معاملات يمكنك التفكير فيها بوضوح

تطبّق Postgres التحكم في التزامن متعدد الإصدارات (MVCC)، فلا يحجب القرّاء الكتّاب ولا يحجب الكتّاب القرّاء. وتوفر مستويات العزل الثلاثة المهمة عمليًا، وتنفيذها لمستوى SERIALIZABLE (العزل القابل للتسلسل عبر اللقطات) يحقق ما يعد به الاسم دون قفل كل شيء. فصل عزل المعاملات في التوثيق الرسمي قصير، وعلى كل مهندس يلمس المال أن يكون قد قرأه.

القيود بوصفها خط الدفاع الأخير

تتيح لك NOT NULL وCHECK وUNIQUE والمفاتيح الأجنبية وقيود الاستبعاد ومشغّلات القيود القابلة للتأجيل أن تكتب «يجب أن يتوازن القيد» و«لا يمكن أن تهبط المحفظة تحت حد السحب على المكشوف» و«لا تتشارك بطاقتان نشطتان نفس تجزئة رقم البطاقة» في مكان يمر به كل مسار كود بالضرورة. كود التطبيق فيه أخطاء؛ والقيود تحوّل هذه الأخطاء إلى رسائل خطأ صاخبة بدلًا من فساد صامت في البيانات.

نظام أنواع مبني للمال والوقت

يمنحك NUMERIC حسابًا عشريًا دقيقًا بالدقة التي تختارها. ويخزّن timestamptz لحظة زمنية ويحوّلها إلى منطقة الجلسة، ما ينقذك من أخطاء فروق التوقيت بين القاهرة والرياض ودبي التي تبتلي الأنظمة التي تخزّن طوابع زمنية بلا منطقة. ويتيح JSONB الاحتفاظ باستجابة معالج الدفع الخام بجوار الصف المطبّع، مفهرسة، دون قاعدة مستندات. وأنواع النطاقات وقيود الاستبعاد تنمذج فترات سريان الرسوم والأسعار بنظافة.

الامتدادات والمنظومة المحيطة

pgcrypto للتجزئة والتشفير المتماثل، وpg_partman لصيانة الأقسام، وpg_stat_statements للعثور على الاستعلام الذي يؤذيك، وPostGIS للسياج الجغرافي حول شبكة الوكلاء، والنسخ المنطقي مع أدوات التقاط تغييرات البيانات المبنية عليه، وأشهرها Debezium. عبارة «نحتاج إلى X» تجد جوابها غالبًا في شيء ناضج بدلًا من شيء تكتبه بنفسك. أضف إلى ذلك أن كل سحابة كبرى تقدمها مُدارة، وأن معظم مطوري الخلفية في المنطقة لمسوها على الأقل، فتحصل على ميزة في التوظيف والمناوبة تشعر بها كل شهر.

المخاطر والمزالق

معاملة قاعدة البيانات كمخزن غبي

أكثر إخفاق أراه هو فريق يستخدم Postgres كمخزن مفتاح-قيمة خلف ORM، ويبقي كل القواعد في كود التطبيق، ثم يكتشف أن خدمتين تختلفان حول معنى الرصيد. إن لم تكن ثوابتك في المخطط، فليس لديك ثوابت؛ لديك أعراف.

الفاصلة العائمة والتقريب الصامت

لا تخزّن المال أبدًا في float أو double؛ هذا معروف ومع ذلك يحدث. الأقل شهرة: NUMERIC(20,4) يقرّب بصمت أي قيمة فيها أكثر من أربع خانات عشرية عند الإدراج، فرسم محسوب بست خانات يصل إلى الدفتر رقمًا مختلفًا عما يظنه محرك الرسوم.

مفاجآت Read Committed

مستوى العزل الافتراضي هو READ COMMITTED. وهو جيد لمعظم القراءات وخطر على عمليات «اقرأ ثم عدّل ثم اكتب» على المال: يمكن لمعاملتين أن تقرآ معًا رصيدًا قدره 100، وتقررا معًا أن سحب 80 ممكن، وتُنفَّذا معًا. هذا هو الجدول الذي أرسمه على السبورة حين ينضم مهندس جديد.

مستوى العزل ما تراه المعاملة الشذوذات التي ما زالت ممكنة الاستخدام في دفتر الحسابات
READ COMMITTED (الافتراضي) لقطة جديدة لكل عبارة قراءات غير قابلة للتكرار، تحديثات ضائعة عند القراءة ثم الكتابة، انحراف الكتابة قراءات التقارير والإدراجات الآمنة عند التكرار؛ أبدًا لفحص الأرصدة
REPEATABLE READ لقطة واحدة للمعاملة كلها انحراف الكتابة (معاملتان تقرآن ثم تكتبان معًا) قراءات يجب أن تبقى متسقة عبر عدة استعلامات
SERIALIZABLE كأن المعاملات نُفذت واحدة تلو الأخرى لا شيء؛ يُجهض المحرك المعاملة بالخطأ 40001 بدلًا من ذلك حركة الأموال وفحص الحدود وكل ما فيه ثابت يمتد عبر صفوف

دفاتر قابلة للتعديل

إذا كان جدول الدفتر يسمح بـ UPDATE أو DELETE، فسيستخدمه أحدهم «لإصلاح» صف، وبعد ستة أشهر لن يستطيع أحد تفسير سبب عدم تطابق رصيد نهاية الشهر مع كشف البنك. التصحيحات قيود جديدة تعكس القيود القديمة. المخطط أدناه يفرض ذلك بمشغّل؛ والمؤسسة تفرضه بعدم منح الصلاحية أصلًا.

المعاملات الطويلة والانتفاخ والالتفاف

تحتفظ MVCC بالإصدارات القديمة من الصفوف حتى لا تعود أي معاملة قادرة على رؤيتها. مهمة تقارير تُبقي معاملة مفتوحة لساعة، أو اتصال خامل داخل معاملة من سكربت منسي، يحجب عملية vacuum وينفخ الجداول، وفي الحالة القصوى يسير بك نحو التفاف معرّفات المعاملات (wraparound) حيث ترفض Postgres الكتابة لحماية نفسها. فصل التنظيف الدوري يشرح الآلية؛ أما idle_in_transaction_session_timeout وتنبيه على عمر أقدم معاملة فهما ما يمنعك من اكتشاف ذلك في الإنتاج.

عواصف الاتصالات ووهم «المُدار»

كل اتصال في Postgres هو عملية في نظام التشغيل. أسطول من حاويات الواجهة البرمجية يتوسع تلقائيًا ويفتح كل منها عشرين اتصالًا سيستنفد max_connections عند أول ذروة في الحركة؛ فمجمّع الاتصالات ليس خيارًا في الإنتاج. والخدمات المُدارة تزيل العتاد والترقيعات والنسخ الاحتياطي الأساسي، لكنها لا تزيل الحاجة إلى شخص يملك خطط الاستعلامات ونظافة الفهارس وإعدادات vacuum وتمارين الاستعادة. المزلق هو الاعتقاد بأن كلمة «مُدار» تغطي كل ذلك.

أنماط البنية والتكامل

دفتر الحسابات في المركز

كل شيء في نموذج بيانات التقنية المالية يتعلق بدفتر الحسابات، لذا صمّمه أولًا، وصمّمه بوصفه محاسبة لا جدول balances تكتب فوقه. أعرافي: قيد يومية واحد لكل حدث تجاري، مع مرجع خارجي يجعله آمنًا عند التكرار؛ ومبالغ سطور موقّعة، المدين موجب والدائن سالب، بحيث يعني «متوازن» ببساطة «مجموعه صفر»؛ وأرصدة مشتقة من السطور، مع رصيد جارٍ مادي فقط حيث تتطلب الإنتاجية ذلك؛ ولا UPDATE ولا DELETE على جداول الدفتر أبدًا، لأن العكس قيد جديد.

دفتر قيد مزدوج بلغة SQL صِرفة

-- Chart of accounts. Customer wallets are liabilities: money we owe the customer.
CREATE TABLE accounts (
    id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    code        TEXT NOT NULL UNIQUE,
    currency    CHAR(3) NOT NULL,
    kind        TEXT NOT NULL CHECK (kind IN ('asset', 'liability', 'equity', 'revenue', 'expense')),
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- One journal entry per business event: a transfer, a fee, a reversal.
CREATE TABLE journal_entries (
    id            BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    external_ref  TEXT NOT NULL UNIQUE,      -- payment id, request id, batch id
    description   TEXT NOT NULL,
    posted_at     TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Signed amounts: debit is positive, credit is negative, so every entry sums to zero.
CREATE TABLE entry_lines (
    id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    entry_id    BIGINT NOT NULL REFERENCES journal_entries(id),
    account_id  BIGINT NOT NULL REFERENCES accounts(id),
    amount      NUMERIC(20,4) NOT NULL CHECK (amount <> 0),
    currency    CHAR(3) NOT NULL
);

-- Balance check, deferred to COMMIT so an entry can be inserted line by line.
CREATE OR REPLACE FUNCTION assert_entry_balanced() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
    IF (SELECT sum(amount) FROM entry_lines WHERE entry_id = NEW.entry_id) <> 0 THEN
        RAISE EXCEPTION 'journal entry % does not balance', NEW.entry_id
              USING ERRCODE = 'integrity_constraint_violation';
    END IF;
    RETURN NULL;
END $$;

CREATE CONSTRAINT TRIGGER entry_lines_balanced
AFTER INSERT ON entry_lines
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE FUNCTION assert_entry_balanced();

-- Append-only: ledger rows are never edited or deleted, only reversed by a new entry.
CREATE OR REPLACE FUNCTION forbid_mutation() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
    RAISE EXCEPTION '% is append-only; post a reversing entry instead', TG_TABLE_NAME;
END $$;

CREATE TRIGGER entry_lines_append_only
BEFORE UPDATE OR DELETE ON entry_lines
FOR EACH ROW EXECUTE FUNCTION forbid_mutation();

تم الاختبار مع PostgreSQL 16.

ثلاثة أشياء في هذا المخطط تقوم بعمل حقيقي. CHECK (amount <> 0) يمنع السطور الصفرية التي تخفي الأخطاء. ومشغّل القيد DEFERRABLE INITIALLY DEFERRED، فيمكن إدراج القيد على عدة عبارات داخل معاملة واحدة ولا يُفحص إلا عند الإيداع، حيث يُفشل القيد غير المتوازن المعاملة كلها. ومشغّل الإلحاق فقط يحوّل عبارة UPDATE entry_lines SET amount = ... حسنة النية إلى خطأ يقول ما الذي ينبغي فعله بدلًا منها. أضف المشغّل نفسه إلى journal_entries، وأبقِ عمود currency على السطور رغم أن الحسابات تحمله: دفاتر العملات المتعددة تحتاجه في النهاية، ويستطيع مشغّل التحقق من تطابق الاثنين.

تحويل ينجو من إعادة المحاولة والتزامن

-- Idempotency: one row per client request key, reused to answer retries.
CREATE TABLE idempotency_keys (
    key         TEXT PRIMARY KEY,
    entry_id    BIGINT REFERENCES journal_entries(id),
    response    JSONB,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Retry policy, implemented in the application around this whole block:
--   * SQLSTATE 40001 (serialization_failure) or 40P01 (deadlock_detected):
--     roll back, wait with jittered backoff, retry the whole transaction (max ~5).
--   * SQLSTATE 23505 on idempotency_keys: another worker owns this request;
--     return its stored response, or 409 if it is still in flight. Do not retry.
BEGIN ISOLATION LEVEL SERIALIZABLE;

-- 1. Claim the request key first; duplicates fail here before touching balances.
INSERT INTO idempotency_keys (key) VALUES ('req_7f3a');

-- 2. Lock both accounts in a fixed order (lowest id first) so concurrent
--    transfers 101->202 and 202->101 queue instead of deadlocking.
SELECT id, currency FROM accounts WHERE id IN (101, 202) ORDER BY id FOR UPDATE;

-- 3. Available balance of the source wallet. Wallets are liabilities, so the
--    credit balance is the negated sum. The application aborts if it is < 250.
SELECT -coalesce(sum(amount), 0) AS available
FROM entry_lines WHERE account_id = 101;

-- 4. Post the entry: debit the sender's wallet, credit the receiver's wallet.
WITH je AS (
    INSERT INTO journal_entries (external_ref, description)
    VALUES ('req_7f3a', 'wallet transfer 101 -> 202')
    RETURNING id
)
INSERT INTO entry_lines (entry_id, account_id, amount, currency)
SELECT je.id, v.account_id, v.amount, 'EGP'
FROM je, (VALUES (101, 250.0000), (202, -250.0000)) AS v(account_id, amount);

-- 5. Store the outcome so a retried request returns the same answer.
UPDATE idempotency_keys
SET entry_id = (SELECT id FROM journal_entries WHERE external_ref = 'req_7f3a'),
    response = jsonb_build_object('status', 'posted', 'amount', '250.0000')
WHERE key = 'req_7f3a';

COMMIT;  -- the deferred balance trigger runs here and can still reject the entry

تم الاختبار مع PostgreSQL 16.

آليتان تتداخلان هنا عن قصد. يضمن SERIALIZABLE ألا يُنتج أي تشابك بين تحويلات متزامنة نتيجة كانت مستحيلة لو نُفذت تسلسليًا؛ وإذا اكتشف واحدة أجهض المعاملة بالخطأ 40001 وعلى التطبيق إعادة المحاولة. أما SELECT ... FOR UPDATE بترتيب المعرّفات تصاعديًا فيجعل الحالة الشائعة رخيصة: تحويلان يلمسان المحفظة نفسها يصطفان خلف بعضهما بدلًا من التسابق نحو فشل تسلسل، والترتيب الثابت يعني أنهما لا يمكن أن يتعطلا في قفل متبادل أبدًا. ويُحجز مفتاح التكرار الآمن قبل قراءة أي رصيد، فالعميل الذي يعيد المحاولة بعد انقطاع في الشبكة يحصل على استجابته المخزنة أو على تعارض نظيف، لا على خصم ثانٍ أبدًا.

فحص الرصيد عبر sum(amount) صحيح، ومع الفهرس التغطوي المعروض لاحقًا سريع بما يكفي لمعظم المنتجات؛ أما الحسابات القليلة التي تستحوذ على معظم الكتابات فنعالجها في قسم الأداء.

التكرار الآمن عند الحافة وصندوق الصادر عند المخرج

مفاتيح التكرار الآمن (idempotency keys) مكانها حدود الواجهة البرمجية وداخل قاعدة البيانات كما هو موضح، وليس في ذاكرة مؤقتة فقط. وللأحداث التي تغادر النظام، يُبقيك نمط صندوق الصادر المعاملاتي (transactional outbox) صادقًا: اكتب صف الحدث في المعاملة نفسها مع قيد الدفتر، ودع مرحّلًا ينشره إلى Kafka أو طابورك. ويمكن لـالنسخ المنطقي الخاص بـ Postgres وأدوات CDC المبنية عليه، ومنها Debezium، أن تحل محل المرحّل ببث الصفوف المودعة إلى المستهلكين دون كتابة مزدوجة.

خيارات تعدد المستأجرين

منصات الدفع والخدمات المصرفية كخدمة ومنتجات قبول مدفوعات التجار تخدم مستأجرين كثيرين من قاعدة كود واحدة. هناك ثلاثة خيارات صادقة؛ والجداول المشتركة مع أمان مستوى الصفوف هي خياري الافتراضي للمنتجات الجديدة، والمخطط في قسم الأمن يوضحها.

النموذج العزل التكلفة التشغيلية متى يناسب
قاعدة بيانات لكل مستأجر الأقوى؛ نسخ احتياطي وإقامة بيانات منفصلان لكل مستأجر الأعلى؛ تتضاعف الترحيلات والمراقبة عدد قليل من المستأجرين الكبار الخاضعين للرقابة مثل البنوك وشركات الاتصالات
مخطط (schema) لكل مستأجر قوي؛ عنقود واحد ومساحات أسماء منفصلة متوسطة؛ يصبح التجميع والترحيل مربكين بعد مئات المستأجرين عشرات المستأجرين متوسطي الحجم ذوي الاحتياجات الخاصة
جداول مشتركة مع tenant_id وRLS يفرضه المحرك على مستوى كل صف الأدنى؛ مخطط واحد وترحيل واحد مستأجرون صغار كثيرون، وتقنية مالية بنمط SaaS، والأسواق الإلكترونية

الأمن والامتثال

الأطر التي ستقابلها

بحسب المنتج والبلد، الأطر التي تشكّل قرارات قاعدة البيانات هي PCI DSS إن كنت تلمس بيانات البطاقات؛ وقواعد الترخيص والإسناد الخارجي لدى SAMA في السعودية وCBUAE في الإمارات والبنك المركزي المصري CBE في مصر؛ وقوانين حماية البيانات في كل بلد (نظام حماية البيانات الشخصية السعودي PDPL، وقانون حماية البيانات الاتحادي الإماراتي مع نظامي DIFC وADGM، وقانون حماية البيانات الشخصية المصري)؛ وللواجهات المصرفية المفتوحة أو العملاء الأوروبيين، PSD2 وأطر الخدمات المصرفية المفتوحة الإقليمية. ولا يسمّي أي منها قاعدة بيانات بعينها، لكنها كلها تطرح الأسئلة نفسها: أين البيانات، ومن يستطيع قراءتها، ومن غيّرها، وهل تستطيع استعادتها، وكم تحتفظ بها.

التشفير أثناء التخزين وأثناء النقل

شفّر الأقراص (كل خدمة مُدارة تفعل ذلك افتراضيًا، وغالبًا بمفتاح تستطيع إحضاره بنفسك) واشترط TLS على كل اتصال، حتى داخل الشبكة الخاصة: ssl = on مع قواعد hostssl في pg_hba.conf أو ما يعادلها في الخدمة المُدارة، إضافة إلى sslmode=verify-full من جهة العملاء. تشفير القرص يلبّي خانة «أثناء التخزين» في قائمة الفحص؛ لكنه لا يفعل شيئًا ضد بيانات اعتماد مسرّبة لقاعدة البيانات، وهذا هو التهديد الواقعي.

pgcrypto مقابل التشفير على مستوى التطبيق

تمنحك pgcrypto الدوال digest وhmac وpgp_sym_encrypt داخل SQL. وهي مناسبة لتجزئة المعرّفات التي تحتاج إلى البحث عنها، مع ملح سري من جهة الخادم، وللأسرار قليلة الحجم. لكنها خيار خاطئ كحماية أساسية لبيانات البطاقات أو أرقام الوثائق، لأن المفتاح يجب أن يكون حاضرًا في جلسة قاعدة البيانات ويظهر في السجلات وفي pg_stat_statements إن لم تكن حذرًا. لتلك الحقول، شفّر في التطبيق بمفتاح من KMS أو HSM، وخزّن النص المشفر مع فهرس أعمى (blind index) للبحث، وأبقِ المفتاح خارج قاعدة البيانات تمامًا. أما بيانات البطاقات تحديدًا فأقوى خطوة هي ألا تخزّنها أصلًا: استخدم الترميز (tokenisation) عبر مزود خاضع لنطاق PCI حتى لا تدخل Postgres لديك في النطاق مطلقًا.

الأدوار وأقل الصلاحيات

دور واحد لكل خدمة، ولا مستخدم خارق مشترك، ولا دور مالك يستخدمه تطبيق. يحصل دور الواجهة البرمجية على SELECT وINSERT على جداول الدفتر ولا شيء غيرهما؛ ولا يُمنح UPDATE ولا DELETE حتى وإن كان المشغّل سيحجبهما. ويحصل فريق التقارير على دور للقراءة فقط على نسخة متماثلة. يتصل البشر عبر خادم وسيط أو وكيل مُدار ببيانات اعتماد قصيرة العمر وأدوار بأسمائهم، وتُسجَّل كل جلسة. وافصل دور الترحيلات عن دور التطبيق حتى لا تستطيع بيانات اعتماد مخترقة للواجهة البرمجية تعديل المشغّلات.

أمان مستوى الصفوف وسجل التدقيق

-- Every tenant-owned table carries tenant_id. The API sets the tenant per
-- transaction: SET LOCAL app.tenant_id = '<uuid>'; SET LOCAL app.user_id = '<id>';
CREATE TABLE wallets (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id   UUID NOT NULL,            -- references tenants(id)
    owner_ref   TEXT NOT NULL,
    status      TEXT NOT NULL DEFAULT 'active',
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX wallets_tenant_idx ON wallets (tenant_id);

ALTER TABLE wallets ENABLE ROW LEVEL SECURITY;
ALTER TABLE wallets FORCE ROW LEVEL SECURITY;   -- the table owner is not exempt

-- missing_ok = true yields NULL when unset; a NULL comparison hides every row.
CREATE POLICY wallets_tenant_isolation ON wallets
    USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
    WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);

-- The API role is not the table owner and must never have BYPASSRLS.
CREATE ROLE app_api NOLOGIN;
GRANT SELECT, INSERT, UPDATE ON wallets TO app_api;

-- Audit trail with before/after images; a definer function writes it, so
-- app_api needs no privilege on audit_log and cannot edit or skip it.
CREATE TABLE audit_log (
    id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    tenant_id   UUID,
    table_name  TEXT NOT NULL,
    operation   TEXT NOT NULL,
    row_pk      TEXT,
    old_row     JSONB,
    new_row     JSONB,
    db_user     TEXT NOT NULL DEFAULT current_user,
    app_user    TEXT DEFAULT current_setting('app.user_id', true),
    at          TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);

CREATE OR REPLACE FUNCTION audit_row_change() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, public AS $$
DECLARE
    o JSONB := CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN to_jsonb(OLD) END;
    n JSONB := CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN to_jsonb(NEW) END;
BEGIN
    INSERT INTO audit_log (tenant_id, table_name, operation, row_pk, old_row, new_row)
    VALUES (coalesce(n ->> 'tenant_id', o ->> 'tenant_id')::uuid, TG_TABLE_NAME,
            TG_OP, coalesce(n ->> 'id', o ->> 'id'), o, n);
    RETURN NULL;
END $$;

CREATE TRIGGER wallets_audit
AFTER INSERT OR UPDATE OR DELETE ON wallets
FOR EACH ROW EXECUTE FUNCTION audit_row_change();

تم الاختبار مع PostgreSQL 16.

ينقل أمان مستوى الصفوف عزل المستأجرين من «كل استعلام يتذكر شرط WHERE» إلى «المحرك يضيفه بنفسه». تضبط الواجهة البرمجية app.tenant_id عبر SET LOCAL في بداية كل معاملة، وهو ما يعمل بشكل صحيح خلف مجمّع اتصالات بوضع المعاملة لأن الإعداد يموت مع المعاملة. وتستخدم السياسة missing_ok = true حتى يرى الطلب الذي نسي ضبط المستأجر لا شيء، وهذا هو الفشل الآمن. وتعمل دالة التدقيق بصلاحيات مُعرِّفها، فلا يحتاج دور التطبيق إلى أي صلاحية على audit_log ولا يستطيع قراءته أو تعديله. وسجّل مستخدم التطبيق إلى جانب مستخدم قاعدة البيانات؛ فكلمة «api» بوصفها فاعل كل تغيير لا تفيد شيئًا في أي تحقيق.

النسخ الاحتياطي والاستعادة إلى نقطة زمنية والاستعادة التي اختبرتها فعلًا

أرشفة WAL المستمرة مع النسخ الأساسية تمنحك الاستعادة إلى نقطة زمنية: إعادة قاعدة البيانات إلى ما كانت عليه في 14:32:07، قبل النشر السيئ مباشرة. الخدمات المُدارة توفر ذلك ضمن نافذة احتفاظ؛ والفرق التي تدير بنفسها تستخدم pgBackRest أو WAL-G. لكن شيئًا من ذلك لا يُحتسب حتى تكون قد استعدت إلى خادم جديد، وشغّلت المطابقة مقابل كشف البنك، وقست زمن العملية كاملة. افعل ذلك كل ربع سنة، ودوّن المدة، وضعها في وثيقة استمرارية الأعمال؛ فالجهات الرقابية تسأل عنها.

إقامة البيانات والخدمات المُدارة داخل المنطقة

إقامة البيانات ليست فقط مكان الخادم الرئيسي. النسخ المتماثلة والنسخ الاحتياطي وأرشيفات WAL وأهداف النسخ المنطقي ومستودع التحليلات كلها تُحتسب. ارسم تدفق البيانات وضع علامة المنطقة على كل صندوق وأبقِ الرسم محدثًا؛ فهذه أول وثيقة سيطلبها أي طلب موافقة على السحابة أو الإسناد الخارجي.

الاحتفاظ والحق في النسيان

يجب الاحتفاظ بالسجلات المالية لسنوات؛ ويجب حذف البيانات الشخصية عند الطلب. لا يتعارض الأمران إلا إذا وضعت بيانات شخصية في الدفتر. اربط الدفتر بمعرّفات حسابات مبهمة، وأبقِ البيانات الشخصية في جداولها الخاصة، وحقق المحو بحذف الصفوف الشخصية أو بإتلاف مفاتيح تشفيرها بينما يبقى الدفتر سليمًا ومجهول الهوية. حدد الاحتفاظ لكل جدول كتابةً، ونفّذه بأقسام تفصلها وتؤرشفها بدلًا من عبارات DELETE التي تنفخ الجدول وسجل WAL.

الأداء والتوسع

تجميع الاتصالات

ضع PgBouncer (أو الوكيل المُدار الذي تقدمه سحابتك) بوضع المعاملة بين الخدمات وقاعدة البيانات. فهو يحوّل آلاف اتصالات العملاء إلى بضع عشرات من اتصالات الخادم، وهذا ما تسعد به Postgres. الثمن أن خصائص مستوى الجلسة (العبارات المُعدّة المسماة، وSET بدون LOCAL، والأقفال الاستشارية الممتدة عبر المعاملات) تحتاج إلى عناية خاصة.

فهارس تستحق تكلفتها

كل فهرس يسرّع بعض القراءات ويبطئ كل كتابة: إدراج سطر دفتر واحد في جدول عليه خمسة فهارس يكتب صفحة الكومة وخمس صفحات فهارس وسجل WAL لها جميعًا، ثم يشحن ذلك السجل إلى كل خادم احتياطي. الفهارس التي تدفع ثمنها على دفتر الحسابات: فهرس مركب على (account_id, posted_at DESC) لكشوف الحساب، يُجعل تغطويًا عبر INCLUDE (amount, currency) حتى لا تلمس مجاميع الأرصدة الكومة أبدًا؛ وفهرس جزئي مثل WHERE status = 'pending' على جدول المدفوعات، يبقى صغيرًا جدًا لأن كل صف تقريبًا في حالة نهائية؛ وفهرس BRIN على أعمدة الوقت في الجداول الإلحاقية فقط، ببضع صفحات لكل قسم بدلًا من شجرة B بحجم البيانات نفسها. واستخدم pg_stat_user_indexes لاكتشاف الفهارس التي لا يقرأها أحد وحذفها.

تقسيم الدفتر بالشهر

-- Partitioned version of entry_lines. The partition key must be part of every
-- unique constraint, so the primary key becomes (id, posted_at). bigserial rather
-- than an identity column: identity on partitioned tables arrived in PostgreSQL 17.
CREATE TABLE entry_lines (
    id          BIGSERIAL,
    entry_id    BIGINT NOT NULL REFERENCES journal_entries(id),
    account_id  BIGINT NOT NULL REFERENCES accounts(id),
    amount      NUMERIC(20,4) NOT NULL CHECK (amount <> 0),
    currency    CHAR(3) NOT NULL,
    posted_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (id, posted_at)
) PARTITION BY RANGE (posted_at);

-- One partition per month, bounded in UTC so DST changes never shift a boundary.
-- Create partitions ahead of time (pg_partman or a scheduled job).
CREATE TABLE entry_lines_2026_09 PARTITION OF entry_lines
    FOR VALUES FROM ('2026-09-01T00:00:00Z') TO ('2026-10-01T00:00:00Z');
CREATE TABLE entry_lines_2026_10 PARTITION OF entry_lines
    FOR VALUES FROM ('2026-10-01T00:00:00Z') TO ('2026-11-01T00:00:00Z');

-- A default partition keeps inserts from failing when a month is missing.
-- Alert when it is not empty: rows there mean the partition job did not run.
CREATE TABLE entry_lines_default PARTITION OF entry_lines DEFAULT;

-- Indexes declared on the parent are created on every partition, now and later.
-- 1. Statement history per account, newest first: the hottest query in a wallet.
--    INCLUDE makes it covering for balance sums, so no heap fetch is needed.
CREATE INDEX entry_lines_account_posted_idx
    ON entry_lines (account_id, posted_at DESC) INCLUDE (amount, currency);

-- 2. All lines of one journal entry: reversals, disputes, reconciliation.
CREATE INDEX entry_lines_entry_idx ON entry_lines (entry_id);

-- 3. BRIN on posted_at: a few pages per partition, ideal for append-only,
--    time-ordered data read by month-end reports that scan ranges.
CREATE INDEX entry_lines_posted_brin ON entry_lines USING brin (posted_at);

-- Partition pruning: a query with WHERE posted_at >= '2026-10-01' touches only the
-- matching partitions; confirm with EXPLAIN that the plan lists just those.

تم الاختبار مع PostgreSQL 16.

التقسيم ليس في المقام الأول تسريعًا للاستعلامات؛ بل أداة تشغيلية. الأقسام الشهرية تبقي كل جدول وفهرس صغيرًا بما يكفي لتنظيفه وإعادة فهرسته بسرعة، وتتيح لك فصل الشهور القديمة وأرشفتها لأغراض الاحتفاظ بدلًا من حذف الصفوف، وتبقي مجموعة العمل للشهر الحالي في الذاكرة. والمشغّلات على مستوى الصفوف المعرّفة على الجدول الأب، ومنها مشغّلا التوازن والإلحاق فقط من المقطع الأول، تُستنسخ على كل قسم. تحذيران: إلحاق قسم جديد بينما يحتوي القسم الافتراضي على صفوف يفرض مسحًا كاملًا للقسم الافتراضي، فأبقِه فارغًا؛ والتقسيم لا يفيد إلا الاستعلامات التي ترشّح على مفتاح التقسيم، فلا بد أن تحمل استعلامات كشف الحساب نطاقًا على posted_at.

التنظيف والانتفاخ وتحديثات HOT

لا تحدّث Postgres صفًا في مكانه أبدًا؛ بل تكتب إصدارًا جديدًا وتترك القديم لعملية vacuum. الجداول كثيرة التحديث (جدول balances، أو جدول payments تتغير حالته خمس مرات) تنتفخ إن لم تواكبها عملية autovacuum. اضبط autovacuum_vacuum_scale_factor أقل بكثير من الافتراضي على الجداول الساخنة، وأبقِ المعاملات قصيرة، وصمّم لتحديثات الصفوف داخل الكومة فقط (HOT): التحديث الذي لا يلمس عمودًا مفهرسًا ويتسع في الصفحة نفسها لا يكتب أي مدخلات فهارس جديدة على الإطلاق. وقيمة fillfactor بين 80 و90 على الجداول الساخنة تترك مساحة لذلك.

NUMERIC مقابل الوحدات الصغرى الصحيحة

هناك طريقتان صحيحتان لتخزين المال. NUMERIC(20,4) يصف نفسه بنفسه، ويتعامل مع عملات ذات وحدات صغرى مختلفة ومع التسعير دون الوحدة، وهو ما تستخدمه معظم الدفاتر في المنطقة. أما الوحدات الصغرى بنوع BIGINT (القروش والهللات والفلوس) فأصغر حجمًا وأسرع في الجمع ويستحيل تقريبها بصمت، مقابل طبقة تحويل واعية بالعملة في كل خدمة. إن اخترت الأعداد الصحيحة، فخزّن أس العملة مع المبلغ ولا تدع / 100 تعيش في الكود دون العملة بجوارها أبدًا.

الحسابات الساخنة وتنازع الأقفال

كل دفتر حقيقي فيه حسابات قليلة تظهر في نصيب كبير من القيود: حساب تعويم التسوية، وحساب إيرادات الرسوم، وتاجر كبير. أقفال الصفوف على هذه الحسابات تجعل كل تحويلاتها تسلسلية. التخفيفات بترتيب الأفضلية: لا تقفل الحساب الساخن أصلًا حين لا يحتاج ثابته إلى ذلك (حساب إيرادات الرسوم لا يمكن أن يصبح سالبًا، فلا حاجة لفحص رصيده)؛ وقسّمه إلى حسابات فرعية ووحّدها في التقارير؛ وجمّع الترحيلات إلى الجانب الساخن في دفعات؛ وبعد ذلك فقط انظر إلى محرك دفاتر متخصص.

النسخ المتماثلة للقراءة والنسخ المنطقي

تخدم النسخ المتماثلة المتدفقة كشوف الحساب ولوحات المعلومات ودور التقارير دون لمس الخادم الرئيسي، مع تأخر عليك مراقبته وإظهاره: «الرصيد كما كان قبل ثوانٍ» مقبول في لوحة معلومات وغير مقبول في فحص حد. أما النسخ المنطقي فينشر التغييرات المودعة لكل جدول إلى خوادم Postgres أخرى أو، عبر CDC، إلى مستودع البيانات، وهو أيضًا الطريق لترقيات الإصدارات الرئيسية بأقل توقف ممكن.

«Postgres تكفي»، بصدق

خادم رئيسي واحد مضبوط جيدًا مع نسخ متماثلة يتعامل مع حجم لن تصل إليه معظم شركات التقنية المالية في المنطقة لسنوات. الإشارات الصادقة على أنك تغادر هذا المجال: معدل الكتابة في الدفتر تهيمن عليه حسابات قليلة لا تستطيع تقسيمها؛ واستعلامات تحليلية عبر سنوات من السطور تتنافس مع التحويلات على الخادم نفسه؛ ومستهلكو الأحداث يحتاجون إلى تدفق التغييرات أسرع مما يوصله مرحّل صندوق الصادر. والإجابات على الترتيب: مخزن متخصص في الدفاتر مثل TigerBeetle أو تقسيم بيانات (sharding) متقن، وClickHouse تتغذى عبر CDC، وKafka مع Debezium. كل واحد منها يضيف سطحًا تشغيليًا؛ فأضفه حين تُقاس الإشارة لا حين تُتوقع.

الفريق والتوظيف في منطقة الشرق الأوسط وشمال أفريقيا

فجوة مديري قواعد البيانات

مديرو قواعد بيانات PostgreSQL المتفرغون نادرون في القاهرة والرياض ودبي، والقلة الموجودة غالبًا داخل البنوك وشركات الاتصالات على أنظمة قديمة. معظم فرق التقنية المالية لن توظف واحدًا قبل جولة التمويل الأولى، وكثير منها لن يوظف أبدًا. خطط لذلك بدلًا من التظاهر بأن الوظيفة ستُشغل يومًا ما.

الخدمات المُدارة بوصفها قرار توظيف

الخدمة المُدارة تشتري لك الوقت، لا المسؤولية.

اختيار Aurora أو Cloud SQL أو Azure أو Supabase أو Neon قرار توظيف بقدر ما هو قرار تقني: أنت تشتري الترقيعات والتبديل عند الفشل والنسخ الاحتياطي ولوحة تحكم مقابل التكلفة وبعض الارتباط بالمزود. لفريق بلا مدير قواعد بيانات هذه الصفقة صحيحة في كل مرة تقريبًا، شريطة أن يبقى هناك من يملك ما لا يملكه المزود: المخطط والفهارس وإعدادات vacuum وخطط الاستعلامات وتمارين الاستعادة.

رفع مهارات مطوري الخلفية في SQL

المسار العملي هو أن تجعل مهندسين أو ثلاثة من مطوري الخلفية بارعين بعمق في Postgres بدلًا من انتظار متخصص. المنهج يكتب نفسه: EXPLAIN (ANALYZE, BUFFERS) على استعلامات حقيقية، وقراءة pg_stat_statements أسبوعيًا، وفهم MVCC وvacuum، وترحيلات لا تأخذ أقفالًا طويلة (إنشاء الفهارس بالتزامن، وقيود NOT VALID تُتحقق لاحقًا)، وجدول مستويات العزل أعلاه حتى يصبح غريزة. ومراجعة شهرية لأبطأ الاستعلامات وتمرين استعادة كل ربع سنة يحوّلان ذلك إلى عادة.

إشارات المقابلات

إشارات جيدة في مرشح خلفية أول: يستطيع شرح لماذا يسمح READ COMMITTED بإنفاق مزدوج وكيف يمنعه؛ ويلجأ إلى قيد قبل أن يلجأ إلى if؛ ويعرف كيف يمنع ترتيب الأقفال التعطل المتبادل؛ ولديه رأي قابل للدفاع عنه في NUMERIC مقابل الأعداد الصحيحة؛ وقد استعاد نسخة احتياطية مرة واحدة على الأقل. إشارات ضعيفة: كل شيء خلف ORM ولم يقرأ خطة استعلام قط، أو «قاعدة مستندات من أجل المرونة» دون القدرة على قول أي اتساق يتنازل عنه.

بناء عضلة المناوبة

من يملك قاعدة البيانات عليه أن يرى لوحات المراقبة كل يوم: تأخر النسخ المتماثل، وعمر أقدم معاملة، والاتصالات، والانتفاخ، ونسبة إصابة الذاكرة المؤقتة، والاستعلامات البطيئة. وكتيبات التشغيل للحوادث الخمسة المرجحة (استنفاد الاتصالات، ومعاملة طويلة تحجب vacuum، والتبديل عند الفشل، وامتلاء القرص، وترحيل سيئ) استثمار أسبوع واحد يسدد نفسه عند أول حادث، ويُستحسن أن تُكتب باللغة التي يقرأها الفريق فعلًا.

إطار اتخاذ القرار

متى تكون Postgres هي النواة الصحيحة

Postgres هي مصدر الحقيقة الصحيح حين يكون المنتج شيئًا على شكل دفتر حسابات (محفظة، إقراض، مدفوعات، اشترِ الآن وادفع لاحقًا، تحويلات، المكتب الخلفي للوساطة)، وحين يكون الفريق صغيرًا أو بلا مدير قواعد بيانات، وحين تحتاج الجهات الرقابية إلى فهم نموذج بياناتك، وحين يكون حجم الكتابة في حدود آلاف المعاملات في الثانية أو أقل. وهذا يصف كل شركة تقنية مالية في المنطقة تقريبًا عند الإطلاق، ومعظمها عند التوسع.

متى تضيف نظامًا متخصصًا

أضف ولا تستبدل، حين تستطيع قياس حد محدد: اختناق في حساب ساخن لا تستطيع الحسابات الفرعية تقسيمه (مخزن متخصص في الدفاتر)، أو تحليلات تتداخل مع المعاملات (مخزن عمودي يتغذى عبر CDC)، أو مستهلكون يحتاجون إلى تدفق تغييرات دائم (Kafka)، أو كتابات نشطة-نشطة متعددة المناطق يفرضها العمل لا تشتهيها البنية (SQL موزعة). أبقِ Postgres مصدرَ الحقيقة وعامل الإضافة كنظام مشتق حتى تكسب الثقة.

مصفوفة القرار

الخيار المعاملات والثوابت الملاءمة للدفاتر العبء التشغيلي بلا مدير قواعد بيانات إقامة البيانات في المنطقة الأنسب لـ
PostgreSQL مُدارة ذاتيًا كاملة: SERIALIZABLE وقيود ومشغّلات وRLS ممتازة عالٍ؛ أنت تملك كل شيء في أي مكان، بما فيه مصر فرق ذات قوة تشغيلية أو قواعد صارمة للاستضافة المحلية
PostgreSQL مُدارة (Aurora وCloud SQL وAzure وSupabase وNeon) كاملة ممتازة منخفض إلى متوسط مناطق خليجية؛ مصر محدودة معظم فرق التقنية المالية عند الإطلاق وبعده بكثير
MySQL / MariaDB جيدة؛ قصة أضعف في التسلسل والقيود، ولا RLS جيدة مع الانضباط منخفض إلى متوسط كما أعلاه فرق لديها خبرة قائمة في MySQL
CockroachDB / SQL موزعة قابلة للتسلسل افتراضيًا، موزعة جيدة؛ زمن استجابة أعلى لكل معاملة متوسط إلى عالٍ؛ مهارات جديدة متعددة المناطق بالتصميم متطلبات نشطة-نشطة متعددة المناطق
TigerBeetle بدائيات قيد مزدوج مبنية خصيصًا استثنائية، للدفتر فقط متوسط؛ مهارات جديدة وتحتاج قاعدة عامة بجوارها استضافة ذاتية في أي مكان معدلات معاملات عالية جدًا على الدفتر نفسه

أفضل قرار لقاعدة بيانات في التقنية المالية هو القرار الذي تستطيع شرحه للمنظّم والمدقق والمهندس الجديد بالرسم نفسه.

الأسئلة الشائعة

هل أخزّن المال كـ NUMERIC أم كوحدات صغرى صحيحة؟

كلاهما صحيح؛ والأعداد العائمة ليست كذلك. NUMERIC(20,4) هو الافتراضي العملي للدفاتر متعددة العملات وأسهل على المدققين في القراءة. والوحدات الصغرى الصحيحة أسرع ولا يمكن تقريبها بصمت لكنها تحتاج إلى أس العملة في كل مكان. اختر واحدًا، ودوّن القاعدة، وافرض المقياس عند حدود الواجهة البرمجية، لأن عمود NUMERIC المحدد الدقة يقرّب ولا يرفض.

هل SERIALIZABLE بطيء جدًا لنظام مدفوعات؟

في حركة الأموال ليس هو عنق الزجاجة عادة؛ بل تنازع الأقفال على الحسابات الساخنة. استخدم SERIALIZABLE للمعاملات التي تحرك المال أو تفحص الحدود، وREAD COMMITTED لكل شيء آخر، وأبقِ كليهما قصيرين. قس معدل إعادة المحاولة بسبب 40001؛ وإذا ارتفع فأصلح نمط الوصول بترتيب الأقفال بدلًا من خفض مستوى العزل.

هل يستطيع خادم Postgres واحد التعامل مع محفظة على مستوى دولة؟

خادم رئيسي واحد مع نسخ متماثلة للقراءة يستطيع تحمّل آلاف معاملات الدفتر في الثانية حين يُصمم المخطط كما سبق، وهذا يغطي الحجم اليومي لمعظم المحافظ في المنطقة مع هامش واسع. أما الذروات (أيام الرواتب، وليالي رمضان، وعرض ترويجي انتشر فجأة) فتُعالج بالاصطفاف عند الحافة والتجميع وتقسيم الحسابات الساخنة، لا بقاعدة بيانات مختلفة. قسّم البيانات أو أضف مخزنًا متخصصًا في الدفاتر حين تقول القياسات ذلك، لا الخوف.

هل أحتاج إلى مدير قواعد بيانات قبل الإطلاق؟

لا، لكنك تحتاج إلى مالك. استخدم خدمة مُدارة، وعيّن مهندسًا أو اثنين ليملكا المخطط والفهارس وvacuum والاستعادة، وامنحهما وقتًا للتعلم. واستعن بمستشار Postgres خارجي لمراجعة ربع سنوية حتى يثق الفريق بنفسه؛ فهذا أرخص من توظيف متفرغ، وأرخص بكثير من انقطاع في الخدمة.

كيف أتعامل مع طلبات «احذف بياناتي» مع دفتر غير قابل للتعديل؟

أبقِ البيانات الشخصية خارج الدفتر. صفوف الدفتر تشير إلى معرّفات حسابات مبهمة؛ أما الأسماء والوثائق وبيانات الاتصال فتعيش في جداولها الخاصة بسياسة احتفاظها الخاصة. المحو يحذف تلك الصفوف أو يتلف مفاتيح تشفيرها بينما يبقى السجل المالي، الذي تلزمك الجهات الرقابية بالاحتفاظ به، سليمًا ولم يعد يحدد هوية أحد.

الخلاصات الرئيسية

  • في التقنية المالية قاعدة البيانات هي المنتج؛ وPostgres هي مصدر الحقيقة الافتراضي الصحيح للمحافظ والإقراض والمدفوعات والشراء الآن والدفع لاحقًا، وعلى كل بديل آخر أن يدافع عن نفسه.
  • ضع الثوابت في المخطط: قيد مزدوج بسطور مجموعها صفر، وجداول دفتر إلحاقية فقط، وقيود CHECK ومشغّلات قابلة للتأجيل.
  • حرّك المال في معاملات SERIALIZABLE مع ترتيب الأقفال ومفاتيح التكرار الآمن؛ أعد المحاولة عند 40001 ولا تخصم مرتين أبدًا.
  • تعدد المستأجرين أكثر أمانًا كجداول مشتركة مع أمان مستوى الصفوف وSET LOCAL لكل معاملة خلف مجمّع اتصالات بوضع المعاملة.
  • أسئلة الامتثال واحدة تحت PCI DSS وSAMA وCBUAE وCBE: أين تعيش البيانات، ومن يقرأها، ومن غيّرها، وهل تستطيع استعادتها، وكم تحتفظ بها.
  • أضف Kafka أو ClickHouse أو TigerBeetle حين تقيس حدًا، لا حين تتوقعه، وأبقِ Postgres مصدر الحقيقة.
  • وظّف على أساس عمق SQL وتملّك قاعدة البيانات بدلًا من انتظار مدير قواعد بيانات؛ فالخدمات المُدارة تشتري الوقت لا المسؤولية.