MVCC في PostgreSQL-3. إصدارات الصف

حسنًا ، لقد ناقشنا بالفعل العزلة وجعلنا استطرادا بخصوص بنية البيانات منخفضة المستوى . وقد وصلنا أخيرًا إلى أكثر الأشياء الرائعة ، أي إصدارات الصف (tuples).

رأس Tuple


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

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

عند إنشاء صف ، يتم تعيين قيمة xmin مساوية لمعرف المعاملة التي نفذت أمر INSERT ، بينما لم يتم ملء xmax .

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

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

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

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

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

إدراج


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

للتجربة ، سننشئ جدولًا جديدًا به عمودين وفهرس على أحدهما:

 => 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 وظيفة 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_aborted و xmin_aborted ما إذا كانت المعاملة ذات المعرف xmin ملتزمة (تم التراجع). يتعلق زوج من وحدات البت المتشابهة بالصفقة باستخدام معرّف xmax .

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

يتم xmin حقل xmin في المجموعة بالمعرف الخاص بالمعاملة الحالية. لأن المعاملة لا تزال نشطة ، يتم إلغاء xmin_committed كل من xmin_committed و xmin_aborted .

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

يتم xmax حقل 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. يتم تخصيص جزئين في هذه الملفات لكل معاملة - "ملتزم" و "تم إحباط" - بنفس الطريقة تمامًا كما هو الحال في رأس المجموعة. تنتشر هذه المعلومات عبر عدة ملفات للراحة فقط ؛ سوف نعود إلى هذا عندما نناقش التجميد. يعمل PostgreSQL مع هذه الملفات صفحة تلو الأخرى ، كما هو الحال مع الآخرين.

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

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

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

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

الجانب الآخر من توفير التكاليف هو أنه بعد التحديثات ، يمكن لأي معاملة (حتى تلك التي تقوم بإجراء 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_aborted .

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

دعنا نحذف صف

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

نرى أن معرف المعاملة مكتوب في حقل xmax ، لكن لم يتم 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_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 لأن حالة المعاملة الحالية غير معروفة حتى الآن.

يشير الإصدار الأول من الصف الآن إلى الإصدار الثاني باعتباره إصدارًا أحدث.

تحتوي صفحة الفهرس الآن على المؤشر الثاني والصف الثاني ، والذي يشير إلى الإصدار الثاني في صفحة الجدول.

بنفس الطريقة بالنسبة للحذف ، تشير قيمة xmax في الإصدار الأول إلى أن الصف مغلق.

أخيرًا ، نلتزم بالمعاملة.

 => COMMIT; 

الفهارس


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

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

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

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

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

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

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

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


في الممارسة العملية ، تستفيد PostgreSQL من التحسين الذي يسمح "بإنفاق" معرفات المعاملات.

إذا كانت المعاملة تقوم بقراءة البيانات فقط ، فلن يؤثر ذلك على ظهور العلامة على الإطلاق. لذلك ، تقوم العملية الخلفية أولاً بتعيين معرّف ظاهري (xid ظاهري) للمعاملة. يتكون هذا المعرف من معرف العملية ورقم متسلسل.

لا يتطلب تعيين هذا المعرف الظاهري التزامن بين جميع العمليات وبالتالي يتم تنفيذه بسرعة كبيرة. سنتعلم سببًا آخر لاستخدام معرفات افتراضية عندما نناقش التجميد.

لا تأخذ لقطات البيانات في الاعتبار المعرف الظاهري على الإطلاق.

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

 => 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; 

Subtransactions


Savepoints


في SQL ، يتم تعريف savepoints ، والتي تسمح باستعادة بعض عمليات المعاملة دون إجهاضها الكامل. لكن هذا غير متوافق مع النموذج أعلاه لأن حالة المعاملة هي واحدة لجميع التغييرات ولا يتم استعادة البيانات فعليًا.

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

تملك الاختبارات الفرعية معرفات خاصة بها (أكبر من معرف المعاملة الرئيسية). تتم كتابة حالات المعاملات الفرعية إلى 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) 

دعونا التراجع إلى savepoint وإدراج الصف الثالث.

 => ROLLBACK TO sp; => INSERT INTO t 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 

الأخطاء و atomicity العملية


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

 => 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 بالفعل نقطة حفظ ضمنية قبل كل أمر وتبدأ في التراجع إليها في حالة الفشل. لا يتم استخدام هذا الوضع افتراضيًا لأن إنشاء نقاط حفظ (حتى بدون التراجع عنها) يستلزم حملًا كبيرًا.

اقرأ على .

Source: https://habr.com/ru/post/ar477648/


All Articles