Пагинация: как правильно поделить данные по страницам

Published on Saturday 12 July 2025

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

Пагинация

В классическом виде задача звучит так: у нас есть всего TotalCount записей в базе данных, нужно вывести их постранично по BatchSize на страницу. Нужно определить количество страниц - PageCount. Для этого нужно поделить TotalCount на BatchSize и если результат получился дробным, то округлить его вверх.

| TotalCount | BatchSize | TotalCount/BatchSize | PageCount |
|------------|-----------|----------------------|----------:|
| 90         | 10        | 9                    |         9 |
| 99         | 10        | 9.9                  |        10 |
| 100        | 10        | 10                   |        10 |
| 101        | 10        | 10.1                 |        11 |

Math.Ceiling

В C# можно воспользоваться методом Math.Ceiling - он округляет вещественное число до ближайшего целого, которое больше либо равно переданному аргументу.

var PageCount = (int)Math.Ceiling((double)TotalCount / BatchSize);

Мы получим корректное значение. Но как вы заметили, нам вначале нужно преобразовать наши целые числа TotalCount, BatchSize в вещественные, вызвать метод, а потом обратно преобразовать результат в целочисленный.

Остаток от деления

Можно заметить, если TotalCount делится нацело на BatchSize, то результатом является просто частное TotalCount/BatchSize, иначе нужно добавить 1 к результату. Преобразуем нашу формулу:

var PageCount = TotalCount / BatchSize + (TotalCount % BatchSize == 0 ? 0 : 1);

Мы избавились от преобразований типа и вызова метода, но добавился один тернарный оператор. Можно ли сделать еще проще? Оказывается, можно.

Целочисленное деление

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

var Add = (BatchSize - 1) / BatchSize;

Числитель у нас всегда меньше знаменателя, поэтому результат целочисленного деления всегда будет равен 0. Добавим это выражение к частному TotalCount / BatchSize:

var PageCount = TotalCount / BatchSize + (BatchSize - 1) / BatchSize;

По правилам целочисленной арифметики второе слагаемое всегда равно 0 и оно не влияет на сумму. Давайте внесем его под общий знаменатель:

var PageCount = (TotalCount + BatchSize - 1) / BatchSize;

Это и есть наша элегантная формула деления с округлением вверх. Почему она работает?

Давайте рассмотрим 2 случая:

  • TotalCount делится нацело на BatchSize, тогда TotalCount / BatchSize даст нужный нам PageCount, а (BatchSize - 1) / BatchSize даст 0 и не повлияет на результат.
  • TotalCount при делении на BatchSize даёт остаток R, тогда TotalCount / BatchSize даст значение PageCount - 1. И у нас остаётся выражение (R + BatchSize - 1) / BatchSize, где R по определению всегда больше 1 и меньше BatchSize. При любых значениях R результат целочисленного деления будет равен 1. Таким образом, в итоге получим значение PageCount.

Бенчмарк

Давайте ряди любопытства сравним все 3 способа.

Код бенчмарка. Нажмите, чтобы развернуть.
using BenchmarkDotNet.Attributes;

namespace Paging;

public class Benchmark
{
    [Params(99)]
    public int TotalCount;

    [Params(10)]
    public int BatchSize;

    [Params(10000)]
    public int Iterations;

    [Benchmark]
    public int MathCeiling()
    {
        var result = 0;
        for (int i = 0; i < Iterations; i++)
            result += (int)Math.Ceiling((double)TotalCount / BatchSize);
        return result;
    }

    [Benchmark]
    public int Mod()
    {
        var result = 0;
        for (int i = 0; i < Iterations; i++)
            result += TotalCount / BatchSize + (TotalCount % BatchSize == 0 ? 0 : 1);
        return result;
    }

    [Benchmark]
    public int Div()
    {
        var result = 0;
        for (int i = 0; i < Iterations; i++)
            result += (TotalCount + BatchSize - 1) / BatchSize;
        return result;
    }
}

Результаты:

BenchmarkDotNet v0.15.2, Windows 10 (10.0.19045.6093/22H2/2022Update)
AMD Ryzen 7 7840H with Radeon 780M Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.412
  [Host]     : .NET 8.0.18 (8.0.1825.31117), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  DefaultJob : .NET 8.0.18 (8.0.1825.31117), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI


| Method      | TotalCount | BatchSize | Iterations |     Mean |     Error |    StdDev |
|-------------|------------|-----------|------------|---------:|----------:|----------:|
| MathCeiling | 99         | 10        | 10000      | 2.298 us | 0.0042 us | 0.0037 us |
| Mod         | 99         | 10        | 10000      | 4.517 us | 0.0597 us | 0.0559 us |
| Div         | 99         | 10        | 10000      | 2.285 us | 0.0455 us | 0.0425 us |

Еще одно подтверждение бессмысленности этих измерений - это необходимость нескольких тысяч итераций, иначе значения неотличимы от нуля. Тем не менее удивительно, что первый и третий варианты показали практически идентичный результат.

Заключение

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

И да, важно, что данная формула работает только для положительных чисел.