본문으로 건너뛰기
버전: 10.x

데이터베이스: 쿼리 빌더 (Database: Query Builder)

소개

라라벨의 데이터베이스 쿼리 빌더는 데이터베이스 쿼리를 쉽고 유연하게 생성하고 실행할 수 있는 편리한 인터페이스를 제공합니다. 이를 통해 애플리케이션 내에서 대부분의 데이터베이스 작업을 수행할 수 있으며, 라라벨에서 지원하는 모든 데이터베이스 시스템과 완벽하게 호환됩니다.

라라벨 쿼리 빌더는 PDO 파라미터 바인딩 방식을 사용하여, SQL 인젝션 공격으로부터 애플리케이션을 안전하게 보호합니다. 쿼리 빌더에 전달되는 문자열은 별도로 정리(clean)하거나 정제(sanitize)할 필요가 없습니다.

[!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] 라라벨의 컬렉션은 데이터 매핑이나 집계 작업에 매우 강력한 다양한 메서드를 제공합니다. 자세한 내용은 컬렉션 문서를 참고해 주세요.

테이블에서 단일 행 또는 컬럼 값 가져오기

데이터베이스 테이블에서 한 행만 조회하고 싶다면, DB 파사드의 first 메서드를 사용할 수 있습니다. 이 메서드는 단일 stdClass 객체를 반환합니다.

$user = DB::table('users')->where('name', 'John')->first();

return $user->email;

전체 행이 아니라 컬럼의 값 하나만 필요하다면, 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;
}

결과를 청크 단위로 가져오기

수천 건이 넘는 대용량 레코드를 다루어야 한다면, DB 파사드가 제공하는 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]);
}
});

[!NOTE] 청크 콜백 내부에서 레코드를 업데이트하거나 삭제할 때, 기본키나 외래키 값이 변경되면 청크 쿼리에 영향을 줄 수 있습니다. 그 결과, 일부 레코드가 결과에서 누락될 수 있습니다.

결과를 지연(래지) 방식으로 스트리밍하기

lazy 메서드는 위에서 설명한 chunk 메서드와 비슷하게 쿼리를 청크 단위로 실행합니다. 하지만, 각 청크를 콜백에 넘기는 대신 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]);
});

[!NOTE] 지연 컬렉션을 순회하면서 레코드를 업데이트하거나 삭제하면, 기본키나 외래키 값의 변경으로 인해 청크 쿼리가 달라질 수 있습니다. 그에 따라, 일부 레코드가 결과에서 누락될 수 있습니다.

집계 함수 사용

쿼리 빌더는 count, max, min, avg, sum 등 다양한 집계(aggregate) 메서드도 제공합니다. 쿼리를 만든 후 이러한 메서드를 호출하면 해당 집계 결과를 바로 조회할 수 있습니다.

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 메서드를 사용해 쿼리에 원하는 컬럼만 선택하는 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 표현식

경우에 따라 쿼리에 임의의 문자열을 삽입해야 할 때가 있습니다. 이럴 때는 DB 파사드가 제공하는 raw 메서드를 사용해 raw 문자열 표현식을 만들 수 있습니다.

$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();

[!NOTE] Raw 구문은 문자열로 쿼리에 직접 삽입되므로, 반드시 SQL 인젝션 취약점이 발생하지 않도록 세심한 주의를 기울여야 합니다.

Raw 메서드

DB::raw 메서드를 사용하는 대신, 쿼리의 다양한 부분에 raw 표현식을 삽입할 수 있는 다음과 같은 메서드들을 활용할 수도 있습니다. 참고: 라라벨은 raw 표현식을 사용하는 쿼리에 대해서는 SQL 인젝션 방어를 보장하지 않습니다.

selectRaw

selectRaw 메서드는 addSelect(DB::raw(/* ... */)) 대신 사용할 수 있습니다. 이 메서드는 두 번째 인자로 바인딩 배열도 선택적으로 받을 수 있습니다.

$orders = DB::table('orders')
->selectRaw('price * ? as price_with_tax', [1.0825])
->get();

whereRaw / orWhereRaw

whereRaworWhereRaw는 쿼리에 raw "where" 절을 삽입하는 데 사용할 수 있습니다. 이 메서드들 역시 두 번째 인자로 바인딩 배열을 전달할 수 있습니다.

$orders = DB::table('orders')
->whereRaw('price > IF(state = "TX", ?, 100)', [200])
->get();

havingRaw / orHavingRaw

