День 2278. #ЗаметкиНаПолях
Разбираем Курсорную Пагинацию. Окончание
Начало
Продолжение
Улучшаем курсорную пагинацию
Для ускорения пагинации можно добавить индекс:
CREATE INDEX idx_user_notes_date_id ON user_notes (date DESC, id DESC);
Индекс создаётся в обратном порядке, чтобы соответствовать порядку в запросах. Однако, если выполнить план запроса, мы увидим, что индекс используется, но запрос может выполняться даже дольше, чем без него:
EXPLAIN ANALYZE SELECT u.id, u.date, u.note, u.user_id
FROM user_notes AS u
WHERE u.date < @date OR (u.date = @date AND u.id <= @lastId)
ORDER BY u.date DESC, u.id DESC
LIMIT 1000;
Возможно, размер данных слишком мал, чтобы получить преимущество от индекса. Но мы можем использовать один трюк – сравнение кортежей:
SELECT u.id, u.date, u.note, u.user_id
FROM user_notes AS u
WHERE (u.date, u.id) <= (@date, @lastId)
ORDER BY u.date DESC, u.id DESC
LIMIT 1000;
В этом случае индекс сработает, серьёзно ускорив выполнение. Оптимизатор запросов не может определить, можно ли использовать составной индекс для сравнения на уровне строк. Однако индекс эффективно используется при сравнении кортежей.
У провайдера EF для Postgres есть метод EF.Functions.LessThanOrEqual, который принимает ValueTuple в качестве аргумента. Мы можем использовать его для создания сравнения кортежей:
query = query.Where(x => EF.Functions.LessThanOrEqual(
ValueTuple.Create(x.Date, x.Id),
ValueTuple.Create(date, lastId)));
Кодирование курсора
Мы можем закодировать курсор, используемый для извлечения следующего набора результатов. Клиенты получат курсор в виде строки Base64. Им не нужно знать внутреннюю структуру курсора:
using System.Buffers.Text;
using System.Text;
using System.Text.Json;
public record Cursor(DateOnly Date, Guid LastId)
{
public string Encode() =>
Base64Url.EncodeToString(
Encoding.UTF8.GetBytes(
JsonSerializer.Serialize(this)));
public static Cursor? Decode(string? cursor)
{
if (string.IsNullOrWhiteSpace(cursor))
return null;
try
{
return JsonSerializer.Deserialize<Cursor>(
Encoding.UTF8.GetString(
Base64Url.DecodeFromChars(cursor)));
}
catch
{
return null;
}
}
}
Вот пример использования:
var cursor = new Cursor(new DateOnly(2025, 4, 26),
Guid.Parse("019500f9-8b41-74cf-ab12-25a48d4d4ab4"));
var encoded = cursor.Encode();
// eyJEYXRlIjoiMjAyNS0wMi0xNSIsIkxhc3RJZCI6IjAxOTUwMGY5LThiNDEtNzRjZi1hYjEyLTI1YTQ4ZDRkNGFiNCJ9
var decoded = Cursor.Decode(encoded);
Источник: https://www.milanjovanovic.tech/blog/understanding-cursor-pagination-and-why-its-so-fast-deep-dive
>>Click here to continue<<