HardТеория9 min

Query Builder (Конструктор запросов)

Selects, joins, wheres, ordering, grouping, aggregates, inserts, updates, deletes, raw expressions, chunking, lazy collections

Query Builder Laravel предоставляет удобный, fluent-интерфейс для создания и выполнения SQL-запросов. Он использует PDO parameter binding для защиты от SQL-инъекций. Работает со всеми поддерживаемыми СУБД: MySQL, PostgreSQL, SQLite, SQL Server.

Получение результатов (Select)

use Illuminate\Support\Facades\DB;

// Get all rows from a table
$users = DB::table('users')->get();
// Returns: Illuminate\Support\Collection

// Get a single row
$user = DB::table('users')->where('name', 'John')->first();
// Returns: stdClass|null

// Get a single row or throw exception
$user = DB::table('users')->where('name', 'John')->firstOrFail();
// Throws: Illuminate\Database\RecordNotFoundException

// Get a single column value
$email = DB::table('users')->where('name', 'John')->value('email');
// Returns: string|null

// Get a single row by ID
$user = DB::table('users')->find(3);
// Returns: stdClass|null

// Get a collection of single column values
$emails = DB::table('users')->pluck('email');
// Returns: Collection of strings

// Pluck with custom keys
$roles = DB::table('users')->pluck('name', 'email');
// Returns: Collection ['[email protected]' => 'John', '[email protected]' => 'Jane']

Выборка колонок

// Select specific columns
$users = DB::table('users')
    ->select('name', 'email as user_email')
    ->get();

// Add more columns to existing query
$query = DB::table('users')->select('name');
$users = $query->addSelect('email')->get();

// Distinct values
$titles = DB::table('posts')
    ->select('title')
    ->distinct()
    ->get();

Raw Expressions

use Illuminate\Support\Facades\DB;

// selectRaw
$orders = DB::table('orders')
    ->selectRaw('price * quantity as total_price')
    ->get();

// whereRaw
$orders = DB::table('orders')
    ->whereRaw('price > IF(state = "TX", ?, 100)', [200])
    ->get();

// havingRaw
$orders = DB::table('orders')
    ->select('department', DB::raw('SUM(price) as total_sales'))
    ->groupBy('department')
    ->havingRaw('SUM(price) > ?', [2500])
    ->get();

// orderByRaw
$orders = DB::table('orders')
    ->orderByRaw('updated_at - created_at DESC')
    ->get();

// groupByRaw
$orders = DB::table('orders')
    ->selectRaw('city, state, count(*) as total')
    ->groupByRaw('city, state')
    ->get();

// DB::raw() for inline expressions
$users = DB::table('users')
    ->select(DB::raw('count(*) as user_count, status'))
    ->where('status', '<>', 1)
    ->groupBy('status')
    ->get();

Joins

use Illuminate\Support\Facades\DB;

// Inner Join
$users = DB::table('users')
    ->join('contacts', 'users.id', '=', 'contacts.user_id')
    ->join('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.*', 'contacts.phone', 'orders.price')
    ->get();

// Left Join
$users = DB::table('users')
    ->leftJoin('posts', 'users.id', '=', 'posts.user_id')
    ->get();

// Right Join
$users = DB::table('users')
    ->rightJoin('posts', 'users.id', '=', 'posts.user_id')
    ->get();

// Cross Join
$sizes = DB::table('sizes')
    ->crossJoin('colors')
    ->get();

// Advanced Join (multiple conditions)
$users = DB::table('users')
    ->join('contacts', function ($join) {
        $join->on('users.id', '=', 'contacts.user_id')
             ->where('contacts.is_primary', true);
    })
    ->get();

// Lateral Join (PostgreSQL)
$latestPosts = DB::table('users')
    ->joinLateral(
        DB::table('posts')
            ->whereColumn('posts.user_id', 'users.id')
            ->orderByDesc('created_at')
            ->limit(3),
        'latest_posts'
    )
    ->get();

// Subquery Join
$latestPrices = DB::table('products')
    ->joinSub(
        DB::table('prices')
            ->select('product_id', DB::raw('MAX(created_at) as latest_date'))
            ->groupBy('product_id'),
        'latest_prices',
        'products.id',
        '=',
        'latest_prices.product_id'
    )
    ->get();

Where-условия

use Illuminate\Support\Facades\DB;

// Basic where
$users = DB::table('users')
    ->where('votes', '=', 100)
    ->where('age', '>', 35)
    ->get();

// Shorthand (= is default)
$users = DB::table('users')->where('votes', 100)->get();

// Array of conditions
$users = DB::table('users')->where([
    ['status', '=', '1'],
    ['subscribed', '<>', '1'],
])->get();

// Or Where
$users = DB::table('users')
    ->where('votes', '>', 100)
    ->orWhere('name', 'John')
    ->get();

// Or Where with closure (grouped conditions)
$users = DB::table('users')
    ->where('votes', '>', 100)
    ->orWhere(function ($query) {
        $query->where('name', 'Abigail')
              ->where('votes', '>', 50);
    })
    ->get();
// SQL: SELECT * FROM users WHERE votes > 100 OR (name = 'Abigail' AND votes > 50)

// Where Not
$products = DB::table('products')
    ->whereNot(function ($query) {
        $query->where('clearance', true)
              ->orWhere('price', '<', 10);
    })
    ->get();

// Where Any / All (Laravel 11)
$users = DB::table('users')
    ->whereAny(['name', 'email', 'phone'], 'like', '%search%')
    ->get();
// SQL: WHERE (name LIKE '%search%' OR email LIKE '%search%' OR phone LIKE '%search%')

$users = DB::table('users')
    ->whereAll(['name', 'email'], 'like', '%john%')
    ->get();
// SQL: WHERE (name LIKE '%john%' AND email LIKE '%john%')

Специализированные Where

// whereBetween / whereNotBetween
$users = DB::table('users')
    ->whereBetween('votes', [1, 100])
    ->get();

// whereIn / whereNotIn
$users = DB::table('users')
    ->whereIn('id', [1, 2, 3])
    ->get();

// whereNull / whereNotNull
$users = DB::table('users')
    ->whereNull('updated_at')
    ->get();

// whereDate / whereMonth / whereDay / whereYear / whereTime
$users = DB::table('users')
    ->whereDate('created_at', '2024-12-31')
    ->get();

$users = DB::table('users')
    ->whereMonth('created_at', '12')
    ->get();

$users = DB::table('users')
    ->whereYear('created_at', '2024')
    ->get();

// whereColumn (compare two columns)
$users = DB::table('users')
    ->whereColumn('first_name', 'last_name')
    ->get();

$users = DB::table('users')
    ->whereColumn('updated_at', '>', 'created_at')
    ->get();

// whereExists
$users = DB::table('users')
    ->whereExists(function ($query) {
        $query->select(DB::raw(1))
              ->from('orders')
              ->whereColumn('orders.user_id', 'users.id');
    })
    ->get();

// JSON Where (for JSON columns)
$users = DB::table('users')
    ->where('preferences->dining->meal', 'salad')
    ->get();

$users = DB::table('users')
    ->whereJsonContains('options->languages', 'en')
    ->get();

$users = DB::table('users')
    ->whereJsonLength('options->languages', '>', 1)
    ->get();

// Full Text Where (MySQL/PostgreSQL)
$users = DB::table('users')
    ->whereFullText('bio', 'web developer')
    ->get();

Subquery Where

// Where with subquery
$users = DB::table('users')
    ->where(function ($query) {
        $query->select('type')
              ->from('memberships')
              ->whereColumn('memberships.user_id', 'users.id')
              ->orderByDesc('memberships.start_date')
              ->limit(1);
    }, 'Pro')
    ->get();

// Select with subquery
$users = DB::table('users')
    ->addSelect([
        'last_login' => DB::table('logins')
            ->select('created_at')
            ->whereColumn('user_id', 'users.id')
            ->orderByDesc('created_at')
            ->limit(1),
    ])
    ->get();

Ordering, Grouping, Limit & Offset

use Illuminate\Support\Facades\DB;

// Order By
$users = DB::table('users')
    ->orderBy('name', 'desc')
    ->orderBy('email', 'asc')
    ->get();

// Latest / Oldest (shorthand for orderBy created_at)
$users = DB::table('users')->latest()->get();
$users = DB::table('users')->oldest()->get();
$users = DB::table('users')->latest('updated_at')->get();

// Random order
$randomUser = DB::table('users')->inRandomOrder()->first();

// Reorder (remove all existing orders)
$query = DB::table('users')->orderBy('name');
$users = $query->reorder()->orderBy('email')->get();

// Group By / Having
$users = DB::table('users')
    ->select('account_id', DB::raw('count(*) as total'))
    ->groupBy('account_id')
    ->having('total', '>', 5)
    ->get();

// Limit & Offset
$users = DB::table('users')
    ->offset(10)
    ->limit(5)
    ->get();

// Or using skip/take (aliases)
$users = DB::table('users')->skip(10)->take(5)->get();

Aggregates

use Illuminate\Support\Facades\DB;

$count = DB::table('users')->count();
$max   = DB::table('orders')->max('price');
$min   = DB::table('orders')->min('price');
$avg   = DB::table('orders')->avg('price');
$sum   = DB::table('orders')->sum('price');

// Aggregate with conditions
$price = DB::table('orders')
    ->where('finalized', true)
    ->avg('price');

// Exists
$exists = DB::table('orders')
    ->where('finalized', true)
    ->exists();

$doesntExist = DB::table('orders')
    ->where('finalized', false)
    ->doesntExist();

Insert

use Illuminate\Support\Facades\DB;

// Single row
DB::table('users')->insert([
    'email' => '[email protected]',
    'votes' => 0,
]);

// Multiple rows
DB::table('users')->insert([
    ['email' => '[email protected]', 'votes' => 0],
    ['email' => '[email protected]', 'votes' => 0],
]);

// Insert and get auto-incrementing ID
$id = DB::table('users')->insertGetId([
    'email' => '[email protected]',
    'votes' => 0,
]);

// Insert or Ignore (skip duplicates)
DB::table('users')->insertOrIgnore([
    ['id' => 1, 'email' => '[email protected]'],
    ['id' => 2, 'email' => '[email protected]'],
]);

// Upsert (insert or update on conflict)
DB::table('flights')->upsert(
    [
        ['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
        ['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150],
    ],
    uniqueBy: ['departure', 'destination'],  // Conflict columns
    update: ['price'],                       // Columns to update on conflict
);

Update

use Illuminate\Support\Facades\DB;

// Basic update
$affected = DB::table('users')
    ->where('id', 1)
    ->update(['votes' => 1]);

// Update or Insert (updateOrInsert)
DB::table('users')->updateOrInsert(
    ['email' => '[email protected]'],   // Search conditions
    ['name' => 'John', 'votes' => 2],  // Values to update/insert
);

// Increment / Decrement
DB::table('users')->increment('votes');
DB::table('users')->increment('votes', 5);
DB::table('users')->decrement('votes');
DB::table('users')->decrement('votes', 5);

// Increment with additional update
DB::table('users')->increment('votes', 1, ['name' => 'John']);

// Increment or Decrement multiple columns
DB::table('users')->incrementEach([
    'votes' => 5,
    'balance' => 100,
]);

Delete

use Illuminate\Support\Facades\DB;

// Delete rows
$deleted = DB::table('users')->delete();
$deleted = DB::table('users')->where('votes', '>', 100)->delete();

// Truncate (empty table, reset auto-increment)
DB::table('users')->truncate();

Chunking (Обработка большого объёма данных)

use Illuminate\Support\Facades\DB;

// chunk() - process in batches
DB::table('users')->orderBy('id')->chunk(100, function ($users) {
    foreach ($users as $user) {
        // Process each user
    }

    // Return false to stop chunking
    // return false;
});

// chunkById() - safer for updates (uses id-based pagination)
DB::table('users')->chunkById(100, function ($users) {
    foreach ($users as $user) {
        DB::table('users')
            ->where('id', $user->id)
            ->update(['active' => true]);
    }
});

Lazy Collections

use Illuminate\Support\Facades\DB;

// lazy() - returns LazyCollection (memory efficient)
DB::table('users')->orderBy('id')->lazy()->each(function ($user) {
    // Process one user at a time, keeping memory low
});

// lazyById() - uses id-based cursor (safer for updates)
DB::table('users')->lazyById()->each(function ($user) {
    // Process each user
});

// lazyByIdDesc() - reverse order
DB::table('users')->lazyByIdDesc()->each(function ($user) {
    // Process in descending order
});

// Lazy with collection methods
$count = DB::table('users')
    ->orderBy('id')
    ->lazy()
    ->filter(fn ($user) => $user->active)
    ->count();

Cursor (наиболее эффективный по памяти)

// cursor() - uses PHP generator, keeps ONE row in memory
foreach (DB::table('users')->cursor() as $user) {
    // Only one row in memory at a time
}

Сравнение методов:

Метод Память Запросы Безопасность при UPDATE
get() Высокая (все в память) 1 -
chunk() Средняя (пакетами) N (offset) Нет (offset сдвигается)
chunkById() Средняя (пакетами) N (by id) Да
lazy() Низкая N (offset) Нет
lazyById() Низкая N (by id) Да
cursor() Минимальная 1 Нет

Pessimistic Locking

use Illuminate\Support\Facades\DB;

// Shared lock (for read)
$users = DB::table('users')
    ->where('votes', '>', 100)
    ->sharedLock()
    ->get();
// SQL: SELECT * FROM users WHERE votes > 100 LOCK IN SHARE MODE

// Exclusive lock (for update)
$users = DB::table('users')
    ->where('votes', '>', 100)
    ->lockForUpdate()
    ->get();
// SQL: SELECT * FROM users WHERE votes > 100 FOR UPDATE

Debugging

// Get the SQL and bindings
$query = DB::table('users')->where('id', 1);
echo $query->toSql();       // SELECT * FROM "users" WHERE "id" = ?
dump($query->getBindings()); // [1]

// toRawSql() - with bindings substituted
echo $query->toRawSql();    // SELECT * FROM "users" WHERE "id" = 1

// dd() / dump() - dump and die
DB::table('users')->where('id', 1)->dd();
DB::table('users')->where('id', 1)->dump();

// Explain
DB::table('users')
    ->where('votes', '>', 100)
    ->explain()
    ->dd();

Проверь себя

Что делает whereAny() в Laravel 11?

Какой метод наиболее эффективен по памяти при итерации по миллионам строк?

Какой SQL сгенерирует вызов lockForUpdate()?

Что делает метод upsert() в Query Builder?

Чем chunkById() отличается от chunk() при обработке больших таблиц?