تصور کنید باید 10 میلیون رکورد را از دیتابیس بخوانید و روی این رکوردها یک پردازش انجام دهید. قاعدتاً اولین کاری که انجام می‌دهیم این است که داده‌ها را به صورت صفحه‌بندی‌شده (Pagination) و با یک مرتب‌سازی مشخص از دیتابیس بخوانیم، آن‌ها را پردازش کنیم و سپس به صفحه بعد برویم.

اما بعد از اینکه تعداد زیادی از رکوردها را خواندیم، سرعت خواندن اطلاعات از دیتابیس کاهش پیدا می‌کند و هر بار زمان بیشتری طول می‌کشد تا داده‌ها از دیتابیس خوانده شوند.

اگر داده‌ها را به صورت صفحه‌بندی‌شده خوانده باشید، احتمالاً چنین کوئری‌ای به دیتابیس ارسال شده است:

SELECT *
FROM dbo.OrderItems
ORDER BY CreatedAt DESC,
         Id DESC OFFSET 20000 ROWS FETCH NEXT 100 ROWS ONLY;

مشکل این کوئری این است که برای هر بار خواندن داده، دیتابیس باید تمام رکوردهای قبلی را SCAN کند و آن‌ها را رد کند تا به رکوردهای موردنظر برسد.

راه‌حل برای اینکه بتوانیم تمام داده‌ها را بدون این کاهش سرعت از دیتابیس بخوانیم، استفاده از Keyset Pagination است.

Keyset Pagination

در Keyset Pagination، برای رفتن به صفحه بعد، باید اطلاعات آخرین رکوردی که در صفحه قبل خوانده شده است را ارسال کنیم.

با این کار، دیتابیس نیازی ندارد تمام رکوردهای قبلی را Scan کند و می‌تواند مستقیماً سراغ داده‌هایی برود که با شرط موردنظر مطابقت دارند.

برای مثال:

SELECT TOP 100
       *
FROM dbo.OrderItems
WHERE CreatedAt < @LastCreatedAt
      OR
      (
          CreatedAt = @LastCreatedAt
          AND Id < @LastId
      )
ORDER BY CreatedAt DESC,
         Id DESC;

اما هر پیاده‌سازی، مزایا و معایب خودش را دارد.

در یک پیاده‌سازی ساده از Keyset Pagination، شما مستقیماً نمی‌توانید به یک صفحه خاص بروید. برای مثال، نمی‌توانید مستقیماً به صفحه 51 از 10,000 بروید.

همچنین پیاده‌سازی بالا بیشتر برای رفتن به صفحات بعدی طراحی شده است و نمی‌توانید به سادگی به صفحه قبل برگردید.

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

switch (direction)
{
    case KeysetPageDirection.First:
        query = query.OrderByDescending(oi => oi.CreatedAt).ThenByDescending(oi => oi.Id);
        queryingAscending = false;
        break;

    case KeysetPageDirection.Last:
        query = query.OrderBy(oi => oi.CreatedAt).ThenBy(oi => oi.Id);
        queryingAscending = true;
        break;

    case KeysetPageDirection.Previous when decodedCursor != null:
        query = query
            .Where(oi =>
                oi.CreatedAt > decodedCursor.CreatedAt ||
                (oi.CreatedAt == decodedCursor.CreatedAt && oi.Id > decodedCursor.Id))
            .OrderBy(oi => oi.CreatedAt)
            .ThenBy(oi => oi.Id);
        queryingAscending = true;
        break;

    case KeysetPageDirection.Next:
    default:
        if (decodedCursor != null)
        {
            query = query.Where(oi =>
                oi.CreatedAt < decodedCursor.CreatedAt ||
                (oi.CreatedAt == decodedCursor.CreatedAt && oi.Id < decodedCursor.Id));
        }
        query = query.OrderByDescending(oi => oi.CreatedAt).ThenByDescending(oi => oi.Id);
        queryingAscending = false;
        break;
}

پیاده‌سازی Keyset Pagination برای صفحاتی که به صورت معمولی صفحه‌بندی شده‌اند، مقداری پیچیدگی سمت Frontend نیز اضافه می‌کند؛ چرا که Frontend باید در هر Request، آخرین موقعیت خوانده‌شده را نیز ارسال کند.

اما روش مرسوم و بهتر از ارسال مستقیم Id این است که از سمت Backend یک Cursor برای Frontend ارسال شود.

معمولاً این Cursor شامل فیلدهایی است که در زمان اجرای Query در دیتابیس به آن‌ها نیاز داریم. در مثال ما، این فیلدها شامل Id و CreatedAt هستند.

مقدار Cursor معمولاً ابتدا Serialize می‌شود و سپس به صورت Base64 برای Frontend ارسال می‌شود. Frontend نیز در Request بعدی، آن را برای Backend ارسال می‌کند.

در نتیجه، Backend با استفاده از Cursor می‌تواند تشخیص دهد که از چه نقطه‌ای باید خواندن داده‌ها را ادامه دهد.

در نهایت، برای سناریوهایی که نیاز دارید حجم زیادی از داده‌ها را بخوانید و نیازی به رفتن به صفحات قبلی و بعدی یا رفتن مستقیم به یک صفحه خاص ندارید، Keyset Pagination می‌تواند گزینه بسیار مناسبی باشد.

Powered by Froala Editor

نظرات