SQL Select

Drizzle는 타입 안전성과 합성 가능성을 유지하면서도 데이터베이스에서 데이터를 가져오는 가장 SQL스러운 방법을 제공합니다. Drizzle는 대부분의 모든 SQL 방언의 쿼리 기능과 능력을 기본적으로 지원하며, 아직 지원하지 않는 기능은 강력한 sql 연산자를 통해 사용자가 직접 추가할 수 있습니다.

다음 예제를 위해 users 테이블이 다음과 같이 정의되어 있다고 가정해 보겠습니다:

PostgreSQL
MySQL
SQLite
SingleStore
import { pgTable, serial, text } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  age: integer('age'),
});

기본적인 SELECT 쿼리

테이블의 모든 행과 모든 컬럼을 선택하는 방법:

const result = await db.select().from(users);
/*
  {
    id: number;
    name: string;
    age: number | null;
  }[]
*/
select "id", "name", "age" from "users";

결과 타입은 테이블 정의를 기반으로 자동으로 추론됩니다. 이때 컬럼의 null 허용 여부도 포함됩니다.

Drizzle는 항상 select * 대신 select 절에서 명시적으로 컬럼을 나열합니다.
이는 쿼리 결과에서 필드 순서를 보장하기 위해 내부적으로 필요하며, 일반적으로 좋은 관행으로 간주됩니다.

부분 선택

특정 상황에서는 테이블에서 일부 컬럼만 선택하고 싶을 수 있습니다. 이 경우 .select() 메서드에 선택 객체를 제공하여 원하는 컬럼만 선택할 수 있습니다.

const result = await db.select({
  field1: users.id,
  field2: users.name,
}).from(users);

const { field1, field2 } = result[0];

위 코드는 SQL로 다음과 같이 변환됩니다.

select "id", "name" from "users";

SQL과 마찬가지로, 테이블 컬럼뿐만 아니라 임의의 표현식을 선택 필드로 사용할 수도 있습니다.

const result = await db.select({
  id: users.id,
  lowerName: sql`lower(${users.name})`,
}).from(users);

이 코드는 SQL로 다음과 같이 변환됩니다.

select "id", lower("name") from "users";
IMPORTANT

sql<string>을 지정하면 Drizzle에게 해당 필드의 예상 타입string임을 알리는 것입니다.
만약 잘못된 타입을 지정하면(예: 문자열로 반환될 필드에 sql<number>를 사용), 런타임 값이 예상 타입과 일치하지 않을 수 있습니다. Drizzle은 제공된 타입 제네릭을 기반으로 타입 캐스팅을 수행할 수 없습니다. 이 정보는 런타임에 사용할 수 없기 때문입니다.

반환된 값에 런타임 변환을 적용해야 한다면 .mapWith() 메서드를 사용할 수 있습니다.

조건부 선택

특정 조건에 따라 동적으로 선택 객체를 만들 수 있습니다:

async function selectUsers(withName: boolean) {
  return db
    .select({
      id: users.id,
      ...(withName ? { name: users.name } : {}),
    })
    .from(users);
}

const users = await selectUsers(true);

이 코드는 withName 매개변수가 true일 때만 name 필드를 포함하는 객체를 선택합니다. 조건에 따라 동적으로 필드를 추가하거나 제외할 수 있습니다.

고유한 행 선택하기

데이터셋에서 고유한 행만 가져오려면 .select() 대신 .selectDistinct()를 사용할 수 있습니다.

await db.selectDistinct().from(users).orderBy(usersTable.id, usersTable.name);

await db.selectDistinct({ id: users.id }).from(users).orderBy(usersTable.id);
select distinct "id", "name" from "users" order by "id", "name";

select distinct "id" from "users" order by "id";

PostgreSQL에서는 distinct on 절을 사용하여 고유한 행을 결정하는 방법을 지정할 수도 있습니다.

IMPORTANT

distinct on 절은 PostgreSQL에서만 지원됩니다.

await db.selectDistinctOn([users.id]).from(users).orderBy(users.id);
await db.selectDistinctOn([users.name], { name: users.name }).from(users).orderBy(users.name);
select distinct on ("id") "id", "name" from "users" order by "id";
select distinct on ("name") "name" from "users" order by "name";

