DBA: البحث عن فهارس عديمة الفائدة

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

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

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

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

  • فهارس غير مستخدمة
  • بادئة "استنساخ"
  • الطابع الزمني "في المنتصف"
  • منطقي قابل للفهرسة
  • صفائف في الفهرس
  • قمامة فارغة

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

SELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0;

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

ما هي المؤشرات [غير] المناسبة


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

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



الآن فمن الواضح أن حقل A أمر على الصعيد العالمي، وB - فقط ضمن قيمة محددة و . دعنا نلقي نظرة على أمثلة للظروف التي تحدث في الاستعلامات الحقيقية ، وكيف "ستسير" حول الفهرس.

جيد: شرط البادئة


لاحظ أن الفهرس btree(A, B)يتضمن " مؤشر فرعي" btree(A). هذا يعني أن جميع القواعد الموضحة أدناه ستعمل مع أي فهرس بادئة.

بمعنى ، إذا قمت بإنشاء فهرس أكثر تعقيدًا من المثال الخاص بنا ، فهناك شيء من هذا النوع btree(A, B, C)- يمكنك افتراض أن قاعدة البيانات الخاصة بك "تظهر" تلقائيًا:

  • btree(A, B, C)
  • btree(A, B)
  • btree(A)

وهذا يعني أن الوجود "المادي" لمؤشر البادئة في قاعدة البيانات لا لزوم له في معظم الحالات. بعد كل شيء ، كلما زاد عدد الفهارس التي يجب أن يكتبها الجدول - كلما كان الأمر أسوأ بالنسبة لـ PostgreSQL ، لأنه يسمي تضخيم الكتابة - اشتكى Uber من ذلك (وهنا يمكنك العثور على تحليل لمطالباتهم ).

وإذا كان هناك شيء يمنع القاعدة من العيش بشكل جيد ، فمن الجدير العثور عليه والقضاء عليه. لنلقي نظرة على مثال:

CREATE TABLE tbl(A integer, B integer, val integer);
CREATE INDEX ON tbl(A, B)
  WHERE val IS NULL;
CREATE INDEX ON tbl(A) --  #1
  WHERE val IS NULL;
CREATE INDEX ON tbl(A, B, val);
CREATE INDEX ON tbl(A); --  #2

استعلام بحث فهرس البادئة
WITH sch AS (
  SELECT
    'public'::text sch -- schema
)
, def AS (
  SELECT
    clr.relname nmt
  , cli.relname nmi
  , pg_get_indexdef(cli.oid) def
  , cli.oid clioid
  , clr
  , cli
  , idx
, (
    SELECT
      array_agg(T::text ORDER BY f.i)
    FROM
      (
        SELECT
          clr.oid rel
        , i
        , idx.indkey[i] ik
        FROM
          generate_subscripts(idx.indkey, 1) i
      ) f
    JOIN
      pg_attribute T
        ON (T.attrelid, T.attnum) = (f.rel, f.ik)
  ) fld$
  FROM
    pg_class clr
  JOIN
    pg_index idx
      ON idx.indrelid = clr.oid AND
      idx.indexprs IS NULL
  JOIN
    pg_class cli
      ON cli.oid = idx.indexrelid
  JOIN
    pg_namespace nsp
      ON nsp.oid = cli.relnamespace AND
      nsp.nspname = (TABLE sch)
  WHERE
    NOT idx.indisunique AND
    idx.indisready AND
    idx.indisvalid
  ORDER BY
    clr.relname, cli.relname
)
, fld AS (
  SELECT
    *
  , ARRAY(
      SELECT
        (att::pg_attribute).attname
      FROM
        unnest(fld$) att
    ) nmf$
  , ARRAY(
      SELECT
        (
          SELECT
            typname
          FROM
            pg_type
          WHERE
            oid = (att::pg_attribute).atttypid
        )
      FROM
        unnest(fld$) att
    ) tpf$
  , CASE
      WHEN def ~ ' WHERE ' THEN regexp_replace(def, E'.* WHERE ', '')
    END wh
  FROM
    def
)
, pre AS (
  SELECT
    nmt
  , wh
  , nmf$
  , tpf$
  , nmi
  , def
  FROM
    fld
  ORDER BY
    1, 2, 3
)
SELECT DISTINCT
  Y.*
