<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>기록하는 습관</title>
    <link>https://jjingho.tistory.com/</link>
    <description>Java와 백엔드 기술을 공부하고 공유합니다.</description>
    <language>ko</language>
    <pubDate>Mon, 20 Jul 2026 14:06:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>짱호</managingEditor>
    <image>
      <title>기록하는 습관</title>
      <url>https://tistory1.daumcdn.net/tistory/3875678/attach/6ad137a740c84db1bc264fa99dd802f8</url>
      <link>https://jjingho.tistory.com</link>
    </image>
    <item>
      <title>3년차 개발자의 2022년 회고</title>
      <link>https://jjingho.tistory.com/180</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;항상 회고를 써야겠다는 생각을 가지고 있었지만 행동으로 옮기지는 않았었다.&lt;br /&gt;스스로 회고할만한 경험이나 성장을 못했다고 생각했었기 때문이다.&lt;br /&gt;돌이켜보면 아주 천천히, 조금씩은 성장하고 있었지만 &lt;b&gt;그저 그런 핑계를 대며 그냥 안 썼던 것 같다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 해를 마무리하며 자신을 되돌아보는 시간을 가지는 건 아주 좋은 습관이라 생각한다.&lt;br /&gt;좋은 습관을 내 것으로 만들기 위해 올해를 기점으로 꾸준히 회고를 써볼 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 회고에서는 내가 어떤 경험을 했고 어떤 생각을 했었는지 기록으로 남겨본다.&lt;/p&gt;
&lt;h1&gt;1. 이직&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2020년 1월에 입사해서 2022년 1월까지, 총 2년간 근무했던 회사에서 퇴사하고&lt;br /&gt;2022년 2월부터 새로운 회사로 출근하게 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이직 결심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신입으로 입사해서 2년 동안 일했던 회사에서 이직을 결심하게 된 이유는 크게 2가지가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;백엔드 개발자로써 성장과 커리어에 대한 의구심&lt;/li&gt;
&lt;li&gt;함께했던 동료들의 이직&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사 초기부터 내가 맡았던 업무는 빅데이터 수집 솔루션의 화면 개발이었다.&lt;br /&gt;물론 간단한 백엔드 업무도 함께 수행하긴 했었지만, 백엔드 개발자로 폭풍 성장하고 싶은 나의 욕구를 채워주진 못했다. 조직의 방향성과 개인의 목표가 일치했다면 더할 나위 없었겠지만 나의 상황은 그렇지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상의 워라밸과 동료들도 좋은 편이라 편하게 다니기엔 좋은 회사였지만, 경험과 성장을 추구하는 나와 회사의 fit이 맞지 않는다고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자아실현을 이루기 위해서는&lt;/b&gt; 우선 자체 서비스를 하고 있는 회사로 이직을 해야겠다는 생각이 나를 지배하고 있을 때쯤 IT 업계에 어마어마한 물이 들어오기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;21년도는 코로나로 인한 IT 업계 대호황 시기였다. 기업들은 좋은 개발자들을 끌어모으기 위해 연봉을 올렸고, 서로 경쟁했다. 물들어 올 때 노를 젓듯 동료들은 좋은 조건에 좋은 회사로 하나 둘 이직하기 시작했다. 이때부터 마음속으로만 가지고 있던 이직의 불씨가 활활 타오르기 시작했다.&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;1377&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/85ORw/btrU7HECOl5/bD7PjWqC0kRWNX4OC7bPa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/85ORw/btrU7HECOl5/bD7PjWqC0kRWNX4OC7bPa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/85ORw/btrU7HECOl5/bD7PjWqC0kRWNX4OC7bPa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F85ORw%2FbtrU7HECOl5%2FbD7PjWqC0kRWNX4OC7bPa0%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;365&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1377&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;h2 data-ke-size=&quot;size26&quot;&gt;이직 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마음을 먹고 이직 준비를 시작했지만 2년 차 개발자, 만 1년이 조금 넘은 개발자가 이직하기란 쉽지 않았다.&lt;br /&gt;매력적인 이력을 가지고 있는 것도 아니고, 남들보다 뛰어난 것도 아닌 그저 그런 실력을 가지고 있었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 메타인지조차 제대로 되지 않는 상태였기 때문에 내가 뭘 모르고, 뭐가 부족한지 조차 판단할 수 없었던 것 같다. 그래서 나는 자체 서비스를 하고 있는 스타트업부터 대기업까지 마구마구 지원을 하고 닥치는 대로 면접을 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 기업들의 면접 경험은 나에게 간접적인 경험치를 선물해 줬다.&lt;br /&gt;실제 서비스에서 발생할 수 있는 문제나 해결 방법들을 물어보는 질문들이 자주 나왔고, 이를 해결하기 위한 필요 기술들의 지식들을 많이 물어봤었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 서비스 기업에서는 어떤 고민을 하고 있고, 어떤 사람들을 원하는지 대략 감을 잡을 수 있었다.&lt;br /&gt;수많은 좌절을 안겨준 면접들이 지금 내게 어떤 지식이나 경험이 필요한지 알려주는 길잡이 역할을 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직을 준비하는 동안 자존감이 많이 낮아졌고 스트레스도 많이 받았었지만, 한편으론 가장 많이 성장할 수 있었던 시기였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 건 꺾이지 않는 마음&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결실&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 이직을 준비하고 6개월 만에 결실을 맺을 수 있었다.&lt;br /&gt;입사하게 된 회사는 내가 좋은 이미지를 가지고 있는 회사였고 멋진 사옥과 풀 재택근무 제도를 가진 소위 말하는 꽤 괜찮은 회사였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 다양한 서비스 중 원하는 팀에 지원할 수 있었고, 평소 콘텐츠 서비스에 관심이 많았기에 이쪽 업무를 선택해 새로운 커리어를 시작할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존과는 너무 다른 규모와 환경이 낯설었지만, 팀원분들의 도움으로 금방 적응할 수 있었다.&lt;br /&gt;지금 시점으로도 아직 모르는 게 산더미지만, 내가 맡은 부분에서 성능 개선이나 다양한 문제들을 해결하며 나름 가치 있는 일들을 하고 있다는 생각이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성장에 진심인 동료들과 성장을 적극적으로 지원하는 팀에서 많이 배우며 성장하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;2. 회사&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 이직을 하게 되면서 &lt;b&gt;근무 환경&lt;/b&gt;과 &lt;b&gt;업무&lt;/b&gt;에도 많은 변화가 있었다.&lt;br /&gt;재택근무 형태로 일을 하게 됐고 B2C 서비스에 대한 궁금증도 해소할 수 있었다.&lt;br /&gt;또한, 더 나은 서비스를 만들어가기 위해 어떤 기술과 지식이 필요한지 많이 배우게 됐고 제너럴한 지식보다 서비스 개선과 관련된 기술에 대해 더 고민하게 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재택근무 적응기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 회사는 재택근무로 일하는 리모트 근무 형태를 가지고 있었다.&lt;br /&gt;기본은 재택, 출근은 자유롭게 선택할 수 있는 유연한 근무 제도가 아주 마음에 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출근 거리에 대한 부담이 없는 게 가장 큰 장점이었고 출/퇴근에 사용되는 시간을 효율적으로 쓸 수 있다는 점도 나에게 큰 장점으로 다가왔다. 나는 매일 1 ~ 2시간씩 공부를 하거나, 글을 쓰거나, 코딩을 하는 라이프 사이클을 가지려 노력하고 있는데 여기에 시간을 충분히 투자해도 내 일상에 다른 스케줄을 끼워 넣을 수 있는 시간적 여유가 생긴 게 너무 좋았다.&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;2000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5lz30/btrUZYOACNO/WqV4bSTG3eYxlMv3YZShf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5lz30/btrUZYOACNO/WqV4bSTG3eYxlMv3YZShf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5lz30/btrUZYOACNO/WqV4bSTG3eYxlMv3YZShf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5lz30%2FbtrUZYOACNO%2FWqV4bSTG3eYxlMv3YZShf0%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;419&quot; height=&quot;419&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무적으로도 입사 초반에 적응이 조금 어려웠던 것 빼고는 단점이 없었다.&lt;br /&gt;물론 사람 by 사람이겠지만, 나의 경우 일에 대한 집중도도 높아지고 업무 효율도 훨씬 좋아서 오히려 일을 더 많이 한 것 같다. (이게 단점이라면 단점일라나..? )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 재택근무를 지원하는 회사에만 다니고 싶을 정도로 나에게 잘 맞는 근무 형태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레거시 개선하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사 후 메인으로 담당하게 된 네이버 연동 서비스 파트는 꽤 오랫동안 커밋 내역이 없는 레거시 코드였다.&lt;br /&gt;반대로 생각하면 아주 잘 돌아가고 있는 프로젝트로 볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전임자들이 모두 퇴사한 상태여서 개략적인 인수인계만 받은 후 내부 코드 분석을 통해 비즈니스 요구사항을 파악하며 어느 정도 익숙해져가고 있을 때쯤, 자주 보이는 운영 이슈들이 눈에 띄기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 이슈가 2 ~ 3개의 패턴을 가지고 반복적으로 발생하고 있었고, 그것들을 처리하는 것이 자연스럽게 내 업무 중 하나가 되었다. 즉, 반복적이고 귀찮은 일에 업무 리소스를 낭비하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨텐츠를 운영하는 운영팀에서는 해당 이슈가 처리되지 않으면 일 처리가 진행되지 않기 때문에 실시간으로 처리해줘야 하는 불편함도 있었다. &lt;s&gt;(&lt;del&gt;다른 업무를 하다 흐름이 끊기는&amp;hellip; 컨텍스트 스위칭  )&lt;/del&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 그냥 문제가 발생하는 원인 자체를 제거해버리기로 마음먹었고 리팩터링과 성능 개선을 함께 진행하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 문제 원인 제거를 위해 이슈 발생 빈도 별 &lt;b&gt;개선 우선순위를 결정&lt;/b&gt;해야 했고, 상품 연동 중 Lock wait timeout exceeded; try restarting transaction 문제가 발생해 연동에 실패하는 문제에 대한 개선이 가장 시급하다고 판단했다. 네이버에는 상품이 생성되지만 우리 쪽 DB는 롤백되는 연동 정합성 문제가 발생하고, 삭제 API를 통해 수동으로 네이버 상품을 삭제해줘야 하는 귀찮음을 동반했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 몇 가지 가설을 세우고 하나씩 개선과 검증 절차를 거쳐 정확한 원인을 파악하기 시작했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가설 1. 쿼리 자체 문제가 아닐까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lock wait timeout exceeded; try restarting transaction 문제가 가장 많이 발생하는 구간은 DB에 연동 데이터를 UPDATE 할 때였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 UPDATE 시 조회된 인덱스에 Lock을 건다.&lt;br /&gt;따라서 UPDATE 조건절이 인덱스를 제대로 사용하지 못하는 경우 테이블 전체에 Lock이 걸리는 일이 발생한다. 실제로도 해당 테이블에 적절한 인덱스가 없어 위와 같은 상황이 일어나고 있었고 적절한 인덱스를 추가해 Lock의 범위를 최소화하도록 개선해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉽게도 이슈의 발생 빈도가 줄긴 했지만, 문제가 완전히 해결되진 않았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가설 2. 너무 긴 트랜잭션 단위가 원인인가?!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API에 대한 트랜잭션을 관리하는 건 굉장히 까다로운 일이라 일반적으로 이벤트 리스너를 사용하거나 외부 호출을 트랜잭션 범위에서 제외해 관리하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 시스템은 여러 번의 외부 API 호출을 통해 데이터를 취합해 연동하는 흐름을 가지고 있고, 모든 API 호출이 하나의 트랜잭션으로 묶여있는 형태를 가지고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 단위가 너무 길어지면 트랜잭션이 잠금 획득에 너무 오랜 시간 대기를 할 수도 있고, 장애가 전파되는 등 다양한 문제에 원인이 될 수 있다고 판단했고 외부 호출 로직을 모두 트랜잭션 범위에서 제거하도록 개선해 보았다.&lt;br /&gt;&lt;s&gt;(&lt;del&gt;간략히 적었지만, 당시에는 다양한 가설을 세우고 많은 고민과 학습을 한 끝에 도달한 결론이었다... )&lt;/del&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과, 나를 귀찮게 하던 이슈 하나를 줄일 수 있었다!&lt;br /&gt;지금 생각해보면 특별한 일은 아니지만, 입사 초반의 첫 개선 경험이라 그런지 아직도 기억에 많이 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 문제를 주도적으로 개선했던 경험은 앞으로 어떻게 지속적인 성장을 이루어나갈 것인가에 대한 고민에 대답이 돼준 것 같다. 문제를 깊게 파고들어 분석하고, 이를 해결하기 위해 필요한 지식을 학습하고, 그 지식들로 문제를 해결했을 때의 성취감이 나를 계속 성장시켜줄 것이라는 확신이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 조직과 나의 동반성장을 위해 API 성능 개선이나 쿼리 성능 개선 등 주도적인 개선을 꾸준히 이어가고 있다. 아직도 해결해야 할 문제들이 산더미지만 조금씩, 천천히 풀어나가고 있는 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 내가 어떤 일을 해나가고 싶은지를 더욱 선명하게 만들어준 경험이라 꼭 기록으로 남기고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회사의 대격변&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재택근무 뽕에 흠뻑 젖어가고 있을 때쯤, IT 업계에 밀려 들어오던 물이 슬슬 빠져나가고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인플레이션으로 인해 세계 경제가 얼어붙으면서 IT 업계에도 혹한기가 찾아왔다.&lt;br /&gt;금리가 오르고 투자가 줄어들면서 급격한 겨울이 시작됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밀물과 썰물의 단순한 흐름인 줄 알았지만, 물이 빠져나가는 속도가 거의 쓰나미 수준이었다.&lt;br /&gt;해고가 자유로운 외국 대기업들을 필두로 대량 인원 감축이 시작되면서 불안감은 커져갔다.&lt;br /&gt;특히, 채용 경쟁으로 인해 높아진 개발자들의 임금 비용을 줄이는 게 가장 큰 화두가 되고 있었기에 불안감이 더 커졌던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중, 우리 회사에도 큰 변화가 일어났다.&lt;br /&gt;내가 속한 회사는 그룹사의 모든 서비스를 개발하고 지원하는 그룹사 전체 개발 조직들이 모여있던 회사였는데&lt;br /&gt;갑작스럽게 법인 폐지 소식을 듣게 됐다&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사한지 7개월 만에 나라 잃은 국민이 되어 다른 그룹사로 전배를 가거나 이직을 해야 하는 상황이 온 거다.&lt;br /&gt;불행 중 다행인 건 우리 개발 조직은 전배 갈 곳이 확정되어있다는 것이었는데, 그렇지 못한 다른 조직에서는 희망퇴직을 하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전배가 확정되고 내 상황은 안정화 됐지만, 뭔가 마음 한구석이 계속 찜찜했다.&lt;br /&gt;정말 많은 노력 끝에 입사한 회사인 만큼 자부심이나 애정을 가지고 있었는데, 하루아침에 내 노력이 물거품이 된 기분이었다. 마치 부모한테 버림받고 입양되는 기분이랄까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이뿐만 아니라, 올해까지는 고용 조건과 근무 조건이 그대로 승계되지만 내년부터는 전배 된 회사에 방침에 따라 근무 형태도 변경될 거라고 했다. 즉, 2023년에는 재택근무 제도가 없어진다는 뜻이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 마음에 드는 구석이 하나도 없었지만, 그나마 위안이 되는 건 함께 일하는 동료들은 그대로라는 점이었다.&lt;br /&gt;이 덕분에 그나마 멘탈 회복을 금방 할 수 있었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내년부터 적용되는 근무 형태에는 금방 적응하겠지만, 출/퇴근에 소요되는 3시간이라는 시간을 활용하지 못한다는 점은 극복이 잘 안 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 팀원들과 으쌰으쌰해서 최대한 잘 버텨봐야겠다.(물론 못 버티고 튕겨 나갈 수도 있다. )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;3. 2022 목표 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 초에는 2022년을 알차게 보내기 위한 목표를 세웠었다.&lt;br /&gt;크게 목표를 나눠보자면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경험 위주의 양질의 컨텐츠 생산&lt;/li&gt;
&lt;li&gt;기술 서적 1달에 1권 읽기&lt;/li&gt;
&lt;li&gt;운동&lt;/li&gt;
&lt;li&gt;영어&lt;/li&gt;
&lt;li&gt;토이 프로젝트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과부터 말하면 블로그에 공개할 만한 양질의 컨텐츠를 만들지 못했다.&lt;br /&gt;회사에서 겪은 문제들과 해결 방법에 대한 글은 개인 노트에 모두 기록했지만, 회사 코드를 그대로 사용할 수는 없으니 블로그에 공개하기 위한 제너럴한 예제로 컨버팅이 필요했는데, 이 일이 생각보다 쉽지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 예상한 것보다 더 많은 시간과 노력이 필요했다.&lt;br /&gt;결국 목표한 바를 이루어 내진 못했다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;네, 핑계 맞습니다. 반성합니다. &amp;zwj;♂️&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;기술 서적 1달에 1권 읽기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 목표였던 기술 서적 한 달에 한 권 읽기, 이것 역시 쉽지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;히히히&amp;hellip; &lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;사실 아무 생각 없이 읽으면 11권 읽기는 너무 쉽다. 하지만 내가 읽어야 할 책들은 기술 서적이니, 진짜 내 지식으로 만들기 위해서는 장기 기억으로 변환하는 과정이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 머리가 그리 좋지 않아서 2 ~ 3번씩 반복해서 읽어야 그나마 책 내용이 이해되고 기억에 남았다.&lt;br /&gt;읽은 책 내용들을 요약 정리하거나 이해가 될 때까지 보다 보니 꽤 많은 시간이 소요됐고,&lt;br /&gt;결과적으로 목표로 했던 기술 서적 총 11권 가운데 5권 정도를 읽을 수 있었다.&lt;br /&gt;책을 통해 알게 된 지식을 실무에 적용한 사례나 새롭게 알게 된 지식들도 많아서 개인적으로는 만족 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 목표였던 11권의 책 목록이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클린 아키텍처&lt;/li&gt;
&lt;li&gt;모던 자바 인 액션&lt;/li&gt;
&lt;li&gt;클린 코드&lt;/li&gt;
&lt;li&gt;오브젝트&lt;/li&gt;
&lt;li&gt;실용주의 프로그래머&lt;/li&gt;
&lt;li&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/li&gt;
&lt;li&gt;토비 스프링 3.1&lt;/li&gt;
&lt;li&gt;대규모 서비스를 지탱하는 기술&lt;/li&gt;
&lt;li&gt;리팩토링&lt;/li&gt;
&lt;li&gt;Real MySQL&lt;/li&gt;
&lt;li&gt;소프트웨어 장인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이중 아래의 5권의 책만 읽었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클린 아키텍처&lt;/li&gt;
&lt;li&gt;모던 자바 인 액션&lt;/li&gt;
&lt;li&gt;오브젝트&lt;/li&gt;
&lt;li&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/li&gt;
&lt;li&gt;Real MySQL 8.0&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 못 읽은 6권은 내년에 읽어야겠다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표로 했던 테니스는 아니지만 새로운 운동을 배우기 시작했다.&lt;br /&gt;바로 주짓수! 너무 재밌고 스트레스 해소에도 많은 도움이 되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재택근무 할 때 일이 끝나고 나면 머리가 꽉 차서 집중력이 떨어지는 느낌이었는데,&lt;br /&gt;몸을 움직이고 머리를 비워주니 몸은 피곤하지만 오히려 집중력은 올라갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;건강한 몸에 건강한 정신이 깃든다는 말이 공감됐다.&lt;br /&gt;특별한 이유가 없다면 계속해서 배울 예정이다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;(출근하게 되면 힘들지도..ㅠㅠ)&lt;/del&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어 회화는 올해도 역시 못 배웠다. 아니 안 배웠다.&lt;br /&gt;이러다 평생 못할 거 같다.ㅎㅎ&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBtTjT/btrUYPjY5RX/YTJXUxhEUDxqi3T9sHaLQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBtTjT/btrUYPjY5RX/YTJXUxhEUDxqi3T9sHaLQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBtTjT/btrUYPjY5RX/YTJXUxhEUDxqi3T9sHaLQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBtTjT%2FbtrUYPjY5RX%2FYTJXUxhEUDxqi3T9sHaLQK%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;413&quot; height=&quot;1501&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1501&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지인 추천으로 팀을 구성해 간단한 토이 프로젝트를 진행했다.&lt;br /&gt;기획/PM 1명, 디자인 1명, iOS 1명, AOS 1명, 백엔드 2명으로 구성된 작은 팀이었다.&lt;br /&gt;프로젝트 주제는 맛집을 기록하고 리뷰를 관리하는 간단한 맛집용 노트 앱이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 다들 열정적으로 참여하면서 재미있게 개발했는데, 소셜 로그인이나 맛집 기록 같은 기본적인 기능 개발까지 마치고 나니 다들 바빠졌는지 갑자기 프로젝트가 흐지부지 끝나버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 프로젝트를 진행하면서 좋은 소프트웨어 아키텍처와 좋은 코드를 고민하며 개발했었고 리뷰를 통해 새로운 의견과 시각을 들어볼 수 있어서 나에게 많은 도움이 되는 시간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이전까지 모바일 개발자분들과 협업해볼 기회가 없었는데, 토이 프로젝트를 통해 경험해볼 수 있었던 점도 좋았던 것 같다. 앞으로 이런 기회가 얼마나 더 있을진 모르겠지만, 더 많은 토이 경험을 해보고 싶어졌다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;이왕이면 수익 창출이 되는걸로..츄릅&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;4. 일일 커밋&lt;/h1&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;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VbHg3/btrUZYukMy7/v1J1ZdcK0pIQPSHUHXfr40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VbHg3/btrUZYukMy7/v1J1ZdcK0pIQPSHUHXfr40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VbHg3/btrUZYukMy7/v1J1ZdcK0pIQPSHUHXfr40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVbHg3%2FbtrUZYukMy7%2Fv1J1ZdcK0pIQPSHUHXfr40%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;504&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일일 커밋을 2년째 이어오고 있다.&lt;br /&gt;&lt;a href=&quot;https://jjingho.tistory.com/133&quot;&gt;일일 커밋 1년 회고&lt;/a&gt;를 한지 벌써 1년이 지났다니, 시간이 정말 빠르게 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 꾸준함의 가치를 믿고 있다.&lt;br /&gt;꾸준함이 쌓이고 쌓이다 보면 비로소 빛을 낼 거라는 믿음.&lt;br /&gt;가치 있는 일을 꾸준히 이어가다 보면 언젠가, 나에게 분명 긍정적인 효과를 가져다줄 것이라 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꾸준함을 유지하는 건 어려운 일이다.&lt;br /&gt;그래서 나는 일일 커밋을 통해 학습에 대한 꾸준함을 이어가고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 읽기, 글 쓰기, 사이드 프로젝트, 코딩, 스터디 등 자유로운 학습 주제를 일일 커밋에 대입할 수 있다.&lt;br /&gt;이와 동시에 기록으로 남길 수 있으니 학습에 이보다 더 좋은 방법이 있을까란 생각이 든다.&lt;br /&gt;아마도 나에게 딱 맞는 완벽한 학습법을 찾지 않는 이상 일일 커밋은 계속 이어갈 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2023년도 열심히!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;5. 스터디&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나에게는 21년도부터 꾸준히 이어온 &lt;a href=&quot;https://github.com/java-starter/ReadingBooks/issues?q=is%3Aissue+is%3Aclosed&quot;&gt;스터디 그룹&lt;/a&gt;이 있다.&lt;br /&gt;전 직장 동료들과 만들었던 스터디인데 서로의 성장에 조금이나마 강제성을 부여하기 위해 시작됐다.&lt;br /&gt;21년도에 &lt;b&gt;이펙티브 자바&lt;/b&gt; 스터디를 시작으로 &lt;b&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/b&gt;, 현재는 Real MySQL 8.0을 읽으며 함께 스터디하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 스터디만을 위한 그룹이 아니라 기술적 고민이나 궁금증을 자유롭게 논의하며 시야를 넓히는데 많은 도움이 되기도 하고, 회사 생활에서의 고충이나 어려움들을 얘기하면서 위로를 받기도 한다. 또한, 대부분 비슷한 고민이나 관심사를 가지기 때문에 서로에게 힘이 되어주고 좋은 자극제가 되어주고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 성장에 굉장히 많은 도움이 된 스터디였고 준 것보다 받은 게 더 많은 스터디 그룹이였던 것 같다.&lt;br /&gt;앞으로도 이 그룹이 계속 이어진다면 받는 것보다 주는 것이 많아지는 그런 동료가 돼주고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;6. 개발자&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 기여한 소프트웨어 혹은 서비스가 사용자들에게 가치 있게 사용될 수 있도록 완성도 높은 제품을 만들어가는 것, 이를 위해 끊임없이 고민하고 성장하는 개발자가 되는 것은 내가 일을 시작할 때부터 가지고 있던 &lt;b&gt;목표&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학생 때 약 350명의 학생들이 사용했던 학식 알리미라는 서비스를 운영해 보면서, 이런 부분이 얼마나 많은 성취감과 동기부여를 주는지 몸소 느꼈기 때문에 가지게 된 확고한 직업관이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 생각을 가지고 있어서인지 항상 많은 트래픽이 일어나는 서비스에서 일하고 싶었다.&lt;br /&gt;규모가 클수록 많은 사용자의 피드백을 받아볼 수 있고, 이에 따라 서비스의 완성도를 높이거나 성능을 개선하는 일을 할 수 있을 거라 믿었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이전 회사는 온프레미스 방식으로 고객사에 설치되어 운영되는 형태의 솔루션을 파는 회사였다. 그러다 보니 사용자들이 어떤 부분에서 불편함을 느끼는지, 어떤 부분에서 가치를 전달받고 있는지 피드백을 받기 어려웠다. 그래서인지 일하는 게 그리 즐겁지 않았고 오히려 흥미가 떨어졌던 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래서 지금은?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 나는 B2C 서비스의 내부 시스템을 개발하는 팀에서 일하고 있다.&lt;br /&gt;트래픽을 직접 맞는 포지션은 아니지만 결제나 정산, 연동 등의 시스템 개발 업무를 수행하는 팀이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 나는 연동 쪽을 맡아 개발하게 됐는데, 처음에는 맡은 업무에 대한 아쉬움이 있었다.&lt;br /&gt;백엔드 개발자로서 트래픽을 안정적으로 처리하고 개선하는 업무를 해보고 싶었기 때문이다.&lt;br /&gt;주변에 많은 사람들도 트래픽을 직접 맞아보는 건 중요한 경험이라고, 이런 걸 경험해봐야 성장할 수 있다고 말했었다. 나 또한 이에 동의했었고 이런 업무 자체에 대한 궁금증도 있었다. 그래서 더욱 아쉽게 느껴졌던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 연동 업무를 수행할수록 해당 업무에서도 충분히 재미와 즐거움을 찾을 수 있었다.&lt;br /&gt;내 기준 사용자는 내부 직원들인데, 그래서인지 오히려 즉각적인 피드백을 받을 수 있었고 더 빠르고 안정적인 연동 기능을 제공하기 위해 고민하는 과정에서 뜻밖의 즐거움을 찾은 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;연동이 빨라진 것 같아요!&amp;rdquo;&lt;br /&gt;&amp;ldquo;연동 오류가 줄어들었어요!&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 피드백을 받을 때면 기분이 너무 좋았다.&lt;br /&gt;개발자라는 직업을 시작할 때부터 가지고 있던 &lt;b&gt;목표를 조금 이뤄가고 있는 기분이 들었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 작은 성취감들은 성능 개선과 품질 개선을 더 고민하게 되는 긍정적인 노력들로 변해가고 있다.&lt;br /&gt;그래서 요즘은 개발하는 게 너무 재밌고 즐겁다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경험을 통해 느낀 건 다른 사람들이 중요하다고 말하는 경험들 보다 내가 구축한 개발 세계관에서 중요한 룰이 무엇인지, 나는 어떤 부분에서 즐거움과 재미를 느끼는지, 어떤 일을 할 때 가치를 느끼고 있는지를 잘 아는 게 더 중요한 것 같다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 하고 싶지 않은 일도 많이 생길 것이다.&lt;br /&gt;하지만 그 안에서도 가치 있는 일을 찾는다면, 어떤 일이든 재미있게 개발할 수 있을 거라 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 경험치가 부족한 우물 안 개구리지만, 앞으로는 이런 마음가짐으로 개발자라는 직업에 더 몰입해 볼 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;7. 마치며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년은 나에게 많은 경험과 성장을 선사해준 해였다.&lt;br /&gt;개인적으로는 개발자를 시작하고 가장 만족스러운 한 해인 것 같다.&lt;br /&gt;그리던 개발자로서의 삶의 첫 번째 조각을 맞췄고 이제야 제대로 뛰기 시작했다는 생각이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열심히 뿌려놓은 씨앗을 수확하며 경험했던 &lt;b&gt;작은 성공의 달콤함&lt;/b&gt;은 나를 계속해서 움직이게 만드는 동력이 돼주었고, 그 동력을 기반으로 성장에 대한 목마름을 해소하면서 내가 가진 세계관을 더욱 단단히 굳힐 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7CEsD/btrU1M73WNF/rkKRkBfV4cSisZkwDtKeo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7CEsD/btrU1M73WNF/rkKRkBfV4cSisZkwDtKeo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7CEsD/btrU1M73WNF/rkKRkBfV4cSisZkwDtKeo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7CEsD%2FbtrU1M73WNF%2FrkKRkBfV4cSisZkwDtKeo1%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;472&quot; height=&quot;174&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보면 2020년과 2021년의 내가 열심히 씨앗을 뿌려두었기 때문에 지금처럼 긍정적인 상황으로 올 수 있었던 것 같다. 올해는 성공적인 수확을 거뒀으니, 또 다른 성장을 위해 다시 씨앗을 뿌려둬야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;씨앗들이 건강하게 자라길 바라며 2022년을 마친다.&lt;/p&gt;</description>
      <category>장호의 머릿속</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/180</guid>
      <comments>https://jjingho.tistory.com/180#entry180comment</comments>
      <pubDate>Sun, 1 Jan 2023 23:29:07 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 쿼리 작성 및 최적화 - SELECT 편(1 / 2)</title>
      <link>https://jjingho.tistory.com/179</link>
      <description>&lt;h1&gt;SELECT 절의 처리 순서&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFunTI/btrTwABhLa4/4eKi2idPrpqkqK1swlFUXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFunTI/btrTwABhLa4/4eKi2idPrpqkqK1swlFUXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFunTI/btrTwABhLa4/4eKi2idPrpqkqK1swlFUXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFunTI%2FbtrTwABhLa4%2F4eKi2idPrpqkqK1swlFUXK%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;1920&quot; height=&quot;410&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 순서가 바뀌어서 실행되는 형태의 쿼리는 거의 없다.&lt;br /&gt;쿼리에서 어느 절이 먼저 실행되는지 모르면 처리 내용이나 처리 결과를 예측할 수 없으므로 잘 알아두자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 실행 순서를 벗어나는 쿼리가 필요하다면 서브쿼리로 작성된 &lt;b&gt;인라인 뷰&lt;/b&gt;를 사용하면 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인라인 뷰(Inline View)란?&lt;br /&gt;하나의 질의문 내에서만 생성되어 사용 되어지고 질의문 수행 종료 후에는 사라지는 뷰를 뜻한다.&lt;br /&gt;일반적으로 FROM 절에서 서브쿼리를 하나의 테이블로 사용하는 형태를 말한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 인라인 뷰가 사용되면 임시 테이블이 사용되기 때문에 주의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;WHERE 조건과 GROUP BY 절, ORDER BY 절의 인덱스 사용&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스를 사용하기 위한 기본 규칙&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 컬럼 변형 없이 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절이나 ORDER BY, GROUP BY가 인덱스를 사용하려면 값에 대한 변형 없이 인덱스 컬럼을 그대로 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM salaries WHERE salary * 10 &amp;gt; 150000;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 쿼리는 salary 인덱스가 있더라도 값을 변형해서 비교하므로 인덱스를 이용할 수 없다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절에 사용되는 비교 조건에서 연산자 양쪽의 비교 대상 값은 데이터 타입이 일치해야 한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM tb_test WHERE age=2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리의 age 컬럼은 문자열 타입으로 선언되었지만 비교 연산에는 숫자 타입으로 비교된다. 따라서 ref나 range가 아닌 index(인덱스 풀 스캔)이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 MySQL 옵티마이저가 내부적으로 문자열 타입을 숫자 타입으로 변환한 후 비교 작업을 처리하는데, 타입이 변환된 후 비교 작업을 처리해야 하므로 인덱스 레인지 스캔이 불가능한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 SQL을 작성할 때는 반드시 데이터 타입을 맞춰 비교 조건을 사용하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WHERE 절의 인덱스 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절의 인덱스 사용 방법은 &lt;b&gt;작업 범위 결정 조건&lt;/b&gt;과 &lt;b&gt;체크 조건&lt;/b&gt; 두 가지 방식으로 구분한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 범위 결정 조건은 동등 비교 조건이나 IN으로 구성된 조건에 사용된 컬럼들이 인덱스의 구성과 좌측부터 얼마나 일치하는가에 따라 달라진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfglqv/btrTAa9g7TO/3lPjNKbNfDEy4QnKtj53sK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfglqv/btrTAa9g7TO/3lPjNKbNfDEy4QnKtj53sK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfglqv/btrTAa9g7TO/3lPjNKbNfDEy4QnKtj53sK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfglqv%2FbtrTAa9g7TO%2F3lPjNKbNfDEy4QnKtj53sK%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;754&quot; height=&quot;243&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서 WHERE 조건절의 순서는 실제 인덱스의 사용 여부와 무관하다.&lt;br /&gt;옵티마이저는 인덱스를 사용할 수 있는 조건을 뽑아서 최적화를 수행할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 조건절에서 COL_1과 COL_2는 동등 비교 조건이고 COL_3은 범위 비교 조건이다.&lt;br /&gt;따라서 그 뒤 컬럼인 COL_4는 인덱스의 범위 결정 조건으로 사용되지 못하고 체크 조건으로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 컬럼 인덱스에서는 N-1번째 컬럼이 N번째 컬럼에 의존해 다시 정렬된다. 즉, COL_3 컬럼까지는 비교 작업의 범위를 줄이는데(작업 범위 결정 조건) 도움을 주지만 COL_4 컬럼은 COL_3에 의존적이므로 범위를 좁히지 못하고 단순히 비교 용도(필터링 조건)로만 사용되어 체크 조건으로 사용되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시들은 모두 AND 조건으로 연결되는 경우를 가정한 것이며, OR 조건으로 묶이면 더욱 복잡해진다.&lt;br /&gt;OR로 연결되면 읽어서 비교해야 할 레코드가 더 늘어나기 때문에 주의해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GROUP BY 절의 인덱스 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY 절의 각 컬럼은 비교 연산자를 가지지 않으므로 작업 범위 결정 조건이나 체크 조건을 구분해서 생각할 필요 없이 &lt;b&gt;GROUP BY 절에 명시된 컬럼의 순서가 인덱스 구성 컬럼 순서와 동일하면 인덱스를 이용할 수 있다.&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;GROUP BY 인덱스 사용 조건
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GROUP BY 절에 명시된 컬럼이 인덱스 컬럼의 순서와 같아야 한다.&lt;/li&gt;
&lt;li&gt;순서상 인덱스의 앞쪽에 있는 컬럼이 GROUP BY 절에 명시되지 않으면 인덱스를 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;GROUP BY 절에 명시된 컬럼이 하나라도 인덱스에 없으면 인덱스를 사용하지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cO1pe6/btrTy25czM2/tp2Wp17jp0HscaK8Xwx1tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cO1pe6/btrTy25czM2/tp2Wp17jp0HscaK8Xwx1tK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cO1pe6/btrTy25czM2/tp2Wp17jp0HscaK8Xwx1tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcO1pe6%2FbtrTy25czM2%2Ftp2Wp17jp0HscaK8Xwx1tK%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;696&quot; height=&quot;233&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절과 GROUP BY 절이 혼용된 쿼리가 인덱스를 사용할 수 있는지를 판별하기 위해서는 WHERE 절에서 동등 비교 조건으로 사용된 컬럼을 GROUP BY 절로 옮겨보자.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;- // 원본 쿼리
... WHERE COL_1 ='상수' ... GROUP BY COL_2, COL_3

