إم في سي سي-3. إصدارات السلسلة

لذلك، نظرنا في القضايا المتعلقة عازلة، وتراجع عن تنظيم البيانات على مستوى منخفض. وأخيرا وصلنا إلى الجزء الأكثر إثارة للاهتمام - إصدارات السلسلة.

لقب

كما قلنا من قبل، يمكن أن يوجد كل صف في نفس الوقت في عدة إصدارات في قاعدة البيانات. يجب أن يتم تمييز كل إصدار عن الآخر بطريقة أو بأخرى، ولهذا الغرض، تحتوي كل نسخة على علامتين تحددان "زمن" عمل هذا الإصدار (xmin وxmax). في علامات الاقتباس - لأنه ليس الوقت في حد ذاته هو الذي يستخدم، ولكن عداد متزايد خاص. وهذا العداد هو رقم المعاملة.

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

عند إنشاء صف، يتم تعيين xmin على رقم المعاملة التي أصدرت أمر INSERT، ويتم ترك xmax فارغًا.

عند حذف صف، يتم وضع علامة على قيمة xmax للإصدار الحالي برقم المعاملة التي نفذت عملية الحذف.

عندما يتم تعديل صف بواسطة أمر UPDATE، يتم تنفيذ عمليتين فعليًا: DELETE وINSERT. يقوم الإصدار الحالي من الصف بتعيين xmax مساويًا لعدد المعاملة التي أجرت التحديث. ثم يتم إنشاء نسخة جديدة من نفس السلسلة؛ تتطابق قيمة xmin مع قيمة xmax للإصدار السابق.

يتم تضمين الحقلين xmin وxmax في رأس إصدار الصف. بالإضافة إلى هذه الحقول، يحتوي الرأس على حقول أخرى، على سبيل المثال:

  • قناع المعلومات عبارة عن سلسلة من البتات التي تحدد خصائص هذا الإصدار. هناك الكثير منهم. سننظر تدريجيا في أهمها.
  • ctid هو رابط للإصدار التالي والأحدث من نفس السطر. بالنسبة للإصدار الأحدث والأحدث من السلسلة، يشير ctid إلى هذا الإصدار نفسه. يحتوي الرقم على النموذج (x,y)، حيث x هو رقم الصفحة، وy هو رقم الفهرس في المصفوفة.
  • صورة نقطية فارغة - تحدد تلك الأعمدة الخاصة بإصدار معين والتي تحتوي على قيمة فارغة (NULL). NULL ليست إحدى قيم نوع البيانات العادية، لذلك يجب تخزين السمة بشكل منفصل.

ونتيجة لذلك، يكون الرأس كبيرًا جدًا - على الأقل 23 بايت لكل إصدار من السطر، وعادةً ما يكون أكبر بسبب الصورة النقطية NULL. إذا كان الجدول "ضيقًا" (أي يحتوي على عدد قليل من الأعمدة)، فقد يستغرق الحمل أكثر من المعلومات المفيدة.

أدخل

دعونا نلقي نظرة فاحصة على كيفية تنفيذ عمليات السلسلة ذات المستوى المنخفض، بدءًا من الإدراج.

لإجراء التجارب، لنقم بإنشاء جدول جديد يحتوي على عمودين وفهرس على أحدهما:

=> CREATE TABLE t(
  id serial,
  s text
);
=> CREATE INDEX ON t(s);

لنقم بإدراج صف واحد بعد بدء المعاملة.

=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');

هذا هو رقم معاملتنا الحالية:

=> SELECT txid_current();
 txid_current 
--------------
         3664
(1 row)

دعونا نلقي نظرة على محتويات الصفحة. تتيح لك وظيفة heap_page_items الخاصة بملحق pageinspect الحصول على معلومات حول المؤشرات وإصدارات الصفوف:

=> SELECT * FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-------------------
lp          | 1
lp_off      | 8160
lp_flags    | 1
lp_len      | 32
t_xmin      | 3664
t_xmax      | 0
t_field3    | 0
t_ctid      | (0,1)
t_infomask2 | 2
t_infomask  | 2050
t_hoff      | 24
t_bits      | 
t_oid       | 
t_data      | x0100000009464f4f