고급 선택 기능

Drizzle API는 TypeScript로 구동되며, 다양한 유연한 방법으로 쿼리를 작성할 수 있습니다.

고급 부분 선택 기능을 미리 살펴보고 싶다면, 더 자세한 사용 예제는 전용 가이드를 참고하세요.

예제 1
예제 2
예제 3
예제 4
import { getTableColumns, sql } from 'drizzle-orm';

await db.select({
    ...getTableColumns(posts),
    titleLength: sql`length(${posts.title})`,
  }).from(posts);

---

필터

.where() 메서드에서 필터 연산자를 사용해 쿼리 결과를 필터링할 수 있습니다:

import { eq, lt, gte, ne } from 'drizzle-orm';

await db.select().from(users).where(eq(users.id, 42));
await db.select().from(users).where(lt(users.id, 42));
await db.select().from(users).where(gte(users.id, 42));
await db.select().from(users).where(ne(users.id, 42));
...
select "id", "name", "age" from "users" where "id" = 42;
select "id", "name", "age" from "users" where "id" < 42;
select "id", "name", "age" from "users" where "id" >= 42;
select "id", "name", "age" from "users" where "id" <> 42;

모든 필터 연산자는 sql 함수를 사용해 구현됩니다.
이 함수를 직접 사용해 임의의 SQL 필터를 작성하거나, 자신만의 연산자를 만들 수도 있습니다.
Drizzle에서 제공하는 연산자들이 어떻게 구현되었는지 참고할 수 있습니다.

import { sql } from 'drizzle-orm';

function equals42(col: Column) {
  return sql`${col} = 42`;
}

await db.select().from(users).where(sql`${users.id} < 42`);
await db.select().from(users).where(sql`${users.id} = 42`);
await db.select().from(users).where(equals42(users.id));
await db.select().from(users).where(sql`${users.id} >= 42`);
await db.select().from(users).where(sql`${users.id} <> 42`);
await db.select().from(users).where(sql`lower(${users.name}) = 'aaron'`);
select "id", "name", "age" from "users" where 'id' < 42;
select "id", "name", "age" from "users" where 'id' = 42;
select "id", "name", "age" from "users" where 'id' = 42;
select "id", "name", "age" from "users" where 'id' >= 42;
select "id", "name", "age" from "users" where 'id' <> 42;
select "id", "name", "age" from "users" where lower("name") = 'aaron';

필터 연산자와 sql 함수에 제공된 모든 값은 자동으로 파라미터화됩니다.
예를 들어, 다음 쿼리:

await db.select().from(users).where(eq(users.id, 42));

는 다음과 같이 변환됩니다:

select "id", "name", "age" from "users" where "id" = $1; -- params: [42]

not 연산자로 조건을 반전시키기:

import { eq, not, sql } from 'drizzle-orm';

await db.select().from(users).where(not(eq(users.id, 42)));
await db.select().from(users).where(sql`not ${users.id} = 42`);
select "id", "name", "age" from "users" where not ("id" = 42);
select "id", "name", "age" from "users" where not ("id" = 42);

템플릿 보간 덕분에 스키마를 변경하거나 테이블 및 컬럼 이름을 바꿔도 쿼리에 자동으로 반영됩니다.
이는 raw SQL을 작성할 때 컬럼이나 테이블 이름을 하드코딩하는 것과 대조적입니다.

필터 조합하기

and()or() 연산자를 사용하여 필터를 논리적으로 조합할 수 있습니다.

import { eq, and, sql } from 'drizzle-orm';

await db.select().from(users).where(
  and(
    eq(users.id, 42),
    eq(users.name, 'Dan')
  )
);
await db.select().from(users).where(sql`${users.id} = 42 and ${users.name} = 'Dan'`);
select "id", "name", "age" from "users" where "id" = 42 and "name" = 'Dan';
select "id", "name", "age" from "users" where "id" = 42 and "name" = 'Dan';
import { eq, or, sql } from 'drizzle-orm';

await db.select().from(users).where(
  or(
    eq(users.id, 42), 
    eq(users.name, 'Dan')
  )
);
await db.select().from(users).where(sql`${users.id} = 42 or ${users.name} = 'Dan'`);
select "id", "name", "age" from "users" where "id" = 42 or "name" = 'Dan';
select "id", "name", "age" from "users" where "id" = 42 or "name" = 'Dan';