havingRaworHavingRaw 메서드는 "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 절

쿼리 빌더에서는 쿼리에 조인 절을 추가할 수도 있습니다. 가장 기본적인 "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 메서드를 사용할 수 있습니다. Cross join은 두 테이블 간의 데카르트 곱(모든 조합)을 생성합니다.

$sizes = DB::table('sizes')
->crossJoin('colors')
->get();

고급 Join 절

더 복잡한 조인 조건을 지정해야 할 경우, 두 번째 인자로 클로저(Closure)를 join 메서드에 전달할 수 있습니다. 이 클로저에는 Illuminate\Database\Query\JoinClause 인스턴스가 전달되며, 이 객체를 통해 조인 조건을 더욱 세밀하게 정의할 수 있습니다.

DB::table('users')
->join('contacts', function (JoinClause $join) {
$join->on('users.id', '=', 'contacts.user_id')->orOn(/* ... */);
})
->get();

조인에 "where" 절을 추가하고 싶은 경우, JoinClause 인스턴스가 제공하는 whereorWhere 메서드를 사용할 수 있습니다. 이때는 두 컬럼을 비교하는 대신, 컬럼과 값을 비교합니다.

DB::table('users')
->join('contacts', function (JoinClause $join) {
$join->on('users.id', '=', 'contacts.user_id')
->where('contacts.user_id', '>', 5);
})
->get();

서브쿼리 조인(Subquery 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

[!NOTE] Lateral join은 현재 PostgreSQL, MySQL >= 8.0.14, SQL Server에서만 지원됩니다.

joinLateralleftJoinLateral 메서드를 사용하면, 서브쿼리와 함께 "lateral join"을 수행할 수 있습니다. 각 메서드는 두 개의 인자를 받으며, 하나는 서브쿼리, 다른 하나는 서브쿼리의 테이블 별칭입니다. 조인 조건은 서브쿼리 내부의 where 절에서 지정해야 합니다. Lateral join은 각 행마다 평가되며, 서브쿼리 외부의 컬럼도 참조할 수 있습니다.

예를 들어 사용자의 최근 세 개 블로그 게시물을 함께 조회하고 싶다면, 아래와 같이 구현할 수 있습니다. 각 사용자는 가장 최근 게시글 최대 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();

유니온(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로 결합된 쿼리는 중복 결과가 제거되지 않습니다. unionAll 메서드의 사용법은 union과 동일합니다.

기본 Where 절

Where 절

쿼리 빌더의 where 메서드를 사용해 쿼리에 "where" 조건을 추가할 수 있습니다. where의 가장 기본적인 사용법은 세 인자를 필요로 합니다. 첫 번째 인자는 컬럼명, 두 번째는 데이터베이스에서 지원하는 비교 연산자, 세 번째 인자는 컬럼과 비교할 값입니다.

예를 들어, 다음 쿼리는 votes 컬럼 값이 100과 같고, age 컬럼 값은 35보다 큰 사용자만 조회합니다.

$users = DB::table('users')
->where('votes', '=', 100)
->where('age', '>', 35)
->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', 'T%')
->get();

또한 조건 배열을 where 함수에 전달할 수도 있습니다. 이 배열의 각 요소는, 보통 where 메서드의 세 인자로 전달하는 구조(컬럼명, 연산자, 값)를 각각 담아야 합니다.

$users = DB::table('users')->where([
['status', '=', '1'],
['subscribed', '<>', '1'],
])->get();

[!NOTE] PDO는 컬럼명 바인딩을 지원하지 않습니다. 쿼리에서 참조하는 컬럼명, 특히 "order by" 컬럼에 사용자가 입력한 값이 직접 사용되면 안 됩니다.

Or Where 절

여러 번 where 메서드를 체이닝할 때, 기본적으로 각 조건은 and 연산자로 결합됩니다. 하지만 orWhere 메서드를 사용하면 조건을 or 연산자로 결합할 수 있습니다. orWhere의 인자 역시 where 메서드와 동일하게 전달하면 됩니다.

$users = DB::table('users')
->where('votes', '>', 100)
->orWhere('name', 'John')
->get();

만약 괄호로 묶인(or 그룹핑된) 조건을 만들고 싶다면, orWhere의 첫 번째 인자로 클로저를 전달할 수 있습니다.

$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)

[!NOTE] 전역 스코프가 적용될 때 예기치 않은 결과를 방지하려면 orWhere 호출은 항상 괄호로 그룹핑하는 것이 좋습니다.

whereNot 절

whereNotorWhereNot 메서드를 사용하면 지정한 쿼리 제약 조건 그룹을 반전시킬 수 있습니다. 예를 들어, 아래 쿼리는 할인 상품이거나 가격이 10 미만인 제품을 제외합니다.

$products = DB::table('products')
->whereNot(function (Builder $query) {
$query->where('clearance', true)
->orWhere('price', '<', 10);
})
->get();

whereAny / whereAll 절

특정한 쿼리 조건을 여러 컬럼에 적용해야 할 때가 있습니다. 예를 들어, 주어진 컬럼 목록 중 하나라도 특정 값과 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%'
)

