SQL Select

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

다음 예제에서는 이와 같이 정의된 users 테이블이 있다고 가정합니다:

PostgreSQL
MySQL
SQLite
SingleStore
MSSQL
CockroachDB
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'),
});

기본 조회

테이블의 모든 행을 모든 컬럼을 포함하여 조회합니다:

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];
select "id", "name" from "users";

SQL처럼 테이블 컬럼뿐만 아니라 임의의 표현식을 선택 필드로 사용할 수 있습니다:

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

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

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

참고

v1.0.0-beta.1부터 컬럼에 .as()를 사용할 수 있습니다:

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

조건부 조회

조건에 따라 동적 선택 객체를 가질 수 있습니다:

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

const users = await selectUsers(true);

Distinct 조회

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

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

await db.selectDistinct({ id: users.id }).from(users).orderBy(users.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";

고급 조회

TypeScript를 기반으로 Drizzle API를 사용하면 다양하고 유연한 방식으로 조회 쿼리를 작성할 수 있습니다.

고급 부분 조회 미리보기입니다. 더 자세한 고급 사용 예제는 전용 가이드를 참조하세요.

IMPORTANT

getColumnsdrizzle-orm@1.0.0-beta.2부터 사용 가능합니다(여기에서 자세히 읽기)

pre-1 버전(예: 0.45.1)을 사용하는 경우 getTableColumns를 사용하세요


example 1
example 2
example 3
example 4
import { getColumns, sql } from 'drizzle-orm';

await db.select({
    ...getColumns(posts),
    titleLength: sql<number>`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);

스키마를 안전하게 변경하고 테이블과 컬럼의 이름을 변경할 수 있으며, 템플릿 보간 덕분에 쿼리에 자동으로 반영됩니다. 원시 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는 쿼리에서 필터를 결합하는 강력하고 유연한 방법을 제공합니다.

조건부 필터링 미리보기입니다. 더 자세한 고급 사용 예제는 전용 가이드를 참조하세요.

example 1
example 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

MSSQL

.limit().offset()을 사용하여 쿼리에 limitoffset 절을 추가합니다 - 예를 들어 페이지네이션을 구현하려면:

await db.select().from(users).limit(10);
await db.select().from(users).limit(10).offset(10);
select "id", "name", "age" from "users" limit 10;
select "id", "name", "age" from "users" limit 10 offset 10;

Fetch & offset

MSSQL

MSSQL에서 FETCHOFFSETORDER BY 절의 일부이므로 .orderBy() 함수 이후에만 사용할 수 있습니다

await db.select().from(users).orderBy(asc(users.id)).offset(5);
await db.select().from(users).orderBy(asc(users.id)).offset(5).fetch(10);
select [id], [name], [age] from [users] offset 5 rows;
select [id], [name], [age] from [users] offset 5 rows fetch next 10 rows;

Top

MSSQL

쿼리 결과 집합에서 반환되는 행을 지정된 수의 행으로 제한합니다

await db.select().from(users).top(10);
select top (10) [id], [name], [age] from [users];

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

// order by multiple fields
await db.select().from(users).orderBy(users.name, users.name2);
await db.select().from(users).orderBy(asc(users.name), desc(users.name2));
select "id", "name", "age" from "users" order by "name";
select "id", "name", "age" from "users" order by "name" desc;

select "id", "name", "age" from "users" order by "name", "name2";
select "id", "name", "age" from "users" order by "name" asc, "name2" desc;

고급 페이지네이션

TypeScript를 기반으로 Drizzle API를 사용하면 가능한 모든 SQL 페이지네이션 및 정렬 접근 방식을 구현할 수 있습니다.

고급 페이지네이션 미리보기입니다. 더 자세한 고급 사용 예제는 limit offset 페이지네이션커서 페이지네이션 가이드를 참조하세요.

example 1
example 2
example 3
example 4
await db
  .select()
  .from(users)
  .orderBy(asc(users.id)) // order by is mandatory
  .limit(4) // the number of rows to return
  .offset(4); // the number of rows to skip

---

WITH 절

insert, update, delete에서 WITH 문을 사용하는 방법을 확인하세요

with 절을 사용하면 복잡한 쿼리를 CTE(Common Table Expression)라는 작은 하위 쿼리로 분할하여 단순화할 수 있습니다:

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;

with 내부에 insert, updatedelete 문을 제공할 수도 있습니다

const sq = db.$with('sq').as(
    db.insert(users).values({ name: 'John' }).returning(),
);

const result = await db.with(sq).select().from(sq);
with "sq" as (insert into "users" ("id", "name") values (default, 'John') returning "id", "name")
select "id", "name" from "sq"
const sq = db.$with('sq').as(
    db.update(users).set({ age: 25 }).where(eq(users.name, 'John')).returning(),
);
const result = await db.with(sq).select().from(sq);
with "sq" as (update "users" set "age" = 25 where "users"."name" = 'John' returning "id", "name", "age")
select "id", "name", "age" from "sq"
const sq = db.$with('sq').as(
  db.delete(users).where(eq(users.name, 'John')).returning(),
);

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

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


const sq = db.$with('sq').as(db.select({
  name: sql<string>`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";

서브쿼리는 테이블을 사용할 수 있는 모든 곳에서 사용할 수 있습니다. 예를 들어 조인에서:

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";

---

집계

Drizzle을 사용하면 원시 SQL에서 하는 것처럼 .groupBy().having()을 사용하여 그룹화 및 필터링을 통해 sum, count, avg 등의 함수를 사용한 집계를 수행할 수 있습니다:

import { gt } from 'drizzle-orm';

await db.select({
  age: users.age,
  count: sql<number>`cast(count(${users.id}) as int)`,
})
  .from(users)
  .groupBy(users.age);

await db.select({
  age: users.age,
  count: sql<number>`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";

select "age", cast(count("id") as int)
  from "users"
  group by "age"
  having cast(count("id") as int) > 1;

cast(... as int)count()가 PostgreSQL에서 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";
// It's equivalent to writing
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";
// It's equivalent to writing
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";
// It's equivalent to writing
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";
// It's equivalent to writing
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";
// It's equivalent to writing
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";
// It's equivalent to writing
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";
// It's equivalent to writing
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";
// It's equivalent to writing
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<number>`cast(count(${details.productId}) as int)`,
    quantitySum: sql<number>`sum(${details.quantity})`,
    totalPrice: sql<number>`sum(${details.quantity} * ${details.unitPrice})`,
  })
  .from(orders)
  .leftJoin(details, eq(orders.id, details.orderId))
  .groupBy(orders.id)
  .orderBy(asc(orders.id))
  .all();

$count

db.$count()count(*)의 유틸리티 래퍼로, 단독으로 사용하거나 서브쿼리로 사용할 수 있는 매우 유연한 연산자입니다. 자세한 내용은 GitHub discussion에서 확인할 수 있습니다.

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

const count = await db.$count(users, eq(users.name, "Dan")); // works with filters
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)),
  },
});

---

Iterator

MySQL
PostgreSQL[WIP]
SQLite[WIP]
SingleStore[WIP]
MSSQL
CockroachDB[WIP]

쿼리에서 매우 많은 양의 행을 반환해야 하고 모두 메모리에 로드하고 싶지 않은 경우 .iterator()를 사용하여 쿼리를 비동기 이터레이터로 변환할 수 있습니다:

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

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

준비된 문(prepared statement)에서도 작동합니다:

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

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

---

Use Index

USE INDEX 힌트는 쿼리를 처리할 때 고려할 인덱스를 옵티마이저에 제안합니다. 옵티마이저가 이러한 인덱스를 강제로 사용하지는 않지만 적합한 경우 우선순위를 지정합니다.

MySQL
PostgreSQL
SQLite
SingleStore
MSSQL
CockroachDB
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

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

MySQL
PostgreSQL
SQLite
SingleStore
MSSQL
CockroachDB
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
MSSQL
CockroachDB
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'));