GREATEST и LEAST сравнивают несколько значений и возвращают одно: самое большое или самое маленькое.
Например:
SELECT GREATEST(10, 25, 7) AS max_value;
Результат:
max_value
---------
25
А так можно найти минимальное значение:
SELECT LEAST(10, 25, 7) AS min_value;
Результат:
min_value
---------
7
На первый взгляд это похоже на MAX и MIN, но смысл другой. MAX и MIN смотрят на много строк и считают максимум или минимум по столбцу. А GREATEST и LEAST смотрят на несколько значений внутри одной строки.
Это как сравнить несколько чисел, лежащих рядом на одной карточке: текущую цену, минимальную цену, максимальную скидку, дату регистрации, дату последнего заказа. Функции идут не «вниз по таблице», а «вбок по колонкам».
Базовый синтаксис
У обеих функций синтаксис простой:
GREATEST(value1, value2, value3)
LEAST(value1, value2, value3)
GREATEST возвращает самое большое значение из списка.
LEAST возвращает самое маленькое значение из списка.
Например:
SELECT
GREATEST(5, 10, 3) AS biggest,
LEAST(5, 10, 3) AS smallest;
Результат:
biggest | smallest
--------+---------
10 | 3
Аргументов может быть больше двух. Главное, чтобы значения можно было сравнить между собой.
Можно сравнивать числа:
SELECT GREATEST(100, 250, 180) AS result;
Можно сравнивать даты:
SELECT LEAST(DATE '2026-01-10', DATE '2026-01-03') AS first_date;
Можно сравнивать выражения:
SELECT GREATEST(amount, amount_with_bonus, 0) AS final_amount
FROM orders;
GREATEST и LEAST работают внутри одной строки
Представим таблицу заказов:
id | amount | min_amount
---+--------+-----------
1 | 500 | 100
2 | 50 | 100
3 | 1200 | 100
Если нужно взять сумму заказа, но не меньше 100, можно написать:
SELECT
id,
amount,
GREATEST(amount, 100) AS amount_with_floor
FROM orders;
Результат:
id | amount | amount_with_floor
---+--------+------------------
1 | 500 | 500
2 | 50 | 100
3 | 1200 | 1200
Что произошло?
Для каждой строки PostgreSQL сравнил amount и 100.
Если amount больше 100, он оставил amount.
Если amount меньше 100, он вернул 100.
То есть GREATEST сработал отдельно для каждой строки.
Чем GREATEST отличается от MAX
Из-за названий легко запутаться. Кажется, что GREATEST и MAX делают одно и то же: ищут максимум. Но они работают в разных направлениях.
MAX — агрегатная функция. Она смотрит на много строк и возвращает одно значение на группу или на всю таблицу.
SELECT MAX(amount) AS biggest_order
FROM orders;
Этот запрос найдёт самый большой заказ во всей таблице.
А GREATEST сравнивает значения внутри текущей строки:
SELECT
id,
GREATEST(amount, 100) AS amount_with_floor
FROM orders;
Этот запрос вернёт результат для каждой строки.
Запомнить можно так:
MAX работает вертикально — по строкам.
GREATEST работает горизонтально — по значениям одной строки.
То же самое с MIN и LEAST.
MIN ищет минимум по строкам:
SELECT MIN(amount) AS smallest_order
FROM orders;
LEAST выбирает меньшее значение внутри строки:
SELECT
id,
LEAST(amount, 1000) AS amount_with_cap
FROM orders;
Выбираем самую свежую дату
Один из самых понятных примеров — выбрать более свежую дату из двух колонок.
Допустим, у пользователя есть дата регистрации, а у заказа — дата создания. Нужно понять, какое из двух событий произошло позже.
SELECT
u.id,
GREATEST(u.created_at, o.created_at) AS last_touch
FROM users u
JOIN orders o ON o.user_id = u.id;
GREATEST сравнивает две даты и возвращает более позднюю.
Если пользователь зарегистрировался 2026-01-01, а заказ сделал 2026-01-10, результатом будет 2026-01-10.
Такой приём удобен, когда нужно найти последнюю активность из нескольких источников: регистрация, заказ, вход в аккаунт, обновление профиля.
Например:
SELECT
id,
GREATEST(created_at, last_login_at, updated_at) AS last_activity_at
FROM users;
Этот запрос вернёт самую позднюю дату из трёх колонок для каждого пользователя.
Выбираем самую раннюю дату
LEAST делает обратное: возвращает самое маленькое значение.
Для дат это означает самую раннюю дату.
SELECT
id,
LEAST(created_at, first_order_at) AS first_touch
FROM users;
Если нужно понять, когда пользователь впервые появился в вашей системе — зарегистрировался или сделал первый заказ, — LEAST помогает выбрать более раннее событие.
Ограничение снизу: нижняя граница
Частая задача: значение не должно быть меньше определённого минимума.
Например, бонусные баллы не должны уходить ниже нуля.
SELECT
id,
points,
GREATEST(points, 0) AS points_safe
FROM users;
Если points = -20, результат будет 0.
Если points = 150, результат останется 150.
То есть GREATEST(points, 0) говорит: «верни значение, но не ниже нуля».
Другой пример — минимальная сумма заказа для отчёта:
SELECT
id,
amount,
GREATEST(amount, 1) AS amount_for_report
FROM orders;
Так можно защититься от нулевых или слишком маленьких значений, если дальше они участвуют в расчётах.
Ограничение сверху: верхняя граница
LEAST удобно использовать, когда значение не должно быть больше определённого максимума.
Например, скидка не должна превышать 50%.
SELECT
id,
discount_percent,
LEAST(discount_percent, 50) AS discount_capped
FROM orders;
Если скидка была 70, результат станет 50.
Если скидка была 20, она останется 20.
То есть LEAST(discount_percent, 50) говорит: «верни значение, но не выше 50».
Ещё пример: ограничим зарплату верхней границей для отчёта.
SELECT
id,
name,
LEAST(salary, 200000) AS salary_capped
FROM employees;
Это не меняет реальные данные, а только показывает значение в отчёте с верхней отсечкой.
Как зажать значение в диапазон
Самый полезный трюк — зажать число между нижней и верхней границей. Такой приём часто называют clamp.
Например, значение должно быть не меньше 1 и не больше 1000.
Формула:
GREATEST(1, LEAST(1000, value))
Пример с заказами:
SELECT
id,
amount,
GREATEST(1, LEAST(1000, amount)) AS amount_clamped
FROM orders;
Разберём изнутри наружу.
Сначала работает LEAST:
LEAST(1000, amount)
Он срезает слишком большие значения сверху. Если amount = 1500, результат станет 1000.
Потом работает GREATEST:
GREATEST(1, ...)
Он подтягивает слишком маленькие значения снизу. Если после первого шага значение меньше 1, результат станет 1.
Получается коридор:
1 <= result <= 1000
Примеры:
amount | amount_clamped
-------+---------------
-50 | 1
500 | 500
1500 | 1000
Очень удобный приём для скидок, рейтингов, баллов, процентов и любых значений, которые должны жить в заданных рамках.
Пример: ограничиваем зарплату для отчёта
Допустим, в отчёте зарплаты нужно показывать в диапазоне от 30000 до 200000.
Слишком маленькие значения подтягиваем до 30000, слишком большие — срезаем до 200000.
SELECT
id,
name,
salary,
GREATEST(30000, LEAST(200000, salary)) AS salary_banded
FROM employees;
Такой запрос не меняет зарплату в таблице. Он просто создаёт отдельную колонку для отчёта.
Если salary = 25000, в отчёте будет 30000.
Если salary = 90000, останется 90000.
Если salary = 250000, в отчёте будет 200000.
Важно: нижняя граница должна быть меньше верхней. Если случайно перепутать и написать коридор от 200000 до 30000, формула не поймёт ваш замысел и не выдаст ошибку. Она просто посчитает то, что написано, а результат будет бессмысленным.
GREATEST и LEAST вместо громоздкого CASE
Многие задачи с GREATEST и LEAST можно решить через CASE.
Например, нижняя граница через CASE:
SELECT
id,
CASE
WHEN amount < 100 THEN 100
ELSE amount
END AS amount_with_floor
FROM orders;
То же самое через GREATEST:
SELECT
id,
GREATEST(amount, 100) AS amount_with_floor
FROM orders;
Второй вариант короче и читается проще: «возьми большее из суммы и 100».
Верхняя граница через CASE:
SELECT
id,
CASE
WHEN discount_percent > 50 THEN 50
ELSE discount_percent
END AS discount_capped
FROM orders;
То же самое через LEAST:
SELECT
id,
LEAST(discount_percent, 50) AS discount_capped
FROM orders;
CASE всё ещё нужен для сложной логики. Но когда задача сводится к «не ниже» или «не выше», GREATEST и LEAST обычно выглядят аккуратнее.
Использование в UPDATE
GREATEST и LEAST можно использовать не только в SELECT, но и в UPDATE.
Например, повысим зарплату сотрудникам отдела продаж на 10%, но гарантируем минимум 40000.
UPDATE employees
SET salary = GREATEST(40000, salary * 1.10)
WHERE dept = 'sales';
Если после повышения зарплата всё ещё меньше 40000, PostgreSQL поставит 40000.
Если зарплата стала больше 40000, останется рассчитанное значение.
Можно и наоборот: начислить бонус, но не дать ему превысить лимит.
UPDATE employees
SET bonus = LEAST(bonus + 5000, 30000)
WHERE dept = 'support';
Здесь бонус увеличивается на 5000, но итоговое значение не будет выше 30000.
Пример: дни с последней активности
Допустим, нужно посчитать, сколько дней прошло с последнего важного события пользователя: регистрации или заказа.
SELECT
u.id,
CURRENT_DATE - GREATEST(u.created_at, o.created_at)::date AS days_idle
FROM users u
JOIN orders o ON o.user_id = u.id;
GREATEST(u.created_at, o.created_at) выбирает более свежую дату.
Затем мы приводим её к дате и вычитаем из текущей даты.
Получается количество дней с последнего касания.
Такой запрос можно использовать в аналитике активности: кто давно ничего не делал, кому пора отправить письмо, кто недавно вернулся.
Главная ловушка: NULL
Самая важная тема в GREATEST и LEAST — это NULL.
NULL означает неизвестное значение. И разные СУБД обрабатывают его по-разному.
В PostgreSQL GREATEST и LEAST игнорируют NULL, если среди аргументов есть нормальные значения.
SELECT GREATEST(5, NULL, 9) AS result;
В PostgreSQL результат будет:
result
------
9
NULL как будто не участвует в сравнении.
Но если все аргументы равны NULL, результат тоже будет NULL.
SELECT GREATEST(NULL, NULL) AS result;
Результат:
result
------
NULL
На этом месте часто ломается переносимость запросов.
NULL в MySQL и ClickHouse
В MySQL поведение другое: если среди аргументов есть хотя бы один NULL, результатом будет NULL.
SELECT GREATEST(5, NULL, 9) AS result;
В MySQL такой запрос вернёт NULL.
То есть один NULL «отравляет» всё выражение.
В ClickHouse поведение зависит от версии и настроек, поэтому при переносе запроса лучше не полагаться на память. Самый надёжный подход — явно обработать NULL через COALESCE.
Особенно важно помнить это, если вы переносите запросы из PostgreSQL в MySQL или ClickHouse. В PostgreSQL выражение могло годами спокойно работать, а в другой базе внезапно начать возвращать NULL.
Как безопасно обработать NULL через COALESCE
Если значение может быть NULL, лучше заранее решить, чем его заменить.
Например, сумма заказа может быть неизвестна. Для сравнения с нулём можно заменить NULL на 0.
SELECT
id,
GREATEST(COALESCE(amount, 0), 0) AS amount_non_negative
FROM orders;
Разберём:
COALESCE(amount, 0)
Если amount не NULL, вернётся amount.
Если amount равен NULL, вернётся 0.
Дальше GREATEST(..., 0) гарантирует, что значение не будет отрицательным.
Такой код читается явно: мы сами сказали, как обрабатывать неизвестные значения.
Для дат можно выбрать запасную дату:
SELECT
id,
GREATEST(
COALESCE(last_login_at, DATE '1900-01-01'),
COALESCE(updated_at, DATE '1900-01-01')
) AS last_activity_at
FROM users;
Здесь DATE '1900-01-01' используется как очень старая дата, чтобы NULL не мешал выбрать реальную свежую дату.
Но запасное значение нужно выбирать осознанно. Не существует универсального правильного значения для всех случаев.
Типы должны быть сравнимы
GREATEST и LEAST сравнивают значения. Значит, PostgreSQL должен понимать, как эти значения привести к общему типу.
Так нормально:
SELECT GREATEST(10, 20, 30) AS result;
Все аргументы — числа.
Так тоже нормально:
SELECT GREATEST(DATE '2026-01-01', DATE '2026-02-01') AS result;
Оба аргумента — даты.
А вот смешивать несовместимые вещи опасно:
SELECT GREATEST(10, 'abc') AS result;
PostgreSQL не сможет честно сравнить число и текстовое значение abc как однотипные данные.
Если вы сравниваете разные типы, лучше привести их явно.
Например, если сумма хранится как текст, сначала приведите её к числу:
SELECT
id,
GREATEST(amount_text::numeric, 0) AS amount_safe
FROM orders;
Но лучше, конечно, хранить числа в числовых типах, а даты — в типах даты и времени. Тогда запросы проще и надёжнее.
Сравнение текста
GREATEST и LEAST могут сравнивать не только числа и даты, но и строки.
SELECT
GREATEST('apple', 'banana', 'orange') AS greatest_word,
LEAST('apple', 'banana', 'orange') AS least_word;
Результат зависит от правил сортировки строк в базе. Обычно строки сравниваются лексикографически, то есть примерно как в словаре, но с учётом настроек collation.
Для новичка главное понимать: «больше» для текста — это не «длиннее». Это порядок сортировки.
Например, слово banana может быть «больше» слова apple, потому что при сортировке оно идёт позже.
Для бизнес-логики строки через GREATEST и LEAST используют реже, чем числа и даты, но такая возможность есть.
Практический пример: цена в допустимых границах
Допустим, у товара есть рассчитанная цена, но бизнес требует:
- цена не ниже
100;
- цена не выше
10000.
SELECT
id,
raw_price,
GREATEST(100, LEAST(10000, raw_price)) AS final_price
FROM products;
Примеры результата:
raw_price | final_price
----------+------------
50 | 100
1500 | 1500
12000 | 10000
Так можно быстро привести значения к допустимому диапазону перед показом в отчёте или витрине.
Практический пример: безопасная скидка
Допустим, скидка рассчитывается формулой, но не должна быть меньше 0 и больше 70.
SELECT
id,
calculated_discount,
GREATEST(0, LEAST(70, calculated_discount)) AS discount_percent
FROM orders;
Если формула дала -5, в отчёте будет 0.
Если формула дала 40, останется 40.
Если формула дала 120, будет 70.
Это намного короче, чем большой CASE, и хорошо читается.
Практический пример: последняя дата из нескольких колонок
В таблице пользователей могут быть разные даты активности:
created_at
last_login_at
last_order_at
profile_updated_at
Нужно найти последнюю активность.
SELECT
id,
GREATEST(
created_at,
last_login_at,
last_order_at,
profile_updated_at
) AS last_activity_at
FROM users;
В PostgreSQL NULL в отдельных колонках будет проигнорирован, если есть хотя бы одна не NULL дата.
Но если такой запрос должен одинаково работать в разных СУБД, лучше обработать NULL явно:
SELECT
id,
GREATEST(
COALESCE(created_at, TIMESTAMP '1900-01-01 00:00:00'),
COALESCE(last_login_at, TIMESTAMP '1900-01-01 00:00:00'),
COALESCE(last_order_at, TIMESTAMP '1900-01-01 00:00:00'),
COALESCE(profile_updated_at, TIMESTAMP '1900-01-01 00:00:00')
) AS last_activity_at
FROM users;
Запрос длиннее, зато поведение становится предсказуемым.
Практический пример: минимум из нескольких оценок
Допустим, у ученика есть несколько оценок по модулям, и нужно найти самый слабый результат.
SELECT
id,
LEAST(sql_score, api_score, theory_score) AS weakest_score
FROM students;
Если нужно защититься от NULL, можно заменить неизвестные оценки на нейтральное значение. Но здесь важно выбрать правильную логику.
Если NULL означает «модуль ещё не пройден», возможно, его нельзя заменять на 100. Иначе ученик будет выглядеть лучше, чем есть.
Иногда лучше явно показать, что данных не хватает. Поэтому с COALESCE не нужно торопиться: сначала решите, что означает NULL в вашей предметной области.
Частые ошибки
Первая ошибка — путать GREATEST с MAX.
SELECT MAX(amount)
FROM orders;
Это максимум по строкам таблицы.
SELECT GREATEST(amount, 100)
FROM orders;
Это сравнение внутри каждой строки.
Вторая ошибка — забывать про NULL.
В PostgreSQL NULL игнорируется, если есть другие значения. В MySQL результат может стать NULL. Поэтому для переносимых запросов лучше использовать COALESCE.
Третья ошибка — перепутать порядок в формуле диапазона.
Правильный вариант:
SELECT GREATEST(1, LEAST(1000, amount)) AS amount_clamped
FROM orders;
Сначала срезаем сверху через LEAST, потом подтягиваем снизу через GREATEST.
Четвёртая ошибка — сравнивать несовместимые типы.
SELECT GREATEST(created_at, amount) AS result
FROM orders;
Дата и сумма — разные сущности. Даже если базу удастся заставить что-то привести, смысл такого сравнения почти наверняка будет плохим.
Когда использовать GREATEST и LEAST
Используйте GREATEST, когда нужно выбрать большее значение внутри строки:
SELECT GREATEST(amount, 0) AS amount_non_negative
FROM orders;
Используйте LEAST, когда нужно выбрать меньшее значение внутри строки:
SELECT LEAST(discount_percent, 50) AS discount_capped
FROM orders;
Используйте их вместе, когда нужно зажать значение в диапазон:
SELECT GREATEST(0, LEAST(100, score)) AS score_safe
FROM exam_results;
Используйте их для дат, когда нужно выбрать самое раннее или самое позднее событие из нескольких колонок:
SELECT GREATEST(created_at, updated_at) AS last_change_at
FROM users;
Главное из статьи
GREATEST и LEAST сравнивают несколько значений внутри одной строки.
GREATEST возвращает самое большое значение:
SELECT GREATEST(5, 10, 3) AS result;
LEAST возвращает самое маленькое значение:
SELECT LEAST(5, 10, 3) AS result;
Это не агрегатные функции. Они не считают максимум или минимум по всей таблице. Для этого есть MAX и MIN.
Главный практический приём — ограничить значение диапазоном:
SELECT GREATEST(1, LEAST(1000, amount)) AS amount_clamped
FROM orders;
GREATEST удобно использовать для нижней границы, LEAST — для верхней.
С NULL нужно быть особенно внимательным. В PostgreSQL NULL среди аргументов обычно игнорируется, если есть другие значения. В MySQL один NULL может сделать результат NULL. Поэтому для переносимых запросов лучше явно использовать COALESCE.
Если запомнить одну мысль, пусть будет такая: MAX и MIN смотрят вниз по строкам, а GREATEST и LEAST смотрят вбок по значениям одной строки. Именно поэтому они так хороши для дат, скидок, лимитов, диапазонов и аккуратной бизнес-логики прямо в SQL.
GREATESTиLEASTсравнивают несколько значений и возвращают одно: самое большое или самое маленькое.Например:
SELECT GREATEST(10, 25, 7) AS max_value;Результат:
А так можно найти минимальное значение:
SELECT LEAST(10, 25, 7) AS min_value;Результат:
На первый взгляд это похоже на
MAXиMIN, но смысл другой.MAXиMINсмотрят на много строк и считают максимум или минимум по столбцу. АGREATESTиLEASTсмотрят на несколько значений внутри одной строки.Это как сравнить несколько чисел, лежащих рядом на одной карточке: текущую цену, минимальную цену, максимальную скидку, дату регистрации, дату последнего заказа. Функции идут не «вниз по таблице», а «вбок по колонкам».
Базовый синтаксис
У обеих функций синтаксис простой:
GREATESTвозвращает самое большое значение из списка.LEASTвозвращает самое маленькое значение из списка.Например:
SELECT GREATEST(5, 10, 3) AS biggest, LEAST(5, 10, 3) AS smallest;Результат:
Аргументов может быть больше двух. Главное, чтобы значения можно было сравнить между собой.
Можно сравнивать числа:
SELECT GREATEST(100, 250, 180) AS result;Можно сравнивать даты:
SELECT LEAST(DATE '2026-01-10', DATE '2026-01-03') AS first_date;Можно сравнивать выражения:
SELECT GREATEST(amount, amount_with_bonus, 0) AS final_amount FROM orders;GREATEST и LEAST работают внутри одной строки
Представим таблицу заказов:
Если нужно взять сумму заказа, но не меньше
100, можно написать:SELECT id, amount, GREATEST(amount, 100) AS amount_with_floor FROM orders;Результат:
Что произошло?
Для каждой строки PostgreSQL сравнил
amountи100.Если
amountбольше100, он оставилamount.Если
amountменьше100, он вернул100.То есть
GREATESTсработал отдельно для каждой строки.Чем GREATEST отличается от MAX
Из-за названий легко запутаться. Кажется, что
GREATESTиMAXделают одно и то же: ищут максимум. Но они работают в разных направлениях.MAX— агрегатная функция. Она смотрит на много строк и возвращает одно значение на группу или на всю таблицу.SELECT MAX(amount) AS biggest_order FROM orders;Этот запрос найдёт самый большой заказ во всей таблице.
А
GREATESTсравнивает значения внутри текущей строки:SELECT id, GREATEST(amount, 100) AS amount_with_floor FROM orders;Этот запрос вернёт результат для каждой строки.
Запомнить можно так:
MAXработает вертикально — по строкам.GREATESTработает горизонтально — по значениям одной строки.То же самое с
MINиLEAST.MINищет минимум по строкам:SELECT MIN(amount) AS smallest_order FROM orders;LEASTвыбирает меньшее значение внутри строки:SELECT id, LEAST(amount, 1000) AS amount_with_cap FROM orders;Выбираем самую свежую дату
Один из самых понятных примеров — выбрать более свежую дату из двух колонок.
Допустим, у пользователя есть дата регистрации, а у заказа — дата создания. Нужно понять, какое из двух событий произошло позже.
SELECT u.id, GREATEST(u.created_at, o.created_at) AS last_touch FROM users u JOIN orders o ON o.user_id = u.id;GREATESTсравнивает две даты и возвращает более позднюю.Если пользователь зарегистрировался
2026-01-01, а заказ сделал2026-01-10, результатом будет2026-01-10.Такой приём удобен, когда нужно найти последнюю активность из нескольких источников: регистрация, заказ, вход в аккаунт, обновление профиля.
Например:
SELECT id, GREATEST(created_at, last_login_at, updated_at) AS last_activity_at FROM users;Этот запрос вернёт самую позднюю дату из трёх колонок для каждого пользователя.
Выбираем самую раннюю дату
LEASTделает обратное: возвращает самое маленькое значение.Для дат это означает самую раннюю дату.
SELECT id, LEAST(created_at, first_order_at) AS first_touch FROM users;Если нужно понять, когда пользователь впервые появился в вашей системе — зарегистрировался или сделал первый заказ, —
LEASTпомогает выбрать более раннее событие.Ограничение снизу: нижняя граница
Частая задача: значение не должно быть меньше определённого минимума.
Например, бонусные баллы не должны уходить ниже нуля.
SELECT id, points, GREATEST(points, 0) AS points_safe FROM users;Если
points = -20, результат будет0.Если
points = 150, результат останется150.То есть
GREATEST(points, 0)говорит: «верни значение, но не ниже нуля».Другой пример — минимальная сумма заказа для отчёта:
SELECT id, amount, GREATEST(amount, 1) AS amount_for_report FROM orders;Так можно защититься от нулевых или слишком маленьких значений, если дальше они участвуют в расчётах.
Ограничение сверху: верхняя граница
LEASTудобно использовать, когда значение не должно быть больше определённого максимума.Например, скидка не должна превышать
50%.SELECT id, discount_percent, LEAST(discount_percent, 50) AS discount_capped FROM orders;Если скидка была
70, результат станет50.Если скидка была
20, она останется20.То есть
LEAST(discount_percent, 50)говорит: «верни значение, но не выше50».Ещё пример: ограничим зарплату верхней границей для отчёта.
SELECT id, name, LEAST(salary, 200000) AS salary_capped FROM employees;Это не меняет реальные данные, а только показывает значение в отчёте с верхней отсечкой.
Как зажать значение в диапазон
Самый полезный трюк — зажать число между нижней и верхней границей. Такой приём часто называют
clamp.Например, значение должно быть не меньше
1и не больше1000.Формула:
GREATEST(1, LEAST(1000, value))Пример с заказами:
SELECT id, amount, GREATEST(1, LEAST(1000, amount)) AS amount_clamped FROM orders;Разберём изнутри наружу.
Сначала работает
LEAST:LEAST(1000, amount)Он срезает слишком большие значения сверху. Если
amount = 1500, результат станет1000.Потом работает
GREATEST:GREATEST(1, ...)Он подтягивает слишком маленькие значения снизу. Если после первого шага значение меньше
1, результат станет1.Получается коридор:
Примеры:
Очень удобный приём для скидок, рейтингов, баллов, процентов и любых значений, которые должны жить в заданных рамках.
Пример: ограничиваем зарплату для отчёта
Допустим, в отчёте зарплаты нужно показывать в диапазоне от
30000до200000.Слишком маленькие значения подтягиваем до
30000, слишком большие — срезаем до200000.SELECT id, name, salary, GREATEST(30000, LEAST(200000, salary)) AS salary_banded FROM employees;Такой запрос не меняет зарплату в таблице. Он просто создаёт отдельную колонку для отчёта.
Если
salary = 25000, в отчёте будет30000.Если
salary = 90000, останется90000.Если
salary = 250000, в отчёте будет200000.Важно: нижняя граница должна быть меньше верхней. Если случайно перепутать и написать коридор от
200000до30000, формула не поймёт ваш замысел и не выдаст ошибку. Она просто посчитает то, что написано, а результат будет бессмысленным.GREATEST и LEAST вместо громоздкого CASE
Многие задачи с
GREATESTиLEASTможно решить черезCASE.Например, нижняя граница через
CASE:SELECT id, CASE WHEN amount < 100 THEN 100 ELSE amount END AS amount_with_floor FROM orders;То же самое через
GREATEST:SELECT id, GREATEST(amount, 100) AS amount_with_floor FROM orders;Второй вариант короче и читается проще: «возьми большее из суммы и
100».Верхняя граница через
CASE:SELECT id, CASE WHEN discount_percent > 50 THEN 50 ELSE discount_percent END AS discount_capped FROM orders;То же самое через
LEAST:SELECT id, LEAST(discount_percent, 50) AS discount_capped FROM orders;CASEвсё ещё нужен для сложной логики. Но когда задача сводится к «не ниже» или «не выше»,GREATESTиLEASTобычно выглядят аккуратнее.Использование в UPDATE
GREATESTиLEASTможно использовать не только вSELECT, но и вUPDATE.Например, повысим зарплату сотрудникам отдела продаж на
10%, но гарантируем минимум40000.UPDATE employees SET salary = GREATEST(40000, salary * 1.10) WHERE dept = 'sales';Если после повышения зарплата всё ещё меньше
40000, PostgreSQL поставит40000.Если зарплата стала больше
40000, останется рассчитанное значение.Можно и наоборот: начислить бонус, но не дать ему превысить лимит.
UPDATE employees SET bonus = LEAST(bonus + 5000, 30000) WHERE dept = 'support';Здесь бонус увеличивается на
5000, но итоговое значение не будет выше30000.Пример: дни с последней активности
Допустим, нужно посчитать, сколько дней прошло с последнего важного события пользователя: регистрации или заказа.
SELECT u.id, CURRENT_DATE - GREATEST(u.created_at, o.created_at)::date AS days_idle FROM users u JOIN orders o ON o.user_id = u.id;GREATEST(u.created_at, o.created_at)выбирает более свежую дату.Затем мы приводим её к дате и вычитаем из текущей даты.
Получается количество дней с последнего касания.
Такой запрос можно использовать в аналитике активности: кто давно ничего не делал, кому пора отправить письмо, кто недавно вернулся.
Главная ловушка: NULL
Самая важная тема в
GREATESTиLEAST— этоNULL.NULLозначает неизвестное значение. И разные СУБД обрабатывают его по-разному.В PostgreSQL
GREATESTиLEASTигнорируютNULL, если среди аргументов есть нормальные значения.SELECT GREATEST(5, NULL, 9) AS result;В PostgreSQL результат будет:
NULLкак будто не участвует в сравнении.Но если все аргументы равны
NULL, результат тоже будетNULL.SELECT GREATEST(NULL, NULL) AS result;Результат:
На этом месте часто ломается переносимость запросов.
NULL в MySQL и ClickHouse
В MySQL поведение другое: если среди аргументов есть хотя бы один
NULL, результатом будетNULL.SELECT GREATEST(5, NULL, 9) AS result;В MySQL такой запрос вернёт
NULL.То есть один
NULL«отравляет» всё выражение.В ClickHouse поведение зависит от версии и настроек, поэтому при переносе запроса лучше не полагаться на память. Самый надёжный подход — явно обработать
NULLчерезCOALESCE.Особенно важно помнить это, если вы переносите запросы из PostgreSQL в MySQL или ClickHouse. В PostgreSQL выражение могло годами спокойно работать, а в другой базе внезапно начать возвращать
NULL.Как безопасно обработать NULL через COALESCE
Если значение может быть
NULL, лучше заранее решить, чем его заменить.Например, сумма заказа может быть неизвестна. Для сравнения с нулём можно заменить
NULLна0.SELECT id, GREATEST(COALESCE(amount, 0), 0) AS amount_non_negative FROM orders;Разберём:
COALESCE(amount, 0)Если
amountнеNULL, вернётсяamount.Если
amountравенNULL, вернётся0.Дальше
GREATEST(..., 0)гарантирует, что значение не будет отрицательным.Такой код читается явно: мы сами сказали, как обрабатывать неизвестные значения.
Для дат можно выбрать запасную дату:
SELECT id, GREATEST( COALESCE(last_login_at, DATE '1900-01-01'), COALESCE(updated_at, DATE '1900-01-01') ) AS last_activity_at FROM users;Здесь
DATE '1900-01-01'используется как очень старая дата, чтобыNULLне мешал выбрать реальную свежую дату.Но запасное значение нужно выбирать осознанно. Не существует универсального правильного значения для всех случаев.
Типы должны быть сравнимы
GREATESTиLEASTсравнивают значения. Значит, PostgreSQL должен понимать, как эти значения привести к общему типу.Так нормально:
SELECT GREATEST(10, 20, 30) AS result;Все аргументы — числа.
Так тоже нормально:
SELECT GREATEST(DATE '2026-01-01', DATE '2026-02-01') AS result;Оба аргумента — даты.
А вот смешивать несовместимые вещи опасно:
SELECT GREATEST(10, 'abc') AS result;PostgreSQL не сможет честно сравнить число и текстовое значение
abcкак однотипные данные.Если вы сравниваете разные типы, лучше привести их явно.
Например, если сумма хранится как текст, сначала приведите её к числу:
SELECT id, GREATEST(amount_text::numeric, 0) AS amount_safe FROM orders;Но лучше, конечно, хранить числа в числовых типах, а даты — в типах даты и времени. Тогда запросы проще и надёжнее.
Сравнение текста
GREATESTиLEASTмогут сравнивать не только числа и даты, но и строки.SELECT GREATEST('apple', 'banana', 'orange') AS greatest_word, LEAST('apple', 'banana', 'orange') AS least_word;Результат зависит от правил сортировки строк в базе. Обычно строки сравниваются лексикографически, то есть примерно как в словаре, но с учётом настроек collation.
Для новичка главное понимать: «больше» для текста — это не «длиннее». Это порядок сортировки.
Например, слово
bananaможет быть «больше» словаapple, потому что при сортировке оно идёт позже.Для бизнес-логики строки через
GREATESTиLEASTиспользуют реже, чем числа и даты, но такая возможность есть.Практический пример: цена в допустимых границах
Допустим, у товара есть рассчитанная цена, но бизнес требует:
100;10000.SELECT id, raw_price, GREATEST(100, LEAST(10000, raw_price)) AS final_price FROM products;Примеры результата:
Так можно быстро привести значения к допустимому диапазону перед показом в отчёте или витрине.
Практический пример: безопасная скидка
Допустим, скидка рассчитывается формулой, но не должна быть меньше
0и больше70.SELECT id, calculated_discount, GREATEST(0, LEAST(70, calculated_discount)) AS discount_percent FROM orders;Если формула дала
-5, в отчёте будет0.Если формула дала
40, останется40.Если формула дала
120, будет70.Это намного короче, чем большой
CASE, и хорошо читается.Практический пример: последняя дата из нескольких колонок
В таблице пользователей могут быть разные даты активности:
Нужно найти последнюю активность.
SELECT id, GREATEST( created_at, last_login_at, last_order_at, profile_updated_at ) AS last_activity_at FROM users;В PostgreSQL
NULLв отдельных колонках будет проигнорирован, если есть хотя бы одна неNULLдата.Но если такой запрос должен одинаково работать в разных СУБД, лучше обработать
NULLявно:SELECT id, GREATEST( COALESCE(created_at, TIMESTAMP '1900-01-01 00:00:00'), COALESCE(last_login_at, TIMESTAMP '1900-01-01 00:00:00'), COALESCE(last_order_at, TIMESTAMP '1900-01-01 00:00:00'), COALESCE(profile_updated_at, TIMESTAMP '1900-01-01 00:00:00') ) AS last_activity_at FROM users;Запрос длиннее, зато поведение становится предсказуемым.
Практический пример: минимум из нескольких оценок
Допустим, у ученика есть несколько оценок по модулям, и нужно найти самый слабый результат.
SELECT id, LEAST(sql_score, api_score, theory_score) AS weakest_score FROM students;Если нужно защититься от
NULL, можно заменить неизвестные оценки на нейтральное значение. Но здесь важно выбрать правильную логику.Если
NULLозначает «модуль ещё не пройден», возможно, его нельзя заменять на100. Иначе ученик будет выглядеть лучше, чем есть.Иногда лучше явно показать, что данных не хватает. Поэтому с
COALESCEне нужно торопиться: сначала решите, что означаетNULLв вашей предметной области.Частые ошибки
Первая ошибка — путать
GREATESTсMAX.SELECT MAX(amount) FROM orders;Это максимум по строкам таблицы.
SELECT GREATEST(amount, 100) FROM orders;Это сравнение внутри каждой строки.
Вторая ошибка — забывать про
NULL.В PostgreSQL
NULLигнорируется, если есть другие значения. В MySQL результат может статьNULL. Поэтому для переносимых запросов лучше использоватьCOALESCE.Третья ошибка — перепутать порядок в формуле диапазона.
Правильный вариант:
SELECT GREATEST(1, LEAST(1000, amount)) AS amount_clamped FROM orders;Сначала срезаем сверху через
LEAST, потом подтягиваем снизу черезGREATEST.Четвёртая ошибка — сравнивать несовместимые типы.
SELECT GREATEST(created_at, amount) AS result FROM orders;Дата и сумма — разные сущности. Даже если базу удастся заставить что-то привести, смысл такого сравнения почти наверняка будет плохим.
Когда использовать GREATEST и LEAST
Используйте
GREATEST, когда нужно выбрать большее значение внутри строки:SELECT GREATEST(amount, 0) AS amount_non_negative FROM orders;Используйте
LEAST, когда нужно выбрать меньшее значение внутри строки:SELECT LEAST(discount_percent, 50) AS discount_capped FROM orders;Используйте их вместе, когда нужно зажать значение в диапазон:
SELECT GREATEST(0, LEAST(100, score)) AS score_safe FROM exam_results;Используйте их для дат, когда нужно выбрать самое раннее или самое позднее событие из нескольких колонок:
SELECT GREATEST(created_at, updated_at) AS last_change_at FROM users;Главное из статьи
GREATESTиLEASTсравнивают несколько значений внутри одной строки.GREATESTвозвращает самое большое значение:SELECT GREATEST(5, 10, 3) AS result;LEASTвозвращает самое маленькое значение:SELECT LEAST(5, 10, 3) AS result;Это не агрегатные функции. Они не считают максимум или минимум по всей таблице. Для этого есть
MAXиMIN.Главный практический приём — ограничить значение диапазоном:
SELECT GREATEST(1, LEAST(1000, amount)) AS amount_clamped FROM orders;GREATESTудобно использовать для нижней границы,LEAST— для верхней.С
NULLнужно быть особенно внимательным. В PostgreSQLNULLсреди аргументов обычно игнорируется, если есть другие значения. В MySQL одинNULLможет сделать результатNULL. Поэтому для переносимых запросов лучше явно использоватьCOALESCE.Если запомнить одну мысль, пусть будет такая:
MAXиMINсмотрят вниз по строкам, аGREATESTиLEASTсмотрят вбок по значениям одной строки. Именно поэтому они так хороши для дат, скидок, лимитов, диапазонов и аккуратной бизнес-логики прямо в SQL.