&amp;mdash; // WHERE 조건절의 COL_1 칼럼을 GROUP BY 절의 앞쪽으로 포함시켜 본 쿼리 
... WHERE COL_1 ='상수' ... GROUP BY COL_1, COL_2, COL_3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 변경해도 똑같은 결과가 조회된다면 인덱스를 사용할 수 있는 쿼리로 판단하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ORDER BY 절의 인덱스 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORDER BY와 GROUP BY는 처리 방법이 거의 비슷하다. 따라서 인덱스 사용 조건도 거의 흡사하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 다른 점이 있다면, 정렬되는 컬럼의 오름차순 및 내림차순 옵션이 인덱스와 같거나 정반대인 경우에만 사용할 수 있다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba4hYM/btrTy2YtKFC/q73WNcbXQfuzVSfazx1oyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba4hYM/btrTy2YtKFC/q73WNcbXQfuzVSfazx1oyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba4hYM/btrTy2YtKFC/q73WNcbXQfuzVSfazx1oyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba4hYM%2FbtrTy2YtKFC%2Fq73WNcbXQfuzVSfazx1oyK%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;1920&quot; height=&quot;839&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;839&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;h2 data-ke-size=&quot;size26&quot;&gt;WHERE 조건과 ORDER BY(또는 GROUP BY) 절의 인덱스 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 사용하는 쿼리는 일반적으로 WHERE 절과 GROUP BY 절, ORDER BY 절 등을 포함한 복잡한 형태의 쿼리로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 쿼리에는 하나의 인덱스만 사용 가능하므로(index_merge 제외) 여러 개의 절이 같이 사용된 쿼리 문장은 다음 3가지 중 한 가지 방법으로만 인덱스를 이용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE 절과 ORDER BY 절이 동시에 같은 인덱스를 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE 절의 비교 조건과 ORDER BY 절의 정렬 대상이 모두 하나의 인덱스에 연속해서 포함돼 있을 때 사용 가능&lt;/li&gt;
&lt;li&gt;가장 빠른 성능을 보이므로 이 방식으로 처리될 수 있도록 튜닝하거나 인덱스를 생성하는 것이 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;WHERE 절만 인덱스 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORDER BY 절은 인덱스를 통해 검색된 레코드를 별도 정렬 처리 과정(Using Filesort)으로 처리&lt;/li&gt;
&lt;li&gt;WHERE 조건절에 일치하는 레코드 건수가 많지 않을 때 효율적인 방식(레코드 건수가 많은 경우 Using Filesort가 부하를 유발)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ORDER BY 절만 인덱스를 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORDER BY 절의 순서대로 인덱스를 읽으며 WHERE 절의 조건에 일치하는지 비교하는 형태&lt;/li&gt;
&lt;li&gt;대량의 레코드를 조회해서 정렬해야 할 때 이런 형태로 튜닝&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;783&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWlmle/btrTxH78LCW/1uBcTngVPuuOz7oW2edF50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWlmle/btrTxH78LCW/1uBcTngVPuuOz7oW2edF50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWlmle/btrTxH78LCW/1uBcTngVPuuOz7oW2edF50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWlmle%2FbtrTxH78LCW%2F1uBcTngVPuuOz7oW2edF50%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;1920&quot; height=&quot;783&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;783&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식도 마찬가지로 WHERE 절에 동등 비교로 비교된 컬럼과 ORDER BY 절에 명시된 컬럼이 순서대로 빠짐없이 인덱스에 왼쪽부터 일치해야 한다. 그렇지 않다면 역시 인덱스를 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 범위 조건 비교가 사용되는 쿼리 예제를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM tb.test WHERE COL_1 &amp;gt; 10 ORDER BY COL_1, COL_2, COL_3;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리에서 COL_1에 만족하는 값은 여러 개일 수 있지만 ORDER BY 절에서 인덱스가 순서대로 모두 명시되었기 때문에 인덱스를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음과 같은 경우 인덱스를 이용하지 못한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM tb.test WHERE COL_1 &amp;gt; 10 ORDER BY COL_2, COL_3;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COL_1이 동등 비교 조건이었다면 인덱스를 사용할 수 있었겠지만, 범위 조건이므로 ORDER BY 절에서 COL_1이 명시되지 않은 이 쿼리는 인덱스를 이용하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GROUP BY 절과 ORDER BY 절의 인덱스 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 절이 모두 하나의 인덱스를 사용해서 처리되려면 GROUP BY 절과 ORDER BY 절에 명시된 컬럼의 순서와 내용이 모두 같아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 둘 중 하나라도 인덱스를 사용할 수 없는 경우 두 절 모두에서 인덱스를 사용할 수 없다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;-- // 인덱스 사용 불가
... GROUP BY col_1, col_2 ORDER BY col_2 
... GROUP BY col_1, col_2 ORDER BY col_1, col_3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.7 버전까지는 GROUP BY 컬럼에 대한 정렬까지 함께 수행하는 것이 기본 작동 방식이지만, MySQL 8.0 버전부터는 GROUP BY 컬럼의 정렬까지 보장하지 않는 형태로 변경되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WHERE 조건과 ORDER BY 절, GROUP BY 절의 인덱스 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE, GROUP BY, ORDER BY 절이 모두 포함된 쿼리가 인덱스를 사용하는지 판단하기 위해서 다음 흐름도를 적용해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE 절이 인덱스를 사용할 수 있는가?&lt;/li&gt;
&lt;li&gt;GROUP BY 절이 인덱스를 사용할 수 있는가?&lt;/li&gt;
&lt;li&gt;GROUP BY 절과 ORDER BY 절이 동시에 인덱스를 사용할 수 있는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PohXK/btrTukshlYN/VKPJyEOZkfY8YKnyvbsbu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PohXK/btrTukshlYN/VKPJyEOZkfY8YKnyvbsbu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PohXK/btrTukshlYN/VKPJyEOZkfY8YKnyvbsbu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPohXK%2FbtrTukshlYN%2FVKPJyEOZkfY8YKnyvbsbu0%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;1920&quot; height=&quot;1181&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1181&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;h1&gt;WHERE 절의 비교 조건 사용 시 주의사항&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NULL 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 NULL 값이 포함된 레코드도 인덱스로 관리한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM titles WHERE to_date IS NULL;

+----+-------+-----+----------+-------------------------+
| id | table |type |key       |Extra                    |
+----+-------+-----+----------+-------------------------+
| 1  | titles|ref  |ix_todate | Using where;Using index |
+----+-------+-----+----------+-------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리의 실행계획에서 to_date 컬럼이 NULL인 레코드를 조회하는 쿼리지만 인덱스를 ref 방식으로 적절히 사용하고 있음을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼의 값이 NULL인지 확인할 때는 ISNULL() 함수를 사용해도 되지만, 가급적 IS NULL 연산자를 사용하는 것을 권장한다. ISNULL() 함수를 사용한 다음과 같은 쿼리 형태의 경우 인덱스를 사용하지 못하기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM titles WHERE ISNULL(to_date)=1; 
SELECT * FROM titles WHERE ISNULL(to_date)=true;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문자열이나 숫자 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 컬럼이나 숫자 컬럼을 비교할 때는 반드시 그 타입에 맞는 상수값을 사용해야 한다. 컬럼의 타입에 맞게 상수 리터럴을 비교 조건에 사용하는 것은 인덱스 사용 여부에도 영향을 미치므로 아주 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;날짜 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에는 날짜만 저장하는 DATE 타입과 날짜와 시간을 함께 저장하는 DATETIME과 TIMESTAMP 타입이 있으며, 시간만 저장하는 TIME 타입도 있기 때문에 각 비교 조건들이 상당히 헷갈릴 수 있다.&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;DATE 또는 DATETIME과 문자열 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DATE 또는 DATETIME 타입의 값과 문자열을 비교할 때는 문자열 값을 자동으로 DATETIME 타입의 값으로 변환해서 비교를 수행한다. 문자열을 DATE나 DATETIME 타입으로 명시적으로 변환하지 않아도 MySQL이 내부적으로 변환을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔씩 아래와 같이 DATE나 DATETIME 타입의 컬럼을 문자열로 변경하는 경우가 있는데, 이는 인덱스를 효율적으로 사용할 수 없으니 가능하면 &lt;b&gt;컬럼을 변경하지 말고 상수를 변경하는 형태로 조건을 사용하는 것이 좋다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- // 인덱스 사용 X
SELECT COUNT(*) FROM employees
WHERE DATE_FORMAT(hire_date,'%Y-%m-%d') &amp;gt; '2011-07-23' ;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 날짜 타입 컬럼의 값을 더하거나 빼는 함수로 변형한 후 비교해도 인덱스를 사용할 수 없다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- // 인덱스 사용 X
SELECT COUNT(*) FROM employees
WHERE DATE_ADD(hire_date, INTERVAL 1 YEAR) &amp;gt; '2011-07-23' ;

