<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ShootForTheMoon</title>
    <link>https://lealea.tistory.com/</link>
    <description>Shoot for the moon!</description>
    <language>ko</language>
    <pubDate>Fri, 10 Jul 2026 17:22:05 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Lea Hwang</managingEditor>
    <image>
      <title>ShootForTheMoon</title>
      <url>https://tistory1.daumcdn.net/tistory/5302154/attach/c4c09ef2a56c4b379fd08b9aa38d5c76</url>
      <link>https://lealea.tistory.com</link>
    </image>
    <item>
      <title>[CS 스터디] 5주간의 데이터베이스 스터디 회고</title>
      <link>https://lealea.tistory.com/387</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;JSCODE 모의면접 신청 계기&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;스터디 진행 방식&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;잘한 점&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;아쉬운 점&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;앞으로는 이렇게&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;JSCODE 모의면접 신청 계기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;1월은 나태해지기 쉬운 시기라 생각한다. 혼자 CS 공부를 한다고 했지만, 흐지부지될 가능성이 있어 모의면접 스터디를 신청하게 되었다. 특히 데이터베이스 관련 공부를 하고 프로젝트에 적용하고 있는 중이었어서 더 심화된 부분을 학습하고 싶었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;또한, 글로 된 지식에서 벗어나 상대방에게 말로 전달하는 능력도 기르고 싶었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;스터디 진행 방식&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;매주 1번 비대면으로 만나서 4명이서 진행하였다. 1명은 면접자 2명은 면접관 1명은 타임키퍼였다. 15분 동안 질문과 답변을 진행하고 5분 동안은 피드백을 하면서 잘한 점과 개선할 점을 정리해서 공유하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;2564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Yani/btsL5lqTZih/85wiSSVHxsa908s8loaqT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Yani/btsL5lqTZih/85wiSSVHxsa908s8loaqT1/img.png&quot; data-alt=&quot;기본 개념과 꼬리질문을 정리한 후 발표하는 시간을 가졌다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Yani/btsL5lqTZih/85wiSSVHxsa908s8loaqT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Yani%2FbtsL5lqTZih%2F85wiSSVHxsa908s8loaqT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1306&quot; height=&quot;2564&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;2564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기본 개념과 꼬리질문을 정리한 후 발표하는 시간을 가졌다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;잘한 점&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;스터디에 한 번도 빠지지 않고 참여했다. 스터디 시간이 늦은 저녁이라 피곤도 하고 내용 자체가 적지 않았기에 하루 정도는 쉴까라는 생각도 하고 대충할까라는 생각도 했지만 그래도 하는 김에 완벽히 하고 싶었던 것 같다. 처음 스터디를 신청할 때의 마음가짐을 잊지 않고 5주간 임한 점을 스스로 칭찬하고 싶다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;아쉬운 점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;질문을 더 많이할걸 후회가 남는다. 물론 내가 가장 많이 질문했지만 그럼에도 더 많이 연구하고 실험하고 궁금한 부분을 질문드릴 걸 하는 아쉬움이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;앞으로는 이렇게&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;어느정도 이론적인 깊이는 쌓였다고 생각한다. 하지만 요즘 혼자 있고 작업하는 시간이 많아서 그런지 한국말을 하는 데 어려움이 있다. 말로 상대방을 설득시키는 역량도 향상하고 싶다고 느낀다. 그래서 바로 이어서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;통화로 매일 모의 면접을 진행하는 스터디에 신청했는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;... 아직 결과는 나오지 않았다. 예전에 전화영어를 몇 달한 적이 있는데 매일 말을 하니까 술술 나왔던 좋은 경험이 있어 기대를 하고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;잘한 점, 아쉬운 점, 개선점이 보이는 스터디였다. 이 또한 내가 노력한 결과라고 생각한다. 앞으로도 부족한 점을 보완하면서 성장하는 개발자가 되고 싶다!&lt;/p&gt;</description>
      <category>CS/Database</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/387</guid>
      <comments>https://lealea.tistory.com/387#entry387comment</comments>
      <pubDate>Mon, 3 Feb 2025 20:00:49 +0900</pubDate>
    </item>
    <item>
      <title>1:1 채팅 기능 개선 (feat. NoSQL 및 비동기 로직 적용)</title>
      <link>https://lealea.tistory.com/377</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;프론트엔드와 함께하는 프로젝트에서 1:1 채팅 기능 개발을 맡았다. 기본 기능을 구현한 후, 남은 3일 동안 가능한 리팩토링 방안을 고민했고, 최종적으로 데이터베이스를 &lt;span style=&quot;color: #ef6f53;&quot;&gt;RDBMS에서 NoSQL로 변경하고(채팅 저장, 조회)&lt;/span&gt; 추가적으로 &lt;span style=&quot;color: #ef6f53;&quot;&gt;채팅 저장 로직을 비동기로 전환&lt;/span&gt;하기로 결정했다. 아래는 개선 과정에서 고려했던 주요 사항들을 기록으로 남겨보려 한다. 부족한 부분이 있겠지만 기한안에 완성을 목표로 했다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;채팅 설계&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;저장소 (MySQL &amp;rarr; MongoDB &amp;rarr; 아카이빙)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; 어떤 데이터베이스를 쓸 것인가?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;&amp;nbsp; AS-IS : MySQL&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;&amp;nbsp; TO-BE : 키-값 저장소로 변경(MongoDB)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; 그럼에도, 아카이빙&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동기 &amp;rarr; 비동기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;&amp;nbsp; AS-IS : 동기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;&amp;nbsp; TO-BE : 비동기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 스프링 이벤트&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 외부 브로커(메시지 큐)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 스프링 이벤트를 선택한 이유&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;성능 테스트&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; 확인 할 포인트&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; 아틀러리 툴을 사용해 성능 테스트 진행&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;&amp;nbsp; Case 1. RDBMS vs 스프링 이벤트&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;&amp;nbsp; Case 2. RDBMS &amp;rarr; NoSQL&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;최종 확인 (스프링 이벤트 + NoSQL 적용)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;채팅 설계&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;조건&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt; 응답 시간이 낮은 1:1 채팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;텍스트만 주고 받을 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;채팅 이력은 영원히 보관 &amp;rarr; 아카이빙&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;웹소켓 연결(양방향)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;처음에는 HTTP 연결이지만 특정 핸드셰이크 절차를 거쳐 웹 소켓 연결로 업그레이드된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;연결 후에는 서버는 클라이언트에게 비동기적으로 메시지를 전송할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;상태 유지가 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; - 각 클라이언트가 채팅 서버와 독립적인 네트워크 연결을 유지해야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; - 클라이언트는 보통 서버가 살아있는 한 다른 서버로 연결을 변경하지 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;추가로 확인 할 부분&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;트래픽 규모가 얼마 되지 않을 때는 모든 기능을 서버 한 대로 구현할 수 있다. 그렇다면 서버 한 대로 얼마나 많은 접속을 동시에 허용할 수 있는지는 모니터링이 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;저장소 (MySQL &amp;rarr; MongoDB &amp;rarr; 아카이빙)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;어떤 데이터베이스를 쓸 것인가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #6164c6;&quot;&gt;데이터의 유형&lt;/span&gt;과 &lt;span style=&quot;color: #6164c6;&quot;&gt;읽기/쓰기 연산의 패턴&lt;/span&gt;을 고려하여 결정한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;채팅 시스템에서 다루는 데이터는 크게 두 가지이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(1) 사용자 프로파일, 설정, 친구 목록과 같은 일반적인 데이터는 &amp;rarr; 안정성을 보장하는 관계형 데이터베이스에 보관한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #6164c6;&quot;&gt;(2) 채팅 이력(읽기/쓰기 연산 패턴)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;페이스북이나 왓츠앱은 매일 600억 개의 메시지를 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;주로 최근에 주고받은 메시지를 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;하지만 검색이나 특정 메시지로 점프하는 경우도 데이터 계층에서 지원해야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;1:1 채팅 앱의 경우 읽기:쓰기 비율은 대략 1:1&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 이러한 이유로 키-값 저장소로 변경하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AS-IS : MySQL&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TO-BE : 키-값 저장소로 변경(MongoDB)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;수평적 규모 확장이 쉽다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;데이터 접근 지연시간이 낮다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;관계형 데이터베이스는 인덱스가 커지면 데이터에 대한 무작위 접근을 처리하는 비용이 늘어난다&lt;br /&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;많은 안정적인 채팅 서비스가 키-값 저장소를 쓰고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메시지 데이터를 어떻게 보관할 것인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;메시지 ID는 고유하고 시간 순서와 일치해야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;RDBMS는 auto_increment가 대안&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;하지만, NoSQL은 보통 해당 기능을 제공하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; - 지역적 순서 번호 생성기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; - 지역적이라 함은 ID의 유일성은 같은 그룹 안에서만 보증하면 충분&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; - 왜 나면, 1:1 채팅 세션 안에서만 유지하면 되니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #6164c6;&quot;&gt;➡️ 채팅 세션 내 유일성을 보장하기 위해 chatRoomId_sendTime 복합 인덱스 적용&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;db.chat_messages.getIndexes()&lt;/p&gt;
