Генерируемые столбцы и SQL-представления: практическое руководство для Laravel

В этой статье мы подробно разберём, как использовать генерируемые столбцы SQL в Laravel для ускорения работы приложения.

Eloquent ORM упрощает работу с данными, но финальная производительность определяется SQL-запросами. Две встроенные возможности СУБД — генерируемые столбцы и представления — позволяют значительно повысить производительность запросов. В этой статье мы разберём, как использовать их в Laravel, управляя схемой через миграции.

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

Далее мы рассмотрим, в каких сценариях эти функции могут быть полезны для Laravel-приложений.

Генерируемые столбцы: что это и зачем нужны

Генерируемый столбец автоматически вычисляет своё значение на основе выражения с использованием других столбцов в той же строке. Например, можно определить столбец c как результат сложения столбцов a и b с помощью выражения a + b.

Существует два типа генерируемых столбцов:

Генерируемые столбцы поддерживаются большинством современных SQL-движков, включая MySQL, MariaDB и PostgreSQL. В этой статье мы будем использовать MySQL для всех примеров.

Рассмотрим классический пример: чтобы получить полное имя пользователей (users), можно определить столбец full_name, сгенерированный автоматически.

CREATE TABLE users (
id INT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
full_name VARCHAR(100)
GENERATED ALWAYS AS (CONCAT(first_name, ' ', last_name)) VIRTUAL
);

Так определяется виртуальный генерируемый столбец. Его значение не хранится на диске, а вычисляется при каждом обращении к таблице. Чтобы исключить вычисление при каждом чтении, столбец можно определить как хранимый:

full_name VARCHAR(100)
GENERATED ALWAYS AS (CONCAT(first_name, ' ', last_name)) STORED

Генерируемый столбец (->storedAs($expression)) определяется в миграции Laravel следующим образом:

// Пример создания хранимого генерируемого столбца в миграции
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('first_name');
$table->string('last_name');
$table->string('full_name')
->storedAs('CONCAT(first_name, " ", last_name)');
});

Виртуальный столбец (->virtualAs($expression)) определяется в миграции Laravel следующим образом:

// Пример создания виртуального генерируемого столбца в миграции
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('first_name');
$table->string('last_name');
$table->string('full_name')
->virtualAs('CONCAT(first_name, " ", last_name)');
});

После этого можно обрабатывать full_name как любой другой столбец:

$users = User::where('full_name', 'like', 'John%')->get();

Генерируемые столбцы и аксессоры Eloquent

Наиболее близкая аналогия в Laravel — аксессоры Eloquent. Они также служат для вычисления значений на основе других полей модели. Например:

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model
{
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}"
);
}
}

С этим аксессором вызов $user->full_name на экземпляре модели User возвращает вычисленное полное имя.

Хотя результат обращения к полю ($user->full_name) выглядит одинаково, механизм и возможности фундаментально различаются. Ключевые отличия заключаются в следующем:

Преимущество 1: Фильтрация, сортировка и индексация

Laravel вычисляет значения аксессоров только после извлечения данных из базы данных. Это означает, что их нельзя использовать для сортировки или фильтрации запросов, а также нельзя индексировать таблицу на их основе. Однако генерируемые столбцы преодолевают эти ограничения, поскольку они интегрированы непосредственно на уровне базы данных.

Предположим, что в модели Product есть аксессор full_price:

protected function fullPrice(): Attribute
{
return Attribute::make(
get: fn () => $this->price * $this->tax_rate
);
}

Вы можете обращаться к $product->full_price, но не можете использовать это поле в условиях запроса:

Product::where('full_price', '>', 100)->get(); // ❌ Такой запрос не будет выполнен

Теперь перенесём эту логику в генерируемого столбец:

Schema::table('products', function (Blueprint $table) {
$table->decimal('full_price', 10, 2)
->storedAs('price * tax_rate')
->index();
});

После этого запрос будет работать корректно:

Product::where('full_price', '>', 100)->get(); // ✅ Запрос выполняется успешно

Это обеспечивает возможность фильтрации, сортировки и индексации вычисляемого значения. Все операции выполняются на стороне СУБД, что повышает их эффективность.

