sqlpostgresqlmysqlclickhouse

Что такое CEIL в SQL: округление вверх, страницы и корзины

CEIL отвечает на вопрос «сколько целых единиц нужно»; используйте его для пагинации, упаковки и верхних границ диапазонов.

10 мин чтенияСправочникsql · postgresql · mysql · clickhouse · math · pagination

CEIL округляет число вверх до ближайшего целого.

У функции есть второе имя — CEILING. В большинстве популярных СУБД они означают одно и то же.

Главная идея простая: если после деления остался хоть маленький хвостик, нужна ещё одна целая единица.

Например:

  • 101 товар по коробкам на 100 штук — это 2 коробки;
  • 26 записей по 25 на страницу — это 2 страницы;
  • 41000 зарплаты при шаге 10000 — это верхняя полка 50000.

CEIL — это не про красивое округление. Это про инженерную логику: нельзя положить половину строки на страницу, половину заказа в батч или кусочек товара в несуществующую коробку.

Что делает CEIL

CEIL(x) возвращает ближайшее целое число, которое не меньше x.

Проще говоря, функция округляет число вверх.

SELECT
  CEIL(4.1) AS a,
  CEIL(4.0) AS b,
  CEIL(-4.1) AS c,
  CEILING(7.0001) AS d;

Результат:

a b c d
5 4 -4 8

Разберём каждое значение.

CEIL(4.1) возвращает 5, потому что ближайшее целое сверху — это 5.

CEIL(4.0) возвращает 4, потому что число уже целое.

CEIL(-4.1) возвращает -4, потому что вверх на числовой оси — это в сторону большего числа. А -4 больше, чем -4.1.

CEILING(7.0001) возвращает 8, потому что даже маленькая дробная часть тянет число вверх.

CEIL и CEILING — одно и то же

В SQL можно встретить оба варианта:

SELECT CEIL(4.2) AS value;

И такой:

SELECT CEILING(4.2) AS value;

Оба запроса возвращают:

value
5

Обычно выбирают более короткий вариант CEIL, но CEILING иногда читается понятнее для новичков, потому что прямо напоминает английское слово «потолок».

FLOOR — это «пол», округление вниз.

CEILING — это «потолок», округление вверх.

CEIL всегда округляет вверх

Важно не путать CEIL с обычным округлением.

SELECT
  CEIL(4.1) AS ceil_value,
  ROUND(4.1) AS round_value;

Результат:

ceil_value round_value
5 4

ROUND(4.1) возвращает 4, потому что 4.1 ближе к 4.

А CEIL(4.1) возвращает 5, потому что ему всё равно, к чему число ближе. Он всегда идёт вверх.

Ещё пример:

SELECT
  CEIL(4.9) AS ceil_value,
  ROUND(4.9) AS round_value;

Результат:

ceil_value round_value
5 5

Здесь результаты совпали, но причина разная.

ROUND выбрал 5, потому что 4.9 ближе к 5.

CEIL выбрал 5, потому что это ближайшее целое сверху.

Отрицательные числа: вверх значит к нулю

С положительными числами всё выглядит очевидно: 4.1 округляется до 5.

Но с отрицательными значениями у новичков часто возникает путаница.

SELECT
  CEIL(-4.1) AS value;

Результат:

value
-4

Почему не -5?

Потому что CEIL идёт вверх по числовой оси, то есть к большему числу.

На числовой оси это выглядит так:

-6       -5       -4       -3       -2       -1        0
                  ^
                -4.1 goes up to -4

Число -4 больше, чем -4.1, поэтому это и есть округление вверх.

Простое правило:

для положительных чисел CEIL уходит дальше от нуля;

для отрицательных чисел CEIL идёт к нулю.

Подсчёт страниц пагинации

Самый классический пример для CEIL — пагинация.

Допустим, на странице показывается 25 заказов. Всего оплаченных заказов 126. Сколько нужно страниц?

