MVCC-5. التنظيف في الصفحة والساخنة

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

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

التنظيف في الصفحة مع تحديثات منتظمة


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

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

Fillfactor هو معلمة التخزين التي يمكن تعريفها للجدول (والفهرس). يقوم PostgreSQL بإدراج صف جديد (INSERT) على صفحة فقط إذا كانت هذه الصفحة أقل من ملء النسبة المئوية بالكامل. المساحة المتبقية مخصصة للإصدارات الجديدة من السلاسل التي تنتج عن التحديثات (UPDATE). القيمة الافتراضية للجداول هي 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'; 

هناك أربعة إصدارات من السطر في الصفحة:

 => 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. يشار إلى هذا بالفرق بين القيم على الصفحات والقيم العليا: فهو يتجاوز حد 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 (لأن الصفر مشغول بمعلومات التعريف):

 => 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) كمعرف إصدار الصف. بعد ذلك سيحاول الحصول على الصف المقابل من صفحة الجدول ، ولكن بفضل الحالة الميتة للمؤشر ، سيجد أن هذا الإصدار لم يعد موجودًا وسوف يتجاهله. (في الواقع ، في المرة الأولى التي يكتشف فيها عدم وجود نسخة من صف الجدول ، سيغير PostgreSQL أيضًا حالة المؤشر في صفحة الفهرس بحيث لا يصل إلى صفحة الجدول مرة أخرى.)

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

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


لماذا من السيء الاحتفاظ بروابط لجميع إصدارات سلسلة في الفهرس؟

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

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

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

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

ومع ذلك ، إذا تغيرت قيمة العمود الذي لا ينتمي إلى أي فهرس ، فلا فائدة من إنشاء سجل إضافي في الشجرة B التي تحتوي على نفس قيمة المفتاح. هذه هي الطريقة التي يعمل بها التحسين ، يسمى التحديث السريع - تحديث كومة الذاكرة المؤقتة فقط.

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

  • يتم وضع علامة على السلاسل التي تم تغييرها وإدراجها في السلسلة مع بت تحديث كومة الذاكرة المؤقتة؛
  • يتم تمييز الصفوف التي لم يتم الرجوع إليها من الفهرس بت بت Heap Only Tuple (أي ، "فقط الإصدار المجدول للصف") ؛
  • يتم دعم الربط المنتظم لإصدارات السلسلة من خلال الحقل ctid.

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

لإلقاء نظرة على تشغيل تحديث 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) 

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

  • تشير إشارة تحديث Heap Hot إلى أنك بحاجة إلى السير في سلسلة 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 ، لذلك يجب أن يؤدي التحديث التالي إلى تنظيف في الصفحة. ولكن هذه المرة في الصفحة هي سلسلة من التحديثات. يجب أن يظل "رأس" هذه السلسلة الساخنة دائمًا في مكانه ، حيث يشير الفهرس إليها ، ويمكن تحرير بقية المؤشرات: من المعروف أنه لم يتم الرجوع إليها من الخارج.

من أجل عدم لمس "الرأس" ، يتم استخدام عنونة مزدوجة: يتلقى المؤشر الذي يشير إليه الفهرس - في هذه الحالة (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 لحجز بعض المساحة على الصفحة للتحديثات. بالطبع ، يجب أن نأخذ في الاعتبار أنه كلما انخفض عامل التعبئة ، تظل المساحة غير المخصصة على الصفحة ، وبالتالي ، يزداد الحجم الفعلي للجدول.

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


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

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

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

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

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

أن تستمر .

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


All Articles