-- // 인덱스 사용 O -&amp;gt; 상수를 변경하는 형태 권장
SELECT COUNT(*) FROM employees
WHERE hire_date &amp;gt; DATE_SUB('2011-07-23', INTERVAL 1 YEAR);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DATE와 DATETIME의 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DATETIME 값을 DATE 타입으로 만들지 않고 비교하면 MySQL 서버가 DATE 타입의 값을 DATETIME으로 변환해서 비교를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DATE 타입인 &amp;ldquo;2022-11-19&amp;rdquo; 값을 DATETIME 타입인 &amp;ldquo;2022-11-19 00:00:00&amp;rdquo;으로 변환해서 비교를 수행한다. 따라서 DATE와 DATETIME을 비교할 때는 성능보다는 쿼리 결과에 주의해서 사용해야 한다.&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;DATETIME과 TIMESTAMP의 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DATE 혹은 DATETIME 타입 값과 TIMESTAMP의 값을 별도 타입 변환 없이 비교하면 원하는 결괏값을 얻을 수 없다. 반드시 비교 값으로 사용되는 상수 리터럴을 비교 대상 컬럼의 타입으로 변환해서 사용하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DATETIME 타입이라면 FROM_UNIXTIME() 함수를 이용해 DATETIME 타입으로 만들어 비교해야 한다. 반대로 TIMESTAMP라면 UNIX_TIMESTAMP() 함수를 이용해 DATETIME을 TIMESTAMP로 변환해 비교해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FROM_UNIXTIME()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TIMESTAMP &amp;rarr; DATETIME&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;UNIX_TIMESTAMP()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DATETIME &amp;rarr; TIMESTAMP&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Short-Circuit Evaluation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Short-circuit Evaluation이란 여러 개의 표현식이 AND 또는 OR 연산자로 연결된 경우 선행 표현식의 결과에 따라 뒤에 연산을 평가할지 말지 결정하는 최적화를 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 다음과 같은 표현식이 있다고 가정해보자.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;if (isMember() &amp;amp;&amp;amp; hasName()) {
    ....
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isMember() 함수가 True를 반환한다면 후행 표현식인 hasName() 함수를 호출해 결과를 확인해야 한다. 하지만 isMember() 함수가 False를 반환한다면 hasName()을 검사할 필요가 없다. 후행 표현식을 확인하지 않아도 결과는 False를 반환하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Short-circuit Evaluation는 쿼리 성능에 영향을 준다.&lt;br /&gt;&lt;b&gt;WHERE 조건절에 명시된 조건이 인덱스를 사용할 수 없는 경우&lt;/b&gt; 순서에 따라 조회하는 레코드 건수가 달라질 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 결과를 반환하는 쿼리를 살펴보자.&lt;br /&gt;두 쿼리 모두 인덱스를 사용하지 못하고 풀 테이블 스캔을 사용한다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- // 1번 조건을 만족하는 레코드 건수
SELECT COUNT(*) FROM salaries
WHERE CONVERT_TZ(from_date,'+00:00', '+09:00') &amp;gt; '1991-01-01';
+----------+
| COUNT(*) |  
+----------+
|  2442943 |
+----------+

-- // 2번 조건을 만족하는 레코드 건수
SELECT COUNT(*) FROM salaries
WHERE to_date &amp;lt; '1985-01-01' ;
+----------+
| COUNT(*) |  
+----------+
|        0 |
+----------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위의 두 쿼리를 하나의 쿼리로 조합하고 WHERE 조건의 순서만 변경해서 결과를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- // (1번 조건, 2번 조건) 순으로 조합
SELECT * FROM salaries
WHERE CONVERT_TZ(from_date,'+00:00', '+09:00') &amp;gt; '1991-01-01'
AND to_date &amp;lt; '1985-01-01';

==&amp;gt; (0.73 sec)

-- // (2번 조건, 1번 조건) 순으로 조합
SELECT * FROM salaries
WHERE to_date &amp;lt; '1985-01-01'
AND CONVERT_TZ(from_date,'+00:00', '+09:00') &amp;gt; '1991-01-01';

==&amp;gt; (0.52 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 성능에 차이가 있는 걸 확인할 수 있다.&lt;br /&gt;현재는 근소한 차이지만 만약 더 많은 자원을 소모하는 조건이었다면 성능 차이는 더욱 커졌을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 MySQL 서버는 쿼리의 WHERE 절에 나열된 조건을 순서대로 Short-circuit Evaluation 방식으로 평가해 레코드를 반환할지 말지를 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 WHERE 절의 조건 중 인덱스를 사용할 수 있는 조건이 있다면 MySQL 서버는 그 조건을 최우선으로 사용하며, 나열된 조건의 순서가 인덱스 사용 여부를 결정하지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL 서버에서 쿼리를 작성할 때 가능하면 복잡한 연산 또는 다른 테이블의 레코드를 읽어야 하는 서브쿼리 조건 등은 WHERE 절의 뒤쪽으로 배치하는 것이 성능상 도움이 된다는 것을 알아두자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DISTINCT&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LIMIT n&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LIMIT는 쿼리 결과에서 지정된 순서에 위치한 레코드만 가져오고자 할 때 사용한다.&lt;br /&gt;일반적으로 MySQL 서버에서 LIMIT는 쿼리의 가장 마지막에 실행된다. 따라서 GROUP BY 절이나 DISTINCT 등과 같이 사용됐을 때 어떻게 동작하는지 알아둘 필요가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;SELECT * FROM employees LIMIT 0, 10;&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 쿼리는 풀 테이블 스캔을 하면서 10개의 레코드를 읽어 들인 시점에 읽기 작업을 멈춘다.&lt;/li&gt;
&lt;li&gt;이러한 형태의 쿼리는 LIMIT를 이용해 쿼리를 상당히 빨리 끝낼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELECT * FROM employees GROUP BY first_name LIMIT 0, 10;&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 쿼리는 GROUP BY 처리가 완료되고 LIMIT 처리를 수행한다.&lt;/li&gt;
&lt;li&gt;따라서 LIMIT가 실질적 서버 작업을 크게 줄여주지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELECT DISTINCT first_name FROM employees LIMIT 0, 10;&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 쿼리는 중복 제거 작업(DISTINCT)을 반복 처리하다 유니크한 레코드가 10건 채워지면 작업을 중지한다.&lt;/li&gt;
&lt;li&gt;LIMIT 절을 활용해 작업량을 상당히 줄인 쿼리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LIMIT 주의 사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LIMIT의 인자로 표현식이나 별도의 서브쿼리를 사용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM employees LIMIT (100-10) ;
ERROR 1064 (42000) :You have an error in your SQL syntax;check the manual that corresponds to your MySQL server version for the right syntax to use near '(100-10)' at line 1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이징 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM salaries ORDER BY salary LIMIT 0,10;
10 rows in set (0.00 sec)

SELECT * FROM salaries ORDER BY salary LIMIT 2000000,10;
10 rows in set (1.57 sec)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 10건의 레코드를 가져오는 결과는 같지만 MySQL 서버가 처리하는 작업 내용이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 쿼리의 경우 2,000,010건의 레코드를 읽은 후 2,000,000건은 버리고 마지막 10건만 사용자에게 반환한다. 이러한 비효율을 막기 위해서는 다음과 같이 WHERE 조건절로 읽어야 할 위치를 찾고 그 위치에서 10건만 반환하도록 쿼리를 변경하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM salaries
WHERE salary &amp;gt;= 154888 AND NOT(salary=154888 AND emp_no &amp;lt;= 109334) 
ORDER BY salary LIMIT 0, 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서&lt;code&gt;NOT(salary=154888 AND emp_no &amp;lt;= 109334)&lt;/code&gt; 조건을 명시한 이유는 중복이나 누락을 방지하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복을 허용하는 인덱스를 사용하는 경우 단순히 이전 페이지의 마지막 값보다 큰 값을 조회하거나 크거나 같은 경우를 조회하면 중복이나 누락이 발생할 수 있기 때문이다. &lt;b&gt;페이징을 위한 쿼리 작성 시 주의해야 할 부분이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;COUNT()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COUNT() 함수는 결과 레코드의 건수를 반환하는 함수다.&lt;br /&gt;InnoDB 스토리지 엔진에서는 WHERE 조건이 없는 COUNT(*) 쿼리라고 해도 데이터나 인덱스를 읽어야 레코드 건수를 가져올 수 있다. 따라서 큰 테이블에서 COUNT() 함수를 사용할 때는 주의를 기울어야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의 사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORDER BY나 LEFT JOIN과 같은 &lt;b&gt;레코드 건수를 가져오는 것과 무관한 작업을 포함&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;COUNT(*) 쿼리에 ORDER BY 절은 어떤 경우에도 필요하지 않다.&lt;br /&gt;또한, LEFT JOIN도 레코드 건수의 변화가 없는 경우나 아우터 테이블에서 별도 체크를 하지 않아도 된다면 제거하는 것이 성능상 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인덱스를 사용하지 못하는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이징해서 데이터를 가져오는 것보다 몇 배 ~ 몇십 배 느리게 실행될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;레코드의 결과가 NULL이 아닌 레코드 건수만 반환
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NULL이 될 수 있는 컬럼은 의도대로 작동하지 않을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>BackEnd/Real MySQL 8.0</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/179</guid>
      <comments>https://jjingho.tistory.com/179#entry179comment</comments>
      <pubDate>Tue, 13 Dec 2022 19:20:29 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 실행 계획 분석하기(Extra)</title>
      <link>https://jjingho.tistory.com/178</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Extra 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획에서 성능과 관련된 중요한 내용이 Extra 컬럼에 자주 표시된다.&lt;br /&gt;주로 내부적인 처리 알고리즘에 대해 깊이 있는 내용을 보여주는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Extra 컬럼에 표시될 수 있는 문장을 하나씩 자세히 살펴보자.&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;const row not found&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획에서 const 접근 방법으로 테이블을 읽었지만 실제로 해당 테이블에 레코드가 1건도 존재하지 않으면 나타나는 문장이다.&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;Deleting all rows&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토리지 엔진의 핸들러 차원에서 테이블의 모든 레코드를 삭제하는 기능을 제공하는 경우 해당 문구가 표시된다. WHERE 조건절이 없는 DELETE 문장의 실행 계획에서 자주 표시되며, 모든 레코드를 삭제하는 핸들러 API를 호출함으로써 처리됐다는 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 버전에서는 Deleting all rows 최적화는 표시되지 않는다.&lt;br /&gt;테이블의 모든 레코드를 삭제하는 경우 WEHERE 조건절이 없는 DELETE 보다 TRUNCATE TABLE 명령을 사용할 것을 권장하고 있다.&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;Distinct&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회하려는 값을 중복 없이 유니크하게 가져오기 위해 DISTINCT 키워드를 사용하면 Extra 컬럼에 Distinct가 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qcn5K/btrQRFZ5pkw/6MwUIdgTOivNokXfRihLE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qcn5K/btrQRFZ5pkw/6MwUIdgTOivNokXfRihLE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qcn5K/btrQRFZ5pkw/6MwUIdgTOivNokXfRihLE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQcn5K%2FbtrQRFZ5pkw%2F6MwUIdgTOivNokXfRihLE1%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;1424&quot; height=&quot;410&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시처럼 두 테이블을 조인해서 dept_no만 중복없이 유니크하게 가져오기 위해 DISTINCT 키워드를 사용한다. 쿼리의 DISTINCT를 처리하기 위해 조인하지 않아도 되는 항목은 모두 무시하고 dept_emp 테이블에서는 필요한 레코드만 읽은 것을 볼 수 있다.&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;FirstMatch&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세미 조인의 여러 최적화 중에서 FirstMatch 전략이 사용되면 FirstMatch(table_name)이 표시된다.&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;Full scan on NULL key&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;col1 IN (SELECT col2 FROM &amp;hellip;)&lt;/code&gt; 과 같은 조건을 가진 쿼리에서 자주 발생하는 표시 값이다. 만약 col1이 NULL이면 서브쿼리에 사용된 테이블에 대해 풀 테이블 스캔을 해야만 결과를 알아낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Full scan on NULL key는 쿼리 실행 중 col1이 NULL을 만나면 풀 테이블 스캔을 사용할 것이라는 사실을 알려주는 키워드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 col1이 NOT NULL로 정의된 컬럼이라면 해당 키워드는 아예 표시되지 않는다. 또한, col1이 절대 NULL이 아님을 옵티마이저에게 알려주면(IS NOT NULL) 옵티마이저는 이러한 NULL 비교 규칙을 무시하게 된다.&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;Impossible HAVING&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리에 사용된 HAVING 절의 조건을 만족하는 레코드가 없을 때 나타나는 키워드다. 실행 계획에 이 키워드가 출력된다면 쿼리가 제대로 작성되지 못한 경우가 대부분이므로 쿼리를 다시 점검하는 것이 좋다.&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;Impossible WHERE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 비슷하게 WHERE 조건이 항상 False인 경우 나타나는 키워드다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM employees WHERE emp_no IS NULL:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리에서 emp_no는 PK이므로 NULL이 될 수 없다. 따라서 emp_no IS NULL이라는 조건은 불가능한 WHERE 조건이므로 Extra 컬럼에 Impposible WHERE 키워드가 표시된다.&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;LooseScan&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세미조인 최적화 중 LooseScan 최적화 전략이 사용된 경우 LooseScan 키워드가 표시된다.&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;No mathcing min / max row&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MIN()이나 MAX() 같은 집합 함수가 있는 쿼리의 조건절에 일치하는 레코드가 없는 경우 No machting min/max row 메시지가 출력된다.&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;no matching row in const table&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인에 사용된 테이블에서 const 방법으로 접근할 때 일치하는 레코드가 없다면 나타나는 메시지다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM dept_emp de,
  (SELECT emp_no FROM employees WHERE emp_no = 0) tb1
WHERE tb1.emp_no = de.emp_no AND de.dept_no='d0O5' ;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인에 사용된 테이블의 조건이 const(상수) 방법을 사용하고 있지만 일치하는 레코드가 없는 경우 실행 계획을 만들기 위한 기초 자료가 없기 때문에 no matching row in const table 메시지가 표시된다.&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;No matching rows after partition pruning&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 메시지는 파티션된 테이블에 대한 UPDATE나 DELETE 할 대상 레코드가 없는 경우 표시된다. 단순히 삭제할 레코드가 없음을 의미하는 것이 아니라 대상 파티션이 없다는 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;No tables used&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 그대로 FROM 절이 없는 쿼리나 FROM DUAL 형태의 쿼리 실행 계획에서 출력되는 메시지다.&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;Not exists&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아우터 조인(LEFT OUTER JOIN)을 이용해 안티-조인을 수행하는 쿼리에서는 Not exists 메시지가 표시된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안티-조인이란?&lt;br /&gt;A 테이블에는 존재하지만 B 테이블에는 없는 값을 조회하는 형태의 쿼리를 말한다. 즉, 일반 조인을 했을 때 나오지 않는 결과만 가져오는 방법이다. 일반적으로 NOT IN이나 NOT EXISTS 연산자를 주로 이용한다. 레코드가 많은 경우 아우터 조인을 이용해 구현하면 성능을 더 끌어올릴 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Not exists 메시지는 옵티마이저가 테이블 조인 조건에 일치하는 레코드의 존재 유무를 딱 1건만 조회해보고 처리를 완료하는 최적화를 말한다.&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;Plan isn&amp;rsquo;t ready yet&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0부터는 다른 커넥션에서 실행 중인 쿼리의 실행 계획을 &lt;code&gt;EXPLAIN FOR CONNECTION&lt;/code&gt; 명령을 통해 살펴볼 수 있다. Plan isn&amp;rsquo;t ready yet 메시지는 이름 그대로 조회한 커넥션이 쿼리 실행 계획을 수립하지 못한 상태에서 &lt;code&gt;EXPLAIN FOR CONNECTION&lt;/code&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;Range checked for each record(index map:N)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인 조건에 상수가 없고 둘 다 변수를 사용하는 경우 인덱스 레인지 스캔과 풀 테이블 스캔 중 어느 것이 더 효율적인지 옵티마이저는 판단할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해를 돕기 위해 아래 예제를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN 
SELECT *
FROM employees e1, employees e2 
WHERE e2.emp_no &amp;gt;= e1.emp_no;

+------+------------+-------+-------+-----------------------------------------------+
|id    |select_type | table | type  | Extra                                         |
+------+------------+-------+-------+-----------------------------------------------+
| 1    |SIMPLE      | e1    | ALL   | NULL                                          |
| 1    |SIMPLE      | e2    | ALL   | Range checked for each record (index map: 0x1 |
+------+------------+-------+-------+-----------------------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;e1 테이블의 레코드를 하나씩 읽을 때마다 e1.emp_no 값이 계속 바뀌므로 쿼리 비용 계산을 위한 기준 값이 계속 변하게 된다. 따라서 옵티마이저는 어떤 접근 방법으로 e2 테이블을 읽는 것이 효율적인지 판단할 수 없게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Range checked for each record는 &amp;ldquo;레코드마다 인덱스 레인지 스캔을 체크한다.&amp;rdquo;라는 의미를 가지고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQjb5X/btrQUVUtJGV/zVGVVmxCkT5K1uVb84RFAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQjb5X/btrQUVUtJGV/zVGVVmxCkT5K1uVb84RFAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQjb5X/btrQUVUtJGV/zVGVVmxCkT5K1uVb84RFAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQjb5X%2FbtrQUVUtJGV%2FzVGVVmxCkT5K1uVb84RFAk%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;704&quot; height=&quot;396&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 살펴보면 &lt;code&gt;index map: 0x1&lt;/code&gt; 이라는 메시지도 함께 표시되어 있다. 이는 후보 인덱스의 순번을 나타내며, 해석하기 위해선 이진수로 변환을 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 다음과 같은 테이블의 실행 계획에서 &lt;code&gt;index map: 0x19&lt;/code&gt;라는 값이 표시됐다고 가정해보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE tb_member( 
 mem_id INTEGER NOT NULL,
 mem_name VARCHAR(100) NOT NULL, 
 mem_nickname VARCHAR(100) NOT NULL, 
 mem_region TINYINT,
 mem_gender TINYINT,
 mem_phone VARCHAR(25),
 PRIMARY KEY (mem_id),
 INDEX ix_nick_name (mem_nickname, mem_name), 
 INDEX ix_nick_region (mem_nickname, mem_region), 
 INDEX ix_nick_gender (mem_nickname, mem_gender), 
 INDEX ix_nick_phone (mem_nickname, mem_phone)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 인덱스는 총 5개이고 0x19를 이진수로 변환하면 11001이다.&lt;br /&gt;이 비트 배열을 해석하는 방법은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpSOS4/btrQUT3rpKq/Nls699lv1ijeTkFQ5tBtGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpSOS4/btrQUT3rpKq/Nls699lv1ijeTkFQ5tBtGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpSOS4/btrQUT3rpKq/Nls699lv1ijeTkFQ5tBtGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpSOS4%2FbtrQUT3rpKq%2FNls699lv1ijeTkFQ5tBtGk%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;1652&quot; height=&quot;272&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 값이 1인 인덱스를 사용 가능한 인덱스 후보로 선정했음을 의미한다.&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;Recursive&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0부터는 CTE(Common Table Expression)을 이용해 재귀 쿼리를 작성할 수 있게 됐다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;WITH RECURSIVE cte (n) AS 
(
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM cte WHERE n &amp;lt; 5
)
SELECT * FROM cte;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 WITH 구문을 이용해 CTE를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리의 WITH 절의 동작을 살펴보면 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;n이라는 컬럼 하나를 가진 cte라는 이름의 임시 테이블을 생성&lt;/li&gt;
&lt;li&gt;n 컬럼의 값이 1부터 5까지 1씩 증가해서 레코드 5건을 만들어 cte 내부 임시 테이블에 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 WITH 절 다음의 SELECT 쿼리에서는 생성된 임시 내부 테이블을 풀 스캔해 결과를 반환한다. 이때 실행계획에 Recursive 구문이 표시된다.&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;Rematerialize&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;래터럴 조인(LATERAL JOIN)되는 테이블은 선행 테이블의 레코드 별로 서브쿼리를 실행해 그 결과를 임시 테이블에 저장한다. 이 과정을 Rematerializing이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM employees e
 LEFT JOIN LATERAL (SELECT *
                    FROM salaries s
                    WHERE s.emp_no=e.emp_no
                    ORDER BY s.from_date DESC LIMIT 2) s2 ON s2.emp_no = e.emp_no
WHERE e.first_name='Matt' ;

+------+------------------+------------+-------+--------------+----------------------------+
|id    |select_type       | table      | type  | key          | Extra                      |
+------+------------------+------------+-------+--------------+----------------------------+
| 1    |PRIMARY           | e          | ref   | ix_firstname | Rematerialize (&amp;lt;derived2&amp;gt;) |
| 1    |PRIMARY           | &amp;lt;derived2&amp;gt; | ref   | &amp;lt;auto_key0&amp;gt;  | NULL                       |
| 2    |DEPENDENT DERIVED | s          | ref   | PRIMARY      | Using filesort             |
+------+------------------+------------+-------+--------------+----------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리의 실행 계획에서는 조인되는 서브쿼리를 임시 테이블 derived2로 저장하고 employees 테이블과 생성된 임시 테이블을 조인한다. 그런데 derived2 임시 테이블은 employees 테이블의 레코드마다 새로 내부 임시 테이블에 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 매번 임시 테이블이 새로 생성되는 경우 Rematerialize 문구가 표시된다.&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;Select tables optimized away&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MIN() 또는 MAX()만 SELECT 절에 사용되거나 GROUP BY로 MIN(), MAX()를 조회하는 쿼리가 인덱스를 오름차순 또는 내림차순으로 1건만 읽는 형태의 최적화가 적용되면 해당 메시지가 표시된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT MAX(emp_no), MIN(emp_no) FROM employees;

+------+------------+-------+------+------+------------------------------+
|id    |select_type | table | type | key  | Extra                        |
+------+------------+-------+------+------+------------------------------+
| 1    |SIMPLE      | NULL  | NULL | NULL | Select tables optimized away |
+------+------------+-------+------+------+------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;epm_no는 PK이므로 인덱스를 사용할 수 있고, 이미 정렬되어 있으므로 &lt;code&gt;SELECT MAX(emp_no), MIN(emp_no)&lt;/code&gt; 구문은 인덱스의 첫 번째 레코드와 마지막 레코드만 읽어서 최솟값, 최댓값을 가져올 수 있다. 따라서 Select tables optimized away 최적화가 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vOxwF/btrQTjI6Fxc/6dVbpoKcXgwF6mL9hm30AK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vOxwF/btrQTjI6Fxc/6dVbpoKcXgwF6mL9hm30AK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vOxwF/btrQTjI6Fxc/6dVbpoKcXgwF6mL9hm30AK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvOxwF%2FbtrQTjI6Fxc%2F6dVbpoKcXgwF6mL9hm30AK%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;453&quot; height=&quot;390&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, WHERE 절이 포함된 쿼리라도 Select tables optimized away 최적화가 가능하다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT MAX(from_date), MIN(from_date) FROM salaries WHERE emp_no=10002;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;salaries 테이블에 (emp_no, from_date) 인덱스가 생성되어 있는 경우, 먼저 emp_no = 10002 인 레코드를 검색하고 첫 번째와 마지막 레코드를 가져올 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zLzr3/btrQVqzVyph/C3byrRkOSHU73u6sdn9W21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zLzr3/btrQVqzVyph/C3byrRkOSHU73u6sdn9W21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zLzr3/btrQVqzVyph/C3byrRkOSHU73u6sdn9W21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzLzr3%2FbtrQVqzVyph%2FC3byrRkOSHU73u6sdn9W21%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;479&quot; height=&quot;417&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;644&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;h3 data-ke-size=&quot;size23&quot;&gt;Start temporary, End temporary&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세미 조인 최적화 중 Duplicate Weed-out 최적화 전략이 사용되면 해당 문구가 표시된다. Duplicate Weed-out 최적화 전략은 불필요한 중복을 제거하기 위해 내부 임시 테이블을 사용하는데, 조인되어 내부 임시 테이블에 저장되는 테이블을 식별할 수 있도록 첫 번째 테이블에 Start temporary 문구를 보여주고 끝나는 부분에 End temporary를 표시해준다.&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;Using filesort&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORDER BY 처리가 인덱스를 사용하지 못할 때 Using filesort가 표시된다.&lt;br /&gt;이는 조회된 레코드를 정렬용 메모리 버퍼(Sort buffer)에 복사해 정렬을 수행하게 된다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 메시지가 표시되는 쿼리는 많은 부하를 일으키므로 쿼리 튜닝이나 인덱스를 생성하는 것이 좋다.&lt;/b&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;Using index(커버링 인덱스)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 파일 접근 없이 인덱스만 읽어서 쿼리를 처리할 수 있는 경우 Using index가 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;employees 테이블에 firstname에 대한 인덱스를 생성하면 다음과 같은 리프 노드를 가지게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YyXJS/btrQVzwLBPA/qfZ1SJZ75vLoLmzpkGb1j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YyXJS/btrQVzwLBPA/qfZ1SJZ75vLoLmzpkGb1j1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YyXJS/btrQVzwLBPA/qfZ1SJZ75vLoLmzpkGb1j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYyXJS%2FbtrQVzwLBPA%2FqfZ1SJZ75vLoLmzpkGb1j1%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;1210&quot; height=&quot;946&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;946&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 인덱스는 테이블의 PK를 데이터 파일에 접근하는 주소로 사용하므로 (firstname, emp_no)와 같은 인덱스를 생성한 효과를 가진다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT first_name
FROM employees
WHERE first_name BETWEEN 'Babette' AND 'Gad';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리를 실행하면 ix_firstname 인덱스를 검색하게 된다.&lt;br /&gt;SELECT에 필요한 컬럼은 first_name이므로 데이터 파일을 조회할 필요 없이 인덱스에서 데이터를 가져올 수 있다. firstname은 이미 인덱스에 (first_name, emp_no) 형태로 존재하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;인덱스만으로 처리되는 것을 커버링 인덱스&lt;/b&gt;라고 하며, 데이터 파일을 읽어올 필요가 없어 매우 빠르게 처리된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT first_name, birth_date
FROM employees
WHERE first_name BETWEEN 'Babette' AND 'Gad';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위와 같이 조회 컬럼을 변경하면 커버링 인덱스를 사용하지 못하고 데이터 파일에 접근하게 된다. 인덱스에 존재하는 정보만으로 쿼리를 처리할 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드 건수에 따라 차이는 있겠지만 커버링 인덱스로 처리할 수 있을 때와 그렇지 못할 때의 성능 차이는 수십 ~ 수백 배까지 날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;Using index condition&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 컨디션 푸시다운 최적화를 사용하면 Using index condition 메시지가 표시된다.&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;Using index for group-by&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 활용해 GROUP BY가 처리될 때 Using index for group-by가 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY 처리는 그루핑 기준 컬럼을 이용해 정렬 작업을 수행하고 다시 정렬된 결과를 그루핑하는 고부하 작업을 필요로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;Using index for skip scan&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스 스킵 스캔 최적화&lt;/b&gt;를 사용하면 Using index for skip scan이 표시된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 스킵 스캔이란?&lt;br /&gt;인덱스 최적화 기능으로 조건절에 첫 번째 인덱스가 없어도 두 번째 인덱스만으로 인덱스를 검색할 수 있게 해주는 기능이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Using join buffer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인 버퍼가 사용되는 경우 Using join buffer 메시지가 표시된다.&lt;br /&gt;실제 조인에 필요한 인덱스는 드리븐 테이블에만 필요하다.(드리븐 테이블이 검색 위주로 사용되기 때문에)&lt;br /&gt;MySQL 옵티마이저는 인덱스가 없는 테이블이 있으면 그 테이블을 드라이빙 테이블로 선정하고 조인을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 드리븐 테이블에 적절한 인덱스가 없다면 MySQL 서버는 블록 네스티드 루프 조인이나 해시 조인을 사용하는데, 이때 조인 버퍼가 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Using join buffer는 실제 조인을 수행하면서 조인 버퍼를 활용했다는 것을 의미한다.&lt;br /&gt;해당 메시지 뒤에는 다음과 같이 어떻게 처리되었는지도 함께 표시된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Using join buffer(Block Nested Loop)&lt;/li&gt;
&lt;li&gt;Using join buffer(Batched Key Access)&lt;/li&gt;
&lt;li&gt;Using join buffer(hash join)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Using MRR&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MRR(Multi Range Read)은 스토리지 엔진이 MySQL 엔진이 넘겨주는 키 값을 기준으로 한 건씩 읽어서 반환하는 방식의 한계점을 최적화하기 위해 도입됐다.(레코드 단위로 API 호출이 필요하기 때문에 디스크 접근이 많이 발생한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MRR의 개념은 MySQL 엔진이 여러 개의 키 값을 한 번에 스토리지 엔진에게 전달하고, 스토리지 엔진은 키 값을 정렬해서 최소한의 페이지 접근만 할 수 있도록 최적화하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Extra 컬럼에 Using MRR 메시지가 표시된다면 MRR 엔진을 사용한 최적화가 이루어졌다는 것을 의미한다.&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;Using sort_union(&amp;hellip;), Using union(&amp;hellip;), Using intersect(&amp;hellip;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획에 type 컬럼이 index_merge 접근 방법으로 실행되는 경우 2개 이상의 인덱스가 동시에 사용될 수 있다. 이때 두 인덱스로부터 읽은 결과를 어떻게 병합했는지 다음과 같은 메시지로 상세히 표시해준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Using intersect(&amp;hellip;)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 사용할 수 있는 조건이 AND 조건으로 연결된 경우 교집합을 추출해 내는 작업을 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Using union(&amp;hellip;)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 사용할 수 있는 조건이 OR 조건으로 연결된 경우 합집합을 추출해 내는 작업을 수행&lt;/li&gt;
&lt;li&gt;일반적으로 동등 비교처럼 일치하는 레코드 건수가 많지 않은 경우 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Using sort_union(&amp;hellip;)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Using union과 같은 작업을 수행하지만 Using union으로 처리될 수 없는 경우, 프라이머리 키만 먼저 읽어서 정렬하고 병합한 후 레코드를 읽어서 반환하는 작업을 수행&lt;/li&gt;
&lt;li&gt;크다 혹은 작다 처럼 상대적으로 많은 레코드 일치가 일어나는 경우 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Using temporary&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;임시 테이블이 사용된 경우 표시되는 메시지&lt;/b&gt;다.&lt;br /&gt;실제 임시 테이블은 디스크 또는 메모리에 생성될 수 있는데, Using temporary 메시지만으로는 메모리에 생성됐는지 디스크에 생성됐는지 판단할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메시지가 표시되는 대표적인 예로, &lt;b&gt;인덱스를 사용하지 못하는 GROUP BY 쿼리&lt;/b&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;Using where&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 엔진 레이어에서 별도의 가공을 통해 필터링 작업을 처리한 경우 Using where 메시지가 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버는 MySQL 엔진과 스토리지 엔진이라는 두 개의 레이어로 나눠져 있다.&lt;br /&gt;스토리지 엔진은 레코드를 읽거나 저장하는 역할을 하고, MySQL 엔진은 스토리지 엔진으로부터 전달받은 레코드를 조인, 필터링 등과 같은 가공 또는 연산 작업을 수행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mgsaF/btrQSFTcgnZ/P0iuuaJ3OIBlqWOCyfgzt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mgsaF/btrQSFTcgnZ/P0iuuaJ3OIBlqWOCyfgzt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mgsaF/btrQSFTcgnZ/P0iuuaJ3OIBlqWOCyfgzt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmgsaF%2FbtrQSFTcgnZ%2FP0iuuaJ3OIBlqWOCyfgzt0%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;598&quot; height=&quot;219&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 스토리지 엔진에서 200건의 레코드를 읽고, MySQL 엔진에서 별도의 필터링이나 가공 처리 필요 없이 클라이언트에게 전달하면 Using where 메시지는 표시되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해를 돕기 위해 다음 쿼리를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN 
SELECT *
FROM employees
WHERE emp_no BETWEEN 10001 AND 10100
   AND gender='F';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 범위 결정 조건은 &lt;code&gt;emp_no BETWEEN 10001 AND 10100&lt;/code&gt; 이며, &lt;code&gt;AND gender='F'&lt;/code&gt; 는 체크 조건이다.&lt;br /&gt;즉, 작업 범위에 해당하는 레코드는 총 100건이지만 체크 조건까지 만족하는 레코드는 37건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크 조건은 MySQL 엔진에서 처리됨으로 MySQL 엔진에서 63건의 레코드가 필터링되어 버려진 것을 알 수 있다. 따라서 실행 계획에는 Using where 메시지가 표시된다.&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;Zero limit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리의 결과가 몇 개의 컬럼을 가지고, 각 컬럼의 타입은 무엇인지 등의 메타 데이터만 필요한 경우 쿼리 마지막에 &lt;code&gt;LIMIT 0&lt;/code&gt; 을 사용하면 되는데, 이때 옵티마이저는 사용자의 의도를 알아채고 결괏값의 메타 정보만 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Extra 컬럼에 표시되는 메시지가 Zero limit다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>BackEnd/Real MySQL 8.0</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/178</guid>
      <comments>https://jjingho.tistory.com/178#entry178comment</comments>
      <pubDate>Thu, 10 Nov 2022 23:20:25 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 실행 계획 분석하기(type ~ filtered)</title>
      <link>https://jjingho.tistory.com/177</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;type 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리의 실행 계획에서 type 이후의 컬럼은 테이블 레코드를 어떤 방식으로 읽었는지를 나타낸다. 일반적으로 &lt;b&gt;쿼리 튜닝에서 인덱스를 효율적으로 사용하는지 확인하는 것이 중요하므로 type 컬럼은 반드시 체크해야 하는 중요한 정보다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 12개 접근 방법은 type 컬럼에 표시될 수 있는 값들 중 성능이 빠른 순서대로 나열한 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;system&lt;/li&gt;
&lt;li&gt;const&lt;/li&gt;
&lt;li&gt;eq_ref&lt;/li&gt;
&lt;li&gt;ref&lt;/li&gt;
&lt;li&gt;fulltext&lt;/li&gt;
&lt;li&gt;ref_or_null&lt;/li&gt;
&lt;li&gt;unique_subquery&lt;/li&gt;
&lt;li&gt;index_subquery&lt;/li&gt;
&lt;li&gt;range&lt;/li&gt;
&lt;li&gt;index_merge&lt;/li&gt;
&lt;li&gt;index&lt;/li&gt;
&lt;li&gt;ALL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 ALL을 뺀 나머지는 모두 index를 사용한 접근 방법이며, index_merge를 제외한 나머지 접근 방법은 하나의 인덱스만 사용한다. 따라서 &lt;b&gt;type 컬럼에도 라인 별로 하나의 인덱스 이름만 표시된다.&lt;/b&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;system&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드가 1건만 존재하는 테이블 또는 한 건도 존재하지 않는 테이블을 참조하는 형태의 접근 방법이다. InnoDB 스토리지 엔진에서는 나타나지 않고 MyISAM이나 MEMORY에서만 사용되는 접근 방법이다.&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;const&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 레코드 건수와 관계없이 &lt;b&gt;쿼리가 프라이머리 키나 유니크 키 컬럼을 이용하는 where 조건절을 가지고 있으며, 반드시 1건을 반환하는 쿼리의 처리 방식&lt;/b&gt;을 const라고 한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM employees WHERE emp_no = 10001;

+------+------------+------------+------+---------+---------+
|id    |select_type | table      | type | key     | key_len |
+------+------------+------------+------+---------+---------+
| 1    |SIMPLE      | employees  | const| PRIMARY |       4 |
+------+------------+------------+------+---------+---------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식을 다른 DBMS에서는 &lt;b&gt;유니크 인덱스 스캔&lt;/b&gt;이라고 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM dept_emp WHERE dept_no='d005' ;

+------+------------+------------+------+---------+---------+
|id    |select_type | table      | type | key     | rows    |
+------+------------+------------+------+---------+---------+
| 1    |SIMPLE      | dept_emp   | ref  | PRIMARY | 165571  |
+------+------------+------------+------+---------+---------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 컬럼으로 구성된 프라이머리 키나 유니크 키 중에서 인덱스의 일부 컬럼만 조건으로 사용할 때는 const 타입의 접근 방법을 사용할 수 없다. MySQL 엔진이 데이터를 읽어보지 않고서는 레코드가 1건이라는 것을 확신할 수 없기 때문이다. 따라서 프라이머리 키의 일부만 조건으로 사용할 때는 type 컬럼에 ref로 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음과 같이 &lt;b&gt;프라이머리 키나 유니크 인덱스의 모든 컬럼을 동등 조건으로 WEHRE 절에 명시하면 const 접근 방법을 사용&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM dept_emp WHERE dept_no='d005' and emp_no=10001;

+------+------------+------------+------+---------+---------+------+
|id    |select_type | table      | type | key     | key_len | rows |
+------+------------+------------+------+---------+---------+------+
| 1    |SIMPLE      | dept_emp   | const| PRIMARY | 20      |    1 |
+------+------------+------------+------+---------+---------+------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획의 type 컬럼이 const(상수)로 표시되는 이유는 MySQL의 옵티마이저가 쿼리를 최적화하는 단계에서 쿼리를 먼저 실행해 통째로 상수화 하기 때문이다.&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;eq_ref&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eq_ref 접근 방법은 여러 테이블이 조인되는 쿼리의 실행 계획에서만 표시된다.&lt;br /&gt;&lt;b&gt;드라이빙 테이블의 컬럼 값을 드리븐 테이블의 프라이머리 키나 유니크 키 컬럼의 검색 조건에 사용할 때를 가리켜 eq_ref라고 한다.&lt;/b&gt; 이때 두 번째 이후에 읽는 테이블의 type 컬럼에 eq_ref가 표시된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;eq_ref 접근 방법의 제약 조건
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 번째 이후에 읽히는 테이블을 유니크 키로 검색할 때, 그 유니크 인덱스는 NOT NULL이어야 한다.&lt;/li&gt;
&lt;li&gt;다중 컬럼으로 만들어진 PK나 유니크 인덱스라면 인덱스의 모든 컬럼이 비교 조건에 사용돼야만 eq_ref 접근 방법이 사용될 수 있다.&lt;/li&gt;
&lt;li&gt;두 번째 이후에 읽는 테이블에서 반드시 1건의 레코드만 반환한다는 보장이 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해를 돕기 위해 다음 예제를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM dept_emp de, employees e
WHERE e.emp_no=de.emp_no AND de.dept_no='d005';

+------+------------+-------+-------+---------+---------+--------+
|id    |select_type | table | type  | key     | key_len | rows   |
+------+------------+-------+-------+---------+---------+--------+
| 1    |SIMPLE      | de    | ref   | PRIMARY | 16      | 165571 |
| 1    |SIMPLE      | e     | eq_ref| PRIMARY | 4       |      1 |
+------+------------+-------+-------+---------+---------+--------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id 값이 같은 것을 보아 조인으로 실행된다는 것을 알 수 있고, 실행 계획에서 de 테이블이 위쪽에 있으므로 드라이빙 테이블이 된다는 것을 알 수 있다. 이는 dept_emp 테이블을 먼저 읽고 &lt;code&gt;e.emp_no = de.emp_no&lt;/code&gt; 조건을 이용해 employees 테이블을 검색한다는 것을 나타낸다. 이때, emp_no는 employees 테이블의 PK이므로 두 번째 라인에서 eq_ref가 표시된 것을 확인할 수 있다.&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;ref&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eq_ref와는 다르게 조인의 순서와 관계없이 사용되며, PK나 유니크 키 등의 제약 조건이 없다. 인덱스의 종류와 관계없이 동등 조건으로 검색할 때는 ref 접근 방법이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환되는 레코드가 반드시 1건이라는 보장이 없으므로 const나 eq_ref보다는 느리지만, 동등 조건으로만 비교되므로 매우 빠른 레코드 조회 방법 중 하나다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM dept_emp WHERE dept_no='d005' ;

+------+------------+----------+-----+---------+---------+-------+
|id    |select_type | table    | type| key     | key_len | ref   |
+------+------------+----------+-----+---------+---------+-------+
| 1    |SIMPLE      | dept_emp | ref | PRIMARY | 16      | const |
+------+------------+----------+-----+---------+---------+-------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서는 dept_emp의 PK를 구성하는 컬럼(dept_no, emp_no)의 일부만 동등 조건으로 사용됐기 때문에 일치 레코드가 1건이라는 보장이 없다. 따라서 const가 아닌 ref 접근 방법이 사용되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 살펴본 실행 계획의 type에 대해 다시 한번 정리해보면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;const
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인 순서에 상관없이 PK나 유니크 키의 모든 컬럼에 대해 동등 조건으로 검색(1건의 레코드만 반환)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;eq_ref
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;드라이빙 테이블의 컬럼 값을 이용해 드리븐 테이블의 PK나 유니크 키로 동등 조건 검색(1건의 레코드만 반환)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ref
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인 순서와 인덱스의 종류에 상관없이 동등 조건으로 검색(1건의 레코드만 반환된다는 보장이 없어도 됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지 접근 방법 모두 WHERE 조건절에 &lt;b&gt;동등 비교 연산자(=, &amp;lt;=&amp;gt;)&lt;/b&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;fulltext&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버의 전문 검색 인덱스를 사용해 레코드를 읽는 접근 방법을 의미한다.&lt;br /&gt;전문 검색은 &lt;code&gt;MACTH (&amp;hellip;) AGAINST (&amp;hellip;)&lt;/code&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;ref_or_null&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ref 접근 방법과 같지만, NULL 비교가 추가된 형태다. ref 접근 방법 또는 NULL 비교(IS NULL) 접근 방법을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM titles
WHERE to_date='1985-03-01' OR to_date IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;unique_subquery&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 조건절에서 사용될 수 있는 IN(subquery) 형태의 쿼리를 위한 접근 방법이다.&lt;br /&gt;의미 그대로 서브쿼리에서 중복되지 않는 유니크한 값만 반환할 때 이 접근 방법을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM departments
WHERE dept_no IN (SELECT dept_no FROM dept_emp WHERE emp_no=10001) ;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;index_subquery&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IN 연산자 특성상 중복된 값이 먼저 제거돼야 한다.&lt;br /&gt;앞서 살펴본 unique_subquery의 경우 서브쿼리가 중복된 값을 반환하지 않는다는 보장이 있으므로 중복을 제거할 필요가 없었지만, 일반적으로 IN(subquery) 형태의 쿼리는 서브쿼리가 중복된 값을 반환할 수 있기 때문에 인덱스를 이용해 중복을 할 수 있는 경우 별도의 중복 제거를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IN(subquery) 형태의 쿼리에서 인덱스를 이용해 서브쿼리의 중복된 값을 제거할 수 있을 때 index_subquery가 표시된다.&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;unique_subquery
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IN(subquery) 형태의 조건에서 서브쿼리의 반환 값에는 중복이 없으므로 중복 제거 작업이 필요 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;index_subquery
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IN(subquery) 형태의 조건에서 서브쿼리의 반환 값에 중복이 있을 수 있지만 인덱스를 이용해 중복된 값을 제거할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;range&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 레인지 스캔 형태의 접근 방법이다.&lt;br /&gt;인데스를 하나의 값이 아니라 범위로 검색하는 경우를 의미한다.&lt;br /&gt;주로 &lt;code&gt;&amp;lt;, &amp;gt;, IS NULL, BETWEEN, IN, LIKE&lt;/code&gt; 등의 연산자를 이용해 인덱스를 검색할 때 이용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버가 가지고 있는 접근 방법의 우선순위는 낮지만, range 접근 방법도 상당히 빠른 접근 방법이며 해당 접근 방법만 사용해도 최적의 성능이 보장된다고 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM employees WHERE emp_no BETWEEN 10002 AND 10004;

+------+------------+----------+------+---------+---------+------+
|id    |select_type | table    | type | key     | key_len | rows |
+------+------------+----------+------+---------+---------+------+
| 1    |SIMPLE      | employees| range| PRIMARY | 4       |    3 |
+------+------------+----------+------+---------+---------+------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;index_merge&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index_merge 접근 방법은 2개 이상의 인덱스를 이용해 각각의 검색 결과를 만들어낸 후, 그 결과를 병합해 처리하는 방식이다. 하지만 다음과 같은 특징 때문에 이름만큼 그렇게 효율적으로 작동하는 것은 아니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 인덱스를 읽어야 하므로 range 접근 방법보다 효율성이 떨어진다.&lt;/li&gt;
&lt;li&gt;전문 검색 인덱스를 사용하는 쿼리에는 적용되지 않는다.&lt;/li&gt;
&lt;li&gt;항상 2개 이상의 집합이 되기 때문에 교집합이나 합집합, 또는 중복 제거와 같은 부가적인 작업이 더 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM employees
WHERE emp_no BETWEEN 10001 AND 11000 OR first_name='Smith' ;

+------+------------+----------------------+---------+-------------------------------------------------+
|id    |select_type | key                  | key_len | Extra                                           |
+------+------------+----------------------+---------+-------------------------------------------------+
| 1    |index_merge | PRIMARY, ix_firstname| 4, 58   | Using union(PRIMARY, ix_firstname); Using where |
+------+------------+----------------------+---------+-------------------------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리에서 &lt;code&gt;emp_no BETWEEN 10001 AND 11000&lt;/code&gt; 조건은 employees 테이블의 PK를 이용해 조회하고 &lt;code&gt;first_name='Smith'&lt;/code&gt; 조건은 ix_firstname 인덱스를 이용해 조회한 후, 두 결과를 병합하는 형태로 실행 계획을 수립한다.&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;index&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index 접근 방법은 인덱스를 처음부터 끝까지 읽는 인덱스 풀 스캔을 의미한다.&lt;br /&gt;&lt;b&gt;접근 방법의 이름 때문에 효율적으로 인덱스를 사용하는 것으로 자주 오해하는 접근 방법이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;range 접근 방법과 같이 인덱스의 필요 부분만 읽는 것이 아니라 &lt;b&gt;인덱스를 풀 스캔하는 접근 방법이라는 것을 잊지 말자.&lt;/b&gt; 인덱스는 데이터 파일보다 크기가 작기 때문에 빠르게 처리되고 정렬이라는 인덱스의 장점을 활용할 수 있기 때문에 테이블을 풀 스캔하는 것보다 훨씬 효율적이라 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;index 접근 방법은 다음 조건 중 &lt;code&gt;1번 + 2번&lt;/code&gt; 조건을 충족하거나 &lt;code&gt;1번 + 3번&lt;/code&gt; 조건을 충족하는 쿼리에서 사용된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;range나 const, ref 같은 접근 방법으로 인덱스를 사용할 수 없는 경우&lt;/li&gt;
&lt;li&gt;인덱스에 포함된 컬럼만으로 처리할 수 있는 경우(커버링 인덱스)&lt;/li&gt;
&lt;li&gt;인덱스를 이용해 정렬이나 그루핑 작업이 가능한 경우(별도의 정렬이 필요 없는 경우)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ALL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀 테이블 스캔을 의미하는 접근 방법이다.&lt;br /&gt;테이블을 처음부터 끝까지 읽어 불필요한 레코드를 제거한 후 반환한다.&lt;br /&gt;앞서 살펴본 접근 방법으로 처리할 수 없을 때 가장 마지막에 선택되는 가장 비효율적인 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 데이터 웨어하우스나 배치 프로그램처럼 대량의 레코드를 처리하는 쿼리에서는 잘못 튜닝된 쿼리보다 더 나은 접근 방법이 될 수 있다. 하지만 일반적으로 빠른 응답을 사용자에게 보내야 하는 웹 서비스와 같은 온라인 트랜잭션 처리 환경에서는 적합하지 않다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리드 어헤드(Read Ahead)&lt;br /&gt;InnoDB에서 제공하는 대량의 디스크 I/O를 유발하는 작업을 위해 한 번에 많은 페이지를 읽어 들이는 기능이다.&lt;br /&gt;MySQL 서버에서는 백그라운드 읽기 스레드가 최대 64개의 페이지씩 한번에 디스크로 읽어 들이므로 빠르게 레코드를 읽을 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;possible_keys 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;possible_keys 컬럼은 옵티마이저가 &lt;b&gt;최적의 실행 계획을 만들기 위해 후보로 선정했던 접근 방법에서 사용되는 인덱스의 목록&lt;/b&gt;을 나타낸다. 즉, &lt;b&gt;사용될 법한 인덱스의 목록&lt;/b&gt;인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 possible_keys 컬럼은 쿼리를 튜닝하는데 큰 도움이 되지 않는다. 특별한 경우를 제외하고는 그냥 무시해도 된다. &lt;b&gt;possible_keys 컬럼에 인덱스가 나열됐다고 해서 그 인덱스를 사용한다고 판단하는 일이 없도록 주의하자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;key 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key 컬럼에 표시되는 인덱스는 최종 선택된 실행 계획에서 사용하는 인덱스를 의미한다.&lt;br /&gt;즉, 실제 사용된 인덱스를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 쿼리 튜닝을 할 때 의도했던 인덱스가 key 컬럼에 표시되는지 확인하는 것이 중요하다.&lt;br /&gt;type 컬럼이 index_merge인 경우를 제외한 나머지 경우에는 테이블당 하나의 인덱스만 이용할 수 있다.(index_merge는 2개 이상의 인덱스가 사용됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 인덱스를 사용하지 못한다면 key 컬럼은 NULL로 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;key_len 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key_len 컬럼은 쉽게 무시하는 정보지만 실제로는 매우 중요한 정보를 나타낸다.&lt;br /&gt;key_len 컬럼 값은 쿼리를 처리하기 위해 다중 컬럼으로 구성된 인덱스에서 몇 개의 컬럼까지 사용했는지 알려준다. 더 &lt;b&gt;정확하게는 인덱스의 각 레코드에서 몇 바이트까지 사용했는지 알려준다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제를 살펴보자. PK는 dept_no, emp_no로 구성됐고 dept_emp 테이블을 조회하는 쿼리다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM dept_nmp
WHERE dept_no='d005';

+------+------------+----------+------+---------+---------+
|id    |select_type | table    | tpye | key     | key_len |
+------+------------+----------+------+---------+---------+
| 1    |SIMPLE      | dept_emp | ref  | PRIMARY | 16      |
+------+------------+----------+------+---------+---------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리는 PK 중 dept_no만 비교에 사용한다. key_len이 16으로 표시된 이유는 dept_no의 컬럼 타입은 CHAR(4)이므로 PK에서 앞쪽 16바이트만 유효하게 사용했다는 것을 나타내기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자를 위해 메모리 공간에 할당해야 하는 4바이트와 실제 dept_no 값의 길이 4바이트를 곱해 16이라는 값이 표시된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 emp_no 조건을 추가하면 key_len 컬럼 값은 다음과 같이 변경된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM dept_nmp
WHERE dept_no='d005' AND emp_no=10001;

+------+------------+----------+------+---------+---------+
|id    |select_type | table    | tpye | key     | key_len |
+------+------------+----------+------+---------+---------+
| 1    |SIMPLE      | dept_emp | const| PRIMARY | 20      |
+------+------------+----------+------+---------+---------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;emp_no는 Integer 타입으로 4바이트 메모리 공간을 차지한다.&lt;br /&gt;따라서 detp_no 컬럼의 길이와 emp_no 컬럼의 길이를 합친 결과인 20이 표시되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ref 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근 방법이 ref면 동등 비교 조건으로 어떤 값이 제공됐는지 보여준다.&lt;br /&gt;상수를 지정하면 const로 표시되고, 다른 테이블의 컬럼 값이면 테이블명과 컬럼명이 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ref 컬럼에 출력되는 내용은 크게 신경 쓰지 않아도 무방&lt;/b&gt;하지만, 콜레이션 변환이나 값 자체의 연산을 거쳐 참조되는 경우(func으로 표시된다.) 조금 주의해서 볼 필요가 있다. 사용자가 명시적으로 값을 변환하거나 문자 집합이 일치하지 않는 두 문자열 컬럼을 조인할 때, 숫자 타입의 컬럼과 문자열 타입의 컬럼으로 조인할 때가 대표적인 예다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제에서 emp_no 값에서 1을 뺀 값으로 테이블과 조인한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN 
SELECT *
FROM employees e, dept_emp de WHERE e.emp_no=(de.emp_no-1);

+------+------------+-------+-------+---------+------+
|id    |select_type | table | tpye  | key     | ref  |
+------+------------+-------+-------+---------+------+
| 1    |SIMPLE      | de    | ALL   | NULL    | NULL |
| 1    |SIMPLE      | e     | eq_ref| PRIMARY | func |
+------+------------+-------+-------+---------+------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ref 값이 func으로 표시되는 것을 확인할 수 있다. 조인 조건에 산술 표현식을 넣어 쿼리를 만들었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;rows 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 옵티마이저는 가능한 처리 방식을 나열하고, 최종적으로 가장 비용이 적은 하나의 실행 계획을 수립한다. 이때 비용 비교 기준은 얼마나 많은 레코드를 읽고 비교해야 하는지에 대한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;rows 컬럼은 이렇게 실행 계획의 효율성 판단을 위해 예측했던 레코드 건수&lt;/b&gt;를 보여준다. 이는 쿼리를 처리하기 위해 얼마나 많은 레코드를 읽고 체크해야 하는지를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 이 값을 참조해 가장 효율적인 실행 계획을 수립하게 된다.&lt;br /&gt;이해를 돕기 위해 다음 예제를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM dept_emp WHERE from_date&amp;gt;='1985-01-01'

+------+------------+----------+------+------+---------+-------+
|id    |select_type | table    | tpye | key  | key_len | rows  |
+------+------------+----------+------+------+---------+-------+
| 1    |SIMPLE      | dept_emp | ALL  | NULL | NULL    | 331143|
+------+------------+----------+------+------+---------+-------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dept_emp 테이블에는 약 33만 건의 데이터가 들어있고 from_date로 생성된 ix_fromdate 인덱스가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획 살펴보면 해당 쿼리를 처리하는데 인덱스 레인지 스캔이 아닌 풀 테이블 스캔을 선택했다.&lt;br /&gt;그 이유는 옵티마이저가 331,143 건의 레코드를 읽어야 할 것으로 예측했고 실제 테이블의 데이터 수는 약 33만 건이므로 풀 테이블 스캔이 더 효율적이라 판단한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 범위를 조금 더 줄인 쿼리를 실행해보면 어떻게 될까?&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM dept_emp WHERE from_date&amp;gt;='2012-07-01'

+------+------------+----------+-------+-------------+---------+------+
|id    |select_type | table    | tpye  | key         | key_len | rows |
+------+------------+----------+-------+-------------+---------+------+
| 1    |SIMPLE      | dept_emp | range | ix_fromdate | 3       |  292 |
+------+------------+----------+-------+-------------+---------+------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 읽어야 할 레코드를 292건으로 예측했고, 따라서 실행 계획도 인덱스 레인지 스캔으로 변경된 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rows 컬럼에 표시되는 값은 스토리지 엔진별로 가지고 있는 통계 정보를 참조해 산출해낸 예측값이라 정확하지는 않다. 하지만 이 값이 어느 정도 근접해야만 옵티마이저가 제대로 된 실행 계획을 수립할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;filtered 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 일치하는 레코드 개수를 가능한 정확히 파악해야 효율적인 실행 계획을 수립할 수 있다. &lt;b&gt;앞서 살펴본 rows 컬럼은 인덱스를 사용하는 조건에만 일치하는 레코드 건수를 예측한 것이지만, 모두 인덱스를 사용할 수 있는 것은 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, 조인이 사용되는 경우 WHERE 절에서 인덱스를 사용할 수 있는 조건도 중요하지만 &lt;b&gt;인덱스를 사용하지 못하는 조건에 일치하는 레코드 건수를 파악하는 것도 매우 중요하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;filetered 컬럼의 값은 필터링이 되고 남은 레코드의 비율을 의미한다.&lt;/b&gt;&lt;br /&gt;예를 들어, rows 컬럼 값이 233건이고 filtered 값이 16.03%라고 하면 233건 중 16.03%의 레코드만 남았다는 뜻이다. 즉, 233건 중 약 37건(233 * 0.1603)만 모든 조건을 만족한다는 것을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제를 통해 더 자세히 알아보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선행 조건
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;employees 테이블의 &lt;code&gt;e.first_name='Matt'&lt;/code&gt; 조건은 인덱스 사용이 가능하다.&lt;/li&gt;
&lt;li&gt;salaries 테이블의 &lt;code&gt;s.salary BETWEEN 50000 AND 60000&lt;/code&gt; 조건은 인덱스 사용이 가능하다.&lt;/li&gt;
&lt;li&gt;그 외 나머지 조건은 인덱스를 사용하지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN 
SELECT *
FROM employees e,
     salaries s
WHERE e.first_name='Matt' // employees 인덱스 사용 가능
   AND e.hire_date BETWEEN '1990-01-01' AND '1991-01-01' // employees 인덱스 사용 불가능
   AND s.emp_no=e.emp_no 
   AND s.from_date BETWEEN '1990-01-01' AND '1991-01-01' // salaries 인덱스 사용 불가능
   AND s.salary BETWEEN 50000 AND 60000 // salaries 인덱스 사용 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리에 대한 실행 계획은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;+------+------------+-------+------+--------------+------+----------+
|id    |select_type | table | tpye | key          | rows | filtered |
+------+------------+-------+------+--------------+------+----------+
| 1    |SIMPLE      | e     | ref  | ix_firstname | 233  |   16.03  |
| 1    |SIMPLE      | s     | ref  | PRIMARY      | 10   |   0.48   |
+------+------------+-------+------+--------------+------+----------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;employees 테이블에서 &lt;code&gt;e.first_name='Matt'&lt;/code&gt; 인덱스 조건에 일치하는 레코드는 233건이고 &lt;code&gt;AND e.hire_date BETWEEN '1990-01-01' AND '1991-01-01'&lt;/code&gt; 조건까지 만족하는 레코드는 37건(233 * 0.1603)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 인덱스 조건에 일치하는 모든 레코드(233건)를 찾은 후, 인덱스를 사용하지 못하는 조건에 대해 필터링을 수행한 것이다. 이렇게 filtered 컬럼 값을 이용해 인덱스를 사용하지 못하는 조건에 일치하는 레코드 건수를 파악할 수 있으며, 모든 조건이 필터링된 후 남은 레코드의 비율이 filtered 컬럼의 값이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 메모리 사용량을 낮추기 위해 대상 건수가 적은 테이블을 드라이빙 테이블로 선택할 가능성이 높다. 따라서 filtered 컬럼에 표시되는 값이 얼마나 정확히 예측되느냐에 따라 조인 성능이 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 인덱스를 사용하지 못하는 조건에 일치하는 레코드 건수를 파악하는 것이 중요하다고 설명한 것도 이러한 이유 때문이다.&lt;/p&gt;</description>
      <category>BackEnd/Real MySQL 8.0</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/177</guid>
      <comments>https://jjingho.tistory.com/177#entry177comment</comments>
      <pubDate>Sun, 23 Oct 2022 13:50:06 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 실행 계획 분석하기(id ~ partitions)</title>
      <link>https://jjingho.tistory.com/176</link>
      <description>&lt;h1&gt;실행 계획 분석&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;id 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SELECT 쿼리 별로 부여되는 식별자 값이다.&lt;/b&gt;&lt;br /&gt;예를 들어 하나의 SELECT 문장에서 여러 개의 테이블을 조인하면 조인되는 테이블 개수만큼 실행 계획 레코드가 출력되지만 같은 id 값을 가지게 된다. 반면에 하나의 SELECT 문장에 여러 개의 단위 SELECT 쿼리가 포함되어 있는 경우 각기 다른 id 값을 가지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 가지 주의할 점은 실행 계획의 id 컬럼이 테이블의 접근 순서를 의미하지 않는다는 것이다. 명확한 테이블 접근 순서를 알고싶다면 TREE 형태의 포맷으로 실행 계획을 출력해 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;select_type 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select_type은 단위 SELECT 쿼리가 어떤 타입의 쿼리인지 표시되는 컬럼이다.&lt;br /&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;SIMPLE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UNION이나 서브쿼리를 사용하지 않는 단순한 SELECT 쿼리인 경우 SIMPLE로 표시된다. 쿼리 문장이 아무리 복잡해도 실행 계획에서 select_type이 SIMPLE인 단위 쿼리는 하나만 존재한다. 일반적으로 제일 바깥 SELECT 쿼리의 select_type이 SIMPLE로 표시된다.&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;PRIMARY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UNION이나 서브쿼리를 가지는 SELECT 쿼리의 실행 계획에서 가장 바깥쪽에 있는 단위 쿼리는 select_type이 PRIMARY로 표시된다. SIMPLE과 마찬가지로 하나만 존재한다.&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;UNION&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UNION으로 결합하는 단위 SELECT 쿼리 중 &lt;b&gt;첫 번째를 제외한 두 번째 이후 단위 SELECT 쿼리의 select_type은 UNION으로 표시된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 단위 SELECT는 쿼리 결과를 모아서 저장하는 &lt;b&gt;임시 테이블(DERIVED)로 표시&lt;/b&gt;된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * FROM (
    (SELECT emp_no FROM employees e1 LIMIT 10) UNION ALL
  (SELECT emp_no FROM employees e2 LIMIT 10) UNION ALL
  (SELECT emp_no FROM employees e3 LIMIT 10)) tb;

+----+------------+------------+-------+-------------+------+-------+-------------+
|id  |select_type | table      | type  |key          |ref   |rows   |Extra        |
+----+------------+------------+-------+-------------+------+-------+-------------+
| 1  |PRIMARY     | &amp;lt;derived2&amp;gt; | ALL   |NULL         |NULL  |   30  | NULL        |
| 2  |DERIVED     | e1         | index |ix_hiredate  |NULL  |300252 | Using index |
| 3  |UNION       | e2         | index |ix_hiredate  |NULL  |300252 | Using index |
| 4  |UNION       | e3         | index |ix_hiredate  |NULL  |300252 | Using index |
+----+------------+------------+-------+-------------+------+-------+-------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UNION이 되는 단위 SELECT 쿼리 3개 중 첫 번째(e1 테이블)만 UNION이 아니고 나머지 2개는 UNION으로 표시돼 있다. 세 개의 서브쿼리로 조회된 결과를 UNION ALL로 결합해 임시 테이블을 만들어서 사용하고 있기 때문에 첫 번째 쿼리는 DERIVED라는 select_type을 갖는다.&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;DEPENDENT UNION&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DEPENDENT는 UNION이나 UNION ALL로 결합된 &lt;b&gt;단위 쿼리가 외부 쿼리에 의해 영향을 받는 것을 의미한다&lt;/b&gt;. 다음 예제 쿼리를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN SELECT *
FROM employees e1 WHERE e1.emp_no IN (
  SELECT e2.emp_no FROM employees e2 WHERE e2.first_name='Matt' 
  UNION
  SELECT e3.emp_no FROM employees e3 WHERE e3.last_name='Matt'
);

+------+-------------------+------------+--------+----------+------+--------+------------------+
|id    |select_type        | table      | type   |key       |ref   |rows    |Extra             |
+------+-------------------+------------+--------+----------+------+--------+------------------+
| 1    |PRIMARY            | e1         | ALL    | NULL     | NULL | 300252 | Using where      |
| 2    |DEPENDENT SUBQUERY | e1         | eq_ref | PRIMARY  | func |  1     | Using where      |
| 3    |DEPENDENT UNION    | e2         | eq_ref | PRIMARY  | func |  1     | Using where      |
| NULL |UNION RESULT       | &amp;lt;union2, 3&amp;gt;| ALL    | NULL     | NULL | NULL   | Using temporary  |
+------+-------------------+------------+--------+----------+------+--------+------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 SELECT 쿼리가 UNION으로 결합됐으므로 select_type이 UNION으로 표시됐다.&lt;br /&gt;MySQL 옵티마이저는 IN 절 내부의 서브 쿼리를 먼저 처리하지 않고 외부의 employees 테이블을 먼저 읽은 다음 서브쿼리를 실행한다. employees 테이블의 컬럼 값이 서브쿼리에 영향을 주기 때문에 DEPENDENT 키워드가 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 UNION에 사용된 SELECT 쿼리의 WHERE 조건에 외부에 정의된 employees 테이블의 emp_no 컬럼이 사용되기 때문에 DEPENDENT UNION이 표시된 것이다.&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;UNION RESULT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UNION RESULT는 UNION 결과를 담아두는 테이블을 의미한다.&lt;br /&gt;MySQL 8.0 이전 버전에서는 UNION의 결과를 임시 테이블로 생성했는데, MySQL 8.0부터는 UNION ALL인 경우 임시 테이블을 사용하지 않도록 기능이 개선됐다. 하지만 UNION은 여전히 임시 테이블에 결과를 버퍼링한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획상에서 임시 테이블을 가리키는 라인의 select_type이 UNION RESULT이며, 단위 쿼리가 아니기 때문에 별도의 id 값은 부여되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT emp_no FROM salaries WHERE salary &amp;gt; 100000
UNION DISTINCT
SELECT emp_no FROM dept_emp WHERE from_date &amp;gt; '2001-01-01';

+------+-------------+-------------+-------+-------------+---------+---------------------------+
|id    |select_type  | table       | type  |key          | rows    | Extra                     |
+------+-------------+-------------+-------+-------------+---------+---------------------------+
| 1    |PRIMARY      | salaries    | range | ix_salary   | 191348  | Using where; Using index  |
| 2    |UNION        | dept_emp    | range | ix_fromdate |      1  | Using where; Using index  |
| NULL |UNION RESULT | &amp;lt;union1, 2&amp;gt; | ALL   | NULL        |    NULL | Using temporary           |
+------+-------------+-------------+-------+-------------+---------+---------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7xeFN/btrOFlu8g4s/cMPvxWUFfmCkJGKcVxOp41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7xeFN/btrOFlu8g4s/cMPvxWUFfmCkJGKcVxOp41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7xeFN/btrOFlu8g4s/cMPvxWUFfmCkJGKcVxOp41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7xeFN%2FbtrOFlu8g4s%2FcMPvxWUFfmCkJGKcVxOp41%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;714&quot; height=&quot;280&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;union1, 2&amp;gt;는 id 값이 1인 단위 쿼리와 2인 단위 쿼리의 조회 결과를 UNION 했다는 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 동일한 쿼리에서 UNION DISTINCT를 UNION ALL로 변경하면 임시 테이블에 버퍼링을 하지 않으므로 UNION RESULT 표시 라인이 사라진다.&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;SUBQUERY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select_type의 SUBQUERY는 FROM 절 이외에서 사용되는 서브쿼리를 의미한다.&lt;br /&gt;FROM 절에 사용된 서브쿼리는 DERIVED(파생 테이블)로 표시되고, 그 밖의 위치에서 사용된 서브쿼리는 전부 SUBQUERY로 표시된다.&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;DEPENDENT SUBQUERY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리가 바깥쪽 SELECT 쿼리에서 정의된 컬럼에 의존적인 경우 DEPENDENT SUBQUERY가 표시된다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT e.first_name,
  (SELECT COUNT(*)
   FROM dept_emp de, dept_manager dm
   WHERE dm.dept_no = de.dept_no AND de.emp_no = e.emp_no) AS cnt
FROM employees e
WHERE e.first_name='Matt' ;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안쪽 서브쿼리 결과가 바깥쪽 SELECT 쿼리의 컬럼에 의존적이므로 DEPENDENT 키워드가 붙는다. 또한 DEPENDENT SUBQUERY는 외부 쿼리가 먼저 수행된 후 내부 쿼리가 실행돼야 하므로 일반 서브쿼리보다 처리 속도가 느릴 때가 많다.&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;DERIVED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DERIVED는 단위 SELECT 쿼리의 실행 결과로 메모리나 디스크에 임시 테이블을 생성하는 것을 의미한다.&lt;/b&gt; select_type이 DERIVED 인 경우에 생성되는 임시 테이블을 파생 테이블이라고도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.5 버전까지는 서브쿼리가 FROM 절에 사용된 경우 항상 DERIVED(파생 테이블)을 만들었기 때문에 성능상 불리한 점이 많았지만, MySQL 5.6 버전부터는 옵티마이저 옵션에 따라 쿼리 특성에 맞게 임시 테이블에도 인덱스를 추가해서 만들 수 있게 최적화됐다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EXPLAIN SELECT *
FROM (SELECT de.emp_no FROM dept_emp de GROUP BY de.emp_no) tb, 
  employees e
WHERE e.emp_no=tb.emp_no;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리는 FROM 절의 서브쿼리를 제거하고 조인으로 처리할 수 있다. &lt;b&gt;가능하면 DERIVED 형태의 실행 계획을 조인으로 해결할 수 있게 쿼리를 변경하는 것이 좋다.&lt;/b&gt; MySQL 8.0부터는 FROM 절의 서브쿼리 최적화도 많이 개선되어 &lt;b&gt;불필요한 서브쿼리를 조인으로 재작성하는 최적화를 지원하지만&lt;/b&gt;, &lt;b&gt;옵티마이저의 처리 능력에도 한계가 있으므로 최적화된 쿼리를 작성하는 것은 매우 중요하다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 튜닝을 위해 실행 계획을 분석한다면 select_type 컬럼이 DERIVED인 것을 먼저 확인하고 제거하자.&lt;br /&gt;또한, 서브쿼리를 조인으로 해결할 수 있는 경우 조인을 사용하는 것을 강력히 권장한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DEPENDENT DERIVED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 이전 버전에서는 FROM 절의 서브쿼리는 외부 컬럼을 사용할 수 없었는데, MySQL 8.0 부터는 래터럴 조인 기능이 추가되면서 FROM 절의 서브쿼리에서도 외부 컬럼을 참조할 수 있게 됐다. &lt;b&gt;래터럴 조인을 사용하면 select_type 컬럼이 DEPENDENT DERIVED 키워드가 표시된다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;래터럴 조인이란?&lt;br /&gt;특정 그룹별로 서브쿼리를 실행해서 그 결과와 조인하는 기능이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UNCACHEABLE SUBQUERY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건이 똑같은 서브쿼리가 실행될 때는 쿼리를 다시 실행하지 않고 이전의 실행 결과를 내부적인 캐시 공간에 담아두고 재사용하게 된다. 하지만 &lt;b&gt;서브쿼리에 포함된 요소에 의해 캐시 자체가 불가능한 경우 select_type이 UNCACHEABLE SUBQUERY로 표시된다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s0pnA/btrO3FtN3js/lJrv3xtfK8coVnkuNqf05k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s0pnA/btrO3FtN3js/lJrv3xtfK8coVnkuNqf05k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s0pnA/btrO3FtN3js/lJrv3xtfK8coVnkuNqf05k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs0pnA%2FbtrO3FtN3js%2FlJrv3xtfK8coVnkuNqf05k%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;1288&quot; height=&quot;638&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 select_type이 SUBQUERY인 경우 캐시를 사용하는 방법을 나타내며 캐시가 처음 한 번만 생성된 것을 알 수 있다. 다만, select_type이 DEPENDENT SUBQUERY인 경우 캐시 방식은 똑같지만 한 번만 캐시되는 것이 아니라 외부 쿼리의 값 단위로 캐시가 만들어진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SUBQUERY
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번만 실행해서 그 결과를 캐시하고 필요할 때 캐시된 결과를 이용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DEPENDENT SUBQUERY
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존하는 바깥쪽 쿼리의 컬럼 값 단위로 캐시해두고 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 사용하지 못하게 하는 요소로는 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 변수가 서브쿼리에 사용된 경우&lt;/li&gt;
&lt;li&gt;NOT-DETERMINISTIC 속성의 스토어드 루틴이 서브쿼리내에 사용된 경우&lt;/li&gt;
&lt;li&gt;UUID(), RAND() 같은 결과값이 호출할 때마다 달라지는 함수가 서브쿼리에 사용된 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UNCACHEABLE UNION&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UNION 수행 시 포함된 요소에 의해 캐시가 불가능한 경우 select_type이 UNCACHEABLE UNION로 표시된다.&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;MATERIALIZED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.6 버전부터 도입된 select_type으로 주로 FROM 절이나 IN(subquery) 형태의 쿼리에 사용된 서브쿼리의 최적화를 위해 사용된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN 
SELECT *
FROM employees e
WHERE e.emp_no IN (SELECT emp_no FROM salaries WHERE salary BETWEEN 100 AND 1000);

+------+-------------+-------------+-------+-----------+------+--------------------------+
|id    |select_type  | table       | type  |key        | rows | Extra                    |
+------+-------------+-------------+-------+-----------+------+--------------------------+
| 1    |SIMPLE       | &amp;lt;subquery2&amp;gt; | ALL   | NULL      | NULL | NULL                     |
| 1    |SIMPLE       | e           | eq_ref| PRIMARY   |    1 | NULL                     |
| 2    |MATERIALIZED | salaries    | range | ix_salary |    1 | Using where; Using index |
+------+-------------+-------------+-------+-----------+------+--------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서 MySQL 5.6 버전까지는 employees 테이블을 읽어서 레코드마다 salaries 테이블을 읽는 서브쿼리가 실행되는 형태로 처리됐다. 하지만 MySQL 5.7 버전부터는 서브쿼리의 내용을 임시 테이블로 구체화(Materialization)한 후, 임시 테이블과 employees 테이블을 조인하는 형태로 최적화되어 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;table 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버의 실행 계획은 단위 SELECT 쿼리 기준이 아니라 테이블 기준으로 표시된다. 테이블의 이름에 별칭이 부여된 경우 별칭이 표시된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 테이블을 사용하지 않는 SELECT 쿼리의 경우 table 컬럼이 NULL로 표시된다.&lt;/li&gt;
&lt;li&gt;또는 &amp;lt;union M, N&amp;gt; 으로 표시되는 것은 임시 테이블을 의미한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;M, N은 단위 SELECT 쿼리의 id 값을 가리킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH5EJS/btrO6MYK4H8/6d0LxiLNFmkIuNi4EHlDE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH5EJS/btrO6MYK4H8/6d0LxiLNFmkIuNi4EHlDE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH5EJS/btrO6MYK4H8/6d0LxiLNFmkIuNi4EHlDE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH5EJS%2FbtrO6MYK4H8%2F6d0LxiLNFmkIuNi4EHlDE1%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;706&quot; height=&quot;290&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 살펴본 &lt;b&gt;id 컬럼, select_type 컬럼, table 컬럼은 실행 계획의 각 라인에 명시된 테이블이 어떤 순서로 실행되는지 판단하는 근거를 표시해주는 역할을 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예시를 통해 앞서 살펴본 3개의 컬럼을 이용해 테이블이 어떤 순서로 실행되는지 실행 계획을 분석해보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT * 
FROM
  (SELECT de.emp_no FROM dept_emp de GROUP BY de.emp_no) tb,
   employees e
WHERE e.emp_no = tb.emp_no;

+------+------------+------------+-------+--------------------+--------+-------------+
|id    |select_type | table      | type   |key                | rows   | Extra       |
+------+------------+------------+-------+--------------------+--------+-------------+
| 1    |PRIMARY     | &amp;lt;derived2&amp;gt; | ALL    | NULL              | 331143 | NULL        |
| 1    |PRIMARY     | e          | eq_ref | PRIMARY           |      1 | NULL        |
| 2    |DERIVED     | de         | index  | ix_empno_fromdate | 331143 | Using index |
+------+------------+------------+-------+--------------------+--------+-------------+&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 테이블이 이므로 id 값이 2인 라인이 먼저 실행되고 그 결과가 파생 테이블로 준비된다.&lt;/li&gt;
&lt;li&gt;세 번째 라인(id 값이 2인) select_type이 DERIVED 이므로 dept_emp 테이블을 읽어 파생 테이블을 생성한다.&lt;/li&gt;
&lt;li&gt;세 번째 라인의 분석이 끝났으니 다시 첫 번째 라인으로 돌아간다.&lt;/li&gt;
&lt;li&gt;첫 번째와 두 번째 id 값이 같은 것으로 보아 조인되는 쿼리라는 것을 알 수 있다.&lt;br /&gt;표시 순서에 따라 &lt;b&gt;첫 번째 라인이 드라이빙 테이블&lt;/b&gt;이 되고 &lt;b&gt;두 번째 라인이 드리븐 테이블&lt;/b&gt;이 된다는 것을 알 수 있다. 즉, 테이블을 먼저 읽어서 e 테이블로 조인을 실행한 것으로 분석할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;partitions 컬럼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션을 참조하는 쿼리(파티션 키 컬럼을 WHERE 조건으로 가진)의 경우 옵티마이저가 쿼리 처리를 위해 필요한 파티션의 목록만 모아서 실행 계획의 partitions 컬럼에 표시해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해를 돕기위한 예제를 살펴보자. 우선, hire_date 컬럼값을 기준으로 5년 단위로 나누어진 파티션을 생성한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE employees_2 (
  emp_no int NOT NULL,
  birth_date DATE NOT NULL, 
  first_name VARCHAR(14) NOT NULL, 
  last_name VARCHAR(16) NOT NULL, 
  gender ENUM('M','F') NOT NULL, 
  hire_date DATE NOT NULL, 
  PRIMARY KEY (emp_no, hire_date)
) PARTITION BY RANGE COLUMNS(hire_date) 
(PARTITION p1986_1990 VALUES LESS THAN ('1990-01-01'), 
 PARTITION p1991_1995 VALUES LESS THAN ('1996-01-01'), 
 PARTITION p1996_2000 VALUES LESS THAN ('2000-01-01'), 
 PARTITION p2O01_2005 VALUES LESS THAN ('2006-01-01'));

INSERT INTO employees_2 SELECT * FROM employees;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 hire_date 컬럼 값이 1999-11-15 ~ 2000-01-15 사이인 레코드를 검색하는 쿼리를 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN 
SELECT *
FROM employees_2
WHERE hire_date BETWEEN '1999-11-15' AND '2000-01-15' ;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 쿼리에 필요한 데이터는 p1996_2000과 p2000_2006 파티션에 저장돼 있다. 실제로 옵티마이저는 hire_date 컬럼의 조건을 확인해 해당 파티션에 필요한 데이터가 들어있다는 것을 알아낸다. 따라서 실행 계획에서도 나머지 파티션에 대해서는 데이터 분포 등의 분석을 수행하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파티션이 여러 개인 테이블에서 불필요한 파티션을 빼고 접근해야 할 것으로 판단되는 테이블만 골라내는 과정을 파티션 프루닝(Partition pruning)이라고 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위 쿼리의 실제 실행 계획을 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;+------+------------+------------+------------------------+------+-------+
|id    |select_type | table      | partitions             | type | rows  | 
+------+------------+------------+------------------------+------+-------+
| 1    |SIMPLE     | employees_2 | p1996_2000, p2000_2006 | ALL  | 21743 |
+------+------------+------------+------------------------+------+-------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 살펴보면 쿼리 처리를 위해 필요한 파티션 목록이 partitions 컬럼에 표시된 것을 확인할 수 있다. 한 가지 특이한 점은 type의 값이 ALL(풀 테이블 스캔)이라는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 풀 테이블 스캔으로 테이블의 일부만 읽을 수 있는 것일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 대부분의 RDBMS에서 지원하는 파티션은 물리적으로 개별 테이블처럼 별도의 저장 공간을 가지기 때문이다. 따라서 해당 쿼리의 경우 employees_2 테이블의 모든 파티션이 아니라 p1996_2000 파티션과 p2000_2006 파티션만 풀 스캔을 실행한 것이다.&lt;/p&gt;</description>
      <category>BackEnd/Real MySQL 8.0</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/176</guid>
      <comments>https://jjingho.tistory.com/176#entry176comment</comments>
      <pubDate>Wed, 19 Oct 2022 21:55:58 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 실행 계획 통계 정보와 실행 계획을 확인하는 방법</title>
      <link>https://jjingho.tistory.com/175</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 쿼리를 최적으로 처리될 수 있게 하려면 쿼리의 &lt;b&gt;실행 계획을 수립&lt;/b&gt;할 수 있어야 한다. 이에 가장 큰 영향을 미치는 정보는 바로 통계 정보다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;통계 정보&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.7 버전까지 &lt;b&gt;테이블과 인덱스에 대한 개략적인 정보를 가지고 실행 계획을 수립&lt;/b&gt;했다. 하지만 테이블 컬럼의 값들이 어떻게 분포돼 있는지에 대한 정보가 없기 때문에 실행 계획의 정확도가 떨어지는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 MySQL8.0부터는 데이터 분포도를 수집해 저장하는 &lt;b&gt;히스토그램 정보&lt;/b&gt;가 도입됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테이블 및 인덱스 통계 정보&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 기반 최적화에서 가장 중요한 것은 통계 정보다.&lt;br /&gt;통계 정보가 정확하지 않다면 엉뚱한 방향으로 쿼리를 실행할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 1억 건의 레코드가 저장된 테이블의 통계 정보가 갱신되지 않아서 10건 미만인 것처럼 통계 정보가 구성되어 있다면 부정확한 통계 정보 때문에 0.1초에 끝날 쿼리가 1시간이 소요될 수도 있다.&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;MySQL 서버의 통계 정보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.5 버전까지는 통계 정보가 메모리에만 관리되었기 때문에 MySQL 서버가 재시작된 경우 통계 정보가 모두 사라지고 모든 테이블의 통계 정보는 다시 수집돼야 했다. 따라서 MySQL 5.6 버전부터는 테이블에 대한 통계 정보를 영구적으로 관리할 수 있도록 개선되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블을 생성할 때 STATS_PERSISTENT 옵션을 설정해 테이블 단위로 영구적인 통계 정보를 보관할지 결정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;STATS_PERSISTENT = 0
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통계 정보를 메모리에만 관리(MySQL 5.5 버전과 동일한 방식)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;STATS_PERSISTENT = 1
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통계 정보를 innodb_index_stats와 innodb_table_stats 테이블에 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;STATS_PERSISTENT = DEFAULT
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 통계 정보 영구적 관리 여부를 innodb_stats_persistent 시스템 변수 값으로 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 영구적 통계 정보를 사용하고자 한다면 &lt;b&gt;innodb_stats_auto_recalc&lt;/b&gt; 시스템 설정 변수의 기본 값을 OFF로 설정해 통계 정보 자동 갱신을 막는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영구적인 통계 정보를 사용할 때 더 정확한 통계 정보를 수집하고자 한다면 &lt;b&gt;innodb_stats_persistent_sample_pages&lt;/b&gt; 시스템 변수에 높은 값을 설정해 쿼리 성능을 끌어올릴 수 있다. 하지만 이 값이 너무 높으면 정보 수집 시간이 길어지므로 주의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;히스토그램&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.7 버전까지는 단순히 인덱스 컬럼의 유니크한 값의 개수 정도만 통계 정보로 가지고 있었다. 하지만 옵티마이저가 최적의 실행 계획을 수립하기에는 이 정보만으로는 많이 부족했다. 이러한 부족한 정보를 보완하기 위해 MySQL 8.0부터는 데이터 분포도를 참조할 수 있는 히스토그램 정보를 활용할 수 있게 됐다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히스토그램 정보는 컬럼 단위로 관리되는데, 이 정보는 자동으로 수집되지 않고 ANALZE TABLE &amp;hellip; UPDATE HISTOGRAM 명령으로 수동으로 수집 및 관리된다. 수집된 히스토그램 정보는 시스템 딕셔너리에 함께 저장되고 MySQL 서버가 시작될 때 딕셔너리의 히스토그램 정보를 information_schema 데이터베이스의 column_statistics 테이블로 로드한다. 히스토그램 정보 수집 과정을 정리하면 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ANALZE TABLE &amp;hellip; UPDATE HISTOGRAM 명령으로 히스토그램 수동 수집 및 관리&lt;/li&gt;
&lt;li&gt;수집된 히스토그램 정보는 시스템 딕셔너리에 저장&lt;/li&gt;
&lt;li&gt;MySQL 서버 시작 시 딕셔너리 히스토그램 정보를 information_schema 데이터베이스의 column_statistics 테이블로 로드&lt;/li&gt;
&lt;li&gt;실제 히스토그램 정보 조회는 column_statistics 테이블 조회&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;ANALYZE TABLE employees.employees UPDATE HISTOGRAM ON gender, hire_date;

SELECT * FROM COLUMN_STATISTICS 
WHERE SCHEMA_NAME='employees' AND TABLE_NAME='employees'\G&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 버전에서는 2종류의 히스토그램 타입이 지원된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글턴 히스토그램(Singleton)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;칼럼 값 개별로 레코드 건수를 관리하는 히스토그램&lt;/li&gt;
&lt;li&gt;컬럼이 가지는 값 별로 버킷 할당&lt;/li&gt;
&lt;li&gt;각 버킷이 &lt;b&gt;컬럼의 값과 발생 빈도의 비율&lt;/b&gt;, 2개 값을 가짐&lt;/li&gt;
&lt;li&gt;Value-Based 히스토그램 또는 도수 분포라고 불린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rrgFY/btrN666y1Mi/HCIRcxtbDCvKvhtkGeuzRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rrgFY/btrN666y1Mi/HCIRcxtbDCvKvhtkGeuzRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rrgFY/btrN666y1Mi/HCIRcxtbDCvKvhtkGeuzRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrrgFY%2FbtrN666y1Mi%2FHCIRcxtbDCvKvhtkGeuzRK%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;549&quot; height=&quot;430&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;430&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;높이 균형 히스토그램(Equi-Height)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;칼럼 값 범위를 균등한 개수로 구분해 관리하는 히스토그램&lt;/li&gt;
&lt;li&gt;개수가 균등한 칼럼 값의 범위별로 버킷 할당&lt;/li&gt;
&lt;li&gt;각 버킷이 &lt;b&gt;범위 시작 값, 마지막 값, 발생 빈도율, 버킷에 포함된 유니크 값의 개수&lt;/b&gt; 등 4개 값을 가짐&lt;/li&gt;
&lt;li&gt;Height-Balanced 히스토그램이라고 불린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDBCrl/btrN79onByZ/lF8LHBDZ5hRFu7JwbAQOg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDBCrl/btrN79onByZ/lF8LHBDZ5hRFu7JwbAQOg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDBCrl/btrN79onByZ/lF8LHBDZ5hRFu7JwbAQOg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDBCrl%2FbtrN79onByZ%2FlF8LHBDZ5hRFu7JwbAQOg0%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;565&quot; height=&quot;582&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;582&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;h3 data-ke-size=&quot;size23&quot;&gt;히스토그램의 용도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히스토그램이 도입되기 이전 MySQL 서버에도 테이블과 인덱스에 대한 통계 정보는 존재했다. 하지만 테이블의 전체 레코드 건수와 인덱스된 컬럼이 가지는 유니크 값의 개수 정도만 통계 정보로 가지고 있었고, 실제 응용 프로그램의 데이터는 항상 균등한 분포를 가지지 않는다는 점을 기존 MySQL 서버는 고려하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히스토그램은 범위 별로 레코드의 건수와 유니크한 값의 개수 정보를 가져 훨씬 정확한 예측을 할 수 있으므로 이러한 단점을 보완하기 위해 MySQL 8.0 버전부터 히스토그램이 도입됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 성이 Zita이고 1950년대 출생인 사람이 143명인 테이블 데이터에서 다음과 같은 쿼리가 있다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;
// 히스토그램이 없을 때 예측치
&amp;gt; EXPLAIN SELECT *
FROM employees
WHERE first_name='Zita'
AND birth_date BETWEEN '1950-01-01' AND '1960-01-01';

+----+------------+----------+------+--------------+------+---------+
| id |select_type | table    | type | key          | rows | filtered|                      |
+----+------------+----------+------+--------------+------+---------+
| 1  | SIMPLE     | employees| ref  | ix_firstname | 224  | 11.11   |
+----+------------+----------+--------------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히스토그램이 없을 때 실행 계획에서는 first_name=&amp;rsquo;Zita&amp;rsquo; 조건 일치 224건 중 11.11%(24.8명)만 1950년대 출생일 것으로 예측했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;// 히스토그램 정보 수집
&amp;gt; ANALYZE TABLE employees
UPDATE histogram ON first_name, birth_date;

&amp;gt; EXPLAIN SELECT *
FROM employees
WHERE first_name='Zita'
AND birth_date BETWEEN '1950-01-01' AND '1960-01-01';

+----+------------+----------+------+--------------+------+---------+
| id |select_type | table    | type | key          | rows | filtered|                      |
+----+------------+----------+------+--------------+------+---------+
| 1  | SIMPLE     | employees| ref  | ix_firstname | 224  | 60.82   |
+----+------------+----------+--------------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 동일한 쿼리임에도 히스토그램 정보를 수집한 후 예측치는 60.82%(136.2명)로 상승했고, 실제 조회 결과(143명)와 예측치(136.2명)가 크게 차이가 안나는 것을 확인할 수 있다. 이렇듯 단순 통계 정보만 이용한 경우와 히스토그램을 이용한 경우 차이가 매우 큰 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히스토그램과 인덱스는 완전히 다른 객체이기 때문에 비교 대상은 아니지만, 인덱스는 부족한 통계 정보를 수집하기 위해 사용된다는 측면에서 어느 정도 공통점을 가진다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버는 쿼리의 실행 계획을 수립할 때 사용 가능한 인덱스들로부터 조건절에 일치하는 레코드 건수를 대략 파악하고 최종적으로 가장 나은 실행 계획을 선택한다. 이때, 조건에 일치하는 레코드 건수를 예측하기 위해 옵티마이저는 실제 인덱스의 B-Tree를 샘플링해서 살펴보는데 이를 &lt;b&gt;인덱스 다이브(Index Dive)&lt;/b&gt;라고 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코스트 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버가 쿼리를 처리하려면 다음과 같은 작업을 필요로 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디스크로부터 데이터 페이지 읽기&lt;/li&gt;
&lt;li&gt;메모리로부터 데이터 페이지 읽기&lt;/li&gt;
&lt;li&gt;인덱스 키 비교&lt;/li&gt;
&lt;li&gt;레코드 평가&lt;/li&gt;
&lt;li&gt;메모리 임시 테이블 작업&lt;/li&gt;
&lt;li&gt;디스크 임시 테이블 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버는 쿼리에 대해 이러한 작업이 얼마나 필요한지 예측하고 전체 작업 비용을 계산한 결과를 바탕으로 실행 계획을 수립한다. 이렇게 &lt;b&gt;전체 쿼리 비용을 계산하는데 필요한 단위 작업들의 비용을 코스트 모델&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL 5.7 이전 버전&lt;/b&gt;까지는 이런 작업들의 비용을 MySQL &lt;b&gt;서버 소스 코드에 상수화해서 사용&lt;/b&gt;했다. 하지만 사용하는 하드웨어에 따라 비용은 달라질 수 있기 때문에 비용을 상수화해 적용하는 것은 최적의 실행 계획 수립에 방해 요소였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.7 버전부터 각 단위 작업의 비용을 DMBS 관리자가 조정할 수 있게 개선하여 이러한 단점을 개선했지만, 인덱스되지 않은 컬럼의 데이터 분포나 메모리에 올라가 있는 페이지의 비율 등 비용 계산과 연관된 부분의 정보는 여전히 부족한 상태였다. 이러한 정보는 MySQL 8.0 버전에서야 비로소 히스토그램(데이터 분포)과 인덱스 별 메모리의 페이지 비율이 관리됨으로써 옵티마이저의 실행 계획 수립에 사용되기 시작했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;row_evaluate_cost
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스토리지 엔진이 반환한 레코드가 쿼리의 조건에 일치하는지 평가하는 단위 작업&lt;/li&gt;
&lt;li&gt;값이 증가할수록 풀 테이블 스캔과 같이 많은 레코드를 처리하는 쿼리의 비용이 높아짐&lt;/li&gt;
&lt;li&gt;값이 증가할수록 레인지 스캔과 같이 상대적으로 적은 수의 레코드를 처리하는 쿼리의 비용은 낮아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;key_compare_cost
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키 값의 비교 작업에 필요한 비용&lt;/li&gt;
&lt;li&gt;값이 증가할수록 레코드 정렬과 같이 키 값 비교 처리가 많은 경우 쿼리 비용이 높아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;EXPLAIN FORMAT = TREE
SELECT *
FROM employees WHERE first_name='Matt' \G&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;1438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zehhj/btrN78JM88J/W1gwo3PVmRTKskOGHmK4Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zehhj/btrN78JM88J/W1gwo3PVmRTKskOGHmK4Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zehhj/btrN78JM88J/W1gwo3PVmRTKskOGHmK4Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzehhj%2FbtrN78JM88J%2FW1gwo3PVmRTKskOGHmK4Bk%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;1350&quot; height=&quot;1438&quot; data-origin-width=&quot;1350&quot; data-origin-height=&quot;1438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코스트 모델에서 중요한 것은 각 단위 작업에 설정되는 비용 값이 커지면 어떤 실행 계획들이 고비용으로 바뀌고 어떤 실행 계획들이 저비용으로 바뀌는지를 파악하는 것이다. 아래의 예제는 개략적으로 코스트 모델을 이해하고 각 단위 작업의 비용 조절을 연습해볼 수 있는 기준이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key_compare_cost
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 비용을 높이면 MySQL 서버 옵티마이저가 가능하면 정렬을 수행하지 않는 방향의 실행 계획을 선택할 가능성이 높아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;row_evaluate_cost
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 비용을 높이면 풀 스캔을 실행하는 쿼리들의 비용이 높아지고 MySQL 서버 옵티마이저는 가능하면 인덱스 레인지 스캔을 사용하는 실행 계획을 선택할 가능성이 높아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;disk_temptable_create_cost와 disk_temptable_row_cost
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 비용을 높이면 MySQL 옵티마이저는 디스크에 임 시 테이블을 만들지 않는 방향의 실행 계획을 선택할 가능성이 높아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;memory_temptable_create_cost와 memory_temptable_row_cost
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 비용을 높이면 MySQL 서버 옵티마이저는 메모리 임시 테이블을 만들지 않는 방향의 실행 계획을 선택할 가능성이 높아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;io_block_read_cost
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 비용이 높아지면 MySQL 서버 옵티마이저는 가능하면 InnoDB 버퍼 풀에 데이터 페이지가 많이 적재돼 있는 인덱스를 사용하는 실행 계획을 선택할 가능성이 높아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;memory_block_read_cost
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 비용이 높아지면 MySQL 서버는 InnoDB 버퍼 풀에 적재된 데이터 페이지가 상대적으로 적다고 하더라도 그 인덱스를 시용할 가능성이 높아짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;실행 계획 확인&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버의 실행 계획은 DESC 또는 EXPLAIN 명령으로 확인할 수 있다.&lt;br /&gt;MySQL 8.0부터는 실행 계획의 출력 포맷과 실제 쿼리의 실행 결과까지 확인할 수 있는 기능이 추가됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 계획 출력 포맷&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 이전 버전에서는 EXPLAIN EXTENDED 또는 EXPLAIN PARTITIONS 명령이 구분돼 있었지만, 해당 옵션은 현재(MySQL 8.0) 문법에서 제거됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0부터는 &lt;b&gt;FORMAT 옵션을 사용해&lt;/b&gt; 실행 계획의 표시 방법을 &lt;b&gt;JSON이나 TREE, 단순 테이블 형태&lt;/b&gt;로 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EXPLAIN의 기본값은 테이블 형태 출력이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;// 테이블 형태로 출력
EXPLAIN 
SELECT *
FROM employees e
    INNER JOIN salaries s ON s.emp_no=e.emp_no
WHERE first_name='ABC';

+----+------------+--------+----------+-----+---------------------+-------------+--------+------+-----+---------+------+
|id  |select_type | table |partitions |type |possible_keys        |key          |key_len |ref   |rows |filtered |Extra |
+----+------------+--------+----------+-----+---------------------+-------------+--------+------+-----+---------+------+
| 1  |SIMPLE      | e     | NULL      |ref  |PRIMARY,ix_firstname |ix_firstname |58      |const | 1   | 100.00  | NULL |
| 1  |SIMPLE      | s     | NULL      |ref  |PRIMARY              |PRIMARY      |4       |const | 10  | 100.00  | NULL |
+----+------------+--------+----------+-----+---------------------+-------------+--------+------+-----+---------+------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 트리 형태로 출력
EXPLAIN FORMAT=TREE 
SELECT *
FROM employees e
    INNER JOIN salaries s ON s.emp_no=e.emp_no
WHERE first_name='ABC'\G

*************************** 1. row ***************************
EXPLAIN:-&amp;gt; Nested loop inner join (cost=2.40 rows=10)
-&amp;gt; Index lookup on e using ix_firstname (first_name='ABC') (cost=0.35 rows=1) 
-&amp;gt; Index lookup on s using PRIMARY(emp_no=e.emp_no) (cost=2.05 rows=10)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;// JSON 형태로 출력
EXPLAIN FORMAT=JSON 
SELECT *
FROM employees e
    INNER JOIN salaries s ON s.emp_no=e.emp_no
WHERE first_name='ABC'\G

*************************** 1, row ***************************
EXPLAIN:{ 
    &quot;query_block&quot; : {
        &quot;select_id&quot;: 1, 
        &quot;cost_info&quot; : {
            &quot;query_cost&quot; :&quot;2.40&quot;
        }, 
        &quot;nested_loop&quot;:[
        {
            &quot;table&quot;: {
                &quot;table.name&quot;:&quot;e&quot;, 
                &quot;access_type&quot;:&quot;ref&quot;, 
                &quot;possible_keys&quot;:[
                    &quot;PRIMARY&quot;,
                    &quot;ix_firstname&quot;
                ],
                &quot;key&quot;:&quot;ix_firstnam e&quot;, 
                &quot;used_key_parts&quot;:[
                    &quot;first_name&quot;
                ],
......&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쿼리의 실행 시간 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0.18 버전부터 EXPLAIN ANALYZE 기능이 추가돼 쿼리의 실행 계획과 단계별 소요된 시간 정보를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v3sqi/btrN8zUXEqf/1nhT6tKYbWO4ZDZg3YDmC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v3sqi/btrN8zUXEqf/1nhT6tKYbWO4ZDZg3YDmC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v3sqi/btrN8zUXEqf/1nhT6tKYbWO4ZDZg3YDmC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv3sqi%2FbtrN8zUXEqf%2F1nhT6tKYbWO4ZDZg3YDmC0%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;1638&quot; height=&quot;1190&quot; data-origin-width=&quot;1638&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 실행 순서는 다음 기준으로 읽으면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;들여쓰기가 같은 레벨에서는 상단에 위치한 라인이 먼저 실행&lt;/li&gt;
&lt;li&gt;들여쓰기가 다른 레벨에서는 가장 안쪽에 위치한 라인이 먼저 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위 쿼리는 다음과 같은 실행 순서를 가진다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;1. D) Index lookup on e using ix_firstname 
2. F) Index lookup on s using PRIMARY
3. E) Filter
4. C) Nested loop inner join
5. B) Aggregate using temporary table 
6. A) Table scan on &amp;lt;temporary&amp;gt;

SELECT e.emp_no, avg(s.salary) 
FROM employees e
    INNER JOIN salaries s ON s.emp_no=e.emp_no 
               AND s.salary&amp;gt;50000
               AND s.from_date&amp;lt;= '1990-01-01'
               AND s.to_date&amp;gt;'1990-01-01' 
WHERE e.first_name='Matt'
GROUP BY e.hire_date \G

1. employee 테이블에서 ix_firstname 인덱스를 이용해 first_name = 'Matt' 인 레코드를 찾는다.
2. salaries 테이블의 PRIMARY 키를 통해 emp_no가 1번 결과와 동일한 레코드를 찾는다.
3. INNER JOIN의 조건에 일치하는 건만 가져온다.
4. 1번과 3번의 결과를 조인한다.
5. 임시 테이블에 결과를 저장하면서 GROUP BY 집계를 실행한다.
6. 임시 테이블의 결과를 읽어서 결과를 반환한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>BackEnd/Real MySQL 8.0</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/175</guid>
      <comments>https://jjingho.tistory.com/175#entry175comment</comments>
      <pubDate>Wed, 5 Oct 2022 23:21:35 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Transaction 알아보기</title>
      <link>https://jjingho.tistory.com/174</link>
      <description>&lt;h1&gt;트랜잭션이란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 어떤 작업의 완전성을 보장해주는 것을 의미한다.&lt;br /&gt;논리적인 작업 단위를 완벽하게 처리하거나 모두 취소하여 작업의 일부만 적용되는 현상을 방지하는 기술이다. 즉, 데이터의 정합성을 보장하기 위한 기능이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 가장 쉬운 예로 계좌 송금 시스템을 떠올릴 수 있다.&lt;br /&gt;계좌 송금 시스템의 논리적인 작업 단위는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A가 B에게 계좌 송금 신청&lt;/li&gt;
&lt;li&gt;A가 송금 가능한 상태인지 확인(신청한 송금 금액이 계좌에 들어있는지)&lt;/li&gt;
&lt;li&gt;A가 신청한 금액만큼 A의 계좌 금액 차감&lt;/li&gt;
&lt;li&gt;B에게 금액 송금&lt;/li&gt;
&lt;li&gt;B의 계좌에 송금된 금액이 가산&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 논리적 작업을 수행하는 도중 4번 과정에서(B에게 금액을 송금) 장애가 발생한다면 어떻게 될까?&lt;br /&gt;당연히 송금은 취소되고 A의 계좌에서 차감됐던 금액도 원상복구가 된다.&lt;br /&gt;이게 가능한 이유가 바로 &lt;b&gt;트랜잭션 덕분이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에 트랜잭션이 보장되지 않았다면 A의 계좌는 송금 금액만큼 차감됐지만 B의 계좌 금액은 가산되지 않는 불상사가 일어났을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tj1Cj/btrMBscrtg9/s4eDpYgKfJCuno7zmGeOLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tj1Cj/btrMBscrtg9/s4eDpYgKfJCuno7zmGeOLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tj1Cj/btrMBscrtg9/s4eDpYgKfJCuno7zmGeOLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftj1Cj%2FbtrMBscrtg9%2Fs4eDpYgKfJCuno7zmGeOLk%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;1200&quot; height=&quot;675&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이란 개념은 우리가 사용하는 모든 서비스에 적용되어있다. 트랜잭션 덕분에 데이터의 정합성이 보장되고 그렇기에 우리는 안심하고 서비스를 사용할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 우리는 Java + Spring 기반의 애플리케이션 개발을 할 때 트랜잭션을 어떻게 적용할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;Spring의 트랜잭션&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 트랜잭션을 사용하는 방법은 크게 두 가지다.&lt;br /&gt;&lt;b&gt;트랜잭션 서비스 추상화 계층&lt;/b&gt;을 이용하는 방법과 &lt;b&gt;선언적 트랜잭션&lt;/b&gt;을 이용하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트랜잭션 서비스 추상화 계층&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 트랜잭션 추상화 기술을 제공하고 있다.&lt;br /&gt;이를 이용하면 애플리케이션에서 트랜잭션 API를 이용하지 않고 트랜잭션 경계설정 작업이 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 추상화 계층의 상위 인터페이스인 &lt;b&gt;PlatformTransactionManager&lt;/b&gt;를 통해 일관된 방식으로 트랜잭션을 제어하는 경계설정 작업이 가능해지는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 각자의 환경에 맞는 TransactionManager 클래스를 주입해서 트랜잭션 추상화 기술을 사용할 수 있다. JDBC를 이용하는 경우는 DataSourceTransactionManager를 주입해 사용하면 되고, JPA를 이용하는 경우 JpaTransactionManager를 주입하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;998&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqtXaS/btrMDgiio0B/81d798jIGWobgkhqk0eOwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqtXaS/btrMDgiio0B/81d798jIGWobgkhqk0eOwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqtXaS/btrMDgiio0B/81d798jIGWobgkhqk0eOwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqtXaS%2FbtrMDgiio0B%2F81d798jIGWobgkhqk0eOwk%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;1684&quot; height=&quot;998&quot; data-origin-width=&quot;1684&quot; data-origin-height=&quot;998&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 트랜잭션 추상화 API의 사용 방법을 간단히 살펴보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SomeService {
    // 환경에 맞는 트랜잭션 매니저 주입
  private final PlatformTransactionManager transactionManager;

    public void remittance() {
        TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
         // 트랜잭션 시작
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

        try {
            // 송금에 대한 비즈니스 로직 실행
            businessLogic();
            transactionManager.commit(transactionStatus);
        } catch (Exception e) {
            transactionManager.rollback(transactionStatus);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DefaultTransactionDefinition&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefaultTransactionDefinition은 트랜잭션에 대한 네 가지 속성(propagation, isolationLevel, timeout, readOnly)을 담고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;propagation(트랜잭션 전파 옵션)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션의 경계에서 이미 선행되는 트랜잭션이 있거나 없는 경우 트랜잭션을 어떻게 동작시킬 것인가에 대한 설정&lt;/li&gt;
&lt;li&gt;이 속성에 대한 설명은 이후 아래에서 더 자세히 살펴보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;isolation(격리 수준)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 격리 수준에 대한 설정&lt;/li&gt;
&lt;li&gt;스프링 트랜잭션의 기본값은 &lt;i&gt;&lt;code&gt;Isolation.DEFAULT&lt;/code&gt;&lt;/i&gt;로 현재 사용 중인 DB 격리 수준의 기본값을 따른다.&lt;/li&gt;
&lt;li&gt;격리 수준에 대한 자세한 설명은 &lt;a href=&quot;https://jjingho.tistory.com/166&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MySQL 격리 수준&lt;/a&gt;을 참고하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;timeout(제한 시간)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션의 수행 시간을 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;readOnly(읽기 전용)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 전용 트랜잭션 설정&lt;/li&gt;
&lt;li&gt;해당 옵션을 명시하면 트랜잭션에서 시도되는 데이터 조작을 방지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TransactionStatus&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionStatus는 시작된 &lt;b&gt;트랜잭션에 대한 구분 정보&lt;/b&gt;를 담고 있으며, 트랜잭션에 대한 조작이 필요할 때(커밋이나 롤백) PlatformTransactionManager 메서드의 파라미터로 전달해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;선언적 트랜잭션 - @Transactional&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언적 트랜잭션이라고도 불리는 @Transactional 애너테이션은 트랜잭션을 단순하고 직관적으로 사용할 수 있게 해 준다. 따라서 일반적으로 가장 많이 사용되는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 방법은 아주 간단하다.&lt;br /&gt;설정 파일에 @EnableTransactionManagement를 선언한 후, 트랜잭션을 적용하고 싶은 &lt;b&gt;타입 혹은 메서드에&lt;/b&gt; &lt;b&gt;@Transactional 애너테이션을&lt;/b&gt; 붙여주면 된다. 스프링 부트에서는 AutoConfiguration에 의해 @EnableTransactionManagement가 자동으로 설정되므로 별도의 설정 자체도 필요 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언적 트랜잭션을 사용한 코드의 예시를 살펴보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SomeService {

    @Transactinal
    public void remittance() {
        businessLogic();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션에 대한 코드를 작성하지 않고 @Transactional 애너테이션을 명시하는 것만으로 트랜잭션 기능을 사용할 수 있게 되었고 트랜잭션 코드와 비즈니스 로직이 분리되어 훨씬 명확한 코드가 됐다. 또한, @Transactional은 속성 정보를 메서드마다 다르게 설정할 수 있어 세밀한 트랜잭션 속성의 제어가 필요한 경우 아주 유연하게 사용할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 애너테이션 하나만으로 이런 멋진 일들이 가능한 것일까?&lt;br /&gt;비밀은 Spring의 핵심 기술 중 하나인 &lt;b&gt;AOP&lt;/b&gt;에 숨겨져 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SXkZ7/btrMBsKka7C/RvpbiiMMCZk2L1X8l3hs3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SXkZ7/btrMBsKka7C/RvpbiiMMCZk2L1X8l3hs3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SXkZ7/btrMBsKka7C/RvpbiiMMCZk2L1X8l3hs3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSXkZ7%2FbtrMBsKka7C%2FRvpbiiMMCZk2L1X8l3hs3K%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;1094&quot; height=&quot;496&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP란 애플리케이션에서 사용되는 부가 기능들을 모듈화해 재사용할 수 있도록 하는 기술이다. 예를 들면 트랜잭션이나 로깅, 실행 시간 측정 등 비즈니스 로직과 함께 수행되는 부가 기능들을 모듈화 하고, 특정 시점에 끼워 넣어 재사용함으로써 중복을 제거하고 비즈니스 로직과 부가 기능을 명확히 분리하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 AOP는 &lt;b&gt;런타임 시점에 별도의 코드 조작 없이&lt;/b&gt; 자동으로 프록시를 생성하고 이를 이용해 Target Object에 부가 기능을 적용시킨다. 위의 예시 코드를 풀어보면 개략적으로 다음과 같은 형태가 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 프록시 생성
public class TargetObjectProxy {
    private SomeService target;

    public void remittance() {
        TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);

        try {
            // target 메서드 호출
            target.businessLogic();
            transactionManager.commit(transactionStatus);
        } catch (Exception e) {
            transactionManager.rollback(transactionStatus);
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring AOP의 프록시 자동생성 기법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring은 반복적인 위임 코드가 필요한 &lt;b&gt;프록시 클래스 코드의 중복 문제&lt;/b&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;JDK Dynamic Proxy&lt;/li&gt;
&lt;li&gt;CGLIB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XtXyQ/btrME3PyL0W/tesQYQpWlkjCRMPHZJfqz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XtXyQ/btrME3PyL0W/tesQYQpWlkjCRMPHZJfqz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XtXyQ/btrME3PyL0W/tesQYQpWlkjCRMPHZJfqz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXtXyQ%2FbtrME3PyL0W%2FtesQYQpWlkjCRMPHZJfqz1%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;488&quot; height=&quot;427&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JDK Dynamic Proxy는 인터페이스를 구현한 오브젝트에 대해&lt;/b&gt; 프록시 클래스를 런타임에 동적으로 생성해준다. 타겟 오브젝트의 &lt;b&gt;인터페이스를 구현한 프록시 객체를 생성하므로 구체 클래스에 대한 타입 캐스팅이 불가능&lt;/b&gt;하다. 따라서 프록시 빈을 정상적으로 사용하려면 의존 주입 시 &lt;b&gt;반드시 인터페이스의 타입을 명시해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
public class SomeController {
    @Autowired
    private SomeServiceImpl someService; // 런타임 오류 발생 -&amp;gt; 구체 클래스 타입 캐스팅 불가능
}

@Service
public class SomeServiceImpl implements SomeService {
     .....
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k6IM4/btrMDYA9XJs/FNKYd9Ydg9tcbRiv9vkxL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k6IM4/btrMDYA9XJs/FNKYd9Ydg9tcbRiv9vkxL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k6IM4/btrMDYA9XJs/FNKYd9Ydg9tcbRiv9vkxL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk6IM4%2FbtrMDYA9XJs%2FFNKYd9Ydg9tcbRiv9vkxL0%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;482&quot; height=&quot;438&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CGLIB는&lt;/b&gt; 타겟 클래스의 바이트 코드를 조작해 프록시 객체를 생성한다. 타겟 오브젝트를 상속해 프록시 객체를 생성하므로 JDK Dynamic Proxy와는 다르게 구체 클래스에 대해서도 프록시 생성이 가능하다. 리플랙션을 이용하는 JDK Dynamic Proxy에 비해 속도가 빠르며 구체 클래스가 AOP를 사용할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타겟 오브젝트를 상속해 프록시를 구현하므로 상속이 불가능한 final 클래스나 final 메서드, private 메서드는 AOP의 대상이 되지 않으며 public 메서드만 프록시를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Transactional 속성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 애너테이션은 클래스 레벨 또는 메서드 레벨에 부착할 수 있으며 다양한 속성을 설정할 수 있어 아주 간편하게 원하는 옵션을 트랜잭션에 끼워 넣을 수 있는 기능을 제공하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 트랜잭션이 제공하고 있는 속성 정보는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brdvDc/btrMDnVSk0S/NIiUOio3ZgmzOmbK11ryfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brdvDc/btrMDnVSk0S/NIiUOio3ZgmzOmbK11ryfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brdvDc/btrMDnVSk0S/NIiUOio3ZgmzOmbK11ryfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrdvDc%2FbtrMDnVSk0S%2FNIiUOio3ZgmzOmbK11ryfK%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;1332&quot; height=&quot;518&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;height: 247px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;속성(옵션)&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;value&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;transactionManager의 별칭을 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;transactionManager&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;지정된 transactionManager의 식별 값(qualifier value) 또는 빈 이름 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;label&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;트랜잭션 설명 목적으로 사용되는 label을 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;propagation&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;트랜잭션이 어떻게 동작할 것인가에 대한 전파 옵션을 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;isolation&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;트랜잭션의 격리 수준을 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;timeout&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;트랜잭션 타임아웃 설정(int)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;timeoutString&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;트랜잭션 타임아웃 설정(String)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;readOnly&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;읽기 전용(데이터 조작 방지) 트랜잭션 설정, true인 경우 활성화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;rollbackFor&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;검사 예외(Checked Exception)발생 시 롤백을 수행할 예외 지정 속성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;rollbackForClassName&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;rollbackFor과 동일하지만 클래스명을 문자열로 지정하는 속성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;noRollbackFor&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;비검사 예외, 즉 RuntimeException과 그 하위 예외 발생 시 롤백 처리를 수행하지 않을 예외를 지정하는 속성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 175.07px;&quot;&gt;noRollbackForClassName&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 641.766px;&quot;&gt;noRollbackFor와 동일하지만 클래스명을 문자열로 명시하는 속성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;Propagation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 전파 옵션(propagation)은 트랜잭션의 경계에서 이미 선행되는 트랜잭션이 있거나 없는 경우 트랜잭션을 어떻게 동작시킬 것인가에 대한 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양하고 복잡한 현실 세계의 문제를 해결하기 위해서는 트랜잭션을 단순하게만 사용할 수는 없다. 비즈니스 요구사항에 따라 트랜잭션도 복잡해질 수 있다. 예를 들어, 선행 트랜잭션 내부에서 독립적으로 실행되어야 하는 트랜잭션이 존재한다거나, 커밋이나 롤백 시점을 어떤 트랜잭션에 의존적으로 동작하게 만들지 제어해야 하는 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 트랜잭션 전파 옵션(propagation)은 트랜잭션 동작 방식을 애플리케이션단에서 조금 더 쉽게 설정하고 사용할 수 있도록 하는 옵션이다.&lt;/p&gt;
&lt;table style=&quot;height: 551px; width: 862px;&quot; width=&quot;874&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 24px;&quot;&gt;
&lt;td style=&quot;width: 161.203px; height: 24px;&quot;&gt;속성&lt;/td&gt;
&lt;td style=&quot;width: 658.797px; height: 24px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px; width: 163.203px;&quot;&gt;REQUIRED&lt;/td&gt;
&lt;td style=&quot;height: 38px; width: 660.797px;&quot;&gt;- 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 생성&lt;br /&gt;- 이미 트랜잭션이 존재하는 경우 새로운 트랜잭션을 생성하지 않고 기존 트랜잭션을 그대로 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 163.203px;&quot;&gt;REQUIRES_NEW&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 660.797px;&quot;&gt;- 항상 새로운 독립 트랜잭션을 생성&lt;br /&gt;&lt;span style=&quot;background-color: #efefef;&quot;&gt;- 생성된 트랜잭션은 별도의 커밋 &amp;amp; 롤백 시점을 가짐&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 163.203px;&quot;&gt;MANDATORY&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 660.797px;&quot;&gt;- 트랜잭션이 존재하지 않는 경우 예외 발생&lt;br /&gt;&lt;span style=&quot;background-color: #efefef;&quot;&gt;- 이미 트랜잭션이 존재하는 경우 새로운 트랜잭션을 생성하지 않고 기존 트랜잭션을 그대로 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px; width: 163.203px;&quot;&gt;NESTED&lt;/td&gt;
&lt;td style=&quot;height: 38px; width: 660.797px;&quot;&gt;- 트랜잭션이 존재하지 않는다면 새로운 트랜잭션을 생성&lt;br /&gt;&lt;span style=&quot;background-color: #efefef;&quot;&gt;- 이미 트랜잭션이 존재하는 경우 새로운 트랜잭션을 생성&lt;br /&gt;&lt;span style=&quot;background-color: #efefef;&quot;&gt;- 중첩 트랜잭션 롤백 시 선행 트랜잭션에 전파되지 않음&lt;br /&gt;&lt;span style=&quot;background-color: #efefef;&quot;&gt;- 선행 트랜잭션 롤백 시 중첩 트랜잭션도 함께 롤백(전파)되고 커밋될 때 중첩 트랜잭션도 함께 커밋&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px; width: 163.203px;&quot;&gt;SUPPORTS&lt;/td&gt;
&lt;td style=&quot;height: 38px; width: 660.797px;&quot;&gt;- 트랜잭션이 존재하지 않는다면 트랜잭션을 사용하지 않음&lt;br /&gt;&lt;span style=&quot;background-color: #efefef;&quot;&gt;- 이미 트랜잭션이 존재하는 경우 새로운 트랜잭션을 생성하지 않고 기존 트랜잭션을 그대로 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 163.203px;&quot;&gt;NOT_SUPPORTED&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 660.797px;&quot;&gt;- 트랜잭션을 사용하지 않음&lt;br /&gt;&lt;span style=&quot;background-color: #efefef;&quot;&gt;- 트랜잭션을 무시하고 로직을 수행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 163.203px;&quot;&gt;NEVER&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 660.797px;&quot;&gt;- 트랜잭션이 존재하는 경우 예외 발생&lt;br /&gt;&lt;span style=&quot;background-color: #efefef;&quot;&gt;- 트랜잭션을 허용하지 않는 경우에 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SomeService {

    // 기본값인 REQUIRED 전파 옵션 사용
    @Transactional
    public void remittance1() {
        businessLogic();
    }

    // REQUIRES_NEW 전파 옵션 사용
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void remittance2() {
        businessLogic();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RollbackFor &amp;amp; NoRollbackFor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 트랜잭션은 확인된 예외(Checked Exception)에 대해서는 롤백을 수행하지 않는다.&lt;/b&gt; 스프링 트랜잭션의 &lt;b&gt;롤백 대상은 RuntimeException과 Error이며&lt;/b&gt; RuntimeException을 상속한 NullPointerException이나 IllegalArgumentException과 같은 &lt;b&gt;비검사 예외(Unchecked Exception)가 롤백의 대상&lt;/b&gt;이라고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rollbackFor 속성은 Checked Exception 발생 시 롤백을 수행할 예외를 설정할 때 사용된다. 해당 속성 값으로는 Throwable의 하위 클래스 범위 내에 N개의 예외 클래스를 지정할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk9Dek/btrMBsXQH0a/A1K82b3bC9U2efa6hhN1NK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk9Dek/btrMBsXQH0a/A1K82b3bC9U2efa6hhN1NK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk9Dek/btrMBsXQH0a/A1K82b3bC9U2efa6hhN1NK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk9Dek%2FbtrMBsXQH0a%2FA1K82b3bC9U2efa6hhN1NK%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;886&quot; height=&quot;70&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;70&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 애너테이션의 rollbackFor 속성의 기본 값은 RuntimeException과 Error이다. rollbackFor 속성을 명시하지 않은 경우 롤백의 대상은 RuntimeException과 Error가 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// rollbackFor 옵션 생략
@Transactional
public void remittance() {
    businessLogic();
}

// 위 메서드와 동일한 동작
@Transactional(rollbackFor = { RuntimeException.class, Error.class })
public void remittance() {
    businessLogic();
}&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;rollbackFor 옵션에는 Throwable의 하위 클래스를 모두 지정할 수 있으므로 Exception 클래스 자체를 지정할 수도 있다. 이렇게 하면 모든 예외에 대해 롤백을 수행할 수 있게 되지만, Exception은 거의 모든 예외를 포함하므로 다른 규칙들을 잡아먹어 버린다. 필수 사항은 아니지만 rollbackFor 옵션에는 좀 더 구체적인 예외 정보를 명시해주는 것이 좋은 선택이 될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 모든 예외에 대해 롤백 수행
@Transactional(rollbackFor = { Exception.class })
public void remittance() {
    businessLogic();
}

// 구체적인 CheckedException을 명시해 특정 예외 발생 시 롤백을 수행
@Transactional(rollbackFor = { FileNotFoundException.class, RuntimeException.class, Error.class })
public void remittance() {
    businessLogic();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;noRollbackFor 옵션은 rollbackFor 옵션과 반대로 Error나 RuntimeException 예외 발생 시 롤백을 수행하지 않을 예외를 지정하는 속성이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 특정 비즈니스 로직에서 발생하는 예외(RuntimeException 상속)에 대해서만 롤백 수행
@Transactional(noRollbackFor = { SomeBusinessException.class })
public void remittance() {
    businessLogic();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Transactional 사용 시 주의사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서는 메서드나 클래스 레벨에 @Transactional 애너테이션을 부착하는 것만으로 손쉽게 트랜잭션 기능을 사용할 수 있다. 하지만 동작 방식에 대한 이해 없이 마구 사용하다 보면 왜 트랜잭션이 제대로 동작을 안 하는지 어디서 문제가 발생하지 찾기가 쉽지 않다.(내 얘기..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;private 메서드와 final 키워드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 AOP는 대상에 대한 프록시 객체를 생성하고 트랜잭션 기능을 끼워 넣는 식으로 동작한다. 이때 타겟 오브젝트를 상속(extends)하거나 구현(implements)해서 프록시를 생성하는데 트랜잭션을 적용해야 할 대상 메서드의 접근 제어자가 private인 경우 재정의가 불가능하고 호출 자체도 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 맥락으로 메서드에 final 키워드를 명시하면 재정의를 금지시키므로 AOP를 적용할 대상이 되지 못한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tIVHg/btrMBsQ5HdX/Ck4I4KS9R3ycz2A3uGMybK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tIVHg/btrMBsQ5HdX/Ck4I4KS9R3ycz2A3uGMybK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tIVHg/btrMBsQ5HdX/Ck4I4KS9R3ycz2A3uGMybK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtIVHg%2FbtrMBsQ5HdX%2FCk4I4KS9R3ycz2A3uGMybK%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;884&quot; height=&quot;328&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;328&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;968&quot; data-origin-height=&quot;308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m8bXZ/btrMBsDvbFT/pIVw0HK3F10T9H7b1CseyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m8bXZ/btrMBsDvbFT/pIVw0HK3F10T9H7b1CseyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m8bXZ/btrMBsDvbFT/pIVw0HK3F10T9H7b1CseyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm8bXZ%2FbtrMBsDvbFT%2FpIVw0HK3F10T9H7b1CseyK%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;968&quot; height=&quot;308&quot; data-origin-width=&quot;968&quot; data-origin-height=&quot;308&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;h3 data-ke-size=&quot;size23&quot;&gt;프록시 내부 호출&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class SomeService {

    private final SomeRepository someRepository;

    public void targetMethod() {
        innerMethod();
    }

    @Transactional
    public void innerMethod() {
        someRepository.save(new Some(&quot;something!&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 트랜잭션은 정상적으로 동작하지 않는다.&lt;br /&gt;앞서 살펴봤듯 트랜잭션 AOP를 적용할 때 타겟 오브젝트의 &lt;b&gt;프록시 객체를 생성하&lt;/b&gt;고 트랜잭션 기능을 끼워 넣는다. 따라서 클라이언트는 &lt;b&gt;프록시 빈을 호출&lt;/b&gt;하고, 트랜잭션을 시작한 후 &lt;b&gt;프록시의 타겟 메서드를 호출&lt;/b&gt;한다. 타겟 메서드 실행 후에는 커밋이나 롤백을 수행한다. 이 말은 클라이언트가 프록시로 감싸진 타겟 메서드를 호출했을 때 정상적으로 트랜잭션 AOP가 적용될 수 있다는 걸 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위 예제에서는 &lt;code&gt;targetMethod()&lt;/code&gt;가 내부 메서드 &lt;code&gt;innerMethod()&lt;/code&gt;를 호출하는 형태를 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;targetMethod()&lt;/code&gt;는 클라이언트에게 호출될 때 프록시를 통해 호출되지만 실제 트랜잭션이 필요한 &lt;code&gt;innerMethod()&lt;/code&gt;는 프록시에게 호출되는 것이 아닌 자기 자신(targetMethod)에게 호출당하는 것이기 때문에 트랜잭션이 동작하지 않는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 innerMethod()를 분리해서 프록시로 감싸 지게 만들거나 self injection을 통해 해결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class SomeService {

    private final InnerService innerService;

    public void targetMethod() {
        // 프록시로 감싸진 internalMethod 호출
        innerService.internalMethod();
    }
}

// innerMethod 분리!
@Slf4j
@Component
@RequiredArgsConstructor
public class InnerService {

    private final SomeRepository someRepository;

    @Transactional
    public void innerMethod() {
        someRepository.save(new Some(&quot;something!&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// self injection을 통해 innerMethod를 프록시로 감싸기
@Slf4j
@Service
@RequiredArgsConstructor
public class SomeService {
    @Autowired
    private SomeService someService;
    private final SomeRepository someRepository;

    public void targetMethod() {
        someService.internalMethod();
    }

    @Transactional
    public void innerMethod() {
        someRepository.save(new Some(&quot;something!&quot;));
    }
}&lt;/code&gt;&lt;/pre&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 API에 의존적인 비즈니스 로직이나 수행 시간이 너무 긴 로직의 경우 트랜잭션 AOP를 사용하는 것이 비효율적일 수 있다. &lt;b&gt;@Transactional은 메서드 단위로 경계 설정되므로 트랜잭션 범위를 제어하기 어렵기 때문이다&lt;/b&gt;. 트랜잭션의 범위가 커질수록 DB 커넥션이나 잠금에 대한 유지 시간이 길어져 다양한 문제를 일으킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 다음과 같은 흐름을 가진 메서드가 있다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1) 처리 시작
2) 사용자의 로그인 여부 확인
3) 사용자의 글쓰기 내용의 오류 여부 확인
4) 첨부로 업로드된 파일 확인 및 저장
5) 사용자의 입력 내용을 DBMS에 저장
6) 첨부 파일 정보를 DBMS에 저장
7) 저장된 내용 또는 기타 정보를 DBMS에서 조회
8) 게시물 등록에 대한 알림 메일 발송
9) 알림 메일 발송 이력을 DBMS에 저장
10) 처리 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional을 이용했다면 처리 로직의 트랜잭션의 범위는 다음과 같이 잡히게 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1) 처리 시작
  =&amp;gt; 데이터베이스 커넥션 생성
  =&amp;gt; 트랜잭션 시작
2) 사용자의 로그인 여부 확인
3) 사용자의 글쓰기 내용의 오류 여부 확인
4) 첨부로 업로드된 파일 확인 및 저장
5) 사용자의 입력 내용을 DBMS에 저장
6) 첨부 파일 정보를 DBMS에 저장
7) 저장된 내용 또는 기타 정보를 DBMS에서 조회
8) 게시물 등록에 대한 알림 메일 발송
9) 알림 메일 발송 이력을 DBMS에 저장
  &amp;lt;= 트랜잭션 종료(COMMIT)
  &amp;lt;= 데이터베이스 커넥션 반납
10) 처리 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직 흐름에 따라 메서드 수행 로직 전체가 트랜잭션 범위로 설정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 2~4번 작업은 단순 조회 작업이므로 트랜잭션에 포함될 필요는 없다. 또한, 8번의 &lt;b&gt;메일 발송 작업은 외부 메일 서버에서 수행하므로 불필요한 트랜잭션 범위&lt;/b&gt;이고 만약 네트워크 장애 등의 이유로 외부 메일 서버가 통신 불가의 상태에 빠진다면 웹 서버뿐 아니라 DB 서버 장애로까지 이어질 수 있기 때문에 트랜잭션 범위에서 제외시키는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 DB에 데이터 저장이 일어나는 시점은 5번과 6번, 9번이기 때문에 다음과 같이 트랜잭션 범위를 최소화하고 나눠주면 위험도를 낮출 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1) 처리 시작
2) 사용자의 로그인 여부 확인
3) 사용자의 글쓰기 내용의 오류 발생 여부확인 
4) 첨부로업로드된 파일 확인 및 저장
  =&amp;gt; 데이터베이스 커넥션 생성(또는 커넥션 풀에서 가져오기)
  =&amp;gt; 트랜잭션 시작
5) 사용자의 입력 내용을 DBMS에 저장 
6) 청부 파일 정보를 DBMS에 저장
  &amp;lt;= 트랜잭션 종료(COMMIT)
7) 저장된내용 또는 기타 정보를 DBMS에서 조회 
8) 게시물등록에 대한 알림 메일 발송
  =&amp;gt; 트랜잭션 시작
