MVCC في PostgreSQL-5. فراغ في الصفحة والتحديثات الساخنة

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

الآن سننتقل إلى مشكلتين متصلتين بشكل وثيق: الفراغ في الصفحة والتحديثات الساخنة . يمكن إحالة كلتا التقنيتين إلى التحسينات ؛ أنها مهمة ، ولكنها غير مشمولة تقريبًا في الوثائق.

فراغ في الصفحة أثناء التحديثات المنتظمة


عند الوصول إلى صفحة إما لتحديث أو قراءة ، إذا كان PostgreSQL يدرك أن الصفحة نفدت مساحتها ، فيمكنها عمل فراغ سريع داخل الصفحة. يحدث هذا في أي من الحالات:

  1. لم يعثر التحديث السابق في هذه الصفحة على مساحة كافية لتخصيص إصدار صف جديد في نفس الصفحة. يتم تذكر مثل هذا الموقف في رأس الصفحة ، وفي المرة القادمة يتم تفريغ الصفحة.
  2. الصفحة أكثر من fillfactor المئة ممتلئة. في هذه الحالة ، يتم إجراء فراغ على الفور دون تأجيل حتى التالي التالي.

fillfactor هي معلمة تخزين يمكن تعريفها لجدول (وللفهرس). يقوم PostgresSQL بإدراج صف جديد في صفحة فقط إذا كانت الصفحة أقل من fillfactor المئة ممتلئة. المساحة المتبقية محجوزة للأنابيب الجديدة التي تم إنشاؤها نتيجة للتحديثات. القيمة الافتراضية للجداول هي 100 ، أي أنه لا توجد مساحة مخصصة (والقيمة الافتراضية للفهارس هي 90).

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

لنفس الأسباب ، لا يتم تحديث خريطة المساحة الحرة ؛ هذا أيضًا يحتفظ بمساحة إضافية للتحديثات بدلاً من إدراجها. لا يتم تحديث خريطة الرؤية أيضًا.

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

دعنا نفكر كمثال لكيفية عملها. لنقم بإنشاء جدول وفهارس في كلا العمودين.

 => CREATE TABLE hot(id integer, s char(2000)) WITH (fillfactor = 75); => CREATE INDEX hot_id ON hot(id); => CREATE INDEX hot_s ON hot(s); 

إذا كان العمود s يخزن الأحرف اللاتينية فقط ، فسيشغل كل إصدار صف 2004 بايت بالإضافة إلى 24 بايت من الرأس. حددنا معلمة تخزين fillfactor على 75 ٪ ، والتي تحتفظ بمساحة كافية فقط لثلاثة صفوف.

