Поделиться через


Разбиение на страницы

Разбивка на страницы ссылается на получение результатов в страницах, а не все одновременно; Обычно это делается для больших наборов результатов, где отображается пользовательский интерфейс, позволяющий пользователю перейти к следующей или предыдущей странице результатов.

Предупреждение

Независимо от используемого метода разбиения на страницы всегда убедитесь, что порядок полностью уникален. Например, если результаты упорядочены только по дате, но может быть несколько результатов с одной и той же датой, то результаты могут быть пропущены при пагинации, если они упорядочены по-разному в двух запросах на пагинацию. Упорядочивание по дате и идентификатору (или любому другому уникальному свойству или сочетанию свойств) делает порядок полностью уникальным и помогает избежать этой проблемы. Обратите внимание, что по умолчанию в реляционных базах данных не применяется упорядочение, даже по первичному ключу.

Примечание.

Azure Cosmos DB имеет собственный механизм для разбиения на страницы, см. выделенную страницу документации.

Постраничная нумерация с смещением

Распространенный способ реализации разбиения на страницы с базами данных — использовать SkipTake операторы LINQ (OFFSET и LIMIT в SQL). Учитывая размер страницы 10 результатов, третью страницу можно получить с помощью EF Core следующим образом:

var position = 20;
var nextPage = await context.Posts
    .OrderBy(b => b.PostId)
    .Skip(position)
    .Take(10)
    .ToListAsync();

К сожалению, в то время как этот метод очень интуитивно понятен, он также имеет некоторые серьезные недостатки:

  1. База данных по-прежнему должна обрабатывать первые 20 записей, даже если они не возвращаются в приложение; это создает, возможно, значительную нагрузку вычислений, которая увеличивается с числом пропущенных строк.
  2. Если какие-либо обновления происходят одновременно, ваша разбивка на страницы может в конечном итоге пропустить определенные записи или показать их дважды. Например, если запись удаляется по мере перемещения пользователя с страницы 2 на 3, весь набор результатов "сдвигается вверх", а одна запись будет пропущена.

Разбиение на страницы набора ключей

Рекомендуемой альтернативой разбивки на страницы с использованием смещения, иногда называемой разбивкой по ключу или разбивкой по поиску, является использование WHERE для пропуска строк вместо смещения. Это означает, что необходимо запомнить соответствующие значения из последней полученной записи (вместо использования смещения) и запрашивать следующие строки после нее. Например, при условии, что последняя запись на последней странице, которую мы извлекли, имела значение идентификатора 55, мы просто сделали следующее:

var lastId = 55;
var nextPage = await context.Posts
    .OrderBy(b => b.PostId)
    .Where(b => b.PostId > lastId)
    .Take(10)
    .ToListAsync();

Предполагая, что индекс определен на PostId, этот запрос очень эффективен и не чувствителен к параллельным изменениям, происходящим в меньших значениях идентификаторов.

Пагинация по набору ключей подходит для интерфейсов пагинации, где пользователь может перемещаться вперед и назад, но не поддерживает случайный доступ, когда пользователь может перейти на любую конкретную страницу. Для пагинации произвольного доступа требуется использование пагинации с использованием смещения, как описано выше; из-за недостатков такой пагинации тщательно подумайте, действительно ли для вашего случая использования требуется пагинация произвольного доступа, или же перемещение на следующую/предыдущую страницу достаточно. Если требуется пагинация с произвольным доступом, надежная реализация может использовать пагинацию по набору ключей при переходе на следующую/предыдущую страницу и навигацию по смещению при переходе на любую другую страницу.

Несколько ключей разбиения на страницы

При использовании пагинации с использованием наборов ключей часто необходимо упорядочивать по нескольким свойствам. Например, следующий запрос выполняет постраничное разбиение, отсортированное по дате и идентификатору.

var lastDate = new DateTime(2020, 1, 1);
var lastId = 55;
var nextPage = await context.Posts
    .OrderBy(b => b.Date)
    .ThenBy(b => b.PostId)
    .Where(b => b.Date > lastDate || (b.Date == lastDate && b.PostId > lastId))
    .Take(10)
    .ToListAsync();

Это гарантирует, что следующая страница начинается точно там, где закончилась предыдущая. При добавлении дополнительных ключей упорядочивания можно добавить дополнительные условия.

Примечание.

Большинство баз данных SQL поддерживают более простую и эффективную версию приведенного выше, с использованием значений строк: WHERE (Date, Id) > (@lastDate, @lastId). EF Core в настоящее время не поддерживает выражение этого в запросах LINQ, это отслеживается #26822.

Индексы

Как и в случае с любым другим запросом, правильное индексирование жизненно важно для хорошей производительности: убедитесь, что у вас есть индексы, соответствующие порядку разбиения на страницы. При упорядочении по нескольким столбцам можно определить индекс по нескольким столбцам; это называется составным индексом.

Дополнительные сведения см. на странице документации по индексам.

Дополнительные ресурсы