Если просто разделить 126 на 25, получится 5.04. Но страниц не может быть 5.04. Нужна шестая страница, даже если на ней будет всего один заказ.

SELECT
  COUNT(*) AS total_orders,
  CEIL(COUNT(*)::numeric / 25) AS total_pages
FROM orders
WHERE status = 'paid';

Результат может быть таким:

total_orders total_pages
126 6

Вот в этом и сила CEIL: он превращает неполный остаток в ещё одну целую страницу.

Почему нельзя использовать ROUND для страниц

Представьте, что у нас 26 заказов и 25 заказов на страницу.

SELECT
  CEIL(26::numeric / 25) AS pages_by_ceil,
  ROUND(26::numeric / 25) AS pages_by_round;

Результат:

pages_by_ceil pages_by_round
2 1

26 / 25 — это 1.04.

ROUND округляет к ближайшему целому и получает 1.

Но это неверно для пагинации: один заказ не поместился на первую страницу, значит нужна вторая.

Для страниц, коробок, батчей и контейнеров почти всегда нужен именно CEIL, а не ROUND.

Пагинация по группам

CEIL можно использовать не только для всей таблицы, но и по группам.

Например, нужно понять, сколько страниц понадобится для пользователей из каждой страны, если показывать по 20 пользователей на страницу.

SELECT
  country,
  COUNT(*) AS users,
  CEIL(COUNT(*)::numeric / 20) AS pages
FROM users
GROUP BY country
ORDER BY users DESC;

Результат может быть таким:

country users pages
DE 83 5
ES 41 3
PL 20 1
FR 7 1

Как читать:

  • для DE нужно 5 страниц, потому что 80 пользователей поместятся на 4 страницы, а ещё 3 уйдут на пятую;
  • для PL нужна 1 страница, потому что 20 пользователей помещаются ровно;
  • для FR тоже нужна 1 страница, хотя пользователей всего 7.

Главная ловушка: целочисленное деление

В PostgreSQL есть важный нюанс: если делить целое число на целое число, результат тоже будет целым.

SELECT 5 / 2 AS value;

Результат:

value
2

Не 2.5, а 2.

Дробная часть уже потерялась. Поэтому такой запрос даст неправильный результат:

SELECT CEIL(5 / 2) AS pages;

Результат:

pages
2

А правильно должно быть 3.

Почему так произошло? Сначала SQL выполнил 5 / 2 и получил 2. Только потом применил CEIL. Но округлять уже нечего: дробная часть исчезла раньше.

Правильный вариант — привести хотя бы одно число к дробному типу:

SELECT CEIL(5::numeric / 2) AS pages;

Результат:

pages
3

Можно и так:

SELECT CEIL(5.0 / 2) AS pages;

Главная мысль: если хотите, чтобы CEIL увидел дробную часть, не теряйте её на этапе деления.

Почему эта ошибка коварная

Ошибка с целочисленным делением особенно неприятна, потому что на ровных данных всё выглядит правильно.

SELECT
  CEIL(100 / 25) AS wrong_but_looks_ok,
  CEIL(100::numeric / 25) AS correct;

Результат:

wrong_but_looks_ok correct
4 4

Оба варианта дали 4. Кажется, что всё хорошо.

Но стоит появиться неполной последней странице:

SELECT
  CEIL(101 / 25) AS wrong_result,
  CEIL(101::numeric / 25) AS correct_result;

Результат:

wrong_result correct_result
4 5

Вот здесь баг становится заметным.

Поэтому в формулах пагинации лучше сразу писать явно:

CEIL(total::numeric / per_page)

или приводить к дробному типу другой операнд.

CEIL для коробок, батчей и контейнеров

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

Например, есть товары, и в одну коробку помещается 12 штук. Нужно посчитать, сколько коробок нужно для каждого заказа.

SELECT
  order_id,
  items_count,
  CEIL(items_count::numeric / 12) AS boxes
FROM orders
ORDER BY order_id;

Результат:

order_id items_count boxes
101 12 1
102 13 2
103 24 2
104 25 3