لاحظ أن كلمة الكومة في PostgreSQL تشير إلى الجداول. وهذا استخدام غريب آخر للمصطلح - الكومة معروفة بنية البياناتالذي ليس له أي شيء مشترك مع الجدول. تُستخدم الكلمة هنا بمعنى "كل شيء مُجمَّع معًا"، على عكس الفهارس المرتبة.

تعرض الوظيفة البيانات "كما هي"، بتنسيق يصعب فهمه. لمعرفة ذلك، سنترك جزءًا فقط من المعلومات ونقوم بفك شفرتها:

=> SELECT '(0,'||lp||')' AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'unused'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'redirect to '||lp_off
         WHEN 3 THEN 'dead'
       END AS state,
       t_xmin as xmin,
       t_xmax as xmax,
       (t_infomask & 256) > 0  AS xmin_commited,
       (t_infomask & 512) > 0  AS xmin_aborted,
       (t_infomask & 1024) > 0 AS xmax_commited,
       (t_infomask & 2048) > 0 AS xmax_aborted,
       t_ctid
FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-+-------
ctid          | (0,1)
state         | normal
xmin          | 3664
xmax          | 0
xmin_commited | f
xmin_aborted  | f
xmax_commited | f
xmax_aborted  | t
t_ctid        | (0,1)

وإليكم ما فعلناه:

  • تمت إضافة صفر إلى رقم الفهرس لجعله يبدو مثل t_ctid: (رقم الصفحة، رقم الفهرس).
  • تم فك تشفير حالة مؤشر lp_flags. هنا هو "عادي" - وهذا يعني أن المؤشر يشير فعليًا إلى إصدار السلسلة. وسننظر إلى معاني أخرى لاحقًا.
  • ومن بين جميع بتات المعلومات، تم التعرف على زوجين فقط حتى الآن. تشير البتات xmin_committed وxmin_aborted إلى ما إذا كان رقم المعاملة xmin ملتزمًا (تم إحباطه). تشير البتتان المتشابهتان إلى رقم المعاملة xmax.

ماذا نرى؟ عندما تقوم بإدراج صف، سيظهر رقم الفهرس 1 في صفحة الجدول، للإشارة إلى الإصدار الأول والوحيد من الصف.

في إصدار السلسلة، يتم ملء حقل xmin برقم المعاملة الحالية. لا تزال المعاملة نشطة، لذا لم يتم تعيين البتات xmin_commited وxmin_aborted.

يشير حقل ctid لإصدار الصف إلى نفس الصف. وهذا يعني أن الإصدار الأحدث غير موجود.

يتم تعبئة الحقل xmax برقم وهمي 0 لأن هذا الإصدار من الصف لم يتم حذفه وهو حاليًا. لن تهتم المعاملات بهذا الرقم لأنه تم تعيين البت xmax_aborted.

لنأخذ خطوة أخرى نحو تحسين إمكانية القراءة عن طريق إضافة بتات المعلومات إلى أرقام المعاملات. ولنقم بإنشاء دالة، لأننا سنحتاج الطلب أكثر من مرة:

=> CREATE FUNCTION heap_page(relname text, pageno integer)
RETURNS TABLE(ctid tid, state text, xmin text, xmax text, t_ctid tid)
AS $$
SELECT (pageno,lp)::text::tid AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'unused'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'redirect to '||lp_off
         WHEN 3 THEN 'dead'
       END AS state,
       t_xmin || CASE
         WHEN (t_infomask & 256) > 0 THEN ' (c)'
         WHEN (t_infomask & 512) > 0 THEN ' (a)'
         ELSE ''
       END AS xmin,
       t_xmax || CASE
         WHEN (t_infomask & 1024) > 0 THEN ' (c)'
         WHEN (t_infomask & 2048) > 0 THEN ' (a)'
         ELSE ''
       END AS xmax,
       t_ctid
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ LANGUAGE SQL;

في هذا النموذج، يكون أكثر وضوحًا ما يحدث في رأس إصدار الصف:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)

يمكن الحصول على معلومات مماثلة، ولكنها أقل تفصيلاً، من الجدول نفسه، باستخدام الأعمدة الزائفة xmin وxmax:

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3664 |    0 |  1 | FOO
(1 row)

تثبيت

إذا اكتملت المعاملة بنجاح، عليك أن تتذكر حالتها - لاحظ أنها ملتزمة. للقيام بذلك، يتم استخدام بنية تسمى XACT (وقبل الإصدار 10 كانت تسمى CLOG (سجل الالتزام) ولا يزال من الممكن العثور على هذا الاسم في أماكن مختلفة).