JSON where 절

라라벨은 JSON 컬럼 타입을 지원하는 데이터베이스(MySQL 5.7+, PostgreSQL, SQL Server 2016, SQLite 3.39.0(JSON1 확장 필요))에서 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을 사용한다면, whereJsonContains 메서드에 값의 배열을 전달할 수도 있습니다.

$users = DB::table('users')
->whereJsonContains('options->languages', ['en', 'de'])
->get();

JSON 배열의 길이로 조회하려면 whereJsonLength 메서드를 사용할 수 있습니다.

$users = DB::table('users')
->whereJsonLength('options->languages', 0)
->get();

$users = DB::table('users')
->whereJsonLength('options->languages', '>', 1)
->get();

추가 where 절

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();

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
)

[!NOTE] 쿼리에 매우 많은 정수 배열을 바인딩할 경우, whereIntegerInRaw 또는 whereIntegerNotInRaw 메서드를 사용해 메모리 사용량을 크게 줄일 수 있습니다.

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

whereDate 메서드는 컬럼의 값을 특정 날짜와 비교할 때 사용할 수 있습니다.

$users = DB::table('users')
->whereDate('created_at', '2016-12-31')
->get();

whereMonth 메서드는 컬럼의 값을 특정 월과 비교할 때 사용할 수 있습니다.

$users = DB::table('users')
->whereMonth('created_at', '12')
->get();

whereDay 메서드는 컬럼의 값을 특정 일과 비교할 때 사용할 수 있습니다.

$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();

whereColumn / orWhereColumn

whereColumn 메서드는 두 컬럼의 값이 같은지 확인할 수 있습니다.

$users = DB::table('users')
->whereColumn('first_name', 'last_name')
->get();

비교 연산자를 함께 전달할 수도 있습니다.

$users = DB::table('users')
->whereColumn('updated_at', '>', 'created_at')
->get();

컬럼 비교의 배열을 whereColumn에 전달할 수도 있습니다. 이 조건들은 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')

[!NOTE] 전역 스코프가 적용될 때 예기치 않은 동작을 방지하려면 항상 orWhere 호출을 그룹핑해야 합니다.

고급 where 절

whereExists 절

whereExists 메서드를 사용하면 "where exists" SQL 절을 작성할 수 있습니다. 이 메서드는 클로저를 받아, 클로저 안에서 "exists" 절에 들어갈 쿼리를 정의할 수 있습니다.

$users = DB::table('users')
->whereExists(function (Builder $query) {
$query->select(DB::raw(1))
->from('orders')
->whereColumn('orders.user_id', 'users.id');
})
->get();

또는, 클로저 대신 쿼리 객체를 whereExists 메서드에 직접 전달할 수도 있습니다.

$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 메서드에 전달하면 됩니다. 예를 들어, 아래 쿼리는 최근 "membership"이 특정 타입인 사용자를 모두 조회합니다.

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" 절이 필요할 수도 있습니다. 이럴 때는 컬럼, 연산자, 클로저를 차례로 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();

전체 텍스트 where 절

[!NOTE] 전체 텍스트 where 절은 현재 MySQL과 PostgreSQL에서 지원됩니다.

whereFullTextorWhereFullText 메서드를 사용하면 전체 텍스트 인덱스가 설정된 컬럼에 대해 전체 텍스트 기반의 "where" 절을 쿼리에 추가할 수 있습니다. 이 메서드는 라라벨이 사용 중인 데이터베이스 시스템에 맞게 적절한 SQL로 변환해줍니다. 예를 들어, 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 메서드

latestoldest 메서드를 사용하면 날짜 기준으로 손쉽게 결과를 정렬할 수 있습니다. 기본적으로 테이블의 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 호출 시 컬럼명과 방향을 전달하면 기존 모든 "order by" 절이 제거되고 새롭게 정렬 조건이 적용됩니다.