Обратите внимание: в MySQL виртуальный столбец (VIRTUAL) нельзя индексировать напрямую. Если требуется индекс, столбец должен быть хранимым (STORED). В отличие от MySQL, PostgreSQL позволяет создавать индексы непосредственно по выражениям (CREATE INDEX ... ON table ((expression))), что делает индексацию виртуальных вычисляемых полей гораздо более гибкой.

Преимущество 2: Доступность во всех типах запросов

Аксессоры применимы только к экземплярам моделей Eloquent. Если выполняется запрос к базе данных с помощью конструктора запросов или сырых SQL-запросов, логика аксессора не будет запущена, и вычисленное поле не будет включено в результаты.

Например, вот аксессор, вычисляющий возраст пользователя по его дате рождения (birth_date):

protected function age(): Attribute
{
return Attribute::make(
get: fn () => $this->birth_date
? Carbon::parse($this->birth_date)->diffInYears(now())
: null
);
}

Если запрашиваете пользователя с помощью Eloquent, аксессор вернёт рассчитанный возраст:

$user = User::find(1);
echo $user->age; // ✅ Работает

Но если вместо этого использовать конструктор запросов:

$user = DB::table('users')->where('id', 1)->first();
echo $user->age; // ❌ Не существует

Поскольку для запуска аксессора не использовалась модель Eloquent, поле age не будет включено в результат.

В отличие от аксессора, генерируемый столбец является частью схемы базы данных. Его значение доступно при любом способе извлечения данных: через Eloquent, конструктор запросов или сырой SQL.

Так можно добавить генерируемый столбец age в таблицу users на основе столбца birth_date:

Schema::table('users', function (Blueprint $table) {
$table->unsignedInteger('age')
->virtualAs("TIMESTAMPDIFF(YEAR, birth_date, CURDATE())");
});

Таким образом, столбец age становится частью схемы таблицы. Это вычисляемое поле доступно для использования в условиях WHERE и операциях сортировки:

User::where('age', '>=', 18)->orderBy('age')->get();

Поскольку этот расчёт зависит от текущей даты, целесообразнее определить age как виртуальный столбец, а не как хранимый. Сохранённый age будет обновляться только при изменении birth_date (что в большинстве случаев никогда не произойдёт!).

Преимущество 3: Производительность и оптимизация вычислений

Основная выгода — производительность. Генерируемые столбцы ускоряют запросы двумя способами:

  1. Сокращение сканирования: Фильтрация и сортировка происходят в СУБД до передачи данных в PHP.
  2. Возможность индексации: Индекс по хранимому столбцу ускоряет поиск так же, как по обычному.

Дополнительный плюс — выгрузка вычислений. Движок базы данных (MySQL, PostgreSQL) вычисляет значения (даты, арифметику) эффективнее, чем PHP, особенно на больших объёмах данных.

Например, представьте, что необходимо узнать, активна ли подписка: находится ли её start_date в прошлом, а end_date — в будущем.

Если обрабатывать эту задачу с помощью аксессора, метод is_active может выглядеть следующим образом:

protected function isActive(): Attribute
{
return Attribute::make(
get: fn () => $this->start_date <= now() && $this->end_date >= now()
);
}

Вы можете получить все активные подписки следующим образом:

Subscription::get()->filter(fn($subscription) => $subscription->is_active);

Такой подход приемлем для небольших наборов данных. Однако при обработке тысяч или миллионов записей вычисление значения в PHP для каждой строки приводит к чрезмерному потреблению ресурсов. Для каждой подписки Laravel должен загружать строку, выполнять сравнение и хранить результат в памяти, что может значительно замедлить работу.

Использование генерируемого столбца вместо аксессора решает проблему производительности. Механизм базы данных выполняет вычисление как часть самого запроса, используя оптимизированные внутренние функции.

Вот как можно определить виртуальный генерируемый столбец is_active в миграции:

Schema::table('subscriptions', function (Blueprint $table) {
$table->boolean('is_active')
->virtualAs('start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE');
});

Активные подписки теперь можно отфильтровать непосредственно на уровне базы данных:

Subscription::where('is_active', true)->get();

Такой подход значительно эффективнее с точки зрения производительности.

Генерируемые столбцы — мощный инструмент для оптимизации, но их использование должно быть взвешенным. Сформулируем чёткие критерии, когда стоит выбрать аксессор Eloquent, а когда — перенести логику в БД.

Рекомендации по применению генерируемых столбцов

Генерируемые столбцы подходят не для всех задач. Используйте аксессоры Eloquent в следующих случаях:

Таким образом, генерируемые столбцы — это инструмент для оптимизации вычислений внутри одной таблицы. Но что делать, когда данные для отчёта или сложной выборки нужно собрать из нескольких таблиц? Здесь на помощь приходит второй мощный инструмент — SQL-представления.

SQL-представления: виртуальные таблицы в Laravel

SQL-представления предназначены для работы с данными из нескольких таблиц и представляют собой сохранённый SELECT-запрос. В нём доступны все возможности SQL: JOIN, агрегация, фильтрация. А для Laravel представление выглядит и работает как обычная таблица.

Если генерируемые столбцы работают в рамках одной таблицы, то для объединения и агрегации данных из нескольких источников нужен другой инструмент — SQL-представления.

Рассмотрим пример интернет-магазина, в котором каждую неделю на товары действует скидка благодаря активной рекламной акции. Ваши миграции могут выглядеть следующим образом:

Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->decimal('price', 10, 2);
// Другие поля продукта...
});

Schema::create('promotions', function (Blueprint $table) {
$table->id();
$table->decimal('discount', 3, 2); // например, 0.30 для 30 %
$table->date('start_date');
$table->date('end_date');
// Другие поля продвижения...
});

При прямом запросе таблицы products отображается исходная цена. Для получения цены с учётом скидки требуется объединить таблицу promotions и рассчитать скидку:

SELECT
p.id,
p.name,
p.price AS full_price,
p.price * (1 - pr.discount) AS discounted_price
FROM
products p
LEFT JOIN
promotions pr ON CURRENT_DATE BETWEEN pr.start_date AND pr.end_date;

Повторение сложной логики JOIN в множестве запросов увеличивает объем кода, усложняет его поддержку и повышает риск ошибок. Если ваши JOIN-запросы становятся сложными, изучите продвинутые техники оптимизации запросов. Здесь на помощь приходят представления. Вы можете создать представление price_list, чтобы инкапсулировать эту логику:

CREATE VIEW price_list AS
SELECT
p.id,
p.name,
p.price AS full_price,
p.price * (1 - pr.discount) AS discounted_price
FROM
products p
LEFT JOIN
promotions pr ON CURRENT_DATE BETWEEN pr.start_date AND pr.end_date;

Эта команда создаёт представление — виртуальную таблицу, доступную только для чтения. При изменении записей в таблицах products или promotions данные в price_list автоматически обновляются.

Стандартные SQL-представления в MySQL доступны только для чтения. Попытка выполнить INSERT или UPDATE напрямую в такое представление вызовет ошибку. Для модификации данных необходимо работать с исходными таблицами.

После создания представления оно становится доступным для запросов из приложения:

$products = DB::table('price_list')->get();

Это упрощает работу с данными в коде. Для интеграции этого представления в проект рекомендуется создать миграцию, создающую представление за вас:

<?php

use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
public function up(): void
{
DB::statement(<<<SQL

CREATE VIEW price_list AS
SELECT
p.id,
p.name,
p.price AS full_price,
p.price * (1 - COALESCE(pr.discount, 0)) AS discounted_price
FROM products p
LEFT JOIN promotions pr ON CURRENT_DATE
BETWEEN pr.start_date AND pr.end_date

SQL
);
}

public function down(): void
{
DB::statement('DROP VIEW IF EXISTS price_list;');
}
};

В некоторых случаях также имеет смысл создать модель Eloquent для этого представления. Вы можете определить типичную модель и установить имя представления как $table.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class PriceList extends Model
{
// Задайте имя представления в качестве таблицы
protected $table = 'price_list';
}

Теперь представление можно запрашивать с помощью Eloquent:

