ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • N+1 문제 해결하기
    TIL 2024. 4. 5. 22:06

     

     

     

    프로젝트를 진행하던 중, 위키를 수정할때마다 기존에 수정한 이력이 있는 사용자들 모두에게 알림을 보내는 기능을 구현

     

    해야했다.  기존에는 SELECT문으로 해당 위키를 수정한 사람들의 목록을 출력하고 그 숫자만큼 반복문으로, 알림을 추가

     

    하는 Insert 쿼리를 돌려주었다.   그렇게 N+1 문제를 안게되었다.

     

     

     

    N+1 문제란 

     

    SELECT문으로 나온 데이터의 개수만큼 다른 쿼리문(update, insert, delete)을 반복하는 경우를 말한다.

     

    그래서 실제로는 1+N에 더 가깝다고 볼 수 있다.

     

     

    문제가 되는 이유)

     

    그래서 이게 왜 문제가되냐면, DB에 연결하는 작업은 프로그램에서 가장 느린 작업이다. 그런데 SELECT문으로 가져온

     

    데이터의 결과수 만큼 새로운 쿼리문을 반복한다는 것은 가져온 데이터의 수가 몇개인지도 예측할 수 없는 상황에서 그만

     

    큼 <DB연결 + DB작업>을 하는 것은 언제 해당 작업이 끝날지 알 수 없다. 

     

    특히 해당 기능은 이전에 해당 위키를 수정한 이력이 있는 사용자들 모두에게 알림을 보내는 기능이다.

     

    해당 기능이 수행될때마다 계속해서 누적되어 더욱 무거워지는 작업이고, 만약 1001번째 작업자가 수정하고, 연달아 2번

     

    째, 3번째 사람도 추가적으로 수정한다면 순식간에 3000천개의 데이터가 생성되고 단순 반복문으로 이를 시도한다면 

     

    3000번 DB연결 + 3000번 쿼리문실행으로 아주 쉽게 서버가 다운될 것이다.

     

     

      

     

    이를 지난시간에 배운 Bulk Insert와  unnest를 이용하여 해결해주었다.

     

     

     

    기존의 잘못된코드)

     

    //기존 게임수정자들 추출
    const historyUserSQLResult = await poolClient.query(
        `SELECT DISTINCT 
            user_idx
        FROM
            history
        WHERE 
            game_idx = $1`,
        [gameIdx]
    );
    let historyUserList = historyUserSQLResult.rows; // SELECT문의 결과리스트
    
    // SELECT문(1) 결과(N)만큼 쿼리문실행 (1+N문제) 
    for(int i=0; i<historyUserList.length; i++) {
    	await generateNotification(poolClient, 'MODIFY_GAME', gameIdx, historyUserList[i]);
    }

     

     

     

     

    수정된코드)

    //기존 게임수정자들 추출
    const historyUserSQLResult = await poolClient.query(
        `SELECT DISTINCT 
            user_idx
        FROM
            history
        WHERE 
            game_idx = $1`,
        [gameIdx]
    );
    let historyUserList = historyUserSQLResult.rows; // SELECT문의 결과리스트
    
    await generateNotifications({
        conn: poolClient,
        type: 'MODIFY_GAME',
        gameIdx: gameIdx,
        //SELECT문의 결과로 받아온 리스트들을 user_idx만 뽑아 다시 리스트로만들고 이 리스트를
        //알림생성모듈의 매개변수로 전달
        toUserIdx: historyUserList.map((elem) => elem.user_idx), 
    });

     

     

    수정된코드)  - 알림생성모듈에 unnest 적용)

     

    //Bulk Insert와 unnest를 적용하기
    // unnest의 $2부분에 userIdx의 리스트가 들어가게되면서
    // SELECT unnest로 리스트의 개수만큼, 데이터 행이 생성되고, insert문에 대입되는 구조
    const generateNotifications = async (option) => {
        (option.poolClient || pool).query(
            `INSERT INTO
                    notification (type, game_idx, post_idx, user_idx)
                SELECT
                    2, $1, NULL,
                    UNNEST($2::int[])`,
            [option.gameIdx, option.toUserIdx]
        );
    };
    
    //모듈 exports
    module.exports = { generateNotification, generateNotifications };

     

     

     

    이걸로 단1번의 DB연결과 쿼리문 실행으로 모두에게 알림을 보낼 수 있게되었다.

     

    해당작업의 수행시간을 1/100이하로 줄인셈이다.

Designed by Tistory.