> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 쿼리 빌더

> Laravel의 DB::table()을 사용한 쿼리 빌더의 기본부터 응용까지 설명합니다. Eloquent를 사용하지 않고 직접 SQL을 조립하는 방법과 구분 사용을 배웁니다.

## 쿼리 빌더란

Laravel의 쿼리 빌더는 데이터베이스 쿼리를 유창한 인터페이스로 구축·실행하는 구조입니다. `DB` 파사드의 `table()` 메서드로부터 시작해, 메서드 체인으로 쿼리를 조립합니다.

```php theme={null}
use Illuminate\Support\Facades\DB;

$users = DB::table('users')->get();
```

내부에서는 PDO 파라미터 바인딩을 사용하고 있기 때문에, SQL 인젝션 대책이 자동으로 이루어집니다.

<Info>
  쿼리 빌더는 Laravel이 지원하는 모든 데이터베이스(MySQL, MariaDB, PostgreSQL, SQLite, SQL Server)에서 동작합니다. 데이터베이스를 전환해도 같은 코드를 사용할 수 있습니다.
</Info>

## Eloquent와의 구분 사용

| 상황                | 권장       |
| ----------------- | -------- |
| 모델과 릴레이션이 필요      | Eloquent |
| 복잡한 집계나 리포트       | 쿼리 빌더    |
| 성능이 중요한 대량 데이터 처리 | 쿼리 빌더    |
| 기존 테이블에 대한 단순한 조작 | 쿼리 빌더    |
| 마이그레이션이나 시더 내의 처리 | 쿼리 빌더    |

## 데이터 취득

### 전건 취득

```php theme={null}
$users = DB::table('users')->get();

foreach ($users as $user) {
    echo $user->name;
}
```

`get()`은 `Illuminate\Support\Collection`을 반환합니다. 각 레코드는 PHP의 `stdClass` 오브젝트입니다.

### 1건 취득

```php theme={null}
// 최초 1건을 취득 (찾지 못하면 null)
$user = DB::table('users')->where('name', '홍길동')->first();

// 찾지 못하면 예외를 던진다 (404 응답을 자동 반환)
$user = DB::table('users')->where('name', '홍길동')->firstOrFail();

// 특정 컬럼의 값만 취득
$email = DB::table('users')->where('name', '홍길동')->value('email');

// ID로 취득
$user = DB::table('users')->find(3);
```

### 특정 컬럼의 값을 리스트로 취득

```php theme={null}
// email 컬럼 값의 컬렉션
$emails = DB::table('users')->pluck('email');

// name을 키, email을 값으로 하는 연관 컬렉션
$emailByName = DB::table('users')->pluck('email', 'name');
```

### 대량 데이터의 분할 처리

```php theme={null}
use Illuminate\Support\Collection;

// 100건씩 처리
DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
    foreach ($users as $user) {
        // 처리...
    }
});

// 업데이트하면서 청크 처리하는 경우는 chunkById를 사용
DB::table('users')->where('active', false)
    ->chunkById(100, function (Collection $users) {
        foreach ($users as $user) {
            DB::table('users')
                ->where('id', $user->id)
                ->update(['active' => true]);
        }
    });
```

<Warning>
  청크 처리 중에 레코드를 업데이트·삭제하는 경우, `chunk()`가 아닌 `chunkById()`를 사용해 주세요. `chunk()`에서는 레코드의 어긋남이 생길 수 있습니다.
</Warning>

### 스트리밍 (LazyCollection)

```php theme={null}
DB::table('users')->orderBy('id')->lazy()->each(function (object $user) {
    // 1건씩 처리
});
```

## 집계

```php theme={null}
$count   = DB::table('users')->count();
$maxAge  = DB::table('users')->max('age');
$minAge  = DB::table('users')->min('age');
$avgAge  = DB::table('users')->avg('age');
$total   = DB::table('orders')->sum('amount');

// 조건부 집계
$avgPremium = DB::table('orders')
    ->where('plan', 'premium')
    ->avg('amount');
```

### 레코드의 존재 확인

```php theme={null}
if (DB::table('orders')->where('finalized', 1)->exists()) {
    // 레코드가 존재
}

if (DB::table('orders')->where('finalized', 1)->doesntExist()) {
    // 레코드가 존재하지 않음
}
```

## SELECT 절

```php theme={null}
// 취득할 컬럼을 지정
$users = DB::table('users')
    ->select('name', 'email as user_email')
    ->get();

// 중복을 제외
$users = DB::table('users')->distinct()->get();

// 나중에 컬럼을 추가
$query = DB::table('users')->select('name');
$users = $query->addSelect('age')->get();
```

## WHERE 절

### 기본적인 조건

```php theme={null}
// 등가 조건 (=는 생략 가능)
$users = DB::table('users')->where('votes', 100)->get();

// 비교 연산자를 지정
$users = DB::table('users')->where('votes', '>=', 100)->get();
$users = DB::table('users')->where('name', 'like', '홍%')->get();

// 여러 조건 (AND)
$users = DB::table('users')
    ->where('status', 'active')
    ->where('age', '>', 20)
    ->get();

// OR 조건
$users = DB::table('users')
    ->where('votes', '>', 100)
    ->orWhere('name', '홍길동')
    ->get();
```

### 조건의 그룹화

```php theme={null}
use Illuminate\Database\Query\Builder;

// OR 조건을 그룹화하여 AND와 조합
$users = DB::table('users')
    ->where('active', true)
    ->where(function (Builder $query) {
        $query->where('role', 'admin')
              ->orWhere('role', 'moderator');
    })
    ->get();
// WHERE active = 1 AND (role = 'admin' OR role = 'moderator')
```

### whereIn / whereBetween / whereNull

```php theme={null}
// IN 절
$users = DB::table('users')
    ->whereIn('id', [1, 2, 3])
    ->get();

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

// BETWEEN 절
$users = DB::table('users')
    ->whereBetween('age', [20, 40])
    ->get();

// NULL 판정
$users = DB::table('users')->whereNull('deleted_at')->get();
$users = DB::table('users')->whereNotNull('email_verified_at')->get();
```

### whereLike (패턴 매치)

```php theme={null}
// 기본적으로 대문자 소문자를 구별하지 않음
$users = DB::table('users')
    ->whereLike('name', '%홍%')
    ->get();

// 대문자 소문자를 구별
$users = DB::table('users')
    ->whereLike('name', '%Hong%', caseSensitive: true)
    ->get();
```

### whereAny / whereAll (여러 컬럼에 대한 동일 조건)

```php theme={null}
// 어느 하나의 컬럼이 LIKE 조건에 매치
$users = DB::table('users')
    ->where('active', true)
    ->whereAny(['name', 'email', 'bio'], 'like', '%Laravel%')
    ->get();

// 모든 컬럼이 LIKE 조건에 매치
$posts = DB::table('posts')
    ->whereAll(['title', 'content'], 'like', '%Laravel%')
    ->get();
```

### whereNullSafeEquals (NULL 안전한 등가 비교)

`whereNullSafeEquals`와 `orWhereNullSafeEquals`는, 컬럼의 값을 지정한 값과 비교할 때 **두 개의 NULL 값을 동등한 것으로 간주하는** 비교를 수행합니다.

통상의 `=` 연산자로는 `NULL = NULL`은 `false`가 되지만, `whereNullSafeEquals`에서는 `NULL`끼리를 동등하다고 판정합니다. MySQL의 `<=>` 연산자, PostgreSQL의 `IS NOT DISTINCT FROM`에 대응합니다.

```php theme={null}
$lastLoginIp = $request->input('last_login_ip');

// $lastLoginIp가 null인 경우에도 last_login_ip IS NULL의 사용자를 취득할 수 있음
$users = DB::table('users')
    ->whereNullSafeEquals('last_login_ip', $lastLoginIp)
    ->get();
```

<Info>
  통상의 `where('column', null)`은 `WHERE column IS NULL`로 변환되지만, `whereNullSafeEquals('column', $value)`는 바인딩된 값이 `null`이든 `null`이 아니든 일관되게 동작합니다. 사용자 입력값이 `null`이 될 수 있는 경우에 특히 편리합니다.
</Info>

## JOIN

```php theme={null}
// INNER JOIN
$users = DB::table('users')
    ->join('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.name', 'orders.amount')
    ->get();

// LEFT JOIN
$users = DB::table('users')
    ->leftJoin('orders', 'users.id', '=', 'orders.user_id')
    ->get();

// 여러 테이블의 JOIN
$users = DB::table('users')
    ->join('contacts', 'users.id', '=', 'contacts.user_id')
    ->join('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.*', 'contacts.phone', 'orders.amount')
    ->get();
```

### 서브 쿼리 JOIN

```php theme={null}
// 서브 쿼리를 사용한 JOIN
$latestOrders = DB::table('orders')
    ->select('user_id', DB::raw('MAX(created_at) as last_order_at'))
    ->groupBy('user_id');

$users = DB::table('users')
    ->joinSub($latestOrders, 'latest_orders', function ($join) {
        $join->on('users.id', '=', 'latest_orders.user_id');
    })
    ->get();
```

## 정렬·그룹화·리미트

```php theme={null}
// 정렬
$users = DB::table('users')
    ->orderBy('name', 'asc')
    ->get();

// 여러 컬럼으로 정렬
$users = DB::table('users')
    ->orderBy('last_name')
    ->orderBy('first_name', 'desc')
    ->get();

// 랜덤 순
$users = DB::table('users')->inRandomOrder()->get();

// 그룹화
$orders = DB::table('orders')
    ->select('status', DB::raw('COUNT(*) as count'))
    ->groupBy('status')
    ->get();

// HAVING 절
$orders = DB::table('orders')
    ->select('user_id', DB::raw('SUM(amount) as total'))
    ->groupBy('user_id')
    ->having('total', '>', 10000)
    ->get();

// 리미트와 오프셋
$users = DB::table('users')
    ->skip(10)   // OFFSET
    ->take(5)    // LIMIT
    ->get();
```

## 서브 쿼리

```php theme={null}
// WHERE 절의 서브 쿼리
$activeUsers = DB::table('users')->select('id')->where('is_active', 1);

$comments = DB::table('comments')
    ->whereIn('user_id', $activeUsers)
    ->get();

// SELECT 절의 서브 쿼리
$users = DB::table('users')
    ->select('name')
    ->selectSub(function ($query) {
        $query->from('orders')
              ->selectRaw('COUNT(*)')
              ->whereColumn('orders.user_id', 'users.id');
    }, 'order_count')
    ->get();
```

## Raw 식

<Warning>
  Raw 식은 SQL 문자열로 직접 쿼리에 삽입됩니다. 사용자 입력을 직접 전달하면 SQL 인젝션의 위험이 있습니다. 반드시 바인딩을 사용해 안전하게 기술해 주세요.
</Warning>

```php theme={null}
// DB::raw() — 임의의 SQL 식을 삽입
$users = DB::table('users')
    ->select(DB::raw('count(*) as user_count, status'))
    ->groupBy('status')
    ->get();

// selectRaw — SELECT 절에 Raw 식을 추가
$orders = DB::table('orders')
    ->selectRaw('price * ? as price_with_tax', [1.10])
    ->get();

// whereRaw — WHERE 절에 Raw 식을 추가
$orders = DB::table('orders')
    ->whereRaw('price > IF(state = "JP", ?, 100)', [500])
    ->get();

// havingRaw — HAVING 절에 Raw 식을 추가
$orders = DB::table('orders')
    ->select('department', DB::raw('SUM(amount) as total'))
    ->groupBy('department')
    ->havingRaw('SUM(amount) > ?', [100000])
    ->get();

// orderByRaw — ORDER BY 절에 Raw 식을 추가
$orders = DB::table('orders')
    ->orderByRaw('updated_at - created_at DESC')
    ->get();
```

## INSERT / UPDATE / DELETE

### INSERT

```php theme={null}
// 1건 삽입
DB::table('users')->insert([
    'email' => 'hong@example.com',
    'name'  => '홍길동',
]);

// 여러 건 삽입
DB::table('users')->insert([
    ['email' => 'hong@example.com', 'name' => '홍길동'],
    ['email' => 'kim@example.com', 'name' => '김철수'],
]);

// 삽입 후에 AUTO_INCREMENT의 ID를 취득
$id = DB::table('users')->insertGetId([
    'email' => 'lee@example.com',
    'name'  => '이영희',
]);
```

### UPSERT (INSERT OR UPDATE)

```php theme={null}
// 존재하면 업데이트, 없으면 삽입
DB::table('users')->upsert(
    [
        ['email' => 'hong@example.com', 'name' => '홍길동', 'votes' => 5],
        ['email' => 'kim@example.com', 'name' => '김철수', 'votes' => 10],
    ],
    uniqueBy: ['email'],    // 중복 체크의 컬럼
    update: ['name', 'votes'] // 업데이트할 컬럼
);
```

### UPDATE

```php theme={null}
// 조건부 업데이트
$affected = DB::table('users')
    ->where('id', 1)
    ->update(['name' => '홍길순', 'updated_at' => now()]);

// 인크리먼트·디크리먼트
DB::table('users')->where('id', 1)->increment('votes');       // +1
DB::table('users')->where('id', 1)->increment('votes', 5);    // +5
DB::table('users')->where('id', 1)->decrement('votes');       // -1
DB::table('users')->where('id', 1)->decrement('balance', 100); // -100
```

### DELETE

```php theme={null}
// 조건부 삭제
$deleted = DB::table('users')->where('status', 'inactive')->delete();

// 테이블 전건 삭제 (AUTO_INCREMENT를 리셋)
DB::table('users')->truncate();
```

## 조건부 쿼리 (when)

쿼리 조건을 동적으로 적용하고 싶을 때는 `when()`을 사용하면 조건 분기를 깔끔하게 작성할 수 있습니다.

```php theme={null}
$status = request('status');
$sortBy = request('sort', 'name');

$users = DB::table('users')
    ->when($status, function ($query, $status) {
        $query->where('status', $status);
    })
    ->when($sortBy === 'email', function ($query) {
        $query->orderBy('email');
    }, function ($query) {
        $query->orderBy('name');
    })
    ->get();
```

## 디버그

```php theme={null}
// 생성되는 SQL을 확인
$sql = DB::table('users')->where('active', true)->toSql();
// "select * from `users` where `active` = ?"

// SQL과 바인딩을 양쪽 확인
$bindings = DB::table('users')->where('active', true)->getBindings();

// 쿼리를 실행하여 덤프 (실행은 계속)
DB::table('users')->where('active', true)->dump();

// 쿼리를 실행하여 덤프하고 종료
DB::table('users')->where('active', true)->dd();
```

<Tip>
  `dd()`는 디버그 시에 편리하지만, 프로덕션 환경에서는 절대로 사용하지 말아 주세요. `toSql()`과 `getBindings()`로 SQL과 바인딩을 확인하는 것이 안전합니다.
</Tip>

## 정리

<AccordionGroup>
  <Accordion title="자주 사용하는 메서드 일람">
    | 메서드               | 설명                 |
    | ----------------- | ------------------ |
    | `get()`           | 전건 취득 (Collection) |
    | `first()`         | 최초의 1건 취득          |
    | `find($id)`       | ID로 1건 취득          |
    | `value($column)`  | 컬럼의 값을 1건 취득       |
    | `pluck($column)`  | 컬럼 값의 리스트를 취득      |
    | `count()`         | 건수                 |
    | `sum($col)`       | 합계                 |
    | `avg($col)`       | 평균                 |
    | `max($col)`       | 최댓값                |
    | `min($col)`       | 최솟값                |
    | `exists()`        | 존재 확인              |
    | `insert([...])`   | 삽입                 |
    | `update([...])`   | 업데이트               |
    | `delete()`        | 삭제                 |
    | `chunk($n, fn)`   | 분할하여 처리            |
    | `when($cond, fn)` | 조건부 쿼리             |
    | `toSql()`         | 생성 SQL을 확인         |
  </Accordion>

  <Accordion title="쿼리 빌더 vs Eloquent">
    쿼리 빌더는 Eloquent보다도 저레벨이며, 모델의 인스턴스가 아닌 `stdClass`를 반환합니다.
    릴레이션이나 모델 이벤트(옵저버)가 필요 없는 경우, 쿼리 빌더 쪽이 심플하고 고속입니다.

    ```php theme={null}
    // Eloquent: User 모델의 인스턴스를 반환
    $users = User::where('active', true)->get();
    echo $users[0]->name; // User 오브젝트

    // 쿼리 빌더: stdClass를 반환
    $users = DB::table('users')->where('active', true)->get();
    echo $users[0]->name; // stdClass 오브젝트
    ```
  </Accordion>
</AccordionGroup>


## Related topics

- [MongoDB](/ko/mongodb.md)
- [검색](/ko/search.md)
- [PHP 어트리뷰트](/ko/advanced/php-attributes.md)
- [Laravel 13 신기능 정리](/ko/blog/laravel-13-new-features.md)
- [2026년 3월 Laravel 업데이트](/ko/blog/changelog/202603.md)
