Ez a cikk jelenleg oroszul van — az angol fordítás folyamatban van.
GREATEST и LEAST берут список аргументов и возвращают самый большой или самый маленький из них в пределах одной строки. Их используют, когда нужно сравнить не строки между собой, а несколько столбцов или выражений внутри текущей строки: например, выбрать самую свежую из двух дат, поставить нижнюю или верхнюю отсечку на число или зажать значение в заданный диапазон. Это не агрегаты — они ничего не схлопывают в группы, а смотрят на колонки бок о бок, по горизонтали, и выдают по одному значению на каждую строку.
Сигнатура у обеих функций одинаковая: на вход идёт список из двух и более выражений сопоставимого типа, на выходе — одно значение того же типа. GREATEST отдаёт максимум списка, LEAST — минимум. Аргументы можно мешать: столбцы, литералы, результаты других вызовов, поэтому функции легко вкладываются друг в друга.
Главное, на что стоит смотреть заранее, — это типы и NULL. Типы должны быть сравнимы между собой, иначе PostgreSQL приведёт их к общему типу или откажется выполнять запрос; а NULL в списке аргументов в разных СУБД обрабатывается по-разному, и именно на этом чаще всего ломается перенос запроса. Оба момента разберём ниже на конкретных примерах.
Чем это отличается от MAX и MIN
Путаница рождается из-за слов: «максимум» он и есть «максимум». Но направление работы у функций разное. MAX и MIN — агрегаты, они идут вертикально, вниз по строкам группы, и выдают одно число на всю выборку. GREATEST и LEAST идут горизонтально, по аргументам одной строки, и выдают значение на каждую строку.
SELECT MAX(amount) AS biggest_order
FROM orders;
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]. Читается формула изнутри наружу: сначала срезаем сверху, потом подтягиваем снизу.
SELECT
id,
amount,
GREATEST(1, LEAST(1000, amount)) AS amount_clamped
FROM orders;
Приём незаменим там, где значение обязано держаться в рамках: скидки, бонусные баллы, нормализация зарплат для отчётов.
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, а явно прикрывать аргументы.
SELECT GREATEST(5, NULL, 9) AS demo;
Грабли: не закладывайтесь на «NULL сам собой отбросится», перенося запрос с PostgreSQL на MySQL. Логика, которая годами молча работала, после миграции внезапно начнёт выдавать NULL, и поймать это глазами в большом запросе почти невозможно. Если столбец может быть NULL, оберните его в COALESCE — тогда поведение становится явным и одинаковым везде.
SELECT
id,
GREATEST(COALESCE(amount, 0), 0) AS amount_nonneg
FROM orders;
Полезные сочетания
GREATEST и LEAST хорошо ложатся не только в SELECT. Внутри UPDATE и вычисляемых выражений они избавляют от громоздких CASE.
UPDATE employees
SET salary = GREATEST(40000, salary * 1.10)
WHERE dept = 'sales';
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 один раз — и сэкономите себе вечер отладки потом.
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у разных движков расходится, и именно на этом ломается код при переезде с одной СУБД на другую.NULLпросто выкидываются из сравнения.GREATEST(5, NULL, 9)вернёт9. И только когда все аргументы до единогоNULL, результат тоже станетNULL.NULLсреди аргументов, чтобы весь результат обнулился.GREATEST(5, NULL, 9)в MySQL вернётNULL— одинNULLотравляет всё выражение.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;-- 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один раз — и сэкономите себе вечер отладки потом.