PostgreSQL Antipatterns: تنقل التسجيل

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

أي أن هناك لوحة في القاعدة events، وحقلها tsهو نفس الوقت الذي نريد فيه عرض هذه السجلات بطريقة منظمة تمامًا:

CREATE TABLE events(
  id
    serial
      PRIMARY KEY
, ts
    timestamp
, data
    json
);

CREATE INDEX ON events(ts DESC);

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

# 0 "أنا مجرم في أمي"


cur.execute("SELECT * FROM events;")
rows = cur.fetchall();
rows.sort(key=lambda row: row.ts, reverse=True);
limit = 26
print(rows[offset:offset+limit]);

إنها ليست مزحة - نادرًا ، ولكنها موجودة في البرية. في بعض الأحيان بعد العمل مع ORM ، قد يكون من الصعب التبديل إلى عمل "مباشر" مع SQL.

ولكن دعنا ننتقل إلى مشاكل أكثر شيوعًا وأقل وضوحًا.

رقم 1. عوض


SELECT
  ...
FROM
  events
ORDER BY
  ts DESC
LIMIT 26 OFFSET $1; -- 26 -   , $1 -  

26? . , 25 , 1, , - .

, «» , . PostgreSQL , , — .

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

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

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

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

توسيع المؤشر


يدرك المطور الماكر أنك بحاجة إلى جعل مفتاح الفهرس فريدًا ، والطريقة الأسهل هي توسيعه بحقل فريد متعمد ، وهو PK مثالي لـ:

CREATE UNIQUE INDEX ON events(ts DESC, id DESC);

ويتغير الطلب:

SELECT
  ...
ORDER BY
  ts DESC, id DESC
LIMIT 26 OFFSET $1;

# 2 الانتقال إلى "المؤشرات"


بعد مرور بعض الوقت ، يأتي DBA إليك وهو "سعيد" لأن طلباتك تقوم بتحميل الخادم بشكل هزلي مع مجموعات OFFSET التي تجرها الخيول ، وبشكل عام ، حان الوقت للتبديل إلى التنقل من آخر قيمة معروضة . يتغير طلبك مرة أخرى:

SELECT
  ...
WHERE
  (ts, id) < ($1, $2) --      
ORDER BY
  ts DESC, id DESC
LIMIT 26;

لقد تنفست الصعداء قبل أن تأتي ...

# 3 تنظيف الفهرس


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

ولكن ماذا تفعل مع المشكلة الأولية "القفز" بين السجلات؟ .. وكل شيء بسيط - تحتاج إلى اختيار الكتل بعدد غير محدود من السجلات!

بشكل عام ، من يمنعنا من قراءة ليس "26 بالضبط" ، ولكن "ليس أقل من 26"؟ على سبيل المثال ، بحيث توجد في الكتلة التالية سجلات ذات قيم مختلفة بشكل واضحts - فلن تكون هناك مشاكل في "القفز" بين الكتل!

هيريس كيفية القيام بذلك:

SELECT
  ...
WHERE
  ts < $1 AND
  ts >= coalesce((
    SELECT
      ts
    FROM
      events
    WHERE
      ts < $1
    ORDER BY
      ts DESC
    LIMIT 1 OFFSET 25
  ), '-infinity')
ORDER BY
  ts DESC;

ما الذي يجري هنا؟

  1. ننزل 25 سجلاً ونحصل على قيمة "الحدود" ts.
  2. إذا لم يكن هناك شيء بالفعل ، فاستبدل القيمة NULL بـ -infinity.
  3. اطرح الجزء الكامل من القيم بين القيمة المستلمة tsوالمعلمة $ 1 التي تم تمريرها من الواجهة (القيمة المرسومة "الأخيرة" السابقة).
  4. إذا تم إرجاع كتلة تحتوي على أقل من 26 إدخالاً ، فهذا هو الأخير.

أو نفس الصورة:


نظرًا لأن نموذجنا لا يحتوي الآن على "بداية" محددة ، فلا شيء يمنعنا من "عكس" هذا الاستعلام في الاتجاه المعاكس وتنفيذ التحميل الديناميكي لكتل ​​البيانات من "النقطة المرجعية" في كلا الاتجاهين - لأسفل وأعلى.

تعليق


  1. , , « ». Index Only Scan.
  2. , , ts , . — « 00:00:00.000», . , . , .

All Articles