XACT ليس جدول كتالوج النظام؛ هذه هي الملفات الموجودة في الدليل PGDATA/pg_xact. لديهم بتان لكل معاملة: تم الالتزام بها وتم إحباطها - تمامًا كما هو الحال في رأس إصدار الصف. تم تقسيم هذه المعلومات إلى عدة ملفات فقط من أجل التيسير، وسنعود إلى هذه المشكلة عندما نفكر في التجميد. ويتم العمل مع هذه الملفات صفحة تلو الأخرى، كما هو الحال مع جميع الملفات الأخرى.

لذلك، عند تنفيذ معاملة في XACT، يتم تعيين البت الملتزم لهذه المعاملة. وهذا هو كل ما يحدث أثناء الالتزام (على الرغم من أننا لا نتحدث عن سجل التسجيل المسبق بعد).

عندما تصل معاملة أخرى إلى صفحة الجدول التي نظرنا إليها للتو، سيتعين عليها الإجابة على عدة أسئلة.

  1. هل اكتملت معاملة xmin؟ إذا لم يكن الأمر كذلك، فيجب ألا تكون النسخة التي تم إنشاؤها من السلسلة مرئية.
    يتم إجراء هذا الفحص من خلال النظر إلى بنية أخرى موجودة في الذاكرة المشتركة للمثيل وتسمى ProcArray. يحتوي على قائمة بجميع العمليات النشطة، ولكل منها رقم معاملتها الحالية (النشيطة).
  2. وإذا اكتمل فكيف – بالالتزام أو الإلغاء؟ إذا تم الإلغاء، فيجب ألا يكون إصدار الصف مرئيًا أيضًا.
    وهذا هو بالضبط الغرض من XACT. ولكن، على الرغم من أن الصفحات الأخيرة من XACT يتم تخزينها في مخازن مؤقتة في ذاكرة الوصول العشوائي (RAM)، إلا أن التحقق من XACT في كل مرة لا يزال مكلفًا. لذلك، بمجرد تحديد حالة المعاملة، تتم كتابتها إلى البتات xmin_committed وxmin_aborted من إصدار السلسلة. إذا تم تعيين إحدى هذه البتات، فستعتبر حالة المعاملة xmin معروفة ولن تحتاج المعاملة التالية إلى الوصول إلى XACT.

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

الجانب السلبي للتوفير هو أنه بعد التغييرات، يمكن لأي معاملة (حتى تلك التي تقوم بقراءة بسيطة - SELECT) أن تبدأ في تغيير صفحات البيانات في ذاكرة التخزين المؤقت.

لذلك، دعونا إصلاح التغيير.

=> COMMIT;

لم يتغير شيء في الصفحة (لكننا نعلم أن حالة المعاملة مسجلة بالفعل في XACT):

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)

الآن سيتعين على المعاملة التي تصل إلى الصفحة أولاً تحديد حالة معاملة xmin وكتابتها في بتات المعلومات:

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 0 (a) | (0,1)
(1 row)

إزالة

عند حذف صف، تتم كتابة رقم معاملة الحذف الحالية في حقل xmax للإصدار الحالي، ويتم مسح البت xmax_aborted.

لاحظ أن القيمة المحددة لـ xmax المقابلة للمعاملة النشطة تعمل كقفل صف. إذا أرادت معاملة أخرى تحديث هذا الصف أو حذفه، فسوف تضطر إلى الانتظار حتى تكتمل المعاملة xmax. سنتحدث أكثر عن الحظر لاحقًا. في الوقت الحالي، نلاحظ فقط أن عدد أقفال الصفوف غير محدود. لا تشغل مساحة في ذاكرة الوصول العشوائي (RAM) ولا يتأثر أداء النظام بعددها. صحيح أن المعاملات "الطويلة" لها عيوب أخرى، ولكن المزيد عن ذلك لاحقًا.

دعونا نحذف السطر.

=> BEGIN;
=> DELETE FROM t;
=> SELECT txid_current();
 txid_current 
--------------
         3665
(1 row)

نرى أن رقم المعاملة مكتوب في الحقل xmax، لكن لم يتم تعيين بتات المعلومات:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)

إلغاء