$query = DB::table('users')->orderBy('name');

$usersOrderedByEmail = $query->reorder('email', 'desc')->get();

그룹화

groupBy 및 having 메서드

예상하신 대로, groupByhaving 메서드를 사용해 쿼리 결과를 그룹화할 수 있습니다. 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

skip 및 take 메서드

skiptake 메서드를 사용하면 쿼리에서 반환되는 결과 개수를 제한하거나, 일정 개수만큼 결과를 건너뛸 수 있습니다.

$users = DB::table('users')->skip(10)->take(5)->get();

또는, limitoffset 메서드를 사용할 수도 있습니다. 두 메서드는 각각 takeskip 메서드와 기능적으로 동일합니다.

$users = DB::table('users')
->offset(10)
->limit(5)
->get();

조건부 절

때로는 쿼리의 일부 절을 다른 조건에 따라 적용하고 싶을 때가 있습니다. 예를 들어, 들어오는 HTTP 요청에 입력값이 있을 때만 where 조건을 추가하고 싶을 수 있습니다. 이럴 때는 when 메서드를 사용할 수 있습니다.

$role = $request->string('role');

$users = DB::table('users')
->when($role, function (Builder $query, string $role) {
$query->where('role_id', $role);
})
->get();

when 메서드는 첫 번째 인자가 true일 때만 주어진 클로저를 실행합니다. 첫 번째 인자가 false일 경우, 클로저는 실행되지 않습니다. 위의 예시에서 when 메서드에 전달된 클로저는, 요청에서 role 필드가 존재하고 true로 평가될 때만 실행됩니다.

when 메서드의 세 번째 인자로 또 다른 클로저를 전달할 수 있습니다. 이 클로저는 첫 번째 인자가 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 메서드도 제공합니다. insert 메서드는 컬럼명과 값의 배열을 인수로 받습니다.

DB::table('users')->insert([
'email' => '[email protected]',
'votes' => 0
]);

여러 레코드를 한 번에 삽입하고 싶을 때는 배열 안에 여러 배열(각각이 하나의 레코드에 해당)을 전달하세요.

DB::table('users')->insert([
['email' => '[email protected]', 'votes' => 0],
['email' => '[email protected]', 'votes' => 0],
]);

insertOrIgnore 메서드는 레코드를 삽입할 때 발생하는 오류를 무시합니다. 이 메서드를 사용할 때는, 중복 레코드로 인한 오류 외에도 데이터베이스 엔진에 따라 다른 종류의 오류도 무시될 수 있음을 유념해야 합니다. 예를 들어, insertOrIgnoreMySQL의 strict 모드를 우회합니다.

DB::table('users')->insertOrIgnore([
['id' => 1, 'email' => '[email protected]'],
['id' => 2, 'email' => '[email protected]'],
]);

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' => '[email protected]', 'votes' => 0]
);

[!WARNING] PostgreSQL을 사용하는 경우 insertGetId 메서드는 자동 증가 컬럼의 이름이 반드시 id여야 합니다. 만약 다른 "시퀀스"에서 ID를 가져오고 싶다면, insertGetId 메서드의 두 번째 인수로 컬럼명을 전달할 수 있습니다.

업서트(Upserts)

upsert 메서드는 존재하지 않는 레코드는 삽입하고, 이미 존재하는 레코드는 여러분이 지정한 새로운 값으로 업데이트합니다. 이 메서드의 첫 번째 인수는 삽입하거나 업데이트할 값들의 배열입니다. 두 번째 인수는 테이블 내에서 레코드를 고유하게 식별할 컬럼들을 지정합니다. 마지막 세 번째 인수는 일치하는 레코드가 이미 데이터베이스에 있을 경우 업데이트할 컬럼 배열입니다.

DB::table('flights')->upsert(
[
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
],
['departure', 'destination'],
['price']
);

위 예시에서, 라라벨은 두 개의 레코드를 삽입하려 시도합니다. 만약 departuredestination 컬럼 값이 동일한 레코드가 이미 존재한다면, 해당 레코드의 price 컬럼이 업데이트됩니다.

[!WARNING] SQL Server를 제외한 모든 데이터베이스는 upsert 메서드의 두 번째 인수로 지정된 컬럼에 "primary" 또는 "unique" 인덱스가 설정되어 있어야 합니다. 또한, MySQL 데이터베이스 드라이버는 upsert 메서드의 두 번째 인수를 무시하며, 항상 테이블의 "primary"와 "unique" 인덱스를 기반으로 기존 레코드 존재 여부를 확인합니다.