$products = PriceList::where('discounted_price', '<', 50)->get();

Этот подход обеспечивает работу с данными через интерфейс Eloquent. Важное ограничение: представления, как правило, доступны только для чтения. Создавать или изменять записи рекомендуется через исходные модели, такие как Product.

Сравнение подходов: когда что использовать

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

КритерийАксессор EloquentГенерируемый столбецSQL-представление
ЛогикаСложная PHP-логика, работа с внешними данными (сессии, env)Простое выражение на SQL (арифметика, конкатенация, базовые функции)Сложные запросы с JOIN, агрегатными функциями (SUM, COUNT), группировкой
ПроизводительностьПодходит для малых данных. Вычисления в PHP после выборки.Вычисления в СУБД. Хранимый столбец можно индексировать для скорости поиска/сортировки.Агрегация происходит в СУБД, исключает обработку больших объёмов в PHP.
Использование в запросахНельзя использовать в WHERE, ORDER BY, JOINМожно использовать в WHERE, ORDER BY. Хранимый столбец можно индексировать.Работает как виртуальная таблица. Можно использовать в запросах (фильтрация, сортировка).
Область примененияДополнительные вычисляемые поля моделиВычисляемые поля в рамках одной строки или таблицыАгрегация и объединение данных из нескольких таблиц
Чтение/ЗаписьТолько чтение (get-аксессор)Только чтение (значение генерируется автоматически)Только чтение (в большинстве СУБД)

Ограничения и особенности использования

Перед внедрением этих инструментов в продакшен учитывайте следующие нюансы.

Для генерируемых столбцов

  1. Привязка к СУБД: Выражения в генерируемых столбцах зависят от синтаксиса конкретной базы данных. Например, TIMESTAMPDIFF — функция MySQL. Миграция на PostgreSQL потребует переписывания этих выражений.
  2. Сложность изменения хранимых столбцов: Изменение (ALTER TABLE) хранимого генерируемого столбца в большой таблице может привести к длительной блокировке и пересчёту значений для всех строк. Планируйте такие операции на время минимальной нагрузки.
  3. Индексация только для хранимых столбцов: Как отмечено ранее, индекс можно создать только для хранимого столбца. Виртуальный столбец не занимает место на диске, но и не может быть проиндексирован.

Для SQL-представлений

  1. Производительность сложных представлений: Представление — это сохранённый запрос. Если в его основе лежит тяжёлый запрос с множеством JOIN и агрегаций, то обращение к представлению будет выполняться столько же времени. Представления не заменяют оптимизацию исходных запросов. Если базовый запрос неэффективен, представление лишь инкапсулирует эту неэффективность.
  2. Обновление данных: Как уже упоминалось, стандартные SQL-представления доступны только для чтения. Для модификации данных используйте исходные таблицы или модели Eloquent.
  3. Отладка: Ошибка в запросе, лежащем в основе представления, может быть неочевидной при отладке кода приложения. Всегда проверяйте и тестируйте SQL-код представления отдельно.

Основной принцип: управление схемой через миграции

Независимо от выбора инструмента, все изменения схемы базы данных должны описываться в миграциях Laravel. Это касается создания и изменения генерируемых столбцов, а также представлений.

Используя миграции, вы превращаете встроенные возможности СУБД в управляемый и поддерживаемый актив вашего приложения.

Алгоритм выбора инструмента

  1. Нужно вычисляемое поле в одной таблице для WHERE/ORDER BY?
    • Да → Генерируемый столбец (STORED с индексом)
    • Нет, только для чтения → Генерируемый столбец (VIRTUAL)
  2. Нужно объединить данные из нескольких таблиц или сложная агрегация?
    • Да → SQL-представление
  3. Логика сложная, требует данных из PHP или внешних источников?
    • Да → Аксессор Eloquent
Вычисляемое поле?
├─ Да, в одной таблице для WHERE/ORDER BY? → Генерируемый столбец (STORED + индекс)
├─ Да, в одной таблице, только для чтения → Генерируемый столбец (VIRTUAL)
├─ Да, из нескольких таблиц/агрегация → SQL-представление
└─ Нет, сложная логика/PHP-зависимости → Аксессор Eloquent

