Базы данных MySQL в PlanetScale работают на базе Vitess, которая накладывает ограничения на каждый запрос в своем стандартном режиме OLTP. В PlanetScale запрос может возвращать, обновлять или удалять не более 100 000 строк, а размер результирующего набора данных не должен превышать 64 МиБ. Отчетный запрос или экспорт CSV для большой таблицы могут завершиться ошибкой из-за этих ограничений, даже если тот же самый запрос успешно выполняется на локальном сервере MySQL.

В Vitess предусмотрен второй рабочий режим — OLAP, в котором эти ограничения отсутствуют. В Laravel 13.36 добавлен метод  DB::olap() , который переключает соединение в режим OLAP на время выполнения callback-функции.

#Ручное переключение режимов

До этого релиза запустить запрос в режиме OLAP можно было с помощью сырого выражения (raw statement) и блока  try / finally :

try {
    DB::statement("set workload = 'olap'");
 
    $rows = Order::query()->where('created_at', '>=', now()->subYears(3))->get();
} finally {
    DB::statement("set workload = 'oltp'");
}
 

Сырое выражение выполняется на соединении для записи. Если у соединения настроены реплики чтения, инструкция  select  отправляется на реплику чтения, которая все еще находится в режиме OLTP.

Режим работы также является настройкой сессии, поэтому пропущенный блок  finally  оставит соединение в режиме OLAP для следующей задачи в воркере очереди или следующего запроса в Octane.

#Использование DB::olap()

Джек Бейлисс (Jack Bayliss) добавил метод  olap()  в класс MySQL-соединения. Он устанавливает режим работы, выполняет callback и возвращает режим OLTP обратно в блоке  finally . Метод возвращает то же значение, что и переданный callback:

$revenue = DB::olap(fn () => Order::query()
    ->selectRaw("customer_id, date_format(created_at, '%Y-%m') as month, sum(total) as revenue")
    ->where('created_at', '>=', now()->subYears(3))
    ->groupBy('customer_id', 'month')
    ->get()
);
 

Метод устанавливает режим работы как для соединения записи, так и для соединения чтения, поэтому запрос выполняется в режиме OLAP независимо от используемого соединения. Если соединение считывания еще не установлено,  olap()  предварительно установит его.

Если база данных не работает на Vitess,  olap()  просто выполняет callback и больше ничего не делает. Вы можете использовать один и тот же код с локальным сервером MySQL, в CI и на PlanetScale.

Callback получает текущее соединение в качестве аргумента, что полезно при работе с соединением, отличным от соединения по умолчанию:

DB::connection('analytics')->olap(function ($connection) {
    return $connection->table('page_views')
        ->selectRaw('path, count(*) as views')
        ->groupBy('path')
        ->get();
});
 

Метод  olap()  определен в классе  MySqlConnection , поэтому вызов  DB::olap() , когда соединение по умолчанию настроено для PostgreSQL или SQLite, приведет к выбросу исключения  BadMethodCallException .

#Экспорт через очередь

Задача из очереди, которая записывает большой экспорт в файл, может выполнять все запросы внутри callback-функции:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
 
class ExportOrders implements ShouldQueue
{
    use Queueable;
 
    public function __construct(public string $path) {}
 
    public function handle(): void
    {
        $stream = fopen('php://temp', 'w+');
 
        fputcsv($stream, ['id', 'customer_id', 'total', 'created_at']);
 
        DB::olap(function () use ($stream) {
            Order::query()
                ->select('id', 'customer_id', 'total', 'created_at')
                ->lazyById(5000)
                ->each(fn (Order $order) => fputcsv($stream, [
                    $order->id,
                    $order->customer_id,
                    $order->total,
                    $order->created_at,
                ]));
        });
 
        rewind($stream);
 
        Storage::put($this->path, $stream);
    }
}
 

Каждый чанк, получаемый методом  lazyById() , обрабатывается в режиме OLAP. Как только callback завершает работу, соединение воркера возвращается в режим OLTP для выполнения следующей задачи.

#Выполняйте запросы внутри callback-функции

Режим переключается обратно сразу после того, как callback возвращает управление. Ленивый (lazy) результат, запрос которого еще не был выполнен, выполнится позже — уже в режиме OLTP:

// The query runs after olap() has switched back to OLTP
$orders = DB::olap(fn () => Order::query()->cursor());
 
foreach ($orders as $order) {
    // ...
}
 

Методы  cursor() ,  lazy()  и  lazyById()  возвращают объект  LazyCollection , который выполняет запрос в момент итерации. Выполняйте итерацию внутри callback-функции, как в примере с экспортом выше, или вызывайте метод  get()  прямо там.

#Определение Vitess

Тот же PR добавляет в класс MySQL-соединения метод  isVitess() . Он возвращает  true , если строка версии сервера содержит  Vitess  или  PlanetScale :

if (DB::connection()->isVitess()) {
    // PlanetScale-specific behavior
}
 

Метод  olap()  использует эту проверку, чтобы решить, следует ли переключать режим работы. Как и  isMaria() , он подключается к базе данных для чтения версии сервера.

#Когда использовать режим OLAP

Документация PlanetScale не рекомендует использовать режим OLAP для обычных запросов приложения. Ограничения OLTP защищают ресурсы базы данных от истощения долгоиграющими запросами. На странице системных ограничений рекомендуется разбивать большие запросы на страницы или копировать данные в отдельную аналитическую базу данных для построения отчетов. В ней также предупреждается, что OLAP-запросы могут завершаться с ошибками при прерывании соединения с PlanetScale.

Метод  DB::olap()  отлично подходит для периодического экспорта, заполнения данных (backfill) или административных отчетов, которым необходимо обработать более 100 000 строк за один запрос.

#Дополнительное чтение

Джек Бейлисс добавил методы  olap()  и  isVitess()  в рамках пулл-реквеста #61932.