FROM
  pre X
JOIN
  pre Y
    ON Y.nmi <> X.nmi AND
    (Y.nmt, Y.wh) IS NOT DISTINCT FROM (X.nmt, X.wh) AND
    (
      Y.nmf$[1:array_length(X.nmf$, 1)] = X.nmf$ OR
      X.nmf$[1:array_length(Y.nmf$, 1)] = Y.nmf$
    )
ORDER BY
  1, 2, 3;

من الناحية المثالية ، يجب أن تحصل على تحديد فارغ ، ولكن انظر - هذه هي مجموعات الفهرس المشبوهة لدينا:

nmt | wh            | nmf$      | tpf$             | nmi             | def
---------------------------------------------------------------------------------------
tbl | (val IS NULL) | {a}       | {int4}           | tbl_a_idx       | CREATE INDEX ...
tbl | (val IS NULL) | {a,b}     | {int4,int4}      | tbl_a_b_idx     | CREATE INDEX ...
tbl |               | {a}       | {int4}           | tbl_a_idx1      | CREATE INDEX ...
tbl |               | {a,b,val} | {int4,int4,int4} | tbl_a_b_val_idx | CREATE INDEX ...

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

حسن: جميع الثوابت عدا الحقل الأخير


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



  • WHERE A = constA AND B [op] constB / = ANY(...) / IN (...)
    op : { =, >, >=, <, <= }
  • WHERE A = constA AND B BETWEEN constB1 AND constB2
  • WHERE A = constA ORDER BY B

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

  • WHERE A [op] const / = ANY(...) / IN (...)
    op : { =, >, >=, <, <= }
  • WHERE A BETWEEN const1 AND const2
  • ORDER BY A
  • WHERE (A, B) [op] (constA, constB) / = ANY(...) / IN (...)
    op : { =, >, >=, <, <= }
  • ORDER BY A, B

سيئ: العد الكامل لـ "الطبقة"


باستخدام جزء من الاستعلامات ، يصبح التعداد الوحيد للحركة في الفهرس تعدادًا كاملاً لجميع القيم في إحدى "الطبقات". من المحظوظ إذا كانت هناك وحدة لهذه القيم - وإذا كان هناك الآلاف؟ ..

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

  • WHERE A <> const
  • WHERE B [op] const / = ANY(...) / IN (...)
  • ORDER BY B
  • ORDER BY B, A

تالف: الفاصل الزمني أو المجموعة غير موجود في الحقل الأخير


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

  • WHERE A BETWEEN constA1 AND constA2 AND B BETWEEN constB1 AND constB2
  • WHERE A = ANY(...) AND B = const
  • WHERE A = ANY(...) ORDER BY B
  • WHERE A = ANY(...) AND B = ANY(...)

سيء: التعبير بدلاً من الحقل


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

  • WHERE A - const1 [op] const2
    الإصلاح: WHERE A [op] const1 + const2
  • WHERE A::typeOfConst = const
    الإصلاح: WHERE A = const::typeOfA

نأخذ بعين الاعتبار أصالة الحقول


افترض أنك بحاجة إلى المؤشر (A, B)، وتريد أن تختار إلا من خلال المساواة : (A, B) = (constA, constB). سيكون استخدام مؤشر التجزئة مثاليًا ، ولكن ... بالإضافة إلى عدم تسجيل هذه الفهارس (عدم تسجيلها) حتى الإصدار 10 ، لا يمكن أن توجد أيضًا في عدة حقول:

CREATE INDEX ON tbl USING hash(A, B);
-- ERROR:  access method "hash" does not support multicolumn indexes

بشكل عام ، لقد اخترت btree. إذن ما هي أفضل طريقة لترتيب الأعمدة فيه - (A, B)أو (B, A)؟ للإجابة على هذا السؤال ، من الضروري أن تأخذ بعين الاعتبار معلمة مثل أصالة البيانات في العمود المقابل - أي عدد القيم الفريدة التي تحتوي عليها.

دعونا نتخيل ذلك A = {1,2}, B = {1,2,3,4}، ونرسم مخططًا تفصيليًا لشجرة الفهرس لكلا الخيارين:



في الواقع ، كل عقدة في الشجرة التي نرسمها هي صفحة في الفهرس. وكلما زاد عدد مساحة القرص التي سيشغلها الفهرس ، زادت المدة التي تستغرقها القراءة منه.

في مثالنا ، (A, B)يحتوي الخيار على 10 عقد ، و (B, A)- 12. أي أنه من الأكثر ربحية وضع "الحقول" بأقل عدد ممكن من القيم الفريدة "الأولى" .

سيئ: الكثير وفي غير مكانه (الطابع الزمني "في الوسط")


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

CREATE TABLE tbl(A integer, B timestamp);
CREATE INDEX ON tbl(A, B);
CREATE INDEX ON tbl(B, A); -- - 



استعلام البحث عن فهارس الطابع الزمني [tz] غير النهائية
WITH sch AS (
  SELECT
    'public'::text sch -- schema
)
, def AS (
  SELECT
    clr.relname nmt
  , cli.relname nmi
  , pg_get_indexdef(cli.oid) def
  , cli.oid clioid
  , clr
  , cli
  , idx
, (
    SELECT
      array_agg(T::text ORDER BY f.i)
    FROM
      (
        SELECT
          clr.oid rel
        , i
        , idx.indkey[i] ik
        FROM
          generate_subscripts(idx.indkey, 1) i
      ) f
    JOIN
      pg_attribute T
        ON (T.attrelid, T.attnum) = (f.rel, f.ik)
  ) fld$
, (
    SELECT
      array_agg(replace(opcname::text, '_ops', '') ORDER BY f.i)
    FROM
      (
        SELECT
          clr.oid rel
        , i
        , idx.indclass[i] ik
        FROM
          generate_subscripts(idx.indclass, 1) i
      ) f
    JOIN
      pg_opclass T
        ON T.oid = f.ik
  ) opc$
  FROM
    pg_class clr
  JOIN
    pg_index idx
      ON idx.indrelid = clr.oid
  JOIN
    pg_class cli
      ON cli.oid = idx.indexrelid
  JOIN
    pg_namespace nsp
      ON nsp.oid = cli.relnamespace AND
      nsp.nspname = (TABLE sch)
  WHERE
    NOT idx.indisunique AND
    idx.indisready AND
    idx.indisvalid
  ORDER BY
    clr.relname, cli.relname
)
, fld AS (
  SELECT
    *
  , ARRAY(
      SELECT
        (att::pg_attribute).attname
      FROM
        unnest(fld$) att
    ) nmf$
  , ARRAY(
      SELECT
        (
          SELECT
            typname
          FROM
            pg_type
          WHERE
            oid = (att::pg_attribute).atttypid
        )
      FROM
        unnest(fld$) att
    ) tpf$
  FROM
    def
)
SELECT
  nmt
, nmi
, def
, nmf$
, tpf$
, opc$
FROM
  fld
WHERE
  'timestamp' = ANY(tpf$[1:array_length(tpf$, 1) - 1]) OR
  'timestamptz' = ANY(tpf$[1:array_length(tpf$, 1) - 1]) OR
  'timestamp' = ANY(opc$[1:array_length(opc$, 1) - 1]) OR
  'timestamptz' = ANY(opc$[1:array_length(opc$, 1) - 1])
ORDER BY
  1, 2;

هنا نحلل على الفور كل من أنواع حقول الإدخال نفسها وفئات عوامل التشغيل المطبقة عليها - نظرًا لأن بعض وظيفة timestamptz مثل date_trunc قد تتحول إلى حقل فهرس.

nmt | nmi         | def              | nmf$  | tpf$             | opc$
----------------------------------------------------------------------------------
tbl | tbl_b_a_idx | CREATE INDEX ... | {b,a} | {timestamp,int4} | {timestamp,int4}

سيئ: قليل جدًا (منطقي)


على الجانب الآخر من نفس العملة ، يصبح الوضع هو الفهرس حيث يكون المجال المنطقي ، والذي يمكن أن يأخذ 3 قيم فقط NULL, FALSE, TRUE. بطبيعة الحال ، فإن وجودها منطقي إذا كنت تريد استخدامه للفرز التطبيقي - على سبيل المثال ، عن طريق تعيينها كنوع من العقدة في التسلسل الهرمي للشجرة - سواء كان مجلدًا أو ورقة شجر ("المجلدات أولاً").