고급 필터링

TypeScript와 함께 사용하면 Drizzle API는 쿼리에서 필터를 결합할 수 있는 강력하고 유연한 방법을 제공합니다.

조건부 필터링을 간단히 살펴보겠습니다. 더 자세한 고급 사용법 예제는 전용 가이드를 참고하세요.

예제 1
예제 2
const searchPosts = async (term?: string) => {
  await db
    .select()
    .from(posts)
    .where(term ? ilike(posts.title, term) : undefined);
};
await searchPosts();
await searchPosts('AI');

---

Limit과 Offset

.limit().offset()을 사용하여 쿼리에 limitoffset 절을 추가할 수 있습니다. 예를 들어, 페이지네이션을 구현할 때 유용합니다:

await db.select().from(users).limit(10);
await db.select().from(users).limit(10).offset(10);

위 코드는 다음과 같은 SQL 쿼리로 변환됩니다:

select "id", "name", "age" from "users" limit 10;
select "id", "name", "age" from "users" limit 10 offset 10;

Order By

.orderBy()를 사용하여 쿼리에 order by 절을 추가할 수 있습니다. 이를 통해 지정된 필드로 결과를 정렬할 수 있습니다.

import { asc, desc } from 'drizzle-orm';

// 이름 기준으로 오름차순 정렬
await db.select().from(users).orderBy(users.name);

// 이름 기준으로 내림차순 정렬
await db.select().from(users).orderBy(desc(users.name));

// 여러 필드로 정렬
await db.select().from(users).orderBy(users.name, users.name2);

// 이름은 오름차순, 이름2는 내림차순으로 정렬
await db.select().from(users).orderBy(asc(users.name), desc(users.name2));

위 코드는 다음과 같은 SQL 쿼리로 변환됩니다.

-- 이름 기준으로 오름차순 정렬
select "id", "name", "age" from "users" order by "name";

-- 이름 기준으로 내림차순 정렬
select "id", "name", "age" from "users" order by "name" desc;

-- 이름과 이름2 기준으로 정렬
select "id", "name", "age" from "users" order by "name", "name2";

-- 이름은 오름차순, 이름2는 내림차순으로 정렬
select "id", "name", "age" from "users" order by "name" asc, "name2" desc;

고급 페이징

TypeScript로 구동되는 Drizzle API를 사용하면 모든 SQL 페이징 및 정렬 방식을 구현할 수 있습니다.

고급 페이징 기능을 간단히 살펴보겠습니다. 더 자세한 사용 예제는 limit offset 페이징커서 기반 페이징 가이드를 참고하세요.

예제 1
예제 2
예제 3
예제 4
await db
  .select()
  .from(users)
  .orderBy(asc(users.id)) // 정렬은 필수
  .limit(4) // 반환할 행 수
  .offset(4); // 건너뛸 행 수

---

WITH 절

insert, update, delete와 함께 WITH 문을 사용하는 방법을 확인하세요.

WITH 절을 사용하면 복잡한 쿼리를 더 작은 하위 쿼리인 공통 테이블 표현식(CTE)으로 나누어 단순화할 수 있습니다.

const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42)));

const result = await db.with(sq).select().from(sq);
with sq as (select "id", "name", "age" from "users" where "id" = 42)
select "id", "name", "age" from sq;

CTE에서 임의의 SQL 값을 필드로 선택하고 다른 CTE나 메인 쿼리에서 참조하려면, 해당 값에 별칭(alias)을 추가해야 합니다.

const sq = db.$with('sq').as(db.select({ 
  name: sql`upper(${users.name})`.as('name'),
})
.from(users));

const result = await db.with(sq).select({ name: sq.name }).from(sq);

별칭을 제공하지 않으면 필드 타입이 DrizzleTypeError가 되며, 다른 쿼리에서 참조할 수 없습니다. 타입 오류를 무시하고 필드를 사용하려고 하면, 별칭 없이 해당 필드를 참조할 방법이 없기 때문에 런타임 오류가 발생합니다.

서브쿼리로 선택하기