Заказ на 13 товаров требует 2 коробки. Нельзя взять одну коробку и положить туда лишний товар «сбоку».

Вот где CEIL читается особенно естественно: любое превышение лимита требует ещё одну целую единицу.

Бакетинг: разбиение по верхней границе

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

Допустим, мы хотим сгруппировать сотрудников по зарплатным полкам шириной 10000.

SELECT
  CEIL(salary / 10000.0) * 10000 AS salary_band,
  COUNT(*) AS people
FROM employees
GROUP BY CEIL(salary / 10000.0) * 10000
ORDER BY salary_band;

Результат может быть таким:

salary_band people
40000 3
50000 8
60000 12
70000 5

Здесь salary_band — это верхняя граница диапазона.

Например:

salary salary_band
40000 40000
41000 50000
49999 50000
50000 50000
50100 60000

Зарплата 41000 попадает в полку 50000, потому что это диапазон до 50000.

Если вам нужна нижняя граница диапазона, лучше использовать FLOOR, а не CEIL.

CEIL для ценовых сегментов

Похожим способом можно разбить суммы заказов на ценовые сегменты.

SELECT
  CEIL(amount / 100.0) AS price_bucket,
  COUNT(*) AS orders
FROM orders
GROUP BY CEIL(amount / 100.0)
ORDER BY price_bucket;

Результат:

price_bucket orders
1 18
2 37
3 24
4 9

Здесь price_bucket = 1 означает суммы до 100, price_bucket = 2 — суммы выше 100 и до 200, и так далее.

Если хочется видеть не номер корзины, а верхнюю границу, можно умножить обратно на 100:

SELECT
  CEIL(amount / 100.0) * 100 AS price_limit,
  COUNT(*) AS orders
FROM orders
GROUP BY CEIL(amount / 100.0) * 100
ORDER BY price_limit;

Результат:

price_limit orders
100 18
200 37
300 24
400 9

Так отчёт становится понятнее: price_limit = 300 — это заказы, которые попали в сегмент до 300.

CEIL, FLOOR и ROUND: разница

У этих функций похожие названия, но разный смысл.

CEIL округляет вверх.

FLOOR округляет вниз.

ROUND округляет к ближайшему значению.

SELECT
  x,
  CEIL(x) AS up,
  FLOOR(x) AS down,
  ROUND(x) AS nearest
FROM (
  VALUES
    (2.1),
    (2.5),
    (2.9),
    (-2.5)
) AS t(x);

Результат:

x up down nearest
2.1 3 2 2
2.5 3 2 3
2.9 3 2 3
-2.5 -2 -3 -3

Как выбирать:

  • нужна верхняя граница — CEIL;
  • нужна нижняя граница — FLOOR;
  • нужно обычное округление — ROUND;
  • нужно просто отбросить дробную часть — TRUNC.

Почему для пагинации нужен именно CEIL

Ещё раз закрепим на бытовом примере.

У нас есть 51 пользователь. На одной странице помещается 50.

SELECT
  CEIL(51::numeric / 50) AS pages_by_ceil,
  FLOOR(51::numeric / 50) AS pages_by_floor,
  ROUND(51::numeric / 50) AS pages_by_round;

Результат:

pages_by_ceil pages_by_floor pages_by_round
2 1 1

Правильный ответ — 2 страницы.

FLOOR ошибся, потому что отбросил неполную страницу.

ROUND тоже ошибся, потому что 1.02 ближе к 1, чем к 2.

CEIL дал верный результат, потому что даже один лишний пользователь требует ещё одну страницу.

Что будет с NULL

Если передать в CEIL значение NULL, результат тоже будет NULL.

SELECT CEIL(NULL) AS value;

Результат:

value
NULL

SQL не может округлить неизвестное значение. Поэтому неизвестность остаётся неизвестностью.

В отчётах это важно. Например, если цена может быть пустой:

SELECT
  id,
  CEIL(amount / 100.0) AS price_bucket
FROM orders;

