트러블슈팅

조회 속도 개선 (쿼리최적화)

dddzr 2025. 4. 6. 21:30

🎯 문제

쪽지함 기능 개발 후 쪽지 목록을 조회 할 때 데이터가 적을 땐 문제를 느끼지 못 했는데, 10만건 이상 데이터 삽입 후 테스트 했을 때 조회 속도가 느려졌다. 조회 속도를 개선해보자!! 😊

전체 조회 개선, 검색 개선 -> 페이징 개선 으로 진행함.

급하면 결론만 보기!!

📌 화면 (보낸 쪽지 목록)

✔ 페이지 진입 시 최신순으로 조회.

✔ 검색(필터)조건에 따라 조회.

✔ 페이지 이동 시 한 페이지 개수 설정만큼 가져옴.

 

📌 속도 측정 방법

✅ 애플리케이션 개발자 도구 네트워크 응답을 측정

조회 개수, 검색 조건을 설정할 수 있는데, 조회 개수 10개 고정하고 전체 조회와 수신자(받은 사람) 검색 시간을 측정했다.

  • 내가 보낸 쪽지 10만건 + 다른 쪽지 x
  • 내가 보낸 쪽지 20만건 + 다른 쪽지 10만건

 

✔ 참고

  • 다른 쪽지: 다른 유저가 보낸 쪽지로 조회화면에 나오지 않는 데이터 (발신자 ID로 조회)
  • 위 조건에서 개수는 정확히 10만건 x, 100개 미만의 데이터가 더 있었지만 무시했다!! (테스트를 혼자만 할 수 있는 환경이 아니라서..)
  • 속도는 2가지 경우에서 측정하지 못 하거나 캡쳐를 못 한 것도 있다. 😜

✅ app에 반영 전 DBeaver 툴에서 쿼리 속도 비교

SET profiling = 1;

SHOW PROFILES;

애플리케이션에서 페이지 로드, 검색 시

조건에 맞는 데이터 개수 + 실제 데이터 가져오는 쿼리 2개가 실행되는데

디피 툴에서 쿼리 2개 각각 실행시간 측정해서 더한 것 = 개발자 도구 응답 시간 얼추 일치했다.

  • 측정 예시 (데이터: 보낸 쪽지 20만건 + 다른 쪽지 10만건)

1. Exists 사용

1-1. 수신자 검색 개수 -  3.3s

SELECT COUNT(DISTINCT n.note_id)

	FROM notes n

    LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

	LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

    WHERE n.regt_id = 'user1'

    AND EXISTS (

	                    SELECT 1 FROM users u_sub

	                    WHERE u_sub.user_id = ntm2.user_id

	                    AND u_sub.user_nm_kr LIKE '%용%'

	                )

 

1.2 수신자 검색 - 7.13s

 

SELECT

    	n.note_id, n.title, n.content, n.regt_id, n.regt_dt

    	, ntm.trans_id, ntm.trans_type, ntm.read_yn, COUNT(nam.attach_id) as attach_cnt

    	, GROUP_CONCAT(DISTINCT u2.user_nm_kr) AS recv_nms

    FROM notes n

    LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

    LEFT JOIN note_attach_mgmt nam ON n.note_id = nam.attach_id

	LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

	LEFT JOIN users u2 ON ntm2.user_id = u2.user_id

    WHERE n.regt_id = 'user1'

    AND EXISTS (

	                    SELECT 1 FROM users u_sub

	                    WHERE u_sub.user_id = ntm2.user_id

	                    AND u_sub.user_nm_kr LIKE '%용%'

	                )

    GROUP BY n.note_id

    ORDER BY n.note_id desc

 

2. Exists 미사용

2-1. 수신자 검색  - 5.1s

SELECT COUNT(DISTINCT n.note_id)

	FROM notes n

    LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

	LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

	LEFT JOIN users u2 ON ntm2.user_id = u2.user_id

    WHERE n.regt_id = 'user1'	

    AND u2.user_nm_kr LIKE '%용%'

 

