Prisma 6 → Drizzle 2.0으로 옮긴 지 한 달째인데 운영 모니터링에서 N+1이 잡혔습니다. 분명히 with로 관계 가져왔는데 쿼리가 N+1로 쪼개져 나가요.
환경: Drizzle 2.0.3, PostgreSQL 18, Node 24
코드:
// schema.ts
export const posts = pgTable('posts', { ... })
export const tags = pgTable('tags', { ... })
export const postTags = pgTable('post_tags', { ... })
export const postsRelations = relations(posts, ({ many }) => ({
postTags: many(postTags),
}))
export const postTagsRelations = relations(postTags, ({ one }) => ({
post: one(posts, { fields: [postTags.postId], references: [posts.id] }),
tag: one(tags, { fields: [postTags.tagId], references: [tags.id] }),
}))
// 쿼리
const result = await db.query.posts.findMany({
with: { postTags: { with: { tag: true } } },
limit: 50,
})
실제 나가는 쿼리 (pg_stat_statements):
SELECT * FROM posts LIMIT 50; -- 1번
SELECT * FROM post_tags WHERE post_id = $1; -- 50번 반복
SELECT * FROM tags WHERE id = $1; -- 평균 3번씩 × 50 = 150번
총 200쿼리가 나가서 페이지 응답이 1.4초로 느립니다. Prisma의 include는 IN 절로 묶어서 3쿼리로 끝나던 게 왜 Drizzle에선 다른가요?
댓글 3
Drizzle 1.x까지 알려진 한계였는데 2.0에서 거의 해결됐습니다. N+1이 발생하는 건 보통 두 가지 케이스 중 하나입니다.
케이스 ① — relations 정의가 같은 파일에 모여있지 않음
Drizzle은 relations을 분석해서 한 번에 LATERAL JOIN으로 SQL을 생성합니다. 그런데
postsRelations·postTagsRelations·tagsRelations세 개가 모두 동일한 import 그래프에 있어야 합니다. 파일을 쪼개두고 일부만 import하면 Drizzle이 모르는 관계로 처리해서 fallback 모드로 떨어집니다.해결:
schema/index.ts에서 모든 테이블·relations을 re-export하고,drizzle({ schema })호출 시 한 번에 넘기세요.케이스 ② — tagsRelations이 누락
질문 코드를 보면
postTagsRelations는 있지만tagsRelations가 없네요. tag → postTags 역방향이 정의 안 되어 있어도 Drizzle이 "이건 단방향" 으로 처리되는데, with 절의 최종 leaf인tag가 매번 별도 SELECT로 빠지는 케이스가 이때 발생합니다.이 두 가지 다 적용 후 다시 EXPLAIN 보면 LATERAL JOIN 1쿼리로 합쳐집니다. 실제 SQL은 이런 모양:
추가 — orderBy/limit 조합 함정
관계 안에
orderBy나limit이 있으면 (예: "각 post의 최근 댓글 3개") Drizzle은 LATERAL을 못 만들고 다시 N+1로 떨어집니다. 이 경우는 별도 쿼리를 두 번 날리고 코드에서 group by 하는 게 더 빠릅니다.한 가지 더 — N+1을 의도적으로 두 쿼리로 푸는 패턴도 자주 씁니다. 큰 테이블 두 개를 LATERAL로 묶으면 인덱스를 못 타는 케이스가 있어서요.
총 2쿼리. 인덱스가
post_tags(post_id)에 잘 잡혀 있다면 위의 LATERAL JOIN보다 30~40% 빠른 경우가 많아요.그리고 EXPLAIN ANALYZE 결과에서 Seq Scan이 보이면
post_tags에 인덱스 빠진 거니 그것부터 확인하세요. Drizzle은 인덱스 정의도 schema에 적어둘 수 있습니다 —index('idx_post_tags_post').on(postTags.postId).두 번째 답변의 inArray + group 패턴도 다음에 더 큰 테이블에 적용해보겠습니다. 감사합니다!