چرا صفحه‌های انتهایی ممکن است کند شوند؟

فهرست سفارش، رویداد یا پیام معمولاً به چند صفحه تقسیم می‌شود. روش جابه‌جایی بر اساس تعداد ردیف‌های قبلی ساده است، اما در صفحه‌های عمیق می‌تواند کار زیادی برای کنارگذاشتن ردیف‌ها ایجاد کند. اگر کاربر معمولاً از آخرین نتیجه ادامه می‌دهد، نشانگر می‌تواند مرز مناسب‌تری باشد. این مقاله روش ادامه از آخرین کلید دیده‌شده را با یک مثال محلی و قابل آزمون توضیح می‌دهد.

ترتیب باید قطعی باشد. اگر چند رویداد مقدار زمان یا شمارهٔ یکسان دارند، همان ستون به‌تنهایی مرز صفحه را مشخص نمی‌کند. در نمونه، ترکیب شماره و شناسهٔ یکتا برای مرتب‌سازی و ادامهٔ جست‌وجو به کار می‌رود و نمایه نیز با همین ترتیب ساخته می‌شود. نشانگر تنها مقدار این دو کلید را حمل می‌کند؛ رمزگذاری ظاهری آن به معنی امضا یا جلوگیری از دست‌کاری نیست.

مرز نشانگر و ترتیب بدون ابهام

ورودی نشانگر از نظر طول، قالب و نوع داده بررسی می‌شود و مقدارهای آن با پارامتر به پرس‌وجو می‌روند. اندازهٔ صفحه سقف مشخص دارد و از ورودی کاربر، متن دستور ساخته نمی‌شود. با این حال، کنترل دسترسی باید در خود پرس‌وجوی واقعی محصول باقی بماند. نشانگر معتبر نباید اجازه دهد دادهٔ متعلق به سازمان یا کاربر دیگری خوانده شود؛ این مثال فقط قرارداد صفحه‌بندی را جدا آزمایش می‌کند.

چهار فایل را در یک پوشه ذخیره کنید و python -m unittest test_pages.py را اجرا کنید. سپس python demo_pages.py را برای دیدن سه دستهٔ چهارتایی اجرا کنید. نمونه از پایگاه دادهٔ درون‌حافظه‌ای اس‌کیوالایت استفاده می‌کند تا بدون نصب سرور قابل اجرا باشد. مفهوم مقایسهٔ چندستونی با پستگرس هم مرتبط است، ولی نتیجهٔ طرح اجرا و کارایی آن باید روی موتور و دادهٔ واقعی شما جدا سنجیده شود.

آزمون، چند ردیف با کلید مرتب‌سازی یکسان و درج یک ردیف قدیمی‌تر میان دو صفحه را پوشش می‌دهد. ردیف‌های قبلی تکرار نمی‌شوند، اما این روش تصویر ثابت از کل پایگاه داده ایجاد نمی‌کند. تغییر مقدار کلید یا حذف ردیف‌ها می‌تواند رفتار مشاهده را عوض کند. اگر گزارش حسابرسی به نمای ثابت نیاز دارد، تراکنش، سطح جداسازی و مدت نگهداری آن را به‌عنوان مسئله‌ای جدا طراحی کنید.

فایل‌های کامل و قابل اجرا

چهار فایل زیر کامل‌اند؛ پیش‌نیاز، ترتیب اجرا و روش بررسی در متن آمده است. کدها خلاصه یا پنهان نشده‌اند.

1. فایل کامل schema.sql

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

CREATE TABLE events (
  id INTEGER PRIMARY KEY,
  sequence INTEGER NOT NULL,
  label TEXT NOT NULL
);
CREATE INDEX events_cursor_idx ON events(sequence, id);

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

2. فایل کامل pages.py

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

import base64
import json


def encode_cursor(row):
    data = json.dumps([row[1], row[0]], separators=(',', ':')).encode()
    return base64.urlsafe_b64encode(data).decode()


