Drizzle ORM 심화 가이드 — 고급 쿼리와 성능 최적화
이 글의 핵심
Drizzle ORM은 TypeScript 스키마에서 SQL에 가까운 타입 안전 쿼리를 제공합니다. 이 글에서는 다중 조인·서브쿼리, 트랜잭션과 행 수준 락, Prepared Statement, 관계형 로딩과 N+1 방지, drizzle-kit 마이그레이션 전략, Prisma와의 설계·운영 비교, 인덱스·배치·연결 풀까지 실무 성능 관점으로 설명합니다.
이 글의 핵심
Drizzle ORM은 TypeScript로 스키마를 정의하고, 빌드 타임에 SQL과 타입을 맞추는 경량 ORM입니다. 단순 CRUD를 넘어서 운영 환경에서는 복잡한 조인, 서브쿼리와 CTE, 트랜잭션·락, Prepared Statement, 관계형 데이터 접근 패턴의 성능, 마이그레이션 파이프라인, 그리고 Prisma 등 다른 ORM과의 선택이 문제로 이어집니다. 본문은 PostgreSQL을 기준으로 설명하되, 개념은 MySQL·SQLite 등 Drizzle이 지원하는 드라이버에도 대부분 이전할 수 있습니다.
1. 전제 지식과 Drizzle의 위치
이미 drizzle-orm으로 테이블을 정의하고 db.select().from(...) 수준의 쿼리를 실행해 본 독자를 대상으로 합니다. 스키마 우선(schema-first)으로 pgTable, mysqlTable 등으로 컬럼을 선언하고, drizzle-kit으로 마이그레이션을 생성하는 흐름을 알고 있다고 가정합니다.
Drizzle의 철학은 “SQL에 가깝게 쓰되 타입이 깨지지 않게”에 가깝습니다. 따라서 고급 주제로 갈수록 순수 SQL 지식(EXPLAIN, 격리 수준, 락, 인덱스)이 그대로 실력으로 이어집니다. ORM이 마법을 대신해 주지 않으므로, 실행 계획과 트랜잭션 경계를 직접 설계해야 합니다.
2. 복잡한 조인과 서브쿼리
2.1 다중 조인과 별칭
동일 테이블을 두 번 조인하거나, 조인 결과에 명시적 별칭이 필요하면 서브쿼리를 as로 이름 붙인 파생 테이블로 쓰는 패턴이 자주 쓰입니다. Drizzle에서는 조인 대상을 sql 보조나 서브쿼리 빌더로 감싼 뒤 .as('alias') 형태로 연결합니다.
import { eq, and, sql } from 'drizzle-orm';
import { users, orders, orderItems } from './schema';
// 사용자·주문·주문항목을 조인하고, "해당 사용자의 최신 주문"만 남기는 조건 예시
const rows = await db
.select({
userName: users.name,
orderId: orders.id,
lineTotal: sql<number>`sum(${orderItems.qty} * ${orderItems.unitPrice})`,
})
.from(users)
.innerJoin(
orders,
and(
eq(orders.userId, users.id),
eq(
orders.createdAt,
sql`(select max(o2.created_at) from orders o2 where o2.user_id = ${users.id})`,
),
),
)
.innerJoin(orderItems, eq(orderItems.orderId, orders.id))
.groupBy(users.id, users.name, orders.id);
위 예시는 “최근 주문”을 서브쿼리로 두는지, 윈도우 함수로 두는지에 따라 SQL이 달라집니다. Drizzle에서는 sql 템플릿으로 DB별 윈도우 함수를 직접 넣거나, 가독성을 위해 뷰(view)를 두는 선택도 흔합니다.
왜 중요한가: 조인 깊이가 늘수록 중복 행 폭발과 집계 오류(중복 합산)가 생깁니다. 서브쿼리·CTE·윈도우 중 무엇을 쓸지는 데이터 분포와 인덱스에 따라 달라지므로, EXPLAIN ANALYZE로 행 수와 실제 비용을 확인하는 것이 전제입니다.
2.2 EXISTS·IN·상관 서브쿼리
“한 조건을 만족하는 행이 존재하는가”는 exists가, “목록에 포함되는가”는 in이 자연스럽습니다. Drizzle에서는 exists, inArray, notInArray, sql을 조합합니다.
import { exists, inArray, eq, and, sql } from 'drizzle-orm';
import { users, orders } from './schema';
const activeUserIds = [1, 2, 3];
await db
.select()
.from(users)
.where(
and(
inArray(users.id, activeUserIds),
exists(
db
.select({ one: sql`1` })
.from(orders)
.where(and(eq(orders.userId, users.id), eq(orders.status, 'paid'))),
),
),
);
상관 서브쿼리는 인덱스가 잘 타면 빠르지만, 바깥 행마다 안쪽을 반복하는 형태가 되면 비용이 커집니다. 이 경우 조인으로 바꾸거나, 임시 결과를 먼저 집계하는 편이 낫습니다.
2.3 CTE(WITH)와 가독성
복잡한 리포트 쿼리는 CTE로 단계를 나누면 유지보수에 유리합니다. Drizzle 버전에 따라 with 헬퍼가 제공되므로, 프로젝트의 drizzle-orm 문서를 확인해 공식 with API를 쓰는 것이 좋습니다. CTE가 항상 물리적으로 “임시 테이블”을 만드는 것은 아니며, 옵티마이저가 인라인할 수도 있습니다. 성능은 최종 실행 계획으로 판단합니다.
3. 트랜잭션과 락
3.1 기본 트랜잭션
결제·재고·포인트처럼 여러 테이블이 함께 맞아야 하는 연산은 하나의 트랜잭션으로 묶습니다. Drizzle에서는 드라이버별 db.transaction 콜백에서 tx 객체로 동일 API를 사용합니다.
await db.transaction(async (tx) => {
const [user] = await tx
.select()
.from(users)
.where(eq(users.id, userId))
.for('update'); // PostgreSQL: 행 잠금
if (!user || user.balance < amount) {
throw new Error('INSUFFICIENT_FUNDS');
}
await tx
.update(users)
.set({ balance: sql`${users.balance} - ${amount}` })
.where(eq(users.id, userId));
await tx.insert(ledger).values({
userId,
amount: -amount,
reason: 'PAYMENT',
});
});
포인트: throw로 트랜잭션을 롤백시키는 패턴이 일반적입니다. 애플리케이션 예외 타입을 정해 재시도 가능한 오류(데드락 등)와 비즈니스 오류를 구분하면 운영에 유리합니다.
3.2 격리 수준과 데드락
PostgreSQL에서 read committed가 기본이며, 필요 시 세션·트랜잭션 단위로 격리 수준을 올릴 수 있습니다. 격리 수준을 올리면 팬텀·반복 읽기는 줄지만 락 경합·데드락 위험이 커집니다.
실무에서는 다음을 습관화합니다.
- 같은 순서로 락을 잡기: 예를 들어 항상
user_id오름차순으로 여러 행을 갱신하면 데드락을 줄일 수 있습니다. - 락 보유 시간 최소화: 외부 HTTP 호출은 트랜잭션 밖으로.
- idempotency 키: 결제·웹훅은 재시도가 붙으므로, 유일 제약 + 멱등 처리로 안전장치를 둡니다.
3.3 SELECT … FOR UPDATE SKIP LOCKED
작업 큐·배치에서 다음 작업을 가져올 때 skip locked 패턴이 쓰입니다. Drizzle에서는 for('update', { skipLocked: true }) 형태(버전별 API 확인)로 매핑합니다. 이 패턴은 경쟁하는 워커가 동시에 같은 행을 집지 않게 해 줍니다.
4. Prepared Statement
4.1 왜 쓰는가
Prepared Statement는 파라미터 바인딩을 강제하여 SQL 인젝션을 줄이고, 동일 SQL을 반복 실행할 때 파싱·계획 캐시 이점을 기대할 수 있습니다. 다만 모든 쿼리에 자동으로 이득이 있는 것은 아니며, 부하 패턴에 따라 달라집니다.
4.2 Drizzle에서의 사용 예
import { eq, sql } from 'drizzle-orm';
const userById = db
.select()
.from(users)
.where(eq(users.id, sql.placeholder('id')))
.prepare('user_by_id');
const row = await userById.execute({ id: 42 });
주의: placeholder 이름·바인딩 객체 형태는 Drizzle 버전과 드라이버에 따라 다를 수 있으므로, 사용 중인 버전 문서를 기준으로 맞춥니다. 서버리스·커넥션 풀 환경에서는 준비문이 연결마다 어떻게 캐시되는지도 확인합니다.
4.3 동적 IN 리스트와의 긴장
파라미터 개수가 요청마다 달라지면 “같은 SQL”이 아니어서 준비문 이점이 줄어듭니다. 이때는 임시 테이블에 ID를 넣고 조인하거나, 배열 파라미터(PostgreSQL = ANY($1::int[])) 같은 DB 기능을 검토합니다.
5. 관계형 쿼리 최적화
5.1 N+1 문제의 본질
N+1은 “부모 N행을 읽은 뒤 자식을 행마다 추가 조회”하는 패턴입니다. Drizzle은 SQL 친화적이라 조인·서브쿼리·배치 로딩을 코드로 명시하는 쪽이 자연스럽습니다. 관계 헬퍼를 쓰더라도 생성되는 SQL 수를 로그·메트릭으로 확인해야 합니다.
5.2 조인으로 한 번에 가져오기
리스트 화면에서 사용자·프로필·최근 주문을 함께 보여 준다면, 한 번의 조인 쿼리로 필요한 컬럼만 선택하는 편이 대체로 예측 가능합니다. select 절에 불필요한 JSON·대용량 텍스트를 넣지 않는 것도 중요합니다.
5.3 배치 로딩(데이터로더)
그래프QL·REST에서 트리 구조로 내려줄 때는 요청 단위로 ID를 모아 inArray 한 번에 가져오는 패턴이 깔끔합니다. 애플리케이션 레이어에서 Promise 캐시를 두어 동일 요청 내 중복 조회를 제거합니다.
5.4 인덱스와 쿼리 형태의 정합성
조인 키·필터 컬럼·정렬 컬럼에 맞는 복합 인덱스가 없으면 ORM을 바꿔도 느립니다. Drizzle을 쓰더라도 EXPLAIN, pg_stat_statements(PostgreSQL), 슬로 쿼리 로그로 상위 쿼리부터 줄입니다.
6. 마이그레이션 전략
6.1 drizzle-kit 워크플로
일반적인 흐름은 다음과 같습니다.
- 스키마 파일을 진실 원본으로 유지한다.
drizzle-kit generate로 SQL 마이그레이션을 생성한다.- 스테이징에서 적용·검증 후 프로덕션에 반영한다.
팀 규모가 크면 마이그레이션 리뷰 필수, 다운 마이그레이션 정책(롤백 vs 복구 스크립트)을 문서화합니다.
6.2 제로 다운타임을 깨는 변경
다음은 특히 주의합니다.
- 타입 변경(문자열 → 숫자 등): 단계적 이중 쓰기·백필이 필요할 수 있습니다.
- NOT NULL 추가: 기본값·백필 없이 추가하면 테이블 락과 장시간 쿼리를 유발할 수 있습니다.
- 대형 테이블 인덱스 생성: PostgreSQL의
CONCURRENTLY등 DB별 온라인 DDL을 검토합니다.
6.3 시드·로컬 개발
로컬은 migrate 후 시드 스크립트로 최소 데이터를 넣고, E2E는 도커 DB + 고정 시드로 재현성을 확보합니다. 프로덕션 데이터를 복제해 오는 방식은 개인정보·보안 이슈가 있으므로 별도 절차가 필요합니다.
7. Prisma vs Drizzle 비교
비교는 “누가 더 좋다”보다 제품·팀 맥락에 맞는지가 중요합니다.
| 관점 | Prisma | Drizzle |
|---|---|---|
| 쿼리 스타일 | 고수준 API·관계 탐색이 편리 | SQL에 가깝고 제어가 세밀 |
| 스키마 | schema.prisma 중심 | TypeScript 스키마 + SQL 마이그레이션 |
| 번들·런타임 | 기능이 풍부한 대신 무게 증가 | 비교적 경량 지향 |
| 학습 곡선 | 초반 생산성 높음 | SQL·인덱스 이해도가 그대로 필요 |
| 마이그레이션 | 익숙한 워크플로가 많음 | 팀이 SQL·DDL에 익숙해야 안전 |
Drizzle이 유리한 경우: SQL·인덱스·실행 계획을 팀이 직접 다루고, 복잡한 리포트·배치가 많으며, 경량 런타임이 중요한 경우.
Prisma가 유리한 경우: CRUD 중심·빠른 프로토타입·관계형 탐색 API를 넓게 쓰고, 마이그레이션·클라이언트 일체화를 우선할 때.
이주 시에는 1:1 기능 대응표를 만들고, 트랜잭션·락·raw SQL·마이그레이션 스크립트를 단계적으로 이전하는 편이 안전합니다.
8. 실전 성능 튜닝 체크리스트
8.1 애플리케이션 계층
- 커넥션 풀 크기: CPU·DB
max_connections·앱 인스턴스 수와 함께 튜닝합니다. 풀이 과하면 DB가 컨텍스트 스위칭으로 느려질 수 있습니다. - 트랜잭션 범위: 짧게. 외부 API·파일 I/O는 밖으로.
- 배치 크기:
insert·copy·배치 업데이트는 청크 단위로 나눕니다. - 불필요한
select *금지: Drizzle에서도 명시적 컬럼 선택이 대역폭·디코딩 비용을 줄입니다.
8.2 데이터베이스 계층
- 인덱스: WHERE·JOIN·ORDER BY에 맞춘 복합 인덱스, 부분 인덱스·표현식 인덱스 검토.
- 통계 정보: 대량 적재 후
ANALYZE·자동 통계가 제대로 도는지 확인. - 파티셔닝: 시계열·대형 로그는 범위 파티셔닝으로 유지보수성을 높일 수 있습니다.
8.3 관측성
- 슬로 쿼리 로그, APM의 DB 구간, pg_stat_statements로 상위 소비 쿼리를 고정적으로 추적합니다.
- 캐시(Redis 등)는 일관성 요구사항과 함께 설계합니다. 캐시가 오히려 복잡도를 키우는 경우도 많습니다.
9. 프로덕션 연결 계층과 서버리스 패턴
Drizzle은 드라이버와 풀 설정이 곧 운영 품질입니다. Neon·Supabase·RDS 프록시처럼 중간에 풀러가 있으면, 앱 쪽 max를 “인스턴스 수 × max” 합산으로 모델링해야 합니다. 세션 수준 기능(임시 테이블, SET, 커서)은 트랜잭션 모드에 따라 달라지므로 PgBouncer 문서와 같이 읽습니다.
서버리스(Vercel, Lambda, Cloudflare Workers) 에서는 프로세스가 짧거나 수천 개로 퍼지므로, 요청마다 새 클라이언트를 만들면 연결이 폭증합니다. 해결 축은 (1) 호스트 제공 서버리스 드라이버(예: Neon @neondatabase/serverless)·HTTP 경로, (2) 외부 풀(PgBouncer, RDS Proxy) + 낮은 앱 max, (3) 단일 장기 워커로 DB 붙이고 서버리스는 큐만 넣기—입니다.
읽기 전용 복제본을 Drizzle에서 쓸 때는 withReplicas 패턴(버전·API 확인)이나 읽기/쓰기용 db 인스턴스를 분리해 트랜잭션은 항상 프라이머리로 고정합니다. 강한 일관성이 필요한 읽기는 복제 지연을 전제로 하지 않습니다.
마이그레이션 실행 주체를 런타임과 분리하면 장애 반경이 줄어듭니다. 앱 기동 시 migrate()를 돌리는 대신 CI/CD 전용 잡이나 Kubernetes Job으로 DDL을 적용하고, 앱은 이미 적용된 스키마만 가정합니다.
10. 문제 해결 가이드
10.1 예상보다 느린 조인
- 조인 키 타입·콜레이션 불일치로 인덱스를 못 타는 경우가 있습니다.
- 선택도가 낮은 컬럼만 걸린 인덱스는 도움이 되지 않을 수 있습니다. 카디널리티를 다시 봅니다.
10.2 간헐적 데드락
- 트랜잭션 내 갱신 순서를 통일하고, 락 대기 시간 타임아웃·재시도 정책을 둡니다.
- 동일 비즈니스 키에 대한 동시 요청을 애플리케이션 뮤텍스로 직렬화하는 경우도 있습니다(분산 환경에서는 Redis 등과의 트레이드오프).
10.3 마이그레이션 후 플랜 악화
- 통계가 오래되었거나, 파라미터 스니핑 이슈일 수 있습니다. 통계 갱신, Prepared vs 일반 실행 비교, 인덱스 사용 여부를 다시 확인합니다.
10.4 서버리스·풀러 환경 특유 증상
- 간헐적 “connection closed” → 콜드 스타트·유휴 타임아웃·풀러 세션 한도. 연결 재시도와 짧은 트랜잭션을 조합합니다.
- 로컬만 빠르고 배포만 느림 → 리전·TLS·풀러 홉 추가. 동일 리전 배치와 준비문 캐시 특성을 확인합니다.
11. 정리
Drizzle ORM의 심화 주제는 결국 SQL과 트랜잭션 모델을 얼마나 정확히 다루느냐로 귀결됩니다. 복잡한 조인과 서브쿼리는 중복 행·집계 오류를 동반하기 쉬우므로 실행 계획으로 검증하고, 트랜잭션과 락은 경합·데드락·외부 I/O 분리 원칙을 지킵니다. Prepared Statement는 반복 부하에 맞춰 도입하고, 관계형 최적화는 N+1 제거·인덱스·배치 로딩의 삼각형으로 접근합니다. 프로덕션 연결 계층에서는 풀러·서버리스·복제 지연을 함께 모델링하고, 마이그레이션은 DDL의 운영 영향을 전제로 단계적 전략을 세우며, Prisma와의 선택은 팀의 SQL 역량과 제품 단계에 맡기는 것이 가장 현실적입니다.
배포 전에는 git add·git commit·git push 후 npm run deploy를 실행하는 저장소 규칙을 따릅니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Drizzle ORM의 복잡한 조인·서브쿼리, 트랜잭션·락, Prepared Statement, 관계 쿼리 최적화, 마이그레이션 전략, Prisma 비교, 실전 성능 튜닝을 한글로 정리합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
참고
- Drizzle 공식 문서: https://orm.drizzle.team/
- PostgreSQL 문서(격리·락): https://www.postgresql.org/docs/current/
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「Drizzle ORM 심화 가이드 — 고급 쿼리와 성능 최적화」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「Drizzle ORM 심화 가이드 — 고급 쿼리와 성능 최적화」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Drizzle ORM 완벽 가이드 | TypeScript·SQL·마이그레이션·Prisma 대안
- PostgreSQL 고급 최적화 가이드 — 쿼리 성능 튜닝·인덱스·파티셔닝·복제
- [Drizzle ORM Complete Guide | Schema· Queries](/en/blog/drizzle-orm-complete-guide/
이 글에서 다루는 키워드 (관련 검색어)
Drizzle, ORM, TypeScript, SQL, Performance 등으로 검색하시면 이 글이 도움이 됩니다.