SQL과 마찬가지로, 서브쿼리 API를 사용해 다른 쿼리 안에 쿼리를 포함시킬 수 있습니다:

const sq = db.select().from(users).where(eq(users.id, 42)).as('sq');
const result = await db.select().from(sq);
select "id", "name", "age" from (select "id", "name", "age" from "users" where "id" = 42) "sq";

서브쿼리는 테이블을 사용할 수 있는 모든 곳에서 사용할 수 있습니다. 예를 들어 조인(join)에서도 활용 가능합니다:

const sq = db.select().from(users).where(eq(users.id, 42)).as('sq');
const result = await db.select().from(users).leftJoin(sq, eq(users.id, sq.id));
select "users"."id", "users"."name", "users"."age", "sq"."id", "sq"."name", "sq"."age" from "users"
  left join (select "id", "name", "age" from "users" where "id" = 42) "sq"
    on "users"."id" = "sq"."id";

---

집계(Aggregations)

Drizzle를 사용하면 sum, count, avg와 같은 함수를 활용해 집계를 수행할 수 있습니다. 이때 .groupBy().having()을 사용해 그룹화와 필터링을 할 수 있으며, 이는 원시 SQL에서와 동일한 방식으로 작동합니다.

import { gt } from 'drizzle-orm';

// 나이별 사용자 수 조회
await db.select({
  age: users.age,
  count: sql`cast(count(${users.id}) as int)`,
})
  .from(users)
  .groupBy(users.age);

// 나이별 사용자 수가 1명 초과인 경우 조회
await db.select({
  age: users.age,
  count: sql`cast(count(${users.id}) as int)`,
})
  .from(users)
  .groupBy(users.age)
  .having(({ count }) => gt(count, 1));
-- 나이별 사용자 수 조회
select "age", cast(count("id") as int)
  from "users"
  group by "age";

-- 나이별 사용자 수가 1명 초과인 경우 조회
select "age", cast(count("id") as int)
  from "users"
  group by "age"
  having cast(count("id") as int) > 1;

cast(... as int)는 PostgreSQL에서 count()bigint를 반환하고, MySQL에서는 decimal을 반환하기 때문에 필요합니다. 이 값들은 숫자 대신 문자열로 처리됩니다.
또는 런타임에 값을 숫자로 변환하기 위해 .mapWith(Number)를 사용할 수도 있습니다.

만약 count 집계가 필요하다면, $count API를 사용하는 것을 권장합니다.

집계 헬퍼 함수

Drizzle는 일반적인 경우에 sql 템플릿을 작성하지 않아도 되도록 래핑된 sql 함수 세트를 제공합니다.

집계 함수는 주로 SELECT 문의 GROUP BY 절과 함께 사용됩니다. 따라서 하나의 쿼리에서 집계 함수와 다른 컬럼을 함께 선택할 때는 반드시 .groupBy 절을 사용해야 합니다.

count

expression에 있는 값의 개수를 반환합니다.

import { count } from 'drizzle-orm'

await db.select({ value: count() }).from(users);
await db.select({ value: count(users.id) }).from(users);
select count("*") from "users";
select count("id") from "users";
// 아래와 동일한 코드
await db.select({ 
  value: sql`count('*'))`.mapWith(Number) 
}).from(users);

await db.select({ 
  value: sql`count(${users.id})`.mapWith(Number) 
}).from(users);

countDistinct

expression에 있는 중복되지 않은 값의 개수를 반환합니다.

import { countDistinct } from 'drizzle-orm'

await db.select({ value: countDistinct(users.id) }).from(users);
select count(distinct "id") from "users";
// 아래와 동일한 코드
await db.select({ 
  value: sql`count(${users.id})`.mapWith(Number) 
}).from(users);

avg

expression에 있는 모든 null이 아닌 값의 평균(산술 평균)을 반환합니다.

import { avg } from 'drizzle-orm'

await db.select({ value: avg(users.id) }).from(users);
select avg("id") from "users";
// 아래와 동일한 코드
await db.select({ 
  value: sql`avg(${users.id})`.mapWith(String) 
}).from(users);

avgDistinct

expression에 있는 모든 null이 아닌 값의 평균(산술 평균)을 반환합니다.

