Являются ли агрегатные функции в базах данных антипаттерном?

Ответ

Нет, агрегатные функции (COUNT, SUM, AVG, MAX, MIN и др.) не являются антипаттернами. Это мощный и неотъемлемый инструмент SQL, предназначенный для выполнения вычислений над набором строк прямо в базе данных.

Использование агрегатных функций становится проблемой или антипаттерном только при неправильном применении:

  1. Агрегация на стороне приложения вместо БД: Выборка тысяч строк из базы данных, чтобы затем в коде на Go посчитать их сумму или среднее — это классический антипаттерн. Это создает избыточную нагрузку на сеть, потребляет много памяти в приложении и гораздо медленнее, чем выполнение агрегации средствами СУБД.
  2. Выполнение на неиндексированных полях: Агрегация по столбцам, которые не проиндексированы и используются в условиях WHERE или GROUP BY, может приводить к полному сканированию таблицы (full table scan) и быть очень медленной.
  3. Длительные блокировки: Сложные агрегатные запросы на больших таблицах могут надолго блокировать строки или всю таблицу, мешая другим операциям.
  4. Сложные, нечитаемые запросы: Вложение множества агрегатных функций и подзапросов может сделать SQL-запрос нечитаемым и сложным в поддержке.

Рекомендации:

  • Переносите вычисления в БД: Всегда старайтесь выполнять агрегацию как можно ближе к данным, то есть в самой базе данных.
  • Используйте индексы: Убедитесь, что для полей в WHERE, JOIN и GROUP BY существуют подходящие индексы.
  • Анализируйте запросы: Используйте EXPLAIN (или EXPLAIN ANALYZE), чтобы понять план выполнения вашего запроса и найти узкие места.

Правильный пример (SQL):

-- Эффективно считает количество активных пользователей и их средний рейтинг
SELECT COUNT(*) as total_users, AVG(rating) as avg_rating 
FROM users 
WHERE is_active = TRUE;

Пример агрегации в Go (допустимо для малых, уже загруженных в память срезов):

// Эта функция имеет смысл, если срез users уже находится в памяти приложения
// по какой-то другой причине. Не стоит загружать данные из БД специально для этого.
func calculateAverageAge(users []User) float64 {
    if len(users) == 0 {
        return 0
    }
    sum := 0
    for _, u := range users {
        sum += u.Age
    }
    return float64(sum) / float64(len(users))
}

Ответ 18+ 🔞

А, ну вот, опять про эти ваши агрегатные функции пошли разговоры! Слушай, давай сразу начистоту: COUNT, SUM, AVG — это не антипаттерны, ёпта! Это как молоток, блядь. Молоток — он что, плохой? Нет, он инструмент, блядь! Но если ты им по пальцам себе вмажешь — это не молоток виноват, это ты, мудак, криворукий!

Проблема начинается, когда люди их нихуя не понимают и применяют как попало. Вот смотри, классика жанра:

  1. Выгрузка всего дерьма в приложение. Представь: у тебя в базе миллион строк, а тебе надо среднее арифметическое посчитать. И что делает наш герой? Выгребает все эти миллионы записей по сети в свой Go-код, в память, блядь, забивает, а потом уже там циклом бегает и складывает. Это пиздец какой-то! Это как привезти целый карьер песка к себе на кухню, чтобы отсеять три крупинки золота. СУБД для того и создана, чтобы считать, фильтровать и агрегировать, она это делает в сотни раз быстрее! Запрос SELECT AVG(price) FROM products выполнится на порядки быстрее, чем любая твоя самописная хуйня на Go после выгрузки всех price.

  2. Пляски на неиндексированных полях. Ну вот представь: у тебя таблица на гигабайт, и ты решил посчитать MAX(some_column). А на этом столбце индекса нет, блядь. Что сделает база? Правильно, пойдёт читать всю таблицу целиком, как последний двоечник учебник перед экзаменом. Это называется full table scan, и это пиздец как медленно. Волшебной палочки нет — хочешь быстро искать и агрегировать по полю, индексируй его, ёбта!

  3. Долгие, как жизнь, блокировки. Запустил ты агрегацию по жирной таблице на продакшене, а она взяла и повесила на неё монопольную блокировку. И всё, приехали. Пока твой запрос считает, остальные запросы к этой таблице встали в очередь, как за колбасой в девяностые. Аппликейшн лёг, алёрты завыли. Красота!

  4. Запросы-монстры. Это когда в один SQL впихивают столько вложенных агрегаций, подзапросов и джойнов, что разобраться в этой каше может только тот, кто её написал, да и то через неделю уже забудет. Читаемость, блядь, на нуле! Поддерживать такое — это как искать иголку в стоге сена, который ещё и горит.

Так что же делать, спросишь ты?

  • Считай там, где данные живут! Логику агрегации — в БД. Это её работа. Твоё приложение должно получать уже готовый, посчитанный результат. SELECT COUNT(*) FROM orders WHERE status = 'completed' — вот твой друг.
  • Индексы, ёпта, индексы! Перед сложными агрегациями глянь план выполнения через EXPLAIN ANALYZE. Увидишь там Seq Scan на большой таблице — готовься к боли. Добавь индекс, перепиши запрос.
  • Не усложняй без нужды. Если запрос выглядит как лапша, разбей его на части, используй CTE (WITH), сделай его читаемым. Твои коллеги, которые будут это смотреть через полгода, тебе спасибо не скажут, но хотя бы проклинать будут меньше.

Нормальный пример (SQL):

-- База сама, быстро, по индексам посчитает сколько активных юзеров и их средний рейтинг
SELECT COUNT(*) as total_users, AVG(rating) as avg_rating
FROM users
WHERE is_active = TRUE;

А вот пример на Go, который имеет право на жизнь только в одном случае:

// Допустимо ТОЛЬКО если этот слайс users УЖЕ ЗАГРУЖЕН в память
// по какой-то другой, веской причине. Специально тащить данные из БД для этого — идиотизм.
func calculateAverageAge(users []User) float64 {
    if len(users) == 0 {
        return 0
    }
    sum := 0
    for _, u := range users {
        sum += u.Age
    }
    return float64(sum) / float64(len(users))
}

Короче, агрегатные функции — это огонь, но обращаться с ними надо с умом, а не как слон в посудной лавке. Всё, вопрос исчерпан, иди работай.