Практический кейс 1: Ускорение сортировки по вычисляемому полю

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

Проблема: Страница с товарами, отсортированная по вычисляемой марже (profit_margin), загружалась медленно (~350 мс). Попытки реализовать пагинацию нарушали корректность сортировки.

Причина: Поле profit_margin было аксессором в модели Eloquent. Сортировка Product::get()->sortByDesc('profit_margin') происходила в PHP после полной выгрузки всех записей из БД. Пагинация на уровне Laravel в этом случае невозможна.

Решение: Перенести вычисление в базу данных через хранимый генерируемый столбец с индексом.

$products = Product::get()->sortByDesc('profit_margin');
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;

class Product extends Model
{
protected function profitMargin(): Attribute
{
return Attribute::make(
get: fn () => $this->price > 0
? round((($this->price - $this->cost) / $this->price) * 100, 2)
: 0
);
}
}

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

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

Оптимальным решением является создание хранимого генерируемого столбца. Для этого добавляем следующую миграцию:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('products', function (Blueprint $table): void {
$table->decimal('profit_margin', 5, 2)
->storedAs('CASE WHEN price > 0 THEN ROUND(((price - cost) / price) * 100, 2) ELSE 0 END')
->index();
});
}

public function down(): void
{
Schema::table('products', function (Blueprint $table): void {
$table->dropColumn('profit_margin');
});
}
};

Затем, вернувшись в контроллер, вносится следующее изменение:

-$products = Product::get()->sortByDesc('profit_margin');
+$products = Product::orderBy('profit_margin', 'desc')->get();

После внедрения изменений и создания индекса, тестирование на наборе данных в ~50 000 товаров показало снижение времени выполнения запроса с ~350 мс до ~70 мс.

Внедрение изменений привело к улучшению по трём ключевым направлениям:

Практический кейс 2: Создание отчётов через SQL-представления

Вторая задача: формирование отчёта о ежедневных продажах

Проблема: Страница отчёта вызывала таймаут (истечение времени ожидания).

$orders = Order::with('items')
->withCount('items')
->withSum('items', 'price')
->get()
->groupBy(fn ($order) => $order->created_at->toDateString());

Цель — отобразить эти данные в виде таблицы:

| Date       | Total Items | Total in USD |
|------------|-------------|--------------|
| 2025-06-08 | 15 | $450.00 |
| 2025-06-09 | 22 | $730.50 |
| 2025-06-10 | 10 | $300.00 |

Ключевая проблема, выявленная в ходе анализа, заключается в группировке и агрегации данных. Для таких задач важно понимать, как выполняются SQL-запросы и как работают агрегатные функции.

Механизм проблемы: Запрос загружает все модели Order и связанные модели Item в память PHP. Например, для 100 000 заказов с 4 товарами каждый это создаёт ~400 000 объектов, что приводит к высокому потреблению памяти и времени процессора. Затем группировка выполняется средствами PHP, что неэффективно.

Нужно сгруппировать данные на уровне SQL. Для этого рекомендуется использовать функцию DATE(created_at):

$orders = Order::selectRaw('DATE(created_at) as date')
->with('items')
->withCount('items')
->withSum('items', 'price')
->groupByRaw('DATE(created_at)')
->get();
При выполнении этого запроса возникает ошибка
SQLSTATE[42000]: Syntax error or access violation: 1055
Expression #2 of SELECT list is not in GROUP BY clause and
contains nonaggregated column 'my_app.orders.id' which is
not functionally dependent on columns in GROUP BY clause;
this is incompatible with sql_mode=only_full_group_by
(Connection: mysql, SQL:

select
DATE(created_at) as date,
(
select count(*)
from `order_items`
where `orders`.`id` = `order_items`.`order_id`
) as `items_count`,
(
select sum(`order_items`.`price`)
from `order_items`
where `orders`.`id` = `order_items`.`order_id`
) as `items_sum_price`
from `orders`
group by DATE(created_at))

)