CREATE TABLE tbl(
  id
    serial
      PRIMARY KEY
, leaf_pid
    integer
, leaf_type
    boolean
, public
    boolean
);
CREATE INDEX ON tbl(leaf_pid, leaf_type); --   
CREATE INDEX ON tbl(public, id); -- - 

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

CREATE INDEX ON tbl(id) WHERE public;

استعلام البحث المنطقي في الفهارس
WITH sch AS (
  SELECT
    'public'::text sch -- schema
)
, def AS (
  SELECT
    clr.relname nmt
  , cli.relname nmi
  , pg_get_indexdef(cli.oid) def
  , cli.oid clioid
  , clr
  , cli
  , idx
, (
    SELECT
      array_agg(T::text ORDER BY f.i)
    FROM
      (
        SELECT
          clr.oid rel
        , i
        , idx.indkey[i] ik
        FROM
          generate_subscripts(idx.indkey, 1) i
      ) f
    JOIN
      pg_attribute T
        ON (T.attrelid, T.attnum) = (f.rel, f.ik)
  ) fld$
, (
    SELECT
      array_agg(replace(opcname::text, '_ops', '') ORDER BY f.i)
    FROM
      (
        SELECT
          clr.oid rel
        , i
        , idx.indclass[i] ik
        FROM
          generate_subscripts(idx.indclass, 1) i
      ) f
    JOIN
      pg_opclass T
        ON T.oid = f.ik
  ) opc$
  FROM
    pg_class clr
  JOIN
    pg_index idx
      ON idx.indrelid = clr.oid
  JOIN
    pg_class cli
      ON cli.oid = idx.indexrelid
  JOIN
    pg_namespace nsp
      ON nsp.oid = cli.relnamespace AND
      nsp.nspname = (TABLE sch)
  WHERE
    NOT idx.indisunique AND
    idx.indisready AND
    idx.indisvalid
  ORDER BY
    clr.relname, cli.relname
)
, fld AS (
  SELECT
    *
  , ARRAY(
      SELECT
        (att::pg_attribute).attname
      FROM
        unnest(fld$) att
    ) nmf$
  , ARRAY(
      SELECT
        (
          SELECT
            typname
          FROM
            pg_type
          WHERE
            oid = (att::pg_attribute).atttypid
        )
      FROM
        unnest(fld$) att
    ) tpf$
  FROM
    def
)
SELECT
  nmt
, nmi
, def
, nmf$
, tpf$
, opc$
FROM
  fld
WHERE
  (
    'bool' = ANY(tpf$) OR
    'bool' = ANY(opc$)
  ) AND
  NOT(
    ARRAY(
      SELECT
        nmf$[i:i+1]::text
      FROM
        generate_series(1, array_length(nmf$, 1) - 1) i
    ) &&
    ARRAY[ --  -  
      '{leaf_pid,leaf_type}'
    ]
  )
ORDER BY
  1, 2;

nmt | nmi               | def              | nmf$        | tpf$        | opc$
------------------------------------------------------------------------------------
tbl | tbl_public_id_idx | CREATE INDEX ... | {public,id} | {bool,int4} | {bool,int4}

صفائف في btree


نقطة منفصلة هي محاولة "فهرسة المصفوفة" باستخدام فهرس btree. هذا ممكن تمامًا نظرًا لأن العوامل المقابلة تنطبق عليهم :
(<, >, = . .) , B-, , . ( ). , , .
ولكن المشكلة هي أن لاستخدام شيء يريد أن مشغلي إدراج والتقاطع : <@, @>, &&. بالطبع ، هذا لا يعمل - لأنهم بحاجة إلى أنواع أخرى من الفهارس . كيف لا يعمل هذا btree لوظيفة الوصول إلى عنصر معين arr[i].

نتعلم أن نجد مثل:

CREATE TABLE tbl(
  id
    serial
      PRIMARY KEY
, pid
    integer
, list
    integer[]
);
CREATE INDEX ON tbl(pid);
CREATE INDEX ON tbl(list); -- - 