9) 알림 메일 발송 이력을 DBMS에 저장
  &amp;lt;= 트랜잭션 종료(COMMIT)
  &amp;lt;= 데이터베이스 커넥션 종료(또는 커넥션 풀에 반납) 
10) 처리 완료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시처럼 트랜잭션 범위를 최소화하기 위해서는 개발자가 직접 트랜잭션의 경계를 설정해줘야 한다. 트랜잭션의 경계를 수동으로 설정하는 방법은 앞서 살펴본 &lt;b&gt;트랜잭션 서비스 추상화 계층을 사용하는 방법&lt;/b&gt;과 &lt;b&gt;TransactionTemplate를 사용하는 방법&lt;/b&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;b&gt;트랜잭션 서비스 추상화 계층&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;/**
* 트랜잭션 서비스 추상화 계층을 이용한 트랜잭션 범위 최소화
*/
@Service
@RequiredArgsConstructor
public class SomeService { 
    // 환경에 맞는 트랜잭션 매니저 주입
    private final PlatformTransactionManager transactionManager;

    public void businessLogic() {
        사용자의 로그인 여부 확인();
        사용자의 글쓰기 내용의 오류 여부 확인();
        첨부로 업로드된 파일 확인 및 저장();
        doSaveTransaction();
        저장된 내용 또는 기타 정보를 DBMS에서 조회();
        게시물 등록에 대한 알림 메일 발송(); 
        saveEmailHistoryTransaction();
    }

