فهارس الغلاف لـ GiST

مؤشر التغطية ليس مجرد ميزة أخرى قد تكون مفيدة. هذا الشيء عملي بحت. بدونها ، قد لا يعطي Index Only Scan الفوز. على الرغم من أن مؤشر التغطية في المواقف المختلفة فعال بطرق مختلفة.

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

أنهت Anastasia Lubennikova (Postgres Professional) طريقة btree بحيث يمكن إدراج أعمدة إضافية في الفهرس. تم تضمين هذا التصحيح في برنامج PostgreSQL 11. لكن لم يكن لدى تصحيحات طرق الوصول إلى GiST / SP-GiST وقت لتنضج قبل إصدار هذا الإصدار. بحلول عيد الميلاد المجيد الثاني عشر.

نشأت رغبة بناءة في الحصول على فهارس شاملة لـ GiST منذ فترة طويلة: تم تقديم تصحيح اختبار من Andrey Borodin إلى المجتمع مرة أخرى في منتصف أبريل 2018. لقد قام بكل الأعمال الأساسية الصعبة للغاية.

في أوائل أغسطس 2019 ، أضاف ألكساندر كوروتكوف تحسينات تجميلية وارتكب التصحيح.

للتظاهر وبعض الأبحاث ، سنقوم بإنشاء مجموعة من 3 ملايين مستطيل. في الوقت نفسه ، بضع كلمات حول نوع المربع ، لأنه ليس كل التلاعب بها بديهية.

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

( ( x1 , y1 ) , ( x2 , y2 ) ) ( x1 , y1 ) , ( x2 , y2 ) x1 , y1 , x2 , y2 

في الممارسة العملية ، عليك أن تكتب ، مثل ، مثل هذا:

 SELECT box('1,2', '3,4'); box ------------- (3,4),(1,2) (1 row) 

أولاً ، يُظهر لنا Postgres القمة اليمنى العليا ، ثم أسفل اليسار. إذا نكتب مثل هذا ،

 SELECT box('5,2', '3,4'); box ------------- (5,4),(3,2) (1 row) 

ثم سنتأكد من أن بوستجرس لم يعطِ القمم التي قدموها له. قام بحساب اليمين العلوي والسفلي الأيسر من اليسار العلوي والسفلي الأيمن. هذه خاصية ملائمة عندما يكون موقع القمم غير معروف مسبقًا - في حالة التوليد العشوائي ، على سبيل المثال. التدوين "1،2" ، "3،4" يعادل النقطة (1،2) ، النقطة (3،4). هذا النموذج هو في بعض الأحيان أكثر ملاءمة.


للعمل: ابحث في 3 ملايين مستطيل


 CREATE TABLE boxes(id serial, thebox box, name text); 

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




 INSERT INTO boxes(thebox, name) SELECT box( point( random()-random(), random()-random() ), point( random()-random(), random()-random() ) ), 'box no.' || x FROM generate_series(1,3000000) AS g(x); 


حجم الجدول الذي يعرض \dt+ هو 242 ميغابايت. الآن يمكنك البدء في البحث.

نحن نبحث بدون فهرس:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------------- Gather (cost=1000.00..47853.00 rows=3000 width=46) (actual time=0.140..246.998 rows=139189 loops=1) Workers Planned: 2 Workers Launched: 2 -> Parallel Seq Scan on boxes (cost=0.00..46553.00 rows=1250 width=46) (actual time=0.011..106.708 rows=46396 loops=3) Filter: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Rows Removed by Filter: 953604 Planning Time: 0.040 ms Execution Time: 259.262 ms (8 rows) 

نرى أن هناك مسح تسلسلي متوازي - المسح المتسلسل (وإن كان متوازياً).

قم بإنشاء فهرس منتظم وغير شامل:

 CREATE INDEX ON boxes USING gist(thebox); 

