데이터베이스: 쿼리 빌더
번역일: 2026년 6월 21일
데이터베이스: 쿼리 빌더
- 소개
- 쿼리 실행
- Select 절
- Raw 표현식
- Join
- Union
- 기본 Where 절
- 고급 Where 절
- 정렬, 그룹화, Limit, Offset
- 조건부 절
- Insert 문
- Update 문
- Delete 문
- 비관적 잠금
- 디버깅
소개
Laravel의 데이터베이스 쿼리 빌더는 데이터베이스 쿼리를 편리하고 유연하게 작성·실행할 수 있는 인터페이스를 제공합니다. Laravel이 지원하는 모든 데이터베이스 시스템에서 동작하며, 애플리케이션에서 필요한 대부분의 데이터베이스 작업을 처리할 수 있습니다.
쿼리 빌더는 SQL 인젝션 공격을 방지하기 위해 PDO 파라미터 바인딩을 사용합니다. 쿼리 빌더에 전달하는 값들은 별도로 이스케이프하거나 정제할 필요가 없습니다.
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', '홍길동')->first();
return $user->email;행 전체가 아닌 특정 컬럼의 값만 필요하다면 value 메서드를 사용하세요. 해당 컬럼의 값을 직접 반환합니다.
$email = DB::table('users')->where('name', '홍길동')->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;
});청크 처리 도중 레코드를 업데이트할 경우, 예상치 못한 방식으로 결과가 변경될 수 있습니다. 이럴 때는 chunk 대신 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
청크 콜백 내에서 기본 키나 외래 키를 변경하면 청크 쿼리에 영향을 줄 수 있으며, 일부 레코드가 결과에서 누락될 수 있습니다.
지연 스트리밍
lazy 메서드는 내부적으로 청크 처리와 유사하게 동작하지만, 클로저에 결과를 전달하는 대신 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();이미 쿼리 빌더 인스턴스가 있을 때 컬럼을 추가하려면 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는 "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을 수행합니다. 첫 번째 인수는 조인할 테이블명이며, 나머지 인수는 조인 조건 컬럼을 지정합니다. 한 번의 쿼리에서 여러 테이블을 조인할 수도 있습니다.
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을 수행합니다.
$sizes = DB::table('sizes')
->crossJoin('colors')
->get();고급 Join 절
더 복잡한 Join 조건이 필요하다면 join 메서드의 두 번째 인수로 클로저를 전달하세요. 클로저는 Illuminate\Database\Query\JoinClause 인스턴스를 받으며, 이를 통해 조인 조건을 상세하게 지정할 수 있습니다.
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 메서드를 사용하면 서브쿼리와 조인할 수 있습니다. 각 메서드는 서브쿼리, 테이블 별칭, 관련 컬럼을 정의하는 클로저 세 가지를 인수로 받습니다. 아래 예시는 각 사용자의 가장 최근 게시글의 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개의 행을 결과에 만들 수 있습니다.
$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을 사용하세요. 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" 컬럼명을 포함하여 쿼리에서 참조하는 컬럼명을 사용자 입력으로 결정하게 해서는 안 됩니다.
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 절
여러 컬럼에 동일한 조건을 적용해야 할 때가 있습니다. 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%'
)JSON Where 절
Laravel은 JSON 컬럼 타입을 지원하는 데이터베이스에서 JSON 쿼리도 지원합니다. 현재 MySQL 5.7+, PostgreSQL, SQL Server 2016, SQLite 3.39.0(JSON1 확장 필요)이 지원됩니다. JSON 컬럼을 쿼리하려면 -> 연산자를 사용하세요.
$users = DB::table('users')
->where('preferences->dining->meal', 'salad')
->get();whereJsonContains로 JSON 배열을 쿼리할