데이터베이스: 쿼리 빌더
업데이트됨번역일: 2026년 7월 28일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 7월 28일
- 번역 갱신
- 2026년 7월 28일
데이터베이스: 쿼리 빌더
- 소개
- 데이터베이스 쿼리 실행
- Select 구문
- Raw 표현식
- Join
- Union
- 기본 Where 절
- 고급 Where 절
- 정렬, 그룹화, Limit, Offset
- 조건부 절
- Insert 구문
- Update 구문
- Delete 구문
- 비관적 잠금
- 재사용 가능한 쿼리 컴포넌트
- 디버깅
소개
Laravel의 데이터베이스 쿼리 빌더는 데이터베이스 쿼리를 편리하고 유연하게 작성할 수 있는 인터페이스를 제공합니다. 대부분의 데이터베이스 작업을 PHP 코드로 간결하게 표현할 수 있으며, Laravel이 지원하는 모든 데이터베이스에서 동일하게 동작합니다.
Laravel 쿼리 빌더는 SQL 인젝션 공격을 방어하기 위해 PDO 파라미터 바인딩을 사용합니다. 쿼리 빌더에 전달하는 값을 별도로 이스케이프하거나 정제할 필요가 없습니다.
WARNING
PDO는 컬럼명 바인딩을 지원하지 않습니다. 따라서 쿼리에서 참조하는 컬럼명(특히 order by 절 등)에 사용자 입력값이 직접 반영되지 않도록 주의해야 합니다.
데이터베이스 쿼리 실행
테이블의 모든 행 조회
DB 파사드의 table 메서드로 쿼리를 시작합니다. table 메서드는 해당 테이블에 대한 쿼리 빌더 인스턴스를 반환하며, 이후 다양한 조건을 메서드 체이닝으로 추가하고 마지막에 get 메서드를 호출해 결과를 가져올 수 있습니다.
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
/**
* 모든 사용자 목록을 보여줍니다.
*/
public function index()
{
$users = DB::table('users')->get();
return view('user.index', ['users' => $users]);
}
}get 메서드는 쿼리 결과를 Illuminate\Support\Collection 인스턴스로 반환합니다. 각 행은 PHP stdClass 객체로 표현됩니다. 컬럼 값은 객체의 프로퍼티로 접근할 수 있습니다.
use Illuminate\Support\Facades\DB;
$users = DB::table('users')->get();
foreach ($users as $user) {
echo $user->name;
}NOTE
Laravel 컬렉션은 데이터를 매핑, 필터링하는 등 다양한 강력한 메서드를 제공합니다. 자세한 내용은 컬렉션 문서를 참고하세요.
단일 행 또는 컬럼 조회
테이블에서 단 하나의 행만 가져오려면 first 메서드를 사용하세요. stdClass 객체를 반환합니다.
$user = DB::table('users')->where('name', '홍길동')->first();
return $user->email;행 전체가 아닌 특정 컬럼의 값만 필요하다면 value 메서드를 사용하세요. 해당 컬럼의 값을 바로 반환합니다.
$email = DB::table('users')->where('name', '홍길동')->value('email');id 컬럼 값으로 특정 행을 조회하려면 find 메서드를 사용하세요.
$user = DB::table('users')->find(3);특정 컬럼 값 목록 조회
특정 컬럼의 값들만 뽑아 컬렉션으로 받고 싶다면 pluck 메서드를 사용하세요. 예를 들어 모든 사용자의 이름 목록을 가져올 수 있습니다.
use Illuminate\Support\Facades\DB;
$titles = DB::table('users')->pluck('name');
foreach ($titles as $title) {
echo $title;
}pluck의 두 번째 인자로 컬럼명을 지정하면, 반환되는 컬렉션의 키로 해당 컬럼 값이 사용됩니다.
$titles = DB::table('users')->pluck('name', 'id');
foreach ($titles as $id => $title) {
echo $title;
}결과 청크 처리
수천, 수만 건의 데이터를 한 번에 메모리에 올리면 성능 문제가 생길 수 있습니다. chunk 메서드를 사용하면 결과를 일정 단위로 나눠 처리할 수 있어 메모리를 효율적으로 사용할 수 있습니다. Artisan 커맨드에서 대량의 데이터를 처리할 때 특히 유용합니다.
use Illuminate\Support\Facades\DB;
DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
foreach ($users as $user) {
// ...
}
});클로저에서 false를 반환하면 이후 청크 처리를 중단합니다.
DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
// 레코드 처리...
return false; // 여기서 중단
});청크를 처리하면서 해당 레코드를 동시에 업데이트할 경우, 결과 순서가 예상과 달라질 수 있습니다. 청크 내에서 조회한 레코드를 업데이트할 때는 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를 사용하세요.
결과 지연 스트리밍
lazy 메서드는 chunk와 유사하게 쿼리를 청크 단위로 실행하지만, 각 청크를 콜백에 전달하는 대신 LazyCollection으로 반환합니다. 덕분에 결과 전체를 하나의 스트림처럼 다룰 수 있습니다.
use Illuminate\Support\Facades\DB;
DB::table('users')->orderBy('id')->lazy()->each(function (object $user) {
// ...
});마찬가지로, 순회하면서 레코드를 업데이트할 경우 lazyById 또는 lazyByIdDesc를 사용하는 것이 안전합니다.
DB::table('users')->where('active', false)
->lazyById()->each(function (object $user) {
DB::table('users')
->where('id', $user->id)
->update(['active' => true]);
});WARNING
lazy 사용 중 레코드를 추가하거나 삭제하면 결과가 예상과 달라질 수 있습니다. 순회 중 데이터를 수정할 때는 lazyById 또는 lazyByIdDesc를 사용하세요.
집계 함수
쿼리 빌더는 count, max, min, avg, sum 등의 집계 메서드를 제공합니다. 쿼리를 구성한 후 이 메서드들을 호출하면 됩니다.
use Illuminate\Support\Facades\DB;
$users = DB::table('users')->count();
$price = DB::table('orders')->max('price');물론 집계 메서드를 다른 절과 조합해 원하는 집계 범위를 정밀하게 지정할 수도 있습니다.
$price = DB::table('orders')
->where('finalized', 1)
->avg('price');레코드 존재 여부 확인
쿼리 조건에 맞는 레코드가 있는지 확인할 때 count 대신 exists나 doesntExist 메서드를 사용하면 의도가 더 명확하게 드러납니다.
if (DB::table('orders')->where('finalized', 1)->exists()) {
// ...
}
if (DB::table('orders')->where('finalized', 1)->doesntExist()) {
// ...
}Select 구문
Select 절 지정
항상 모든 컬럼을 가져올 필요는 없습니다. select 메서드로 원하는 컬럼만 지정할 수 있습니다.
use Illuminate\Support\Facades\DB;
$users = DB::table('users')
->select('name', 'email as user_email')
->get();distinct 메서드를 사용하면 중복을 제거한 결과를 반환합니다.
$users = DB::table('users')->distinct()->get();이미 쿼리 빌더 인스턴스가 있고 기존 select 절에 컬럼을 추가하고 싶다면 addSelect 메서드를 사용하세요.
$query = DB::table('users')->select('name');
$users = $query->addSelect('age')->get();Raw 표현식
쿼리 빌더가 제공하지 않는 복잡한 SQL 표현이 필요할 때는 DB::raw 메서드로 임의의 SQL 문자열을 쿼리에 삽입할 수 있습니다.
WARNING
Raw 표현식은 SQL 인젝션 취약점이 생길 수 있습니다. 사용자 입력값을 Raw 표현식에 직접 포함하지 마세요.
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();Raw 메서드
DB::raw 외에도 쿼리의 각 부분에 raw 표현식을 삽입하는 전용 메서드들이 있습니다.
WARNING
아래 메서드들도 마찬가지로 사용자 입력값을 직접 포함하면 SQL 인젝션 위험이 있습니다.
`selectRaw`
selectRaw 메서드는 addSelect(DB::raw(...)) 대신 사용할 수 있습니다. 두 번째 인자로 바인딩 배열을 전달할 수 있습니다.
$orders = DB::table('orders')
->selectRaw('price * ? as price_with_tax', [1.0825])
->get();`whereRaw / orWhereRaw`
whereRaw와 orWhereRaw는 쿼리에 raw where 절을 삽입합니다. 두 번째 인자로 바인딩 배열을 받습니다.
$orders = DB::table('orders')
->whereRaw('price > IF(state = "TX", ?, 100)', [200])
->get();`havingRaw / orHavingRaw`
havingRaw와 orHavingRaw는 having 절에 raw 문자열을 삽입합니다. 두 번째 인자로 바인딩 배열을 받습니다.
$orders = DB::table('orders')
->select('department', DB::raw('SUM(price) as total_sales'))
->groupBy('department')
->havingRaw('SUM(price) > ?', [2500])
->get();`orderByRaw`
orderByRaw는 order by 절에 raw 문자열을 삽입합니다.
$orders = DB::table('orders')
->orderByRaw('updated_at - created_at DESC')
->get();`groupByRaw`
groupByRaw는 group by 절에 raw 문자열을 삽입합니다.
$orders = DB::table('orders')
->select('city', 'state')
->groupByRaw('city, state')
->get();Join
Inner Join
쿼리 빌더로 다양한 Join을 표현할 수 있습니다. 기본 Inner Join은 join 메서드를 사용합니다. 첫 번째 인자는 조인할 테이블명, 이후 인자들은 조인 조건 컬럼입니다.
use Illuminate\Support\Facades\DB;
$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 / Right Join
Inner Join 대신 Left Join이나 Right Join을 하려면 leftJoin 또는 rightJoin 메서드를 사용하세요. 사용법은 join과 동일합니다.
$users = DB::table('users')
->leftJoin('posts', 'users.id', '=', 'posts.user_id')
->get();
$users = DB::table('users')
->rightJoin('posts', 'users.id', '=', 'posts.user_id')
->get();Cross Join
Cross Join은 crossJoin 메서드를 사용하세요. 두 테이블의 카테시안 곱을 반환합니다.
$sizes = DB::table('sizes')
->crossJoin('colors')
->get();고급 Join 조건
더 복잡한 Join 조건이 필요하다면 join 메서드의 두 번째 인자로 클로저를 전달하세요. 클로저는 Illuminate\Database\Query\JoinClause 인스턴스를 받으며, 이를 통해 다양한 Join 조건을 지정할 수 있습니다.
DB::table('users')
->join('contacts', function (JoinClause $join) {
$join->on('users.id', '=', 'contacts.user_id')->orOn(/* ... */);
})
->get();Join 절에 where 조건을 추가하려면 JoinClause의 where 또는 orWhere 메서드를 사용하세요. 두 컬럼을 비교하는 대신 값과 비교합니다.
DB::table('users')
->join('contacts', function (JoinClause $join) {
$join->on('users.id', '=', 'contacts.user_id')
->where('contacts.user_id', '>', 5);
})
->get();서브쿼리 Join
joinSub, leftJoinSub, rightJoinSub 메서드를 사용하면 서브쿼리를 Join 대상으로 사용할 수 있습니다. 각 메서드는 서브쿼리(빌더 인스턴스, 클로저, raw 문자열), 테이블 별칭, 조인 조건 컬럼을 인자로 받습니다.
$latestPosts = DB::table('posts')
->select('user_id', DB::raw('MAX(created_at) as last_post_created_at'))
->where('is_published', true)
->groupBy('user_id');
$users = DB::table('users')
->joinSub($latestPosts, 'latest_posts', function (JoinClause $join) {
$join->on('users.id', '=', 'latest_posts.user_id');
})->get();Lateral Join
WARNING
Lateral Join은 현재 PostgreSQL, MySQL >= 8.0.14, SQL Server에서 지원됩니다.
joinLateral과 leftJoinLateral 메서드를 사용하면 서브쿼리와 Lateral Join을 수행할 수 있습니다. 각 메서드는 서브쿼리와 테이블 별칭을 인자로 받습니다. Join 조건은 서브쿼리 내부의 where 절에서 지정합니다.
$latestPosts = DB::table('posts')
->select('id as post_id', 'title as post_title', 'created_at as post_created_at')
->whereColumn('user_id', 'users.id')
->orderBy('created_at', 'desc')
->limit(3);
$users = DB::table('users')
->joinLateral($latestPosts, 'latest_posts')
->get();Union
쿼리 빌더는 두 쿼리를 합치는 union 메서드를 제공합니다. 먼저 첫 번째 쿼리를 작성하고, union 메서드로 두 번째 쿼리를 연결합니다.
use Illuminate\Support\Facades\DB;
$first = DB::table('users')
->whereNull('first_name');
$users = DB::table('users')
->whereNull('last_name')
->union($first)
->get();union은 중복 레코드를 제거합니다. 중복을 포함한 결과가 필요하다면 unionAll 메서드를 사용하세요. 사용법은 union과 동일합니다.
기본 Where 절
Where 절
where 메서드를 사용해 쿼리에 조건을 추가할 수 있습니다. 가장 기본적인 형태는 컬럼명, 비교 연산자, 비교할 값 세 가지를 인자로 받습니다.
$users = DB::table('users')
->where('votes', '=', 100)
->get();편의상 단순 동등 비교(=)는 연산자를 생략하고 컬럼명과 값만 전달할 수 있습니다.
$users = DB::table('users')->where('votes', 100)->get();다른 연산자도 모두 사용할 수 있습니다.
$users = DB::table('users')
->where('votes', '>=', 100)
->get();
$users = DB::table('users')
->where('votes', '<>', 100)
->get();
$users = DB::table('users')
->where('name', 'like', '이%')
->get();where에 배열을 전달하면 여러 조건을 한 번에 추가할 수도 있습니다. 배열의 각 요소는 [컬럼, 연산자, 값] 형태입니다.
$users = DB::table('users')->where([
['status', '=', '1'],
['subscribed', '<>', '1'],
])->get();WARNING
PDO는 컬럼명 바인딩을 지원하지 않으므로, 쿼리에서 참조하는 컬럼명(특히 order by 포함)에 사용자 입력값을 직접 사용하지 마세요.
Or Where 절
여러 where 조건을 체이닝하면 기본적으로 AND로 연결됩니다. OR 조건이 필요하다면 orWhere 메서드를 사용하세요. where와 동일한 인자를 받습니다.
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere('name', '김철수')
->get();OR 조건을 괄호로 묶어야 한다면 orWhere의 첫 번째 인자로 클로저를 전달하세요.
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere(function (Builder $query) {
$query->where('name', '관리자')
->where('votes', '>', 50);
})
->get();위 코드는 다음 SQL을 생성합니다.
select * from users where votes > 100 or (name = '관리자' and votes > 50)Where Not 절
whereNot과 orWhereNot 메서드는 전달된 조건 그룹을 부정합니다.
$products = DB::table('products')
->whereNot(function (Builder $query) {
$query->where('clearance', true)
->orWhere('price', '<', 10);
})
->get();Where Any / All / None 절
동일한 조건을 여러 컬럼에 적용해야 할 때가 있습니다. whereAny 메서드는 지정한 컬럼 중 하나라도 조건을 만족하는 경우를 찾습니다.
$users = DB::table('users')
->where('active', true)
->whereAny([
'name',
'email',
'phone',
], 'like', '김%')
->get();위 코드는 다음 SQL을 생성합니다.
SELECT *
FROM users
WHERE active = true AND (
name LIKE '김%' OR
email LIKE '김%' OR
phone LIKE '김%'
)whereAll 메서드는 지정한 모든 컬럼이 조건을 만족하는 경우를 찾습니다.
$posts = DB::table('posts')
->where('published', true)
->whereAll([
'title',
'content',
], 'like', '%Laravel%')
->get();위 코드는 다음 SQL을 생성합니다.
SELECT *
FROM posts
WHERE published = true AND (
title LIKE '%Laravel%' AND
content LIKE '%Laravel%'
)whereNone 메서드는 지정한 모든 컬럼이 조건을 만족하지 않는 경우를 찾습니다.
$posts = DB::table('albums')
->where('published', true)
->whereNone([
'title',
'lyrics',
'tags',
], 'like', '%explicit%')
->get();위 코드는 다음 SQL을 생성합니다.
SELECT *
FROM albums
WHERE published = true AND NOT (
title LIKE '%explicit%' OR
lyrics LIKE '%explicit%' OR
tags LIKE '%explicit%'
)JSON Where 절
Laravel은 JSON 컬럼 타입을 지원하는 데이터베이스(MySQL 5.7+, PostgreSQL, SQL Server 2016, SQLite 3.39.0+)에서 JSON 컬럼 조회를 지원합니다. -> 연산자를 사용해 JSON 경로를 지정하세요.
$users = DB::table('users')
->where('preferences->dining->meal', 'salad')
->get();whereJsonContains로 JSON 배열에 특정 값이 포함되어 있는지 확인할 수 있습니다.
$users = DB::table('users')
->whereJsonContains('options->languages', 'en')
->get();MySQL과 PostgreSQL에서는 여러 값으로 배열을 전달할 수도 있습니다.
$users = DB::table('users')
->whereJsonContains('options->languages', ['en', 'de'])
->get();whereJsonLength로 JSON 배열의 길이를 기준으로 조회할 수 있습니다.
$users = DB::table('users')
->whereJsonLength('options->languages', 0)
->get();
$users = DB::table('users')
->whereJsonLength('options->languages', '>', 1)
->get();추가 Where 절
whereLike / orWhereLike / whereNotLike / orWhereNotLike
whereLike 메서드는 패턴 매칭을 통한 조회를 제공합니다. 기본적으로 대소문자를 구분하지 않으며, caseSensitive 인자로 설정할 수 있습니다.
$users = DB::table('users')
->whereLike('name', '%홍%')
->get();대소문자를 구분한 검색:
$users = DB::table('users')
->whereLike('name', '%홍%', caseSensitive: true)
->get();$users = DB::table('users')
->orWhereLike('name', '%홍%')
->get();
$users = DB::table('users')
->whereNotLike('name', '%홍%')
->get();
$users = DB::table('users')
->orWhereNotLike('name', '%홍%')
->get();whereBetween / orWhereBetween
whereBetween으로 컬럼 값이 두 값 사이에 있는지 확인합니다.
$users = DB::table('users')
->whereBetween('votes', [1, 100])
->get();whereNotBetween / orWhereNotBetween
whereNotBetween으로 두 값 범위 밖의 레코드를 조회합니다.
$users = DB::table('users')
->whereNotBetween('votes', [1, 100])
->get();whereBetweenColumns / whereNotBetweenColumns / orWhereBetweenColumns / orWhereNotBetweenColumns
컬럼 값이 같은 행의 두 다른 컬럼 값 사이에 있는지 확인합니다.
$patients = DB::table('patients')
->whereBetweenColumns('weight', ['minimum_allowed_weight', 'maximum_allowed_weight'])
->get();whereIn / whereNotIn / orWhereIn / orWhereNotIn
whereIn으로 컬럼 값이 지정한 배열에 포함되어 있는지 확인합니다.
$users = DB::table('users')
->whereIn('id', [1, 2, 3])
->get();whereNotIn으로 배열에 포함되지 않는 레코드를 조회합니다.
$users = DB::table('users')
->whereNotIn('id', [1, 2, 3])
->get();whereIn의 두 번째 인자로 쿼리 빌더 인스턴스를 전달할 수도 있습니다.
$activeUsers = DB::table('users')->select('id')->where('is_active', 1);
$users = DB::table('comments')
->whereIn('user_id', $activeUsers)
->get();WARNING
whereIn에 많은 수의 정수 바인딩을 추가하면 메모리 사용량이 크게 증가할 수 있습니다.
whereNull / whereNotNull / orWhereNull / orWhereNotNull
whereNull로 컬럼 값이 NULL인 레코드를 조회합니다.
$users = DB::table('users')
->whereNull('updated_at')
->get();whereNotNull로 컬럼 값이 NULL이 아닌 레코드를 조회합니다.
$users = DB::table('users')
->whereNotNull('updated_at')
->get();whereDate / whereMonth / whereDay / whereYear / whereTime
날짜 관련 비교를 위한 메서드들입니다.
$users = DB::table('users')
->whereDate('created_at', '2024-01-01')
->get();
$users = DB::table('users')
->whereMonth('created_at', '3')
->get();
$users = DB::table('users')
->whereDay('created_at', '15')
->get();
$users = DB::table('users')
->whereYear('created_at', '2024')
->get();
$users = DB::table('users')
->whereTime('created_at', '=', '11:20:45')
->get();whereColumn / orWhereColumn
whereColumn으로 두 컬럼 값이 같은지 비교합니다.
$users = DB::table('users')
->whereColumn('first_name', 'last_name')
->get();비교 연산자를 추가로 지정할 수도 있습니다.
$users = DB::table('users')
->whereColumn('updated_at', '>', 'created_at')
->get();배열로 여러 조건을 한 번에 전달할 수도 있으며, 조건들은 AND로 연결됩니다.
$users = DB::table('users')
->whereColumn([
['first_name', '=', 'last_name'],
['updated_at', '>', 'created_at'],
])->get();논리적 그룹화
여러 where 절을 괄호로 묶어 논리적으로 그룹화해야 할 때가 있습니다. 특히 orWhere와 함께 사용할 때 예상치 못한 동작을 방지하기 위해 그룹화가 필요합니다. where 메서드에 클로저를 전달하면 됩니다.
$users = DB::table('users')
->where('name', '=', '홍길동')
->where(function (Builder $query) {
$query->where('votes', '>', 100)
->orWhere('title', '=', 'Admin');
})
->get();위 코드는 다음 SQL을 생성합니다.
select * from users where name = '홍길동' and (votes > 100 or title = 'Admin')고급 Where 절
Where Exists 절
whereExists 메서드는 WHERE EXISTS SQL 절을 작성합니다. 클로저로 서브쿼리를 정의합니다.
$users = DB::table('users')
->whereExists(function (Builder $query) {
$query->select(DB::raw(1))
->from('orders')
->whereColumn('orders.user_id', 'users.id');
})
->get();클로저 대신 쿼리 빌더 인스턴스를 직접 전달할 수도 있습니다.
$orders = DB::table('orders')
->select(DB::raw(1))
->whereColumn('orders.user_id', 'users.id');
$users = DB::table('users')
->whereExists($orders)
->get();위 두 예시는 모두 다음 SQL을 생성합니다.
select * from users
where exists (
select 1
from orders
where orders.user_id = users.id
)서브쿼리 Where 절
서브쿼리의 결과를 직접 값과 비교하는 where 절을 작성할 수 있습니다. where 메서드의 첫 번째 인자로 클로저, 두 번째 인자로 연산자, 세 번째 인자로 비교 값을 전달합니다.
use App\Models\User;
use Illuminate\Database\Query\Builder;
$users = User::where(function (Builder $query) {
$query->select('type')
->from('membership')
->whereColumn('membership.user_id', 'users.id')
->orderByDesc('membership.start_date')
->limit(1);
}, 'Pro')->get();컬럼과 서브쿼리를 비교할 수도 있습니다. 첫 번째 인자로 컬럼명, 두 번째 인자로 연산자, 세 번째 인자로 서브쿼리 클로저를 전달합니다.
use App\Models\Income;
use Illuminate\Database\Query\Builder;
$incomes = Income::where('amount', '<', function (Builder $query) {
$query->selectRaw('avg(i.amount)')->from('incomes as i');
})->get();전문 검색 Where 절
WARNING
전문 검색 Where 절은 현재 MySQL과 PostgreSQL에서 지원됩니다.
whereFullText와 orWhereFullText 메서드는 전문 검색 인덱스가 설정된 컬럼에 대한 전문 검색 where 절을 추가합니다. Laravel이 사용하는 데이터베이스에 맞는 SQL로 자동 변환됩니다. MySQL에서는 MATCH AGAINST 절이 생성됩니다.
$users = DB::table('users')
->whereFullText('bio', '개발자')
->get();벡터 유사도 절
WARNING
벡터 유사도 절은 현재 PostgreSQL(pgvector 확장 필요)에서 지원됩니다. pgvector 설정에 대해서는 PostgreSQL 문서를 참고하세요.
whereVectorDistance와 orderByVectorDistance 메서드를 사용하면 벡터 컬럼과 지정한 벡터 간의 유사도를 기준으로 레코드를 조회하고 정렬할 수 있습니다.
use Illuminate\Database\Query\VectorDistance;
$documents = DB::table('documents')
->whereVectorDistance('embedding', [0.1, 0.2, 0.3], VectorDistance::Cosine, '<=', 0.5)
->orderByVectorDistance('embedding', [0.1, 0.2, 0.3], VectorDistance::Cosine)
->get();정렬, 그룹화, Limit, Offset
정렬
`orderBy` 메서드
orderBy 메서드로 쿼리 결과를 특정 컬럼 기준으로 정렬합니다. 첫 번째 인자는 정렬 기준 컬럼, 두 번째 인자는 정렬 방향(asc 또는 desc)입니다.
$users = DB::table('users')
->orderBy('name', 'desc')
->get();여러 컬럼으로 정렬하려면 orderBy를 여러 번 체이닝하면 됩니다.
$users = DB::table('users')
->orderBy('name', 'desc')
->orderBy('email', 'asc')
->get();`latest` / `oldest` 메서드
latest와 oldest 메서드를 사용하면 날짜 기준으로 간편하게 정렬할 수 있습니다. 기본 기준 컬럼은 created_at이며, 다른 컬럼을 지정할 수도 있습니다.
$user = DB::table('users')
->latest()
->first();무작위 정렬
inRandomOrder 메서드는 쿼리 결과를 무작위로 정렬합니다. 랜덤 사용자 한 명을 뽑을 때 유용합니다.
$randomUser = DB::table('users')
->inRandomOrder()
->first();기존 정렬 제거
reorder 메서드는 이전에 적용된 order by 절을 모두 제거합니다.
$query = DB::table('users')->orderBy('name');
$unorderedUsers = $query->reorder()->get();reorder에 컬럼과 방향을 전달하면 기존 정렬을 모두 제거하고 새로운 정렬을 적용합니다.
$query = DB::table('users')->orderBy('name');
$usersOrderedByEmail = $query->reorder('email', 'desc')->get();그룹화
`groupBy` / `having`
groupBy와 having 메서드로 쿼리 결과를 그룹화할 수 있습니다.
$users = DB::table('users')
->groupBy('account_id')
->having('account_id', '>', 100)
->get();havingBetween으로 그룹 필터링 범위를 지정할 수 있습니다.
$report = DB::table('orders')
->selectRaw('count(id) as number_of_orders, customer_id')
->groupBy('customer_id')
->havingBetween('number_of_orders', [5, 15])
->get();여러 컬럼으로 그룹화하려면 groupBy에 여러 인자를 전달하세요.
$users = DB::table('users')
->groupBy('first_name', 'status')
->having('account_id', '>', 100)
->get();더 복잡한 having 절을 작성하려면 havingRaw 메서드를 참고하세요.
Limit과 Offset
`skip` / `take`
skip(= offset)과 take(= limit) 메서드로 반환할 결과 수와 시작 위치를 제어할 수 있습니다.
$users = DB::table('users')->skip(10)->take(5)->get();limit과 offset 메서드도 동일한 역할을 합니다.
$users = DB::table('users')
->offset(10)
->limit(5)
->get();조건부 절
특정 조건에 따라 쿼리 절을 동적으로 적용하고 싶을 때 when 메서드를 사용합니다. 첫 번째 인자가 true일 때만 두 번째 인자로 전달한 클로저가 실행됩니다.
$role = $request->input('role');
$users = DB::table('users')
->when($role, function (Builder $query, string $role) {
$query->where('role_id', $role);
})
->get();세 번째 인자로 클로저를 전달하면 첫 번째 인자가 false일 때 실행됩니다.
$sortByVotes = $request->boolean('sort_by_votes');
$users = DB::table('users')
->when($sortByVotes, function (Builder $query, bool $sortByVotes) {
$query->orderBy('votes');
}, function (Builder $query) {
$query->orderBy('name');
})
->get();Insert 구문
쿼리 빌더의 insert 메서드로 테이블에 레코드를 삽입합니다. 컬럼명과 값의 배열을 인자로 전달합니다.
DB::table('users')->insert([
'email' => 'gildong@example.com',
'votes' => 0,
]);중첩 배열을 전달하면 여러 레코드를 한 번에 삽입할 수 있습니다.
DB::table('users')->insert([
['email' => 'gildong@example.com', 'votes' => 0],
['email' => 'cheolsu@example.com', 'votes' => 0],
]);insertOrIgnore 메서드는 삽입 중 중복 에러를 무시합니다. 이 메서드를 사용하면 중복 레코드 에러뿐만 아니라 데이터베이스 엔진에 따라 다른 종류의 에러도 무시될 수 있으니 주의하세요.
DB::table('users')->insertOrIgnore([
['id' => 1, 'email' => 'gildong@example.com'],
['id' => 2, 'email' => 'cheolsu@example.com'],
]);insertUsing 메서드는 서브쿼리로 삽입할 데이터를 결정합니다.
DB::table('pruned_users')->insertUsing([
'id', 'name', 'email', 'email_verified_at',
], DB::table('users')->select(
'id', 'name', 'email', 'email_verified_at'
)->where('updated_at', '<=', now()->subMonth()));자동 증가 ID
테이블에 자동 증가 ID가 있다면 insertGetId 메서드를 사용해 삽입 후 새로 생성된 ID를 바로 받을 수 있습니다.
$id = DB::table('users')->insertGetId(
['email' => 'gildong@example.com', 'votes' => 0]
);WARNING
PostgreSQL에서는 insertGetId가 기본적으로 id 컬럼을 자동 증가 컬럼으로 가정합니다. 다른 시퀀스에서 ID를 가져오려면 두 번째 인자로 컬럼명을 전달하세요.
Upsert
upsert 메서드는 존재하지 않으면 삽입하고, 이미 존재하면 지정한 컬럼 값을 업데이트합니다. 첫 번째 인자는 삽입 또는 업데이트할 데이터, 두 번째 인자는 레코드를 고유하게 식별하는 컬럼, 세 번째 인자는 이미 존재할 때 업데이트할 컬럼 목록입니다.
DB::table('flights')->upsert(
[
['departure' => '서울', 'destination' => '부산', 'price' => 59000],
['departure' => '서울', 'destination' => '제주', 'price' => 89000],
],
['departure', 'destination'],
['price']
);WARNING
SQL Server를 제외한 모든 데이터베이스에서 upsert의 두 번째 인자로 지정하는 컬럼은 primary 또는 unique 인덱스가 있어야 합니다. 또한 MySQL 드라이버는 두 번째 인자를 무시하고 항상 테이블의 primary와 unique 인덱스를 기준으로 판단합니다.
Update 구문
update 메서드로 기존 레코드를 수정합니다. where로 조건을 지정해 원하는 레코드만 업데이트할 수 있습니다. 업데이트된 행 수를 반환합니다.
$affected = DB::table('users')
->where('id', 1)
->update(['votes' => 1]);Update Or Insert
updateOrInsert 메서드는 조건에 맞는 레코드가 있으면 업데이트하고, 없으면 새로 삽입합니다. 첫 번째 인자는 조건, 두 번째 인자는 업데이트할 값입니다.
DB::table('users')
->updateOrInsert(
['email' => 'gildong@example.com', 'name' => '홍길동'],
['votes' => '2'],
);updateOrInsert에 클로저를 전달하면 레코드 존재 여부에 따라 삽입하거나 업데이트할 속성을 동적으로 결정할 수 있습니다.
DB::table('users')->updateOrInsert(
['user_id' => $user_id],
fn ($exists) => $exists ? [
'name' => $data['name'],
'updated_at' => Carbon::now(),
] : [
'name' => $data['name'],
'email' => $data['email'],
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
],
);JSON 컬럼 업데이트
JSON 컬럼을 업데이트할 때는 -> 구문을 사용해 JSON 객체의 특정 키만 업데이트할 수 있습니다. MySQL >= 5.7, PostgreSQL >= 9.5에서 지원됩니다.
$affected = DB::table('users')
->where('id', 1)
->update(['options->enabled' => true]);증감 연산
increment와 decrement 메서드로 컬럼 값을 간편하게 증가시키거나 감소시킬 수 있습니다.
DB::table('users')->increment('votes');
DB::table('users')->increment('votes', 5);
DB::table('users')->decrement('votes');
DB::table('users')->decrement('votes', 5);증감 연산과 동시에 다른 컬럼도 업데이트하려면 세 번째 인자로 배열을 전달하세요.
DB::table('users')->increment('votes', 1, ['name' => '홍길동']);incrementEach와 decrementEach를 사용하면 여러 컬럼을 한 번에 증감할 수 있습니다.
DB::table('users')->incrementEach([
'votes' => 5,
'balance' => 100,
]);Delete 구문
delete 메서드로 테이블에서 레코드를 삭제합니다. 삭제된 행 수를 반환합니다. where로 범위를 지정해 삭제하는 것이 일반적입니다.
$deleted = DB::table('users')->delete();
$deleted = DB::table('users')->where('votes', '>', 100)->delete();테이블의 모든 레코드를 삭제하고 자동 증가 ID를 0으로 초기화하려면 truncate 메서드를 사용하세요.
DB::table('users')->truncate();테이블 Truncate와 PostgreSQL
PostgreSQL에서 truncate 작업을 수행하면 CASCADE 동작이 적용됩니다. 즉, 다른 테이블에서 이 테이블을 참조하는 외래키 레코드들도 함께 삭제됩니다.
비관적 잠금
쿼리 빌더는 SELECT 시 비관적 잠금을 적용하는 메서드를 제공합니다.
"공유 잠금(shared lock)"을 적용하려면 sharedLock 메서드를 사용하세요. 공유 잠금은 트랜잭션이 커밋될 때까지 선택된 행이 수정되는 것을 방지합니다.
DB::table('users')
->where('votes', '>', 100)
->sharedLock()
->get();"for update" 잠금을 적용하려면 lockForUpdate 메서드를 사용하세요. 선택된 행이 다른 트랜잭션에 의해 수정되거나 공유 잠금되는 것을 방지합니다.
DB::table('users')
->where('votes', '>', 100)
->lockForUpdate()
->get();재사용 가능한 쿼리 컴포넌트
여러 곳에서 공통으로 사용하는 쿼리 로직을 재사용하고 싶을 때, tap 메서드를 이용하면 쿼리 빌더 인스턴스를 클로저에 전달해 원하는 조건을 추가하고 동일한 빌더를 반환받을 수 있습니다.
use Illuminate\Database\Query\Builder;
$users = DB::table('users')
->tap(function (Builder $query) {
// 공통 where 조건 등 추가 작업
})
->get();더 체계적으로 재사용하려면 별도의 클래스로 쿼리 컴포넌트를 정의하고, tap에 전달하거나 Eloquent 스코프(scope)로 활용할 수 있습니다.
디버깅
쿼리를 작성하다가 실제로 어떤 SQL이 생성되는지 확인하고 싶을 때 dd와 dump 메서드를 사용하세요.
dd 메서드는 현재 쿼리 바인딩과 SQL을 출력하고 실행을 중단합니다.
DB::table('users')->where('votes', '>', 100)->dd();dump 메서드는 SQL을 출력하되 실행을 중단하지 않고 계속 진행합니다.
DB::table('users')->where('votes', '>', 100)->dump();dumpRawSql 메서드는 파라미터 바인딩이 실제 값으로 치환된 SQL을 출력합니다. ddRawSql도 동일하게 동작하되 실행을 중단합니다.
DB::table('users')->where('votes', '>', 100)->dumpRawSql();
DB::table('users')->where('votes', '>', 100)->ddRawSql();쿼리 빌더
소개
Laravel의 데이터베이스 쿼리 빌더는 데이터베이스 쿼리를 쉽고 직관적으로 작성·실행할 수 있는 유연한 인터페이스를 제공합니다. 애플리케이션에서 필요한 대부분의 데이터베이스 작업을 처리할 수 있으며, Laravel이 지원하는 모든 데이터베이스 시스템과 완벽하게 호환됩니다.
쿼리 빌더는 내부적으로 PDO 파라미터 바인딩을 사용하여 SQL 인젝션 공격으로부터 애플리케이션을 보호합니다. 쿼리 빌더에 전달하는 문자열을 별도로 이스케이프하거나 정제할 필요가 없습니다.
WARNING
PDO는 컬럼명 바인딩을 지원하지 않습니다. 따라서 "order by" 컬럼을 포함하여 쿼리에서 참조하는 컬럼명을 사용자 입력값으로 결정하지 않도록 주의하세요.
쿼리 빌더
데이터베이스 쿼리 실행
테이블의 모든 행 조회
DB 파사드의 table 메서드를 사용해 쿼리를 시작할 수 있습니다. table 메서드는 지정한 테이블에 대한 플루언트 쿼리 빌더 인스턴스를 반환하며, 메서드 체이닝으로 조건을 추가한 뒤 get 메서드로 최종 결과를 가져옵니다.
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* 모든 사용자 목록을 반환합니다.
*/
public function index(): View
{
$users = DB::table('users')->get();
return view('user.index', ['users' => $users]);
}
}get 메서드는 Illuminate\Support\Collection 인스턴스를 반환하며, 각 결과는 PHP의 stdClass 객체입니다. 컬럼 값은 객체 프로퍼티로 접근할 수 있습니다.
use Illuminate\Support\Facades\DB;
$users = DB::table('users')->get();
foreach ($users as $user) {
echo $user->name;
}NOTE
Laravel 컬렉션은 데이터 변환과 집계를 위한 강력한 메서드를 다양하게 제공합니다. 자세한 내용은 컬렉션 문서를 참고하세요.
단일 행 / 컬럼 조회
단일 행만 필요하다면 DB 파사드의 first 메서드를 사용하세요. 이 메서드는 stdClass 객체 하나를 반환합니다.
$user = DB::table('users')->where('name', 'John')->first();
return $user->email;조건에 맞는 행이 없을 때 Illuminate\Database\RecordNotFoundException을 던지게 하려면 firstOrFail 메서드를 사용하세요. 이 예외를 별도로 처리하지 않으면 클라이언트에게 자동으로 404 HTTP 응답이 반환됩니다.
$user = DB::table('users')->where('name', 'John')->firstOrFail();행 전체가 아니라 특정 컬럼 값 하나만 필요하다면 value 메서드를 사용합니다. 컬럼 값을 직접 반환합니다.
$email = DB::table('users')->where('name', 'John')->value('email');id 컬럼 값으로 단일 행을 조회하려면 find 메서드를 사용하세요.
$user = DB::table('users')->find(3);특정 컬럼 값 목록 조회
단일 컬럼의 값만 모아 Illuminate\Support\Collection으로 받고 싶다면 pluck 메서드를 사용합니다.
use Illuminate\Support\Facades\DB;
$titles = DB::table('users')->pluck('title');
foreach ($titles as $title) {
echo $title;
}두 번째 인자로 키로 사용할 컬럼을 지정할 수도 있습니다.
$titles = DB::table('users')->pluck('title', 'name');
foreach ($titles as $name => $title) {
echo $title;
}결과 청크 처리
수만 건 이상의 레코드를 처리해야 할 때는 chunk 메서드를 고려하세요. 이 메서드는 결과를 지정한 크기로 나누어 클로저에 순차적으로 전달합니다. 예를 들어, users 테이블 전체를 100건씩 나누어 처리하는 코드는 다음과 같습니다.
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
foreach ($users as $user) {
// ...
}
});클로저에서 false를 반환하면 이후 청크 처리를 중단할 수 있습니다.
DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
// 레코드 처리...
return false;
});청크 처리 중 레코드를 업데이트하면 청크 결과가 의도치 않게 바뀔 수 있습니다. 조회한 레코드를 업데이트해야 한다면 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]);
}
});chunkById와 lazyById 메서드는 내부적으로 자체 WHERE 조건을 쿼리에 추가합니다. 따라서 직접 작성하는 조건은 클로저 안에서 논리 그룹으로 묶는 것이 좋습니다.
DB::table('users')->where(function ($query) {
$query->where('credits', 1)->orWhere('credits', 2);
})->chunkById(100, function (Collection $users) {
foreach ($users as $user) {
DB::table('users')
->where('id', $user->id)
->update(['credits' => 3]);
}
});WARNING
청크 콜백 안에서 레코드를 업데이트하거나 삭제할 때 기본 키나 외래 키 값이 변경되면 청크 쿼리 결과에 영향을 줄 수 있습니다. 이 경우 일부 레코드가 청크 결과에서 누락될 수 있으니 주의하세요.
지연 스트리밍
lazy 메서드는 내부적으로 chunk 메서드처럼 쿼리를 청크 단위로 실행하지만, 클로저 대신 LazyCollection을 반환합니다. 덕분에 결과 전체를 하나의 스트림처럼 다룰 수 있습니다.
use Illuminate\Support\Facades\DB;
DB::table('users')->orderBy('id')->lazy()->each(function (object $user) {
// ...
});반복 중 레코드를 업데이트할 계획이라면 lazyById 또는 lazyByIdDesc 메서드를 사용하세요. 기본 키를 기준으로 자동 페이지네이션을 처리합니다.
DB::table('users')->where('active', false)
->lazyById()->each(function (object $user) {
DB::table('users')
->where('id', $user->id)
->update(['active' => true]);
});WARNING
반복 중 레코드를 업데이트하거나 삭제할 때 기본 키나 외래 키 값이 변경되면 청크 쿼리에 영향을 줄 수 있으며, 일부 레코드가 결과에서 누락될 수 있습니다.
집계 함수
쿼리 빌더는 count, max, min, avg, sum 등 다양한 집계 메서드를 제공합니다. 쿼리를 구성한 뒤 이 메서드들을 호출하면 됩니다.
use Illuminate\Support\Facades\DB;
$users = DB::table('users')->count();
$price = DB::table('orders')->max('price');다른 절과 함께 조합해 원하는 조건에 맞는 집계값을 구할 수도 있습니다.
$price = DB::table('orders')
->where('finalized', 1)
->avg('price');레코드 존재 여부 확인
조건에 맞는 레코드가 있는지 확인할 때 count 대신 exists와 doesntExist 메서드를 사용하면 더 명확하고 간결합니다.
if (DB::table('orders')->where('finalized', 1)->exists()) {
// ...
}
if (DB::table('orders')->where('finalized', 1)->doesntExist()) {
// ...
}Select 구문
Select 절 지정하기
항상 테이블의 모든 컬럼을 조회할 필요는 없습니다. select 메서드를 사용하면 원하는 컬럼만 지정해서 가져올 수 있습니다.
use Illuminate\Support\Facades\DB;
$users = DB::table('users')
->select('name', 'email as user_email')
->get();distinct 메서드를 사용하면 중복을 제거한 결과를 반환하도록 강제할 수 있습니다.
$users = DB::table('users')->distinct()->get();이미 쿼리 빌더 인스턴스가 있고, 기존 select 절에 컬럼을 추가하고 싶다면 addSelect 메서드를 사용하세요.
$query = DB::table('users')->select('name');
$users = $query->addSelect('age')->get();Raw 표현식
쿼리 안에 임의의 SQL 문자열을 직접 삽입해야 할 때는 DB 파사드의 raw 메서드를 사용합니다.
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();WARNING
Raw 표현식은 문자열 그대로 쿼리에 삽입되므로, SQL 인젝션 취약점이 생기지 않도록 각별히 주의해야 합니다.
Raw 메서드
DB::raw를 직접 쓰는 대신, 쿼리의 특정 절에 Raw 표현식을 삽입하는 전용 메서드들을 사용할 수 있습니다. 단, Raw 표현식을 사용하는 쿼리는 Laravel이 SQL 인젝션으로부터 안전하다고 보장하지 않습니다.
selectRaw
selectRaw는 addSelect(DB::raw(/* ... */)) 대신 사용할 수 있습니다. 두 번째 인자로 바인딩 배열을 선택적으로 전달할 수 있습니다.
$orders = DB::table('orders')
->selectRaw('price * ? as price_with_tax', [1.0825])
->get();whereRaw / orWhereRaw
whereRaw와 orWhereRaw는 Raw WHERE 절을 쿼리에 삽입합니다. 두 번째 인자로 바인딩 배열을 선택적으로 전달할 수 있습니다.
$orders = DB::table('orders')
->whereRaw('price > IF(state = "TX", ?, 100)', [200])
->get();havingRaw / orHavingRaw
havingRaw와 orHavingRaw는 Raw 문자열을 HAVING 절의 값으로 사용합니다. 두 번째 인자로 바인딩 배열을 선택적으로 전달할 수 있습니다.
$orders = DB::table('orders')
->select('department', DB::raw('SUM(price) as total_sales'))
->groupBy('department')
->havingRaw('SUM(price) > ?', [2500])
->get();orderByRaw
orderByRaw는 Raw 문자열을 ORDER BY 절의 값으로 사용합니다.
$orders = DB::table('orders')
->orderByRaw('updated_at - created_at DESC')
->get();groupByRaw
groupByRaw는 Raw 문자열을 GROUP BY 절의 값으로 사용합니다.
$orders = DB::table('orders')
->select('city', 'state')
->groupByRaw('city, state')
->get();Joins
Inner Join
쿼리 빌더의 join 메서드를 사용하면 쿼리에 JOIN 절을 추가할 수 있습니다. 첫 번째 인수에는 조인할 테이블 이름을, 나머지 인수에는 조인 조건에 사용할 컬럼을 지정합니다. 한 번의 쿼리에서 여러 테이블을 조인하는 것도 가능합니다.
use Illuminate\Support\Facades\DB;
$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 / Right Join
Inner Join 대신 Left Join이나 Right Join을 수행하려면 leftJoin 또는 rightJoin 메서드를 사용하세요. 사용 방법은 join 메서드와 동일합니다.
$users = DB::table('users')
->leftJoin('posts', 'users.id', '=', 'posts.user_id')
->get();
$users = DB::table('users')
->rightJoin('posts', 'users.id', '=', 'posts.user_id')
->get();Cross Join
crossJoin 메서드를 사용하면 Cross Join을 수행할 수 있습니다. Cross Join은 두 테이블 간의 카테시안 곱(cartesian product)을 생성합니다. 즉, 첫 번째 테이블의 모든 행과 두 번째 테이블의 모든 행이 조합됩니다.
$sizes = DB::table('sizes')
->crossJoin('colors')
->get();고급 Join 절
더 복잡한 조인 조건이 필요한 경우, join 메서드의 두 번째 인수로 클로저를 전달할 수 있습니다. 클로저는 Illuminate\Database\Query\JoinClause 인스턴스를 받으며, 이를 통해 조인 조건을 세밀하게 지정할 수 있습니다.
DB::table('users')
->join('contacts', function (JoinClause $join) {
$join->on('users.id', '=', 'contacts.user_id')->orOn(/* ... */);
})
->get();조인 절 안에서 컬럼과 값을 비교하는 WHERE 조건이 필요하다면, JoinClause 인스턴스의 where 및 orWhere 메서드를 사용할 수 있습니다. 이 메서드들은 두 컬럼을 서로 비교하는 것이 아니라, 컬럼과 특정 값을 비교합니다.
DB::table('users')
->join('contacts', function (JoinClause $join) {
$join->on('users.id', '=', 'contacts.user_id')
->where('contacts.user_id', '>', 5);
})
->get();서브쿼리 Join
joinSub, leftJoinSub, rightJoinSub 메서드를 사용하면 서브쿼리를 대상으로 조인할 수 있습니다. 각 메서드는 세 가지 인수를 받습니다: 서브쿼리, 테이블 별칭(alias), 그리고 조인 조건을 정의하는 클로저입니다.
아래 예시는 각 사용자의 가장 최근 게시글 작성 시각(last_post_created_at)을 함께 조회하는 쿼리입니다.
$latestPosts = DB::table('posts')
->select('user_id', DB::raw('MAX(created_at) as last_post_created_at'))
->where('is_published', true)
->groupBy('user_id');
$users = DB::table('users')
->joinSub($latestPosts, 'latest_posts', function (JoinClause $join) {
$join->on('users.id', '=', 'latest_posts.user_id');
})->get();Lateral Join
WARNING
Lateral Join은 현재 PostgreSQL, MySQL 8.0.14 이상, SQL Server에서만 지원됩니다.
joinLateral 및 leftJoinLateral 메서드를 사용하면 서브쿼리와의 Lateral Join을 수행할 수 있습니다. 각 메서드는 서브쿼리와 테이블 별칭 두 가지 인수를 받습니다. 조인 조건은 서브쿼리 내부의 where 절에 지정합니다.
일반 서브쿼리 조인과의 차이점은, Lateral Join은 바깥 쿼리의 각 행마다 서브쿼리를 재평가한다는 점입니다. 덕분에 서브쿼리 안에서 바깥 테이블의 컬럼을 직접 참조할 수 있습니다.
아래 예시는 각 사용자와 해당 사용자의 최근 게시글 3개를 함께 조회합니다. 결과 집합에서는 사용자 한 명당 최대 3개의 행이 생성될 수 있으며, 서브쿼리 내 whereColumn을 통해 현재 사용자 행을 참조합니다.
$latestPosts = DB::table('posts')
->select('id as post_id', 'title as post_title', 'created_at as post_created_at')
->whereColumn('user_id', 'users.id')
->orderBy('created_at', 'desc')
->limit(3);
$users = DB::table('users')
->joinLateral($latestPosts, 'latest_posts')
->get();쿼리 빌더
Unions
쿼리 빌더는 두 개 이상의 쿼리를 하나로 합치는 union 메서드를 제공합니다. 먼저 기본 쿼리를 작성한 뒤, union 메서드로 다른 쿼리를 연결하면 됩니다.
use Illuminate\Support\Facades\DB;
$usersWithoutFirstName = DB::table('users')
->whereNull('first_name');
$users = DB::table('users')
->whereNull('last_name')
->union($usersWithoutFirstName)
->get();union 메서드는 결과에서 중복 행을 자동으로 제거합니다. 중복을 제거하지 않고 모든 결과를 그대로 합치려면 unionAll 메서드를 사용하세요. 메서드 시그니처는 union과 동일합니다.
기본 Where 절
Where 절
쿼리 빌더의 where 메서드를 사용하면 쿼리에 "where" 조건을 추가할 수 있습니다. 가장 기본적인 사용법은 세 개의 인자를 전달하는 것입니다. 첫 번째는 컬럼명, 두 번째는 연산자(데이터베이스에서 지원하는 모든 연산자 사용 가능), 세 번째는 비교할 값입니다.
예를 들어, 아래 쿼리는 votes 컬럼 값이 100이고 age 컬럼 값이 35보다 큰 사용자를 조회합니다:
$users = DB::table('users')
->where('votes', '=', 100)
->where('age', '>', 35)
->get();= 연산자를 사용할 때는 두 번째 인자로 값만 전달해도 됩니다. Laravel은 자동으로 = 연산자를 적용합니다:
$users = DB::table('users')->where('votes', 100)->get();연관 배열을 전달하면 여러 컬럼에 대한 조건을 한 번에 지정할 수 있습니다:
$users = DB::table('users')->where([
'first_name' => 'Jane',
'last_name' => 'Doe',
])->get();물론 데이터베이스에서 지원하는 다양한 연산자를 자유롭게 사용할 수 있습니다:
$users = DB::table('users')
->where('votes', '>=', 100)
->get();
$users = DB::table('users')
->where('votes', '<>', 100)
->get();
$users = DB::table('users')
->where('name', 'like', 'T%')
->get();조건을 배열로 묶어서 전달할 수도 있습니다. 배열의 각 요소는 where 메서드에 전달하는 세 인자를 담은 배열이어야 합니다:
$users = DB::table('users')->where([
['status', '=', '1'],
['subscribed', '<>', '1'],
])->get();WARNING
PDO는 컬럼명 바인딩을 지원하지 않습니다. 따라서 "order by" 컬럼을 포함해, 쿼리에서 참조하는 컬럼명을 사용자 입력으로 결정하게 해서는 안 됩니다.
WARNING
MySQL과 MariaDB는 문자열-숫자 비교 시 문자열을 자동으로 정수로 변환합니다. 이 과정에서 숫자가 아닌 문자열은 0으로 변환되어 예기치 않은 결과가 발생할 수 있습니다. 예를 들어 secret 컬럼 값이 aaa인 행이 있을 때 User::where('secret', 0)을 실행하면 해당 행이 반환됩니다. 이를 방지하려면 쿼리에 사용하기 전에 값을 적절한 타입으로 캐스팅하세요.
Or Where 절
where 메서드를 체이닝하면 기본적으로 AND 연산자로 연결됩니다. OR 조건이 필요할 때는 orWhere 메서드를 사용하세요. orWhere는 where와 동일한 인자를 받습니다:
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere('name', 'John')
->get();"or" 조건을 괄호로 묶어야 할 때는 클로저를 첫 번째 인자로 전달합니다:
use Illuminate\Database\Query\Builder;
$users = DB::table('users')
->where('votes', '>', 100)
->orWhere(function (Builder $query) {
$query->where('name', 'Abigail')
->where('votes', '>', 50);
})
->get();위 예시는 다음 SQL을 생성합니다:
select * from users where votes > 100 or (name = 'Abigail' and votes > 50)WARNING
글로벌 스코프가 적용된 경우 예기치 않은 동작을 피하려면 orWhere 호출은 항상 그룹(괄호)으로 묶는 것을 권장합니다.
Where Not 절
whereNot과 orWhereNot 메서드를 사용하면 특정 조건 그룹을 부정(negate)할 수 있습니다. 예를 들어, 아래 쿼리는 재고 정리 중이거나 가격이 10 미만인 상품을 제외하고 조회합니다:
$products = DB::table('products')
->whereNot(function (Builder $query) {
$query->where('clearance', true)
->orWhere('price', '<', 10);
})
->get();Where Any / All / None 절
여러 컬럼에 동일한 조건을 적용해야 할 때가 있습니다. 예를 들어, 지정한 컬럼 중 하나라도 특정 값과 LIKE 패턴이 일치하는 레코드를 조회하려면 whereAny 메서드를 사용합니다:
$users = DB::table('users')
->where('active', true)
->whereAny([
'name',
'email',
'phone',
], 'like', 'Example%')
->get();위 쿼리는 다음 SQL을 생성합니다:
SELECT *
FROM users
WHERE active = true AND (
name LIKE 'Example%' OR
email LIKE 'Example%' OR
phone LIKE 'Example%'
)반대로, 지정한 컬럼 모두가 조건에 일치하는 레코드를 조회하려면 whereAll 메서드를 사용합니다:
$posts = DB::table('posts')
->where('published', true)
->whereAll([
'title',
'content',
], 'like', '%Laravel%')
->get();위 쿼리는 다음 SQL을 생성합니다:
SELECT *
FROM posts
WHERE published = true AND (
title LIKE '%Laravel%' AND
content LIKE '%Laravel%'
)지정한 컬럼 어느 것도 조건에 일치하지 않는 레코드를 조회하려면 whereNone 메서드를 사용합니다:
$albums = DB::table('albums')
->where('published', true)
->whereNone([
'title',
'lyrics',
'tags',
], 'like', '%explicit%')
->get();위 쿼리는 다음 SQL을 생성합니다:
SELECT *
FROM albums
WHERE published = true AND NOT (
title LIKE '%explicit%' OR
lyrics LIKE '%explicit%' OR
tags LIKE '%explicit%'
)JSON Where 절
Laravel은 JSON 컬럼 타입을 지원하는 데이터베이스에서 JSON 컬럼 쿼리도 지원합니다. 현재 지원되는 데이터베이스는 MariaDB 10.3+, MySQL 8.0+, PostgreSQL 12.0+, SQL Server 2017+, SQLite 3.39.0+입니다. JSON 컬럼을 조회할 때는 -> 연산자를 사용합니다:
$users = DB::table('users')
->where('preferences->dining->meal', 'salad')
->get();
$users = DB::table('users')
->whereIn('preferences->dining->meal', ['pasta', 'salad', 'sandwiches'])
->get();JSON 배열을 조회할 때는 whereJsonContains와 whereJsonDoesntContain 메서드를 사용합니다:
$users = DB::table('users')
->whereJsonContains('options->languages', 'en')
->get();
$users = DB::table('users')
->whereJsonDoesntContain('options->languages', 'en')
->get();MariaDB, MySQL, PostgreSQL을 사용한다면 값을 배열로 전달할 수도 있습니다:
$users = DB::table('users')
->whereJsonContains('options->languages', ['en', 'de'])
->get();
$users = DB::table('users')
->whereJsonDoesntContain('options->languages', ['en', 'de'])
->get();특정 JSON 키의 존재 여부로 필터링하려면 whereJsonContainsKey와 whereJsonDoesntContainKey 메서드를 사용합니다:
$users = DB::table('users')
->whereJsonContainsKey('preferences->dietary_requirements')
->get();
$users = DB::table('users')
->whereJsonDoesntContainKey('preferences->dietary_requirements')
->get();JSON 배열의 길이로 필터링하려면 whereJsonLength 메서드를 사용합니다:
$users = DB::table('users')
->whereJsonLength('options->languages', 0)
->get();
$users = DB::table('users')
->whereJsonLength('options->languages', '>', 1)
->get();추가 Where 절
whereLike / orWhereLike / whereNotLike / orWhereNotLike
whereLike 메서드는 패턴 매칭을 위한 "LIKE" 절을 추가합니다. 데이터베이스 종류에 관계없이 일관된 방식으로 문자열 검색을 수행할 수 있으며, 대소문자 구분 여부도 설정할 수 있습니다. 기본값은 대소문자를 구분하지 않습니다:
$users = DB::table('users')
->whereLike('name', '%John%')
->get();대소문자를 구분하는 검색이 필요하다면 caseSensitive 인자를 사용하세요:
$users = DB::table('users')
->whereLike('name', '%John%', caseSensitive: true)
->get();orWhereLike는 "or" 조건으로 LIKE 절을 추가합니다:
$users = DB::table('users')
->where('votes', '>', 100)
->orWhereLike('name', '%John%')
->get();whereNotLike는 "NOT LIKE" 절을 추가합니다:
$users = DB::table('users')
->whereNotLike('name', '%John%')
->get();orWhereNotLike는 "or" 조건으로 NOT LIKE 절을 추가합니다:
$users = DB::table('users')
->where('votes', '>', 100)
->orWhereNotLike('name', '%John%')
->get();WARNING
whereLike의 대소문자 구분(caseSensitive) 옵션은 현재 SQL Server에서 지원되지 않습니다.
whereIn / whereNotIn / orWhereIn / orWhereNotIn
whereIn 메서드는 컬럼 값이 주어진 배열에 포함되는지 확인합니다:
$users = DB::table('users')
->whereIn('id', [1, 2, 3])
->get();whereNotIn 메서드는 컬럼 값이 주어진 배열에 포함되지 않는지 확인합니다:
$users = DB::table('users')
->whereNotIn('id', [1, 2, 3])
->get();두 번째 인자로 서브쿼리 객체를 전달할 수도 있습니다:
$activeUsers = DB::table('users')->select('id')->where('is_active', 1);
$comments = DB::table('comments')
->whereIn('user_id', $activeUsers)
->get();위 예시는 다음 SQL을 생성합니다:
select * from comments where user_id in (
select id
from users
where is_active = 1
)WARNING
정수 바인딩 배열이 매우 큰 경우, whereIntegerInRaw 또는 whereIntegerNotInRaw 메서드를 사용하면 메모리 사용량을 크게 줄일 수 있습니다.
whereBetween / orWhereBetween
whereBetween 메서드는 컬럼 값이 두 값 사이에 있는지 확인합니다:
$users = DB::table('users')
->whereBetween('votes', [1, 100])
->get();whereNotBetween / orWhereNotBetween
whereNotBetween 메서드는 컬럼 값이 두 값의 범위를 벗어나는지 확인합니다:
$users = DB::table('users')
->whereNotBetween('votes', [1, 100])
->get();whereBetweenColumns / whereNotBetweenColumns / orWhereBetweenColumns / orWhereNotBetweenColumns
whereBetweenColumns 메서드는 컬럼 값이 같은 행의 두 컬럼 값 사이에 있는지 확인합니다:
$patients = DB::table('patients')
->whereBetweenColumns('weight', ['minimum_allowed_weight', 'maximum_allowed_weight'])
->get();whereNotBetweenColumns 메서드는 컬럼 값이 같은 행의 두 컬럼 값 범위를 벗어나는지 확인합니다:
$patients = DB::table('patients')
->whereNotBetweenColumns('weight', ['minimum_allowed_weight', 'maximum_allowed_weight'])
->get();whereValueBetween / whereValueNotBetween / orWhereValueBetween / orWhereValueNotBetween
whereValueBetween 메서드는 주어진 값이 같은 행의 두 컬럼 값 사이에 있는지 확인합니다:
$products = DB::table('products')
->whereValueBetween(100, ['min_price', 'max_price'])
->get();whereValueNotBetween 메서드는 주어진 값이 같은 행의 두 컬럼 값 범위를 벗어나는지 확인합니다:
$products = DB::table('products')
->whereValueNotBetween(100, ['min_price', 'max_price'])
->get();whereNull / whereNotNull / orWhereNull / orWhereNotNull
whereNull 메서드는 컬럼 값이 NULL인지 확인합니다:
$users = DB::table('users')
->whereNull('updated_at')
->get();whereNotNull 메서드는 컬럼 값이 NULL이 아닌지 확인합니다:
$users = DB::table('users')
->whereNotNull('updated_at')
->get();whereNullSafeEquals / orWhereNullSafeEquals
whereNullSafeEquals와 orWhereNullSafeEquals 메서드는 두 NULL 값을 동일하게 취급하면서 컬럼 값을 비교합니다. 예를 들어 사용자 입력이 null일 수 있는 경우에 유용합니다:
$lastLoginIp = $request->input('last_login_ip');
$users = DB::table('users')
->whereNullSafeEquals('last_login_ip', $lastLoginIp)
->get();whereDate / whereMonth / whereDay / whereYear / whereTime
whereDate 메서드는 컬럼 값을 특정 날짜와 비교합니다:
$users = DB::table('users')
->whereDate('created_at', '2016-12-31')
->get();whereMonth 메서드는 컬럼 값을 특정 월과 비교합니다:
$users = DB::table('users')
->whereMonth('created_at', '12')
->get();whereDay 메서드는 컬럼 값을 특정 일(day)과 비교합니다:
$users = DB::table('users')
->whereDay('created_at', '31')
->get();whereYear 메서드는 컬럼 값을 특정 연도와 비교합니다:
$users = DB::table('users')
->whereYear('created_at', '2016')
->get();whereTime 메서드는 컬럼 값을 특정 시각과 비교합니다:
$users = DB::table('users')
->whereTime('created_at', '=', '11:20:45')
->get();wherePast / whereFuture / whereToday / whereBeforeToday / whereAfterToday
wherePast와 whereFuture 메서드는 컬럼 값이 과거 또는 미래인지 확인합니다:
$invoices = DB::table('invoices')
->wherePast('due_at')
->get();
$invoices = DB::table('invoices')
->whereFuture('due_at')
->get();whereNowOrPast와 whereNowOrFuture 메서드는 현재 시각을 포함하여 과거 또는 미래인지 확인합니다:
$invoices = DB::table('invoices')
->whereNowOrPast('due_at')
->get();
$invoices = DB::table('invoices')
->whereNowOrFuture('due_at')
->get();whereToday, whereBeforeToday, whereAfterToday 메서드는 컬럼 값이 오늘, 오늘 이전, 오늘 이후인지 각각 확인합니다:
$invoices = DB::table('invoices')
->whereToday('due_at')
->get();
$invoices = DB::table('invoices')
->whereBeforeToday('due_at')
->get();
$invoices = DB::table('invoices')
->whereAfterToday('due_at')
->get();whereTodayOrBefore와 whereTodayOrAfter 메서드는 오늘을 포함하여 이전 또는 이후인지 확인합니다:
$invoices = DB::table('invoices')
->whereTodayOrBefore('due_at')
->get();
$invoices = DB::table('invoices')
->whereTodayOrAfter('due_at')
->get();whereColumn / orWhereColumn
whereColumn 메서드는 두 컬럼의 값이 같은지 확인합니다:
$users = DB::table('users')
->whereColumn('first_name', 'last_name')
->get();비교 연산자를 함께 전달할 수도 있습니다:
$users = DB::table('users')
->whereColumn('updated_at', '>', 'created_at')
->get();여러 컬럼 비교 조건을 배열로 전달하면 AND 연산자로 연결됩니다:
$users = DB::table('users')
->whereColumn([
['first_name', '=', 'last_name'],
['updated_at', '>', 'created_at'],
])->get();논리적 그룹화
여러 "where" 절을 괄호로 묶어 원하는 논리적 구조를 만들어야 할 때가 있습니다. 특히 orWhere 호출은 의도치 않은 쿼리 동작을 방지하기 위해 항상 괄호로 묶는 것이 좋습니다. 이를 위해 where 메서드에 클로저를 전달합니다:
$users = DB::table('users')
->where('name', '=', 'John')
->where(function (Builder $query) {
$query->where('votes', '>', 100)
->orWhere('title', '=', 'Admin');
})
->get();클로저를 where 메서드에 전달하면 쿼리 빌더는 해당 클로저 안의 조건들을 괄호로 묶습니다. 클로저는 쿼리 빌더 인스턴스를 받아 괄호 안에 들어갈 조건을 설정합니다. 위 예시는 다음 SQL을 생성합니다:
select * from users where name = 'John' and (votes > 100 or title = 'Admin')WARNING
글로벌 스코프가 적용될 때 예기치 않은 동작을 방지하려면 orWhere 호출은 항상 그룹으로 묶어야 합니다.
고급 Where 절
Where Exists 절
whereExists 메서드를 사용하면 SQL의 "where exists" 절을 작성할 수 있습니다. 이 메서드는 클로저를 인수로 받으며, 클로저 안에서 쿼리 빌더 인스턴스를 통해 exists 절 내부에 들어갈 서브쿼리를 정의합니다.
$users = DB::table('users')
->whereExists(function (Builder $query) {
$query->select(DB::raw(1))
->from('orders')
->whereColumn('orders.user_id', 'users.id');
})
->get();클로저 대신 쿼리 객체를 직접 전달하는 방식도 사용할 수 있습니다.
$orders = DB::table('orders')
->select(DB::raw(1))
->whereColumn('orders.user_id', 'users.id');
$users = DB::table('users')
->whereExists($orders)
->get();위 두 예시는 모두 동일한 SQL을 생성합니다.
select * from users
where exists (
select 1
from orders
where orders.user_id = users.id
)서브쿼리 Where 절
서브쿼리의 결과와 특정 값을 비교하는 where 절이 필요할 때가 있습니다. 이런 경우 where 메서드에 클로저와 비교할 값을 함께 전달합니다. 예를 들어, 아래 쿼리는 가장 최근 멤버십 유형이 'Pro'인 사용자를 모두 조회합니다.
use App\Models\User;
use Illuminate\Database\Query\Builder;
$users = User::where(function (Builder $query) {
$query->select('type')
->from('membership')
->whereColumn('membership.user_id', 'users.id')
->orderByDesc('membership.start_date')
->limit(1);
}, 'Pro')->get();또는 특정 컬럼의 값을 서브쿼리 결과와 비교할 수도 있습니다. where 메서드에 컬럼명, 연산자, 클로저를 순서대로 전달하면 됩니다. 아래 예시는 평균 금액보다 낮은 수입 레코드를 모두 조회합니다.
use App\Models\Income;
use Illuminate\Database\Query\Builder;
$incomes = Income::where('amount', '<', function (Builder $query) {
$query->selectRaw('avg(i.amount)')->from('incomes as i');
})->get();전문 검색(Full Text) Where 절
WARNING
전문 검색 Where 절은 현재 MariaDB, MySQL, PostgreSQL에서만 지원됩니다.
whereFullText와 orWhereFullText 메서드를 사용하면 전문 검색 인덱스가 설정된 컬럼에 전문 검색 조건을 추가할 수 있습니다. Laravel이 데이터베이스 종류에 맞는 SQL로 자동 변환해 줍니다. 예를 들어 MariaDB나 MySQL을 사용하는 경우 MATCH AGAINST 절이 생성됩니다.
$users = DB::table('users')
->whereFullText('bio', 'web developer')
->get();벡터 유사도 절
NOTE
벡터 유사도 절은 현재 pgvector 익스텐션을 사용하는 PostgreSQL 연결에서만 지원됩니다. 벡터 컬럼과 인덱스 정의 방법은 마이그레이션 문서를 참고하세요.
whereVectorSimilarTo 메서드는 주어진 벡터와의 코사인 유사도를 기준으로 결과를 필터링하고, 관련도 순으로 정렬합니다. minSimilarity 임계값은 0.0에서 1.0 사이의 값으로 지정하며, 1.0은 완전히 동일한 경우를 의미합니다.
$documents = DB::table('documents')
->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4)
->limit(10)
->get();벡터 인수로 일반 문자열을 전달하면, Laravel이 Laravel AI SDK를 사용해 자동으로 임베딩을 생성합니다.
$documents = DB::table('documents')
->whereVectorSimilarTo('embedding', '강원도 와인 추천')
->limit(10)
->get();기본적으로 whereVectorSimilarTo는 거리 기준으로 결과를 정렬합니다(유사도 높은 순). order 인수에 false를 전달하면 이 자동 정렬을 비활성화할 수 있습니다.
$documents = DB::table('documents')
->whereVectorSimilarTo('embedding', $queryEmbedding, minSimilarity: 0.4, order: false)
->orderBy('created_at', 'desc')
->limit(10)
->get();더 세밀한 제어가 필요하다면 selectVectorDistance, whereVectorDistanceLessThan, orderByVectorDistance 메서드를 개별적으로 조합해 사용할 수 있습니다.
$documents = DB::table('documents')
->select('*')
->selectVectorDistance('embedding', $queryEmbedding, as: 'distance')
->whereVectorDistanceLessThan('embedding', $queryEmbedding, maxDistance: 0.3)
->orderByVectorDistance('embedding', $queryEmbedding)
->limit(10)
->get();PostgreSQL 환경에서는 vector 컬럼을 생성하기 전에 pgvector 익스텐션을 먼저 로드해야 합니다.
Schema::ensureVectorExtensionExists();정렬, 그룹화, Limit 및 Offset
정렬
orderBy 메서드
orderBy 메서드를 사용하면 특정 컬럼을 기준으로 쿼리 결과를 정렬할 수 있습니다. 첫 번째 인수에는 정렬 기준이 될 컬럼명을, 두 번째 인수에는 정렬 방향(asc 또는 desc)을 지정합니다:
$users = DB::table('users')
->orderBy('name', 'desc')
->get();여러 컬럼을 기준으로 정렬하려면 orderBy를 필요한 만큼 체이닝하면 됩니다:
$users = DB::table('users')
->orderBy('name', 'desc')
->orderBy('email', 'asc')
->get();정렬 방향은 생략 가능하며, 기본값은 오름차순(asc)입니다. 내림차순으로 정렬하려면 두 번째 인수로 'desc'를 전달하거나, orderByDesc 메서드를 사용하면 됩니다:
$users = DB::table('users')
->orderByDesc('verified_at')
->get();JSON 컬럼 내부의 값을 기준으로 정렬할 때는 -> 연산자를 사용합니다:
$corporations = DB::table('corporations')
->where('country', 'KR')
->orderBy('location->city')
->get();latest / oldest 메서드
latest와 oldest 메서드는 날짜 기준으로 간편하게 정렬할 때 사용합니다. 기본적으로 테이블의 created_at 컬럼을 기준으로 정렬되며, 다른 컬럼을 지정할 수도 있습니다:
$user = DB::table('users')
->latest() // 가장 최근에 생성된 레코드부터
->first();무작위 정렬
inRandomOrder 메서드를 사용하면 결과를 무작위 순서로 정렬할 수 있습니다. 예를 들어 임의의 사용자 한 명을 가져올 때 활용할 수 있습니다:
$randomUser = DB::table('users')
->inRandomOrder()
->first();기존 정렬 조건 제거
reorder 메서드를 사용하면 이전에 적용한 모든 ORDER BY 절을 제거할 수 있습니다:
$query = DB::table('users')->orderBy('name');
$unorderedUsers = $query->reorder()->get();reorder 메서드에 컬럼과 방향을 전달하면, 기존 정렬 조건을 모두 제거하고 새로운 정렬 조건으로 대체할 수 있습니다:
$query = DB::table('users')->orderBy('name');
$usersOrderedByEmail = $query->reorder('email', 'desc')->get();내림차순으로 재정렬할 때는 편의 메서드인 reorderDesc를 사용할 수도 있습니다:
$query = DB::table('users')->orderBy('name');
$usersOrderedByEmail = $query->reorderDesc('email')->get();그룹화
groupBy / having 메서드
groupBy와 having 메서드를 사용하면 쿼리 결과를 그룹화할 수 있습니다. having 메서드의 사용법은 where 메서드와 유사합니다:
$users = DB::table('users')
->groupBy('account_id')
->having('account_id', '>', 100)
->get();havingBetween 메서드를 사용하면 집계 결과를 특정 범위로 필터링할 수 있습니다:
$report = DB::table('orders')
->selectRaw('count(id) as number_of_orders, customer_id')
->groupBy('customer_id')
->havingBetween('number_of_orders', [5, 15])
->get();groupBy에 여러 인수를 전달하면 복수의 컬럼으로 그룹화할 수도 있습니다:
$users = DB::table('users')
->groupBy('first_name', 'status')
->having('account_id', '>', 100)
->get();더 복잡한 HAVING 구문이 필요하다면 havingRaw 메서드를 참고하세요.
Limit과 Offset
limit과 offset 메서드를 사용하면 반환할 결과 수를 제한하거나, 앞의 일부 결과를 건너뛸 수 있습니다. 페이지네이션을 직접 구현할 때 자주 활용됩니다:
$users = DB::table('users')
->offset(10) // 앞의 10개 레코드를 건너뜀
->limit(5) // 이후 5개만 반환
->get();NOTE
실제 서비스에서 페이지네이션을 구현할 때는 offset/limit을 직접 조합하기보다 Laravel의 paginate 또는 simplePaginate 메서드를 사용하는 것이 더 편리합니다.
조건부 절 (Conditional Clauses)
특정 조건이 충족될 때만 쿼리 절을 적용하고 싶은 경우가 있습니다. 예를 들어, HTTP 요청에 특정 입력값이 존재할 때만 where 조건을 추가하는 경우입니다. 이럴 때는 when 메서드를 활용하면 편리합니다.
$role = $request->input('role');
$users = DB::table('users')
->when($role, function (Builder $query, string $role) {
$query->where('role_id', $role);
})
->get();when 메서드는 첫 번째 인자가 true로 평가될 때만 클로저를 실행합니다. 첫 번째 인자가 false이면 클로저는 실행되지 않습니다. 위 예시에서는 요청에 role 값이 존재하고 그 값이 true로 평가될 때만 where 절이 쿼리에 추가됩니다.
세 번째 인자로 클로저를 하나 더 전달할 수도 있습니다. 이 클로저는 첫 번째 인자가 false로 평가될 때 실행됩니다. 아래 예시는 이 기능을 활용해 정렬 기준을 동적으로 설정하는 경우입니다.
$sortByVotes = $request->boolean('sort_by_votes');
$users = DB::table('users')
->when($sortByVotes, function (Builder $query, bool $sortByVotes) {
$query->orderBy('votes'); // 투표 수 기준 정렬
}, function (Builder $query) {
$query->orderBy('name'); // 기본값: 이름 기준 정렬
})
->get();NOTE
when을 활용하면 조건 분기를 쿼리 빌더 체인 밖으로 꺼내지 않아도 되어 코드가 훨씬 깔끔해집니다. 검색 필터나 정렬 옵션처럼 선택적 조건이 많은 목록 조회 API에서 특히 유용합니다.
Insert 구문
레코드 삽입
쿼리 빌더의 insert 메서드를 사용하면 데이터베이스 테이블에 레코드를 삽입할 수 있습니다. 컬럼 이름과 값을 배열로 전달하면 됩니다.
DB::table('users')->insert([
'email' => 'kayla@example.com',
'votes' => 0
]);배열의 배열을 전달하면 여러 레코드를 한 번에 삽입할 수도 있습니다. 각 배열이 테이블에 삽입될 하나의 레코드를 나타냅니다.
DB::table('users')->insert([
['email' => 'picard@example.com', 'votes' => 0],
['email' => 'janeway@example.com', 'votes' => 0],
]);insertOrIgnore 메서드는 레코드 삽입 중 발생하는 오류를 무시합니다. 이 메서드를 사용할 때는 중복 레코드 오류뿐만 아니라, 데이터베이스 엔진에 따라 다른 종류의 오류도 무시될 수 있다는 점에 유의하세요. 예를 들어, insertOrIgnore는 MySQL의 strict 모드를 우회합니다.
DB::table('users')->insertOrIgnore([
['id' => 1, 'email' => 'sisko@example.com'],
['id' => 2, 'email' => 'archer@example.com'],
]);insertUsing 메서드는 서브쿼리로 삽입할 데이터를 결정하여 새 레코드를 테이블에 삽입합니다.
DB::table('pruned_users')->insertUsing([
'id', 'name', 'email', 'email_verified_at'
], DB::table('users')->select(
'id', 'name', 'email', 'email_verified_at'
)->where('updated_at', '<=', now()->minus(months: 1)));자동 증가 ID
테이블에 자동 증가(auto-increment) ID가 있는 경우, insertGetId 메서드를 사용하면 레코드를 삽입한 뒤 해당 ID를 바로 반환받을 수 있습니다.
$id = DB::table('users')->insertGetId(
['email' => 'john@example.com', 'votes' => 0]
);WARNING
PostgreSQL에서 insertGetId 메서드를 사용할 때는 자동 증가 컬럼의 이름이 id여야 합니다. 다른 이름의 시퀀스에서 ID를 가져오려면 insertGetId 메서드의 두 번째 인자로 컬럼명을 전달하세요.
Upsert
upsert 메서드는 존재하지 않는 레코드는 삽입하고, 이미 존재하는 레코드는 지정한 값으로 업데이트합니다. 첫 번째 인자는 삽입 또는 업데이트할 값의 배열이고, 두 번째 인자는 테이블에서 레코드를 고유하게 식별하는 컬럼 목록입니다. 세 번째 인자는 일치하는 레코드가 이미 존재할 때 업데이트할 컬럼 목록입니다.
DB::table('flights')->upsert(
[
['departure' => '인천', 'destination' => '제주', 'price' => 99],
['departure' => '김포', 'destination' => '부산', 'price' => 150]
],
['departure', 'destination'],
['price']
);위 예시에서 Laravel은 두 개의 레코드를 삽입하려 시도합니다. 동일한 departure와 destination 값을 가진 레코드가 이미 존재하면, 해당 레코드의 price 컬럼을 업데이트합니다.
WARNING
SQL Server를 제외한 모든 데이터베이스에서는 upsert의 두 번째 인자로 지정하는 컬럼에 "primary" 또는 "unique" 인덱스가 있어야 합니다. 또한 MariaDB와 MySQL 드라이버는 두 번째 인자를 무시하고, 항상 테이블의 "primary" 및 "unique" 인덱스를 기준으로 기존 레코드를 판별합니다.
레코드 수정
UPDATE 문
쿼리 빌더의 update 메서드를 사용하면 기존 레코드를 수정할 수 있습니다. insert와 마찬가지로 컬럼명과 값의 쌍을 배열로 전달하며, 반환값은 실제로 영향을 받은 행의 수입니다. where 절을 함께 사용해 수정 대상을 제한할 수 있습니다:
$affected = DB::table('users')
->where('id', 1)
->update(['votes' => 1]);Update or Insert
조건에 맞는 레코드가 있으면 수정하고, 없으면 새로 삽입하고 싶을 때는 updateOrInsert 메서드를 사용합니다. 이 메서드는 두 개의 인수를 받습니다:
- 첫 번째 인수 — 레코드를 찾기 위한 조건 (컬럼/값 배열)
- 두 번째 인수 — 수정하거나 삽입할 컬럼/값 배열
레코드가 존재하면 두 번째 인수의 값으로 수정되고, 존재하지 않으면 두 인수를 합친 속성으로 새 레코드가 삽입됩니다:
DB::table('users')
->updateOrInsert(
['email' => 'john@example.com', 'name' => 'John'],
['votes' => '2']
);레코드 존재 여부에 따라 수정/삽입할 속성을 다르게 지정하고 싶다면 클로저를 전달할 수 있습니다. 클로저의 $exists 인수는 해당 레코드의 존재 여부를 나타내는 불리언 값입니다:
DB::table('users')->updateOrInsert(
['user_id' => $user_id],
fn ($exists) => $exists ? [
'name' => $data['name'],
'email' => $data['email'],
] : [
'name' => $data['name'],
'email' => $data['email'],
'marketable' => true,
],
);JSON 컬럼 수정
JSON 컬럼의 특정 키만 수정할 때는 -> 구문을 사용합니다. MariaDB 10.3+, MySQL 5.7+, PostgreSQL 9.5+ 이상에서 지원됩니다:
$affected = DB::table('users')
->where('id', 1)
->update(['options->enabled' => true]);NOTE
options->enabled처럼 -> 구문은 JSON 객체 내부의 중첩 키를 직접 지정합니다. 이 방식은 JSON 컬럼 전체를 교체하지 않고 특정 키만 업데이트합니다.
증가 및 감소
특정 컬럼의 값을 1씩 늘리거나 줄이는 작업은 increment / decrement 메서드로 간편하게 처리할 수 있습니다. 두 번째 인수로 증감할 양을 지정할 수 있습니다:
DB::table('users')->increment('votes'); // 1 증가
DB::table('users')->increment('votes', 5); // 5 증가
DB::table('users')->decrement('votes'); // 1 감소
DB::table('users')->decrement('votes', 5); // 5 감소증감 연산과 동시에 다른 컬럼도 함께 수정하려면 세 번째 인수에 배열을 전달합니다:
DB::table('users')->increment('votes', 1, ['name' => 'John']);여러 컬럼을 한 번에 증가하거나 감소시킬 때는 incrementEach / decrementEach 메서드를 사용합니다:
DB::table('users')->incrementEach([
'votes' => 5,
'balance' => 100,
]);DELETE 구문
쿼리 빌더의 delete 메서드를 사용하면 테이블에서 레코드를 삭제할 수 있습니다. delete 메서드는 영향을 받은 행의 수를 반환합니다. delete 메서드를 호출하기 전에 where 절을 추가하여 삭제 범위를 제한할 수 있습니다:
$deleted = DB::table('users')->delete();
$deleted = DB::table('users')->where('votes', '>', 100)->delete();비관적 잠금 (Pessimistic Locking)
쿼리 빌더는 select 구문 실행 시 **비관적 잠금(pessimistic locking)**을 적용할 수 있는 메서드를 제공합니다.
NOTE
비관적 잠금이란, 동시에 여러 요청이 같은 데이터를 수정하려 할 때 충돌을 방지하기 위해 DB 수준에서 행(row)을 잠그는 방식입니다. 낙관적 잠금(optimistic locking)과 달리 충돌이 발생하기 전에 미리 막는 전략입니다.
공유 잠금 (Shared Lock)
sharedLock 메서드를 사용하면 조회된 행이 트랜잭션이 완료되기 전까지 다른 트랜잭션에 의해 수정되지 않도록 보호합니다:
DB::table('users')
->where('votes', '>', 100)
->sharedLock()
->get();배타적 잠금 (For Update Lock)
lockForUpdate 메서드는 선택된 레코드를 수정하거나 다른 공유 잠금으로 선택하는 것 자체를 차단합니다. 데이터를 읽은 후 반드시 수정할 예정이라면 이 메서드를 사용하세요:
DB::table('users')
->where('votes', '>', 100)
->lockForUpdate()
->get();트랜잭션과 함께 사용하기
비관적 잠금은 반드시 필요한 것은 아니지만, 트랜잭션 안에서 사용하는 것을 강력히 권장합니다. 트랜잭션으로 감싸면 작업 전체가 완료되기 전까지 데이터가 변경되지 않음을 보장하며, 중간에 오류가 발생하더라도 변경 사항이 자동으로 롤백되고 잠금도 해제됩니다.
아래는 사용자 간 포인트 이체를 비관적 잠금과 트랜잭션으로 안전하게 처리하는 예시입니다:
DB::transaction(function () {
$sender = DB::table('users')
->lockForUpdate()
->find(1);
$receiver = DB::table('users')
->lockForUpdate()
->find(2);
if ($sender->balance < 100) {
throw new RuntimeException('잔액이 부족합니다.');
}
DB::table('users')
->where('id', $sender->id)
->update([
'balance' => $sender->balance - 100
]);
DB::table('users')
->where('id', $receiver->id)
->update([
'balance' => $receiver->balance + 100
]);
});이 예시에서 lockForUpdate를 사용하면 송신자와 수신자 레코드를 읽는 순간부터 트랜잭션이 끝날 때까지 다른 트랜잭션이 해당 행을 수정하지 못하도록 막습니다. 만약 잔액 부족으로 예외가 발생하면 트랜잭션 전체가 롤백되어 데이터 정합성이 유지됩니다.
재사용 가능한 쿼리 컴포넌트
애플리케이션 전반에 걸쳐 동일한 쿼리 로직이 반복된다면, tap과 pipe 메서드를 사용해 해당 로직을 재사용 가능한 객체로 분리할 수 있습니다. 예를 들어, 다음과 같이 두 곳에서 거의 동일한 필터링 로직이 사용된다고 가정해 봅시다:
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
$destination = $request->query('destination');
DB::table('flights')
->when($destination, function (Builder $query, string $destination) {
$query->where('destination', $destination);
})
->orderByDesc('price')
->get();
// ...
$destination = $request->query('destination');
DB::table('flights')
->when($destination, function (Builder $query, string $destination) {
$query->where('destination', $destination);
})
->where('user', $request->user()->id)
->orderBy('destination')
->get();두 쿼리에 공통으로 쓰이는 목적지(destination) 필터 로직을 별도의 클래스로 추출할 수 있습니다:
<?php
namespace App\Scopes;
use Illuminate\Database\Query\Builder;
class DestinationFilter
{
public function __construct(
private ?string $destination,
) {
//
}
public function __invoke(Builder $query): void
{
$query->when($this->destination, function (Builder $query) {
$query->where('destination', $this->destination);
});
}
}이제 쿼리 빌더의 tap 메서드를 사용해 이 객체의 로직을 쿼리에 적용할 수 있습니다:
use App\Scopes\DestinationFilter;use Illuminate\Database\Query\Builder;use Illuminate\Support\Facades\DB; DB::table('flights') ->when($destination, function (Builder $query, string $destination) { // $query->where('destination', $destination); // }) // ->tap(new DestinationFilter($destination)) // ->orderByDesc('price') ->get(); // ... DB::table('flights') ->when($destination, function (Builder $query, string $destination) { // $query->where('destination', $destination); // }) // ->tap(new DestinationFilter($destination)) // ->where('user', $request->user()->id) ->orderBy('destination') ->get();NOTE
tap은 쿼리 빌더 인스턴스 자체를 그대로 반환합니다. 따라서 메서드 체이닝을 끊지 않고 중간에 부가 로직을 삽입할 때 적합합니다.
쿼리 파이프 (Query Pipes)
tap 메서드는 항상 쿼리 빌더 인스턴스를 반환합니다. 반면, 쿼리를 직접 실행하고 다른 값을 반환하는 객체를 만들고 싶다면 pipe 메서드를 사용하세요.
예를 들어, 애플리케이션 전반에서 공통으로 사용하는 페이지네이션 로직을 담은 객체를 생각해 봅시다. DestinationFilter는 쿼리 조건만 추가하는 반면, 아래의 Paginate 객체는 쿼리를 직접 실행하고 페이지네이터 인스턴스를 반환합니다:
<?php
namespace App\Scopes;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Query\Builder;
class Paginate
{
public function __construct(
private string $sortBy = 'timestamp',
private string $sortDirection = 'desc',
private int $perPage = 25,
) {
//
}
public function __invoke(Builder $query): LengthAwarePaginator
{
return $query->orderBy($this->sortBy, $this->sortDirection)
->paginate($this->perPage, pageName: 'p');
}
}pipe 메서드를 사용하면 이 객체를 통해 공통 페이지네이션 로직을 간결하게 적용할 수 있습니다:
$flights = DB::table('flights')
->tap(new DestinationFilter($destination))
->pipe(new Paginate);두 메서드의 차이를 정리하면 다음과 같습니다:
| 메서드 | 역할 | 반환값 |
|---|---|---|
tap | 쿼리 조건 추가 등 부가 작업 수행 | 쿼리 빌더 인스턴스 (체이닝 유지) |
pipe | 쿼리 실행 및 결과 변환 | 호출된 객체가 반환하는 임의의 값 |
쿼리 빌더
디버깅
쿼리를 작성하는 도중 현재 쿼리의 바인딩 값과 SQL 문을 확인하고 싶을 때는 dd와 dump 메서드를 사용할 수 있습니다.
dump— 디버그 정보를 출력하되, 이후 요청 처리를 계속 진행합니다.dd— 디버그 정보를 출력한 뒤 요청 실행을 즉시 중단합니다.
DB::table('users')->where('votes', '>', 100)->dump();
DB::table('users')->where('votes', '>', 100)->dd();바인딩 파라미터가 실제 값으로 치환된 완성된 SQL을 확인하고 싶다면 dumpRawSql과 ddRawSql 메서드를 사용하세요. 로그에 남기거나 DB 클라이언트에 직접 붙여넣어 테스트할 때 유용합니다.
DB::table('users')->where('votes', '>', 100)->dumpRawSql();
DB::table('users')->where('votes', '>', 100)->ddRawSql();NOTE
dd와 ddRawSql은 실행 즉시 응답을 종료하므로, 프로덕션 코드에 남기지 않도록 주의하세요. 개발·디버깅 용도로만 사용하고 커밋 전에 반드시 제거하는 것을 권장합니다.