    public void doSaveTransaction() {
        TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            사용자의 입력 내용을 DBMS에 저장();
            첨부 파일 정보를 DBMS에 저장();
            transactionManager.commit(transactionStatus);
        } catch (Exception e) {
            transactionManager.rollback(transactionStatus);
        }
    }

    public void saveEmailHistoryTransaction() {
        TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            알림 메일 발송 이력을 DBMS에 저장(); 
            transactionManager.commit(transactionStatus);
        } catch (Exception e) {
            transactionManager.rollback(transactionStatus);
        }
    }
}&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;b&gt;TransactionTemplate&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;/**
* TransactionTemplate을 이용한 트랜잭션 범위 최소화
*/
@Service
@RequiredArgsConstructor
public class SomeService { 
    private final TransactionTemplate transactionTemplate;

    public void businessLogic() {
        사용자의 로그인 여부 확인(); 
        사용자의 글쓰기 내용의 오류 여부 확인(); 
        첨부로 업로드된 파일 확인 및 저장(); 
        doSaveTransaction();
        저장된 내용 또는 기타 정보를 DBMS에서 조회();
        게시물 등록에 대한 알림 메일 발송(); 
        saveEmailHistoryTransaction();
    }

    public void doSaveTransaction() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override 
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                사용자의 입력 내용을 DBMS에 저장();
                첨부 파일 정보를 DBMS에 저장();
            }
        });
    }

    public void saveEmailHistoryTransaction() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override 
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                알림 메일 발송 이력을 DBMS에 저장();
            }
        });
    }
}&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;TransactionTemplate의 경우 내부 execute 메서드에 try-catch문이 정의되어 있어 커밋과 롤백에 대한 설정을 따로 해줄 필요가 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHuovV/btrMzDMbplt/P98PTmYOPTYmPmwKsdJNNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHuovV/btrMzDMbplt/P98PTmYOPTYmPmwKsdJNNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHuovV/btrMzDMbplt/P98PTmYOPTYmPmwKsdJNNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHuovV%2FbtrMzDMbplt%2FP98PTmYOPTYmPmwKsdJNNk%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;1896&quot; height=&quot;1250&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1250&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;size16&quot;&gt;만약 특정 예외에 대한 롤백을 수행하고 싶다면 로직에 try-catch문을 추가해주기만 하면 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/**
 * TransactionTemplate을 이용한 트랜잭션 범위 최소화
 */