يعمل إحباط التغييرات بشكل مشابه للالتزام، فقط في XACT يتم تعيين البت المجهض للمعاملة. التراجع يكون بنفس سرعة الالتزام. على الرغم من أن الأمر يسمى ROLLBACK، إلا أنه لا يتم إرجاع التغييرات: كل ما تمكنت المعاملة من تغييره في صفحات البيانات يبقى دون تغيير.

=> ROLLBACK;
=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)

عند الوصول إلى الصفحة، سيتم فحص الحالة وسيتم تعيين بت التلميح xmax_aborted على إصدار الصف. يبقى رقم xmax نفسه على الصفحة، ولكن لن ينظر إليه أحد.

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   |   xmax   | t_ctid 
-------+--------+----------+----------+--------
 (0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
(1 row)

تحديث

يعمل التحديث كما لو أنه قام أولاً بحذف الإصدار الحالي من الصف ثم قام بإدراج إصدار جديد.

=> BEGIN;
=> UPDATE t SET s = 'BAR';
=> SELECT txid_current();
 txid_current 
--------------
         3666
(1 row)

ينتج الاستعلام سطرًا واحدًا (الإصدار الجديد):

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | BAR
(1 row)

لكن على الصفحة نرى كلا الإصدارين:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 3666  | (0,2)
 (0,2) | normal | 3666     | 0 (a) | (0,2)
(2 rows)

يتم تمييز النسخة المحذوفة برقم المعاملة الحالية في حقل xmax. علاوة على ذلك، يتم كتابة هذه القيمة فوق القيمة القديمة، حيث تم إلغاء المعاملة السابقة. ويتم مسح البت xmax_aborted لأن حالة المعاملة الحالية غير معروفة بعد.

يشير الإصدار الأول من السطر الآن إلى الحقل الثاني (حقل t_ctid) باعتباره الإصدار الأحدث.

يظهر فهرس ثانٍ في صفحة الفهرس ويشير الصف الثاني إلى الإصدار الثاني في صفحة الجدول.

كما هو الحال مع الحذف، فإن قيمة xmax في الإصدار الأول من الصف هي إشارة إلى أن الصف مقفل.

حسنًا، لنكمل الصفقة.

=> COMMIT;

مؤشرات

لقد تحدثنا حتى الآن فقط عن صفحات الجدول. ماذا يحدث داخل الفهارس؟

تختلف المعلومات الموجودة في صفحات الفهرس بشكل كبير اعتمادًا على نوع الفهرس المحدد. وحتى نوع واحد من الفهرس يحتوي على أنواع مختلفة من الصفحات. على سبيل المثال، تحتوي شجرة B على صفحة بيانات وصفية وصفحات "عادية".

ومع ذلك، تحتوي الصفحة عادةً على مجموعة من المؤشرات إلى الصفوف والصفوف نفسها (تمامًا مثل صفحة الجدول). بالإضافة إلى ذلك، يوجد في نهاية الصفحة مساحة للبيانات الخاصة.

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

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

وفي الوقت نفسه، نجد في صفحة الفهرس مؤشرات لكلا الإصدارين، الإصدار الحالي والقديم:

=> SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1);
 itemoffset | ctid  
------------+-------
          1 | (0,2)
          2 | (0,1)
(2 rows)

المعاملات الافتراضية

من الناحية العملية، يستخدم PostgreSQL تحسينات تسمح له بـ "حفظ" أرقام المعاملات.

إذا كانت المعاملة تقرأ البيانات فقط، فلن يكون لها أي تأثير على رؤية إصدارات الصفوف. لذلك، تقوم عملية الخدمة أولاً بإصدار xi ظاهري للمعاملة. يتكون الرقم من معرف العملية ورقم التسلسل.

إصدار هذا الرقم لا يتطلب التزامن بين كافة العمليات وبالتالي فهو سريع جداً. وسوف نتعرف على سبب آخر لاستخدام الأرقام الافتراضية عندما نتحدث عن التجميد.

لا يتم أخذ الأرقام الافتراضية بعين الاعتبار بأي شكل من الأشكال في لقطات البيانات.

في فترات زمنية مختلفة، قد تكون هناك معاملات افتراضية في النظام بأرقام تم استخدامها بالفعل، وهذا أمر طبيعي. لكن مثل هذا الرقم لا يمكن كتابته في صفحات البيانات، لأنه في المرة التالية التي يتم فيها الوصول إلى الصفحة قد يفقد كل معناه.