حجم فهرس boxes_thebox_idx ، والذي يعرض \di+ ، 262 ميجابايت. استجابة لنفس الطلب ، حصلنا على:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Bitmap Heap Scan on boxes (cost=159.66..9033.30 rows=3000 width=46) (actual time=29.101..80.283 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_idx (cost=0.00..158.91 rows=3000 width=0) (actual time=25.029..25.029 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 0.053 ms Execution Time: 86.206 ms (7 rows) 

تم تقليل وقت البحث بعامل ثلاثة ، وبدلاً من Parallel Seq Scan ، حصلوا على مسح فهرس الصور النقطية. لا تتوازى ، لكنها تعمل بشكل أسرع.

الآن اقتل الفهرس القديم وقم بإنشاء فهرس شامل:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); 

فهرس boxes_thebox_name_idx البدانة: 356 ميغابايت. دعنا نذهب:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN ------------------------------------------------------------------------------ Bitmap Heap Scan on boxes (cost=207.66..9081.30 rows=3000 width=46) (actual time=86.822..152.014 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_name_idx (cost=0.00..206.91 rows=3000 width=0) (actual time=83.044..83.044 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 3.807 ms Execution Time: 157.997 ms (7 rows) 


يتم استخدام Index Only Scan ، لكن الصورة حزينة: الوقت أطول تقريبًا من الوقت بدونه. نقرأ كتيب مُنشئ المؤشرات ، في الجزء الأول :

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

نحن نفعل فراغ. نكرر:

 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Index Only Scan using boxes_thebox_name_idx on boxes (cost=0.41..236.91 rows=3000 width=46) (actual time=0.104..38.651 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Fetches: 0 Planning Time: 0.052 ms Execution Time: 44.337 ms (5 rows) 

مسألة مختلفة تماما! ضعف الربح مقارنة بالمؤشر غير الشامل.


الانتقائية والكسب


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

 CREATE TABLE test_covergist(id serial, tochka point, name text); 

 INSERT INTO test_covergist(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,3000000) AS g(x); 

حجم الجدول هو 211 ميغابايت.

 CREATE INDEX on test_covergist USING gist(tochka); 

الحجم 213 ميغابايت.

من الواضح أننا سنأخذ جميع النقاط المتاحة في مربع:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1087.964..1864.059 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared read=54287 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1084.949..1084.949 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared read=27262 Planning Time: 0.102 ms Execution Time: 2029.501 ms (9 rows) 

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

 -o "-c shared_buffers=1GB" 

الآن:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=231.032..613.326 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared hit=54248 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=228.068..228.068 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=27223 Planning Time: 0.070 ms Execution Time: 755.915 ms (9 rows) 

وهذا هو ، أصبحت القراءة المشتركة نجاحًا مشتركًا ، وتم تقليل الوقت ثلاث مرات.

تفصيل مهم آخر في EXPLAIN: يتم إرجاع 3 ملايين نقطة ، والتنبؤ بعدد السجلات التي تم إرجاعها هو 3 آلاف. Spoiler: لن يتغير هذا الرقم مع أي انتقائية. لا يعرف المُحسّن كيفية تقييم العلاقة الأساسية عند العمل مع أنواع المربعات أو النقاط. ولن تتغير الخطة: لأي حجم للمستطيل ، سيكون هناك فهرس مسح نقطي على test_covergist_tochka_idx.

فيما يلي قياسان آخران مع عدد السجلات الصادرة ، يختلفان حسب أوامر الحجم:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=27.889..134.054 rows=269882 loops=1) Recheck Cond: ('(300000,300000),(0,0)'::box @> tochka) Heap Blocks: exact=27024 Buffers: shared hit=29534 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=24.847..24.847 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Buffers: shared hit=2510 Planning Time: 0.074 ms Execution Time: 151.269 ms (9 rows) 

تقوم بإرجاع سجلات أقل بـ 10 مرات (العدد الفعلي ... الصفوف = 269882) ، وقد انخفض الوقت بنحو 5 مرات.

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','30000,30000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1.882..16.095 rows=2780 loops=1) Recheck Cond: ('(30000,30000),(0,0)'::box @> tochka) Heap Blocks: exact=2624 Buffers: shared hit=2655 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1.035..1.035 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Buffers: shared hit=31 Planning Time: 0.154 ms Execution Time: 16.702 ms (9 rows) 

يتم احتساب محتويات مربع 30K × 30K (2780) في 16 مللي ثانية فقط. وعندما يكون هناك عشرات السجلات ، يتم استخراجها بالفعل في أجزاء من ms ، وهذه القياسات غير موثوقة للغاية.

أخيرًا ، قم بقياس الشيء نفسه باستخدام الفهرس الشامل:

 CREATE INDEX on test_covergist USING gist(tochka) INCLUDE(name); 

الحجم 316 ميغابايت.

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.160..568.707 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=40492 Planning Time: 0.090 ms Execution Time: 709.837 ms (6 rows) 

الوقت هو نفسه تقريبا كما هو الحال مع الفهرس التقليدي ، على الرغم من المسح الضوئي فقط.

ولكن:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.083..53.277 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=3735 Planning Time: 0.077 ms Execution Time: 66.162 ms (6 rows) 

وكان 151 مللي. وبناء على ذلك:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.043..0.639 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=52 Planning Time: 0.053 ms Execution Time: 0.791 ms (6 rows) 

هذا بالفعل جزء صغير من مللي لنفس السجلات نقطة 2780.

المخازن المؤقتة مثل البنادق


يمكن التماس التفسير وإيجاده في بندقية لم تطلق النار بعد لكن ذلك كان معلقًا على الحائط: عدد الكتل المقروءة. في حالة وجود فهرس شامل ، تتم قراءة كتل الفهرس نفسها فقط (Heap Fetches: 0). في ثلاث حالات ، كانت هذه الأرقام 40492 و 3735 و 52. ولكن عند استخدام الفهرس العادي ، تتكون الكتل المقروءة من مجموع البتات المقروءة في فهرس Bitmap Heap Scan (54248 مع 3 ملايين سجل) وتلك التي يجب قراءتها من الكومة (27223) ، حيث لا يمكن استخراج حقل الاسم من فهرس عادي. 54248 + 27223 = 81471. كان الحصري 40492. لحالتين أخريين: 29534 + 2510 = 31044 و ​​2655 + 31 = 2686. في حالة وجود فهرس منتظم ، يتم قراءة المزيد من الكتل على أي حال ، ولكن مع تحسن في الانتقائية ، يبدأ عدد الكتل المقروءة في الاختلاف بترتيب الحجم بدلاً من مرتين بسبب حقيقة أن عدد الكتل الضرورية من الكومة يتناقص ببطء أكثر من قراءة كتل الفهرس.

مجموع السجلات التي تم إرجاعها (بالألف)30002702.7
كتل القراءة (عادي / شامل)81471/4049231044/37352686/52
وقت755/710151/6616 / 0.7


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

 CREATE TABLE test_covergist_small(id serial, tochka point, name text); INSERT INTO test_covergist_small(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,300000) AS g(x); CREATE INDEX ON test_covergist_small USING gist(tochka); EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist_small WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist_small (cost=14.61..867.19 rows=300 width=31) (actual time=36.115..130.435 rows=300000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=2500 Buffers: shared hit=5225 -> Bitmap Index Scan on test_covergist_small_tochka_idx (cost=0.00..14.53 rows=300 width=0) (actual time=35.894..35.895 rows=300000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=2725 Planning Time: 0.060 ms Execution Time: 158.580 (9 rows) 

بعد ذلك ، كرر الشيء نفسه بالنسبة للفهرس الشامل. وهنا النتائج:

مجموع السجلات التي تم إرجاعها (بالألف)300270.25
كتل القراءة (عادي / شامل)5225/37263026/352270/8
وقت158/17820/130.4 / 0.2


في حالة تغطية النقاط بنسبة 100٪ ، كان الاستعلام أبطأ قليلاً من المؤشر المعتاد. علاوة على ذلك ، كما في حالة 3 ملايين ، كل شيء سقط في مكانه. وهذا هو ، الانتقائية مهمة.

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

بدلا من الاستنتاج


دعنا نعود للحظة إلى المستطيلات العشوائية. دعونا نحاول أن نفعل نفس الشيء مع spgist. يمكنك تذكر أو معرفة معنى فهم الاختلافات بين SP-GiST و GiST من خلال قراءة فهارس المقالات في PostgreSQL - 6 . إنشاء فهرس شامل:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); ERROR: access method "spgist" does not support included columns 

للأسف ، بالنسبة إلى SP-GiST ، لم يتم تطبيق الفهارس الشاملة بعد.
لذلك هناك مجال للتحسين!



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


All Articles