Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Be/feature/#522 데이터베이스 최적화 #523

Merged
merged 13 commits into from
Feb 14, 2024

Conversation

kimyu0218
Copy link
Collaborator

@kimyu0218 kimyu0218 commented Feb 6, 2024

변경 사항

  • 커버링 인덱스를 활용하기 위해 정말 필요한 컬럼만 조회하도록 수정
  • 불필요한 쿼리 삭제 (ex. saveINSERTSELECT가 수행됨)
  • 트랜잭션 오버헤드 방지
  • 데이터 타입 수정

고민과 해결 과정

1️⃣ 트랜잭션 적용하기

mysql은 기본적으로 AUTOCOMMIT이 1로 설정되어 있다. 이로 인해 개별 sql문이 트랜잭션으로 취급되고 트랜잭션의 시작과 끝에 오버헤드가 발생하여 성능이 저하될 수 있다.

따라서 연관된 쿼리를 하나의 트랜잭션으로 묶어 오버헤드를 줄여줄 것이다. 다음 코드는 한 번에 여러 개의 쿼리를 일괄로 실행하는 함수인데, 트랜잭션을 사용하지 않아 각각의 save 작업이 개별적인 트랜잭션으로 처리된다. 실행 시 약 395 밀리초가 소요된다.

async saveMessages(
  room: ChattingRoom,
  createMessageDtos: CreateChattingMessageDto[],
): Promise<void> {
  const startTime = new Date();
  for (const createMessageDto of createMessageDtos) {
    const message: ChattingMessage = ChattingMessage.fromDto(
      createMessageDto,
      room,
    );
    await this.chattingMessageRepository.save(message);
  }
  const endTime = new Date();
  const diff = endTime.getTime() - startTime.getTime();
  console.log(`findOneBy execution time: ${diff} milliseconds`);
}
[QUERY]: START TRANSACTION
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ? 
[QUERY]: COMMIT
[QUERY]: START TRANSACTION
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ? 
[QUERY]: COMMIT
[QUERY]: START TRANSACTION
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ?
[QUERY]: COMMIT
[QUERY]: START TRANSACTION
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ?
[QUERY]: COMMIT
[QUERY]: START TRANSACTION
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ? 
[QUERY]: COMMIT
[QUERY]: START TRANSACTION
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ? 
[QUERY]: COMMIT

EntityManager의 트랜잭션을 사용하여 모든 쿼리를 하나의 트랜잭션으로 묶었다. 모든 쿼리가 성공하면 커밋되고, 하나라도 실패하면 롤백하도록 수정했다. 트랜잭션을 활용함으로써 약 140밀리초가 줄어들었다.

async saveMessages(
  room: ChattingRoom,
  createMessageDtos: CreateChattingMessageDto[],
): Promise<void> {
  return this.entityManager.transaction(async (manager) => {
    try {
      const startTime = new Date();
      const messages: ChattingMessage[] = createMessageDtos.map(
        (createMessageDto: CreateChattingMessageDto): ChattingMessage =>
          ChattingMessage.fromDto(createMessageDto, room),
      );
      await manager.save(ChattingMessage, messages);
      const endTime = new Date();
      const diff = endTime.getTime() - startTime.getTime();
      console.log(`findOneBy execution time: ${diff} milliseconds`);
    } catch (err: unknown) {
      throw err;
    }
  });
}
[QUERY]: START TRANSACTION
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ? 
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ? 
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ?
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ?
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ? 
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?) 
[QUERY]: SELECT `ChattingMessage`.`id` AS `ChattingMessage_id`, `ChattingMessage`.`created_at` AS `ChattingMessage_created_at`, `ChattingMessage`.`updated_at` AS `ChattingMessage_updated_at` FROM `chatting_message` `ChattingMessage` WHERE `ChattingMessage`.`id` = ? 
[QUERY]: COMMIT

아까와 달리 모든 쿼리가 끝난 후에 커밋이 이루어지는 것을 확인할 수 있다.

2️⃣ 불필요한 쿼리 삭제하기

위의 쿼리 로그를 보면 INSERT 후에 불필요한 SELECT가 발생하고 있다. 이는 TypeOrm의 save 때문이다. save는 insert 후에 select를 실행하여 삽입한 엔티티를 반환한다. select가 불필요한 경우 insert 메서드를 사용하여 불필요한 조회 작업을 하지 않아야 한다. 아래는 insert로 바꾼 후의 쿼리 로그다.