@Service
@RequiredArgsConstructor
public class SomeService { 
    private final TransactionTemplate transactionTemplate;

    public void businessLogic() {
        사용자의 로그인 여부 확인(); 
        사용자의 글쓰기 내용의 오류 여부 확인(); 
        첨부로 업로드된 파일 확인 및 저장(); 
        doSaveTransaction();
        저장된 내용 또는 기타 정보를 DBMS에서 조회();
        게시물 등록에 대한 알림 메일 발송(); 
        saveEmailHistoryTransaction();
    }

    public void doSaveTransaction() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override 
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    사용자의 입력 내용을 DBMS에 저장();
                    첨부 파일 정보를 DBMS에 저장();
                } catch (Exception e) {
                    status.setRollbackOnly();
                }
            }
        });
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>BackEnd/Spring</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/174</guid>
      <comments>https://jjingho.tistory.com/174#entry174comment</comments>
      <pubDate>Tue, 20 Sep 2022 20:41:15 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 쿼리 힌트</title>
      <link>https://jjingho.tistory.com/173</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 버전이 업그레이드되면서 옵티마이저의 쿼리 실행 계획 최적화 방법도 다양해지고 있다. 하지만 옵티마이저가 개발자나 DBA의 요구 조건을 100% 예측할 수는 없다. 따라서 옵티마이저가 부족한 실행 계획을 수립한 경우에는 우리가 옵티마이저에게 어떻게 실행계획을 수립해야 할지 알려줄 수 있는 방법이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 사용 가능한 쿼리 힌트는 다음과 같이 2가지로 구분할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 힌트&lt;/li&gt;
