Агрегатные функции

Агрегатные функции (SUM, AVG, COUNT, MIN, MAX) — один из краеугольных камней SQL, но вокруг них сформировалось множество мифов, особенно в среде разработчиков Delphi. За 15+ лет консультаций проектов, от бухгалтерских систем до BI-платформ, я убедился: большая часть проблем с производительностью и корректностью данных возникает не из-за СУБД, а из-за ложных предположений разработчика. В этом материале мы разберем пять наиболее живучих заблуждений.
Миф 1: «COUNT(*) медленнее COUNT(column_name)»
Это, пожалуй, самый распространенный миф, переходящий из поколения в поколение разработчиков. Аргумент сторонников этого мифа: «Зачем считать все колонки, если можно считать только одну?». Логика кажется очевидной, но она не учитывает внутреннюю механику СУБД, особенно актуальную в 2026 году.
На уровне ядра базы данных, COUNT(*) — это оптимизированная конструкция, которая подсчитывает количество записей (строк) в результирующем наборе. СУБД не перебирает колонки; она просто инкрементирует счетчик для каждой строки. COUNT(column_name), напротив, вынужден проверять каждое значение на NULL: строки с NULL-значениями в указанной колонке исключаются из подсчета. Эта проверка создает дополнительную нагрузку на процессор.
Современные оптимизаторы запросов (Firebird 5.0, PostgreSQL 16, SQL Server 2022) часто преобразуют COUNT(column_name) к COUNT(*) если колонка объявлена как NOT NULL. Тем не менее, полагаться на эту оптимизацию не следует. При построении отчетов в Delphi через TFDQuery или TADOQuery, используйте COUNT(*) везде, где вам нужно просто количество строк — это стабильнее и, вопреки мифу, быстрее.
Миф 2: «SUM работает только с числами — передача NULL приводит к ошибке»
В сообществах Delphi и SQL встречается мнение, что агрегатная функция SUM «ломается» при NULL-значениях. Это верно лишь отчасти и часто неверно интерпретируется. Согласно стандарту SQL (и подтверждено в документации InterBase, MySQL 8.x и MsSQL), SUM(NULL) возвращает NULL, а не ошибку выполнения.
Проблема возникает не в момент вычисления функции, а в момент, когда разработчик пытается использовать результат в арифметических операциях в коде Delphi. Например, после вызова FDQuery1.FieldByName('total').AsFloat, если total равен NULL, AsFloat вернет 0 — это особенность библиотеки доступа к данным, а не SQL. Реальная уязвимость — это смешивание NULL-значений в столбце. SUM игнорирует NULL, но если все строки в группе содержат NULL, результат будет NULL.
Практический совет: всегда используйте COALESCE или ZERO-функции при построении SQL в Delphi, если NULL неприемлем в коде. Пример: SELECT COALESCE(SUM(Amount), 0) AS TotalAmount FROM Orders. Это устраняет неопределенность на стороне клиента.
Миф 3: «MAX() и MIN() невозможно применить к текстовым полям корректно»
Некоторые разработчики Delphi, особенно перешедшие с Visual FoxPro или Paradox, считают, что MAX/MIN для строк работают «неправильно» из-за сортировки. Действительно, в старых драйверах BDE были проблемы с кодировками (ASCII vs. ANSI vs. Unicode). Но в современных проектах (FireDAC, UniDAC) это утверждение не более чем призрак прошлого.
MIN() и MAX() для строк работают на основе collation (правил сортировки) базы данных. С 2020-х годов почти все СУБД поддерживают Unicode (UTF-8/UTF-16), и разработчик может гибко управлять порядком сравнения через COLLATE clause. Реальная ловушка — это смешанные кодировки в одном поле (бинарный мусор + текст), но это проблема не функции MAX, а архитектуры схемы данных.
Применение MAX к текстовому полю для поиска «последнего значения» — вполне валидный прием, если вы контролируете collation. В Delphi это используется, например, для получения последнего комментария или максимального номера заказа в строковом формате. Единственный серьезный кейс, где стоит быть осторожным — это сравнение дат, хранящихся как строки (формат dd.mm.yyyy). Здесь MAX даст неверный лексикографический результат. Преобразуйте поле к типу DATE: MAX(CAST(DateStr AS DATE)).
Миф 4: «Агрегатные функции в GROUP BY нельзя использовать в WHERE — нужно применять HAVING»
Это классическая ошибка новичков, которая, к сожалению, закрепляется как «жесткое правило». Да, синтаксис SQL не позволяет использовать агрегатные функции напрямую в клаузе WHERE (WHERE SUM(col) > 10 — ошибка). Для фильтрации результатов группировки существует HAVING. Но утверждение, что «фильтрацию ДО группировки тоже нельзя сделать без HAVING» — это заблуждение.
Эффективная производительность запросов в Delphi-приложениях часто страдает именно из-за того, что разработчик выгружает миллионы записей в память и уже там делает цикл с суммированием (или наоборот, навешивает HAVING на таблицу с 5 млн строк без предварительной фильтрации). Оптимальная стратегия: использовать WHERE для максимального сокращения записей ДО агрегации, и только затем применять GROUP BY + HAVING.
Пример из реальной практики 2026 года: доклад «Оптимизация SQL в Delphi» показывает, что разница между SELECT DeptID, SUM(Salary) FROM Employees GROUP BY DeptID HAVING SUM(Salary) > 10000 (без WHERE) и аналогичным запросом с предварительным отсечением WHERE DeptID IN (1,2,3) может достигать 15-кратного ускорения в Firebird и 20-кратного в SQLite. Правило простое: WHERE уменьшает входной поток, HAVING — уже результат.
Миф 5: «AVG всегда корректно вычисляет среднее арифметическое»
Наивное (и небезопасное) предположение, что AVG(column) вернет то же самое, что и ручное деление суммы на количество. С точки зрения математики — да, но с точки зрения данных — нет. Агрегатная функция AVG игнорирует NULL, как и SUM/COUNT. Если вы ожидаете, что NULL — это ноль, то AVG выдаст иной результат.
Пример: таблица оценок студентов. У Пети 3 оценки (5, 4, 5) — AVG = 4.66. У Васи 3 оценки, но одна из них NULL (среднее считается по двум) — AVG = (5+5)/2 = 5. Сравнение студентов по AVG в таком случае некорректно. Это не ошибка SQL, это логическая ошибка аналитика или разработчика, который не учел природу NULL.
В Delphi-коде эта проблема усугубляется, когда мы пишем универсальные отчетные функции. Решение — всегда документировать или проверять, что ваш AVG берет среднее по НЕ-пустым значениям. Если вам нужно среднее, где NULL трактуется как 0, используйте: AVG(COALESCE(column, 0)). В Firebird и PostgreSQL это работает линейно, без заметной потери производительности.
Резюме: как отличить миф от факта
- Используйте COUNT(*) для количественной оценки, если NULL-исключение не требуется — это быстрее и надежнее.
- SUM(NULL) не вызывает сбой, но результат NULL — обрабатывайте COALESCE на уровне SQL, а не в коде Delphi.
- MAX и MIN на строках корректны при правильной сортировке (collation). Опасность — строковые даты и бинарный шлак.
- Фильтруйте через WHERE до GROUP BY, это важнее HAVING для производительности.
- AVG не считает NULL за ноль; это особенность, а не баг. Учитывайте её в отчетах.
Проектируя SQL-запросы в Delphi, помните: интуиция — плохой помощник. Агрегатные функции строго следуют математике множеств и стандарту SQL, который не совпадает с «бытовой логикой». Каждая из описанных ошибок стоила моим клиентам часов дебаггинга. Используйте документированные сценарии и тестируйте с реальными данными, включая NULL и граничные значения.
Добавлено: 27.04.2026