[QUERY]: START TRANSACTION
[QUERY]: INSERT INTO `chatting_message`(`id`, `is_host`, `message`, `created_at`, `updated_at`, `room_id`) VALUES (?, ?, ?, DEFAULT, DEFAULT, ?), (?, ?, ?, DEFAULT, DEFAULT, ?), (?, ?, ?, DEFAULT, DEFAULT, ?), (?, ?, ?, DEFAULT, DEFAULT, ?)
[QUERY]: COMMIT

3️⃣ 데이터 타입 수정하기

데이터 크기를 최적화하여 디스크에 저장되는 데이터 양을 줄여야 한다. 따라서 작은 크기의 데이터 타입을 사용하여 디스크 공간과 메모리를 확보하도록 수정했다.

  • 파일 확장자를 나타내는 ext 컬럼에 enum 적용
    • sqlite에서 enum을 지원하지 않아 tinyint와 ts의 enum으로 대체
  • -128~127을 범위 내의 정수형에 대해 tinyint 적용
  • text 대신 varchar 사용

(선택) 테스트 결과

  • 트랜잭션으로 연관된 작업을 묶어 불필요한 오버헤드 방지
  • insert를 사용하여 불필요한 SELECT 작업 제거
트랜잭션 적용 전 & save 사용 트랜잭션 적용 후 & save 사용 트랜잭션 적용 후 & insert 사용
약 395 밀리초 약 255 밀리초 약 89 밀리초

- findBy -> find, findOneBy -> findOne으로 수정하고 select 옵션을
  작성하여 엔티티 전체를 불러오는 것이 아니라 필요한 컬럼만 가져오도록
  변경
- mysql 스토리지 엔진으로 innoDB를 사용하는데, 버퍼풀이 캐싱 기능을
  효율적으로 사용하기 위해서는 버퍼풀에 정말 필요한 정보만 가져와야 함
연관 있는 작업을 하나의 트랜잭션으로 처리하여 불필요한 오버헤드 방지
- TypeORM의 save 메서드는 insert 후 select를 실행함
- 불필요한 조회 작업을 수행하므로 select가 필요 없는 경우 insert만
  수행하도록 insert 함수로 변경함
@kimyu0218 kimyu0218 added this to the version 1.0.0 milestone Feb 6, 2024
@kimyu0218 kimyu0218 self-assigned this Feb 6, 2024
Copy link

cloudflare-workers-and-pages bot commented Feb 7, 2024

Deploying with  Cloudflare Pages  Cloudflare Pages

Latest commit: d42e1be
Status: ✅  Deploy successful!
Preview URL: https://613fc8a9.web09-magicconch.pages.dev
Branch Preview URL: https://be-feature--522.web09-magicconch.pages.dev

View logs

@kimyu0218 kimyu0218 linked an issue Feb 12, 2024 that may be closed by this pull request
- 데이터 크기를 최적화하여 디스크에 저장되는 데이터 양 절약
- int 대신 tinyint 사용
- text 대신 varchar 사용
- enum을 적용하여 varchar 대신 tinyint를 사용하도록 수정
- 처음엔 타입을 enum으로 적용하려 하였으나 e2e 테스트에서 사용하는
  sqlite에 enum 타입이 없어 e2e 테스트를 패스할 수 없었음
  -> tinyint와 타입스크립트의 enum 타입을 사용
- 데이터베이스에서는 tinyint가 사용됨
- tinyint를 올바른 문자열로 매핑하여 반환하도록 수정
@kimyu0218 kimyu0218 marked this pull request as ready for review February 14, 2024 09:49
@kimyu0218
Copy link
Collaborator Author

신규 버전 배포 전에 DB 자료형 업데이트 필요

Copy link
Collaborator

@HeoJiye HeoJiye left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍👍

@kimyu0218 kimyu0218 merged commit 85e56b3 into dev Feb 14, 2024
3 checks passed
@kimyu0218 kimyu0218 deleted the BE/feature/#522-데이터베이스-최적화 branch February 14, 2024 11:20
@HeoJiye HeoJiye mentioned this pull request Feb 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

✅ 데이터베이스 최적화
3 participants