sqlpostgresqlmysqlfunctions

GREATEST и LEAST в PostgreSQL: как выбрать большее или меньшее значение внутри строки

GREATEST и LEAST возвращают максимум и минимум столбцов внутри одной строки, зажимают число в диапазон через вложенный вызов и по-разному обрабатывают NULL в PostgreSQL, MySQL и ClickHouse.

10 мин чтенияСправочникsql · postgresql · mysql · functions · null

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.

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

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

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