Работали ли вы с генерируемыми (вычисляемыми) колонками в базах данных?

Ответ

Да, использовал генерируемые (generated) колонки в MySQL для упрощения логики приложения и обеспечения целостности данных на уровне БД.

Пример из реального проекта (MySQL 8.0): У нас была таблица invoice_items для хранения позиций в счете. Вместо того чтобы вычислять итоговую сумму (total) в коде PHP при каждом чтении, мы вынесли это в генерируемую колонку.

CREATE TABLE invoice_items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    invoice_id INT NOT NULL,
    unit_price DECIMAL(10, 2) NOT NULL,
    quantity INT NOT NULL,
    discount_percent DECIMAL(5, 2) DEFAULT 0.00,
    -- VIRTUAL колонка (вычисляется на лету)
    discount_amount DECIMAL(10, 2) GENERATED ALWAYS AS (unit_price * quantity * discount_percent / 100) VIRTUAL,
    -- STORED колонка (сохраняется на диске, можно индексировать)
    line_total DECIMAL(10, 2) GENERATED ALWAYS AS ((unit_price * quantity) - discount_amount) STORED,
    INDEX idx_invoice_id (invoice_id),
    FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
);

Как это работает в PHP-приложении:

// При вставке данных указываем только базовые поля
$stmt = $pdo->prepare("INSERT INTO invoice_items (invoice_id, unit_price, quantity, discount_percent) VALUES (?, ?, ?, ?)");
$stmt->execute([$invoiceId, 99.99, 5, 10.00]);

// При выборке `line_total` и `discount_amount` доступны как обычные поля
$stmt = $pdo->query("SELECT id, unit_price, quantity, line_total FROM invoice_items WHERE invoice_id = $invoiceId");
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);

foreach ($items as $item) {
    echo "Item ID: {$item['id']}, Total: {$item['line_total']}n";
    // line_total уже вычислен и готов к использованию
}

Практические соображения:

  • VIRTUAL vs STORED: Использовал VIRTUAL (по умолчанию) для значений, которые редко используются или часто обновляются, чтобы не тратить место на диске. STORED применял для колонок, которые часто используются в WHERE или ORDER BY, так как их можно проиндексировать, что значительно ускоряет поиск.
  • Переносимость: Хотя синтаксис похож в PostgreSQL (GENERATED ALWAYS AS) и SQLite, в старых версиях MySQL (< 5.7) эта функция недоступна. Поэтому всегда проверял версию СУБД перед проектированием схемы.
  • Альтернатива в коде: В проектах, где нельзя было полагаться на генерируемые колонки (например, при использовании облачных БД с ограничениями), реализовывал аналогичную логику через геттеры в моделях Eloquent/Doctrine или триггеры в БД.

Ответ 18+ 🔞

А, ну это же про эти самые generated columns в MySQL! Ёпта, реально полезная штука, когда надоедает в коде одно и то же вычислять по десять раз на дню. Сам недавно на проекте с инвойсами такую же дичь реализовывал, прямо как в твоём примере.

Смотри, какая история была. У нас там таблица с позициями заказа, и каждый раз, когда нужно было итоговую сумму по строке получить, приходилось в коде писать эту формулу: цена * количество минус скидка. И везде, блядь, одно и то же — и в API-ответе, и в отчёте, и при валидации. Чисто технически — терпения ноль ебать, однообразная хуйня.

Решили вынести эту логику прямо в базу, на уровень схемы. Создали колонку line_total как GENERATED ALWAYS AS. И знаешь что? Жизнь наладилась! Вставляешь только базовые данные — цену, количество, процент скидки — а сумма сама, бля, появляется, как по волшебству. В коде теперь просто берёшь её как обычное поле, даже думать не надо.

// Раньше было вот это дерьмо в каждой модели или сервисе
$lineTotal = ($item['unit_price'] * $item['quantity']) * (1 - $item['discount_percent'] / 100);

// А теперь просто
$lineTotal = $item['line_total']; // Всё, пиздец, красота!

Но тут, чувак, есть важный момент, про который многие забывают — разница между VIRTUAL и STORED. Это, бля, принципиально!

  • VIRTUAL — это как обещание. Колонка вычисляется на лету, каждый раз при запросе. Места на диске не жрёт, но если формула сложная и вызывается в тяжёлых отчётах — может подтормаживать. Использовал для всяких промежуточных расчётов, которые редко нужны.
  • STORED — вот это уже серьёзно. Значение вычисляется один раз при вставке или обновлении и хранится на диске. А раз хранится — его можно, внимание, проиндексировать! Если ты эту колонку часто используешь в условиях WHERE или сортировке (ORDER BY), то без индекса будет пиздец как медленно. Сделал её STORED — создал индекс — и запросы полетели в разы быстрее. Но за всё надо платить: место на диске.

С миграциями, правда, иногда забавно выходит. Помню, пытался на одном продакшене, где MySQL версии 5.6 ещё стоял, такую колонку добавить. А там, ёпта, эта фича не поддерживается! Ну я и накрылся медным тазом. Пришлось откатывать миграцию и лезть в код, делать через геттер в модели. С тех пор всегда в начале проекта версию БД проверяю, чтобы потом не было мучительно больно.

Альтернативы, конечно, есть. Можно ту же логику в геттер модели Eloquent запихнуть — тоже удобно. Или, если совсем параноик, на триггерах в БД сделать. Но generated columns — это самый декларативный и чистый способ, на мой взгляд. Как будто сама база данных тебе говорит: «Расслабься, чувак, я всё посчитаю». Главное — понять, где VIRTUAL, а где STORED нужен, и не пытаться использовать это на древних версиях.