2.2 수신자 검색 - 9.2s

SELECT

    	n.note_id, n.title, n.content, n.regt_id, n.regt_dt

    	, ntm.trans_id, ntm.trans_type, ntm.read_yn, COUNT(nam.attach_id) as attach_cnt

    	, GROUP_CONCAT(DISTINCT u2.user_nm_kr) AS recv_nms

    FROM notes n

    LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

    LEFT JOIN note_attach_mgmt nam ON n.note_id = nam.attach_id

	LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

	LEFT JOIN users u2 ON ntm2.user_id = u2.user_id

    WHERE n.regt_id = 'user1'

    AND u2.user_nm_kr LIKE '%용%'

    GROUP BY n.note_id

    ORDER BY n.note_id desc

📌 조회 속도 개선 과정

✅1. 초기 쿼리 (GROUP_CONCAT, LEFT JOIN, 서브쿼리 사용)

🔹 조회 속도

  • (보낸 쪽지 10만건 + 다른 쪽지 x) 전체: 17.71s
  • (보낸 쪽지 20만건 + 다른 쪽지 10만건) 전체**: 38.83s**

 

🔹 문제점

GROUP_CONCAT, LEFT JOIN과 서브쿼리 다중 사용으로 인해 데이터가 많아질수록 선형적으로 성능 저하 발생

🔹 초기 쿼리

<!-- 보낸 쪽지 개수 -->

<select id="selectNoteByRegtIdTotalCnt">

	SELECT COUNT(DISTINCT n.note_id)

	FROM notes n

 LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id

	LEFT JOIN (

	 SELECT

	 n2.note_id,

	 GROUP_CONCAT(ntm2.user_id) AS recv_ids,

	 GROUP_CONCAT(u2.user_nm_kr) AS recv_nms

	 FROM

	 notes n2

	 LEFT JOIN

	 note_trans_mgmt ntm2 ON ntm2.note_id = n2.note_id AND ntm2.trans_type = '2'

		LEFT JOIN users u2

			ON ntm2.user_id = u2.user_id

	 GROUP BY

	 n2.note_id

	) AS recvs ON n.note_id = recvs.note_id

 WHERE n.regt_id = #{regtId} AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

	<if test="keyword != null and keyword != ''">

	 <choose>

	 <when test="searchOption == 'recv'">

	 AND LOWER(recvs.recv_nms) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 </when>

	 <when test="searchOption == 'titl'">

	 AND LOWER(n.title) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 </when>

	 <when test="searchOption == 'cont'">

	 AND LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 </when>

	 <otherwise>

	 AND (

	 LOWER(recvs.recv_nms) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 OR LOWER(n.title) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 OR LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 )

	 </otherwise>

	 </choose>

	</if>

</select>

<!-- 보낸 쪽지 -->

<select id="selectNoteListByRegtId">

 SELECT

 	n.note_id, n.title, n.content, n.regt_id, n.regt_dt

 	, tu.user_nm_kr as userNm, ntm.trans_id, ntm.trans_type, ntm.read_yn

	, COUNT(nam.attach_id) as attach_cnt

 	, recvs.recv_ids, recvs.recv_nms

 FROM notes n

 LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id

 LEFT JOIN note_attach_mgmt nam ON n.note_id = nam.attach_id

	LEFT JOIN users tu ON n.regt_id = tu.user_id

	LEFT JOIN (

	 SELECT

	 n2.note_id,

	 GROUP_CONCAT(ntm2.user_id) AS recv_ids,

	 GROUP_CONCAT(u2.user_nm_kr) AS recv_nms

	 FROM

	 notes n2

	 LEFT JOIN

	 note_trans_mgmt ntm2 ON ntm2.note_id = n2.note_id AND ntm2.trans_type = '2'

		LEFT JOIN users u2

			ON ntm2.user_id = u2.user_id

WHERE n2.regt_id = #{regtId}

	 GROUP BY

	 n2.note_id

	) AS recvs ON n.note_id = recvs.note_id

 WHERE n.regt_id = #{regtId} AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

	<if test="keyword != null and keyword != ''">

	 <choose>

	 <when test="searchOption == 'recv'">

	 AND LOWER(recvs.recv_nms) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 </when>

	 <when test="searchOption == 'titl'">

	 AND LOWER(n.title) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 </when>

	 <when test="searchOption == 'cont'">

	 AND LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 </when>

	 <otherwise>

	 AND (

	 LOWER(recvs.recv_nms) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 OR LOWER(n.title) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 OR LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	 )

	 </otherwise>

	 </choose>

	</if>

 GROUP BY n.note_id

 ORDER BY n.note_id DESC

 LIMIT #{pageSize} OFFSET #{startIndex}