صفيف استعلام البحث في btree
WITH sch AS (
  SELECT
    'public'::text sch -- schema
)
, def AS (
  SELECT
    clr.relname nmt
  , cli.relname nmi
  , pg_get_indexdef(cli.oid) def
  , cli.oid clioid
  , clr
  , cli
  , idx
, (
    SELECT
      array_agg(T::text ORDER BY f.i)
    FROM
      (
        SELECT
          clr.oid rel
        , i
        , idx.indkey[i] ik
        FROM
          generate_subscripts(idx.indkey, 1) i
      ) f
    JOIN
      pg_attribute T
        ON (T.attrelid, T.attnum) = (f.rel, f.ik)
  ) fld$
  FROM
    pg_class clr
  JOIN
    pg_index idx
      ON idx.indrelid = clr.oid
  JOIN
    pg_class cli
      ON cli.oid = idx.indexrelid
  JOIN
    pg_namespace nsp
      ON nsp.oid = cli.relnamespace AND
      nsp.nspname = (TABLE sch)
  WHERE
    NOT idx.indisunique AND
    idx.indisready AND
    idx.indisvalid AND
    cli.relam = (
      SELECT
        oid
      FROM
        pg_am
      WHERE
        amname = 'btree'
      LIMIT 1
    )
  ORDER BY
    clr.relname, cli.relname
)
, fld AS (
  SELECT
    *
  , ARRAY(
      SELECT
        (att::pg_attribute).attname
      FROM
        unnest(fld$) att
    ) nmf$
  , ARRAY(
      SELECT
        (
          SELECT
            typname
          FROM
            pg_type
          WHERE
            oid = (att::pg_attribute).atttypid
        )
      FROM
        unnest(fld$) att
    ) tpf$
  FROM
    def
)
SELECT
  nmt
, nmi
, nmf$
, tpf$
, def
FROM
  fld
WHERE
  tpf$ && ARRAY(
    SELECT
      typname
    FROM
      pg_type
    WHERE
      typname ~ '^_'
  )
ORDER BY
  1, 2;

nmt | nmi          | nmf$   | tpf$    | def
--------------------------------------------------------
tbl | tbl_list_idx | {list} | {_int4} | CREATE INDEX ...

إدخالات فهرس فارغة


آخر مشكلة شائعة هي "تلويث" الفهرس بإدخالات فارغة تمامًا. بمعنى ، يسجل حيث يكون التعبير المفهرس في كل من الأعمدة NULL . ليس لهذه السجلات أي فائدة عملية ، لكنها تضيف ضررًا مع كل إدخال.

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

CREATE TABLE tbl(
  id
    serial
      PRIMARY KEY
, fk
    integer
);
CREATE INDEX ON tbl(fk);

INSERT INTO tbl(fk)
SELECT
  CASE WHEN i % 10 = 0 THEN i END
FROM
  generate_series(1, 1000000) i;

في معظم الحالات ، يمكن تحويل هذا الفهرس إلى فهرس شرطي ، والذي يستغرق أيضًا أقل:

CREATE INDEX ON tbl(fk) WHERE (fk) IS NOT NULL;

_tmp=# \di+ tbl*
                               List of relations
 Schema |      Name      | Type  |  Owner   |  Table   |  Size   | Description
--------+----------------+-------+----------+----------+---------+-------------
 public | tbl_fk_idx     | index | postgres | tbl      | 36 MB   |
 public | tbl_fk_idx1    | index | postgres | tbl      | 2208 kB |
 public | tbl_pkey       | index | postgres | tbl      | 21 MB   |

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