def decode_cursor(cursor):
    if len(cursor) > 128:
        raise ValueError('Cursor too long')
    try:
        values = json.loads(base64.b64decode(cursor, altchars=b'-_', validate=True))
    except (ValueError, TypeError) as error:
        raise ValueError('Invalid cursor') from error
    if not isinstance(values, list) or len(values) != 2 or any(type(v) is not int or v < 0 for v in values):
        raise ValueError('Cursor requires two nonnegative integers')
    return values


def page(connection, after=None, size=20):
    if type(size) is not int or not 1 <= size <= 100:
        raise ValueError('Page size must be 1..100')
    params = []
    where = ''
    if after is not None:
        where = 'WHERE (sequence, id) > (?, ?)'
        params.extend(decode_cursor(after))
    rows = connection.execute(f'SELECT id, sequence, label FROM events {where} ORDER BY sequence, id LIMIT ?', [*params, size]).fetchall()
    return {'rows': rows, 'next': encode_cursor(rows[-1]) if rows else None}

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

3. فایل کامل demo_pages.py

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

from pathlib import Path
import sqlite3
from pages import page

if __name__ == '__main__':
    with sqlite3.connect(':memory:') as connection:
        connection.executescript(Path(__file__).with_name('schema.sql').read_text())
        connection.executemany('INSERT INTO events VALUES (?, ?, ?)', [(i, i // 3, f'event-{i}') for i in range(1, 13)])
        cursor = None
        while True:
            result = page(connection, cursor, size=4)
            if not result['rows']:
                break
            print(result)
            cursor = result['next']

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

4. فایل کامل test_pages.py

این فایل را با همین نام و بدون حذف سطرها کنار بقیهٔ فایل‌ها ذخیره کنید.

from pathlib import Path
import sqlite3
import unittest
from pages import decode_cursor, page


class PaginationTests(unittest.TestCase):
    def test_duplicate_sort_keys_and_concurrent_insert(self):
        with sqlite3.connect(':memory:') as connection:
            connection.executescript(Path(__file__).with_name('schema.sql').read_text())
            connection.executemany('INSERT INTO events VALUES (?, 1, ?)', [(i, str(i)) for i in range(1, 7)])
            first = page(connection, size=3)
            connection.execute("INSERT INTO events VALUES (0, 0, 'earlier')")
            second = page(connection, first['next'], size=3)
            self.assertEqual([r[0] for r in first['rows'] + second['rows']], list(range(1, 7)))
            self.assertEqual(page(connection, second['next'])['rows'], [])

    def test_invalid_cursor_and_size(self):
        with self.assertRaises(ValueError):
            decode_cursor('not-json')
        with self.assertRaises(ValueError):
            page(None, size=0)


if __name__ == '__main__':
    unittest.main()

دستور اجرا، نتیجهٔ مورد انتظار و محدودیت این فایل در بخش اجرای مقاله توضیح داده شده است.

اجرای نمونه و انتخاب روش مناسب محصول

نشانگر برای پیمایش بعدی مناسب است، اما پرش مستقیم به صفحهٔ هزارم یا نمایش شمار کل نتایج را خودبه‌خود حل نمی‌کند. در مثال، برای تشخیص پایان ممکن است یک درخواست خالی دیگر لازم باشد. نیاز محصول را پیش از انتخاب روش مشخص کنید و برنامهٔ اجرا، زمان پاسخ و رفتار درج هم‌زمان را اندازه بگیرید. سریع‌تر شدن احتمالی نباید با تغییر بی‌توضیح ترتیب یا حذف دادهٔ مورد انتظار کاربر همراه شود.

چک‌لیست اجرای عملی

  • مرتب‌سازی با کلید یکتا تکمیل شود.
  • نشانگر و اندازهٔ صفحه اعتبارسنجی شوند.
  • کنترل دسترسی در پرس‌وجوی واقعی حفظ شود.
  • درج هم‌زمان و نیاز به نمای ثابت جدا آزمون شوند.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.