&lt;pre id=&quot;code_1733466670999&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[
  {
    &quot;key&quot;: {
      &quot;_id&quot;: 1
    },
    &quot;name&quot;: &quot;_id_&quot;,
    &quot;v&quot;: 2
  },
  {
    &quot;key&quot;: {
      &quot;chatRoomId&quot;: 1,
      &quot;sendTime&quot;: -1
    },
    &quot;name&quot;: &quot;chatRoomId_sendTime_idx&quot;,
    &quot;v&quot;: 2
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;마지막은, 아카이빙&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;모든 데이터를 메모리에 해시 테이블 O(1)로 저장한다고 해도 메모리 최적화가 여전히 필요하다. 다음과 같은 개선 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;데이터 압축(compression)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 특정 시점이&amp;nbsp;&lt;/span&gt;&lt;/span&gt;지난 데이터는 디스크나 AWS 별도의 스토리지로 아카이빙&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;동기 &amp;rarr; 비동기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AS-IS : 동기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;웹소켓을&lt;span&gt; &lt;/span&gt;통해&lt;span&gt; &lt;/span&gt;채팅&lt;span&gt; &lt;/span&gt;메시지를&lt;span&gt; &lt;/span&gt;받을&lt;span&gt; &lt;/span&gt;때마다&lt;span&gt; &lt;/span&gt;동기적으로&lt;span&gt; &lt;/span&gt;데이터베이스에&lt;span&gt; &lt;/span&gt;저장&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;성능 테스트는 H2 대신 MySQL을 사용해 진행했습니다.&lt;br /&gt;(H2는 메모리 기반이라 정확성이 떨어질 수 있어, 로컬 환경에서 MySQL을 적용해 테스트를 진행함)&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;b&gt;TO-BE : 비동기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;스프링 이벤트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;외부 브로커&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; - Kafka&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; - RabbitMQ&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;스프링 이벤트&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링 애플리케이션에서 이벤트를&amp;nbsp;발행/구독할&amp;nbsp;수 있는 기능, 메시지 무손실을 보장하지 않고, 확장성에 제한적이나 특별한 설정이 없는 간편함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;ApplicationEventPublisher&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;EventListener&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;외부 브로커(메시지 큐)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XexBe/btsLaLWM8Y5/e4hKjXu8KLfORDjc9iR2pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XexBe/btsLaLWM8Y5/e4hKjXu8KLfORDjc9iR2pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XexBe/btsLaLWM8Y5/e4hKjXu8KLfORDjc9iR2pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXexBe%2FbtsLaLWM8Y5%2Fe4hKjXu8KLfORDjc9iR2pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1430&quot; height=&quot;198&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;생산자 또는 발행자(producer / publisher)라고 불리는 입력 서비스가 메시지를 만들어 메시지 큐에 발행(publish)한다. 큐에는 보통 소비자 혹은 구독자(consumer / subscriber)라 불리는 서비스 혹은 서버가 연결되어 있는데, 메시지를 받아 동작을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;특징&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;메시지의 무손실(durability)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; - 메시지 큐에 일단 보관된 메시지는 소비자가 꺼낼 때까지 안전히 보관된다는 특성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;비동기 통신 지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp; &amp;nbsp; - 메시지의 버퍼 역할을 하면서 비동기적으로 전송&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;장점&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;서비스 또는 서버 간 결합이 느슨해져 규모 확장성이 보장되어야 하는 애플리케이션 구성에 좋다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;생산자는 소비자 프로세스가 다운되어 있어도 메시지를 발행할 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;소비자는 생산자 서비스가 가용한 상태가 아니더라도 메시지를 수신할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스프링 이벤트를 선택한 이유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현재는 모놀리식 구조이기 때문이다. 외부 브로커(예: Kafka, RabbitMQ)를 도입하는 것은 멀티 모듈 환경에서 더 적합하다고 생각하고, 현재 구조에서는 오버 엔지니어링이라고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;성능 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;확인할 포인트&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;RDBMS로 메시지 저장(동기), 메세지 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NoSQL로 메세지 저장, 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이벤트 메세지 저장(비동기)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아틀러리 툴을 사용해 성능 테스트 진행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;artillery-load-test-event.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1733463435915&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;config:
  target: 'http://localhost:8090'  # 테스트 대상 서버 URL
  phases:
    - duration: 10  # 해당 단계의 지속 시간 (초)
      arrivalRate: 5  # 초당 도달하는 가상 사용자 수
      name: Warm up  # 첫 번째 단계 이름 (서버 준비 단계)

    - duration: 10  # 해당 단계의 지속 시간 (초)
      arrivalRate: 20  # 초당 도달하는 가상 사용자 수
      name: Ramp up load  # 두 번째 단계 이름 (부하 증가 단계)

    - duration: 30  # 해당 단계의 지속 시간 (초)
      arrivalRate: 100  # 초당 도달하는 가상 사용자 수
      name: Sustained load  # 세 번째 단계 이름 (지속적인 부하 단계)

    - duration: 10  # 해당 단계의 지속 시간 (초)
      arrivalRate: 20  # 초당 도달하는 가상 사용자 수
      name: End of load  # 네 번째 단계 이름 (부하 종료 단계)

scenarios:
  - name: 'event chatting message load test'  # 시나리오 이름 (채팅 메시지 부하 테스트)
    flow:
      - post:
          url: '/api/v1/chat/1/logs'  # API 호출 URL (채팅 메시지 로그 전송)
          json:
            message: 'event message test'  # 전송할 메시지 내용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용한 명령어&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;artillery run --output report.json artillery-test.yaml&lt;br /&gt;&amp;bull;&amp;nbsp;성능 테스트를 실행하고, 결과를 report.json 파일에 저장&lt;br /&gt;&lt;br /&gt;artillery report report.json --output report.html&lt;br /&gt;&amp;bull;&amp;nbsp;report.json 파일을 기반으로 HTML 형식의 보고서를 생성하여 report.html로 저장&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;Case 1. RDBMS vs 스프링 이벤트&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;H2는 메모리 기반이라 정확성이 떨어질 수 있어, 로컬 환경에서 MySQL을 적용해 테스트를 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;MySQL 메시지 저장(동기)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;95p: 124ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;99p: 211ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;요청의&lt;span&gt; 99%&lt;/span&gt;는&lt;span&gt; 211ms&lt;/span&gt;이하의&lt;span&gt; &lt;/span&gt;응답&lt;span&gt; &lt;/span&gt;시간을&lt;span&gt; &lt;/span&gt;기록함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2702&quot; data-origin-height=&quot;1544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btc4Rz/btsLaW4TalX/Vr9v007XRSi9uJVKOt7wh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btc4Rz/btsLaW4TalX/Vr9v007XRSi9uJVKOt7wh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btc4Rz/btsLaW4TalX/Vr9v007XRSi9uJVKOt7wh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtc4Rz%2FbtsLaW4TalX%2FVr9v007XRSi9uJVKOt7wh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2702&quot; height=&quot;1544&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2702&quot; data-origin-height=&quot;1544&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;⬇️&amp;nbsp;MySQL적용한 코드&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1733463572484&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://localhost:3306/flowday_dev
    username: [username] 
    password: [password]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatApiController&lt;/p&gt;
&lt;pre id=&quot;code_1733463631976&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 채팅 메세지 저장
 */
@PostMapping(&quot;/{roomId}/logs&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;gt; saveMessage(
        @PathVariable Long roomId,
        @AuthenticationPrincipal SecurityUser user,
        @RequestBody ChatMessage chatMessage
) {
    Long senderId = user.getId();
    LocalDateTime time = LocalDateTime.now();
    //String responseMessage = HtmlUtils.htmlEscape(chatMessage.message());

    chatService.saveMessage(roomId, senderId, chatMessage.message(), time);

    return ResponseEntity.ok(ApiResponse.success(chatMessage.message()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이벤트 메세지 저장(비동기)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;95p: 12ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;99p: 75ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;요청의&lt;span&gt; 99%&lt;/span&gt;는&lt;span&gt; 75ms&lt;/span&gt;이하의&lt;span&gt; &lt;/span&gt;응답&lt;span&gt; &lt;/span&gt;시간을&lt;span&gt; &lt;/span&gt;기록함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2702&quot; data-origin-height=&quot;1566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UqfW5/btsLa0zo22V/BfkjZC4GC0wgK4js41ev2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UqfW5/btsLa0zo22V/BfkjZC4GC0wgK4js41ev2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UqfW5/btsLa0zo22V/BfkjZC4GC0wgK4js41ev2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUqfW5%2FbtsLa0zo22V%2FBfkjZC4GC0wgK4js41ev2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2702&quot; height=&quot;1566&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2702&quot; data-origin-height=&quot;1566&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스프링 이벤트 (refactor/#78) &amp;rarr; PR&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://github.com/prgrms-web-devcourse-final-project/WEB1_2_FlowDay_BE/pull/104&quot;&gt;https://github.com/prgrms-web-devcourse-final-project/WEB1_2_FlowDay_BE/pull/104&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;⬇️ 코드&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Application&lt;/p&gt;
&lt;pre id=&quot;code_1733463746409&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableAsync
@EnableWebSocket
@SpringBootApplication
@EnableJpaAuditing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ChatController&lt;/p&gt;
&lt;pre id=&quot;code_1733463769178&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 웹 소켓 연결
 */
@MessageMapping(&quot;/chat/{roomId}&quot;)
@SendTo(&quot;/topic/rooms/{roomId}&quot;)
public ChatResponse chatting(
        @DestinationVariable Long roomId,
        @AuthenticationPrincipal SecurityUser user,
        ChatMessage chatMessage
) {
    Long senderId = user.getId();
    LocalDateTime time = LocalDateTime.now();
    String responseMessage = HtmlUtils.htmlEscape(chatMessage.message());

    chatService.saveMessage(roomId, senderId, responseMessage, time);
    // 페이지 정보는 웹소켓 연결에서는 의미 없으므로 0으로 설정
    return new ChatResponse(senderId, responseMessage, time, 0, 0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ChatService&lt;/p&gt;
&lt;pre id=&quot;code_1733463802628&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class ChatService {
    private final ChatMessageRepository chatMessageRepository;
    private final ChatRoomRepository chatRoomRepository;
    private final ApplicationEventPublisher applicationEventPublisher;

    /**
     * [비동기 - 스프링 이벤트] 채팅 메세지 저장
     */
    public void saveMessage(
            final Long roomId,
            final Long senderId,
            final String responseMessage,
            final LocalDateTime time
    ) {
        applicationEventPublisher.publishEvent(
                new ChatMessageEvent(roomId, senderId, responseMessage, time)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ChatMessageEvent&lt;/p&gt;
&lt;pre id=&quot;code_1733463829059&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record ChatMessageEvent(
        Long roomId,
        Long senderId,
        String responseMessage,
        LocalDateTime time
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ChatMessageEventHandler&lt;/p&gt;
&lt;pre id=&quot;code_1733463853222&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class ChatMessageEventHandler {
    private final ChatMessageRepository chatMessageRepository;

    @Async
    @TransactionalEventListener
    public void handle(ChatMessageEvent event) {
        ChatMessageEntity chatMessage = ChatMessageEntity.create(
                event.roomId(),
                event.senderId(),
                event.responseMessage(),
                event.time()
        );
        chatMessageRepository.save(chatMessage);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;성능 개선&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;스프링 이벤트 메시지 저장(비동기)은 MySQL 메시지 저장(동기) 대비 99%의 요청에서 &lt;b&gt;응답 시간이 136ms (약 64%) 개선&lt;/b&gt;되었다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;Case 2. RDBMS &amp;rarr; NoSQL&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;채팅&lt;/span&gt; &lt;span&gt;메시지&lt;/span&gt; &lt;span&gt;저장&lt;/span&gt;/&lt;span&gt;조회&lt;/span&gt; RDBMS &amp;rarr; NoSQL(MongoDB) &lt;span&gt;전환&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MongoDB 연결 (refactor/#84) - PR 완료&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://github.com/prgrms-web-devcourse-final-project/WEB1_2_FlowDay_BE/pull/103&quot;&gt;https://github.com/prgrms-web-devcourse-final-project/WEB1_2_FlowDay_BE/pull/103&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;⬇️ 코드&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1733464045813&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  data:
    mongodb:
      uri: mongodb://localhost:27017/flowday_chat_db
      auto-index-creation: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1733464064979&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// mongodb
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ChatApiController&lt;/p&gt;
&lt;pre id=&quot;code_1733464099828&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; /**
 * [NoSQL] 채팅 메세지 저장
 */
@PostMapping(&quot;{roomId}/logs&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;gt; chatting(
        @PathVariable Long roomId,
        @AuthenticationPrincipal SecurityUser user,
        @RequestBody ChatMessage chatMessage
) {
    Long senderId = user.getId();
    LocalDateTime time = LocalDateTime.now();
    String responseMessage = HtmlUtils.htmlEscape(chatMessage.message());

    ChatMessageDocument chatMessageDocument =
            chatService.saveMessage(roomId, senderId, responseMessage, time);
    return ResponseEntity.ok(ApiResponse.success(chatMessageDocument));
}

 /**
 * [NoSQL] 채팅 조회 (최신 10개, page)
 */
@GetMapping(&quot;/{roomId}&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;gt; getPagedChatMessages(
        @PathVariable Long roomId,
        @RequestParam(value = &quot;page&quot;, defaultValue = &quot;1&quot;) int page,
        @RequestParam(value = &quot;size&quot;, defaultValue = &quot;10&quot;) int size
) {
    Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, &quot;sendTime&quot;));
    Page&amp;lt;ChatMessageDocument&amp;gt; messages = chatService.getPagedChatMessages(roomId, pageable);

    int pageNumber = messages.getNumber() + 1;
    List&amp;lt;ChatResponse&amp;gt; chatResponses = messages.getContent().stream()
            .map(message -&amp;gt; ChatResponse.from(message, pageNumber, messages.getTotalPages()))
            .toList();
    return ResponseEntity.ok(ApiResponse.success(chatResponses));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ChatService&lt;/p&gt;
&lt;pre id=&quot;code_1733464164233&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final ChatMessageDocumentRepository chatMessageDocumentRepository;

/**
 * [NoSQL] 채팅 메세지 저장
 */
public ChatMessageDocument saveMessage(
        final Long roomId,
        final Long senderId,
        final String responseMessage,
        final LocalDateTime time
) {
    ChatMessageDocument chatMessageDocument = ChatMessageDocument.create(
            roomId,
            senderId,
            responseMessage,
            time
    );
    return chatMessageDocumentRepository.save(chatMessageDocument);
}

/**
 * [NoSQL] 채팅 조회
 */
public Page&amp;lt;ChatMessageDocument&amp;gt; getPagedChatMessages(
        final Long roomId,
        final Pageable pageable
) {
    return chatMessageDocumentRepository.findByChatRoomId(roomId, pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ChatResponse&lt;/p&gt;
&lt;pre id=&quot;code_1733464134276&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record ChatResponse(
        Long senderId,
        String message,
        LocalDateTime time,
        Integer pageNumber,  // 현재 페이지 번호
        Integer totalPages   // 총 페이지 수
) {
    public static ChatResponse from(
            final ChatMessageDocument chatMessageDocument,
            Integer pageNumber,
            Integer totalPages
    ) {
        return new ChatResponse(
                chatMessageDocument.getFromId(),
                chatMessageDocument.getTextMessage(),
                chatMessageDocument.getSendTime(),
                pageNumber,
                totalPages
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ChatMessgeDocument&lt;/p&gt;
&lt;pre id=&quot;code_1733464189376&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Document(collection = ChatMessageDocument.COLLECTION_NAME)
@CompoundIndexes({
        @CompoundIndex(name = &quot;chatRoomId_sendTime_idx&quot;, def = &quot;{'chatRoomId': 1, 'sendTime': -1}&quot;)
})
public class ChatMessageDocument {
    public static final String COLLECTION_NAME = &quot;chat_messages&quot;;

    @Id
    private ObjectId id;
    private Long chatRoomId;
    private Long fromId;
    private String textMessage;
    private LocalDateTime sendTime;

    @Builder
    public ChatMessageDocument(
            final ObjectId id,
            final Long chatRoomId,
            final Long fromId,
            final String textMessage,
            final LocalDateTime sendTime
    ) {
        this.id = id;
        this.chatRoomId = chatRoomId;
        this.fromId = fromId;
        this.textMessage = textMessage;
        this.sendTime = sendTime;
    }

    public static ChatMessageDocument create(
            final Long chatRoomId,
            final Long fromId,
            final String textMessage,
            final LocalDateTime sendTime
    ) {
        return ChatMessageDocument.builder()
                .chatRoomId(chatRoomId)
                .fromId(fromId)
                .textMessage(textMessage)
                .sendTime(sendTime)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ChatMessageDocumentRepository&lt;/p&gt;
&lt;pre id=&quot;code_1733464207630&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ChatMessageDocumentRepository extends MongoRepository&amp;lt;ChatMessageDocument, ObjectId&amp;gt; {
    Page&amp;lt;ChatMessageDocument&amp;gt; findByChatRoomId(Long chatRoomId, Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;docker-compose.yml&lt;/p&gt;
&lt;pre id=&quot;code_1733464221383&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.8'
services:
  mongodb:
    image: mongo:6.0.3
    container_name: mongodb_container
    volumes:
      - ./db/mongodb/data:/data/db
    ports:
      - &quot;27017:27017&quot;
    networks:
      - flowday
    command: mongod --noauth

networks:
  flowday:&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;채팅 메세지 저장&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXgLsU/btsK9gDLsEG/5qVTDEzCGIpQ1vNF3e8kB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXgLsU/btsK9gDLsEG/5qVTDEzCGIpQ1vNF3e8kB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXgLsU/btsK9gDLsEG/5qVTDEzCGIpQ1vNF3e8kB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXgLsU%2FbtsK9gDLsEG%2F5qVTDEzCGIpQ1vNF3e8kB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;359&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;359&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;채팅 메세지 조회&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;1180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/82top/btsLaDdDfVF/k2W29WvzjuFKakkuW5Do31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/82top/btsLaDdDfVF/k2W29WvzjuFKakkuW5Do31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/82top/btsLaDdDfVF/k2W29WvzjuFKakkuW5Do31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F82top%2FbtsLaDdDfVF%2Fk2W29WvzjuFKakkuW5Do31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1660&quot; height=&quot;1180&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;1180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;최종 확인 (스프링 이벤트 + NoSQL 적용)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;웹소켓 연결 + 채팅 메세지 전송&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boSWfG/btsK8uJwLBk/hzcTEUIcGtuKyDqofeZf41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boSWfG/btsK8uJwLBk/hzcTEUIcGtuKyDqofeZf41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boSWfG/btsK8uJwLBk/hzcTEUIcGtuKyDqofeZf41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboSWfG%2FbtsK8uJwLBk%2FhzcTEUIcGtuKyDqofeZf41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1660&quot; height=&quot;538&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NoSQL 채팅 메세지 저장&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2872&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EWyhn/btsK9ONyDva/w6JKHJKxSRDGESBVEoWn6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EWyhn/btsK9ONyDva/w6JKHJKxSRDGESBVEoWn6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EWyhn/btsK9ONyDva/w6JKHJKxSRDGESBVEoWn6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEWyhn%2FbtsK9ONyDva%2Fw6JKHJKxSRDGESBVEoWn6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2872&quot; height=&quot;246&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2872&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NoSQL 채팅 메세지 조회&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQX3p8/btsK8XdkQLm/vxemdC3SMOTcHbFs1qIdz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQX3p8/btsK8XdkQLm/vxemdC3SMOTcHbFs1qIdz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQX3p8/btsK8XdkQLm/vxemdC3SMOTcHbFs1qIdz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQX3p8%2FbtsK8XdkQLm%2FvxemdC3SMOTcHbFs1qIdz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1664&quot; height=&quot;1166&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고 :&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;[도서] 가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/p&gt;</description>
      <category>Project</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/377</guid>
      <comments>https://lealea.tistory.com/377#entry377comment</comments>
      <pubDate>Fri, 6 Dec 2024 16:06:11 +0900</pubDate>
    </item>
    <item>
      <title>[CS 스터디] 5주간의 네트워크 스터디 회고</title>
      <link>https://lealea.tistory.com/376</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;JSCODE 모의면접 신청 계기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스터디 진행 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;잘한 점&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아쉬운 점&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞으로는 이렇게&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;JSCODE 모의면접 신청 계기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;10월 말쯤 생각을 했을 때 11월 초에는 프로젝트와 이력서, 포트폴리오가 마무리되는 일정이었다. 따라서 11월 한 달 동안은 부족했던 컴퓨터공학 지식을 채우는 데 전념하려 했다. 그 당시 Java, Spring, 운영체제, 네트워크, 자료구조/알고리즘, 데이터베이스 하나씩 나의 지식의 정도를 확인했을 때, 그 중에서 네트워크가 가장 모호하고 개념이 명확하게 잡히지 않는 상태였다. 그래서 한 달 동안은 네트워크에 전념해 전공생과 현직자 분들과 스터디를 함에 있어서 제일 잘하고 싶다는 목표를 세우게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;스터디 진행 방식&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;총 5주간 일주일에 한 번 진행했다. 평균적으로 4명이서 진행했고 1명이 면접자, 2명이 면접관, 나머지 한 분이 타임키퍼로 시간을 확인해주셨다. 15분 동안 질문과 답변이 오가고 5분은 세 명이서 면접자의 답변이나 눈동자 위치(?) 잘한 점 아쉬운 점을 솔직하게 말해주는 시간을 가졌다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;잘한 점&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;nbsp;1. 나는야 네트워크 마스터가 될거야!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;매주 스터디마다 제공된 질문들이 존재했지만, 만족하지 않고 해당 챕터에서 나올 만한 질문 &lt;span style=&quot;color: #ef6f53;&quot;&gt;전부&lt;/span&gt;를 꼬리질문으로 대비를 하였다. 이는 스터디 발표 때 빛을 발했고 칭찬을 아주 많이 받은 부분이 되었다.&amp;nbsp; 뿌&amp;nbsp;- 듯&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNmBXA/btsK3twqx3a/O2c4XWcvRX6qMku4tCXaqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNmBXA/btsK3twqx3a/O2c4XWcvRX6qMku4tCXaqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNmBXA/btsK3twqx3a/O2c4XWcvRX6qMku4tCXaqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNmBXA%2FbtsK3twqx3a%2FO2c4XWcvRX6qMku4tCXaqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1286&quot; height=&quot;1118&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;1118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;1898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bD6ro1/btsK3tC7HKY/R0BQoFKVHlto2GRJEKEFck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bD6ro1/btsK3tC7HKY/R0BQoFKVHlto2GRJEKEFck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD6ro1/btsK3tC7HKY/R0BQoFKVHlto2GRJEKEFck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbD6ro1%2FbtsK3tC7HKY%2FR0BQoFKVHlto2GRJEKEFck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1286&quot; height=&quot;1898&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;1898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 모호한건 질문!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대부분 내가 공부한 내용에서 질문이 나오긴 했지만, 생각할 부분이 있는 질문이 나오면 바로 메모를 한 후 따로 찾아보았다. 그래도 모호하고 확실하게 짚고 넘어가고 싶은 부분이 있으면 정리해서 질문방에 질문을 하였다. 질문을 나밖에 안 해서 조금 쑥스러웠지만 그럼에도 이건 현직 멘토님과 함께하는 좋은 기회라고 생각했기에 최대한 많이 질문하고 얻어가려고 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. 피드백은 면접자에게 도움이 되는 쪽으로&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 면접관이나 타임키퍼로 활동을 하면서 질문을 한 후 면접자의 답변 태도, 내용, 표정 전부를 관찰하면서 어떤 점에 강점이 있는지, 개선할 부분이 있는지 꼼꼼하게 확인하고 피드백 시간에 말씀드렸다. 나의 피드백을 들을 사람들은 전부 감사하다고 했고 이는 내가 어떤 식으로 답변을 해야 할지 역으로 도움이 되기도 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 내가 제일 잘했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;물론 에이?! 거짓말..이라고 생각할 수 있다. 하지만 나는 처음부터 네트워크에서 나올 만한 개념과 파생되는 질문과 답변을 전부 정리할 생각이었고 누구보다 열심히 누구보다 잘할 생각으로 임했고 증명했다. 나는 정말 잘했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ps. 면접 스터디가 다 끝나고 현직자 분께 추후에 다른 스터디 할 생각 있으면 같이 하자고 제안도 받았고 멘토님께서도 많이 배운다고 말씀하셨다... 정말 어깨가 올라가는 포인트다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_KakaoTalk_Photo_2024-12-02-14-30-14.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DiElw/btsK2Ziihro/ksbUmKJaNWZEpubMDuy0kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DiElw/btsK2Ziihro/ksbUmKJaNWZEpubMDuy0kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DiElw/btsK2Ziihro/ksbUmKJaNWZEpubMDuy0kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDiElw%2FbtsK2Ziihro%2FksbUmKJaNWZEpubMDuy0kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;456&quot; height=&quot;524&quot; data-filename=&quot;edited_edited_KakaoTalk_Photo_2024-12-02-14-30-14.jpeg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;아쉬운 점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 5주간 네트워크만 하다니...&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;한 주의 네트워크 분량을 공부하는데 사실 1.5 ~ 2일 정도밖에 걸리지 않는다. 그렇다면 네트워크뿐만 아니라 다른 과목 하나 정도는 병행이 가능했을 것이다. 물론 다른 프로젝트를 하고는 있지만 그건 핑계고 조금만 더 부지런했다면 가능했을 텐데 아쉬움이 남는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 없다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 최고다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;앞으로는 이렇게&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;저번주에 네트워크 스터디가 끝이 났다. 이번주에는 운영체제, 데이터베이스를 하루에 한 챕터씩 해서 이번주에 끝내는 것을 목표로 한다. 나는 할 수 있다!&lt;/p&gt;</description>
      <category>CS/Network</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/376</guid>
      <comments>https://lealea.tistory.com/376#entry376comment</comments>
      <pubDate>Mon, 2 Dec 2024 14:51:19 +0900</pubDate>
    </item>
    <item>
      <title>[업무에 바로 쓰는 SQL 튜닝] 4.2.6 다수 쿼리를 UNION 연산자로만 합치는 나쁜 SQL문</title>
      <link>https://lealea.tistory.com/370</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;목차&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;❌ 문제의 SQL 문&lt;br /&gt;  실행 계획 살펴보기&lt;br /&gt;⭕ UNION ALL 연산자로 변경해 보자&lt;br /&gt;  정리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;❌ 문제의 SQL 문&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;요구사항 : 성이 Baba이면서 성별이 M인 사원 데이터와 성이 Baba이면서 성별이 F인 사원 데이터를 합해서 조회하는 쿼리를 작성해 주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729750844509&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select 성별, 사원번호
    from 사원
    where 성별 = 'M'
        and 성 = 'Baba'

union

select 성별, 사원번호
    from 사원
    where 성별 = 'F'
        and 성 = 'Baba';&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;수행 결과&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729750873895&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;+--------+--------------+
| 성별    |    사원번호    |
+--------+--------------+
| M      |        11937 |
| M      |        12245 |
| M      |        15596 |
| M      |        15899 |
					...
| F      |       495777 |
| F      |       496003 |
| F      |       498356 |
| F      |       499779 |
+--------+--------------+
226 rows in set (0.0019 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;총 226건으로 소요시간은 0초대이다. 데이터가 많지 않기에 튜닝 결과의 지표로 실행 계획을 포함해서 종합적으로 판단해 보자.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;  실행 계획 살펴보기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2324&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0o5oT/btsKhxNcJph/anKk6wkYXVyP1QrOPW6RHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0o5oT/btsKhxNcJph/anKk6wkYXVyP1QrOPW6RHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0o5oT/btsKhxNcJph/anKk6wkYXVyP1QrOPW6RHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0o5oT%2FbtsKhxNcJph%2FanKk6wkYXVyP1QrOPW6RHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2324&quot; height=&quot;244&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2324&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. id가 1,2인 행은 사원 테이블의 &lt;span style=&quot;color: #409d00;&quot;&gt;I_성별_성 인덱스&lt;/span&gt;를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. id가 3인 행은 id가 1,2인 행의 &lt;span style=&quot;color: #409d00;&quot;&gt;결과를 통합한 뒤 중복을 제거하는 작업&lt;/span&gt;을 처리한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- Extra의 Using temporary를 보면 메모리에 임시테이블을 생성해서 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그 내부에서 union 연산 작업을 처리할 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- 데이터가 적으면 메모리를 사용하겠지만, 결과량이 많다면 디스크에 임시파일을 생성해 union작업을 수행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;여기서는 어떤 게 문제일까? &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;중복 제거가 필요한 상황인지 확인할 필요가 있다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;문제의 SQL문에서 두 select문은 I_성별_성 인덱스를 활용해 데이터를 조회하므로 &lt;span style=&quot;color: #333333;&quot;&gt;이미 중복이 제거된 상태&lt;/span&gt;이다. 따라서 추가적으로 임시 테이블을 만들어서 중복을 제거할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  여러 개의 select문 실행 결과를 합친 후 중복 데이터를 제거하는 작업을 하는 union을 제거하고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단순히 여러 개의 select문 실행 결과를 합치는 union all로 변경해 보자.  &amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;⭕&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;UNION ALL 연산자로 변경해 보자&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729751021023&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select 성별, 사원번호
    from 사원
    where 성별 = 'M'
        and 성 = 'Baba'

union all

select 성별, 사원번호
    from 사원
    where 성별 = 'F'
        and 성 = 'Baba';&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;수행 결과&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1729751034534&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;+--------+--------------+
| 성별    | 사원번호       |
+--------+--------------+
| M      |        11937 |
| M      |        12245 |
| M      |        15596 |
| M      |        15899 |
           ...					
| F      |       495777 |
| F      |       496003 |
| F      |       498356 |
| F      |       499779 |
+--------+--------------+
226 rows in set (0.0016 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;총 226건이며, 소요시간은 0초대이다. 데이터가 많지 않기 때문에 소요 시간만으로 튜닝 성공 여부를 판단하기 어려우므로 실행 계획도 함께 살펴보자.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;실행 계획 확인&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2182&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2rbqN/btsKitXmV1Q/cg7NJtlND8IHkalgarZBa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2rbqN/btsKitXmV1Q/cg7NJtlND8IHkalgarZBa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2rbqN/btsKitXmV1Q/cg7NJtlND8IHkalgarZBa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2rbqN%2FbtsKitXmV1Q%2Fcg7NJtlND8IHkalgarZBa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2182&quot; height=&quot;210&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2182&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. id가 1,2인 행은 사원 테이블의 I_성별_성 인덱스를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. 문제의 SQL문과 달리, id가 1,2인 결과를 단순히 합치므로 세 번째 행을 추가해 중복을 제거할 필요가 없어졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;  정리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;1. 데이터가 많지 않다면 &lt;span style=&quot;color: #ef6f53;&quot;&gt;실행계획&lt;/span&gt;까지 확인은 필수&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;2. 어떤 인덱스를 사용하는지 &amp;rarr; 효율적인지, 임시 테이블을 사용하는지 &amp;rarr; 꼭 필요한 작업인지, 확인&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;3. &lt;span style=&quot;color: #ef6f53;&quot;&gt;중복을 제거하는 연산자 또는 키워드를 사용한다면, 해당 쿼리에서 중복 제거 작업이 중복인지 확인&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;출처 : [도서] 업무에&lt;span&gt;&amp;nbsp;&lt;/span&gt;바로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쓰는 SQL&lt;span&gt;&amp;nbsp;&lt;/span&gt;튜닝&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>CS/Database</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/370</guid>
      <comments>https://lealea.tistory.com/370#entry370comment</comments>
      <pubDate>Mon, 7 Oct 2024 21:22:27 +0900</pubDate>
    </item>
    <item>
      <title>[업무에 바로 쓰는 SQL 튜닝] 4.2.5 습관적으로 중복을 제거하는 나쁜 SQL 문</title>
      <link>https://lealea.tistory.com/369</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;목차&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;❌ 문제의 SQL 문&lt;br /&gt;  실행 계획 살펴보기&lt;br /&gt;⭕ DISTINCT 키워드를 제거해 보자&lt;br /&gt;  정리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;❌ 문제의 SQL 문&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;요구사항 : 사원의 사원번호, 이름, 성, 그리고 부서 관리자의 부서번호를 중복 없이 조회하는 쿼리를 작성해 주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1729645468405&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select
    distinct 사원.사원번호,
    사원.이름,
    사원.성,
    부서관리자.부서번호
from 사원
join 부서관리자 on (사원.사원번호 = 부서관리자.사원번호);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;수행 결과&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;1368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctOi4T/btsKfH9CTqP/nG1F0j8iow22xSkLIKTsv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctOi4T/btsKfH9CTqP/nG1F0j8iow22xSkLIKTsv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctOi4T/btsKfH9CTqP/nG1F0j8iow22xSkLIKTsv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctOi4T%2FbtsKfH9CTqP%2FnG1F0j8iow22xSkLIKTsv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1446&quot; height=&quot;1368&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;1368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 24건이며 소요 시간은 0초에 가깝게 나왔다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;  실행 계획 살펴보기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IMpZc/btsKfetfH2q/LszItOIKVgaPT9AJ3SzbK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IMpZc/btsKfetfH2q/LszItOIKVgaPT9AJ3SzbK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IMpZc/btsKfetfH2q/LszItOIKVgaPT9AJ3SzbK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIMpZc%2FbtsKfetfH2q%2FLszItOIKVgaPT9AJ3SzbK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;216&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. id 값이 둘 다 1이므로 서로 조인하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 부서관리자 테이블의 type이 index로 인덱스 풀 스캔을 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 사원 테이블의 type은 eq_ref으로 사원번호(PK)를 사용해 1건의 데이터를 조회하는 효율적인 방식을 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. DISTINCT를 수행하기 위해 별도의 임시 테이블 Using temporary을 만들고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;그럼, 어떤 부분이 문제일까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사원테이블의 기본 키는 사원번호이다. 이 의미는 문제의 SQL문에서 SELECT절에 작성된 사원.사원번호에는&lt;span style=&quot;color: #ef5369;&quot;&gt; 중복데이터가 없다&lt;/span&gt;는 뜻이다. 따라서&lt;span style=&quot;color: #ef5369;&quot;&gt; DISTINCT를 사용해 중복을 제거하는 건 불필요&lt;/span&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;왜, distinct 키워드 사용이 문제가 될까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DISTINCT 키워드는 나열된 열들을 &lt;span style=&quot;color: #eb5757;&quot; data-token-index=&quot;1&quot;&gt;정렬&lt;/span&gt;한 뒤에 중복 데이터를 삭제한다. 이미 정렬된 데이터가 아니라면 정렬 작업이 부담이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;⭕&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;DISTINCT 키워드를 제거해 보자&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1729645765563&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select
    사원.사원번호,
    사원.이름,
    사원.성,
    부서관리자.부서번호
from 사원
join 부서관리자 on (사원.사원번호 = 부서관리자.사원번호);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;1358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfJfdm/btsKgryfUkh/HB799KIdCxyqv0cwNYZ2Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfJfdm/btsKgryfUkh/HB799KIdCxyqv0cwNYZ2Hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfJfdm/btsKgryfUkh/HB799KIdCxyqv0cwNYZ2Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfJfdm%2FbtsKgryfUkh%2FHB799KIdCxyqv0cwNYZ2Hk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1454&quot; height=&quot;1358&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;1358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전과 같이 총 24건이고 소요시간은 0초대로 변화가 없기에 소요시간을 기준으로 성능 개선 여부를 판단하기 어렵다. 이번에는 실행 계획을 통해 튜닝이 잘 되었는지 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;실행 계획 확인&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2774&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m5uFs/btsKeChjWHY/9dtHw32rkijXk3gsaK81jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m5uFs/btsKeChjWHY/9dtHw32rkijXk3gsaK81jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m5uFs/btsKeChjWHY/9dtHw32rkijXk3gsaK81jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm5uFs%2FbtsKeChjWHY%2F9dtHw32rkijXk3gsaK81jk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2774&quot; height=&quot;220&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2774&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Extra 항목에서 Using temporary가 삭제되었다. 이는 DISTINCT 키워드를 제거한 것과 관련이 있는데, 임시 테이블을 생성해서 추가적인 정렬과 중복 제거를 하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;  정리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;1. 임시 테이블 Using temporary이 있다면, 어떤 이유로 생성되었는지 확인해서 필요한 작업인지 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;2. DISTINCT키워드를 사용할 때에는 중복 제거가 필요한 게 맞는지, 처음부터 중복이 없는 건 아닐지 확인하자!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;- 왜? &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;DISTINCT&lt;/span&gt;는 내부적으로 임시테이블을 활용하고, 정렬작업을 수행하기 때문에, 필요하지 않다면 사용하지 않는 것이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;출처 : [도서] 업무에 바로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쓰는 SQL&lt;span&gt;&amp;nbsp;&lt;/span&gt;튜닝&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/Database</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/369</guid>
      <comments>https://lealea.tistory.com/369#entry369comment</comments>
      <pubDate>Fri, 4 Oct 2024 12:52:33 +0900</pubDate>
    </item>
    <item>
      <title>[업무에 바로 쓰는 SQL 튜닝] 4.2.4 열을 결합하여 사용하는 나쁜 SQL 문</title>
      <link>https://lealea.tistory.com/367</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;목차&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;❌ 문제의 SQL 문&lt;br /&gt;  실행 계획 살펴보기&lt;br /&gt;⭕ 열을 결합하는 과정을 제거하고, 열을 분리해 보자&lt;br /&gt;  정리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;❌ 문제의 SQL 문&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;요구사항 : 사원 테이블에서 성별의 값과 1칸의 공백, 성의 값을 모두 결합한 결과가 &amp;lsquo;M Radwan&amp;rsquo;인 데이터를 조회하는 쿼리를 작성해 주세요.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #eb5757;&quot; data-token-index=&quot;0&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;-&lt;/span&gt; CONCAT&lt;/span&gt; 함수는 여러 문자열을 연결하는 데 사용&lt;/p&gt;
&lt;pre id=&quot;code_1728303928704&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
    FROM 사원
    WHERE CONCAT(성별, ' ', 성) = 'M Radwan';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;출력 :&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkBM0H/btsJWp3BqjQ/sxgOAN6oBtX60Qg29g4ld0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkBM0H/btsJWp3BqjQ/sxgOAN6oBtX60Qg29g4ld0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkBM0H/btsJWp3BqjQ/sxgOAN6oBtX60Qg29g4ld0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkBM0H%2FbtsJWp3BqjQ%2FsxgOAN6oBtX60Qg29g4ld0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;576&quot; height=&quot;318&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;총 102건이고 소요 시간은 약 0.13초가 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;  실행 계획 살펴보기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DDeDf/btsJY4iG0pb/dPxKe0TnXsuDTo3CRQBKwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DDeDf/btsJY4iG0pb/dPxKe0TnXsuDTo3CRQBKwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DDeDf/btsJY4iG0pb/dPxKe0TnXsuDTo3CRQBKwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDDeDf%2FbtsJY4iG0pb%2FdPxKe0TnXsuDTo3CRQBKwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;986&quot; height=&quot;135&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사원 테이블에만 접근하여 데이터를 가져오고 있다. &lt;span style=&quot;color: #0593d3;&quot;&gt;문제의 SQL문에서는 WHERE 조건절로 데이터에 접근&lt;/span&gt;하는 반면, 실행계획을 확인해 보니&lt;span style=&quot;color: #ef5369;&quot;&gt; type 항목이 ALL 테이블 풀 스캔&lt;/span&gt;을 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;어떤 문제가 있는지, 테이블 인덱스 현황을 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1728304011347&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW INDEX FROM 사원;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;189&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X4yoq/btsJWSxuHkS/i6rH9sXt5UbZMSNbDasox0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X4yoq/btsJWSxuHkS/i6rH9sXt5UbZMSNbDasox0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X4yoq/btsJWSxuHkS/i6rH9sXt5UbZMSNbDasox0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX4yoq%2FbtsJWSxuHkS%2Fi6rH9sXt5UbZMSNbDasox0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;162&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;189&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본키는 사원번호 열이고, I_입사일자 인덱스는 입사일자 열, I_성별_성 인덱스는 성별 열 + 성 열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;그럼 WHERE절에서 I_성별_성 인덱스를 활용해 데이터를 빠르게 조회할 수 있었을 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;불필요하게 열을 결합하는 과정에서 문제가 생긴 것을 확인했다. 열을 분리해서 sql문을 다시 작성해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;⭕&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;열을 결합하는 과정을 제거하고, 열을 분리해 보자&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728304062438&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
    FROM 사원
    WHERE 성별 = 'M'
        AND 성 = 'Radwan';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BFNfo/btsJXWFKbcG/kqw1W6OHWQ5OiU3c6I1Xu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BFNfo/btsJXWFKbcG/kqw1W6OHWQ5OiU3c6I1Xu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BFNfo/btsJXWFKbcG/kqw1W6OHWQ5OiU3c6I1Xu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBFNfo%2FbtsJXWFKbcG%2Fkqw1W6OHWQ5OiU3c6I1Xu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;288&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;소요시간이 약 0.13초에서 약 0.01초로 줄어들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실행계획까지 확인해 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVCcGu/btsJXXECCPd/GUS6QHUEOKaAyLXPmhFzt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVCcGu/btsJXXECCPd/GUS6QHUEOKaAyLXPmhFzt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVCcGu/btsJXXECCPd/GUS6QHUEOKaAyLXPmhFzt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVCcGu%2FbtsJXXECCPd%2FGUS6QHUEOKaAyLXPmhFzt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1042&quot; height=&quot;134&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1042&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;I_성별_성 인덱스를 사용해서 테이블에 접근&lt;/span&gt;하고 있고, 튜닝 전에는 약 30만 건의 데이터에 접근했다면 튜닝 후에는 102건의 데이터에만 접근한다는 것을 알 수 있다. 그리고 &lt;span style=&quot;color: #0593d3;&quot;&gt;type유형이 ref&lt;/span&gt;로 인덱스를 이용해 특정 조건(주로 =)에 맞는 데이터를 찾고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;  정리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테이블의 인덱스를 확인하는 습관 덕분에, 이미 설정된 인덱스가 효율적으로 사용되지 않고 있다는 사실을 알게 되었다. &lt;b&gt;멀티 칼럼 인덱스가 설정되어 있는 상태에서, 해당 열들을 결합한 쿼리문을 작성 시 인덱스가 활용되지 않을 수 있다&lt;/b&gt;는 것을 배웠다. 이런 경우에는 열을 나눠서 작성한 후 실행 계획을 확인하여 효율적인 탐색이 이루어지고 있는지 항상 점검해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;출처 : [도서] 업무에 바로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쓰는 SQL 튜닝&lt;/span&gt;&lt;/p&gt;</description>
      <category>CS/Database</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/367</guid>
      <comments>https://lealea.tistory.com/367#entry367comment</comments>
      <pubDate>Wed, 2 Oct 2024 14:29:37 +0900</pubDate>
    </item>
    <item>
      <title>[업무에 바로 쓰는 SQL 튜닝] 4.2.3 형변환으로 인덱스를 활용하지 못하는 나쁜 SQL 문</title>
      <link>https://lealea.tistory.com/366</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;목차&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;❌ 문제의 SQL 문&lt;br /&gt;  실행 계획 살펴보기&lt;br /&gt;⭕ 형변환이 발생하지 않도록 SQL을 수정해 보자&lt;br /&gt;  정리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;❌ 문제의 SQL 문&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;요구사항: 급여 테이블에서 현재 유효한 급여 정보만 조회하는 쿼리를 작성해 주세요.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #f1f1ef;&quot; data-token-index=&quot;0&quot;&gt;사용여부&lt;/span&gt; 열의 값이 1인 데이터가 유효한 급여 정보입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1728230296419&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT COUNT(*)
    FROM 급여
    WHERE 사용여부 = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;출력:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMUgQK/btsJWTILlhf/4BWK6sVPhXL2QegjRXsiFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMUgQK/btsJWTILlhf/4BWK6sVPhXL2QegjRXsiFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMUgQK/btsJWTILlhf/4BWK6sVPhXL2QegjRXsiFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMUgQK%2FbtsJWTILlhf%2F4BWK6sVPhXL2QegjRXsiFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;721&quot; height=&quot;155&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1258&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1건이 출력되었고 데이터는 총 42,842건이며 sql 소요시간은 약 0.48초로 나타났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  실행 계획 살펴보기&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OQw7B/btsJXtiy7Lk/FPyhV2GHmrpt66pU9kT5h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OQw7B/btsJXtiy7Lk/FPyhV2GHmrpt66pU9kT5h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OQw7B/btsJXtiy7Lk/FPyhV2GHmrpt66pU9kT5h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOQw7B%2FbtsJXtiy7Lk%2FFPyhV2GHmrpt66pU9kT5h1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1002&quot; height=&quot;113&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;key항목&lt;/span&gt;이 &lt;span style=&quot;color: #0593d3;&quot;&gt;I_사용여부&lt;/span&gt;로 출력되고, &lt;span style=&quot;color: #0593d3;&quot;&gt;type항목&lt;/span&gt;이&lt;span style=&quot;color: #0593d3;&quot;&gt; index&lt;/span&gt;이므로 &lt;span style=&quot;color: #ef5369;&quot;&gt;인덱스 풀 스캔 방식&lt;/span&gt;으로 테이블의 데이터를 찾고 있다. 그리고 &lt;span style=&quot;color: #0593d3;&quot;&gt;filtered항목&lt;/span&gt;이 &lt;span style=&quot;color: #0593d3;&quot;&gt;10&lt;/span&gt;이므로 MySQL엔진으로 가져온 데이터 중 10%를 추출해서 최종 데이터를 출력했다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기서는 어떤 문제가 있을까? 우선 급여 &lt;b&gt;테이블의 인덱스 현황&lt;/b&gt;을 알아볼 필요가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1728230359269&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW INDEX FROM 급여;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2966&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c98hNB/btsJVhR5Ukr/TDKH4dFf49KU9nMdD8gMm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c98hNB/btsJVhR5Ukr/TDKH4dFf49KU9nMdD8gMm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c98hNB/btsJVhR5Ukr/TDKH4dFf49KU9nMdD8gMm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc98hNB%2FbtsJVhR5Ukr%2FTDKH4dFf49KU9nMdD8gMm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2966&quot; height=&quot;340&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2966&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용여부 열이 인덱스로 구성되어 있지만, 인덱스를 효율적으로 활용하지 못하고 인덱스 풀 스캔으로 수행되고 있는 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이어서 &lt;b&gt;테이블의 구조&lt;/b&gt;를 확인해 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1728230396339&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DESC 급여;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn4A1R/btsJV0a1GRb/fWqhMUUwDneEkLBYNlLPs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn4A1R/btsJV0a1GRb/fWqhMUUwDneEkLBYNlLPs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn4A1R/btsJV0a1GRb/fWqhMUUwDneEkLBYNlLPs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn4A1R%2FbtsJV0a1GRb%2FfWqhMUUwDneEkLBYNlLPs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;483&quot; height=&quot;218&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;922&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;사용여부 열은 문자형인 char(1) 데이터 유형&lt;/span&gt;이다. 그런데 문제의 SQL문에서는 WHERE 사원번호 = 1과 같이 &lt;span style=&quot;color: #0593d3;&quot;&gt;숫자 유형으로 써서 데이터에 접근&lt;/span&gt;했었다. 그럼 DMBS 내부적으로 묵시적 &lt;span style=&quot;color: #ef5369;&quot;&gt;형변환이 발생&lt;/span&gt;했을 것이다. 그 결과 I_사용여부 인덱스를 제대로 활용하지 못하고 엔덱스 풀 스캔을 한 것이다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그럼, 형변환이 발생하지 않도록 SQL을 조정하면 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;⭕&lt;span&gt;&amp;nbsp;&lt;/span&gt;형변환이 발생하지 않도록 SQL을 수정해 보자&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728230450106&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT COUNT(*)
    FROM 급여
    WHERE 사용여부 = '1';&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mps9C/btsJV7ubzu1/fNGcCeTssDI5bjvhum7yp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mps9C/btsJV7ubzu1/fNGcCeTssDI5bjvhum7yp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mps9C/btsJV7ubzu1/fNGcCeTssDI5bjvhum7yp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmps9C%2FbtsJV7ubzu1%2FfNGcCeTssDI5bjvhum7yp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;744&quot; height=&quot;266&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1286&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;sql 소요시간이 기존 약 0.48초에서 약 0.01초로 개선됨을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실행계획까지 확인해 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2150&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt0h46/btsJV2NbHfY/1kAheWKa1pNyogNMBTMXdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt0h46/btsJV2NbHfY/1kAheWKa1pNyogNMBTMXdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt0h46/btsJV2NbHfY/1kAheWKa1pNyogNMBTMXdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt0h46%2FbtsJV2NbHfY%2F1kAheWKa1pNyogNMBTMXdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2150&quot; height=&quot;268&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2150&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;key 항목&lt;/span&gt;에서 &lt;span style=&quot;color: #0593d3;&quot;&gt;I_사용여부 인덱스&lt;/span&gt;를 사용한다는 것을 알 수 있고, &lt;span style=&quot;color: #0593d3;&quot;&gt;type 항목&lt;/span&gt;이 &lt;span style=&quot;color: #0593d3;&quot;&gt;ref&lt;/span&gt;이므로 인덱스를 통해 특정 조건에 맞는 데이터를 효율적으로 가져온다. 또한, &lt;span style=&quot;color: #0593d3;&quot;&gt;filtered 값이 100&lt;/span&gt;이므로 가져온 데이터가 모두 사용되는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;  정리&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1.&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt; 데이터 유형에 맞게 열을 사용하자!&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래야 내부적인 형변환이 발생되지 않는다. 형변환이 일어나면 인덱스를 제대로 활용하지 못하는 경우가 있음에 주의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. &lt;b&gt;실행 계획, 테이블의 인덱스 현황 및 테이블 구조를 함께 확인하면서 개선 가능한 포인트가 있는지 확인&lt;/b&gt;하는 습관을 들이자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;[실행 계획]&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;filtered&lt;/b&gt; :&lt;br /&gt;예를 들어 10이라는 의미는 스토리지 엔진에서 MySQL엔진으로 가져온 데이터 중 10%를 추출해서 최종 테이터를 출력하는 것&lt;br /&gt;&lt;br /&gt;&lt;b&gt;type&lt;/b&gt;: 테이블의 데이터를 어떻게 찾을지&lt;br /&gt;&amp;nbsp; - index : 인덱스 블록을 처음부터 끝까지 훑는 방식으로 인덱스 풀 스캔을 의미함&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;rarr; 그다지 효율적이지 않음&lt;br /&gt;&amp;nbsp; - ref : 인덱스를 이용해 특정 조건(주로 =)에 맞는 데이터를 찾는 방식으로, 조인할 때 주로 사용된다 &amp;rarr; 원하는 데이터를 빠르게 찾을 수 있어 효율적&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;[유용한 SQL문]&lt;/p&gt;
&lt;pre id=&quot;code_1728230879073&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW INDEX FROM [테이블명];
테이블의 인덱스 현황 확인

DESC [테이블명]; 
테이블의 구조 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;출처 : [도서] 업무에 바로 쓰는 SQL 튜닝&lt;/p&gt;</description>
      <category>CS/Database</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/366</guid>
      <comments>https://lealea.tistory.com/366#entry366comment</comments>
      <pubDate>Fri, 13 Sep 2024 10:38:58 +0900</pubDate>
    </item>
    <item>
      <title>[업무에 바로 쓰는 SQL 튜닝] 4.2.2 사용하지 않는 함수를 포함하는 나쁜 SQL문</title>
      <link>https://lealea.tistory.com/365</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;목차&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;❌ 문제의 SQL 문&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  실행 계획 살펴보기&lt;br /&gt;&lt;b&gt;⭕ 불필요한 IFNULL() 함수를 제거해 보자&lt;/b&gt;&lt;br /&gt;&lt;b&gt;  정리&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;❌ 문제의 SQL 문&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;요구사항 : 사원 테이블에서 성별 기준으로 몇 명의 사원이 있는지 확인하는 쿼리문을 작성해 주세요.&lt;/p&gt;
&lt;pre id=&quot;code_1728017481251&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT IFNULL(성별, 'NO DATA') AS 성별, COUNT(*) 건수
    FROM 사원
    GROUP BY IFNULL(성별, 'NO DATA')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 성별의 값이 NULL이라면 NO DATA라고 출력할 수 있도록 IFNULL() 함수를 사용하여 구현했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjbdxj/btsJTUP0zLv/rUPeKTtpTSVNEhLEKbFz70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjbdxj/btsJTUP0zLv/rUPeKTtpTSVNEhLEKbFz70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjbdxj/btsJTUP0zLv/rUPeKTtpTSVNEhLEKbFz70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcjbdxj%2FbtsJTUP0zLv%2FrUPeKTtpTSVNEhLEKbFz70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;283&quot; height=&quot;170&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2개의 행이 출력되었고, 약 0.12초가 소요되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  실행 계획 살펴보기&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2396&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bal7M9/btsJVrey0Uf/O9Pbd7H1F8eIkprGQfZXK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bal7M9/btsJVrey0Uf/O9Pbd7H1F8eIkprGQfZXK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bal7M9/btsJVrey0Uf/O9Pbd7H1F8eIkprGQfZXK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbal7M9%2FbtsJVrey0Uf%2FO9Pbd7H1F8eIkprGQfZXK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2396&quot; height=&quot;380&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2396&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;I_성별_성 인덱스로 &lt;span style=&quot;color: #ef5369;&quot;&gt;인덱스 풀 스캔 방식&lt;/span&gt;을 수행하며, Extra 항목이 &lt;span style=&quot;color: #ef5369;&quot;&gt;Using temporary로 임시 테이블을 생성&lt;/span&gt;한다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;음.. 여기서는 어떤 게 문제일까? 생각해 볼 포인트가 몇 가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 성별 열에 M, F 이외의 값이 존재하는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 성별 열에 NULL을 허용하는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1번 확인 : M, F 이외의 값은 들어있지 않다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/denR0i/btsJV0AD4H2/XhOjNK6HZhcbgF0IvZuuqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/denR0i/btsJV0AD4H2/XhOjNK6HZhcbgF0IvZuuqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/denR0i/btsJV0AD4H2/XhOjNK6HZhcbgF0IvZuuqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdenR0i%2FbtsJV0AD4H2%2FXhOjNK6HZhcbgF0IvZuuqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;577&quot; height=&quot;202&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2번 확인 : Null 열에 NO라고 명시되어 있으므로 NULL값이 될 수 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1728017600124&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DESC 사원;

+--------------+---------------+------+-----+---------+-------+
| Field        | Type          | Null | Key | Default | Extra |
+--------------+---------------+------+-----+---------+-------+
| 사원번호       | int           | NO   | PRI | NULL    |       |
| 생년월일       | date          | NO   |     | NULL    |       |
| 이름          | varchar(14)   | NO   |     | NULL    |       |
| 성           | varchar(16)   | NO   |     | NULL    |       |
| 성별          | enum('M','F') | NO   | MUL | NULL    |       |
| 입사일자       | date          | NO   | MUL | NULL    |       |
+--------------+---------------+------+-----+---------+-------+
6 rows in set (0.0036 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;따라서 IFNULL() 함수를 처리하려고 DB 내부에 별도 임시 테이블을 만들 필요가 없음을 확인했다! &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;불필요한 로직이므로 튜닝 대상이 되겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;&lt;b&gt;⭕ 불필요한 IFNULL() 함수를 제거해 보자&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pjAEk/btsJU7HlPCs/GCU3fyvwYCsr6T4OioaZs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pjAEk/btsJU7HlPCs/GCU3fyvwYCsr6T4OioaZs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pjAEk/btsJU7HlPCs/GCU3fyvwYCsr6T4OioaZs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpjAEk%2FbtsJU7HlPCs%2FGCU3fyvwYCsr6T4OioaZs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;530&quot; height=&quot;212&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;소요시간이 약 0.12초에서 약 0.06초로 줄었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실행계획까지 확인해 보자&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4sQIx/btsJV1ffrJO/jovscLFYkS5moIyYPiDOwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4sQIx/btsJV1ffrJO/jovscLFYkS5moIyYPiDOwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4sQIx/btsJV1ffrJO/jovscLFYkS5moIyYPiDOwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4sQIx%2FbtsJV1ffrJO%2FjovscLFYkS5moIyYPiDOwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2114&quot; height=&quot;262&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;key항목이 I_성별_성 인덱스로 &lt;span style=&quot;color: #ef5369;&quot;&gt;인덱스 풀 스캔 방식으&lt;/span&gt;로 수행되며, Extra항목이 Using index로 &lt;span style=&quot;color: #ef5369;&quot;&gt;임시 테이블 없이 인덱스만 사용하는 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;  정리&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;1. 실행 계획에서 확인할 포인트&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;Extra 항목이 Using temporary이면 임시 테이블을 생성한다는 의미로 하나의 튜닝할 수 있는 포인트가 될 수 있음!&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 함수를 사용할 때 사용하지 않아도 되는 함수는 아닐까 확인해 볼 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;출처 : [도서] 업무에 바로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쓰는 SQL 튜닝&lt;/span&gt;&lt;/p&gt;</description>
      <category>CS/Database</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/365</guid>
      <comments>https://lealea.tistory.com/365#entry365comment</comments>
      <pubDate>Tue, 10 Sep 2024 09:46:32 +0900</pubDate>
    </item>
    <item>
      <title>[업무에 바로 쓰는 SQL 튜닝] 4.2.1 기본키를 변형하는 나쁜 SQL문</title>
      <link>https://lealea.tistory.com/364</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;4장은 문제가 있는 SQL문과 개선된 SQL문을 소개해주고 있다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;팀원과 한 파트씩 나눠서 발표 스터디를 진행하기로 했고, 오늘은 그 첫 번째 사례이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;나는 도커 컨테이너 하나를 생성하고 안에 실습할 데이터베이스를 미리 넣어두었다. 그랬더니 컨테이너에 접속만 하면 쉽게 실습할 수 있어서 아주 편리했다!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;다음은 목차이다. 앞으로도 목차는 비슷하게 흘러갈 예정이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;❌&amp;nbsp;문제의 SQL 문&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  실행 계획 살펴보기&lt;br /&gt;&lt;b&gt;⭕ &lt;/b&gt;사원번호(기본키)가 인덱스를 타도록 수정해 보자&lt;br /&gt;&lt;b&gt;  정리&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;❌&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;문제의 SQL 문&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;[주의] MySQL에서 문자열의 인덱스는 1부터 시작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;요구사항 :&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사원번호가 1100으로 시작하면서 사원번호가 5자리인 사원의 정보를 모두 출력해 주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1727852773932&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
    FROM 사원
    WHERE SUBSTRING(사원번호, 1, 4) = 1100 // 첫번째 자리부터 4번째 자리까지
        AND LENGTH(사원번호) = 5&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot; data-token-index=&quot;0&quot;&gt;출력 : 사원 테이블&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nNvfS/btsJTAb13j0/nA5qJe9awQJ4Z0urvTxpp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nNvfS/btsJTAb13j0/nA5qJe9awQJ4Z0urvTxpp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nNvfS/btsJTAb13j0/nA5qJe9awQJ4Z0urvTxpp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnNvfS%2FbtsJTAb13j0%2FnA5qJe9awQJ4Z0urvTxpp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1338&quot; height=&quot;564&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;총 10건의 결과가 출력되고 실습 환경에서 0.12초 소요되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;쿼리의 문제점은 없을까?, 더 개선할 포인트가 있지 않을까? 실행 계획을 확인해 보자!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;실행 계획 살펴보기&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1972&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTCH7r/btsJS6vwAYj/JeCnpDKkj4ExqLVQjU73v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTCH7r/btsJS6vwAYj/JeCnpDKkj4ExqLVQjU73v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTCH7r/btsJS6vwAYj/JeCnpDKkj4ExqLVQjU73v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTCH7r%2FbtsJS6vwAYj%2FJeCnpDKkj4ExqLVQjU73v0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1972&quot; height=&quot;424&quot; data-origin-width=&quot;1972&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Type 항목이 ALL&lt;/span&gt;로 나타나므로 &lt;span style=&quot;color: #ef5369;&quot;&gt;테이블 풀 스캔&lt;/span&gt; 방식인 것을 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;인덱스를 사용하지 않고 바로 테이블에 접근하여 필요한 범위가 아닌 처음부터 끝까지 스캔하고 있어 비효율적인 쿼리라 추측할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그럼 사원 테이블의 &lt;span style=&quot;color: #ef5369;&quot;&gt;기본키와 인덱스를 파악&lt;/span&gt;해서 세부적인 원인을 알아보도록 하자.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRqRR7/btsJTSi3UFv/H7pBPaUyuKLIAdyKBi2nj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRqRR7/btsJTSi3UFv/H7pBPaUyuKLIAdyKBi2nj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRqRR7/btsJTSi3UFv/H7pBPaUyuKLIAdyKBi2nj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRqRR7%2FbtsJTSi3UFv%2FH7pBPaUyuKLIAdyKBi2nj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;532&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Key_name 열 정보에서 PRIMARY, I_입사일자, I_성별_성이라는 3개 인덱스가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; -&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기본키는 사원번호&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; -&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;I_입사일자&amp;nbsp;인덱스는 1개의 입사일자로 구성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; -&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;I_성별_성 인덱스는 성별과 성 순서대로 구성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그럼 튜닝해야 할 SQL 문은 무엇일까?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여기서 의문이 하나 들어야 한다. &lt;b&gt;&amp;lsquo;문제의 SQL&amp;rsquo;에서 사원번호 열을 조건문으로 작성&lt;/b&gt;&lt;/span&gt;&lt;b&gt;했기&lt;span&gt; &lt;/span&gt;때문에&lt;span&gt; &lt;/span&gt;기본키를&lt;span&gt; &lt;/span&gt;통해&lt;span&gt; &lt;/span&gt;특정&lt;span&gt; &lt;/span&gt;범위를&lt;span&gt; &lt;/span&gt;스캔했어야&lt;span&gt; &lt;/span&gt;했는데&lt;span&gt;, &lt;/span&gt;실제로는&lt;span&gt;&amp;nbsp;왜&amp;nbsp;&lt;/span&gt;풀&lt;span&gt; &lt;/span&gt;테이블&lt;span&gt; &lt;/span&gt;스캔이&lt;span&gt; &lt;/span&gt;발생한&lt;span&gt; &lt;/span&gt;것일까&lt;span&gt;?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그 이유는 &amp;lsquo;&lt;span style=&quot;color: #ef5369;&quot;&gt;사원번호&amp;rsquo; 칼럼이&lt;/span&gt; &lt;span style=&quot;color: #ef5369;&quot;&gt;기본키이나 사원번호르 그대로 쓰는 대신 SUBSTRING(사원번호, 1, 4)와 LENGTH(사원번호)와 같이 가공하여 작성했으므로, 기본 키를 사용하지 않게되어 테이블 풀 스캔을 수행했기 때문이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그럼, 다시 사원번호가 인덱스를 타도록 수정하면 되겠다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;⭕&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;사원번호(기본키)가 인덱스를 타도록 수정해 보자.&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1727852948251&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 첫 번째 방법
SELECT *
    FROM 사원
    WHERE 사원번호 BETWEEN 11000 AND 11009

// 두 번째 방법
SELECT *
    FROM 사원
    WHERE 사원번호 &amp;gt;= 11000 AND 사원번호 &amp;lt;= 11009&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;BETWEEN 구문으로 범위 검색을 수행하거나 비교연산자를 활용&lt;/span&gt;해 사원번호가 변형되지 않도록 하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;해당 쿼리로는 %의 개선이 있었는지 확인해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;총 10건의 결과가 출력되고 0.1232초 &amp;rarr; 0.0012초로 성능이 약 98.99% 개선되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KSmCL/btsJSFd50uZ/kHgc3LHLIjAKNaR7k2lHQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KSmCL/btsJSFd50uZ/kHgc3LHLIjAKNaR7k2lHQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KSmCL/btsJSFd50uZ/kHgc3LHLIjAKNaR7k2lHQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKSmCL%2FbtsJSFd50uZ%2FkHgc3LHLIjAKNaR7k2lHQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1334&quot; height=&quot;560&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그럼 이번에도 실행계획을 보면서 인덱스를 잘 활용했는지 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wuL99/btsJUwT1DKM/JD7ClHA6rfacs05d2l1nzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wuL99/btsJUwT1DKM/JD7ClHA6rfacs05d2l1nzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wuL99/btsJUwT1DKM/JD7ClHA6rfacs05d2l1nzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwuL99%2FbtsJUwT1DKM%2FJD7ClHA6rfacs05d2l1nzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;220&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기본 키(key항목 : PRIMARY)의 특정 범위만 스캔(&lt;span style=&quot;color: #ef5369;&quot;&gt;type항목: range&lt;/span&gt;)한다는 걸 알 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt; &amp;nbsp;정리&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. 기본키를 변형하면, 인덱스가 사용되지 않고 풀 테이블 스캔이 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;실행계획 확인하는 습관을 가지자!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; - Type 항목이 ALL이라면, 풀 스캔 방식이므로 개선 포인트가 있는지 의심&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; - 기본키, 인덱스 확인해서 활용이 되고 있는지 확인&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp; - 쿼리를 튜닝했다면 실행 계획을 확인해 어느 부분에서 개선이 되었는지 확인&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;출처 : [도서] 업무에 바로 쓰는 SQL 튜닝&lt;/span&gt;&lt;/p&gt;</description>
      <category>CS/Database</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/364</guid>
      <comments>https://lealea.tistory.com/364#entry364comment</comments>
      <pubDate>Sun, 8 Sep 2024 19:07:08 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] 2019 KAKAO BLIND RECRUITMENT 후보키</title>
      <link>https://lealea.tistory.com/353</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;후보키&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42890&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/42890&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMcRg9/btsIR12dYn3/5egOZk8vnkXtkz8mUOREW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMcRg9/btsIR12dYn3/5egOZk8vnkXtkz8mUOREW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMcRg9/btsIR12dYn3/5egOZk8vnkXtkz8mUOREW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMcRg9%2FbtsIR12dYn3%2F5egOZk8vnkXtkz8mUOREW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1664&quot; height=&quot;874&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;874&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d13gs6/btsIPUcDR8T/xldqAIenfnluuvDkp6MkZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d13gs6/btsIPUcDR8T/xldqAIenfnluuvDkp6MkZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d13gs6/btsIPUcDR8T/xldqAIenfnluuvDkp6MkZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd13gs6%2FbtsIPUcDR8T%2FxldqAIenfnluuvDkp6MkZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1664&quot; height=&quot;922&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입출력 예&lt;/b&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1608&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B6hII/btsIQhSN2iE/3zkFf6XzGN8motBgTk2wc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B6hII/btsIQhSN2iE/3zkFf6XzGN8motBgTk2wc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B6hII/btsIQhSN2iE/3zkFf6XzGN8motBgTk2wc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB6hII%2FbtsIQhSN2iE%2F3zkFf6XzGN8motBgTk2wc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1608&quot; height=&quot;238&quot; data-origin-width=&quot;1608&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제한사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;relation은 2차원 문자열 배열이다.&lt;/li&gt;
&lt;li&gt;relation의 컬럼(column)의 길이는&amp;nbsp;1&amp;nbsp;이상&amp;nbsp;8&amp;nbsp;이하이며, 각각의 컬럼은 릴레이션의 속성을 나타낸다.&lt;/li&gt;
&lt;li&gt;relation의 로우(row)의 길이는&amp;nbsp;1&amp;nbsp;이상&amp;nbsp;20&amp;nbsp;이하이며, 각각의 로우는 릴레이션의 튜플을 나타낸다.&lt;/li&gt;
&lt;li&gt;relation의 모든 문자열의 길이는&amp;nbsp;1&amp;nbsp;이상&amp;nbsp;8&amp;nbsp;이하이며, 알파벳 소문자와 숫자로만 이루어져 있다.&lt;/li&gt;
&lt;li&gt;relation의 모든 튜플은 유일하게 식별 가능하다.(즉, 중복되는 튜플은 없다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 분석&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt; 후보키 : 릴레이션의 튜플을 유일하게 식별할 수 있는 속성 또는 속성의 집합으로 유일성과 최소성을 만족함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 입력 : 릴레이션&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;bull;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 출력 : 후보키의 최대 개수&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;아이디어 &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;어떤 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;속성이나 속성의 조합이 유일성을 만족하는지 알 수 없기 때문에 모든 경우를 시도해봐야 한다. 만약 어떤 조합으로 튜플 개수를 세었을 때 중복이 존재하지 않는다면, 그 조합은 유일성을 만족한다고 볼 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;또한 최소성을 확인하기 위해, 현재 속성 조합에서 특정 속성을 제거해도 유일성이 유지되는지 검토해야 한다.&lt;/p&gt;
&lt;div&gt;
&lt;div data-message-id=&quot;7ec08e7c-6383-460d-81a9-e9964c9c35dd&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;접근 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 알고리즘 선택: 완전 탐색과 &lt;b&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;조합&lt;/span&gt;&lt;/b&gt;을 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;모든 가능한 조합을 생성하고 ➡️ &lt;span style=&quot;color: #0593d3;&quot;&gt;그중에서 최소성과 유일성을 만족&lt;/span&gt;하는 조합만을 반환할 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 시간 복잡도: &lt;b&gt;&lt;span style=&quot;color: #ef6f53;&quot;&gt;O(2^m)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;조합 생성 부분 (열이 m개인 경우): O(2^m)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;최소성을 확인하고 유일성을 검사하는 부분 (각 튜플에 대해 진행): O(n)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 문제에서 m은 최대 20이므로, 시간 복잡도는 O(2^20)이다. 2^20은 10^8보다 훨씬 작기 때문에 해당 접근 방식으로 구현하는 것이 가능하다고 판단 후 코드 구현에 들어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코드 구현&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 실패 코드&lt;/p&gt;
&lt;pre id=&quot;code_1722385475704&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CandidateKey {
    public int solution(String[][] relation) {
        List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; candidateKeys = new ArrayList&amp;lt;&amp;gt;();
        int rowLen = relation.length;
        int colLen = relation[0].length;

        for(int length=1; length&amp;lt;=colLen; length++){
            List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; combinations = new ArrayList&amp;lt;&amp;gt;();
            getCombinations(0,0, length, combinations, new HashSet&amp;lt;&amp;gt;(), colLen);

            for(Set&amp;lt;Integer&amp;gt; cols : combinations){
                boolean minimality = true;

                for(Set&amp;lt;Integer&amp;gt; key : candidateKeys){
                    // 이 부분
                    if(key.containsAll(cols)){
                        minimality = false;
                        break;
                    }
                }

                if(!minimality){
                    continue;
                }

                Set&amp;lt;String&amp;gt; rowSet = new HashSet&amp;lt;&amp;gt;();
                for(String[] row : relation){
                    StringBuilder rowStr = new StringBuilder();
                    for(int col : cols){
                        rowStr.append(row[col]);
                    }
                    rowSet.add(rowStr.toString());
                }

                if(rowSet.size() == rowLen){
                    candidateKeys.add(new HashSet&amp;lt;&amp;gt;(cols));
                }
            }
        }

        return candidateKeys.size();
    }

    public void getCombinations(int start, int depth, int maxDepth,
                                List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; combinations, Set&amp;lt;Integer&amp;gt; curr,
                                int colLen){

        if(depth == maxDepth){
            combinations.add(new HashSet&amp;lt;&amp;gt;(curr));
            return;
        }

        for(int i = start; i &amp;lt; colLen; i++){
            curr.add(i);
            getCombinations(i + 1, depth + 1, maxDepth, combinations, curr, colLen);
            curr.remove(i);
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이유 : 최소성&amp;nbsp;검사에서는&amp;nbsp;key가&amp;nbsp;cols를&amp;nbsp;포함하는&amp;nbsp;것이&amp;nbsp;아니라,&amp;nbsp;&lt;br /&gt;cols가&amp;nbsp;key를&amp;nbsp;포함하는지&amp;nbsp;확인해야&amp;nbsp;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722385495091&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;key.containsAll(cols) -&amp;gt; cols.containsAll(key)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;추가적으로 메서드 분리 적용해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. &lt;b&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;성공 코드&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1722385520730&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

public class CandidateKey {
    public int solution(String[][] relation) {
        // 최종 후보 키를 저장할 리스트: 조합 + 최소성 + 유일성 만족
        List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; candidateKeys = new ArrayList&amp;lt;&amp;gt;();

        // 릴레이션의 행 수
        int rowLen = relation.length;
        // 릴레이션의 열 수
        int colLen = relation[0].length;

        for (int length = 1; length &amp;lt;= colLen; length++) {
            List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; combinations = new ArrayList&amp;lt;&amp;gt;();

            // 1. 모든 속성 조합을 생성하는 메서드 호출
            getCombinations(0, 0, length, combinations, new HashSet&amp;lt;&amp;gt;(), colLen);

            // 2. 생성된 모든 조합에 대해 최소성과 유일성 검사 시작
            // 2-1. 최소성 검사
            isMinimality(combinations, candidateKeys, relation, rowLen);
        }
        // 후보 키의 개수를 반환
        return candidateKeys.size();
    }

    // 모든 속성 조합을 생성하는 메서드
    public void getCombinations(int start, int depth, int maxDepth,
                                List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; combinations, Set&amp;lt;Integer&amp;gt; curr,
                                int colLen) {

        // 현재 조합의 길이가 최대 길이(maxDepth)에 도달하면, 해당 조합을 리스트에 추가하고 종료
        if (depth == maxDepth) {
            combinations.add(new HashSet&amp;lt;&amp;gt;(curr));
            return;
        }

        for (int i = start; i &amp;lt; colLen; i++) {
            curr.add(i);
            getCombinations(i + 1, depth + 1, maxDepth, combinations, curr, colLen);
            curr.remove(i);
        }
    }

    // 최소성 검사 메서드
    public void isMinimality(List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; combinations, List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; candidateKeys,
                             String[][] relation, int rowLen) {
        for (Set&amp;lt;Integer&amp;gt; cols : combinations) {
            boolean minimality = true;

            // 기존 후보 키들과 비교하여 현재 조합이 최소성을 만족하는지 검사
            for (Set&amp;lt;Integer&amp;gt; key : candidateKeys) {
                // 만약 현재 조합이 기존 후보 키의 부분집합이라면, 최소성 위배
                if (cols.containsAll(key)) {
                    minimality = false;
                    break; // 최소성을 만족하지 않으므로 더 이상 검사할 필요 없음
                }
            }

            // 최소성을 만족할 경우에만 유일성 검사로 넘어감
            if (minimality) {
                // 2-2. 유일성 검사
                isUniqueness(relation, cols, rowLen, candidateKeys);
            }
        }
    }

    // 유일성 검사 메서드
    public void isUniqueness(String[][] relation, Set&amp;lt;Integer&amp;gt; cols,
                             int rowLen, List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; candidateKeys) {
        Set&amp;lt;String&amp;gt; rowSet = new HashSet&amp;lt;&amp;gt;();
        for (String[] row : relation) {
            StringBuilder rowStr = new StringBuilder();
            for (int col : cols) {
                rowStr.append(row[col]);
            }
            // 각 행의 속성 조합 값으로 구성된 문자열 추가
            rowSet.add(rowStr.toString());
        }

        // rowSet의 크기가 행의 수와 같다면, 모든 행이 유일함을 의미
        if (rowSet.size() == rowLen) {
            candidateKeys.add(new HashSet&amp;lt;&amp;gt;(cols));
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정확성: 100.0&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span&gt;합계&lt;/span&gt;: 100.0 / 100.0&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;회고&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;간단한 완전탐색 문제를 풀다가 오늘 푼 문제는 좀 어려웠다. 해결하는 데 3시간 정도 걸렸는데, 나중에 다시 돌아와서 풀어봐야겠다는 생각이 들었다. 유일성과 최소성의 개념은 알고 있었지만 이를 코드로 구현하려면 무엇을 어떻게 확인해야 하는지 꼼꼼히 따져야 했는데, 덕분에 코드 구현 능력이 +1 향상된 것 같아 뿌듯했다. 완전탐색 문제를 좀 더 풀어보면 감이 잡힐 것 같아 앞으로도 꾸준히 문제를 풀어볼 생각이다 &lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>코딩 테스트(JAVA)/프로그래머스</category>
      <author>Lea Hwang</author>
      <guid isPermaLink="true">https://lealea.tistory.com/353</guid>
      <comments>https://lealea.tistory.com/353#entry353comment</comments>
      <pubDate>Wed, 31 Jul 2024 09:32:09 +0900</pubDate>
    </item>
  </channel>
</rss>