В чём причина этой ошибки? Режим only_full_group_by MySQL требует, чтобы все выбранные столбцы в операторе GROUP BY были либо столбцами, по которым выполняется группировка, либо агрегатными функциями (такими как SUM, COUNT и т. д.). В данном случае операторы withCount и withSum генерируют подзапросы в операторе SELECT, не являющиеся агрегатными, что нарушает правило.

Чтобы это сработало, можно переключиться с Eloquent на конструктор запросов. Это решение может быть менее лаконичным, но оно эффективно и полностью соответствует задаче: можно использовать JOIN вместо подзапросов и группировать по DATE(created_at), как и планировалось.

$orders = Order::join('order_items', 'orders.id', '=', 'order_items.order_id')
->selectRaw('DATE(orders.created_at) as date')
->selectRaw('SUM(order_items.price) as revenue')
->selectRaw('COUNT(order_items.id) as items_sold')
->groupByRaw('DATE(orders.created_at)')
->get();

Оптимизированный запрос успешно выполняется и подтверждает свою работоспособность и значительный прирост производительности. Для сравнения подходов было выполнено локальное тестирование на 10 000 заказах, содержащих по 10 товаров. Результаты следующие:

Если аналогичный запрос потребуется в других частях приложения (например, для ежемесячного отчёта в формате CSV), дублировать этот код неэффективно. Хотя можно использовать область видимости в модели Order, возможно, ещё лучшим решением будет создание представления базы данных:

CREATE VIEW daily_orders_summary AS
SELECT
DATE(orders.created_at) AS date,
SUM(order_items.price) AS revenue,
COUNT(order_items.id) AS items_sold
FROM
`orders`
INNER JOIN `order_items` ON `orders`.`id` = `order_items`.`order_id`
GROUP BY
DATE(orders.created_at)

После успешного локального тестирования создаём миграцию:

<?php

use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
public function up(): void
{
DB::statement(<<<SQL
CREATE VIEW daily_orders_summary AS
SELECT
DATE(orders.created_at) AS date,
SUM(order_items.price) AS revenue,
COUNT(order_items.id) AS items_sold
FROM
`orders`
INNER JOIN `order_items` ON `orders`.`id` = `order_items`.`order_id`
GROUP BY
DATE(orders.created_at)
SQL);
}

public function down(): void
{
DB::statement('DROP VIEW IF EXISTS daily_orders_summary');
}
};

И затем, простую модель, связанную с ней:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class DailyOrdersSummary extends Model
{
protected $table = 'daily_orders_summary';
}

Данные из представления доступны через Eloquent:

// Получение даты, когда было продано более 100 товаров
DailyOrdersSummary::where('items_sold', '>', 100)->get();

// Получение даты, когда доход превысил 9000 долларов
DailyOrdersSummary::where('revenue', '>', 9000)->get();

// 10 лучших дат по выручке
DailyOrdersSummary::orderBy('revenue')->limit(10)->get();

В результате были значительно оптимизированы критические участки системы, что повысило их производительность и надёжность. Этот пример демонстрирует, как применение возможностей базы данных позволяет решать сложные задачи по оптимизации.

Заключение

Генерируемые столбцы и SQL-представления — это эффективные инструменты для оптимизации Laravel-приложений на уровне базы данных.

Резюмируя, можно сформулировать следующие правила:

Такой подход позволяет перенести вычислительную нагрузку с уровня приложения (PHP) на СУБД, что упрощает код и повышает производительность при обработке больших объёмов данных.

Всегда создавайте генерируемые столбцы и представления с помощью миграций, чтобы команда знала об их наличии.

Важно учитывать: выбирая генерируемые столбцы или представления для повышения производительности, вы также принимаете на себя связанные с этим компромиссы — в частности, усиление зависимости от синтаксиса конкретной СУБД. Это может создать сложности при миграции на другую базу данных (например, PostgreSQL) или при использовании SQLite для тестирования. Необходимо тщательно оценивать выигрыш в производительности относительно потенциальных затрат на поддержку и будущую миграцию.

Комментарии


Дополнительные материалы

Предыдущая Статья

PHP 8.5: CLI/CGI: Удалён параметр `-z` / `--zend-extension`

Следующая Статья

Частичное применение функций появится в PHP 8.6