</select>

✅2. 서브쿼리 제거

🔹 개선

  • 서브쿼리 (recvs) 를 제거하고, tbn_noterecvmgmt 을 메인 쿼리에 직접 JOIN 하는 방식으로 변경. -> 불필요한 임시 테이블 생성 제거
  • 데이터 필터링이 먼저 실행됨 (WHERE n.regt_id = ?)
  • JOIN 후 바로 GROUP_CONCAT 적용불필요한 중복 연산 감소

🔹 수정된 쿼리

LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

LEFT JOIN users u2 ON ntm2.user_id = u2.user_id

//…WHERE

AND LOWER(u2.user_nm_kr) LIKE CONCAT('%', LOWER(#{keyword}), '%')

🔹 조회 속도

  • (보낸 쪽지 10만건 + 다른 쪽지 x) 전체: 3.06s, 수신자 검색: 15.93s
  • (보낸 쪽지 20만건 + 다른 쪽지 10만건) 전체: 9.76s, 수신자 검색: 25.93s

 

 

위부터 전체 조회, 전체 검색, 제목 검색, 내용 검색, 받는 사람 검색

🔹 문제점

  • 서브쿼리를 제거하면서 조회 성능이 크게 향상됨
  • 하지만 여전히 받는 사람(recv) 검색은 LIKE로 인해 Full Table Scan 발생 가능, 데이터 늘어나면 조회 속도 배로 증가

⭐ 전체 검색이 받는 사람 검색보다 빠르다?

➡️ OR 조건 때문!!

  • 제목(title)이나 내용(content)에서 검색어를 찾으면, 뒤에 실행하지 않고  TRUE가 되어 결과에 포함됨
  • 옵티마이저가 자동으로 순서 최적화 해준다고 하는데, 혹시 몰라서 오래걸리는 u2.user_nm_kr조건을 뒤쪽에 배치했다.

✅ 3. EXISTS 사용 (u2 조인제거)

🔹개선

  • 기존 JOIN에서 LIKE를 사용하면 JOIN 이후 전체 데이터에서 문자열 검색을 수행해야 하므로 비효율적
  • EXISTS를 사용하면 필요한 레코드가 존재하는지만 확인하여 불필요한 데이터 조회 방지
  • JOIN을 사용하면 전체 u2.user_nm_kr 테이블을 스캔한 후 LIKE를 적용해야 하지만, EXISTS는 매칭되는 데이터가 있으면 바로 종료되므로 불필요한 데이터 스캔을 줄일 수 있음.

🔹 수정된 쿼리

AND EXISTS (

SELECT 1 FROM users u_sub

WHERE u_sub.user_id = ntm2.user_id

AND u_sub.user_nm_kr LIKE CONCAT('%', #{keyword}, '%')

)

🔹 조회 속도

(보낸 쪽지 20만건 + 다른 쪽지 10만건) 전체: 3.03s, 수신자 검색: 13.47s

 

✅4. 최종: Where조건 위치 수정, lower함수 제거, 불필요 컬럼 제거(userNm)

🔹 개선

  • WHERE 조건을 Join-on으로 옮김 → 필터링을 먼저 수행하고, 이후 연산을 진행
  • LOWER() 함수 제거 → 불필요 연산 제거(한국어만 들어가는 컬럼이다!!)
  • 불필요한 컬럼(userNm - 보낸사람이름) 제거 → 쿼리 처리량 감소

🔹수정된 쿼리

LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

🔹 조회 속도

(20만건+다른 유저10만건 기준) 전체 조회: 3.17s, 보낸 사람 검색: 11.09s

 

⭐ 참고: 미적용된 방법

✅ 인덱스 추가

인덱스는 user_nm_kr에 추가하고 %text% -> text%로 바꿔 보았지만 사용되지x, force index 사용시 오히려 성능 감소. → 인덱스를 강제할 경우 범위 검색(range scan)이 발생하면서 불필요한 검색이 증가할 수 있음

like 검색 편의성을 위해 제외함.

기존

1	SIMPLE	u2	ref	PRIMARY,idx_user_name	PRIMARY	162	skku.ntm2.user_id	1	Using where

LEFT JOIN users u2 FORCE INDEX (idx_user_name)ON ntm2.user_id = u2.user_id 적용 시

1	SIMPLE	u2	range	idx_user_name	idx_user_name	323		1463	Using where; Using index; Using join buffer (flat, BNL join)

✅ VARCHAR → CHAR

한국 사람 이름 글자 수 제한 정책 없어서 반영하지 않음.

특징 CHAR(n) VARCHAR(n)

길이 고정 길이 (n 바이트) 가변 길이 (데이터 길이 + 1~2바이트)
공백 처리 짧으면 공백( )으로 패딩됨 저장 시 공백 제거됨
속도 고정 길이라 정렬 및 검색이 빠를 수 있음 길이 확인 과정이 있어 약간의 오버헤드 발생 가능
공간 효율성 길이가 짧을수록 공간 낭비 발생 가능 필요한 만큼만 저장하여 공간 효율적
적합한 경우 길이가 일정한 데이터 (예: 국가 코드, 성별 등) 길이가 다양한 문자열 저장 시 유리

✅ FULLTEXT,GRAM TABLE

FULLTEXT,GRAM TABLE 등은 한 글자, %text% 검색이 필요해서 반영x.

✅ @Transactional(readOnly = true)

속도 측정에서 차이x.

// 보낸 쪽지 목록 전체 카운트

@Transactional(readOnly = true)

public int getSendNotesTotalCnt(Criteria cri) {

return noteMapper.selectNoteByRegtIdTotalCnt(cri);

}

// 보낸 쪽지 목록 리스트 (페이징)

@Transactional(readOnly = true)

public List<NoteResponseDTO> getSendNotes(Criteria cri) {

return noteMapper.selectNoteListByRegtId(cri);

}

✅ 비동기 처리

실시간 최신성이 중요한 서비스 특성을 고려해 제외.

🚀 결론

🔹최종 쿼리

✔ 서브쿼리 제거 → JOIN을 이용

✔ EXISTS 사용 → 무조건 더 빠르지 x, 쿼리 속도를 비교해보고 적용할 것!!

✔ 불필요 함수, 컬럼 제거

<!-- 보낸 쪽지 개수 -->

<select id="selectNoteByRegtIdTotalCnt">

	SELECT COUNT(DISTINCT n.note_id)

	FROM notes n

    LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

	LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

    WHERE n.regt_id = #{regtId}

	<if test="keyword != null and keyword != ''">

	    <choose>

	        <when test="searchOption == 'recv'">

	            AND EXISTS (

	                    SELECT 1 FROM users u_sub

	                    WHERE u_sub.user_id = ntm2.user_id

	                    AND u_sub.user_nm_kr LIKE CONCAT('%', #{keyword}, '%')

	                )

	        </when>

	        <when test="searchOption == 'titl'">

	            AND LOWER(n.title) LIKE CONCAT('%', #{keyword}, '%')

	        </when>

	        <when test="searchOption == 'cont'">

	            AND LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	        </when>

	        <otherwise>

	            AND (		

	                LOWER(n.title) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	                OR LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	                OR EXISTS (

	                    SELECT 1 FROM users u_sub

	                    WHERE u_sub.user_id = ntm2.user_id

	                    AND u_sub.user_nm_kr LIKE CONCAT('%', #{keyword}, '%')

	                )

	            )

	        </otherwise>

	    </choose>

	</if>

</select>

<!-- 보낸 쪽지 -->

<select id="selectNoteListByRegtId">

    SELECT

    	n.note_id, n.title, n.content, n.regt_id, n.regt_dt

    	, ntm.trans_id, ntm.trans_type, ntm.read_yn, COUNT(nam.attach_id) as attach_cnt

    	, GROUP_CONCAT(DISTINCT u2.user_nm_kr) AS recv_nms

    FROM notes n

    LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

    LEFT JOIN note_attach_mgmt nam ON n.note_id = nam.attach_id

	LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

	LEFT JOIN users u2 ON ntm2.user_id = u2.user_id

    WHERE n.regt_id = #{regtId}

	<if test="keyword != null and keyword != ''">

	    <choose>

	        <when test="searchOption == 'recv'">

	            AND EXISTS (

	                    SELECT 1 FROM users u_sub

	                    WHERE u_sub.user_id = ntm2.user_id

	                    AND u_sub.user_nm_kr LIKE CONCAT('%', #{keyword}, '%')

	                )

	        </when>

	        <when test="searchOption == 'titl'">

	            AND LOWER(n.title) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	        </when>

	        <when test="searchOption == 'cont'">

	            AND LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	        </when>

	        <otherwise>

	            AND (		

	                LOWER(n.title) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	                OR LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	                OR EXISTS (

	                    SELECT 1 FROM users u_sub

	                    WHERE u_sub.user_id = ntm2.user_id

	                    AND u_sub.user_nm_kr LIKE CONCAT('%', #{keyword}, '%')

	                )

	            )

	        </otherwise>

	    </choose>

	</if>

    GROUP BY n.note_id

    ORDER BY n.note_id DESC

    LIMIT #{pageSize} OFFSET #{startIndex}

</select>

📌 페이징 속도 개선

🔹 문제점

첫 페이지 조회 속도는 문제가 없었지만, 뒷 페이지로 이동 시 오래 걸림

✅ LIMIT과 OFFSET 작동 방식

LIMIT #{pageSize} OFFSET #{startIndex}

✔ LIMIT : 가져올 행(row)의 개수를 지정

✔ OFFSET : 건너뛸 행(row)의 개수를 지정

🔥 왜 느려질까?

1️⃣ DB는 OFFSET만큼의 데이터를 먼저 읽고 버려야 함

2️⃣ OFFSET이 클수록 버리는 데이터가 많아지고, 디스크 I/O 증가

즉, OFFSET은 앞의 데이터를 다 읽고 건너뛴 후 결과를 반환하므로 쿼리 성능이 저하된다. 😢

🔹 개선

  • OFFSET을 제거

✅ ID 기반 페이징

SELECT * FROM 테이블

WHERE note_id < #{마지막_note_id}

ORDER BY note_id DESC

LIMIT 10;

이전 페이지의 마지막 ID(note_id)를 기준으로 다음 데이터를 가져오기 때문에 속도가 빠름!

INDEX를 활용하므로 OFFSET보다 훨씬 효율적!

 

⭐ 참고: 미적용 방법

✅ 서브쿼리로 OFFSET 최적화

SELECT * FROM 테이블

WHERE note_id IN (

SELECT note_id FROM 테이블 ORDER BY note_id DESC LIMIT 10 OFFSET 1000

);

OFFSET이 큰 경우, 먼저 ID만 찾고 최적화할 수 있음

✅ 페이징을 캐싱하기

✔  같은 쿼리가 반복되면 Redis 같은 캐시 활용

🚀 결론

✔ OFFSET대신 마지막 조회 데이터 ID를 기준으로 조회

✔ 프론트엔드에서 요소에 데이터 ID를 가지고 있다가 보내주는 처리 필요함!!

🔹 조회 속도

첫 페이지 조회 속도, 마지막 페이지 조회 속도

 

🔹최종 쿼리

<select id="selectNoteListByRegtId">

    SELECT

    	n.note_id, n.title, n.content, n.regt_id, n.regt_dt

    	, ntm.trans_id, ntm.trans_type, ntm.read_yn, COUNT(nam.attach_id) as attach_cnt

    	, GROUP_CONCAT(DISTINCT u2.user_nm_kr) AS recv_nms

    FROM notes n

    LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

    LEFT JOIN note_attach_mgmt nam ON n.note_id = nam.attach_id

	LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

	LEFT JOIN users u2 ON ntm2.user_id = u2.user_id

    WHERE n.regt_id = #{regtId}

    <if test="lastSno != 0">

    	AND n.note_id <![CDATA[<]]> #{lastSno}

    </if>	

	<if test="keyword != null and keyword != ''">

	    <choose>

	        <when test="searchOption == 'recv'">

	            AND EXISTS (

	                    SELECT 1 FROM users u_sub

	                    WHERE u_sub.user_id = ntm2.user_id

	                    AND u_sub.user_nm_kr LIKE CONCAT('%', #{keyword}, '%')

	                )

	        </when>

	        <when test="searchOption == 'titl'">

	            AND LOWER(n.title) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	        </when>

	        <when test="searchOption == 'cont'">

	            AND LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	        </when>

	        <otherwise>

	            AND (		

	                LOWER(n.title) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	                OR LOWER(n.content) LIKE CONCAT('%', LOWER(#{keyword}), '%')

	                OR EXISTS (

	                    SELECT 1 FROM users u_sub

	                    WHERE u_sub.user_id = ntm2.user_id

	                    AND u_sub.user_nm_kr LIKE CONCAT('%', #{keyword}, '%')

	                )

	            )

	        </otherwise>

	    </choose>

	</if>

    GROUP BY n.note_id

    ORDER BY n.note_id DESC

    LIMIT #{pageSize}

</select>

  • 속도 비교
# offset 이용 - 5.694s
SELECT

    	n.note_id, n.title, n.content, n.regt_id, n.regt_dt

    	, ntm.trans_id, ntm.trans_type, ntm.read_yn, COUNT(nam.attach_id) as attach_cnt

    	, GROUP_CONCAT(DISTINCT u2.user_nm_kr) AS recv_nms

    FROM notes n

    LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

    LEFT JOIN note_attach_mgmt nam ON n.note_id = nam.attach_id

	LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

	LEFT JOIN users u2 ON ntm2.user_id = u2.user_id

    WHERE n.regt_id = 'user1'

    GROUP BY n.note_id

    ORDER BY n.note_id desc

    limit 10 offset 100000

# ID기반 조회 - 0.479s

SELECT

    	n.note_id, n.title, n.content, n.regt_id, n.regt_dt

    	, ntm.trans_id, ntm.trans_type, ntm.read_yn, COUNT(nam.attach_id) as attach_cnt

    	, GROUP_CONCAT(DISTINCT u2.user_nm_kr) AS recv_nms

    FROM notes n

    LEFT JOIN note_trans_mgmt ntm ON n.note_id = ntm.note_id AND ntm.trans_type = "1" AND ntm.use_yn = "Y"

    LEFT JOIN note_attach_mgmt nam ON n.note_id = nam.attach_id

	LEFT JOIN note_trans_mgmt ntm2 ON n.note_id = ntm2.note_id AND ntm2.trans_type = '2'

	LEFT JOIN users u2 ON ntm2.user_id = u2.user_id

    WHERE n.regt_id = 'user1' and n.note_id < '291276'

    GROUP BY n.note_id

    ORDER BY n.note_id desc

    limit 10