icon

데이터베이스 쿼리 최적화


 데이터베이스 쿼리 최적화는 애플리케이션의 성능을 결정짓는 핵심 요소 중 하나입니다.

 NestJS 애플리케이션에서 효율적인 쿼리 최적화는 응답 시간 단축, 서버 리소스 사용 효율화, 그리고 전반적인 사용자 경험 향상으로 이어집니다.

ORM을 사용한 쿼리 최적화

 NestJS에서 주로 사용되는 TypeORM을 예로 들어 쿼리 최적화 전략을 살펴보겠습니다.

  1. 필요한 컬럼만 선택
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}
 
  async getUsers() {
    return this.userRepository.find({
      select: ['id', 'username', 'email'],
    });
  }
}
  1. 관계 데이터 조인
async getUsersWithPosts() {
  return this.userRepository.find({
    relations: ['posts'],
  });
}
  1. 쿼리 빌더 사용
async getActiveUsersWithRecentPosts() {
  return this.userRepository.createQueryBuilder('user')
    .leftJoinAndSelect('user.posts', 'post')
    .where('user.isActive = :isActive', { isActive: true })
    .andWhere('post.createdAt > :date', { date: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) })
    .getMany();
}

N+1 문제 해결

 N+1 문제는 ORM에서 자주 발생하는 성능 이슈로 한 번의 쿼리로 N개의 레코드를 가져온 후 각 레코드에 대해 추가 쿼리를 실행하는 상황을 말합니다.

 해결 방법

  1. Eager Loading
@Entity()
export class User {
  @OneToMany(() => Post, post => post.user, { eager: true })
  posts: Post[];
}
  1. Join Queries
async getUsersWithPosts() {
  return this.userRepository.createQueryBuilder('user')
    .leftJoinAndSelect('user.posts', 'post')
    .getMany();
}

인덱싱 전략

 적절한 인덱스 설계는 쿼리 성능을 크게 향상시킬 수 있습니다.

  1. 단일 컬럼 인덱스
@Entity()
export class User {
  @Index()
  @Column()
  email: string;
}
  1. 복합 인덱스
@Entity()
@Index(['lastName', 'firstName'])
export class User {
  @Column()
  firstName: string;
 
  @Column()
  lastName: string;
}

 인덱스 관리 시 주의사항

  • 과도한 인덱스 생성 지양 (삽입/수정 성능 저하)
  • 주기적인 인덱스 재구성
  • 실제 쿼리 패턴 분석 후 인덱스 설계

페이지네이션 구현

  1. Offset 기반 페이지네이션
async getUsers(page: number, limit: number) {
  return this.userRepository.find({
    skip: (page - 1) * limit,
    take: limit,
  });
}
  1. 커서 기반 페이지네이션
async getUsersAfter(cursorId: number, limit: number) {
  return this.userRepository.createQueryBuilder('user')
    .where('user.id > :cursorId', { cursorId })
    .orderBy('user.id', 'ASC')
    .take(limit)
    .getMany();
}

 커서 기반 페이지네이션은 대용량 데이터셋에서 더 효율적이며, 일관된 결과를 제공합니다.

쿼리 성능 분석

 EXPLAIN을 사용한 쿼리 분석

async analyzeQuery() {
  const rawQuery = this.userRepository.createQueryBuilder('user')
    .where('user.isActive = :isActive', { isActive: true })
    .getQuery();
 
  return this.userRepository.query(`EXPLAIN ${rawQuery}`);
}

 분석 결과를 바탕으로 인덱스 추가, 쿼리 구조 변경 등의 최적화를 수행할 수 있습니다.

캐싱과 쿼리 최적화 결합

 자주 사용되는 쿼리 결과를 캐싱하여 데이터베이스 부하를 줄일 수 있습니다.

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
    @Inject(CACHE_MANAGER)
    private cacheManager: Cache,
  ) {}
 
  async getTopUsers() {
    const cacheKey = 'top_users';
    const cachedUsers = await this.cacheManager.get(cacheKey);
    if (cachedUsers) {
      return cachedUsers;
    }
 
    const users = await this.userRepository.find({
      where: { isActive: true },
      order: { score: 'DESC' },
      take: 10,
    });
 
    await this.cacheManager.set(cacheKey, users, { ttl: 3600 });
    return users;
  }
}

데이터베이스 파티셔닝과 샤딩

 대규모 데이터셋을 관리하기 위해 파티셔닝이나 샤딩을 고려할 수 있습니다.

  1. 파티셔닝 : 단일 데이터베이스 내에서 테이블을 분할
  2. 샤딩 : 여러 데이터베이스 서버에 데이터 분산

 NestJS에서 구현 예

@Injectable()
export class UserService {
  private shards: DataSource[];
 
  constructor() {
    this.shards = [
      new DataSource({ /* shard1 config */ }),
      new DataSource({ /* shard2 config */ }),
    ];
  }
 
  async getUser(id: number) {
    const shardIndex = id % this.shards.length;
    return this.shards[shardIndex].getRepository(User).findOne(id);
  }
}

Best Practices 및 주의사항

  1. 쿼리 최소화 : 필요한 데이터만 조회
  2. 적절한 인덱스 사용 : 쿼리 패턴 분석 후 인덱스 설계
  3. N+1 문제 주의 : Eager Loading 또는 Join 쿼리 활용
  4. 대용량 데이터 처리 : 페이지네이션 구현, 특히 커서 기반 페이지네이션 고려
  5. 쿼리 캐싱 : 자주 사용되는 쿼리 결과 캐싱
  6. 주기적인 쿼리 분석 : EXPLAIN을 통한 쿼리 성능 모니터링
  7. ORM 최적화 : Lazy Loading 주의, 필요 시 Raw 쿼리 사용
  8. 트랜잭션 관리 : 장기 실행 트랜잭션 최소화
  9. 데이터베이스 확장성 : 파티셔닝 또는 샤딩 고려
  10. 정기적인 데이터베이스 유지보수 : 통계 업데이트, 인덱스 재구성 등

 N+1 문제는 ORM 사용 시 자주 발생하는 성능 이슈로, Eager Loading이나 Join 쿼리를 통해 해결할 수 있습니다.

 인덱싱 전략은 쿼리 성능 향상의 핵심요소로, 실제 사용 패턴을 분석하여 적절한 인덱스를 설계해야 합니다.

 페이지네이션 구현 시 대용량 데이터셋에서는 커서 기반 방식이 더 효율적입니다.

 EXPLAIN을 활용한 쿼리 성능 분석은 지속적인 성능 모니터링과 최적화에 필수적입니다.