sqlpostgresqlmysqlfunctions

GREATEST and LEAST in SQL: Row-Wise Max and Min Across Columns

How GREATEST and LEAST return the max and min within a single row, clamp a value to a range, and differ in NULL handling across PostgreSQL, MySQL, and ClickHouse.

3 min qariReferencesql · postgresql · mysql · functions · null
Dan l-artiklu bħalissa huwa bir-Russu — it-traduzzjoni bl-Ingliż għaddejja.

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

Сигнатура у обеих функций одинаковая: на вход идёт список из двух и более выражений сопоставимого типа, на выходе — одно значение того же типа. GREATEST отдаёт максимум списка, LEAST — минимум. Аргументы можно мешать: столбцы, литералы, результаты других вызовов, поэтому функции легко вкладываются друг в друга.

Главное, на что стоит смотреть заранее, — это типы и NULL. Типы должны быть сравнимы между собой, иначе PostgreSQL приведёт их к общему типу или откажется выполнять запрос; а NULL в списке аргументов в разных СУБД обрабатывается по-разному, и именно на этом чаще всего ломается перенос запроса. Оба момента разберём ниже на конкретных примерах.

Чем это отличается от MAX и MIN

Путаница рождается из-за слов: «максимум» он и есть «максимум». Но направление работы у функций разное. MAX и MIN — агрегаты, они идут вертикально, вниз по строкам группы, и выдают одно число на всю выборку. GREATEST и LEAST идут горизонтально, по аргументам одной строки, и выдают значение на каждую строку.

-- Aggregate: one number per group, scans many rows
SELECT MAX(amount) AS biggest_order
FROM orders;

-- Row-wise: one value per row, compares columns side by side
SELECT id, GREATEST(amount, 10) AS amount_floor_10
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;

Зажать значение в диапазон (clamp)

Самый ходовой трюк с этими функциями — загнать число в коридор между нижней и верхней границей. Достигается это вложением одного вызова в другой: GREATEST(lo, LEAST(hi, x)) гарантирует, что результат не выскочит за пределы [lo, hi]. Читается формула изнутри наружу: сначала срезаем сверху, потом подтягиваем снизу.

-- Clamp the order amount into the range [1, 1000]
SELECT
    id,
    amount,
    GREATEST(1, LEAST(1000, amount)) AS amount_clamped
FROM orders;

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

-- Cap every salary at 200000 but never below 30000
SELECT
    id,
    name,
    GREATEST(30000, LEAST(200000, salary)) AS salary_banded
FROM employees;

Разберём, кто за что отвечает:

  • внешний GREATEST(lo, ...) подтягивает слишком маленькие значения вверх до lo;
  • внутренний LEAST(hi, x) опускает слишком большие значения вниз до hi;
  • порядок границ критичен: если случайно задать lo > hi, коридор схлопывается в противоречие, и на выходе вы получите бессмыслицу, причём молча, без ошибки.

Обработка NULL: главная мина

А вот тут начинается самое коварное. Поведение с NULL у разных движков расходится, и именно на этом ломается код при переезде с одной СУБД на другую.

  • PostgreSQL: аргументы NULL просто выкидываются из сравнения. GREATEST(5, NULL, 9) вернёт 9. И только когда все аргументы до единого NULL, результат тоже станет NULL.
  • MySQL: достаточно одного NULL среди аргументов, чтобы весь результат обнулился. GREATEST(5, NULL, 9) в MySQL вернёт NULL — один NULL отравляет всё выражение.
  • ClickHouse: по семантике ближе к MySQL — если хотя бы один аргумент типа Nullable оказался NULL, результат тоже NULL. На «постгресовый» пропуск NULL здесь полагаться нельзя, так что лучше не запоминать ClickHouse как копию PostgreSQL, а явно прикрывать аргументы.
-- PostgreSQL: returns 9, NULL is ignored
-- MySQL: returns NULL, one NULL poisons the result
SELECT GREATEST(5, NULL, 9) AS demo;

Грабли: не закладывайтесь на «NULL сам собой отбросится», перенося запрос с PostgreSQL на MySQL. Логика, которая годами молча работала, после миграции внезапно начнёт выдавать NULL, и поймать это глазами в большом запросе почти невозможно. Если столбец может быть NULL, оберните его в COALESCE — тогда поведение становится явным и одинаковым везде.

-- Portable: define a neutral fallback before comparing
SELECT
    id,
    GREATEST(COALESCE(amount, 0), 0) AS amount_nonneg
FROM orders;

Полезные сочетания

GREATEST и LEAST хорошо ложатся не только в SELECT. Внутри UPDATE и вычисляемых выражений они избавляют от громоздких CASE.

-- Bump salary by 10% but never below the floor of 40000
UPDATE employees
SET salary = GREATEST(40000, salary * 1.10)
WHERE dept = 'sales';
-- Days since the more recent of signup or first order
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 и LEAST сравнивают значения по горизонтали, внутри строки; через вложенный вызов аккуратно зажимают число в диапазон; и требуют явной защиты через COALESCE, если один и тот же код должен одинаково вести себя и в PostgreSQL, и в MySQL. Запомните разницу в NULL один раз — и сэкономите себе вечер отладки потом.

Ipprattika fuq eżerċizzji reali

Solvi eżerċizzji fit-taħriġ tal-SQL b'valutazzjoni u għajnuniet istantanji.

Iftaħ it-taħriġ