Prisma 관계 쿼리란?
Prisma의 관계 쿼리는 include와 select를 통해 연관 데이터를 타입 안전하게 로딩하는 핵심 기능입니다. SQL JOIN을 직접 작성하지 않아도 Prisma가 최적화된 쿼리를 자동 생성합니다. 하지만 사용 방식에 따라 N+1 문제나 과도한 데이터 로딩이 발생할 수 있어, 올바른 패턴을 이해하는 것이 중요합니다.
이 글에서는 include/select 깊은 활용법, Fluent API, 관계 필터링, 집계, 그리고 성능 최적화 전략까지 실전 중심으로 다루겠습니다.
include vs select
| 항목 | include | select |
|---|---|---|
| 동작 | 모든 스칼라 필드 + 관계 추가 | 명시한 필드만 가져옴 |
| 기본 필드 | 자동 포함 | 명시해야 포함 |
| 혼용 | 같은 레벨에서 include + select 동시 사용 불가 | |
| 성능 | 편리하지만 과도한 데이터 가능 | 최소 데이터만 전송 (권장) |
// Schema 예시
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
profile Profile?
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
category Category? @relation(fields: [categoryId], references: [id])
categoryId Int?
tags TagOnPost[]
comments Comment[]
createdAt DateTime @default(now())
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts TagOnPost[]
}
model TagOnPost {
post Post @relation(fields: [postId], references: [id])
postId Int
tag Tag @relation(fields: [tagId], references: [id])
tagId Int
@@id([postId, tagId])
}
include: 관계 데이터 추가 로딩
// 기본 include: 모든 스칼라 필드 + 관계
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
profile: true, // 1:1
posts: true, // 1:N (모든 게시글)
},
});
// user.id, user.name, user.email, user.createdAt → 모두 포함
// user.profile → Profile | null
// user.posts → Post[]
// 중첩 include: 게시글의 댓글과 카테고리까지
const userDeep = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
include: {
comments: {
include: {
author: true, // 댓글 작성자까지
},
},
category: true,
tags: {
include: {
tag: true, // N:M 중간 테이블 → 태그
},
},
},
},
},
});
// include 내 필터링 + 정렬 + 제한
const userWithRecentPosts = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 5,
include: {
comments: {
take: 3,
orderBy: { createdAt: 'desc' },
},
},
},
},
});
select: 필요한 필드만 정밀 선택
// select로 API 응답에 필요한 최소 데이터만 조회
const userSummary = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
name: true,
// email, createdAt → 제외됨
posts: {
select: {
id: true,
title: true,
// content → 제외 (목록에서 불필요)
_count: {
select: { comments: true }, // 댓글 수만
},
},
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10,
},
_count: {
select: { posts: true }, // 총 게시글 수
},
},
});
// 타입이 자동 추론됨:
// {
// id: number;
// name: string;
// posts: { id: number; title: string; _count: { comments: number } }[];
// _count: { posts: number };
// }
Fluent API: 체이닝 관계 탐색
// Fluent API로 관계를 체인 형태로 탐색
// 유저 → 게시글 목록
const posts = await prisma.user
.findUnique({ where: { id: 1 } })
.posts({
where: { published: true },
orderBy: { createdAt: 'desc' },
});
// 게시글 → 작성자 → 프로필
const authorProfile = await prisma.post
.findUnique({ where: { id: 1 } })
.author()
.profile();
// 주의: Fluent API는 각 단계에서 별도 쿼리 실행
// 성능이 중요하면 include/select 사용 권장
관계 필터: where 조건에 관계 활용
// some: 하나 이상의 관계가 조건을 만족
const usersWithPublishedPosts = await prisma.user.findMany({
where: {
posts: {
some: {
published: true,
createdAt: { gte: new Date('2026-01-01') },
},
},
},
});
// every: 모든 관계가 조건을 만족
const usersAllPublished = await prisma.user.findMany({
where: {
posts: {
every: { published: true },
},
},
});
// none: 조건을 만족하는 관계가 없음
const usersWithoutDrafts = await prisma.user.findMany({
where: {
posts: {
none: { published: false },
},
},
});
// 중첩 관계 필터: 특정 태그의 게시글이 있는 유저
const usersWithTag = await prisma.user.findMany({
where: {
posts: {
some: {
tags: {
some: {
tag: { name: 'typescript' },
},
},
},
},
},
});
관계 집계: _count, _avg, _sum
// 관계 카운트 정렬: 인기 유저 목록
const popularUsers = await prisma.user.findMany({
select: {
id: true,
name: true,
_count: {
select: {
posts: true,
// 필터 적용도 가능
},
},
},
orderBy: {
posts: { _count: 'desc' }, // 게시글 수로 정렬
},
take: 10,
});
// groupBy + 관계 집계
const postsByCategory = await prisma.post.groupBy({
by: ['categoryId'],
_count: { id: true },
_avg: { viewCount: true },
where: { published: true },
orderBy: { _count: { id: 'desc' } },
take: 10,
});
N+1 문제와 해결
// ❌ N+1 패턴: 루프 내에서 관계 쿼리
const users = await prisma.user.findMany();
for (const user of users) {
// 유저마다 별도 쿼리 → N+1 발생!
const posts = await prisma.post.findMany({
where: { authorId: user.id },
});
}
// ✅ 해결 1: include로 한 번에 로딩
const usersWithPosts = await prisma.user.findMany({
include: {
posts: {
where: { published: true },
},
},
});
// ✅ 해결 2: IN 쿼리로 배치 로딩
const users = await prisma.user.findMany();
const userIds = users.map(u => u.id);
const allPosts = await prisma.post.findMany({
where: { authorId: { in: userIds } },
});
// 직접 매핑
const postsByUser = new Map();
for (const post of allPosts) {
const arr = postsByUser.get(post.authorId) || [];
arr.push(post);
postsByUser.set(post.authorId, arr);
}
중첩 Write: 관계 데이터 동시 생성
// create + 관계 동시 생성
const newUser = await prisma.user.create({
data: {
name: 'John',
email: 'john@example.com',
profile: {
create: { bio: 'Developer', avatarUrl: '/avatar.jpg' },
},
posts: {
create: [
{
title: 'First Post',
content: 'Hello World',
tags: {
create: [
{ tag: { connectOrCreate: {
where: { name: 'intro' },
create: { name: 'intro' },
}}},
],
},
},
],
},
},
include: { profile: true, posts: { include: { tags: true } } },
});
// update + 관계 조작
const updated = await prisma.user.update({
where: { id: 1 },
data: {
posts: {
updateMany: {
where: { published: false },
data: { published: true }, // 모든 초안 발행
},
deleteMany: {
createdAt: { lt: new Date('2025-01-01') }, // 오래된 글 삭제
},
},
},
});
성능 최적화 체크리스트
| 항목 | 권장 사항 |
|---|---|
| select vs include | API 응답에는 select로 필요한 필드만 (네트워크·DB 부하 감소) |
| 중첩 깊이 | 3단계 이상 중첩 시 별도 쿼리로 분리 |
| take/skip | 관계 로딩 시 반드시 take로 제한 |
| _count | 전체 데이터 불필요 시 _count만 조회 |
| Middleware | 로깅 미들웨어로 느린 쿼리 감지 |
| Client Extension | 공통 include/select 패턴을 Extension으로 재사용 |
마무리
Prisma의 관계 쿼리는 include와 select를 중심으로 타입 안전한 데이터 로딩을 제공합니다. some/every/none 필터로 관계 기반 조건 검색이 가능하고, _count 집계로 불필요한 데이터 전송을 줄일 수 있습니다. 성능을 위해서는 select 우선 사용, 중첩 깊이 제한, take 필수 적용 세 가지 원칙을 지키세요.