업데이트(UPDATE) 문

쿼리 빌더는 레코드를 삽입하는 것 외에도, update 메서드를 사용하여 기존 레코드를 업데이트할 수 있습니다. update 메서드는 insert 메서드와 마찬가지로, 업데이트할 컬럼과 값을 키-값 쌍의 배열 형태로 받습니다. update 메서드는 영향을 받은 행(row)의 개수를 반환합니다. where 절을 사용하여 update 쿼리에 조건을 추가할 수 있습니다.

$affected = DB::table('users')
->where('id', 1)
->update(['votes' => 1]);

업데이트 또는 삽입

때때로 데이터베이스에 기존 레코드가 있으면 업데이트하고, 없으면 새로 생성하고 싶을 때가 있습니다. 이럴 때는 updateOrInsert 메서드를 사용할 수 있습니다. updateOrInsert 메서드는 두 개의 인수를 받습니다. 첫 번째는 레코드를 찾기 위한 조건의 배열, 두 번째는 업데이트할 컬럼과 값 쌍의 배열입니다.

updateOrInsert 메서드는 첫 번째 인수로 전달한 컬럼과 값 조합을 기준으로 데이터베이스에서 일치하는 레코드를 찾으려 시도합니다. 레코드가 존재한다면 두 번째 인수의 값으로 업데이트하고, 찾을 수 없다면 두 인수의 값을 합친 속성으로 새 레코드를 삽입합니다.

DB::table('users')
->updateOrInsert(
['email' => '[email protected]', 'name' => 'John'],
['votes' => '2']
);

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' => 'John']);

또한, incrementEachdecrementEach 메서드를 사용하여 한 번에 여러 컬럼을 증가/감소시킬 수도 있습니다.

DB::table('users')->incrementEach([
'votes' => 5,
'balance' => 100,
]);

삭제(DELETE) 문

쿼리 빌더의 delete 메서드로 테이블에서 레코드를 삭제할 수 있습니다. delete 메서드는 영향을 받은 행(row)의 수를 반환합니다. delete 메서드 호출 전에 "where" 절을 추가해 삭제 대상을 제한할 수 있습니다.

$deleted = DB::table('users')->delete();

$deleted = DB::table('users')->where('votes', '>', 100)->delete();

테이블의 모든 레코드를 삭제하고, 자동 증가 ID도 0으로 초기화하려면 truncate 메서드를 사용할 수 있습니다.

DB::table('users')->truncate();

테이블 잘라내기(Truncation)와 PostgreSQL

PostgreSQL 데이터베이스를 잘라내기(truncate)할 때는 CASCADE 동작이 적용됩니다. 즉, 외래 키로 연결된 다른 테이블의 관련 레코드까지 모두 삭제됩니다.

비관적 잠금(Pessimistic Locking)

쿼리 빌더는 select 문을 실행할 때 "비관적 잠금"을 구현하는 데 도움이 되는 여러 함수도 제공합니다. "공유 잠금(shared lock)"이 필요할 때는 sharedLock 메서드를 호출할 수 있습니다. 공유 잠금은 선택된 행이 트랜잭션이 커밋될 때까지 변경되지 않도록 방지합니다.

DB::table('users')
->where('votes', '>', 100)
->sharedLock()
->get();

또는, lockForUpdate 메서드를 사용할 수도 있습니다. "for update" 잠금은 선택된 레코드가 변경되거나, 다른 공유 잠금과 함께 다시 선택되는 것을 방지합니다.

DB::table('users')
->where('votes', '>', 100)
->lockForUpdate()
->get();

디버깅(Debugging)

쿼리를 작성하는 도중 dddump 메서드를 사용해 현재 쿼리의 바인딩 정보와 SQL을 확인할 수 있습니다. dd 메서드는 디버그 정보를 출력하고 요청 실행을 중단합니다. 반면, dump 메서드는 디버그 정보만 출력하고 요청을 계속 이어갑니다.

DB::table('users')->where('votes', '>', 100)->dd();

DB::table('users')->where('votes', '>', 100)->dump();

또한, 쿼리에서 바인딩된 파라미터가 실제 값으로 치환된 SQL을 출력하고 싶다면, dumpRawSqlddRawSql 메서드를 사용할 수 있습니다.

DB::table('users')->where('votes', '>', 100)->dumpRawSql();

DB::table('users')->where('votes', '>', 100)->ddRawSql();