import { avgDistinct } from 'drizzle-orm'

await db.select({ value: avgDistinct(users.id) }).from(users);
select avg(distinct "id") from "users";
// 아래와 동일한 코드
await db.select({ 
  value: sql`avg(distinct ${users.id})`.mapWith(String) 
}).from(users);

sum

expression에 있는 모든 null이 아닌 값의 합계를 반환합니다.

import { sum } from 'drizzle-orm'

await db.select({ value: sum(users.id) }).from(users);
select sum("id") from "users";
// 아래와 동일한 코드
await db.select({ 
  value: sql`sum(${users.id})`.mapWith(String) 
}).from(users);

sumDistinct

expression에 있는 모든 null이 아니고 중복되지 않은 값의 합계를 반환합니다.

import { sumDistinct } from 'drizzle-orm'

await db.select({ value: sumDistinct(users.id) }).from(users);
select sum(distinct "id") from "users";
// 아래와 동일한 코드
await db.select({ 
  value: sql`sum(distinct ${users.id})`.mapWith(String) 
}).from(users);

max

expression에 있는 값 중 최대값을 반환합니다.

import { max } from 'drizzle-orm'

await db.select({ value: max(users.id) }).from(users);
select max("id") from "users";
// 아래와 동일한 코드
await db.select({ 
  value: sql`max(${expression})`.mapWith(users.id) 
}).from(users);

min

expression에 있는 값 중 최소값을 반환합니다.

import { min } from 'drizzle-orm'

await db.select({ value: min(users.id) }).from(users);
select min("id") from "users";
// 아래와 동일한 코드
await db.select({ 
  value: sql`min(${users.id})`.mapWith(users.id) 
}).from(users);

더 복잡한 예제:

const orders = sqliteTable('order', {
  id: integer('id').primaryKey(),
  orderDate: integer('order_date', { mode: 'timestamp' }).notNull(),
  requiredDate: integer('required_date', { mode: 'timestamp' }).notNull(),
  shippedDate: integer('shipped_date', { mode: 'timestamp' }),
  shipVia: integer('ship_via').notNull(),
  freight: numeric('freight').notNull(),
  shipName: text('ship_name').notNull(),
  shipCity: text('ship_city').notNull(),
  shipRegion: text('ship_region'),
  shipPostalCode: text('ship_postal_code'),
  shipCountry: text('ship_country').notNull(),
  customerId: text('customer_id').notNull(),
  employeeId: integer('employee_id').notNull(),
});

const details = sqliteTable('order_detail', {
  unitPrice: numeric('unit_price').notNull(),
  quantity: integer('quantity').notNull(),
  discount: numeric('discount').notNull(),
  orderId: integer('order_id').notNull(),
  productId: integer('product_id').notNull(),
});


db
  .select({
    id: orders.id,
    shippedDate: orders.shippedDate,
    shipName: orders.shipName,
    shipCity: orders.shipCity,
    shipCountry: orders.shipCountry,
    productsCount: sql`cast(count(${details.productId}) as int)`,
    quantitySum: sql`sum(${details.quantity})`,
    totalPrice: sql`sum(${details.quantity} * ${details.unitPrice})`,
  })
  .from(orders)
  .leftJoin(details, eq(orders.id, details.orderId))
  .groupBy(orders.id)
  .orderBy(asc(orders.id))
  .all();

$count

<$count />는 특정 기능을 수행하는 컴포넌트입니다. 이 컴포넌트는 주로 카운팅 기능을 구현할 때 사용됩니다. 예를 들어, 리스트의 항목 수를 표시하거나 특정 이벤트의 발생 횟수를 추적하는 데 유용합니다.

db.$count()count(*)를 편리하게 사용할 수 있는 유틸리티 래퍼입니다. 이 연산자는 단독으로 사용하거나 서브쿼리로 활용할 수 있어 매우 유연합니다. 더 자세한 내용은 GitHub 토론에서 확인할 수 있습니다.

const count = await db.$count(users);
//    ^? number

const count = await db.$count(users, eq(users.name, "Dan")); // 필터와 함께 사용 가능
select count(*) from "users";
select count(*) from "users" where "name" = 'Dan';

이 기능은 서브쿼리에서 특히 유용합니다:

