데이터베이스: 쿼리 빌더
번역일: 2026년 7월 2일
데이터베이스: 쿼리 빌더
- 소개
- 데이터베이스 쿼리 실행
- Select 구문
- Raw 표현식
- Join
- Union
- 기본 Where 절
- 고급 Where 절
- 정렬, 그룹화, Limit, Offset
- 조건부 절
- Insert 구문
- Update 구문
- Delete 구문
- 비관적 잠금
- 디버깅
소개
Laravel의 데이터베이스 쿼리 빌더는 데이터베이스 쿼리를 편리하고 유창하게 작성할 수 있는 인터페이스를 제공합니다. 복잡한 SQL을 직접 작성하지 않아도 대부분의 데이터베이스 작업을 처리할 수 있으며, 애플리케이션이 지원하는 모든 데이터베이스 시스템에서 동일하게 동작합니다.
Laravel 쿼리 빌더는 SQL 인젝션 공격으로부터 애플리케이션을 보호하기 위해 PDO 파라미터 바인딩을 사용합니다. 쿼리 빌더에 전달하는 문자열은 별도로 이스케이프 처리하지 않아도 됩니다.
WARNING
PDO는 컬럼명 바인딩을 지원하지 않습니다. 따라서 쿼리에서 참조하는 컬럼명, 특히 order by 절의 컬럼명을 사용자 입력으로 결정하는 것은 절대 허용해서는 안 됩니다.
데이터베이스: 쿼리 빌더
소개
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 컬렉션은 데이터를 매핑하거나 가공하는 데 유용한 강력한 메서드들을 제공합니다. 자세한 내용은 컬렉션 문서를 참고하세요.
단일 행 / 컬럼 조회
단일 행만 필요하다면 first 메서드를 사용하세요. 이 메서드는 stdClass 객체 하나를 반환합니다.
$user = DB::table('users')->where('name', 'John')->first();
return $user->email;조건에 맞는 행이 없을 때 Illuminate\Database\RecordNotFoundException을 발생시키고 싶다면 firstOrFail 메서드를 사용하세요. 이 예외를 별도로 처리하지 않으면 Laravel이 자동으로 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;
}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;
});청크 처리 도중 레코드를 업데이트하는 경우, 결과가 예상치 못하게 달라질 수 있습니다. 조회한 레코드를 청크 내에서 업데이트할 계획이라면 반드시 chunkById 메서드를 사용하세요. 이 메서드는 기본 키(primary key)를 기준으로 자동으로 페이지네이션을 처리합니다.
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를 직접 사용하는 대신, 쿼리의 각 절(clause)에 맞는 전용 Raw 메서드를 사용할 수 있습니다. 단, Raw 표현식을 사용하는 쿼리는 Laravel이 SQL 인젝션으로부터 안전함을 보장하지 않습니다.
`selectRaw`
selectRaw는 addSelect(DB::raw(/* ... */)) 대신 사용할 수 있습니다. 두 번째 인수로 바인딩 배열을 선택적으로 전달할 수 있습니다:
$orders = DB::table('orders')
->selectRaw('price * ? as price_with_tax', [1.1])
->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 메서드를 사용하면 테이블 조인을 수행할 수 있습니다. 첫 번째 인수에는 조인할 테이블 이름을, 나머지 인수에는 조인 조건으로 사용할 컬럼을 지정합니다. 한 번의 쿼리에서 여러 테이블을 동시에 조인하는 것도 가능합니다.
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 메서드는 두 테이블의 카테시안 곱(Cartesian Product)을 반환하는 Cross Join을 수행합니다. 예를 들어 사이즈와 색상 조합을 모두 구할 때 유용합니다.
$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을 수행할 수 있습니다. 각 메서드는 서브쿼리와 테이블 별칭(alias) 두 가지 인수를 받습니다. 조인 조건은 서브쿼리 내부의 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;
$first = DB::table('users')
->whereNull('first_name');
$users = DB::table('users')
->whereNull('last_name')
->union($first)
->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('votes', '>=', 100)
->get();
$users = DB::table('users')
->where('votes', '<>', 100)
->get();
$users = DB::table('users')
->where('name', 'like', '김%')
->get();조건 배열을 where 메서드에 전달할 수도 있습니다. 배열의 각 요소는 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', '홍길동')
->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)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', '홍%')
->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 컬럼 타입을 지원하는 데이터베이스에서 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();JSON 배열을 쿼리할 때는 whereJsonContains를 사용합니다:
$users = DB::table('users')
->whereJsonContains('options->languages', 'ko')
->get();MariaDB, MySQL, PostgreSQL에서는 배열 값도 전달할 수 있습니다:
$users = DB::table('users')
->whereJsonContains('options->languages', ['ko', 'en'])
->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 메서드는 데이터베이스 종류에 관계없이 일관된 방식으로 문자열 패턴 매칭을 수행합니다. 기본적으로 대소문자를 구분하지 않습니다:
$users = DB::table('users')
->whereLike('name', '%길동%')
->get();대소문자 구분 검색이 필요하다면 caseSensitive 인수를 사용하세요:
$users = DB::table('users')
->whereLike('name', '%John%', caseSensitive: true)
->get();orWhereLike는 OR 조건으로 LIKE 절을 추가합니다:
$users = DB::table('users')
->where('votes', '>', 100)
->orWhereLike('name', '%길동%')
->get();whereNotLike는 NOT LIKE 조건을 추가합니다:
$users = DB::table('users')
->whereNotLike('name', '%테스트%')
->get();orWhereNotLike는 OR NOT LIKE 조건을 추가합니다:
$users = DB::table('users')
->where('votes', '>', 100)
->orWhereNotLike('name', '%테스트%')
->get();WARNING
whereLike의 대소문자 구분 옵션(caseSensitive: true)은 현재 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();whereIn의 두 번째 인수로 서브쿼리 객체를 전달할 수도 있습니다:
$activeUsers = DB::table('users')->select('id')->where('is_active', 1);
$users = 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();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', '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();오늘을 포함하여 이전/이후를 판단할 때는 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', '=', '홍길동')
->where(function (Builder $query) {
$query->where('votes', '>', 100)
->orWhere('title', '=', 'Admin');
})
->get();클로저를 전달하면 쿼리 빌더가 괄호 그룹을 시작하고, 클로저 안에서 정의한 조건들이 해당 괄호 안에 포함됩니다. 위 예시는 다음 SQL을 생성합니다:
select * from users where name = '홍길동' 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 메서드를 사용하면 전문 검색 인덱스가 설정된 컬럼에 대해 전문 검색 where 절을 추가할 수 있습니다. Laravel은 사용 중인 데이터베이스에 맞는 SQL로 자동 변환해 줍니다. MariaDB나 MySQL을 사용하는 경우 MATCH AGAINST 절이 생성됩니다.
$users = DB::table('users')
->whereFullText('bio', 'web developer')
->get();정렬, 그룹화, 한도 및 오프셋
정렬
`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 메서드를 사용해 쿼리 결과를 그룹화할 수 있습니다. 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 메서드를 참고하세요.
한도 및 오프셋
`skip` 및 `take` 메서드
skip과 take 메서드를 사용해 반환할 결과의 수를 제한하거나, 앞쪽 결과 일부를 건너뛸 수 있습니다. 페이지네이션을 직접 구현할 때 자주 쓰이는 패턴입니다.
$users = DB::table('users')->skip(10)->take(5)->get();limit과 offset 메서드도 동일한 기능을 제공합니다. 각각 take, skip과 동일하게 동작하며, SQL 문법에 익숙한 경우 이 방식이 더 직관적으로 느껴질 수 있습니다.
$users = DB::table('users')
->offset(10)
->limit(5)
->get();조건부 절 (Conditional Clauses)
HTTP 요청에 특정 값이 있을 때만 쿼리 조건을 적용하고 싶은 경우가 있습니다. 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 값이 존재하고 truthy한 경우에만 where 조건이 쿼리에 추가됩니다.
세 번째 인수로 클로저를 하나 더 전달하면, 첫 번째 인수가 false일 때 대신 실행되는 기본 동작을 지정할 수 있습니다. 아래 예시는 정렬 조건을 조건부로 적용하는 전형적인 패턴입니다.
$sortByVotes = $request->boolean('sort_by_votes');
$users = DB::table('users')
->when($sortByVotes, function (Builder $query, bool $sortByVotes) {
$query->orderBy('votes'); // sort_by_votes가 true이면 득표수 정렬
}, function (Builder $query) {
$query->orderBy('name'); // 그렇지 않으면 이름 정렬 (기본값)
})
->get();NOTE
when을 활용하면 if 문 없이도 쿼리 빌더 체이닝을 유지할 수 있어 코드가 더 읽기 쉬워집니다. 필터나 정렬 옵션이 여러 개인 검색 기능을 구현할 때 특히 유용합니다.
Insert 구문
레코드 삽입
쿼리 빌더는 데이터베이스 테이블에 레코드를 삽입할 때 사용하는 insert 메서드를 제공합니다. 컬럼명과 값의 배열을 전달하면 됩니다.
DB::table('users')->insert([
'email' => 'kayla@example.com',
'votes' => 0
]);배열의 배열을 전달하면 여러 레코드를 한 번에 삽입할 수 있습니다. 각 배열이 하나의 레코드를 나타냅니다.
DB::table('users')->insert([
['email' => 'hong@example.com', 'votes' => 0],
['email' => 'kim@example.com', 'votes' => 0],
]);insertOrIgnore 메서드는 레코드 삽입 중 발생하는 오류를 무시합니다. 중복 레코드 오류뿐 아니라, 데이터베이스 엔진에 따라 다른 유형의 오류도 무시될 수 있다는 점에 유의하세요. 예를 들어, MySQL에서는 insertOrIgnore가 strict 모드를 우회합니다.
DB::table('users')->insertOrIgnore([
['id' => 1, 'email' => 'hong@example.com'],
['id' => 2, 'email' => 'kim@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 반환
테이블에 자동 증가(auto-increment) ID가 있을 때, 삽입 후 해당 ID 값을 바로 얻으려면 insertGetId 메서드를 사용하세요.
$id = DB::table('users')->insertGetId(
['email' => 'hong@example.com', 'votes' => 0]
);WARNING
PostgreSQL에서 insertGetId 메서드는 자동 증가 컬럼의 이름이 id라고 가정합니다. 다른 시퀀스(sequence)에서 ID를 가져오려면 컬럼명을 두 번째 인수로 전달하세요.
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 메서드를 사용해 기존 레코드를 수정할 수 있습니다. update 메서드는 insert와 마찬가지로 수정할 컬럼명과 값의 쌍으로 이루어진 배열을 받으며, 영향을 받은 행의 수를 반환합니다. where 절로 수정 대상을 제한할 수 있습니다.
$affected = DB::table('users')
->where('id', 1)
->update(['votes' => 1]);수정 또는 삽입 (updateOrInsert)
조건에 맞는 레코드가 있으면 수정하고, 없으면 새로 삽입하고 싶을 때는 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 컬럼을 수정할 때는 -> 문법을 사용해 JSON 객체 내의 특정 키를 지정합니다. 이 기능은 MariaDB 10.3+, 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' => '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)을 적용할 수 있는 메서드를 제공합니다.
공유 잠금(Shared Lock) 을 적용하려면 sharedLock 메서드를 사용합니다. 공유 잠금이 걸린 행은 트랜잭션이 커밋될 때까지 다른 곳에서 수정할 수 없습니다:
DB::table('users')
->where('votes', '>', 100)
->sharedLock()
->get();배타적 잠금(Exclusive Lock) 을 적용하려면 lockForUpdate 메서드를 사용합니다. 이 잠금은 선택된 레코드가 수정되거나 다른 공유 잠금으로 조회되는 것을 모두 방지합니다:
DB::table('users')
->where('votes', '>', 100)
->lockForUpdate()
->get();비관적 잠금은 반드시 트랜잭션 안에서 사용하는 것을 권장합니다. 트랜잭션으로 감싸면, 전체 작업이 완료될 때까지 조회한 데이터가 변경되지 않음을 보장할 수 있으며, 중간에 오류가 발생하면 변경 사항이 자동으로 롤백되고 잠금도 해제됩니다.
아래는 한 사용자에서 다른 사용자에게 잔액을 이체하는 예시입니다. 두 레코드 모두 lockForUpdate로 잠근 뒤 처리하므로, 다른 트랜잭션이 중간에 끼어드는 것을 방지할 수 있습니다:
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
]);
});NOTE
공유 잠금(sharedLock)은 다른 트랜잭션이 해당 행을 읽는 것은 허용하지만 수정은 막습니다. 반면 배타적 잠금(lockForUpdate)은 읽기와 수정 모두 차단합니다. 동시성이 중요한 결제, 재고 처리 등의 로직에서는 lockForUpdate를 우선 고려하세요.
디버깅
쿼리를 작성하는 도중 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
dump와 dumpRawSql은 쿼리 확인 후에도 요청이 계속 진행되므로, 실제 실행 전에 SQL을 점검할 때 유용합니다. 반면 dd와 ddRawSql은 출력 즉시 실행을 멈추므로, 특정 시점의 쿼리 상태를 정확히 포착하고 싶을 때 사용하세요.