&lt;li&gt;옵티마이저 힌트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;인덱스 힌트&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 힌트는 예전 버전의 MySQL 서버에서 사용되어 오던 STRAIGHT_JOIN과 USE INDEX 같은 힌트를 의미한다. 이 기능들은 각 SQL문법에 영향을 받기 때문에 ANSI-SQL 표준 문법을 준수하지 못하며, 가능하면 MySQL 5.6 버전에 도입된 옵티마이저 힌트를 사용하는 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;STRAIGHT_JOIN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STRAIGHT_JOIN은 여러 개의 테이블이 조인되는 경우 조인 순서를 고정하는 역할을 한다. 즉, 옵티마이저에게 드라이빙 테이블과 드리븐 테이블에 대한 힌트를 주는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 각 테이블의 통계 정보와 쿼리의 조건을 기반으로 최적의 순서로 조인을 수행한다. 일반적으로 조인을 하기 위한 컬럼들의 인덱스 여부로 조인의 순서가 결정되면, 조인 컬럼의 인덱스에 아무런 문제가 없는 경우 레코드가 적은 테이블을 드라이빙으로 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 쿼리의 조인 순서를 변경하려는 경우에는 다음과 같이 STRAIGHT_JOIN 힌트를 사용해 테이블의 조인 순서를 유도할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT /*! STRAIGHT_]OIN */ 
    e.first_name, e.last_name, d.dept_name
FROM employees e, dept_emp de, departments d 
WHERE e.emp_no=de.emp_no
    AND d.dept_no=de.dept_no;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리의 실행 계획을 보면 FROM 절에 명시된 테이블의 순서대로(employees &amp;rarr; dept_emp &amp;rarr; departments) 조인이 수행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 기준에 맞게 조인 순서가 결정되지 않는 경우에만 STRAIGHT_JOIN 힌트로 조인 순서를 조정하는 것이 좋다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임시 테이블과 일반 테이블의 조인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 테이블 조인 컬럼에 인덱스가 없는 경우 옵티마이저가 실행 계획을 제대로 수립하지 못해 성능 저하가 심할 때 레코드 건수가 작을 쪽을 드라이빙으로 선택하는 게 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;임시 테이블끼리 조인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임시 테이블은 항상 인덱스가 없으므로 어느쪽이 드라이빙이 되어도 상관없다. 따라서 크기가 작은 테이블을 드라이빙으로 선택하는 게 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일반 테이블끼리 조인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;양 테이블의 인덱스 상황이 같은 경우(양쪽 모두 인덱스가 있거나 없거나) 레코드 건수가 적은 테이블을 드라이빙으로 선택하는 것이 좋다. 이 외의 경우 조인 컬럼에 인덱스가 없는 테이블을 드라이빙으로 선택하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한 레코드 건수는 테이블 전체의 레코드 건수가 아니라 WHERE 조건에 포함된 레코드 건수를 나타내니 혼동하지 않도록 주의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;USE INDEX / FORCE INDEX / IGNORE INDEX&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 테이블에 3~4개 이상의 컬럼을 포함하는 비슷한 인덱스가 여러 개 존재하는 경우 어떤 인덱스를 선택해야 하는지 혼동할 수 있다. 이런 경우 강제로 특정 인덱스를 사용하도록 힌트를 추가할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;USE INDEX
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;옵티마이저에게 특정 테이블의 인덱스를 사용하도록 &lt;b&gt;권장&lt;/b&gt;하는 힌트&lt;/li&gt;
&lt;li&gt;옵티마이저가 항상 그 힌트를 선택하는 건 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FORCE INDEX
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;USE INDEX보다 영향력이 강한 힌트&lt;/li&gt;
&lt;li&gt;USE INDEX와 비교해 차이가 없으므로 거의 사용할 필요가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;IGNORE INDEX
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 인덱스를 사용하지 못하도록 하는 힌트&lt;/li&gt;
&lt;li&gt;때로는 풀 테이블 스캔을 유도하기 위해 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 좋은 실행 계획이 어떤 것인지 판단하기 힘든 상황이라면 힌트를 사용해 옵티마이저의 실행 계획에 영향을 미치는 것은 피하는 것이 좋다. 최적의 실행 계획은 데이터의 성격에 따라 시시각각 변하므로 옵티마이저가 당시 통계 정보를 가지고 선택하게 하는 것이 가장 좋은 방법이며, 가장 훌륭한 최적화는 튜닝할 필요가 없게 데이터를 최소화하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;옵티마이저 힌트&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0에서는 사용 가능한 힌트 종류가 매우 다양하고 미치는 영향 범위도 매우 다양하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;옵티마이저 힌트의 종류&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 인덱스의 이름을 사용할 수 있는 옵티마이저 힌트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테이블
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 테이블의 이름을 사용할 수 있는 옵티마이저 힌트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;쿼리 블록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 쿼리 블록에 사용할 수 있는 옵티마이저 힌트&lt;/li&gt;
&lt;li&gt;힌트가 명시된 쿼리 블록에 대해서만 영향을 미침&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;글로벌(쿼리 전체)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 쿼리에 대해 영향을 미치는 힌트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MAX_EXECUTION_TIME&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저 힌트 중 유일하게 실행 계획에 영향을 미치지 않는 힌트로, 단순히 쿼리의 최대 실행 시간을 설정하는 힌트다. 지정된 시간을 초과하게 되면 쿼리는 실패하게 된다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT /*+ MAX_EXECUTION_TIME(100) */ * 
FROM employees
ORDER BY last_name LIMIT 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SET_VAR&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SET_VAR 힌트는 실행 계획을 바꾸는 용도뿐만 아니라 조인 버퍼나 정렬용 버퍼의 크기를 일시적으로 증가시켜 대용량 처리 쿼리의 성능을 향상시키는 용도로 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT /★+ SET_VAR(optimizer_switch='index_merge_intersection=off') */ ★ 
FROM employees
WHERE first_name='Georgi' AND emp_no BETWEEN 10000 AND 20000;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SEMIJOIN &amp;amp; NO_SEMIJOIN&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SEMIJOIN 힌트는 세미 조인 최적화의 어떤 세부 전략을 사용할지 제어하는데 사용할 수 있다. 세미 조인의 최적화 전략은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Duplicate Weed-out&lt;/li&gt;
&lt;li&gt;First Match&lt;/li&gt;
&lt;li&gt;Loose Scan&lt;/li&gt;
&lt;li&gt;Materialization&lt;/li&gt;
&lt;li&gt;Table Pull-out&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 SEMIJOIN 힌트의 경우 SEMIJOIN(최적화 전략)의 형태로 사용할 수 있다.&lt;br /&gt;ex) SEMIJOIN(FIRSTMATCH)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Table Pull-out의 경우 별도의 힌트를 사용할 수 없는데, Table Pull-out 전략을 사용하는 경우 항상 더 나은 성능을 보장하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세미 조인 힌트는 서브 쿼리에 명시하거나 서브 쿼리에 쿼리 블록 이름을 정의하고 외부 쿼리 블록에 명시해야 한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 서브쿼리에 세미조인 힌트 명시
EXPLAIN
SELECT *
FROM departments d 
WHERE d.dept.no IN
        (SELECT /*+ SEMIJOIN(MATERIALIZATION) */ de.dept_no 
         FROM dept_emp de);

// 쿼리 블록 이름 정의 및 외부 쿼리 블록에 세미조인 힌트 명시
EXPLAIN
SELECT /*+ SEMIJOIN(@subq1 MATERIALIZATION) */ *
FROM departments d
WHERE d.dept_no IN
    (SELECT /*+ QB_NAME(subq1) */ de.dept_no 
   FROM dept_emp de);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JOIN_FIXED_ORDER &amp;amp; JOIN_ORDER &amp;amp; JOIN_PREFIX &amp;amp; JOIN_SUFFIX&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버는 조인의 순서를 결정하기 위해 STRAIGHT_JOIN 힌트를 사용해왔다. 하지만 이는 쿼리 FROM 절에 사용된 테이블의 순서를 조인 순서에 맞게 변경해야 하는 번거로움이 있었다. 또한 일부 조인 순서를 강제하고 나머지는 옵티마이저에게 맞기는 것도 불가능했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단점을 보완하기 위해 옵티마이저 힌트에서는 다음과 같이 4개의 힌트를 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JOIN_FIXED_ORDER
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FROM 절의 테이블 순서대로 조인을 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JOIN_ORDER
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FROM 절에 사용된 테이블 순서가 아니라 힌트에 명시된 테이블 순서대로 조인 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JOIN_PREFIX
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인에서 드라이빙 테이블만 강제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JOIN_SUFFIX
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인에서 드리븐 테이블만 강제&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;// FROM 절에 나열된 테이블의 순서대로 조인 실행
SELECT /*+ JOIN_FIXED_ORDER() */ *
FROM employees e
    INNER JOIN dept_emp de ON de.emp_no = e.emp_no 
    INNER JOIN departments d ON d.dept_no = de.dept_no;

// 일부 테이블에 대해서만 조인 순서를 나열
SELECT /*+ JOIN_ORDER(d, de) */ *
FROM employees e
    INNER JOIN dept_emp de ON de.emp_no = e.emp_no 
    INNER JOIN departments d ON d.dept_no = de.dept_no;

// 조인의 드라이빙 테이블에 대해서만 조인 순서를 나열 
SELECT /*+ JOIN_PREFIX(e, de) */ *
FROM employees e
    INNER JOIN dept_emp de ON de.emp_no = e.emp_no
    INNER JOIN departments d ON d.dept_no = de.dept_no;

// 조인의 드리븐 테이블에 대해서만 조인 순서를 나열 
SELECT /*+ JOIN_SUFFIX(de, e) */ *
FROM employees e
    INNER JOIN dept_emp de ON de.emp_no = e.emp_no
    INNER JOIN departments d ON d.dept_no = de.dept_no;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MERGE &amp;amp; NO_MERGE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 MySQL 서버에서는 FROM 절에 사용된 서브 쿼리를 항상 내부 임시 테이블로 생성해 불필요한 자원을 소모했었다. 따라서 MySQL 5.7 이상 버전에서는 임시 테이블을 사용하지 않게 FROM 절의 서브 쿼리를 외부 쿼리와 병합하는 최적화를 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 임시 테이블을 생성하는 것이 나은 선택이 될 수도 있기 때문에 옵티마이저가 최적의 방법을 선택하지 못했을 때는 MERGE 또는 NO_MERGE 옵티마이저 힌트를 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;// 외부 쿼리와 병합
EXPLAIN
SELECT /*+ MERGE(sub)*/ *
FROM (SELECT *
            FROM employees
            WHERE first_name='Matt1') sub LIMIT 10;

// 임시 테이블 사용
EXPLAIN
SELECT /*+ N0_MERGE(sub)*/ *
FROM (SELECT * 
            FROM employees
            WHERE first_name='Matt') sub LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INDEX_MERGE &amp;amp; NO_INDEX_MERGE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버는 가능하면 테이블 당 하나의 인덱스만을 이용해 쿼리를 처리하려고 한다. 이때 인덱스를 통해 검색된 레코드의 교집합 또는 합집합만을 구해 결과를 반환하고 &lt;b&gt;하나의 테이블에 대해 여러 개의 인덱스를 동시에 사용하는 것을 인덱스 머지&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INDEX_MERGE와 NO_INDEX_MERGE는 인덱스 머지 실행 계획 사용 여부를 제어하고자 할 때 사용된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN 
SELECT * /*+ INDEX_MERGE(employees ix_firstname, PRIMARY) */ *
FROM employees
WHERE first_name='Georgi' AND emp_no BETWEEN 10000 AND 20000

EXPLAIN
SELECT /*+ NO_INDEX_MERGE(employees PRIMARY) */ *
FROM employees
WHERE first_name='Georgi' AND emp_no BETWEEN 10000 AND 20000;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NO_ICP&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 컨디션 푸시다운 최적화는 항상 성능 향상에 도움이 되므로 옵티마이저는 최대한 인덱스 컨디션 푸시다운 기능을 사용하는 방향으로 실행 계획을 수립한다. 따라서 MySQL 옵티마이저에서는 ICP(Index Condition Pushdown) 힌트는 제공하지 않고 인덱스 컨디션 푸시다운으로 인한 잘못된 실행 계획 수립 시 인덱스 컨디션 푸시다운 최적화를 비활성화하는 힌트인 NO_ICP만을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT /*+ NO_ICP(employees ix_lastname_firstname) */ * 
FROM employees
WHERE last_name='Acton' AND first_name LIKE '%sar';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SKIP_SCAN &amp;amp; NO_SKIP_SCAN&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 스킵 스캔은 인덱스의 선행 칼럼에 대한 조건이 없어도 옵티마이저가 해당 인덱스를 사용할 수 있게 해주는 최적화 기능이다. 하지만 조건이 누락된 선행 컬럼의 유니크 값의 개수가 많아진다면 오히려 성능이 떨어지므로 옵티마이저가 비효율적인 인덱스 스킵 스캔을 선택한 경우 NO_SKIP_SCAN 힌트로 이를 제어할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;// 인덱스 스킵 스캔 비활성화
EXPLAIN
SELECT /*+ NO_SKIP_SCAN(employees ix_gender_birthdate) */ gender, birth_date 
FROM employees
WHERE birth_date&amp;gt;='1965-02-01';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INDEX &amp;amp; NO_INDEX&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INDEX와 NO_INDEX 옵티마이저 힌트는 예전 MySQL 서버에서 사용되던 인덱스 힌트를 대체하는 용도로 사용된다. 대체된 인덱스 힌트는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;USE INDEX
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; INDEX&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;USE INDEX FOR GROUP BY
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; GROUP_INDEX&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;USE INDEX FOR ORDER BY
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; ORDER_INDEX&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;IGNORE INDEX
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; NO_INDEX&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;IGNORE INDEX FOR GROUP BY
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; NO_GROUP_INDEX&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;IGNORE INDEX FOR ORDER BY
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; NO_ORDER_INDEX&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;// 인덱스 힌트 사용
EXPLAIN
SELECT *
FROM employees USE INDEX(ix_firstname) 
WHERE first_name='Matt';

// 옵티마이저 힌트 사용
EXPLAIN
SELECT /*+ INDEX(employees ix_firstname) */ * 
FROM employees
WHERE first_name='Matt';&lt;/code&gt;&lt;/pre&gt;</description>
      <category>BackEnd/Real MySQL 8.0</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/173</guid>
      <comments>https://jjingho.tistory.com/173#entry173comment</comments>
      <pubDate>Fri, 9 Sep 2022 14:20:42 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 옵티마이저의 기본 데이터 처리 2 / 2</title>
      <link>https://jjingho.tistory.com/172</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;GROUP BY 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY도 ORDER BY처럼 스트리밍된 처리를 할 수 없는 작업 중 하나다.&lt;br /&gt;GROUP BY 절에는 그루핑 결과에 필터링 역할을 수행하는 HAVING 절을 사용할 수 있는데, GROUP BY에 사용된 조건은 인덱스를 사용해 처리될 수 없어 &lt;b&gt;HAVING 절을 튜닝하려고 인덱스를 생성하거나 다른 방법을 고민할 필요는 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY 작업은 인덱스를 사용하는 경우와 그렇지 못한 경우로 나눌 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 이용하는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 차례대로 읽는 타이트 인덱스 스캔&lt;/li&gt;
&lt;li&gt;인덱스를 건너뛰면서 읽는 루스 인덱스 스캔&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인덱스를 사용하지 못하는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임시 테이블 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 스캔을 이용하는 GROUP BY(타이트 인덱스 스캔)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;루스 인덱스 스캔을 이용하는 GROUP BY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루스 인덱스 스캔 방식은 인덱스의 레코드를 건너뛰면서 필요한 부분만 읽어 가져오는 것을 의미한다. 루스 인덱스 스캔을 사용할 때는 실행 계획의 Extra 컬럼에 &lt;b&gt;Using index for group-by&lt;/b&gt; 코멘트가 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 쿼리를 실행하면 루스 인덱스 스캔을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EXPLAIN
    SELECT emp_nᄋ
    FROM salaries
    WHERE from_date='1985-03-01' 
    GROUP BY emp_no;

+----+----------+-------+---------+---------------------------------------+
| id | table    | type  | key     | Extra                                 |
+----+----------+-------+---------+---------------------------------------+
|  1 | salaries | range | PRIMARY | Using where; Using index for group-by |
+----+----------+-------+---------+---------------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿼리 실행 순서&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(emp_no, from_date) 인덱스를 차례로 스캔하면서 emp_no의 첫 번째 유일한 값 10001을 찾아낸다.&lt;/li&gt;
&lt;li&gt;emp_no가 10001인 것 중에서 from_date가 &amp;lsquo;1985-03-01&amp;rsquo;인 레코드만 가져온다.&lt;/li&gt;
&lt;li&gt;emp_no의 그다음 유니크한 값을 가져온다.&lt;/li&gt;
&lt;li&gt;3번의 결과가 없으면 처리를 종료하고, 결과가 있다면 2번 과정을 반복 수행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루스 인덱스 스캔 방식은 단일 테이블의 GROUP BY 처리에만 사용할 수 있다. 또한 칼럼 값의 앞쪽 일부만 생성된 인덱스인 프리픽스 인덱스는 루스 인덱스 스캔을 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 인덱스 레인지 스캔과는 다르게 루스 인덱스 스캔에서는 유니크한 값의 수가 적을수록 성능이 향상된다는 것을 생각해두자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;루스 인덱스 스캔을 사용할 수 없는 쿼리 패턴&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// MIN()과 MAX() 이외의 집합 함수가 사용됐기 때문에 루스 인덱스 스캔은 사용 불가
SELECT col1, SUM(col2) FROM tb_test GROUP BY col1;