const users = await db.select({
  ...users,
  postsCount: db.$count(posts, eq(posts.authorId, users.id)),
}).from(users);

관계형 쿼리와 함께 사용하는 예제:

const users = await db.query.users.findMany({
  extras: {
    postsCount: db.$count(posts, eq(posts.authorId, users.id)),
  },
});

위 코드는 $count 컴포넌트를 사용하는 간단한 예제입니다. 이 컴포넌트를 사용하면 여러분의 애플리케이션에서 쉽게 카운팅 기능을 추가할 수 있습니다.

---

이터레이터(Iterator)

MySQL
PostgreSQL[WIP]
SQLite[WIP]
SingleStore[WIP]

쿼리 결과로 매우 많은 양의 행(row)을 반환해야 하는데, 이 모든 데이터를 메모리에 한 번에 로드하고 싶지 않다면 .iterator()를 사용해 쿼리를 비동기 이터레이터로 변환할 수 있습니다.

const iterator = await db.select().from(users).iterator();

for await (const row of iterator) {
  console.log(row);
}

이 기능은 준비된 문장(prepared statements)에서도 동작합니다.

const query = await db.select().from(users).prepare();
const iterator = await query.iterator();

for await (const row of iterator) {
  console.log(row);
}

---

인덱스 사용하기

USE INDEX 힌트는 쿼리를 처리할 때 최적화 도구가 고려해야 할 인덱스를 제안합니다. 최적화 도구는 이 인덱스를 반드시 사용해야 하는 것은 아니지만, 적합하다면 이를 우선적으로 고려합니다.

MySQL
PostgreSQL
SQLite
SingleStore
export const users = mysqlTable('users', {
	id: int('id').primaryKey(),
	name: varchar('name', { length: 100 }).notNull(),
}, () => [usersTableNameIndex]);

const usersTableNameIndex = index('users_name_index').on(users.name);

await db.select()
  .from(users, { useIndex: usersTableNameIndex })
  .where(eq(users.name, 'David'));

이 옵션은 원하는 조인에서도 사용할 수 있습니다.

await db.select()
  .from(users)
  .leftJoin(posts, eq(posts.userId, users.id), { useIndex: usersTableNameIndex })
  .where(eq(users.name, 'David'));

인덱스 무시하기

IGNORE INDEX 힌트는 옵티마이저에게 특정 인덱스를 사용하지 않도록 지시합니다. MySQL은 다른 인덱스(존재하는 경우)를 고려하거나 필요한 경우 전체 테이블 스캔을 수행합니다.

MySQL
PostgreSQL
SQLite
SingleStore
export const users = mysqlTable('users', {
	id: int('id').primaryKey(),
	name: varchar('name', { length: 100 }).notNull(),
}, () => [usersTableNameIndex]);

const usersTableNameIndex = index('users_name_index').on(users.name);

await db.select()
  .from(users, { ignoreIndex: usersTableNameIndex })
  .where(eq(users.name, 'David'));

이 옵션은 원하는 조인에서도 사용할 수 있습니다.

await db.select()
  .from(users)
  .leftJoin(posts, eq(posts.userId, users.id), { useIndex: usersTableNameIndex })
  .where(eq(users.name, 'David'));

FORCE INDEX 힌트

FORCE INDEX 힌트는 쿼리에서 지정된 인덱스를 사용하도록 옵티마이저에 강제합니다. 지정된 인덱스를 사용할 수 없는 경우, MySQL은 다른 인덱스로 대체하지 않고 전체 테이블 스캔을 수행할 수 있습니다.

MySQL
PostgreSQL
SQLite
SingleStore
export const users = mysqlTable('users', {
    id: int('id').primaryKey(),
    name: varchar('name', { length: 100 }).notNull(),
}, () => [usersTableNameIndex]);

const usersTableNameIndex = index('users_name_index').on(users.name);

await db.select()
    .from(users, { forceIndex: usersTableNameIndex })
    .where(eq(users.name, 'David'));

이 옵션은 원하는 조인에서도 사용할 수 있습니다.

await db.select()
    .from(users)
    .leftJoin(posts, eq(posts.userId, users.id), { useIndex: usersTableNameIndex })
    .where(eq(users.name, 'David'));