Для строк, где amount равен NULL, price_bucket тоже будет NULL.

Если по бизнес-логике пустую сумму нужно считать нулём, это надо написать явно:

SELECT
  id,
  CEIL(COALESCE(amount, 0) / 100.0) AS price_bucket
FROM orders;

Но не стоит автоматически заменять все NULL на нули. Сначала решите, что значит пустое значение в вашей задаче.

Тип результата

В PostgreSQL результат CEIL зависит от типа аргумента.

Если передать numeric, результат останется numeric.

Если передать double precision, результат останется double precision.

Выглядеть значение может как целое, но тип не всегда будет integer.

Например:

SELECT CEIL(4.2::numeric) AS value;

Результат выглядит так:

value
5

Но это не обязательно обычный integer.

Если вам нужен именно целочисленный тип, добавьте явное приведение:

SELECT CEIL(4.2)::int AS value;

Это полезно, если результат дальше используется как номер страницы, количество коробок или номер батча.

Различия между СУБД

В PostgreSQL CEIL и CEILING работают одинаково. Главная практическая ловушка — целочисленное деление: 5 / 2 даёт 2, поэтому для точного результата нужно привести число к numeric или использовать дробный литерал.

SELECT CEIL(5::numeric / 2) AS pages;

В MySQL CEIL и CEILING тоже являются синонимами. Обычный оператор / выполняет деление с дробной частью, а для целочисленного деления используется DIV. Поэтому при переносе запросов между СУБД важно смотреть не только на CEIL, но и на сам оператор деления.

В ClickHouse функция называется ceil. У неё есть расширение со вторым аргументом — точностью округления.

Например, можно округлить вверх до одного знака после запятой:

SELECT ceil(4.123, 1) AS value;

Результат:

value
4.2

Это удобно, но такой синтаксис не является универсальным для всех СУБД.

Частые ошибки с CEIL

Первая ошибка — использовать ROUND там, где нужен CEIL.

SELECT ROUND(26::numeric / 25) AS pages;

Результат:

pages
1

Для пагинации это неверно. Нужна вторая страница.

Вторая ошибка — забыть про целочисленное деление в PostgreSQL.

SELECT CEIL(26 / 25) AS pages;

Результат:

pages
1

Правильно:

SELECT CEIL(26::numeric / 25) AS pages;

Результат:

pages
2

Третья ошибка — не понимать, что происходит с отрицательными числами.

SELECT CEIL(-4.9) AS value;

Результат:

value
-4

Это не ошибка SQL. Это округление вверх по числовой оси.

Четвёртая ошибка — не определить смысл корзины.

Если вы пишете:

SELECT CEIL(amount / 100.0) * 100 AS bucket
FROM orders;

то bucket обычно означает верхнюю границу диапазона.

Если вам нужна нижняя граница, формула должна быть через FLOOR.

Когда использовать CEIL

CEIL нужен в задачах, где любой остаток требует ещё одну целую единицу.

Например:

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

Хороший мысленный тест:

Можно ли проигнорировать остаток?

Если нельзя, скорее всего, нужен CEIL.

Главное из статьи

CEIL округляет число вверх до ближайшего целого, которое не меньше исходного значения.

SELECT CEIL(4.1) AS value;

Результат:

value
5

CEIL и CEILING — синонимы.

SELECT CEILING(4.1) AS value;

Результат:

value
5

Для отрицательных чисел округление вверх идёт в сторону нуля:

SELECT CEIL(-4.1) AS value;

Результат:

value
-4

Для пагинации CEIL подходит лучше, чем ROUND и FLOOR, потому что даже одна лишняя строка требует ещё одну страницу.

SELECT CEIL(26::numeric / 25) AS pages;

Результат:

pages
2

В PostgreSQL важно не потерять дробную часть из-за целочисленного деления. Поэтому в формулах вроде пагинации приводите число к дробному типу:

SELECT CEIL(total_count::numeric / per_page) AS pages
FROM report_stats;

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

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр