데이터베이스: 쿼리 빌더
업데이트됨번역일: 2026년 8월 26일
이 페이지는 원문이 업데이트되어 번역이 갱신되었습니다.
- 원문 수정
- 2026년 8월 26일
- 번역 갱신
- 2026년 8월 26일
데이터베이스: 쿼리 빌더
- 소개
- 데이터베이스 쿼리 실행
- Select 구문
- Raw 표현식
- 조인
- 유니온
- 기본 Where 절
- 고급 Where 절
- 정렬, 그룹화, Limit 및 Offset
- 조건부 절
- Insert 구문
- Update 구문
- Delete 구문
- 비관적 잠금
- 재사용 가능한 쿼리 컴포넌트
- 디버깅
쿼리 빌더
목차
- 소개
- 데이터베이스 쿼리 실행
- SELECT 구문
- Raw 표현식
- 조인
- Union
- 기본 WHERE 절
- 고급 WHERE 절
- 정렬, 그룹핑, LIMIT, OFFSET
- 조건부 절
- INSERT 구문
- UPDATE 구문
- DELETE 구문
- 비관적 잠금
- 디버깅
소개
Laravel의 데이터베이스 쿼리 빌더는 데이터베이스 쿼리를 손쉽게 구성하고 실행할 수 있는 유연한 인터페이스를 제공합니다. 애플리케이션에서 필요한 대부분의 데이터베이스 작업을 처리할 수 있으며, Laravel이 지원하는 모든 데이터베이스 시스템과 함께 완벽하게 동작합니다.
쿼리 빌더는 내부적으로 PDO 파라미터 바인딩을 사용하여 SQL 인젝션 공격으로부터 애플리케이션을 보호합니다. 쿼리 빌더에 전달하는 문자열을 별도로 이스케이프하거나 정제할 필요가 없습니다.
WARNING
PDO는 컬럼명 바인딩을 지원하지 않습니다. 따라서 "order by" 컬럼을 포함하여, 쿼리에서 참조하는 컬럼명을 사용자 입력으로 지정하도록 절대 허용해서는 안 됩니다.
데이터베이스 쿼리 실행
테이블의 전체 행 조회
DB 파사드의 table 메서드로 쿼리를 시작할 수 있습니다. table 메서드는 지정한 테이블에 대한 플루언트(fluent) 쿼리 빌더 인스턴스를 반환하며, 여기에 다양한 조건을 체이닝한 뒤 마지막에 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', '홍길동')->first();
return $user->email;조건에 맞는 행이 없을 때 Illuminate\Database\RecordNotFoundException 예외를 발생시키려면 firstOrFail 메서드를 사용하세요. 이 예외를 별도로 처리하지 않으면 Laravel이 자동으로 404 HTTP 응답을 반환합니다.
$user = DB::table('users')->where('name', '홍길동')->firstOrFail();전체 행이 아닌 특정 컬럼 값 하나만 필요하다면 value 메서드를 사용하세요. 컬럼 값을 직접 반환합니다.
$email = DB::table('users')->where('name', '홍길동')->value('email');id 컬럼 값으로 단일 행을 조회하려면 find 메서드를 사용하세요.
$user = DB::table('users')->find(3);특정 컬럼 값 목록 조회
단일 컬럼의 값들을 Illuminate\Support\Collection으로 가져오려면 pluck 메서드를 사용하세요. 아래 예시는 사용자의 title 컬럼 값들을 컬렉션으로 가져옵니다.
use Illuminate\Support\Facades\DB;
$titles = DB::table('users')->pluck('title');
foreach ($titles as $title) {
echo $title;
}pluck 메서드의 두 번째 인자로 컬럼명을 지정하면 해당 컬럼 값을 컬렉션의 키로 사용할 수 있습니다.
$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;
});청크 처리 도중 레코드를 업데이트하면 청크 결과가 예상치 못한 방식으로 달라질 수 있습니다. 조회한 레코드를 업데이트할 계획이라면 chunk 대신 chunkById 메서드를 사용하세요. 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) 스트리밍
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 쿼리에 문자열로 직접 삽입됩니다. SQL 인젝션 취약점이 생기지 않도록 각별히 주의하여 사용하십시오.
Raw 메서드
DB::raw를 직접 사용하는 대신, 쿼리의 각 절(clause)에 맞는 전용 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 메서드는 쿼리의 WHERE 절에 Raw SQL 문자열을 삽입합니다. 마찬가지로 두 번째 인수로 바인딩 배열을 전달할 수 있습니다:
$orders = DB::table('orders')
->whereRaw('price > IF(state = "TX", ?, 100)', [200])
->get();`havingRaw / orHavingRaw`
havingRaw와 orHavingRaw 메서드는 HAVING 절에 Raw SQL 문자열을 삽입합니다. 두 번째 인수로 바인딩 배열을 전달할 수 있습니다:
$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 SQL 문자열을 삽입합니다:
$orders = DB::table('orders')
->orderByRaw('updated_at - created_at DESC')
->get();`groupByRaw`
groupByRaw 메서드는 GROUP BY 절에 Raw SQL 문자열을 삽입합니다:
$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 조건이 필요하다면, join 메서드의 두 번째 인자로 클로저를 전달합니다. 클로저는 Illuminate\Database\Query\JoinClause 인스턴스를 받으며, 이를 통해 다양한 JOIN 조건을 유연하게 구성할 수 있습니다.
DB::table('users')
->join('contacts', function (JoinClause $join) {
$join->on('users.id', '=', 'contacts.user_id')->orOn(/* ... */);
})
->get();JoinClause 인스턴스에서 where 및 orWhere 메서드를 사용하면, 두 컬럼을 비교하는 대신 컬럼과 특정 값을 비교하는 WHERE 조건을 JOIN 절 안에 추가할 수 있습니다.
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을 수행할 수 있습니다. 세 메서드 모두 동일하게 세 가지 인자를 받습니다: 서브쿼리, 테이블 별칭(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을 수행할 수 있습니다. 두 메서드 모두 서브쿼리와 테이블 별칭(alias), 두 가지 인자를 받습니다. JOIN 조건은 서브쿼리 내부의 where 절로 지정합니다.
Lateral Join은 외부 쿼리의 각 행에 대해 서브쿼리를 개별적으로 실행하며, 서브쿼리 안에서 외부 테이블의 컬럼을 참조할 수 있습니다. 일반 서브쿼리 JOIN과의 핵심 차이점입니다.
아래 예시에서는 각 사용자와 함께 해당 사용자의 최근 게시글 최대 3개를 조회합니다. 사용자 한 명당 최대 3개의 행이 결과에 포함될 수 있으며, JOIN 조건은 서브쿼리 내 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은 중복 행을 제거하지만, unionAll은 중복을 그대로 유지합니다. 메서드 시그니처는 union과 동일합니다.
NOTE
SQL의 UNION은 중복을 제거하고, UNION ALL은 중복을 허용합니다. 성능 면에서는 중복 제거가 필요 없다면 unionAll이 더 빠를 수 있습니다.
기본 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 메서드를 사용해 특정 조건 그룹을 부정할 수 있습니다. 예를 들어, 아래 쿼리는 재고 정리 중이거나 가격이 10 미만인 상품을 제외하고 조회합니다.
$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', '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의 대소문자 구분 옵션은 현재 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은 고정 값이 같은 행의 두 컬럼 값 사이에 있는지 확인합니다. whereBetweenColumns와 반대로, 기준이 컬럼이 아닌 값(value)입니다.
$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는 두 NULL 값을 동등하게 처리하면서 컬럼 값을 비교합니다. 일반적인 = 연산자는 NULL = NULL을 false로 처리하지만, 이 메서드는 true로 처리합니다.
$lastLoginIp = $request->input('last_login_ip');
$users = DB::table('users')
->whereNullSafeEquals('last_login_ip', $lastLoginIp)
->get();whereDate / whereMonth / whereDay / whereYear / whereTime
날짜·시간 관련 조건을 세밀하게 지정할 수 있는 메서드들입니다.
// 특정 날짜
$users = DB::table('users')
->whereDate('created_at', '2016-12-31')
->get();
// 특정 월
$users = DB::table('users')
->whereMonth('created_at', '12')
->get();
// 특정 일(일자)
$users = DB::table('users')
->whereDay('created_at', '31')
->get();
// 특정 연도
$users = DB::table('users')
->whereYear('created_at', '2016')
->get();
// 특정 시각
$users = DB::table('users')
->whereTime('created_at', '=', '11:20:45')
->get();wherePast / whereFuture / whereToday / whereBeforeToday / whereAfterToday
현재 시각을 기준으로 과거·미래 여부를 조건으로 걸 수 있습니다.
// 과거 (현재 시각 이전)
$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();오늘 날짜를 기준으로 비교할 때는 아래 메서드들을 사용합니다.
// 오늘
$invoices = DB::table('invoices')
->whereToday('due_at')
->get();
// 오늘 이전
$invoices = DB::table('invoices')
->whereBeforeToday('due_at')
->get();
// 오늘 이후
$invoices = DB::table('invoices')
->whereAfterToday('due_at')
->get();오늘을 포함하여 비교하려면 아래 메서드들을 사용합니다.
// 오늘 이전 (오늘 포함)
$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();클로저를 전달하면 쿼리 빌더는 해당 블록을 괄호로 묶어 처리합니다. 클로저는 쿼리 빌더 인스턴스를 인수로 받으며, 그 안에서 설정한 조건들이 괄호 안에 포함됩니다. 위 코드는 다음 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에서만 지원됩니다.
전문 검색 인덱스가 설정된 컬럼에 대해 전문 검색 "where" 절을 추가하려면 whereFullText와 orWhereFullText 메서드를 사용합니다. Laravel은 각 데이터베이스에 맞는 SQL로 자동 변환해 줍니다. MariaDB나 MySQL을 사용하는 경우에는 MATCH AGAINST 절이 생성됩니다.
$users = DB::table('users')
->whereFullText('bio', 'web developer')
->get();벡터 유사도 절
NOTE
벡터 유사도 절은 현재 pgvector 익스텐션을 사용하는 PostgreSQL 연결과 MariaDB 11.7 이상에서 지원됩니다. 벡터 컬럼 및 인덱스 정의 방법은 마이그레이션 문서를 참고하세요.
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)
->limit(5)
->get();NOTE
실제 서비스에서 페이지네이션이 필요하다면 offset/limit을 직접 다루기보다 Laravel의 페이지네이션 기능을 활용하는 것을 권장합니다. Laravel 페이지네이터는 LIMIT과 OFFSET을 자동으로 처리하며 편리한 링크 생성 기능도 제공합니다.
조건부 절
쿼리에 특정 조건이 충족될 때만 WHERE 절 등을 추가하고 싶을 때가 있습니다. 예를 들어, 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 값이 요청에 포함되어 있고 빈 값이 아닐 때만 where 조건이 쿼리에 추가됩니다.
NOTE
when의 첫 번째 인수로 빈 문자열("")이나 null이 전달되면 false로 평가되어 클로저가 실행되지 않습니다. 검색 필터처럼 선택적 파라미터를 다룰 때 매우 유용한 패턴입니다.
세 번째 인수로 클로저를 추가하면, 첫 번째 인수가 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();sort_by_votes 파라미터가 true이면 투표 수 기준으로 정렬되고, 그렇지 않으면 이름순으로 정렬됩니다. 조건에 따라 다른 동작을 수행하는 폴백 로직을 when 하나로 간결하게 표현할 수 있습니다.
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 메서드는 레코드 삽입 중 발생하는 오류를 무시합니다. 중복 레코드 오류는 물론, 데이터베이스 엔진에 따라 다른 종류의 오류도 함께 무시될 수 있습니다. 예를 들어 MySQL에서는 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여야 합니다. 다른 시퀀스(sequence)에서 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]);업데이트 또는 삽입 (updateOrInsert)
조건에 맞는 레코드가 있으면 수정하고, 없으면 새로 삽입하고 싶을 때는 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]);값 증가 및 감소 (Increment / Decrement)
특정 컬럼의 값을 1씩 증가시키거나 감소시킬 때 편리하게 사용할 수 있는 메서드입니다. 첫 번째 인수로 대상 컬럼명을 지정하고, 두 번째 인수로 증감할 크기를 지정할 수 있습니다. (기본값: 1)
DB::table('users')->increment('votes'); // votes + 1
DB::table('users')->increment('votes', 5); // votes + 5
DB::table('users')->decrement('votes'); // votes - 1
DB::table('users')->decrement('votes', 5); // votes - 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
비관적 잠금이란, 데이터 충돌이 발생할 가능성이 높다고 가정하고 미리 잠금을 걸어 다른 트랜잭션의 접근을 막는 방식입니다. 반대 개념인 낙관적 잠금(Optimistic Locking)은 충돌이 드물다고 가정하고, 실제 저장 시점에만 충돌 여부를 확인합니다.
공유 잠금 (Shared Lock)
sharedLock 메서드를 사용하면 조회한 행(row)이 트랜잭션이 커밋될 때까지 다른 트랜잭션에 의해 수정되지 않도록 공유 잠금을 겁니다:
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를 사용하지 않으면, 두 요청이 동시에 같은 사용자의 잔액을 조회하고 각각 차감·적립하는 경쟁 조건(Race Condition)이 발생할 수 있습니다. 잠금을 걸면 첫 번째 트랜잭션이 완료될 때까지 두 번째 트랜잭션이 대기하므로 데이터 정합성을 유지할 수 있습니다.
재사용 가능한 쿼리 컴포넌트
애플리케이션 곳곳에서 동일한 쿼리 로직이 반복된다면, 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();두 쿼리에서 공통으로 사용되는 목적지 필터링 로직을 별도의 객체로 추출하면 코드 중복을 줄일 수 있습니다.
<?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은 쿼리 빌더 인스턴스를 그대로 반환하므로, 메서드 체이닝을 자연스럽게 이어갈 수 있습니다. 쿼리를 실행하고 다른 값을 반환해야 하는 경우에는 아래에서 설명하는 pipe를 사용하세요.
쿼리 파이프
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 | 쿼리를 실행하거나 변환하는 로직 적용 | 객체가 반환하는 임의의 값 |
쿼리 빌더
디버깅
쿼리를 작성하는 도중 dd 또는 dump 메서드를 사용하면 현재 쿼리의 바인딩 값과 SQL 문을 출력할 수 있습니다. dd 메서드는 디버그 정보를 출력한 뒤 요청 실행을 즉시 중단합니다. 반면 dump 메서드는 디버그 정보를 출력하되 요청 실행은 계속 이어집니다.
DB::table('users')->where('votes', '>', 100)->dd();
DB::table('users')->where('votes', '>', 100)->dump();바인딩 파라미터가 실제 값으로 치환된 완성된 SQL을 확인하고 싶다면 dumpRawSql 또는 ddRawSql 메서드를 사용하세요.
DB::table('users')->where('votes', '>', 100)->dumpRawSql();
DB::table('users')->where('votes', '>', 100)->ddRawSql();NOTE
dd와 ddRawSql은 출력 후 실행을 중단하므로 API 응답이나 배치 처리 중간에 사용할 때는 주의가 필요합니다. 실행 흐름을 유지하면서 확인만 하려면 dump / dumpRawSql을 사용하세요.