استعلام بحث عن مداخل NULL في الفهارس
WITH sch AS (
  SELECT
    'public'::text sch -- schema
)
, def AS (
  SELECT
    clr.relname nmt
  , cli.relname nmi
  , pg_get_indexdef(cli.oid) def
  , cli.oid clioid
  , clr
  , cli
  FROM
    pg_class clr
  JOIN
    pg_index idx
      ON idx.indrelid = clr.oid
  JOIN
    pg_class cli
      ON cli.oid = idx.indexrelid
  JOIN
    pg_namespace nsp
      ON nsp.oid = cli.relnamespace AND
      nsp.nspname = (TABLE sch)
  WHERE
    NOT idx.indisprimary AND
    idx.indisready AND
    idx.indisvalid AND
    NOT EXISTS(
      SELECT
        NULL
      FROM
        pg_constraint
      WHERE
        conindid = cli.oid
      LIMIT 1
    ) AND
    pg_relation_size(cli.oid) > 1 << 20 --  1MB   
  ORDER BY
    clr.relname, cli.relname
)
, fld AS (
  SELECT
    *
  , regexp_replace(
      CASE
        WHEN def ~ ' USING btree ' THEN
          regexp_replace(def, E'.* USING btree (.*?)($| WHERE .*)', E'\\1')
      END
    , E' ([a-z]*_pattern_ops|(ASC|DESC)|NULLS\\s?(?:FIRST|LAST))'
    , ''
    , 'ig'
    ) fld
  , CASE
      WHEN def ~ ' WHERE ' THEN regexp_replace(def, E'.* WHERE ', '')
    END wh
  FROM
    def
)
, q AS (
  SELECT
    nmt
  , $q$-- $q$ || quote_ident(nmt) || $q$
      SET search_path = $q$ || quote_ident((TABLE sch)) || $q$, public;
      SELECT
        ARRAY[
          count(*)
        $q$ || string_agg(
          ', coalesce(sum((' || coalesce(wh, 'TRUE') || ')::integer), 0)' || E'\n' ||
          ', coalesce(sum(((' || coalesce(wh, 'TRUE') || ') AND (' || fld || ' IS NULL))::integer), 0)' || E'\n'
        , '' ORDER BY nmi) || $q$
        ]
      FROM
        $q$ || quote_ident((TABLE sch)) || $q$.$q$ || quote_ident(nmt) || $q$
    $q$ q
  , array_agg(clioid ORDER BY nmi) oid$
  , array_agg(nmi ORDER BY nmi) idx$
  , array_agg(fld ORDER BY nmi) fld$
  , array_agg(wh ORDER BY nmi) wh$
  FROM
    fld
  WHERE
    fld IS NOT NULL
  GROUP BY
    1
  ORDER BY
    1
)
, res AS (
  SELECT
    *
  , (
      SELECT
        qty
      FROM
        dblink(
          'dbname=' || current_database() || ' port=' || current_setting('port')
        , q
        ) T(qty bigint[])
    ) qty
  FROM
    q
)
, iter AS (
  SELECT
    *
  , generate_subscripts(idx$, 1) i
  FROM
    res
)
, stat AS (
  SELECT
    nmt table_name
  , idx$[i] index_name
  , pg_relation_size(oid$[i]) index_size
  , pg_size_pretty(pg_relation_size(oid$[i])) index_size_humanize
  , regexp_replace(fld$[i], E'^\\((.*)\\)$', E'\\1') index_fields
  , regexp_replace(wh$[i], E'^\\((.*)\\)$', E'\\1') index_cond
  , qty[1] table_rec_count
  , qty[i * 2] index_rec_count
  , qty[i * 2 + 1] index_rec_count_null
  FROM
    iter
)
SELECT
  *
, CASE
    WHEN table_rec_count > 0
      THEN index_rec_count::double precision / table_rec_count::double precision * 100
    ELSE 0
  END::numeric(32,2) index_cover_prc
, CASE
    WHEN index_rec_count > 0
      THEN index_rec_count_null::double precision / index_rec_count::double precision * 100
    ELSE 0
  END::numeric(32,2) index_null_prc
FROM
  stat
WHERE
  index_rec_count_null * 4 > index_rec_count --   NULL-
ORDER BY
  1, 2;

-[ RECORD 1 ]--------+--------------
table_name           | tbl
index_name           | tbl_fk_idx
index_size           | 37838848
index_size_humanize  | 36 MB
index_fields         | fk
index_cond           |
table_rec_count      | 1000000
index_rec_count      | 1000000
index_rec_count_null | 900000
index_cover_prc      | 100.00 -- 100%    
index_null_prc       | 90.00  --   90% NULL-""

آمل أن تساعدك بعض الاستفسارات في هذه المقالة.

All Articles