للنظر بسهولة في محتويات صفحة الجدول ، دعونا نعيد إنشاء وظيفة مألوفة بالفعل عن طريق إضافة حقلين آخرين إلى الإخراج:

 => CREATE FUNCTION heap_page(relname text, pageno integer) RETURNS TABLE(ctid tid, state text, xmin text, xmax text, hhu text, hot 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, CASE WHEN (t_infomask2 & 16384) > 0 THEN 't' END AS hhu, CASE WHEN (t_infomask2 & 32768) > 0 THEN 't' END AS hot, t_ctid FROM heap_page_items(get_raw_page(relname,pageno)) ORDER BY lp; $$ LANGUAGE SQL; 

لنقم أيضًا بإنشاء وظيفة للنظر في صفحة الفهرس:

 => CREATE FUNCTION index_page(relname text, pageno integer) RETURNS TABLE(itemoffset smallint, ctid tid) AS $$ SELECT itemoffset, ctid FROM bt_page_items(relname,pageno); $$ LANGUAGE SQL; 

دعونا نتحقق من كيفية عمل الفراغ في الصفحة. للقيام بذلك ، نقوم بإدراج صف واحد وتغييره عدة مرات:

 => INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B'; => UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D'; 

هناك أربعة tuples في الصفحة الآن:

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3979 (c) | 3980 (c) | | | (0,2) (0,2) | normal | 3980 (c) | 3981 (c) | | | (0,3) (0,3) | normal | 3981 (c) | 3982 | | | (0,4) (0,4) | normal | 3982 | 0 (a) | | | (0,4) (4 rows) 

كما هو متوقع ، لقد تجاوزنا عتبة fillfactor . هذا واضح من الفرق بين قيم pagesize upper : يتجاوز الحد pagesize به 75٪ من حجم الصفحة ، مما يجعل 6144 بايت.

 => SELECT lower, upper, pagesize FROM page_header(get_raw_page('hot',0)); 
  lower | upper | pagesize -------+-------+---------- 40 | 64 | 8192 (1 row) 

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

 => UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | dead | | | | | (0,2) | dead | | | | | (0,3) | dead | | | | | (0,4) | normal | 3982 (c) | 3983 | | | (0,5) (0,5) | normal | 3983 | 0 (a) | | | (0,5) (5 rows) 

جميع الفراغات الميتة (0،1) و (0،2) و (0،3) يتم تفريغها بعيدًا ؛ بعد ذلك ، يتم إضافة فئة جديدة (0.5) في المساحة المحررة.

يتم نقل العناصر التي نجت من الفراغ فعليًا نحو عناوين عالية من الصفحة بحيث يتم تمثيل كل المساحة الحرة بمساحة واحدة متصلة. يتم تغيير قيم المؤشرات وفقًا لذلك. بفضل هذا ، لا تنشأ أي مشاكل مع تجزئة المساحة الحرة في الصفحة.

لا يمكن إصدار مؤشرات إلى فراغات مفرغة حيث يتم الرجوع إليها من صفحة الفهرس. لنلقِ نظرة على الصفحة الأولى من فهرس hot_s (نظرًا لأن الصفحة صفر مشغولة hot_s ):

 => SELECT * FROM index_page('hot_s',1); 
  itemoffset | ctid ------------+------- 1 | (0,1) 2 | (0,2) 3 | (0,3) 4 | (0,4) 5 | (0,5) (5 rows) 

نرى أيضًا نفس الصورة في الفهرس الآخر:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (0,5) 2 | (0,4) 3 | (0,3) 4 | (0,2) 5 | (0,1) (5 rows) 

قد تلاحظ أن مؤشرات إلى صفوف الجدول تتبع هنا بترتيب عكسي ، لكن هذا لا يحدث فرقًا لأن جميع المجموعات لها نفس القيمة: id = 1. لكن في المؤشر السابق ، يتم ترتيب المؤشرات حسب قيم s ، وهذا ضروري.

باستخدام الوصول إلى الفهرس ، يمكن لـ PostgreSQL الحصول على (0،1) أو (0،2) أو (0،3) كمعرفات tuple. ستحاول بعد ذلك الحصول على إصدار الصف المناسب من صفحة الجدول ، ولكن بسبب الحالة "الميت" للمؤشر ، ستكتشف PostgreSQL أن مثل هذا الإصدار لم يعد موجودًا وسوف يتجاهله. (في الواقع ، بعد أن اكتشفنا مرة واحدة أن إصدار صف الجدول غير متاح ، فإن PostgreSQL ستغير حالة المؤشر في صفحة الفهرس حتى لا تتمكن من الوصول إلى صفحة الجدول بعد الآن.)

من الضروري أن يعمل الفراغ الموجود في الصفحة ضمن صفحة جدول واحدة فقط ولا يعمل فراغ فهرس الصفحات.

تحديثات ساخنة


لماذا ليس من الجيد تخزين الإشارات إلى جميع إصدارات الصف في الفهرس؟

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

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

علاوة على ذلك ، تحتوي B-tree في PostgreSQL على تفاصيل التنفيذ. إذا كانت صفحة الفهرس لا تحتوي على مساحة كافية لإدراج صف جديد ، يتم تقسيم الصفحة إلى قسمين ، ويتم توزيع جميع البيانات بينهما. وهذا ما يسمى تقسيم الصفحة. ومع ذلك ، عند حذف الصفوف ، لا يتم دمج صفحتين في فهرس واحد. لهذا السبب ، قد يفشل حجم الفهرس في التقليل حتى إذا تم حذف جزء كبير من البيانات.

بطبيعة الحال ، يتم إنشاء المزيد من الفهارس على جدول ، وتواجه المزيد من التعقيدات.

ومع ذلك ، إذا تم تغيير قيمة في عمود غير مفهرسة على الإطلاق ، فليس من المنطقي إنشاء صف شجرة B إضافي يحتوي على نفس قيمة المفتاح. هذا هو بالضبط كيف يعمل التحسين المسمى HOT update ( تحديث Heap-Only Tuple).

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

  • يتم تسمية الصفوف المحدثة الموجودة في السلسلة بجزء تحديث كومة الذاكرة المؤقتة.
  • يتم تصنيف الصفوف التي لم يتم الرجوع إليها من الفهرس بت بت Heap Only Tuple.
  • كالعادة ، يتم ربط إصدارات الصف من خلال الحقل ctid .

إذا كان PostgreSQL يصل إلى صفحة جدول أثناء مسح الفهرس ، ويجد tuple المسمى "Heap Hot محدث" ، فإنه يفهم أنه يجب ألا يتوقف ، ولكن يجب أن يتبع سلسلة HOT ، مع مراعاة كل tuple فيها. بالتأكيد ، بالنسبة لجميع التلاميذ حصلوا على هذا النحو ، يتم التحقق من الرؤية قبل إعادتهم إلى العميل.

لمراقبة كيفية عمل تحديث HOT ، دعنا نحذف فهرسًا واحدًا ونمسح الجدول.

 => DROP INDEX hot_s; => TRUNCATE TABLE hot; 

الآن نعيد إدراج وتحديث صف واحد.

 => INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B'; 

وهذا ما نراه في صفحة الجدول:

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

هناك سلسلة من التغييرات في الصفحة:

  • تشير علامة تحديث تحديث الكومة إلى أنه يجب اتباع سلسلة ctid .
  • تشير علامة Heap Only Tuple إلى أن هذه المجموعة غير مرجعية من الفهارس.

سوف تنمو السلسلة (داخل الصفحة) مع مزيد من التغييرات:

 => UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3986 (c) | 3987 (c) | t | | (0,2) (0,2) | normal | 3987 (c) | 3988 (c) | t | t | (0,3) (0,3) | normal | 3988 (c) | 3989 | t | t | (0,4) (0,4) | normal | 3989 | 0 (a) | | t | (0,4) (4 rows) 

ولكن هناك إشارة واحدة فقط إلى رأس السلسلة في الفهرس:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (0,1) (1 row) 

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

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

فراغ في الصفحة أثناء التحديثات الساخنة


يعتبر التنظيف بالمكنسة الكهربائية خلال تحديثات HOT حالة خاصة ، ولكنها مهمة للفراغ الموجود في الصفحة.

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

من أجل عدم لمس مؤشر الرأس ، يتم استخدام العنونة غير المباشرة: يكتسب المؤشر المشار إليه بواسطة الفهرس - (0،1) في هذه الحالة - حالة "إعادة التوجيه" ، التي تعيد التوجيه إلى المجموعة المناسبة.

 => UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | normal | 3989 (c) | 3990 | t | t | (0,2) (4 rows) 

لاحظ أن:

  • تم تفريغ المواد الأنبوبية (0،1) و (0،2) و (0،3).
  • يبقى مؤشر الرأس (0،1) ، لكنه حصل على حالة "إعادة التوجيه".
  • تمت الكتابة فوق الإصدار الجديد للصف (0،2) نظرًا لعدم وجود إشارات إلى هذه المجموعة بالتأكيد ، وتم إصدار المؤشر (الحالة "غير المستخدمة").

لنقم بتحديث عدة مرات أكثر:

 => UPDATE hot SET s = 'F'; => UPDATE hot SET s = 'G'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 (c) | 3991 (c) | t | t | (0,3) (0,3) | normal | 3991 (c) | 3992 | t | t | (0,5) (0,4) | normal | 3989 (c) | 3990 (c) | t | t | (0,2) (0,5) | normal | 3992 | 0 (a) | | t | (0,5) (5 rows) 

التحديث التالي يؤدي إلى كنس في الصفحة مرة أخرى:

 => UPDATE hot SET s = 'H'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 5 | | | | | (0,2) | normal | 3993 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | unused | | | | | (0,5) | normal | 3992 (c) | 3993 | t | t | (0,2) (5 rows) 

مرةً أخرى ، يتم تفريغ بعض الصفوف ، ويتم تحريك المؤشر إلى رأس السلسلة وفقًا لذلك.

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

كسر سلسلة الساخنة


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

لإعادة إنتاج هذا الموقف ، دعنا نبدأ معاملة متزامنة وإنشاء لقطة البيانات فيها.

 | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT count(*) FROM hot; 
 | count | ------- | 1 | (1 row) 

لن تسمح اللقطة بإزالة الفراغات الموجودة في الصفحة. الآن لنقم بتحديث في الجلسة الأولى:

 => UPDATE hot SET s = 'I'; => UPDATE hot SET s = 'J'; => UPDATE hot SET s = 'K'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 | t | t | (0,5) (0,5) | normal | 3996 | 0 (a) | | t | (0,5) (5 rows) 

في التحديث التالي ، لن تحتوي الصفحة على مساحة كافية ، لكن الفراغ الموجود في الصفحة لن يكون قادرًا على تفريغ أي شيء بعيدًا:

 => UPDATE hot SET s = 'L'; 

 | => COMMIT; -- snapshot no longer needed 

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 (c) | t | t | (0,5) (0,5) | normal | 3996 (c) | 3997 | | t | (1,1) (5 rows) 

في المجموعة (0،5) ، هناك إشارة إلى (1،1) ، والتي هي في الصفحة 1.

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

يوجد الآن صفين في المؤشر ، يشير كل منهما إلى بداية سلسلة HOT:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (1,1) 2 | (0,1) (2 rows) 

لسوء الحظ ، تفتقر الوثائق تقريبًا إلى معلومات حول التحديثات في الصفحة وتحديثات HOT ، ويجب أن تبحث عن إجابات في التعليمات البرمجية المصدر. أنصحك أن تبدأ مع README.HOT .

اقرأ على .

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


All Articles