=> BEGIN;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                         
(1 row)

إذا بدأت المعاملة في تغيير البيانات، فسيتم منحها رقم معاملة حقيقي وفريد.

=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                     3667
(1 row)

=> COMMIT;

المعاملات المتداخلة

حفظ النقاط

محددة في SQL حفظ النقاط (نقطة الحفظ)، والتي تتيح لك إلغاء جزء من المعاملة دون مقاطعتها بالكامل. لكن هذا لا يتناسب مع الرسم البياني أعلاه، نظرًا لأن المعاملة لها نفس الحالة لجميع تغييراتها، ولا يتم إرجاع أي بيانات فعليًا.

لتنفيذ هذه الوظيفة، يتم تقسيم المعاملة التي تحتوي على نقطة حفظ إلى عدة معاملات منفصلة المعاملات المتداخلة (المعاملة من الباطن)، والتي يمكن إدارة حالتها بشكل منفصل.

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

يتم تخزين المعلومات حول تداخل المعاملات في الملفات الموجودة في الدليل PGDATA/pg_subtrans. يتم الوصول إلى الملفات من خلال المخازن المؤقتة في الذاكرة المشتركة للمثيل، ويتم تنظيمها بنفس طريقة المخازن المؤقتة XACT.

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

لنمسح الجدول ونبدأ المعاملة وندخل الصف:

=> TRUNCATE TABLE t;
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 row)

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
(1 row)

الآن دعونا نضع نقطة حفظ ونقوم بإدراج سطر آخر.

=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 row)

لاحظ أن الدالة txid_current() ترجع رقم المعاملة الرئيسي، وليس رقم المعاملة المتداخلة.

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3670 |    0 |  3 | XYZ
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
 (0,2) | normal | 3670 | 0 (a) | (0,2)
(2 rows)

دعنا نعود إلى نقطة الحفظ وندخل السطر الثالث.

=> ROLLBACK TO sp;
=> INSERT INTO t(s) VALUES ('BAR');
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669     | 0 (a) | (0,1)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671     | 0 (a) | (0,3)
(3 rows)

في الصفحة نواصل رؤية الصف الذي تمت إضافته بواسطة المعاملة المتداخلة الملغاة.

نحن إصلاح التغييرات.

=> COMMIT;
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669 (c) | 0 (a) | (0,1)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
(3 rows)

يمكنك الآن أن ترى بوضوح أن كل معاملة متداخلة لها حالتها الخاصة.

لاحظ أنه لا يمكن استخدام المعاملات المتداخلة بشكل صريح في SQL، أي أنه لا يمكنك بدء معاملة جديدة دون إكمال المعاملة الحالية. يتم تنشيط هذه الآلية ضمنيًا عند استخدام نقاط الحفظ، وكذلك عند التعامل مع استثناءات PL/pgSQL وفي عدد من الحالات الأخرى الأكثر غرابة.

=> BEGIN;
BEGIN
=> BEGIN;
WARNING:  there is already a transaction in progress
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
WARNING:  there is no transaction in progress
COMMIT

الأخطاء وذرية العمليات

ماذا يحدث إذا حدث خطأ أثناء إجراء العملية؟ على سبيل المثال، مثل هذا:

=> BEGIN;
=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rows)

=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero

حدث خطأ. الآن تعتبر المعاملة ملغاة ولا يسمح بأي عمليات فيها:

=> SELECT * FROM t;
ERROR:  current transaction is aborted, commands ignored until end of transaction block

وحتى إذا حاولت تنفيذ التغييرات، فسوف يقوم PostgreSQL بالإبلاغ عن الإجهاض:

=> COMMIT;
ROLLBACK

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

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669 (c) | 3672  | (0,4)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
 (0,4) | normal | 3672     | 0 (a) | (0,4)
(4 rows)

يجب أن يقال أن psql لديه وضع لا يزال يسمح للمعاملة بالاستمرار بعد الفشل كما لو تم التراجع عن تصرفات المشغل الخاطئ.

=> set ON_ERROR_ROLLBACK on
=> BEGIN;
=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rows)

=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero

=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rows)

=> COMMIT;

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

قراءة المزيد.

المصدر: www.habr.com

إضافة تعليق