// GROUP BY에 사용된 칼럼이 인덱스 구성 칼럼의 왼쪽부터일치하지 않기 때문에 사용 불가
SELECT col1, col2 FROM tb_test GROUP BY col2, col3; 

// SELECT 절의 칼럼이 GROUP BY와 일치하지 않기 때문에 사용 불가
SELECT col1, col3 FROM tb_test GROUP BY col1, col2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;임시 테이블을 사용하는 GROUP BY&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY의 기준 컬럼이 드라이빙 테이블에 있든 드리븐 테이블에 있든 관계없이 인덱스를 전혀 사용하지 못할 때 임시 테이블을 사용하여 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0에서는 GROUP BY가 필요한 경우 내부적으로 GROUP BY 절의 컬럼들로 구성된 유니크 인덱스를 가진 임시 테이블을 만들어 중복 제거와 집합 함수 연산을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획의 Extra 컬럼에 Using temporary 코멘트가 표시되며, MySQL 8.0부터는 묵시적 정렬을 수행하지 않으므로 Using filesort 코멘트는 표시되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DISTINCT 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DISTINCT는 특정 컬럼의 유니크한 값만 조회하기 위해 사용한다. 이 작업은 집합 함수와 함께 사용하는 경우와 집합 함수가 없는 경우에 따라 영향을 미치는 범위가 달라지기 때문에 구분해 살펴봐야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;집합 함수가 없는 경우 ( SELECT DISTINCT &amp;hellip; )&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT DISTINCT 형태의 쿼리는 GROUP BY와 동일한 방식으로 처리된다.&lt;br /&gt;예를 들어 아래의 두 쿼리는 내부적으로 같은 작업을 수행한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT DISTINCT emp_no FROM salaries; 

SELECT emp_no FROM salaries GROUP BY emp_no;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DISTINCT는 특정 컬럼만 유니크하게 조회하는 것이 아니라 유니크한 레코드(튜플)를 조회한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT DISTINCT first_name, last_name FROM employees;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리에서는 first_name이 유니크한 값을 조회하는 것이 아니라 (first_name, last_name) 조합 전체가 유니크한 레코드를 가져오는 것이다. 쿼리만 봤을 때 쉽게 착각할 수 있는 부분이기 때문에 실수하지 않도록 주의하자.&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;집합 함수와 함께 사용된 DISTINCT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COUNT(), MIN(), MAX()와 같은 집합 함수 내에서 DISTINCT 키워드가 사용된 경우로, 앞서 살펴본 일반적인 DISTINCT와는 다른 형태로 해석된다. &lt;b&gt;집합 함수 내에서 사용된 DISTINCT는 인자로 전달된 칼럼 값이 유니크한 것들을 가져온다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT COUNT(DISTINCT s.salary)
FROM employees e, salaries s 
WHERE e.emp_no=s.emp_no
AND e.emp_no BETWEEN 100001 AND 100100;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리는 &lt;code&gt;COUNT(DISTINCT s.salary)&lt;/code&gt;를 처리하기 위해 임시 테이블을 사용한다. 테이블 조인 결과에서 드리븐 테이블의 컬럼 값(s.salary)만 저장해야하기 때문이다. 이때 salary 컬럼에 유니크 인덱스가 생성되므로 레코드 수가 많아진다면 상당이 느려질 수 있는 형태의 쿼리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집합 함수와 함께 사용된 DISTINCT를 최적화하려면 인덱스된 컬럼에 대해 DISTINCT 처리를 수행하도록 쿼리를 작성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT COUNT(DISTINCT emp_no) FROM employees;
SELECT COUNT(DISTINCT emp_no) FROM dept_emp GROUP BY dept_no;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 컬럼에 대해 DISTINCT 처리를 수행할 때 인덱스 풀 스캔이나 레인지 스캔하면서 임시 테이블 없이 최적화된 처리를 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내부 임시 테이블 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토리지 엔진으로부터 받아온 레코드를 정렬하거나 그루핑할 때 MySQL 서버는 &lt;b&gt;내부적인 임시 테이블&lt;/b&gt;을 사용한다. 내부적인 임시 테이블은 일반적으로 MySQL 엔진이 사용하는 임시 테이블과는 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적인 임시 테이블(사용자가 생성한 임시 테이블)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리에 생성됐다가 테이블의 크기가 커지면 디스크로 옮겨진다.&lt;/li&gt;
&lt;li&gt;특정 예외 케이스에서는 바로 디스크에 임시 테이블이 생성되기도 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;내부적인 임시 테이블
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 세션이나 다른 쿼리에서는 볼 수 없으며 사용하는 것이 불가능하다.&lt;/li&gt;
&lt;li&gt;쿼리의 처리가 완료되면 자동으로 삭제된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 8.0 이전 버전
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 임시 테이블
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MEMORY 스토리지 엔진 사용&lt;/li&gt;
&lt;li&gt;MEMORY 스토리지 엔진이 가변 길이 타입을 지원하지 못하므로 메모리 낭비가 심함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;디스크 임시 테이블
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MyISAM 스토리지 엔진 사용&lt;/li&gt;
&lt;li&gt;MyISAM 스토리지 엔진이 트랜잭션을 지원하지 못하므로 ACID를 보장하지 못함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MySQL 8.0
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 임시 테이블
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TempTable 스토리지 엔진 사용&lt;/li&gt;
&lt;li&gt;TempTable 스토리지 엔진의 가변 길이 타입을 지원으로 메모리 낭비 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;디스크 임시 테이블
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InnoDB 스토리지 엔진 사용&lt;/li&gt;
&lt;li&gt;트랜잭션 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음과 같은 쿼리 패턴은 별도의 데이터 가공 작업이 필요하므로 내부 임시 테이블을 생성한다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORDER BY와 GROUP BY에 명시된 컬럼이 다른 쿼리&lt;/li&gt;
&lt;li&gt;ORDER BY나 GROUP BY에 명시된 컬럼이 조인의 순서상 첫 번째 테이블이 아닌 쿼리&lt;/li&gt;
&lt;li&gt;DISTINCT와 ORDER BY가 동시에 존재하는 쿼리 또는 DISTINCT가 인덱스로 처리되지 못하는 쿼리&lt;/li&gt;
&lt;li&gt;UNION이나 UNION DISTINCT가 사용된 쿼리
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;select_type 칼럼이 UNION RESULT인 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;쿼리의 실행 계획에서 select_type이 DERIVED인 쿼리
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) FROM 절에서 사용된 서브 쿼리&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번 ~ 5번 쿼리 패턴의 경우 실행 계획에 &lt;b&gt;Using temporary&lt;/b&gt;가 표시되지 않지만 내부 임시 테이블을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 ~ 4번 쿼리 패턴의 경우 유니크 인덱스를 사용하는 내부 임시 테이블 생성하며 처리 성능이 상당히 느리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5번 쿼리 패턴의 경우 유니크 인덱스가 없는 내부 임시 테이블을 생성하며 1번 ~ 4번보다 처리 성능이 빠르다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획의 Extra를 통해 임시 테이블을 사용했다는 사실은 알 수 있지만, 임시 테이블이 메모리에서 처리됐는지 디스크에서 처리됐는지는 알 수 없으며, 몇 개의 임시 테이블이 사용됐는지도 알 수 없다. 이때 임시 테이블이 디스크에 생성됐는지 메모리에 생성됐는지 다음과 같은 &lt;b&gt;상태 변수&lt;/b&gt;를 통해 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;// 세션 상태 값 초기화
&amp;gt; FLUSH STATUS;

// 임시 테이블을 사용하는 쿼리
&amp;gt; SELECT ..... FROM ... GROUP BY ....

// 상태 변수 확인
&amp;gt; SHOW SESSION STATUS LIKE 'Created_tmp%';

+--------------------------+--------+
| Variable name            |  Value |
+--------------------------+--------+
| Created_tmp_disk_tables  |      1 |
| Created_tmp_tables       |      1 |
+--------------------------+--------+&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Created_tmp_disk_tables
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리의 처리를 위해 만들어진 내부 임시 테이블의 총 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Created_tmp_tables
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디스크에 내부 임시 테이블이 만들어진 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Created_tmp_disk_tables 개수 - Created_tmp_tables 개수
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리에 내부 임시 테이블이 만들어진 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>BackEnd/Real MySQL 8.0</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/172</guid>
      <comments>https://jjingho.tistory.com/172#entry172comment</comments>
      <pubDate>Wed, 27 Jul 2022 22:03:31 +0900</pubDate>
    </item>
    <item>
      <title>[Real MySQL 8.0] 옵티마이저의 기본 데이터 처리 1 / 2</title>
      <link>https://jjingho.tistory.com/171</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요청된 쿼리는 같은 결과를 반환하지만, 내부적으로 그 결과를 어떻게 만들어낼 것인지에 대한 방법은 매우 다양하다. 따라서 어떤 방법이 최적이고 최소의 비용이 소모되는지 결정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 테이블의 데이터가 어떤 분포로 저장돼 있는지 통계 정보를 참조해 최적의 &lt;b&gt;실행 계획&lt;/b&gt;을 수립한다. 대부분의 DBMS에서도 옵티마이저가 이러한 기능을 담당하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;쿼리 실행 절차&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리가 실행되는 과정은 크게 세 단계로 나눌 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청된 SQL 문장을 쪼개서 MySQL 서버가 이해할 수 있는 수준으로 분리(파스 트리)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL 파싱 단계로 SQL 파서 모듈로 처리&lt;/li&gt;
&lt;li&gt;SQL 문법 오류(Syntax Error)가 이 단계에서 걸러짐&lt;/li&gt;
&lt;li&gt;SQL 파스 트리 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SQL의 파싱 정보(파스 트리)를 확인해 어떤 테이블을 읽을지, 어떤 인덱스를 이용할지 결정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 조건 제거 및 복잡한 연산 단순화&lt;/li&gt;
&lt;li&gt;테이블 조인이 있는 경우 어떤 순서로 테이블을 읽을지 결정&lt;/li&gt;
&lt;li&gt;테이블에 사용된 조건과 인덱스 통계 정보를 이용해 사용할 인덱스 결정&lt;/li&gt;
&lt;li&gt;임시 테이블 사용 여부 결정&lt;/li&gt;
&lt;li&gt;최적화 및 실행 계획 수립 단계로 위 과정들이 완료되면 실행 계획이 수립됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결정된 테이블의 읽기 순서나 인덱스를 이용해 스토리지 엔진으로부터 데이터를 가져옴
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2번에서 만들어진 실행 계획대로 스토리지 엔진에 레코드를 읽어오도록 요청&lt;/li&gt;
&lt;li&gt;스토리지 엔진으로부터 받은 레코드를 조인하거나 정렬하는 작업 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;옵티마이저 종류&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비용 기반 최적화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리를 처리하기 위한 여러 방법을 만들고, 각 단위 작업의 비용과 예측된 통계 정보를 이용해 실행 계획별 비용을 산출&lt;/li&gt;
&lt;li&gt;최소 비용 처리 방법을 선택해 쿼리를 실행&lt;/li&gt;
&lt;li&gt;현재 대부분의 DBMS가 해당 최적화 방법을 사용 중이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;규칙 기반 최적화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대상 테이블의 레코드 건수나 선택도 등을 고려하지 않고 옵티마이저에 내장된 우선순위에 따라 실행 계획을 수립&lt;/li&gt;
&lt;li&gt;같은 쿼리에서는 항상 같은 실행 방법을 선택하는 단점이 존재한다.&lt;/li&gt;
&lt;li&gt;예전 초기 버전으로 최근에는 거의 사용되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;기본 데이터 처리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS는 데이터를 정렬하거나 그루핑하는 기본 데이터 가공 기능을 가지고 있다.&lt;br /&gt;MySQL 서버가 어떤 알고리즘을 이용해 이러한 기본 데이터 가공을 처리하는지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;풀 테이블 스캔과 풀 인덱스 스캔&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀 테이블 스캔은 말 그대로 테이블의 데이터를 처음부터 끝까지 읽어 처리하는 작업을 의미한다. MySQL 옵티마이저는 다음과 같은 조건일 때 주로 풀 테이블 스캔을 선택한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 레코드 건수가 너무 작은 경우(테이블 페이지 1개로 구성된 경우)&lt;/li&gt;
&lt;li&gt;WHERE 절이나 ON 절에 인덱스를 이용할 수 있는 적절한 조건이 없는 경우&lt;/li&gt;
&lt;li&gt;인덱스가 있더라도 조건 일치 레코드 건수가 너무 많은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 DBMS는 풀 테이블 스캔 시 한꺼번에 여러 개의 블록이나 페이지를 읽어오는 기능을 내장하고 있다. InnoDB 스토리지 엔진의 경우 &lt;b&gt;백그라운드 스레드에 의해 시작되는 리드 어헤드 작업&lt;/b&gt;에 의해 이러한 기능이 지원된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리드 어헤드(Read ahead)란&lt;/b&gt; &lt;b&gt;어떤 영역의 데이터가 앞으로 필요해질 것을 예측해서 미리 디스크로부터 읽어 InnoDB 버퍼 풀에 담아두는 것을 의미한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀 테이블 스캔이 일어나면 &lt;b&gt;포그라운드 스레드가 페이지 읽기를 실행&lt;/b&gt;하고 &lt;b&gt;특정 시점부터는 해당 읽기 작업을 백그라운드 스레드로 넘긴다&lt;/b&gt;. 백그라운드 스레드로 넘겨받는 시점부터 한 번에 최대 64개의 데이터 페이지까지 읽어 버퍼 풀에 저장해둔다. 이렇게 하면 포그라운드 스레드는 버퍼 풀에 미리 준비된 데이터를 가져다 사용하면 되므로 쿼리를 빠르게 처리할 수 있게 되는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;풀 인덱스 스캔은 &lt;a href=&quot;https://jjingho.tistory.com/168&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;인덱스 살펴보기 2/2&lt;/a&gt;를 참조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;병렬 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0부터는 용도가 한정돼 있긴 하지만 하나의 쿼리를 여러 스레드가 나누어 동시에 처리할 수 있는 병렬 처리가 가능해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;innodb_parallel_read_threads라는 시스템 변수를 이용해 &lt;b&gt;아무런 WHERE 조건 없이 단순히 테이블 전체 건수를 가져오는 쿼리만 병렬로 처리할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 4개의 스레드를 사용해 쿼리를 병렬 처리
SET SESSION innodb_parallel_read_threads=4;
SELECT COUNT(*) FROM salaries;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 처리용 스레드 수가 늘어날수록 쿼리 처리 속도가 빨라지는 걸 확인할 수 있지만, 서버에 장착된 CPU 코어 개수를 넘어서면 오히려 성능이 떨어질 수 있으니 주의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ORDER BY 처리(Using filesort)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬을 처리하는 방법은 &lt;b&gt;인덱스를 이용하는 방법&lt;/b&gt;과 &lt;b&gt;Filesort 처리 방법&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;인덱스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Insert, Update, Delete 쿼리가 실행될 때 이미 인덱스가 정렬되어 있으므로 읽을 때(Insert, Update, Delete 쿼리의 조건절을 검색할 때) 매우 빠르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부가적인 인덱스 추가/삭제 작업이 필요하므로 실제 Insert, Update, Delete 작업 시 느리다.&lt;/li&gt;
&lt;li&gt;인덱스 때문에 디스크 공간이 더 많이 필요하다.&lt;/li&gt;
&lt;li&gt;버퍼 풀을 위한 메모리가 많이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Filesort
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스가 필요 없으므로 인덱스의 단점이 장점으로 바뀐다.&lt;/li&gt;
&lt;li&gt;레코드가 적을 경우 충분히 빠르다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레코드 건수가 많아질수록 쿼리 응답 속도가 느려진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬 처리를 수행할 때 Filesort보다 인덱스를 이용하도록 튜닝하면 좋지만 모든 정렬에 인덱스를 이용하도록 튜닝하기란 불가능하다. 그 이유는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정렬 기준이 너무 많아 기준 별 인덱스를 생성할 수 없는 경우&lt;/li&gt;
&lt;li&gt;Group By의 결과 또는 DISTINCT 같은 처리 결과를 정렬해야 하는 경우&lt;/li&gt;
&lt;li&gt;임시 테이블의 결과를 재정렬하는 경우&lt;/li&gt;
&lt;li&gt;랜덤 하게 결과 레코드를 가져오는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버가 인덱스를 이용하지 않고 별도의 정렬 처리를 수행하게 되면 실행 계획의 Extra 컬럼에 Using filesort라는 메시지가 표시된다. 이를 통해 MySQL 서버가 어떤 정렬 처리를 수행했는지 알 수 있다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소트 버퍼란 MySQL이 정렬을 수행하기 위해 할당받은 별도의 메모리 공간이다.&lt;/b&gt;&lt;br /&gt;소트 버퍼의 공간은 한정적이므로 정렬해야 할 &lt;b&gt;레코드의 건수가 소트 버퍼 공간보다 큰 경우&lt;/b&gt;가 있다. 이런 경우 소트 버퍼에서 정렬을 수행하고 &lt;b&gt;디스크에 임시 저장&lt;/b&gt;하게 된다. 각 버퍼 크기만큼 정렬된 레코드를 다시 병합하면서 정렬을 수행하는데, 이를 &lt;b&gt;멀티 머지(Multi-merge)&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 작업은 모두 디스크 읽기/쓰기를 유발하며, 레코드가 많을수록 반복 작업 횟수도 많아진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DeZRe/btrH1CpjvLK/0LkIfuHj50gXOv5dXONsf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DeZRe/btrH1CpjvLK/0LkIfuHj50gXOv5dXONsf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DeZRe/btrH1CpjvLK/0LkIfuHj50gXOv5dXONsf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDeZRe%2FbtrH1CpjvLK%2F0LkIfuHj50gXOv5dXONsf1%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;723&quot; height=&quot;270&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소트 버퍼를 크게 잡아서 디스크 작업을 줄이고 메모리 작업을 늘려도 실제 성능에는 큰 차이가 나지 않는다. 오히려 너무 큰 메모리 공간 할당 때문에 성능이 떨어질 수도 있다. 하지만 디스크 I/O를 줄일 수 있으므로 성능이 낮은 장비에는 충분히 도움이 될 수 있다.&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;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글 패스 정렬 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정렬 키와 레코드 전체를 가져와 정렬하는 방식&lt;/li&gt;
&lt;li&gt;정렬 대상 레코드의 크기나 건수가 작은 경우 빠른 성능&lt;/li&gt;
&lt;li&gt;레코드 전체를 가져오므로 더 많은 소트 버퍼 공간이 필요하다.&lt;/li&gt;
&lt;li&gt;additional_field : 레코드의 컬럼들은 고정 사이즈로 메모리 저장&lt;/li&gt;
&lt;li&gt;pack_additional_field : 레코드의 컬럼들은 가변 사이즈로 메모리 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHovcH/btrHZVRgUUY/sq3lLEQyJemhpkC0Mi2o90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHovcH/btrHZVRgUUY/sq3lLEQyJemhpkC0Mi2o90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHovcH/btrHZVRgUUY/sq3lLEQyJemhpkC0Mi2o90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHovcH%2FbtrHZVRgUUY%2Fsq3lLEQyJemhpkC0Mi2o90%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;697&quot; height=&quot;359&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;투 패스 정렬 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정렬 키와 RowID만 가져와 정렬하는 방식&lt;/li&gt;
&lt;li&gt;정렬 대상 컬럼, 프라이머리 키 값만 소트 버퍼에 담아 정렬하고 정렬된 프라이머리 키로 테이블을 다시 조회해 컬럼을 가져온다.&lt;/li&gt;
&lt;li&gt;테이블을 두 번 조회하므로 비효율적이다.&lt;/li&gt;
&lt;li&gt;다만, 정렬 대상 레코드의 크기나 건수가 많은 경우 효율적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 사용한 정렬&lt;/li&gt;
&lt;li&gt;조인에서 &lt;b&gt;드라이빙 테이블만 정렬&lt;/b&gt; - Using filesort&lt;/li&gt;
&lt;li&gt;조인에서 조인 결과를 &lt;b&gt;임시 테이블로 저장 후 정렬&lt;/b&gt; - Using temporary; Using filesort&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번을 기준으로 밑으로 갈수록 정렬 처리 속도는 느려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 정렬 대상 레코드를 최소화하기 위해 다음 2가지 방법 중 하나를 선택한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인의 드라이빙 테이블만 정렬한 다음 조인을 수행&lt;/li&gt;
&lt;li&gt;조인이 끝나고 일치하는 레코드를 모두 가져온 후 정렬을 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하지만 드라이빙 테이블만 정렬한 다음 조인을 수행하는 게 가장 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인덱스를 이용한 정렬&lt;/b&gt; (스트리밍 처리)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반드시 ORDER BY에 명시된 컬럼이 제일 먼저 읽는 테이블(조인의 경우 드라이빙 테이블)에 속해야 한다.&lt;/li&gt;
&lt;li&gt;ORDER BY의 순서대로 생성된 인덱스가 있어야 한다.&lt;/li&gt;
&lt;li&gt;WHERE절에 첫 번째로 읽는 테이블의 컬럼에 대한 조건이 있다면 ORDER BY와 같은 인덱스를 사용할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;해시 인덱스나 전문 검색 인덱스, R-Tree 등에서는 인덱스를 이용한 정렬을 사용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 쿼리는 드라이빙 테이블의 PK를 기준으로 ORDER BY를 수행하므로 인덱스를 이용한 정렬을 사용하게 된다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employees e, salaries s 
WHERE s.emp_no=e.emp_no
    AND e.emp_no BETWEEN 100002 AND 100020 
ORDER BY e.emp_no;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 이미 정렬돼 있기 때문에 순서대로 읽기만 하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vcrMh/btrH2nyvk4v/08C0poqdFJnJST10aO5sG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vcrMh/btrH2nyvk4v/08C0poqdFJnJST10aO5sG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vcrMh/btrH2nyvk4v/08C0poqdFJnJST10aO5sG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvcrMh%2FbtrH2nyvk4v%2F08C0poqdFJnJST10aO5sG0%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;787&quot; height=&quot;334&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;528&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;ol style=&quot;list-style-type: disc;&quot; start=&quot;2&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;조인의 드라이빙 테이블만 정렬&lt;/b&gt; (버퍼링 처리)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조인을 실행하기 전 첫 번째로 읽히는 테이블(드라이빙 테이블)의 레코드를 먼저 정렬한 다음 조인을 실행한다.&lt;/li&gt;
&lt;li&gt;드라이빙 테이블의 컬럼만으로 ORDER BY 절을 작성한 경우 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 쿼리에서 ORDER BY에 명시된 필드는 드라이빙 테이블의 PK와 아무 연관이 없으므로 인덱스를 이용한 정렬이 불가능하다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employees e, salaries s
WHERE s.emp_no=e.emp_no
    AND e . emp_no BETWEEN 100002 AND 100010
ORDER BY e.last_name;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 ORDER BY에 명시된 필드는 드라이빙 테이블에 속하므로 옵티마이저는 드라이빙 테이블을 먼저 검색해 정렬을 수행한 후 salaries와의 조인 작업을 실행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGprhJ/btrH3ccBYJH/Ezr19Fo0tH6NhNRPTsBxM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGprhJ/btrH3ccBYJH/Ezr19Fo0tH6NhNRPTsBxM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGprhJ/btrH3ccBYJH/Ezr19Fo0tH6NhNRPTsBxM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGprhJ%2FbtrH3ccBYJH%2FEzr19Fo0tH6NhNRPTsBxM0%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;587&quot; height=&quot;366&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;712&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;size16&quot;&gt;&lt;b&gt;3. 임시 테이블을 이용한 정렬&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;위의 경우를 뺀 나머지 패턴의 쿼리에서는 항상 조인 결과를 임시 테이블에 저장하고, 다시 정렬하는 과정을 거친다.&lt;/li&gt;
&lt;li&gt;실행 계획의 Extra에 Using temporary; Using filesort로 표시되며 앞선 정렬 방법 중 가장 느리다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 쿼리의 정렬 기준(ORDER BY)은 드리븐 테이블의 컬럼이므로 조인된 데이터를 가지고 정렬할 수밖에 없다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employees e, salaries s 
WHERE s.emp_no=e.emp_no
    AND e.emp.no BETWEEN 100002 AND 100010 
ORDER BY s.salary;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5nwRW/btrH1bevi8S/aNMp2dUgzGGo9hqUDkhKM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5nwRW/btrH1bevi8S/aNMp2dUgzGGo9hqUDkhKM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5nwRW/btrH1bevi8S/aNMp2dUgzGGo9hqUDkhKM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5nwRW%2FbtrH1bevi8S%2FaNMp2dUgzGGo9hqUDkhKM0%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;642&quot; height=&quot;625&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;980&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯 어떤 테이블이 먼저 드라이빙되어 조인되는지도 중요하지만, 어떤 정렬 방식으로 처리되느냐에 따라 더 큰 성능 차이를 만들어낸다. 가&lt;b&gt;능하다면 인덱스를 사용한 정렬로 유도하고 그렇지 못하면 최소한 드라이빙 테이블만 정렬해도 되는 수준으로 유도하는 것이 좋은 쿼리 튜닝 방법&lt;/b&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORDER BY나 GROUP BY 같은 작업은 WHERE 조건을 만족하는 레코드를 모두 가져와서 정렬을 수행하거나 그루핑 작업을 실행해야만 LIMIT로 건수를 제한할 수 있다. 즉, &lt;b&gt;잘못된 ORDER BY나 GROUP BY 작업은&lt;/b&gt; MySQL 서버가 처리해야 할 작업량을 줄이지 못하고 &lt;b&gt;슬로우 쿼리를 자주 발생시킨다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용하지 못하는 정렬이나 그루핑 작업이 왜 느리게 작동하는지 이해하려면 쿼리가 처리되는 방법을 이해해야 한다. 쿼리가 처리되는 방법에는 &lt;b&gt;스트리밍 처리&lt;/b&gt;와 &lt;b&gt;버퍼링 처리&lt;/b&gt;, 2가지 방식으로 구분할 수 있다.&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;b&gt;스트리밍 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 쪽에서 처리할 데이터가 얼마인지에 관계없이 조건에 일치하는 레코드가 검색될 때마다 바로 클라이언트로 전송해주는 방식을 의미한다. 따라서 쿼리가 얼마나 많은 레코드를 조회하냐에 상관없이 &lt;b&gt;빠른 응답 시간을 보장&lt;/b&gt;해준다. 이때, LIMIT 조건을 활용하면 가져오는 레코드 건수가 줄어들어 마지막 레코드를 가져오기까지의 시간을 상당히 줄일 수 있다.&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;b&gt;버퍼링 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORDER BY나 GROUP BY 같은 작업은 쿼리 결과가 스트리밍 되는 것을 불가능하게 만든다. WHERE 조건에 일치하는 레코드를 모두 가져온 후, 정렬하거나 그루핑해야 하기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0bfNh/btrH0SzqWmR/lJFKDIAzrqZEw0HpXau8x0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0bfNh/btrH0SzqWmR/lJFKDIAzrqZEw0HpXau8x0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0bfNh/btrH0SzqWmR/lJFKDIAzrqZEw0HpXau8x0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0bfNh%2FbtrH0SzqWmR%2FlJFKDIAzrqZEw0HpXau8x0%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;501&quot; height=&quot;225&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버퍼링 방식으로 처리되는 쿼리는 먼저 결과를 모은다.&lt;/li&gt;
&lt;li&gt;MySQL 서버에서 일괄 가공해야 하므로 모든 결과를 스토리지 엔진으로부터 가져올 때까지 기다린다.&lt;/li&gt;
&lt;li&gt;정렬 작업을 하는 동안 클라이언트는 결과를 기다려야 하므로 응답 속도가 느려진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 방식 때문에 LIMIT로 결과를 제한하더라도 성능 향상에 별로 도움이 되지 않는다.&lt;/p&gt;</description>
      <category>BackEnd/Real MySQL 8.0</category>
      <author>짱호</author>
      <guid isPermaLink="true">https://jjingho.tistory.com/171</guid>
      <comments>https://jjingho.tistory.com/171#entry171comment</comments>
      <pubDate>Sat, 23 Jul 2022 15:29:40 +0900</pubDate>
    </item>
  </channel>
</rss>