<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Index on jyukki's Blog</title><link>https://jyukki.com/tags/index/</link><description>Recent content in Index on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Wed, 17 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/index/index.xml" rel="self" type="application/rss+xml"/><item><title>블로그 아카이브</title><link>https://jyukki.com/posts/</link><pubDate>Wed, 11 Mar 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/posts/</guid><description>개발 트렌드 분석, 프로젝트 회고, 기술 뉴스를 AI 운영, 보안, 백엔드 설계 관점으로 빠르게 탐색할 수 있는 아카이브 페이지</description><content:encoded><![CDATA[<p>이 페이지는 블로그의 전체 글을 <strong>학습, 트렌드, 프로젝트</strong> 관점으로 빠르게 훑기 위한 허브입니다. 글 수가 늘어나면 최신 글만 따라가서는 맥락이 끊기기 쉬워서, 어떤 독자가 어떤 순서로 읽으면 좋은지 한눈에 잡을 수 있게 구조를 정리해 두는 편이 더 낫다고 생각했습니다.</p>
<p>특히 이 블로그는 단순 뉴스 요약보다, <strong>실무 의사결정에 바로 연결되는 기준</strong>을 남기는 데 초점을 두고 있습니다. 그래서 같은 주제라도 “개념 설명”, “운영 관점 해석”, “프로젝트 구현 경험”이 섞여 있습니다. 아카이브를 볼 때는 최신순보다도, 지금 내게 필요한 읽기 목적이 무엇인지 먼저 정하고 들어오는 편이 효율적입니다.</p>
<h2 id="이번-주-먼저-읽으면-좋은-흐름">이번 주 먼저 읽으면 좋은 흐름</h2>
<p>이번 주에는 에이전트 운영 거버넌스 흐름 위에 <strong>로컬 실행·샌드박스·브라우저 권한·보안 판정·플랫폼 의존성</strong>을 붙여 읽는 편이 가장 효율적입니다. 최근 글이 계속 쌓이면서 “AI 코딩 도구를 제품 기능으로 넣어도 되는가”, “로컬 모델과 브라우저 워커는 어디까지 권한을 가져야 하는가”, “AI가 만든 보안 신호와 코드 변경을 어떤 증거로 검증할 것인가”, “외부 플랫폼이 멈췄을 때 기능을 어떻게 낮출 것인가”가 같은 운영 문제로 이어지고 있습니다.</p>
<p>처음 들어온 독자라면 최신순으로 한 편씩 훑기보다, 아래처럼 <strong>제품화된 코딩 에이전트 → 로컬/브라우저 실행 경계 → 생성 코드 검증 → 권한과 보안 판정 → 실행 증거와 예외 관리</strong>로 묶어 보는 쪽이 더 빠릅니다. 6월 글들은 이 블로그의 최근 문제의식을 압축해서 보여주는 진입점이고, 5월 글들은 그 문제를 운영 체계로 고정하는 배경 글입니다.</p>
<ol>
<li><a href="/posts/2026-06-03-dev-news-senior-insights/">/posts/2026-06-03-dev-news-senior-insights/</a> : Copilot SDK, 샌드박스, 모델 종료, 개발자 토큰 탈취 사례를 통해 코딩 에이전트가 제품 API와 운영 경계 안으로 들어오는 흐름을 잡습니다.</li>
<li><a href="/posts/2026-06-04-dev-news-senior-insights/">/posts/2026-06-04-dev-news-senior-insights/</a> : 로컬 멀티모달 모델, Claude 격리 설계, post-quantum TLS 전환을 함께 보며 “더 강한 자동화일수록 더 분명한 경계가 필요하다”는 기준을 확인합니다.</li>
<li><a href="/posts/2026-06-06-dev-news-senior-insights/">/posts/2026-06-06-dev-news-senior-insights/</a> : AI 코드 품질, 오픈소스 PR 신뢰 모델, pg_durable, 로컬 AI 운영을 묶어 생성 속도보다 검증과 소유권이 중요해지는 이유를 봅니다.</li>
<li><a href="/posts/2026-06-07-dev-news-senior-insights/">/posts/2026-06-07-dev-news-senior-insights/</a> : 에이전트 하네스, AI 챗봇 보안, SQLite 키 설계, 브라우저 로컬 실행, 플랫폼 의존성을 하나의 시스템 경계 문제로 정리합니다.</li>
<li><a href="/posts/2026-05-28-agent-workbench-operating-console-trend/">/posts/2026-05-28-agent-workbench-operating-console-trend/</a> : 코딩 에이전트를 채팅창이 아니라 세션, 승인, 증거, 결과 큐가 있는 운영 콘솔로 다루는 관점을 붙입니다.</li>
<li><a href="/posts/2026-05-25-agentic-pr-governance-trend/">/posts/2026-05-25-agentic-pr-governance-trend/</a> : AI 생성 PR을 merge 속도 문제가 아니라 owner, evidence, blast radius, 회귀 테스트 문제로 보는 기준을 세웁니다.</li>
<li><a href="/posts/2026-05-24-mcp-native-secret-scanning-shift-left-trend/">/posts/2026-05-24-mcp-native-secret-scanning-shift-left-trend/</a> : 에이전트와 MCP 도구가 비밀값을 읽고 쓰는 순간을 shift-left 보안 검사로 다루는 방법을 연결합니다.</li>
<li><a href="/posts/2026-05-20-policy-exception-ledger-agent-governance-trend/">/posts/2026-05-20-policy-exception-ledger-agent-governance-trend/</a> : 정책 예외를 임시 구두 승인으로 두지 않고 owner, scope, expiry, evidence가 있는 ledger로 관리하는 흐름까지 이어 봅니다.</li>
</ol>
<p>이 8편을 함께 읽으면 “좋은 AI 도구를 고르는 방법”보다 더 중요한 질문이 보입니다. 에이전트가 어떤 파일과 네트워크를 만질 수 있는지, 브라우저나 로컬 모델이 어떤 사용자 데이터를 처리하는지, 생성된 diff를 누가 소유하고 어떤 테스트 증거로 받아들일지, 플랫폼 장애나 모델 변경이 생겼을 때 어디서 기능을 낮출지 같은 질문입니다. 자동화의 품질은 모델 이름이 아니라 <strong>권한 경계, 검증 증거, 복구 경로, 책임자</strong>가 얼마나 명확한지에서 갈립니다.</p>
<p>운영 관점으로 바로 적용하려면 읽으면서 아래 네 가지를 체크하면 좋습니다.</p>
<ul>
<li>새 자동화가 외부 전송, 계정 변경, 결제, 배포, 권한 상승 같은 action을 호출하는가?</li>
<li>실행 전후에 owner, 허용 명령, 네트워크 목적지, 테스트 결과, rollback 방법이 남는가?</li>
<li>모델 교체, 패키지 업데이트, 브라우저 API 변경, 플랫폼 장애가 발생했을 때 degradation 기준이 있는가?</li>
<li>사람이 마지막에 판단할 수 있도록 로그와 산출물이 채팅이 아니라 재조회 가능한 위치에 남는가?</li>
</ul>
<p>조금 더 기초부터 이어 읽고 싶다면 아래 순서도 좋습니다.</p>
<ol>
<li><a href="/posts/2026-05-07-dependency-update-pipeline-trend/">/posts/2026-05-07-dependency-update-pipeline-trend/</a> : 의존성 업데이트를 보안 패치 SLO와 변경 위험으로 나눠 운영하는 기준을 먼저 잡습니다.</li>
<li><a href="/posts/2026-05-12-package-release-quarantine-gate-trend/">/posts/2026-05-12-package-release-quarantine-gate-trend/</a> : 새 패키지 버전을 바로 소비하지 않고 provenance, tarball diff, lifecycle script를 격리 창에서 확인하는 실무 기준을 붙입니다.</li>
<li><a href="/posts/2026-05-16-agent-sandbox-egress-policy-trend/">/posts/2026-05-16-agent-sandbox-egress-policy-trend/</a> : 에이전트가 패키지와 문서를 가져올 때 어떤 네트워크 출구를 열어야 하는지, egress policy 관점에서 경계를 잡습니다.</li>
<li><a href="/posts/2026-05-13-ai-vulnerability-triage-pipeline-trend/">/posts/2026-05-13-ai-vulnerability-triage-pipeline-trend/</a> : AI가 만든 취약점 후보를 confirmed, duplicate, hardening, false positive로 나누는 판정 큐를 연결합니다.</li>
<li><a href="/posts/2026-04-14-execution-receipt-agent-operations-trend/">/posts/2026-04-14-execution-receipt-agent-operations-trend/</a> : 업데이트나 에이전트 실행 결과를 나중에 추적 가능한 receipt로 남기는 구조를 연결합니다.</li>
<li><a href="/posts/2026-05-08-agentic-provisioning-contract-trend/">/posts/2026-05-08-agentic-provisioning-contract-trend/</a> : 에이전트가 계정, 토큰, 비용, 배포 자원을 만질 때 필요한 계약과 revoke 경로를 확인합니다.</li>
<li><a href="/posts/2026-05-18-managed-browser-worker-trend/">/posts/2026-05-18-managed-browser-worker-trend/</a> : 에이전트가 로그인된 웹 UI와 SaaS 콘솔을 다룰 때 브라우저 세션, 승인 action, 스크린샷 증거를 어떻게 운영 자원으로 묶을지 정리합니다.</li>
<li><a href="/posts/2026-05-19-agent-artifact-registry-trend/">/posts/2026-05-19-agent-artifact-registry-trend/</a> : diff, 테스트 로그, 스크린샷, decision note를 채팅에 흘리지 않고 검증 가능한 산출물로 보존하는 기준을 붙입니다.</li>
<li><a href="/posts/2026-05-20-policy-exception-ledger-agent-governance-trend/">/posts/2026-05-20-policy-exception-ledger-agent-governance-trend/</a> : 정책 예외를 임시 구두 승인으로 두지 않고 owner, scope, expiry, evidence가 있는 ledger로 관리하는 흐름까지 이어 봅니다.</li>
</ol>
<p>이 9편은 따로 보면 각각 의존성 관리, 공급망 보안, egress 통제, 취약점 triage, 실행 증거, 프로비저닝 계약, 브라우저 런타임, 산출물 보존, 예외 승인처럼 보입니다. 하지만 실제 운영에서는 모두 <strong>자동화가 외부 권한 또는 보안 판단을 만나는 순간의 안전장치</strong>입니다. dependency update bot이 lockfile을 열고, CI가 패키지를 설치하고, AI 스캐너가 취약점 후보를 만들고, 에이전트가 도구를 호출하고, release workflow가 registry나 cloud 권한을 쓰고, 브라우저 워커가 SaaS 콘솔의 버튼을 마주하는 과정은 서로 다른 시스템처럼 보여도 같은 trust boundary 위에 있습니다.</p>
<p>조금 더 깊게 읽고 싶다면 아래 3편을 이어 붙이면 좋습니다.</p>
<ul>
<li><a href="/posts/2026-04-30-tool-contract-test-agent-runtime-trend/">/posts/2026-04-30-tool-contract-test-agent-runtime-trend/</a> : 도구 호출이 늘어날 때 schema, fixture, 회귀 테스트를 왜 먼저 고정해야 하는지 설명합니다.</li>
<li><a href="/posts/2026-04-22-third-party-oauth-supply-chain-trend/">/posts/2026-04-22-third-party-oauth-supply-chain-trend/</a> : 토큰과 OAuth 앱이 코드 변경보다 더 오래 남는 공급망 위험이 되는 이유를 정리합니다.</li>
<li><a href="/posts/2026-03-08-ai-code-provenance-and-sbom-trend/">/posts/2026-03-08-ai-code-provenance-and-sbom-trend/</a> : 패키지와 코드 출처를 증거로 남기는 기본기를 복습하기 좋습니다.</li>
</ul>
<p>따라서 이번 주의 추천 동선은 “좋은 자동화 만들기”가 아니라 <strong>자동화가 무엇을 설치하고, 어떤 네트워크 출구로 움직였고, 어떤 권한으로 실행됐고, 어떤 보안 신호를 냈고, 실패했을 때 어디서 멈출 수 있는지, 그리고 웹 UI를 직접 만질 때 어떤 증거를 남기는지</strong>를 확인하는 흐름입니다. 처음 방문한 독자라면 위 7편만 먼저 읽어도 최근 블로그의 문제의식이 꽤 선명해지고, 이미 자주 읽던 독자라면 공급망·egress 통제·취약점 triage·에이전트 운영·권한 통제·브라우저 자동화를 한 묶음으로 다시 정리할 수 있습니다.</p>
<h2 id="이-아카이브를-가장-잘-쓰는-방법">이 아카이브를 가장 잘 쓰는 방법</h2>
<h3 id="1-요즘-흐름을-빠르게-파악하고-싶을-때">1) 요즘 흐름을 빠르게 파악하고 싶을 때</h3>
<p>최근 개발 트렌드 글부터 2, 3편 읽는 방식이 가장 빠릅니다. 단순히 새 기술 이름을 외우기보다,</p>
<ul>
<li>왜 갑자기 팀들이 그 주제를 중요하게 보는지</li>
<li>비용, 품질, 운영 리스크가 어디서 바뀌는지</li>
<li>당장 체크해야 할 지표나 체크리스트가 무엇인지</li>
</ul>
<p>를 같이 보는 데 초점을 맞추면 좋습니다.</p>
<p>바로 들어가기 좋은 글:</p>
<ul>
<li><a href="/posts/2026-04-19-policy-shadow-rollout-agent-runtime-trend/">/posts/2026-04-19-policy-shadow-rollout-agent-runtime-trend/</a></li>
<li><a href="/posts/2026-04-16-context-contract-registry-agent-input-governance-trend/">/posts/2026-04-16-context-contract-registry-agent-input-governance-trend/</a></li>
<li><a href="/posts/2026-04-10-test-evidence-pipeline-ai-change-review-trend/">/posts/2026-04-10-test-evidence-pipeline-ai-change-review-trend/</a></li>
<li><a href="/posts/2026-04-09-harness-engineering-agent-runtime-frame-trend/">/posts/2026-04-09-harness-engineering-agent-runtime-frame-trend/</a></li>
</ul>
<h3 id="2-개념을-체계적으로-다시-잡고-싶을-때">2) 개념을 체계적으로 다시 잡고 싶을 때</h3>
<p>학습용 글은 용어 설명에서 끝나지 않고, 실제 시스템 설계나 장애 대응에 연결되는 예시를 같이 넣는 편입니다. 그래서 익숙한 주제라도 “왜 이 개념이 운영에서 중요해지는지”를 다시 정리할 때 읽기 좋습니다.</p>
<p>추천 진입 순서:</p>
<ol>
<li>관심 분야 키워드로 검색한다.</li>
<li>짧은 트렌드 글로 문제의식을 먼저 잡는다.</li>
<li>관련 심화 학습 글로 개념을 보강한다.</li>
<li>프로젝트 글에서 구현 흔적과 trade-off를 확인한다.</li>
</ol>
<p>이 순서로 보면 읽은 내용이 머릿속에 더 오래 남습니다.</p>
<h3 id="3-프로젝트-맥락까지-보고-싶을-때">3) 프로젝트 맥락까지 보고 싶을 때</h3>
<p>프로젝트 글은 결과만 나열하기보다, 중간에 부딪힌 문제와 설계가 바뀐 이유를 같이 남겨 두는 쪽을 선호합니다. 그래서 완성된 정답보다는 <strong>생각이 바뀌는 과정</strong>을 보고 싶은 분에게 더 잘 맞습니다.</p>
<p>대표 시리즈:</p>
<ul>
<li><a href="/projects/pgmux/">/projects/pgmux/</a></li>
<li><a href="/posts/sqs-01-architecture/">/posts/sqs-01-architecture/</a></li>
<li><a href="/posts/sqs-02-admin-dashboard/">/posts/sqs-02-admin-dashboard/</a></li>
<li><a href="/posts/sqs-03-storage-architecture/">/posts/sqs-03-storage-architecture/</a></li>
</ul>
<h2 id="추천-읽기-흐름">추천 읽기 흐름</h2>
<h3 id="흐름-a-ai-개발-생산성-거버넌스-리뷰-체계">흐름 A. AI 개발 생산성, 거버넌스, 리뷰 체계</h3>
<ol>
<li>Harness Engineering으로 실행 프레임 관점을 잡고</li>
<li>Tool Permission Manifest / Runtime Validator 계열 글로 통제 구조를 보고</li>
<li>Test Evidence Pipeline 글로 리뷰 단계에서 어떤 증거가 필요한지 연결해서 읽으면 좋습니다.</li>
</ol>
<p>이 흐름은 “AI가 코드를 더 빨리 쓴다”를 넘어서, <strong>팀이 어떻게 안전하게 더 많이 처리할 것인가</strong>를 고민할 때 특히 유용합니다.</p>
<h3 id="흐름-b-백엔드-학습에서-실무-설계로-넘어가기">흐름 B. 백엔드 학습에서 실무 설계로 넘어가기</h3>
<ol>
<li>학습 글에서 기초 개념을 다시 잡고</li>
<li>트렌드 글에서 현업 우선순위를 확인한 뒤</li>
<li>프로젝트 글에서 구현 선택과 한계를 비교해 보세요.</li>
</ol>
<p>이렇게 보면 개념이 추상적으로만 남지 않고, 실제 설계 기준으로 연결됩니다.</p>
<h3 id="흐름-c-시리즈형-프로젝트-따라가기">흐름 C. 시리즈형 프로젝트 따라가기</h3>
<p>프로젝트 글은 앞뒤 문맥이 이어지는 경우가 많아서, 검색으로 한 편만 읽기보다 관련 글을 연속해서 보는 편이 훨씬 낫습니다. 특히 PGMUX, Simple Queue Service 같은 시리즈는 문제 발견 → 설계 수정 → 운영 관점 재정리 순서로 보면 흐름이 잘 보입니다.</p>
<h3 id="흐름-d-에이전트-운영-거버넌스-흐름으로-읽기">흐름 D. 에이전트 운영 거버넌스 흐름으로 읽기</h3>
<p>최근 AI 운영 글은 서로 따로 읽어도 되지만, 아래 순서로 보면 입력, 실행, 전달, 검증 통제가 한 흐름으로 이어집니다.</p>
<ol>
<li><a href="/posts/2026-04-16-context-contract-registry-agent-input-governance-trend/">/posts/2026-04-16-context-contract-registry-agent-input-governance-trend/</a> 에서 입력 계약과 컨텍스트 소유권을 먼저 잡고</li>
<li><a href="/posts/2026-04-05-tool-permission-manifest-runtime-attestation-trend/">/posts/2026-04-05-tool-permission-manifest-runtime-attestation-trend/</a> 으로 실행 권한 경계를 확인한 뒤</li>
<li><a href="/posts/2026-04-14-execution-receipt-agent-operations-trend/">/posts/2026-04-14-execution-receipt-agent-operations-trend/</a> 에서 실제 실행 증거와 추적 구조를 연결하고</li>
<li><a href="/posts/2026-04-17-agent-handoff-packet-runtime-trend/">/posts/2026-04-17-agent-handoff-packet-runtime-trend/</a> 으로 멀티에이전트 handoff를 작업 패킷 관점으로 마무리하고</li>
<li><a href="/posts/2026-04-19-policy-shadow-rollout-agent-runtime-trend/">/posts/2026-04-19-policy-shadow-rollout-agent-runtime-trend/</a> 으로 새 정책을 바로 enforce하지 않고 shadow rollout으로 올리는 기준까지 이어서 보면 좋습니다.</li>
</ol>
<p>이 순서는 &ldquo;에이전트를 어떻게 더 똑똑하게 만들까&quot;보다, <strong>팀이 어떻게 더 안전하게 운영 품질을 유지할까</strong>에 초점을 맞출 때 특히 유용합니다. 특히 마지막 글까지 읽으면 입력 계약과 실행 증거가 왜 결국 정책 배포 기준으로 이어지는지 한 번에 이해하기 쉬워집니다.</p>
<p>추가로 바로 이어 읽고 싶다면 아래 두 편도 잘 붙습니다.</p>
<ul>
<li><a href="/posts/2026-04-12-action-lineage-agent-observability-graph-trend/">/posts/2026-04-12-action-lineage-agent-observability-graph-trend/</a> : handoff 전후에 어떤 실행 흔적을 추적해야 하는지 볼 때 좋습니다.</li>
<li><a href="/posts/2026-04-11-stateful-sandbox-snapshot-environment-replay-trend/">/posts/2026-04-11-stateful-sandbox-snapshot-environment-replay-trend/</a> : packet에 snapshot 참조를 왜 같이 묶어야 하는지 이해할 때 연결감이 좋습니다.</li>
</ul>
<h2 id="검색과-태그를-사용할-때-팁">검색과 태그를 사용할 때 팁</h2>
<ul>
<li>특정 기술 이름이 분명하면 검색창으로 바로 찾는 편이 가장 빠릅니다.</li>
<li>주제가 넓다면 태그로 먼저 범위를 줄이고, 그다음 제목과 설명을 보는 편이 좋습니다.</li>
<li>비슷한 글이 여러 편일 때는 날짜보다 description과 key takeaway를 먼저 확인하면 중복 읽기를 줄일 수 있습니다.</li>
</ul>
<h3 id="qa검증-글을-찾을-때">QA/검증 글을 찾을 때</h3>
<p>프로젝트 글에는 &ldquo;QA&quot;라는 단어가 제목과 본문에 자주 나오지만, 태그에서는 더 넓은 의미의 <strong>Quality Assurance</strong>로 묶어두는 편이 탐색에 유리합니다. QA는 버그를 찾는 단계만 뜻하지 않고, 릴리스 전에 어떤 실패 모드를 먼저 의심할지, 수정 뒤 어떤 회귀를 막을지, 운영자가 어떤 증거를 보고 배포를 승인할지까지 포함하기 때문입니다.</p>
<p>PGMUX 시리즈를 볼 때는 Quality Assurance 태그가 붙은 글을 단순 버그 수정 목록으로 읽기보다, 다음 질문을 들고 읽으면 더 얻는 게 많습니다.</p>
<ul>
<li>정상 경로에서는 통과하지만 특정 조합에서 깨지는 조건은 무엇이었나?</li>
<li>테스트가 놓친 이유는 fixture 부족, 동시성 타이밍, 설정 reload, 프로토콜 경계 중 어디에 있었나?</li>
<li>수정이 다시 다른 경로를 깨뜨리지 않도록 어떤 체크를 추가했나?</li>
<li>릴리스 전에 사람이 봐야 하는 증거는 테스트 성공, race detector, 로그, 벤치마크, runbook 중 무엇인가?</li>
</ul>
<p>이 관점으로 읽으면 <a href="/posts/2026-03-14-pgmux-46-qa-findings-six-bugs/">QA 소견 6건과 운영 안전성 수정</a>, <a href="/posts/2026-03-14-pgmux-48-qa-round3-pool-safety/">QA 3차: 풀 안전성의 마지막 구멍들</a>, <a href="/posts/2026-03-17-pgmux-54-qa-round5-release-hygiene/">QA 5차: 릴리즈 위생과 CI 안정성</a> 같은 글이 단순 회고가 아니라 릴리스 게이트 설계 예시로 보입니다. 특히 AI 코드 리뷰나 자동 수정 도구를 붙이는 팀이라면, &ldquo;찾았다&quot;보다 &ldquo;재발하지 않게 어떤 증거를 남겼나&quot;를 중심으로 읽는 편이 좋습니다.</p>
<h2 id="이런-분께-특히-맞습니다">이런 분께 특히 맞습니다</h2>
<ul>
<li>백엔드와 플랫폼 엔지니어링을 실무 관점으로 정리하고 싶은 분</li>
<li>단순 개념 요약보다 운영 trade-off와 의사결정 기준이 궁금한 분</li>
<li>AI 개발 도구, 런타임 통제, 코드 리뷰 체계 변화를 꾸준히 따라가고 싶은 분</li>
<li>프로젝트 회고를 통해 설계가 바뀌는 과정을 보고 싶은 분</li>
</ul>
<p>필요한 주제가 정해져 있다면 상단 검색과 태그 필터를 먼저 쓰는 게 가장 빠르고, 방향을 아직 못 정했다면 위의 추천 읽기 흐름 중 하나를 골라 따라가면 됩니다.</p>
]]></content:encoded></item><item><title>MySQL 인덱스 설계와 실행 계획 읽기</title><link>https://jyukki.com/learning/deep-dive/deep-dive-mysql-index-explain/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-mysql-index-explain/</guid><description>B-Tree/컴포지트 인덱스 설계, EXPLAIN으로 실행 계획을 해석하고 튜닝하는 방법</description><content:encoded><![CDATA[<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>인덱스를 “붙이면 빨라진다”가 아니라, <strong>어떤 쿼리가 왜 빨라지는지</strong> 설명할 수 있습니다.</li>
<li>복합 인덱스(Composite Index)의 순서를 조건/정렬 패턴에 맞춰 설계할 수 있습니다.</li>
<li><code>EXPLAIN</code>에서 위험 신호(<code>ALL</code>, <code>Using temporary</code>, <code>Using filesort</code>)를 읽고 개선 방향을 잡을 수 있습니다.</li>
</ul>
<h2 id="0-인덱스는-정렬된-길이고-비용도-함께-온다">0) 인덱스는 ‘정렬된 길’이고, 비용도 함께 온다</h2>
<p>인덱스는 보통 B-Tree 기반의 “정렬된 자료구조”입니다.
조회는 빨라지지만, 그만큼 쓰기(insert/update/delete)와 저장 공간 비용이 늘어납니다.</p>
<p>실무 감각:</p>
<ul>
<li>읽기(조회)가 압도적으로 많을수록 인덱스의 가치가 커집니다.</li>
<li>쓰기가 많은 테이블에 인덱스를 무작정 늘리면 오히려 전체 성능이 나빠질 수 있습니다.</li>
</ul>
<h2 id="1-인덱스-설계의-핵심-쿼리-패턴이-먼저다">1) 인덱스 설계의 핵심: 쿼리 패턴이 먼저다</h2>
<p>인덱스를 설계할 때는 “컬럼 목록”이 아니라 “자주 호출되는 쿼리”가 출발점입니다.</p>
<p>대표 패턴:</p>
<ul>
<li><code>WHERE</code> 조건(=, IN, range)</li>
<li><code>ORDER BY</code></li>
<li><code>GROUP BY</code></li>
<li><code>JOIN</code> 키</li>
</ul>
<h2 id="2-복합-인덱스composite에서-제일-중요한-규칙">2) 복합 인덱스(Composite)에서 제일 중요한 규칙</h2>
<h3 id="2-1-선두-컬럼leftmost-prefix">2-1) 선두 컬럼(Leftmost prefix)</h3>
<p>복합 인덱스는 “선두부터” 의미가 있습니다.<br>
예: <code>(user_id, created_at)</code> 인덱스는 <code>user_id</code> 조건이 있을 때 가장 효과가 큽니다.</p>
<h3 id="2-2--조건--range-조건--정렬의-순서가-중요해진다">2-2) = 조건 → range 조건 → 정렬의 순서가 중요해진다</h3>
<p>일반적으로:</p>
<ul>
<li>동등 조건(=)이 먼저,</li>
<li>그 다음 범위(range),</li>
<li>정렬(order by)이 자연스럽게 이어지면 좋습니다.</li>
</ul>
<p>예:</p>
<ul>
<li><code>WHERE user_id = ? AND created_at &gt;= ? ORDER BY created_at DESC LIMIT 20</code></li>
<li>인덱스 후보: <code>(user_id, created_at)</code></li>
</ul>
<h2 id="3-커버링-인덱스-테이블을-안-읽는-효과">3) 커버링 인덱스: “테이블을 안 읽는” 효과</h2>
<p>쿼리가 필요한 컬럼을 인덱스에서만 가져올 수 있으면(covering),
테이블(클러스터 인덱스) 접근을 줄여서 큰 효과가 날 수 있습니다.</p>
<p>하지만 커버링을 위해 인덱스에 컬럼을 과도하게 넣으면 인덱스가 비대해져 역효과가 날 수 있으니,
정말 효과가 큰 쿼리부터 적용하는 편이 좋습니다.</p>
<h2 id="4-explain-읽기-이것만-보면-된다처음엔">4) EXPLAIN 읽기: 이것만 보면 된다(처음엔)</h2>
<p>중요도가 높은 순서:</p>
<ol>
<li><code>type</code>: 접근 방식(대체로 <code>ALL</code>이 가장 위험)</li>
<li><code>key</code>: 실제로 어떤 인덱스를 썼는지</li>
<li><code>rows</code>: 얼마나 읽을 걸로 추정하는지(큰 값이면 의심)</li>
<li><code>Extra</code>: <code>Using temporary</code>, <code>Using filesort</code> 같은 경고</li>
</ol>
<p><code>type</code>은 대략 이런 감각입니다.</p>
<ul>
<li><code>const</code>/<code>ref</code>: 좋은 편(정확한 키/참조)</li>
<li><code>range</code>: 범위 스캔(조건/범위에 따라 비용 달라짐)</li>
<li><code>index</code>: 인덱스 전체 스캔(테이블 전체 스캔보단 낫지만 주의)</li>
<li><code>ALL</code>: 테이블 풀 스캔(대부분 튜닝 후보)</li>
</ul>
<h2 id="5-예시로-감-잡기">5) 예시로 감 잡기</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> id, created_at
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> user_id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">10</span> <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-01-01&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">ORDER</span> <span style="color:#ff79c6">BY</span> created_at <span style="color:#ff79c6">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">LIMIT</span> <span style="color:#bd93f9">20</span>;
</span></span></code></pre></div><p>개선 루틴:</p>
<ol>
<li>EXPLAIN을 보고 <code>key/type/rows/Extra</code> 확인</li>
<li>인덱스 후보를 세우고(<code>(user_id, created_at)</code> 등)</li>
<li>인덱스 추가 전/후로 “실제 쿼리 시간/rows”가 어떻게 변하는지 검증</li>
</ol>
<h2 id="6-자주-하는-실수">6) 자주 하는 실수</h2>
<ul>
<li>인덱스가 “걸려 있다”는 이유로 모든 쿼리가 빠를 거라 믿는 경우(실제로는 <code>ALL/filesort</code>가 나옴)</li>
<li><code>%keyword%</code> 검색에 인덱스를 기대하는 경우(일반 B-Tree 인덱스로는 어렵고, 필요하면 fulltext/검색 엔진 고려)</li>
<li>JOIN 키에 인덱스가 없어서 조인이 폭발하는 경우</li>
<li>인덱스를 너무 많이 만들어 쓰기/스토리지/버퍼 효율이 무너지는 경우</li>
</ul>
<h2 id="연습추천">연습(추천)</h2>
<ul>
<li>한 테이블에서 자주 호출되는 조회 쿼리 2~3개를 고르고, EXPLAIN 결과를 저장해두기</li>
<li>복합 인덱스 순서를 바꿔가며(<code>(A,B)</code> vs <code>(B,A)</code>) 어떤 쿼리에서 효과가 달라지는지 비교해보기</li>
<li><code>Using filesort/temporary</code>가 나오는 쿼리를 일부러 만들고, 인덱스로 개선 가능한지 확인해보기</li>
</ul>
]]></content:encoded></item><item><title>SQL 기본: 조인/집계/인덱스가 먹는 조건 감각</title><link>https://jyukki.com/learning/deep-dive/deep-dive-sql-basics-joins-explain/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-sql-basics-joins-explain/</guid><description>SQL 실행 순서와 조인/집계 성능 함정, 인덱스가 타는 조건을 감각으로 잡는 기본기</description><content:encoded><![CDATA[<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>SQL을 “문법”이 아니라 **실행 관점(어떤 순서로 처리되는지)**으로 이해합니다.</li>
<li>조인/집계/정렬에서 성능이 터지는 지점을 미리 예상할 수 있습니다(왜 느린지 말로 설명 가능).</li>
<li>인덱스가 잘 타는 조건(sargable)과 안 타는 조건을 구분해, 다음 인덱스/튜닝 글을 훨씬 쉽게 따라갈 수 있습니다.</li>
</ul>
<h2 id="0-왜-sql-기본이-중요한가">0) 왜 SQL ‘기본’이 중요한가</h2>
<p>백엔드 성능 이슈의 상당수는 결국 SQL로 귀결됩니다.</p>
<ul>
<li>애플리케이션은 느린 DB를 숨기기 어렵고,</li>
<li>캐시/비동기/샤딩 같은 고급 해법도 “기본 쿼리”가 망가지면 효과가 제한됩니다.</li>
</ul>
<p>그래서 이 글은 “정답 쿼리”가 아니라, <strong>느린 쿼리를 보고 원인을 좁히는 감각</strong>을 만드는 데 집중합니다.</p>
<h2 id="1-sql-실행-순서논리적-처리-순서">1) SQL 실행 순서(논리적 처리 순서)</h2>
<p>보통 SELECT는 아래 순서로 처리된다고 생각하면 디버깅이 쉬워집니다.</p>
<ol>
<li><code>FROM</code> / <code>JOIN</code>(데이터 집합 만들기)</li>
<li><code>WHERE</code>(필터)</li>
<li><code>GROUP BY</code>(그룹)</li>
<li><code>HAVING</code>(그룹 필터)</li>
<li><code>SELECT</code>(컬럼 계산/프로젝션)</li>
<li><code>ORDER BY</code>(정렬)</li>
<li><code>LIMIT/OFFSET</code>(잘라내기)</li>
</ol>
<p>포인트:</p>
<ul>
<li><code>WHERE</code>는 “행”을 줄이고, <code>HAVING</code>은 “그룹”을 줄입니다.</li>
<li><code>ORDER BY</code>는 보통 비쌉니다(정렬 비용). 인덱스로 정렬을 대체할 수 있으면 큰 이득입니다.</li>
</ul>
<h2 id="2-조인-무엇이-느려지는가">2) 조인: 무엇이 느려지는가</h2>
<h3 id="2-1-조인의-비용은-매칭에서-온다">2-1) 조인의 비용은 “매칭”에서 온다</h3>
<p>조인은 결국:</p>
<ul>
<li>A의 각 행에 대해,</li>
<li>B에서 매칭되는 행을 찾는 과정입니다.</li>
</ul>
<p>그래서 핵심은:</p>
<ul>
<li>조인 키에 인덱스가 있는가?</li>
<li>어느 쪽이 드라이빙 테이블(먼저 읽는 쪽)인가?</li>
<li>필터(WHERE)가 얼마나 먼저 적용되는가?</li>
</ul>
<h3 id="2-2-흔한-실수">2-2) 흔한 실수</h3>
<ul>
<li>필터를 늦게 적용해 조인 대상이 불필요하게 커짐</li>
<li>조인 키 타입 불일치(문자열 vs 숫자 등)로 인덱스가 무력화</li>
<li>1:N 조인 후 중복 행이 폭발해 <code>DISTINCT</code>/<code>GROUP BY</code>로 땜질(근본 문제 숨김)</li>
</ul>
<h2 id="3-집계정렬-데이터가-커질수록-폭발한다">3) 집계/정렬: 데이터가 커질수록 폭발한다</h2>
<h3 id="3-1-group-by">3-1) GROUP BY</h3>
<p><code>GROUP BY</code>는 “모아서 계산”이므로 데이터가 커지면 비용이 빠르게 증가합니다.</p>
<ul>
<li>그룹 키가 많을수록(고유값 많을수록) 비용 증가</li>
<li>정렬/해시 기반 집계는 DB마다 다르지만, 공통적으로 “메모리/임시 공간”이 관여합니다</li>
</ul>
<h3 id="3-2-order-by">3-2) ORDER BY</h3>
<p>정렬은 다음 중 하나입니다.</p>
<ul>
<li>인덱스 순서를 그대로 이용(싸다)</li>
<li>별도의 정렬 수행(비싸다, 임시 공간/파일 정렬)</li>
</ul>
<p>그래서 “인덱스가 ORDER BY를 커버할 수 있는지”는 튜닝에서 자주 보는 포인트입니다.</p>
<h2 id="4-인덱스가-먹는-조건sargable-감각">4) 인덱스가 ‘먹는’ 조건(sargable) 감각</h2>
<p>인덱스를 타기 쉬운 조건:</p>
<ul>
<li><code>col = ?</code>, <code>col IN (...)</code></li>
<li><code>col &gt;= ? AND col &lt; ?</code>(범위)</li>
</ul>
<p>인덱스를 망치기 쉬운 조건:</p>
<ul>
<li>컬럼에 함수 적용: <code>DATE(col) = ...</code></li>
<li>앞 와일드카드: <code>LIKE '%abc'</code></li>
<li>타입 캐스팅/불일치로 인해 변환이 발생</li>
</ul>
<p>예:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 인덱스를 못 타기 쉬움(컬럼에 함수)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> <span style="color:#8be9fd;font-style:italic">DATE</span>(created_at) <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;2025-12-16&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 범위로 바꾸면 인덱스를 타기 쉬움
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-12-16 00:00:00&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&lt;</span>  <span style="color:#f1fa8c">&#39;2025-12-17 00:00:00&#39;</span>
</span></span></code></pre></div><h2 id="5-explain을-읽는-최소-루틴맛보기">5) EXPLAIN을 읽는 최소 루틴(맛보기)</h2>
<p>다음 글에서 더 깊게 보겠지만, 최소한 아래는 습관으로 가져가면 좋습니다.</p>
<ul>
<li>scan 범위가 너무 큰가(풀스캔/범위 스캔)</li>
<li>어떤 인덱스를 썼는가(의도한 인덱스가 맞는가)</li>
<li>정렬/임시 테이블이 있는가(정렬 비용/메모리/디스크 사용)</li>
</ul>
<p>중요한 건:</p>
<blockquote>
<p>EXPLAIN은 “정답”이 아니라, 가설을 세우게 해주는 힌트다</p></blockquote>
<h2 id="연습추천">연습(추천)</h2>
<ul>
<li>같은 조건을 “함수 조건”과 “범위 조건”으로 각각 작성하고, EXPLAIN에서 인덱스 사용 여부가 어떻게 달라지는지 비교해보기</li>
<li>1:N 조인에서 결과가 폭발하는 케이스를 만들고, 필터를 조인 전에 적용했을 때 얼마나 줄어드는지 실험해보기</li>
<li>ORDER BY가 있는 조회에서 “인덱스로 정렬이 대체되는 경우/안 되는 경우”를 케이스로 만들어 비교해보기</li>
</ul>
]]></content:encoded></item><item><title>인덱스 기본: B-Tree 구조와 쿼리 성능</title><link>https://jyukki.com/learning/deep-dive/deep-dive-database-indexing/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-database-indexing/</guid><description>인덱스가 왜 빨라지는지(B-Tree/선택도/커버링), 복합 인덱스 설계와 쿼리 튜닝의 기본 감각</description><content:encoded><![CDATA[<h2 id="-1-인덱스는-책의-목차가-아니다">🏎️ 1. 인덱스는 &lsquo;책의 목차&rsquo;가 아니다</h2>
<p>많은 입문서가 인덱스를 책의 목차에 비유하지만, 엔지니어라면 좀 더 정확한 비유가 필요합니다.
인덱스는 **&ldquo;정렬된 상태를 유지하는 별도의 자료구조&rdquo;**입니다.</p>
<ul>
<li><strong>Full/Table Scan</strong>: 사전의 첫 페이지부터 끝 페이지까지 단어 하나하나 찾는 것. <code>O(N)</code></li>
<li><strong>Index Scan</strong>: 사전의 &lsquo;ㄱ&rsquo;, &lsquo;ㄴ&rsquo;, &lsquo;ㄷ&rsquo; 탭을 이용해 범위를 좁히고 이진 탐색하는 것. <code>O(log N)</code></li>
</ul>
<p>하지만 공짜 점심은 없습니다. <strong>정렬된 상태</strong>를 유지해야 하므로, <strong>쓰기(Insert/Update/Delete) 성능을 희생</strong>하여 읽기 속도를 얻는 것입니다.</p>
<hr>
<h2 id="-2-clustered-vs-non-clustered-index">🏗️ 2. Clustered vs Non-Clustered Index</h2>
<p>가장 중요한 개념이자 가장 많이 헷갈리는 개념입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>graph TD
</span></span><span style="display:flex;"><span>    subgraph Clustered_Index [&#34;Clustered Index (PK)&#34;]
</span></span><span style="display:flex;"><span>        Root1[Root] --&gt; Leaf1[Leaf Node]
</span></span><span style="display:flex;"><span>        Leaf1 --&gt; Data1[&#34;📄 실제 데이터 행 (Row Data)&#34;]
</span></span><span style="display:flex;"><span>    end
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    subgraph Non_Clustered_Index [&#34;Non-Clustered Index&#34;]
</span></span><span style="display:flex;"><span>        Root2[Root] --&gt; Leaf2[Leaf Node]
</span></span><span style="display:flex;"><span>        Leaf2 --&gt; Pointer2[&#34;📍 PK 값 (Pointer)&#34;]
</span></span><span style="display:flex;"><span>    end
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    Pointer2 -.-&gt;|Lookup| Root1
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    style Data1 fill:#ffccbc,stroke:#d84315
</span></span><span style="display:flex;"><span>    style Pointer2 fill:#b3e5fc,stroke:#0277bd
</span></span></code></pre></div><ol>
<li><strong>Clustered Index (PK)</strong>: 책의 <strong>페이지 번호</strong>입니다. 리프 노드에 **실제 데이터(Row)**가 저장됩니다. 테이블당 <strong>하나만</strong> 존재할 수 있습니다.</li>
<li><strong>Non-Clustered Index</strong>: 책의 **색인(목차)**입니다. 리프 노드에는 **PK 값(주소)**만 있습니다. 이걸 보고 다시 Clustered Index를 뒤져야 실제 데이터를 얻을 수 있습니다 (이 과정을 <strong>Random Access</strong>라고 하며 비쌉니다).</li>
</ol>
<hr>
<h2 id="-2-b-tree-balanced-tree-구조">🌳 2. B-Tree (Balanced Tree) 구조</h2>
<p>대부분의 RDBMS(MySQL, Oracle)가 사용하는 기본 인덱스 구조입니다.
핵심은 <strong>트리 높이(Height)를 일정하게 유지</strong>하여, 어떤 데이터를 찾든 균일한 속도(Log N)를 보장하는 것입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>graph TD
</span></span><span style="display:flex;"><span>    Root[Root Node] --&gt; Branch1[Branch: 1-50]
</span></span><span style="display:flex;"><span>    Root --&gt; Branch2[Branch: 51-100]
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    Branch1 --&gt; Leaf1[Leaf: 10, 20]
</span></span><span style="display:flex;"><span>    Branch1 --&gt; Leaf2[Leaf: 30, 40]
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    Branch2 --&gt; Leaf3[Leaf: 60, 70]
</span></span><span style="display:flex;"><span>    Branch2 --&gt; Leaf4[Leaf: 80, 90]
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    Leaf1 -.-&gt;|Linked List| Leaf2
</span></span><span style="display:flex;"><span>    Leaf2 -.-&gt;|Linked List| Leaf3
</span></span><span style="display:flex;"><span>    Leaf3 -.-&gt;|Linked List| Leaf4
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    style Root fill:#e3f2fd,stroke:#2196f3
</span></span><span style="display:flex;"><span>    style Branch1 fill:#fff9c4,stroke:#fbc02d
</span></span><span style="display:flex;"><span>    style Leaf1 fill:#c8e6c9,stroke:#4caf50
</span></span></code></pre></div><h3 id="b-tree-vs-hash-index-비교">B-Tree vs Hash Index 비교</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: left">특징</th>
          <th style="text-align: left">B-Tree Index</th>
          <th style="text-align: left">Hash Index</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>구조</strong></td>
          <td style="text-align: left">트리 (Balanced Tree)</td>
          <td style="text-align: left">해시 테이블 (Key-Value)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>시간 복잡도</strong></td>
          <td style="text-align: left"><code>O(log N)</code></td>
          <td style="text-align: left"><code>O(1)</code> (단건 조회 시)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>범위 검색</strong></td>
          <td style="text-align: left"><strong>가능</strong> (<code>BETWEEN</code>, <code>&gt;</code>, <code>&lt;</code>)</td>
          <td style="text-align: left"><strong>불가능</strong> (Equality <code>=</code> 만 가능)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>사용처</strong></td>
          <td style="text-align: left">대부분의 범용 인덱스</td>
          <td style="text-align: left">인메모리 DB, 정확한 일치 검색</td>
      </tr>
  </tbody>
</table>
<h3 id="왜-b-tree일까">왜 B-Tree일까?</h3>
<ol>
<li><strong>균형(Balanced)</strong>: 데이터가 편향되지 않아 최악의 경우에도 빠릅니다.</li>
<li><strong>범위 검색(Range Scan)</strong>: 리프 노드끼리 <strong>Linked List</strong>로 연결되어 있습니다. &ldquo;20살부터 30살까지&rdquo; 찾을 때, 20살만 찾으면 그 뒤는 쭉 읽으면(Scan) 됩니다.</li>
</ol>
<hr>
<h2 id="-3-복합-인덱스-composite-index">🧬 3. 복합 인덱스 (Composite Index)</h2>
<p>여러 컬럼을 묶어서 인덱스를 만들 때 가장 중요한 규칙은 <strong>순서</strong>입니다.</p>
<h3 id="복합-인덱스-성별-나이-순서라면">복합 인덱스: (성별, 나이) 순서라면?</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>classDiagram
</span></span><span style="display:flex;"><span>    class CompositeIndex {
</span></span><span style="display:flex;"><span>        +Male (남)
</span></span><span style="display:flex;"><span>        +Female (여)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    class MaleGroup {
</span></span><span style="display:flex;"><span>        20세
</span></span><span style="display:flex;"><span>        21세
</span></span><span style="display:flex;"><span>        25세
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    class FemaleGroup {
</span></span><span style="display:flex;"><span>        20세
</span></span><span style="display:flex;"><span>        22세
</span></span><span style="display:flex;"><span>        30세
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    CompositeIndex *-- MaleGroup : 1차 정렬 (성별)
</span></span><span style="display:flex;"><span>    CompositeIndex *-- FemaleGroup
</span></span><span style="display:flex;"><span>    MaleGroup : 2차 정렬 (나이)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>(남, 20), (남, 21), (남, 25) ... (여, 20), (여, 22) ...
</span></span></code></pre></div><p>-&gt; <strong>성별</strong>끼리 먼저 뭉치고, 그 안에서 <strong>나이</strong>순으로 정렬됩니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 인덱스 잘 탐 (성별로 먼저 거르고 나이 찾음)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> gender <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;남&#39;</span> <span style="color:#ff79c6">AND</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">20</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 인덱스 못 탐 (성별 없이 나이만? 정렬 안 되어 있음)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">20</span>;
</span></span></code></pre></div><blockquote>
<p><strong>Leftmost Prefix Rule</strong>: 인덱스의 왼쪽 컬럼부터 차례대로 사용해야 인덱스가 적용됩니다. 중간을 건너뛰면 그 뒤 컬럼은 인덱스 효과가 사라집니다.</p></blockquote>
<hr>
<h2 id="-4-커버링-인덱스-covering-index">🛡️ 4. 커버링 인덱스 (Covering Index)</h2>
<p>쿼리에 필요한 모든 컬럼이 인덱스 안에 다 있다면? 실제 데이터 파일(Table Heap)을 열어볼 필요가 없습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>sequenceDiagram
</span></span><span style="display:flex;"><span>    participant Query
</span></span><span style="display:flex;"><span>    participant Index
</span></span><span style="display:flex;"><span>    participant Table
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    Note over Query: SELECT name FROM users &lt;br/&gt; WHERE id = 10
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    Query-&gt;&gt;Index: &#34;id=10 있니?&#34;
</span></span><span style="display:flex;"><span>    Note right of Index: 어, 나 name도 갖고 있는데?
</span></span><span style="display:flex;"><span>    Index--&gt;&gt;Query: 여기 있어! (name=&#39;Alice&#39;)
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    Note over Table: (테이블 접근 안 함 ⚡)
</span></span></code></pre></div><p>이것을 <strong>커버링 인덱스</strong>라고 하며, 쿼리 튜닝의 <strong>치트키</strong>입니다. 불필요한 <code>SELECT *</code>만 피해도 성능이 비약적으로 좋아지는 이유입니다.</p>
<h2 id="요약">요약</h2>
<blockquote>
<p>[!TIP]
<strong>Index Design Checklist</strong>:</p>
<ul>
<li><input disabled="" type="checkbox"> <strong>Cardinality</strong>: 중복도가 낮은(유니크한) 컬럼에 걸어라. (ex. 성별(X) vs 주민번호(O))</li>
<li><input disabled="" type="checkbox"> <strong>Update</strong>: 쓰기가 너무 빈번한 테이블은 인덱스를 최소화해라.</li>
<li><input disabled="" type="checkbox"> <strong>Covering</strong>: 쿼리에 필요한 컬럼을 인덱스에 다 포함시켜 테이블 접근을 막아라.</li>
<li><input disabled="" type="checkbox"> <strong>Composite</strong>: <code>WHERE</code>, <code>ORDER BY</code>, <code>GROUP BY</code> 컬럼 순서대로 인덱스를 구성해라.</li>
</ul></blockquote>
<ol>
<li><strong>Trade-off</strong>: 인덱스는 <strong>쓰기를 죽이고 읽기를 살리는</strong> 기술이다.</li>
<li><strong>B-Tree</strong>: <strong>정렬</strong>되어 있고 <strong>균형</strong> 잡혀 있다.</li>
<li><strong>복합 인덱스</strong>: <strong>순서</strong>가 생명이다. (Leftmost Prefix).</li>
<li><strong>커버링 인덱스</strong>: 테이블 접근을 생략하는 쿼리 튜닝의 핵심.</li>
</ol>
]]></content:encoded></item><item><title>DB 인덱스 최적화 정리</title><link>https://jyukki.com/learning/qna/db-index-optimization-qna/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/qna/db-index-optimization-qna/</guid><description>B-Tree 인덱스, 복합 인덱스 설계, Index Scan 종류별 차이와 실전 최적화 전략 Q&amp;amp;A</description><content:encoded><![CDATA[<h1 id="db-인덱스-최적화-정리">DB 인덱스 최적화 정리</h1>
<h2 id="q1-b-tree-인덱스는-어떻게-동작하나요">Q1. B-Tree 인덱스는 어떻게 동작하나요?</h2>
<h3 id="답변">답변</h3>
<p>**B-Tree (Balanced Tree)**는 대부분의 RDBMS에서 사용하는 <strong>균형 잡힌 트리 구조의 인덱스</strong>입니다.</p>
<p><strong>구조적 특징</strong>:</p>
<ol>
<li><strong>Root Node</strong>: 최상위 노드</li>
<li><strong>Branch Node</strong>: 중간 노드 (범위 정보)</li>
<li><strong>Leaf Node</strong>: 실제 데이터 포인터 (Linked List로 연결)</li>
</ol>
<p><strong>B-Tree 구조 예시</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>인덱스: users(age)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                    [50]                     ← Root
</span></span><span style="display:flex;"><span>                   /    \
</span></span><span style="display:flex;"><span>                 /        \
</span></span><span style="display:flex;"><span>            [20, 35]      [65, 80]           ← Branch
</span></span><span style="display:flex;"><span>            /  |  \        /  |  \
</span></span><span style="display:flex;"><span>          /    |    \    /    |    \
</span></span><span style="display:flex;"><span>      [10]  [25]  [40] [55] [70]  [90]      ← Leaf (실제 데이터 포인터)
</span></span><span style="display:flex;"><span>       ↓     ↓     ↓    ↓    ↓     ↓
</span></span><span style="display:flex;"><span>     Row1  Row2  Row3 Row4 Row5  Row6
</span></span></code></pre></div><p><strong>검색 과정</strong> (age = 25 찾기):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 1. Root Node: 25 &lt; 50 → 왼쪽 Branch
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 2. Branch Node: 20 ≤ 25 &lt; 35 → 중간 Leaf
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 3. Leaf Node: 25 발견 → Row 포인터 반환
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 3번의 블록 I/O (Root → Branch → Leaf)
</span></span></span></code></pre></div><p><strong>시간 복잡도</strong>:</p>
<table>
  <thead>
      <tr>
          <th>연산</th>
          <th>시간 복잡도</th>
          <th>설명</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>검색</td>
          <td>O(log N)</td>
          <td>트리 높이만큼 탐색</td>
      </tr>
      <tr>
          <td>삽입</td>
          <td>O(log N)</td>
          <td>검색 + 삽입</td>
      </tr>
      <tr>
          <td>삭제</td>
          <td>O(log N)</td>
          <td>검색 + 삭제</td>
      </tr>
      <tr>
          <td>범위 검색</td>
          <td>O(log N + K)</td>
          <td>Leaf 노드 순회</td>
      </tr>
  </tbody>
</table>
<p><strong>Full Table Scan vs Index Scan 비교</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 테이블: users (100만 건)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 블록 크기: 8KB, Row 크기: 200 bytes
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 한 블록당 40개 Row
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ Full Table Scan
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 25,000 블록 읽기 (100만 / 40)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 약 200MB I/O
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ Index Scan (B-Tree depth = 3)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_age <span style="color:#ff79c6">ON</span> users(age);
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 3 블록 읽기 (Root + Branch + Leaf)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 해당 Row 블록 1개 추가
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 총 4 블록 (약 32KB I/O)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 6,250배 빠름!
</span></span></span></code></pre></div><h3 id="꼬리-질문-1-b-tree-vs-hash-index-차이는">꼬리 질문 1: B-Tree vs Hash Index 차이는?</h3>
<p><strong>비교표</strong>:</p>
<table>
  <thead>
      <tr>
          <th>특징</th>
          <th>B-Tree</th>
          <th>Hash</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>등호 검색 (=)</td>
          <td>O(log N)</td>
          <td>O(1)</td>
      </tr>
      <tr>
          <td>범위 검색 (&gt;, &lt;)</td>
          <td>O(log N + K)</td>
          <td>불가능 ❌</td>
      </tr>
      <tr>
          <td>정렬 (ORDER BY)</td>
          <td>가능 ✅</td>
          <td>불가능 ❌</td>
      </tr>
      <tr>
          <td>LIKE 검색</td>
          <td>가능 (prefix)</td>
          <td>불가능 ❌</td>
      </tr>
      <tr>
          <td>사용 DB</td>
          <td>대부분</td>
          <td>MySQL (MEMORY), PostgreSQL</td>
      </tr>
  </tbody>
</table>
<p><strong>예시</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ B-Tree 인덱스가 유리
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">BETWEEN</span> <span style="color:#bd93f9">20</span> <span style="color:#ff79c6">AND</span> <span style="color:#bd93f9">30</span>;  <span style="color:#6272a4">-- 범위 검색
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> name <span style="color:#ff79c6">LIKE</span> <span style="color:#f1fa8c">&#39;John%&#39;</span>;       <span style="color:#6272a4">-- Prefix 검색
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">ORDER</span> <span style="color:#ff79c6">BY</span> created_at <span style="color:#ff79c6">DESC</span>;     <span style="color:#6272a4">-- 정렬
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ Hash 인덱스가 유리
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> user_id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">12345</span>;        <span style="color:#6272a4">-- 등호 검색 (PK)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> <span style="color:#ff79c6">cache</span> <span style="color:#ff79c6">WHERE</span> cache_key <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;key123&#39;</span>;   <span style="color:#6272a4">-- 정확한 매칭
</span></span></span></code></pre></div><h3 id="꼬리-질문-2-clustered-index-vs-non-clustered-index는">꼬리 질문 2: Clustered Index vs Non-Clustered Index는?</h3>
<p><strong>Clustered Index</strong> (클러스터형):</p>
<ul>
<li><strong>실제 데이터가 인덱스 순서로 정렬</strong>됨</li>
<li>테이블당 1개만 가능 (보통 PK)</li>
<li>Leaf Node = 실제 데이터</li>
</ul>
<p><strong>Non-Clustered Index</strong> (비클러스터형):</p>
<ul>
<li><strong>인덱스와 데이터가 분리</strong></li>
<li>테이블당 여러 개 가능</li>
<li>Leaf Node = 데이터 포인터</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- MySQL InnoDB 예시
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Clustered Index (PK)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> users (
</span></span><span style="display:flex;"><span>    id <span style="color:#8be9fd;font-style:italic">INT</span> <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>,      <span style="color:#6272a4">-- Clustered Index
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    name <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">100</span>),
</span></span><span style="display:flex;"><span>    age <span style="color:#8be9fd;font-style:italic">INT</span>
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 실제 데이터가 id 순서로 디스크에 저장됨
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Non-Clustered Index (Secondary Index)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_age <span style="color:#ff79c6">ON</span> users(age);
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → age 인덱스 별도 저장
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Leaf Node는 PK(id) 값을 가짐
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → age 인덱스 조회 → PK로 다시 Clustered Index 조회 (2단계)
</span></span></span></code></pre></div><p><strong>성능 차이</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ Clustered Index (빠름)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">100</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 1번 조회 (Clustered Index)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ⚠️ Non-Clustered Index (느림)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 1번: age 인덱스 조회 (id = 100 발견)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 2번: Clustered Index에서 id = 100 조회
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 총 2번 조회
</span></span></span></code></pre></div><hr>
<h2 id="q2-복합-인덱스는-어떻게-설계하나요">Q2. 복합 인덱스는 어떻게 설계하나요?</h2>
<h3 id="답변-1">답변</h3>
<p>**복합 인덱스 (Composite Index)**는 <strong>여러 컬럼을 조합한 인덱스</strong>입니다.</p>
<p><strong>핵심 원칙</strong>: 선택도가 높은 컬럼을 앞에 배치 + 범위 검색 컬럼은 뒤에</p>
<h3 id="컬럼-순서의-중요성">컬럼 순서의 중요성</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 테이블: orders
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 데이터: 100만 건
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- country: 10개 (선택도 낮음)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- status: 5개 (선택도 낮음)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- created_at: 백만 개 (선택도 높음)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 잘못된 순서
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_bad <span style="color:#ff79c6">ON</span> orders(country, status, created_at);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> status <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;COMPLETED&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-01-01&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → country 조건이 없어서 인덱스 사용 불가! ❌
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 올바른 순서 (선택도 높은 컬럼 우선)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_good <span style="color:#ff79c6">ON</span> orders(status, country, created_at);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> status <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;COMPLETED&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-01-01&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → status로 인덱스 시작 가능 ✅
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → created_at으로 범위 검색 ✅
</span></span></span></code></pre></div><h3 id="복합-인덱스-활용-규칙">복합 인덱스 활용 규칙</h3>
<p><strong>Leftmost Prefix Rule</strong> (최좌측 접두어 규칙):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_abc <span style="color:#ff79c6">ON</span> users(a, b, <span style="color:#ff79c6">c</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 인덱스 사용 가능
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> a <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">1</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> a <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">1</span> <span style="color:#ff79c6">AND</span> b <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">2</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> a <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">1</span> <span style="color:#ff79c6">AND</span> b <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">2</span> <span style="color:#ff79c6">AND</span> <span style="color:#ff79c6">c</span> <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">3</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> a <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">1</span> <span style="color:#ff79c6">AND</span> <span style="color:#ff79c6">c</span> <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">3</span>  <span style="color:#6272a4">-- a만 사용
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 인덱스 사용 불가
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> b <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">2</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> <span style="color:#ff79c6">c</span> <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">3</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> b <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">2</span> <span style="color:#ff79c6">AND</span> <span style="color:#ff79c6">c</span> <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">3</span>
</span></span></code></pre></div><p><strong>실무 예시</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 사용자 검색 쿼리 패턴 분석
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 패턴 1: WHERE city = &#39;Seoul&#39; AND age = 25 (70%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 패턴 2: WHERE city = &#39;Seoul&#39; AND gender = &#39;M&#39; (20%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 패턴 3: WHERE city = &#39;Seoul&#39; (10%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 최적 인덱스 설계
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_city_age_gender <span style="color:#ff79c6">ON</span> users(city, age, gender);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 패턴 1 (70%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> city <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;Seoul&#39;</span> <span style="color:#ff79c6">AND</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → idx_city_age_gender 사용 (city, age) ✅
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 패턴 2 (20%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> city <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;Seoul&#39;</span> <span style="color:#ff79c6">AND</span> gender <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;M&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → idx_city_age_gender 사용 (city만) ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → gender는 Skip Scan
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 패턴 3 (10%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> city <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;Seoul&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → idx_city_age_gender 사용 (city) ✅
</span></span></span></code></pre></div><h3 id="범위-검색과-등호-검색-혼합">범위 검색과 등호 검색 혼합</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 범위 검색을 앞에 배치
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_bad <span style="color:#ff79c6">ON</span> orders(created_at, status);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-01-01&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> status <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;COMPLETED&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → created_at 범위 검색 후 status 조건은 인덱스 미사용 ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 등호 검색을 앞에 배치
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_good <span style="color:#ff79c6">ON</span> orders(status, created_at);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> status <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;COMPLETED&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-01-01&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → status 등호 검색 → created_at 범위 검색 ✅
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스 완전 활용
</span></span></span></code></pre></div><h3 id="꼬리-질문-1-인덱스가-너무-많으면-안-좋은-이유는">꼬리 질문 1: 인덱스가 너무 많으면 안 좋은 이유는?</h3>
<p><strong>Write Penalty (쓰기 부담)</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 테이블: users (10개 인덱스)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">INSERT</span> <span style="color:#ff79c6">INTO</span> users <span style="color:#ff79c6">VALUES</span> (...);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 실행 내용:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 1. 실제 데이터 삽입
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 2. 인덱스 1 업데이트
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 3. 인덱스 2 업데이트
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ...
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 11. 인덱스 10 업데이트
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 총 11번 쓰기 작업! ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 성능 영향:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 인덱스 0개: 100,000 INSERTs/sec
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 인덱스 5개: 50,000 INSERTs/sec (50% 감소)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 인덱스 10개: 20,000 INSERTs/sec (80% 감소)
</span></span></span></code></pre></div><p><strong>권장 사항</strong>:</p>
<table>
  <thead>
      <tr>
          <th>테이블 유형</th>
          <th>권장 인덱스 개수</th>
          <th>이유</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>읽기 위주 (조회)</td>
          <td>5-10개</td>
          <td>조회 성능 우선</td>
      </tr>
      <tr>
          <td>쓰기 위주 (로그)</td>
          <td>1-3개</td>
          <td>삽입 성능 우선</td>
      </tr>
      <tr>
          <td>균형 (OLTP)</td>
          <td>3-7개</td>
          <td>읽기/쓰기 균형</td>
      </tr>
  </tbody>
</table>
<h3 id="꼬리-질문-2-skip-scan이란">꼬리 질문 2: Skip Scan이란?</h3>
<p><strong>Index Skip Scan</strong>은 <strong>복합 인덱스의 첫 컬럼 조건이 없어도</strong> 인덱스를 사용하는 최적화 기법입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_gender_age <span style="color:#ff79c6">ON</span> users(gender, age);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 일반적으로는 인덱스 사용 불가
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- (gender 조건 없음)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ Skip Scan 사용 (Oracle, PostgreSQL 11+)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 내부적으로:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> gender <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;M&#39;</span> <span style="color:#ff79c6">AND</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">UNION</span> <span style="color:#ff79c6">ALL</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> gender <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;F&#39;</span> <span style="color:#ff79c6">AND</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → gender 값(M, F)을 자동으로 반복하며 검색
</span></span></span></code></pre></div><p><strong>조건</strong>:</p>
<ul>
<li>첫 컬럼의 Cardinality가 낮아야 함 (값의 종류가 적음)</li>
<li>예: gender (M/F), status (3-5개 값)</li>
</ul>
<hr>
<h2 id="q3-covering-index란-무엇인가요">Q3. Covering Index란 무엇인가요?</h2>
<h3 id="답변-2">답변</h3>
<p><strong>Covering Index</strong>는 <strong>쿼리에 필요한 모든 컬럼을 인덱스가 포함</strong>하여, 테이블 접근 없이 인덱스만으로 결과를 반환하는 최적화 기법입니다.</p>
<p><strong>동작 원리</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 테이블: users (100만 건)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> users (
</span></span><span style="display:flex;"><span>    id <span style="color:#8be9fd;font-style:italic">INT</span> <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>,
</span></span><span style="display:flex;"><span>    name <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">100</span>),
</span></span><span style="display:flex;"><span>    email <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">100</span>),
</span></span><span style="display:flex;"><span>    age <span style="color:#8be9fd;font-style:italic">INT</span>,
</span></span><span style="display:flex;"><span>    city <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">50</span>)
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ Non-Covering Index
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_age <span style="color:#ff79c6">ON</span> users(age);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> id, name, email <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 실행 과정:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 1. idx_age에서 age = 25인 Row 검색
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 2. Leaf Node에서 PK(id) 값 획득
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 3. Clustered Index에서 id로 실제 Row 조회 (name, email 가져옴)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 2번 조회 (Index + Table) ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ Covering Index
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_age_id_name_email <span style="color:#ff79c6">ON</span> users(age, id, name, email);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> id, name, email <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 실행 과정:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 1. idx_age_id_name_email에서 age = 25인 Row 검색
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 2. 인덱스 Leaf Node에 id, name, email이 모두 있음!
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 1번 조회 (Index만) ✅
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 테이블 접근 불필요 (빠름!)
</span></span></span></code></pre></div><p><strong>성능 비교</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 실험: 100만 건 테이블, age = 25 (5만 건)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ Non-Covering Index
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_age <span style="color:#ff79c6">ON</span> users(age);
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> id, name, email <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 실행 시간: 500ms
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 블록 I/O: 50,000 (인덱스 + 테이블)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ Covering Index
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_age_covering <span style="color:#ff79c6">ON</span> users(age, id, name, email);
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> id, name, email <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> age <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">25</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 실행 시간: 50ms (10배 빠름!)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 블록 I/O: 5,000 (인덱스만)
</span></span></span></code></pre></div><h3 id="covering-index-설계-전략">Covering Index 설계 전략</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 빈번한 쿼리 패턴
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> user_id, name, email
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> users
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> status <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;ACTIVE&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-01-01&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">ORDER</span> <span style="color:#ff79c6">BY</span> created_at <span style="color:#ff79c6">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">LIMIT</span> <span style="color:#bd93f9">10</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ Covering Index 설계
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_covering <span style="color:#ff79c6">ON</span> users(
</span></span><span style="display:flex;"><span>    status,          <span style="color:#6272a4">-- WHERE 조건
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    created_at,      <span style="color:#6272a4">-- WHERE 조건 + ORDER BY
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    user_id,         <span style="color:#6272a4">-- SELECT 컬럼
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    name,            <span style="color:#6272a4">-- SELECT 컬럼
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    email            <span style="color:#6272a4">-- SELECT 컬럼
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스만으로 모든 데이터 반환 ✅
</span></span></span></code></pre></div><h3 id="꼬리-질문-1-covering-index의-단점은">꼬리 질문 1: Covering Index의 단점은?</h3>
<p><strong>1. 인덱스 크기 증가</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 과도한 Covering Index
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_too_large <span style="color:#ff79c6">ON</span> users(
</span></span><span style="display:flex;"><span>    age,
</span></span><span style="display:flex;"><span>    name,       <span style="color:#6272a4">-- VARCHAR(100)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    email,      <span style="color:#6272a4">-- VARCHAR(100)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    address,    <span style="color:#6272a4">-- VARCHAR(200)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    description <span style="color:#6272a4">-- TEXT
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>);
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스 크기가 테이블보다 클 수 있음! ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스 캐시 효율 저하
</span></span></span></code></pre></div><p><strong>2. Write Penalty</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">UPDATE</span> users <span style="color:#ff79c6">SET</span> email <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;new@example.com&#39;</span> <span style="color:#ff79c6">WHERE</span> id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">123</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Non-Covering Index:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → users 테이블 업데이트
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → idx_age 인덱스는 email 없으므로 업데이트 불필요
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Covering Index:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → users 테이블 업데이트
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → idx_covering 인덱스도 email 컬럼 업데이트 필요 ⚠️
</span></span></span></code></pre></div><p><strong>권장 사항</strong>:</p>
<table>
  <thead>
      <tr>
          <th>조건</th>
          <th>권장</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>조회 빈도 높음 (90% 이상)</td>
          <td>Covering Index 사용 ✅</td>
      </tr>
      <tr>
          <td>컬럼 크기 작음 (INT, DATE)</td>
          <td>Covering Index 사용 ✅</td>
      </tr>
      <tr>
          <td>업데이트 빈번</td>
          <td>최소 컬럼만 포함 ⚠️</td>
      </tr>
      <tr>
          <td>컬럼 크기 큼 (TEXT, BLOB)</td>
          <td>포함하지 않음 ❌</td>
      </tr>
  </tbody>
</table>
<h3 id="꼬리-질문-2-include-컬럼이란-postgresql-sql-server">꼬리 질문 2: INCLUDE 컬럼이란? (PostgreSQL, SQL Server)</h3>
<p><strong>INCLUDE</strong>는 <strong>인덱스 정렬에는 참여하지 않고, Leaf Node에만 저장</strong>되는 컬럼입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ INCLUDE 사용 (PostgreSQL)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_users_include <span style="color:#ff79c6">ON</span> users(age, status)
</span></span><span style="display:flex;"><span>INCLUDE (name, email);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 동작:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Branch Node: age, status만 저장 (정렬 키)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Leaf Node: age, status, name, email 모두 저장
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 장점:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 1. Branch Node 크기 감소 → 메모리 효율 향상
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 2. Covering Index 효과 유지
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 3. 인덱스 크기 20-40% 감소
</span></span></span></code></pre></div><p><strong>비교</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 일반 복합 인덱스
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_normal <span style="color:#ff79c6">ON</span> users(age, status, name, email);
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Branch/Leaf 모두 4개 컬럼 저장
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스 크기: 500MB
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- INCLUDE 사용
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_include <span style="color:#ff79c6">ON</span> users(age, status) INCLUDE (name, email);
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Branch: 2개 컬럼
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Leaf: 4개 컬럼
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스 크기: 350MB (30% 감소)
</span></span></span></code></pre></div><hr>
<h2 id="q4-index-selectivity-선택도란-무엇인가요">Q4. Index Selectivity (선택도)란 무엇인가요?</h2>
<h3 id="답변-3">답변</h3>
<p>**Index Selectivity (선택도)**는 <strong>인덱스가 얼마나 많은 데이터를 걸러낼 수 있는지</strong>를 나타내는 지표입니다.</p>
<p><strong>계산 공식</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>Selectivity = Distinct 값 개수 / 전체 Row 개수
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>높은 선택도: 1에 가까움 (좋음)
</span></span><span style="display:flex;"><span>낮은 선택도: 0에 가까움 (나쁨)
</span></span></code></pre></div><p><strong>예시</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 테이블: users (100만 건)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 높은 선택도 (인덱스 효과적)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- user_id (Primary Key)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">DISTINCT</span> user_id) <span style="color:#ff79c6">FROM</span> users;  <span style="color:#6272a4">-- 100만 개
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Selectivity = 1,000,000 / 1,000,000 = 1.0 (완벽!)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- email (Unique)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">DISTINCT</span> email) <span style="color:#ff79c6">FROM</span> users;  <span style="color:#6272a4">-- 100만 개
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Selectivity = 1,000,000 / 1,000,000 = 1.0 (완벽!)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ⚠️ 중간 선택도 (상황에 따라)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- city (도시)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">DISTINCT</span> city) <span style="color:#ff79c6">FROM</span> users;  <span style="color:#6272a4">-- 100개
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Selectivity = 100 / 1,000,000 = 0.0001 (낮음)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 낮은 선택도 (인덱스 비효율적)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- gender (성별)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">DISTINCT</span> gender) <span style="color:#ff79c6">FROM</span> users;  <span style="color:#6272a4">-- 2개 (M, F)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Selectivity = 2 / 1,000,000 = 0.000002 (매우 낮음)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- is_active (활성 여부)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">DISTINCT</span> is_active) <span style="color:#ff79c6">FROM</span> users;  <span style="color:#6272a4">-- 2개 (true, false)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Selectivity = 2 / 1,000,000 = 0.000002 (매우 낮음)
</span></span></span></code></pre></div><h3 id="선택도에-따른-인덱스-효과">선택도에 따른 인덱스 효과</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 낮은 선택도 (인덱스 비효율)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_gender <span style="color:#ff79c6">ON</span> users(gender);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> gender <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;M&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 50만 건 반환 (전체의 50%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Full Table Scan이 더 빠를 수 있음! ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 이유:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 1. 인덱스 조회: 50만 번
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 2. 테이블 조회: 50만 번
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 3. Random I/O: 50만 번
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Full Table Scan: 순차 I/O 25,000번이 더 빠름!
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 높은 선택도 (인덱스 효과적)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_email <span style="color:#ff79c6">ON</span> users(email);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> email <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;john@example.com&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 1건 반환 (0.0001%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Index Scan이 훨씬 빠름! ✅
</span></span></span></code></pre></div><h3 id="선택도-개선-전략">선택도 개선 전략</h3>
<p><strong>1. 복합 인덱스로 선택도 향상</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 단일 컬럼 (낮은 선택도)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_status <span style="color:#ff79c6">ON</span> orders(status);
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- status: &#39;PENDING&#39;, &#39;COMPLETED&#39;, &#39;CANCELLED&#39; (3개 값)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Selectivity = 3 / 1,000,000 = 0.000003
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 복합 인덱스 (높은 선택도)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_status_date <span style="color:#ff79c6">ON</span> orders(status, created_at);
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- (status, created_at) 조합: 거의 유니크
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Selectivity ≈ 0.9 (높음!)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> status <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;PENDING&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-01-01&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 복합 인덱스로 선택도 크게 향상 ✅
</span></span></span></code></pre></div><p><strong>2. Partial Index (부분 인덱스)</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 전체 인덱스 (비효율)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_is_active <span style="color:#ff79c6">ON</span> users(is_active);
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- is_active: true (95%), false (5%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> is_active <span style="color:#ff79c6">=</span> <span style="color:#ff79c6">false</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 5만 건 (5%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ Partial Index (PostgreSQL, SQLite)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_inactive_users <span style="color:#ff79c6">ON</span> users(user_id)
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> is_active <span style="color:#ff79c6">=</span> <span style="color:#ff79c6">false</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → is_active = false인 Row만 인덱싱
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스 크기: 95% 감소
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 조회 속도: 동일하거나 더 빠름
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> is_active <span style="color:#ff79c6">=</span> <span style="color:#ff79c6">false</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → idx_inactive_users 사용 ✅
</span></span></span></code></pre></div><h3 id="꼬리-질문-옵티마이저는-언제-인덱스를-사용하지-않나요">꼬리 질문: 옵티마이저는 언제 인덱스를 사용하지 않나요?</h3>
<p><strong>인덱스를 사용하지 않는 경우</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 1. 낮은 선택도 (결과가 전체의 20% 이상)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> gender <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;M&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Full Table Scan 선택
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 2. 함수 사용
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_name <span style="color:#ff79c6">ON</span> users(name);
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> <span style="color:#ff79c6">UPPER</span>(name) <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;JOHN&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스 무효화 ❌
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 함수 기반 인덱스 (Function-based Index)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_upper_name <span style="color:#ff79c6">ON</span> users(<span style="color:#ff79c6">UPPER</span>(name));
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> <span style="color:#ff79c6">UPPER</span>(name) <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;JOHN&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → idx_upper_name 사용 ✅
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 3. LIKE &#39;%keyword&#39; (prefix 아님)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_email <span style="color:#ff79c6">ON</span> users(email);
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> email <span style="color:#ff79c6">LIKE</span> <span style="color:#f1fa8c">&#39;%@gmail.com&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스 무효화 ❌ (prefix가 아님)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> email <span style="color:#ff79c6">LIKE</span> <span style="color:#f1fa8c">&#39;john%&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → idx_email 사용 ✅ (prefix)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 4. OR 조건 (인덱스가 없는 컬럼)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> user_id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">123</span>  <span style="color:#6272a4">-- 인덱스 있음
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>   <span style="color:#ff79c6">OR</span> name <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;John&#39;</span>; <span style="color:#6272a4">-- 인덱스 없음
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Full Table Scan
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 5. 데이터 타입 불일치
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_user_id <span style="color:#ff79c6">ON</span> users(user_id);  <span style="color:#6272a4">-- INT
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> user_id <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;123&#39;</span>;   <span style="color:#6272a4">-- STRING
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 암묵적 형변환으로 인덱스 무효화 ⚠️
</span></span></span></code></pre></div><hr>
<h2 id="q5-실무에서-인덱스-최적화-경험은">Q5. 실무에서 인덱스 최적화 경험은?</h2>
<h3 id="답변-4">답변</h3>
<p><strong>장애 사례 1: 쿼리 실행 시간 30초 → 0.1초</strong></p>
<p><strong>증상</strong>:</p>
<ul>
<li>사용자 검색 API 응답 시간 30초</li>
<li>DB CPU 사용률 90%</li>
</ul>
<p><strong>원인 분석</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 문제 쿼리
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> users
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> city <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;Seoul&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> age <span style="color:#ff79c6">BETWEEN</span> <span style="color:#bd93f9">20</span> <span style="color:#ff79c6">AND</span> <span style="color:#bd93f9">30</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2024-01-01&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">ORDER</span> <span style="color:#ff79c6">BY</span> created_at <span style="color:#ff79c6">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">LIMIT</span> <span style="color:#bd93f9">10</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 실행 계획 확인
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> ...;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 출력:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Seq Scan on users  (cost=0..50000 rows=100000)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">--   Filter: city = &#39;Seoul&#39; AND age &gt;= 20 AND age &lt;= 30
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → Full Table Scan! ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 100만 건 스캔
</span></span></span></code></pre></div><p><strong>해결</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ Covering Index 생성
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_city_age_created_covering <span style="color:#ff79c6">ON</span> users(
</span></span><span style="display:flex;"><span>    city,          <span style="color:#6272a4">-- WHERE 조건 (선택도 중간)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    age,           <span style="color:#6272a4">-- WHERE 조건 (선택도 중간)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    created_at     <span style="color:#6272a4">-- WHERE + ORDER BY
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>) INCLUDE (
</span></span><span style="display:flex;"><span>    user_id, name, email  <span style="color:#6272a4">-- SELECT 컬럼
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 실행 계획 (개선 후)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> ...;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 출력:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Index Only Scan using idx_city_age_created_covering
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">--   (cost=0..100 rows=10)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 인덱스만 사용! ✅
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 1,000건만 스캔 (1,000배 감소)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 성능 결과:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 실행 시간: 30초 → 0.1초 (300배 개선)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- CPU 사용률: 90% → 10%
</span></span></span></code></pre></div><hr>
<p><strong>장애 사례 2: 복합 인덱스 순서 변경으로 성능 10배 향상</strong></p>
<p><strong>증상</strong>:</p>
<ul>
<li>주문 조회 쿼리 느림 (평균 2초)</li>
</ul>
<p><strong>원인</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 기존 인덱스
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_old <span style="color:#ff79c6">ON</span> orders(created_at, status, user_id);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 쿼리 패턴 (80%)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> user_id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">12345</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> status <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;PENDING&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 실행 계획:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Index Scan using idx_old  (cost=0..5000)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">--   Filter: user_id = 12345 AND status = &#39;PENDING&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → created_at 조건이 없어 인덱스 효율 낮음 ⚠️
</span></span></span></code></pre></div><p><strong>분석</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 컬럼별 선택도 분석
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">DISTINCT</span> created_at) <span style="color:#ff79c6">/</span> <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">*</span>) <span style="color:#ff79c6">AS</span> created_selectivity,
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">DISTINCT</span> status) <span style="color:#ff79c6">/</span> <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">*</span>) <span style="color:#ff79c6">AS</span> status_selectivity,
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">DISTINCT</span> user_id) <span style="color:#ff79c6">/</span> <span style="color:#ff79c6">COUNT</span>(<span style="color:#ff79c6">*</span>) <span style="color:#ff79c6">AS</span> user_selectivity
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> orders;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 결과:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- created_selectivity: 0.8 (높음)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- status_selectivity: 0.000005 (매우 낮음, 5개 값)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- user_selectivity: 0.1 (중간)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 쿼리 패턴 분석:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- user_id 조건: 80%
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- status 조건: 90%
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- created_at 조건: 30%
</span></span></span></code></pre></div><p><strong>해결</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 인덱스 순서 변경
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">DROP</span> <span style="color:#ff79c6">INDEX</span> idx_old;
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">INDEX</span> idx_new <span style="color:#ff79c6">ON</span> orders(user_id, status, created_at);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 이유:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 1. user_id: 쿼리에 80% 사용 → 첫 번째
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 2. status: 쿼리에 90% 사용 → 두 번째
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 3. created_at: 쿼리에 30% 사용 → 세 번째 (범위 검색)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 실행 계획 (개선 후):
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- Index Scan using idx_new  (cost=0..50)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">--   Index Cond: user_id = 12345 AND status = &#39;PENDING&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 효율적인 인덱스 사용! ✅
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 성능 결과:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 실행 시간: 2초 → 0.2초 (10배 개선)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 스캔 Row: 10만 건 → 100건 (1,000배 감소)
</span></span></span></code></pre></div><hr>
<p><strong>장애 사례 3: 과도한 인덱스로 INSERT 성능 저하</strong></p>
<p><strong>증상</strong>:</p>
<ul>
<li>주문 생성 API 응답 시간 5초</li>
<li>DB Write 대기 시간 증가</li>
</ul>
<p><strong>원인</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 테이블에 인덱스 15개 존재!
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SHOW</span> <span style="color:#ff79c6">INDEX</span> <span style="color:#ff79c6">FROM</span> orders;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 출력:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_user_id
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_status
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_created_at
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_user_status
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_status_created
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ... (총 15개)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">INSERT</span> <span style="color:#ff79c6">INTO</span> orders <span style="color:#ff79c6">VALUES</span> (...);
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 15개 인덱스 모두 업데이트 필요! ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → 실행 시간: 5초
</span></span></span></code></pre></div><p><strong>분석</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 인덱스 사용 통계 확인 (PostgreSQL)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span>
</span></span><span style="display:flex;"><span>    schemaname,
</span></span><span style="display:flex;"><span>    tablename,
</span></span><span style="display:flex;"><span>    indexname,
</span></span><span style="display:flex;"><span>    idx_scan,  <span style="color:#6272a4">-- 인덱스 스캔 횟수
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    idx_tup_read
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> pg_stat_user_indexes
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> tablename <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;orders&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">ORDER</span> <span style="color:#ff79c6">BY</span> idx_scan <span style="color:#ff79c6">ASC</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 결과:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_user_id: 1,000,000 (활발히 사용)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_status_created: 500,000 (활발히 사용)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_unused_1: 0 (미사용!) ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_unused_2: 10 (거의 미사용) ⚠️
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ...
</span></span></span></code></pre></div><p><strong>해결</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 미사용 인덱스 제거
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">DROP</span> <span style="color:#ff79c6">INDEX</span> idx_unused_1;
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">DROP</span> <span style="color:#ff79c6">INDEX</span> idx_unused_2;
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">DROP</span> <span style="color:#ff79c6">INDEX</span> idx_unused_3;
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ... (7개 제거)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 중복 인덱스 통합
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 기존:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_user_id (user_id)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- idx_user_status (user_id, status)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- → idx_user_id는 불필요! (idx_user_status로 커버 가능)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">DROP</span> <span style="color:#ff79c6">INDEX</span> idx_user_id;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 최종: 15개 → 5개 인덱스만 유지
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- 성능 결과:
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- INSERT 시간: 5초 → 0.5초 (10배 개선)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">-- DB Write 대기: 80% 감소
</span></span></span></code></pre></div><hr>
<h2 id="요약">요약</h2>
<h3 id="b-tree-인덱스">B-Tree 인덱스</h3>
<ul>
<li><strong>구조</strong>: Root → Branch → Leaf (Balanced Tree)</li>
<li><strong>시간 복잡도</strong>: 검색/삽입/삭제 O(log N)</li>
<li><strong>Clustered vs Non-Clustered</strong>: PK는 Clustered, 나머지는 Non-Clustered</li>
</ul>
<h3 id="복합-인덱스-설계">복합 인덱스 설계</h3>
<ul>
<li><strong>컬럼 순서</strong>: 선택도 높은 컬럼 우선, 범위 검색은 마지막</li>
<li><strong>Leftmost Prefix</strong>: 첫 컬럼부터 순서대로 사용해야 인덱스 활용</li>
<li><strong>쿼리 패턴</strong>: 자주 사용되는 조건 조합을 복합 인덱스로</li>
</ul>
<h3 id="covering-index">Covering Index</h3>
<ul>
<li><strong>정의</strong>: 쿼리에 필요한 모든 컬럼을 인덱스에 포함</li>
<li><strong>효과</strong>: 테이블 접근 없이 인덱스만으로 결과 반환</li>
<li><strong>주의</strong>: 인덱스 크기 증가, Write Penalty</li>
</ul>
<h3 id="index-selectivity">Index Selectivity</h3>
<ul>
<li><strong>계산</strong>: Distinct 값 / 전체 Row (1에 가까울수록 좋음)</li>
<li><strong>높은 선택도</strong>: PK, Unique (인덱스 효과적)</li>
<li><strong>낮은 선택도</strong>: Gender, Boolean (인덱스 비효율적)</li>
</ul>
<h3 id="실무-최적화">실무 최적화</h3>
<ul>
<li><strong>실행 계획</strong>: EXPLAIN ANALYZE로 인덱스 사용 확인</li>
<li><strong>통계 분석</strong>: 컬럼별 선택도, 쿼리 패턴 분석</li>
<li><strong>인덱스 정리</strong>: 미사용 인덱스 제거, 중복 인덱스 통합</li>
</ul>
<hr>
<h2 id="-related-deep-dive">🔗 Related Deep Dive</h2>
<p>더 깊이 있는 학습을 원한다면 심화 과정을 참고하세요:</p>
<ul>
<li><strong><a href="/learning/deep-dive/deep-dive-database-indexing/">인덱스 기본</a></strong>: B-Tree 구조와 쿼리 성능 시각화.</li>
<li><strong><a href="/learning/deep-dive/deep-dive-database-engines-lsm/">스토리지 엔진 내부</a></strong>: B-Tree vs LSM-Tree 비교.</li>
<li><strong><a href="/learning/deep-dive/deep-dive-mysql-isolation-locks/">MySQL 격리 수준과 락</a></strong>: 인덱스 락과 데드락 문제.</li>
</ul>
]]></content:encoded></item><item><title>DB 스키마 설계 기본기: 키/제약조건/정규화</title><link>https://jyukki.com/learning/deep-dive/deep-dive-database-schema-design-basics/</link><pubDate>Sat, 15 Nov 2025 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-database-schema-design-basics/</guid><description>PK/UK/FK, 제약조건, 정규화/비정규화, 변경에 강한 스키마 설계 기준을 실무 관점으로 정리</description><content:encoded><![CDATA[<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>요구사항을 테이블/컬럼으로 옮길 때, “감”이 아니라 <strong>키/제약조건/조회 패턴</strong>으로 설계할 수 있습니다.</li>
<li>PK/UK/FK/NOT NULL 같은 제약을 언제 강하게 걸고, 언제 완화해야 하는지 트레이드오프가 생깁니다.</li>
<li>정규화/비정규화를 “이론”이 아니라 <strong>변경 비용 vs 조회 비용</strong> 관점으로 선택할 수 있습니다.</li>
<li>운영 중 스키마가 바뀔 때(마이그레이션) “서비스를 안 깨뜨리는 변경” 감각을 잡습니다.</li>
</ul>
<h2 id="0-스키마-설계는-성능보다-변경-비용을-결정한다">0) 스키마 설계는 ‘성능’보다 ‘변경 비용’을 결정한다</h2>
<p>스키마는 한 번 정하면 끝이 아니라, 기능이 늘고 트래픽이 커지면서 계속 변합니다.
문제는 <strong>변경이 어려운 스키마</strong>가 되면 다음이 연쇄적으로 터진다는 점입니다.</p>
<ul>
<li>컬럼 의미가 불명확해져 데이터 품질이 무너짐(정합성)</li>
<li>인덱스를 붙여도 느림(조회 패턴과 안 맞음)</li>
<li>마이그레이션이 위험해져 배포가 두려워짐</li>
</ul>
<p>그래서 스키마 설계의 핵심 질문은 “정답 테이블”이 아니라,
<strong>이 데이터는 누가 소유하고, 어떻게 변하며, 어떻게 조회되는가</strong>입니다.</p>
<h2 id="1-요구사항을-데이터로-바꿀-때-먼저-정리할-것">1) 요구사항을 데이터로 바꿀 때 먼저 정리할 것</h2>
<h3 id="1-1-엔티티의-수명주기">1-1) 엔티티의 수명주기</h3>
<ul>
<li>생성/수정/삭제(또는 만료) 흐름이 명확한가?</li>
<li>삭제가 진짜 삭제인가? (보관/감사 로그가 필요한가?)</li>
<li>상태(state)가 있다면 상태 전이 규칙이 명확한가?</li>
</ul>
<h3 id="1-2-조회-패턴가장-중요">1-2) 조회 패턴(가장 중요)</h3>
<p>“조회 패턴”이 스키마와 인덱스를 결정합니다.</p>
<ul>
<li>목록 API는 어떤 필터/정렬로 조회하는가?</li>
<li>상세 조회는 어떤 연관 데이터를 같이 보여주는가?</li>
<li>집계가 필요한가? (일/주/월 통계, Top-K 등)</li>
</ul>
<h2 id="2-키-설계-pk는-정체성이고-인덱스의-출발점이다">2) 키 설계: PK는 ‘정체성’이고, 인덱스의 출발점이다</h2>
<h3 id="2-1-자연키-vs-대리키surrogate">2-1) 자연키 vs 대리키(surrogate)</h3>
<ul>
<li>
<p><strong>자연키(자연스러운 의미가 있는 키)</strong>: 예) 이메일, 전화번호</p>
<ul>
<li>장점: 의미가 명확</li>
<li>단점: 변경 가능성/길이/개인정보/정규화(대소문자, 공백) 같은 현실 문제가 많음</li>
</ul>
</li>
<li>
<p><strong>대리키(의미 없는 식별자)</strong>: 예) bigint auto increment, snowflake, UUID</p>
<ul>
<li>장점: 변경에 강하고 FK로 쓰기 편함</li>
<li>단점: “자연스러운 유니크”는 별도 제약(UK)으로 관리해야 함</li>
</ul>
</li>
</ul>
<p>실무에서 흔한 선택:</p>
<ul>
<li>PK는 대리키(bigint/snowflake)를 두고</li>
<li>이메일 같은 자연키는 <code>UNIQUE</code>로 “중복 금지”를 따로 걸어줍니다.</li>
</ul>
<h3 id="2-2-uuid를-pk로-쓸-때-체크-포인트">2-2) UUID를 PK로 쓸 때 체크 포인트</h3>
<p>UUID는 편하지만, (특히 B-Tree에서) 랜덤성이 커서 인덱스 단편화/쓰기 비용이 커질 수 있습니다.</p>
<ul>
<li>트래픽/쓰기량이 크면 “정렬 가능한 ID”(ULID/UUIDv7 등)나 snowflake 계열을 고려</li>
<li>정말 UUID가 필요한지(외부 노출/통합 필요) 기준을 먼저 정합니다</li>
</ul>
<h2 id="3-제약조건-db가-마지막-방어선이-되어야-한다">3) 제약조건: DB가 ‘마지막 방어선’이 되어야 한다</h2>
<p>애플리케이션 검증만 믿고 DB 제약을 약하게 두면, 결국 데이터 품질이 무너집니다.</p>
<h3 id="3-1-not-null">3-1) NOT NULL</h3>
<ul>
<li>“없으면 의미가 없다”면 NOT NULL이 기본입니다.</li>
<li>NULL을 허용하면, 이후 모든 쿼리/코드가 “NULL 처리”를 떠안습니다.</li>
</ul>
<h3 id="3-2-unique">3-2) UNIQUE</h3>
<p>중복이 비즈니스 버그가 되는 값은 DB에서 막는 편이 안전합니다.</p>
<ul>
<li>예: 이메일, 주문번호, 외부 결제 트랜잭션 키 등</li>
</ul>
<h3 id="3-3-foreign-keyfk">3-3) Foreign Key(FK)</h3>
<p>FK는 데이터 정합성을 강하게 만들지만, 쓰기 경합/락/운영 복잡도가 늘 수 있습니다.</p>
<p>실무 기준(요약):</p>
<ul>
<li>단일 DB, 변경 빈도가 낮고 정합성이 최우선 → FK를 적극적으로 사용</li>
<li>고트래픽/대규모, 샤딩/멀티테넌시/서비스 분리 예정 → FK를 약하게(또는 애플리케이션 레벨) 가져가는 경우도 많음</li>
</ul>
<p>중요: FK를 안 쓰더라도 “참조 무결성”은 반드시 다른 방식으로 보장해야 합니다(정리 배치, 소유권 규칙, 삭제 정책).</p>
<h2 id="4-정규화-vs-비정규화-조회-비용과-변경-비용의-교환">4) 정규화 vs 비정규화: ‘조회 비용’과 ‘변경 비용’의 교환</h2>
<h3 id="4-1-정규화가-주는-것">4-1) 정규화가 주는 것</h3>
<ul>
<li>중복이 줄고, 업데이트가 단일 지점에서 일어납니다(변경 비용 ↓).</li>
<li>대신 조회 시 조인이 늘고, “읽기 성능”이 문제로 보일 수 있습니다.</li>
</ul>
<h3 id="4-2-비정규화가-필요한-순간">4-2) 비정규화가 필요한 순간</h3>
<p>비정규화는 “조인이 싫어서”가 아니라,
<strong>읽기 경로가 병목이고, 일관성 요구가 허용 가능한 범위</strong>일 때 의미가 있습니다.</p>
<p>예시:</p>
<ul>
<li>목록 화면에 항상 필요한 요약 필드(예: 댓글 수, 좋아요 수)</li>
<li>통계/랭킹 같은 파생 데이터(원본과 분리된 읽기 모델)</li>
</ul>
<p>비정규화를 할 때 반드시 정해야 하는 것:</p>
<ul>
<li>원본(source of truth)은 어디인가?</li>
<li>파생 데이터는 언제/어떻게 갱신되는가? (동기/비동기, 재계산 전략)</li>
<li>갱신 실패 시 어떻게 복구하는가? (재처리, 배치 리빌드)</li>
</ul>
<h2 id="5-인덱스는-스키마-설계의-후속이다">5) 인덱스는 스키마 설계의 ‘후속’이다</h2>
<p>인덱스는 스키마를 “좋게” 만들지 않습니다.<br>
조회 패턴과 조건이 명확할 때, 인덱스가 성능을 끌어올립니다.</p>
<ul>
<li>어떤 컬럼이 WHERE/ORDER BY에 자주 등장하는가?</li>
<li>카디널리티(분포)가 어떤가?</li>
<li>복합 인덱스의 컬럼 순서는 어떻게 잡을 것인가?</li>
</ul>
<p>인덱스 자체는 다음 글에서 더 깊게 다룹니다.</p>
<h2 id="6-운영-중-스키마-변경마이그레이션-기본-원칙">6) 운영 중 스키마 변경(마이그레이션) 기본 원칙</h2>
<p>스키마는 “한 번에 바꾸는 것”이 아니라, 보통 2~3단계로 나눕니다(확장 → 전환 → 정리).</p>
<p>예시(컬럼 이름 변경):</p>
<ol>
<li>새 컬럼 추가(확장) + 쓰기 시 둘 다 채움</li>
<li>읽기를 새 컬럼으로 전환(배포)</li>
<li>안정화 후 구 컬럼 제거(정리)</li>
</ol>
<p>핵심은 “구버전/신버전이 잠시 공존해도 동작”하도록 만드는 것입니다.</p>
<h2 id="연습추천">연습(추천)</h2>
<ul>
<li>“주문/결제/배송” 도메인을 가정하고, 테이블 3~5개를 잡아 PK/UK/FK/NOT NULL을 설계해보기</li>
<li>목록 API 2개를 정하고(예: 주문 목록, 결제 내역), WHERE/ORDER BY를 기반으로 “필요한 인덱스 후보”를 적어보기</li>
<li>컬럼 추가/삭제/이름 변경을 ‘확장→전환→정리’로 나눠 배포 시나리오를 써보기</li>
</ul>
<h2 id="연결해서-읽기">연결해서 읽기</h2>
<ul>
<li>인덱스 기본: <code>/learning/deep-dive/deep-dive-database-indexing/</code></li>
<li>EXPLAIN 읽는 법: <code>/learning/deep-dive/deep-dive-mysql-index-explain/</code></li>
<li>트랜잭션 경계/JPA 감각: <code>/learning/deep-dive/deep-dive-jpa-transaction-boundaries/</code></li>
</ul>
]]></content:encoded></item><item><title>MySQL Index 성능 비교: B-Tree vs Hash vs Fulltext</title><link>https://jyukki.com/posts/2025-03-15-4-mysql-index/</link><pubDate>Sat, 15 Mar 2025 00:00:00 +0000</pubDate><guid>https://jyukki.com/posts/2025-03-15-4-mysql-index/</guid><description>MySQL의 다양한 인덱스 타입(B-Tree, Hash, Fulltext)을 실제 테스트를 통해 성능 비교해보자.</description><content:encoded><![CDATA[<h2 id="들어가며">들어가며</h2>
<blockquote>
<p>평소에 귀찮아서 B-Tree 인덱스만 사용했는데, 다른 인덱스 타입들도 궁금해졌다.<br>
Hash Index와 Fulltext Index는 어떤 상황에서 유리할까? 직접 테스트해보자!</p></blockquote>
<h2 id="테스트-환경-설정">테스트 환경 설정</h2>
<h3 id="테스트-테이블-생성">테스트 테이블 생성</h3>
<p>다양한 인덱스 타입을 테스트하기 위한 테이블을 생성했다:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> test_index (
</span></span><span style="display:flex;"><span>    id <span style="color:#8be9fd;font-style:italic">INT</span> AUTO_INCREMENT <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>,
</span></span><span style="display:flex;"><span>    created_at DATETIME <span style="color:#ff79c6">DEFAULT</span> <span style="color:#ff79c6">CURRENT_TIMESTAMP</span>, <span style="color:#6272a4">-- B-Tree 테스트용
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    category <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">50</span>), <span style="color:#6272a4">-- Hash Index 테스트용  
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>    description <span style="color:#8be9fd;font-style:italic">TEXT</span>, <span style="color:#6272a4">-- Fulltext Index 테스트용
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">INDEX</span> idx_btree_created_at (created_at),
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">INDEX</span> idx_hash_category <span style="color:#ff79c6">USING</span> HASH (category),
</span></span><span style="display:flex;"><span>    FULLTEXT <span style="color:#ff79c6">INDEX</span> idx_fulltext_description (description)
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><h3 id="테스트-데이터-생성">테스트 데이터 생성</h3>
<p>10만 건의 랜덤 데이터를 생성했다:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">INSERT</span> <span style="color:#ff79c6">INTO</span> test_index (created_at, category, description)
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span>
</span></span><span style="display:flex;"><span>    NOW() <span style="color:#ff79c6">-</span> <span style="color:#8be9fd;font-style:italic">INTERVAL</span> FLOOR(RAND() <span style="color:#ff79c6">*</span> <span style="color:#bd93f9">365</span>) <span style="color:#ff79c6">DAY</span>,
</span></span><span style="display:flex;"><span>    ELT(FLOOR(<span style="color:#bd93f9">1</span> <span style="color:#ff79c6">+</span> (RAND() <span style="color:#ff79c6">*</span> <span style="color:#bd93f9">3</span>)), <span style="color:#f1fa8c">&#39;A&#39;</span>, <span style="color:#f1fa8c">&#39;B&#39;</span>, <span style="color:#f1fa8c">&#39;C&#39;</span>),
</span></span><span style="display:flex;"><span>    CONCAT(
</span></span><span style="display:flex;"><span>        <span style="color:#f1fa8c">&#39;This is a sample description &#39;</span>,
</span></span><span style="display:flex;"><span>        FLOOR(RAND() <span style="color:#ff79c6">*</span> <span style="color:#bd93f9">1000</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#f1fa8c">&#39; with random content&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> information_schema.tables <span style="color:#ff79c6">AS</span> a
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">CROSS</span> <span style="color:#ff79c6">JOIN</span> information_schema.tables <span style="color:#ff79c6">AS</span> b
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">LIMIT</span> <span style="color:#bd93f9">100000</span>;
</span></span></code></pre></div><h2 id="b-tree-인덱스-테스트">B-Tree 인덱스 테스트</h2>
<h3 id="범위-조회-성능">범위 조회 성능</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> created_at <span style="color:#ff79c6">BETWEEN</span> <span style="color:#f1fa8c">&#39;2024-10-01&#39;</span> <span style="color:#ff79c6">AND</span> <span style="color:#f1fa8c">&#39;2024-11-01&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Index</span> range scan <span style="color:#ff79c6">on</span> test_index <span style="color:#ff79c6">using</span> idx_btree_created_at over (<span style="color:#f1fa8c">&#39;2024-10-01 00:00:00&#39;</span> <span style="color:#ff79c6">&lt;=</span> created_at <span style="color:#ff79c6">&lt;=</span> <span style="color:#f1fa8c">&#39;2024-11-01 00:00:00&#39;</span>), <span style="color:#ff79c6">with</span> <span style="color:#ff79c6">index</span> condition: (test_index.created_at <span style="color:#ff79c6">between</span> <span style="color:#f1fa8c">&#39;2024-10-01&#39;</span> <span style="color:#ff79c6">and</span> <span style="color:#f1fa8c">&#39;2024-11-01&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">3824</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">8498</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">19</span>..<span style="color:#bd93f9">26</span>.<span style="color:#bd93f9">6</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">8498</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">IGNORE</span> <span style="color:#ff79c6">INDEX</span>(idx_btree_created_at)      <span style="color:#6272a4">-- B-Tree 인덱스 제거
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> created_at <span style="color:#ff79c6">BETWEEN</span> <span style="color:#f1fa8c">&#39;2024-10-01&#39;</span> <span style="color:#ff79c6">AND</span> <span style="color:#f1fa8c">&#39;2024-11-01&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> Filter: (test_index.created_at <span style="color:#ff79c6">between</span> <span style="color:#f1fa8c">&#39;2024-10-01&#39;</span> <span style="color:#ff79c6">and</span> <span style="color:#f1fa8c">&#39;2024-11-01&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">11054</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0711</span>..<span style="color:#bd93f9">66</span>.<span style="color:#bd93f9">4</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">8498</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> test_index  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">99495</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0548</span>..<span style="color:#bd93f9">42</span>.<span style="color:#bd93f9">1</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="검색-조회-">검색 조회 (=)</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> created_at<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;2025-01-01 06:53:52&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Index</span> lookup <span style="color:#ff79c6">on</span> test_index <span style="color:#ff79c6">using</span> idx_btree_created_at (created_at<span style="color:#ff79c6">=</span><span style="color:#ff79c6">TIMESTAMP</span><span style="color:#f1fa8c">&#39;2025-01-01 06:53:52&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">101</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">288</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0881</span>..<span style="color:#bd93f9">1</span>.<span style="color:#bd93f9">49</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">288</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">IGNORE</span> <span style="color:#ff79c6">INDEX</span>(idx_btree_created_at)      <span style="color:#6272a4">-- B-Tree 인덱스 제거
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> created_at<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;2025-01-01 06:53:52&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">|</span> <span style="color:#ff79c6">-&gt;</span> Filter: (test_index.created_at <span style="color:#ff79c6">=</span> <span style="color:#ff79c6">TIMESTAMP</span><span style="color:#f1fa8c">&#39;2025-01-01 06:53:52&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">270</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">239</span>..<span style="color:#bd93f9">45</span>.<span style="color:#bd93f9">8</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">288</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> test_index  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">99495</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">03</span>..<span style="color:#bd93f9">39</span>.<span style="color:#bd93f9">5</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="결과">결과</h5>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>B-Tree</th>
          <th>인덱스 X</th>
          <th>차이점</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>범위 조회</strong></td>
          <td>26.6ms</td>
          <td>66.4ms</td>
          <td>2.5배 차이</td>
      </tr>
      <tr>
          <td><strong>검색 조회</strong></td>
          <td>1.49ms</td>
          <td>45.8ms</td>
          <td>30배 차이</td>
      </tr>
  </tbody>
</table>
<ul>
<li>확실히 빠르긴하다..</li>
<li>범위 조회 효율이 더 잘나올 줄 알았는데, 검색 효율도 나쁘지않다</li>
<li>아닌가? 데이터가 늘면 늘수록 인덱스 없는 범위 조회 시간은 많이 걸릴라나?</li>
</ul>
<h2 id="2-hash-index-테스트">2. Hash Index 테스트</h2>
<h5 id="범위-조회-between">범위 조회 (BETWEEN)</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> category <span style="color:#ff79c6">BETWEEN</span> <span style="color:#f1fa8c">&#39;A&#39;</span> <span style="color:#ff79c6">AND</span> <span style="color:#f1fa8c">&#39;B&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> Filter: (test_index.category <span style="color:#ff79c6">between</span> <span style="color:#f1fa8c">&#39;A&#39;</span> <span style="color:#ff79c6">and</span> <span style="color:#f1fa8c">&#39;B&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">49747</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0455</span>..<span style="color:#bd93f9">53</span>.<span style="color:#bd93f9">5</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">66775</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> test_index  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">99495</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0401</span>..<span style="color:#bd93f9">41</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">IGNORE</span> <span style="color:#ff79c6">INDEX</span>(idx_hash_category)     <span style="color:#6272a4">-- Hash 인덱스 제거
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> category <span style="color:#ff79c6">BETWEEN</span> <span style="color:#f1fa8c">&#39;A&#39;</span> <span style="color:#ff79c6">AND</span> <span style="color:#f1fa8c">&#39;B&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">|</span> <span style="color:#ff79c6">-&gt;</span> Filter: (test_index.category <span style="color:#ff79c6">between</span> <span style="color:#f1fa8c">&#39;A&#39;</span> <span style="color:#ff79c6">and</span> <span style="color:#f1fa8c">&#39;B&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">11054</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0491</span>..<span style="color:#bd93f9">53</span>.<span style="color:#bd93f9">9</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">66775</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> test_index  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">99495</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">045</span>..<span style="color:#bd93f9">41</span>.<span style="color:#bd93f9">2</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="검색-조회--1">검색 조회 (=)</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> category<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;B&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Index</span> lookup <span style="color:#ff79c6">on</span> test_index <span style="color:#ff79c6">using</span> idx_hash_category (category<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;B&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">5431</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">49747</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0495</span>..<span style="color:#bd93f9">47</span>.<span style="color:#bd93f9">8</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">33477</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">IGNORE</span> <span style="color:#ff79c6">INDEX</span>(idx_hash_category)     <span style="color:#6272a4">-- Hash 인덱스 제거 
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> category<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;B&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> Filter: (test_index.category <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;B&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">49748</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0344</span>..<span style="color:#bd93f9">25</span>.<span style="color:#bd93f9">7</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">33477</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> test_index  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">99495</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0323</span>..<span style="color:#bd93f9">21</span>.<span style="color:#bd93f9">2</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="결과-1">결과</h5>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>Hash 인덱스</th>
          <th>인덱스 X</th>
          <th>차이점</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>범위 조회</strong></td>
          <td>53.5ms</td>
          <td>53.9ms</td>
          <td>X</td>
      </tr>
      <tr>
          <td><strong>검색 조회</strong></td>
          <td>47.8ms</td>
          <td>25.7ms</td>
          <td>2배 차이</td>
      </tr>
  </tbody>
</table>
<ul>
<li>????? 검색 했는데 왜 Hash 인덱스가 더 느리지?..</li>
<li>범위 조회가 안되는 대신 검색 조회 효율을 높이기 위해 사용하는 것 아니었나??</li>
<li>흐음&hellip;.. 데이터가 A, B, C 뿐이라 해시 효율이 안나오나? 데이터를 바꿔보자</li>
</ul>
<h3 id="2-1-hash-index-테스트-데이터-변경">2-1 Hash Index 테스트: 데이터 변경</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> hash_test (
</span></span><span style="display:flex;"><span>    id <span style="color:#8be9fd;font-style:italic">INT</span> AUTO_INCREMENT <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>,
</span></span><span style="display:flex;"><span>    uuid <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">36</span>),
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">INDEX</span> idx_hash_uuid <span style="color:#ff79c6">USING</span> HASH (uuid)
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">INSERT</span> <span style="color:#ff79c6">INTO</span> hash_test (uuid)
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> 
</span></span><span style="display:flex;"><span>    UUID()
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> information_schema.tables <span style="color:#ff79c6">AS</span> a
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">CROSS</span> <span style="color:#ff79c6">JOIN</span> information_schema.tables <span style="color:#ff79c6">AS</span> b
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">LIMIT</span> <span style="color:#bd93f9">100000</span>;
</span></span></code></pre></div><h5 id="검색-조회--2">검색 조회 (=)</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> hash_test  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> uuid<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;eaeaf92b-fb22-11ef-9745-0242ac110002&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> Covering <span style="color:#ff79c6">index</span> lookup <span style="color:#ff79c6">on</span> hash_test <span style="color:#ff79c6">using</span> idx_hash_uuid (uuid<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;eaeaf92b-fb22-11ef-9745-0242ac110002&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">35</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0349</span>..<span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0403</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> hash_test 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">IGNORE</span> <span style="color:#ff79c6">INDEX</span>(idx_hash_uuid)       <span style="color:#6272a4">-- Hash 인덱스 제거 
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> uuid<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;eaeaf92b-fb22-11ef-9745-0242ac110002&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> Filter: (hash_test.uuid <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;eaeaf92b-fb22-11ef-9745-0242ac110002&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10079</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">16</span>..<span style="color:#bd93f9">34</span>.<span style="color:#bd93f9">3</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> hash_test  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10079</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">99750</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">303</span>..<span style="color:#bd93f9">23</span>.<span style="color:#bd93f9">4</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="결과-2">결과</h5>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>Hash 인덱스</th>
          <th>인덱스 X</th>
          <th>차이점</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>검색 조회</strong></td>
          <td>0.0403ms</td>
          <td>34.3ms</td>
          <td>800배 차이</td>
      </tr>
  </tbody>
</table>
<ul>
<li>이게 옳게된 결과지</li>
<li>흐음.. 데이터에 따라 인덱스를 고르는 것도 신중히 해야할 듯</li>
<li>같은 값이 많으면 Hash 인덱스의 성능이 떨어진다!</li>
</ul>
<h2 id="3-fulltext-index-테스트">3. Fulltext Index 테스트</h2>
<h5 id="검색-조회-match">검색 조회 (MATCH)</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> <span style="color:#ff79c6">MATCH</span>(description) AGAINST(<span style="color:#f1fa8c">&#39;random&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> Filter: (<span style="color:#ff79c6">match</span> test_index.<span style="color:#ff79c6">`</span>description<span style="color:#ff79c6">`</span> against (<span style="color:#f1fa8c">&#39;random&#39;</span>))  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">35</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">15</span>.<span style="color:#bd93f9">7</span>..<span style="color:#bd93f9">89</span>.<span style="color:#bd93f9">4</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Full</span><span style="color:#ff79c6">-</span><span style="color:#8be9fd;font-style:italic">text</span> <span style="color:#ff79c6">index</span> <span style="color:#ff79c6">search</span> <span style="color:#ff79c6">on</span> test_index <span style="color:#ff79c6">using</span> idx_fulltext_description (description<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;random&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">35</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">15</span>.<span style="color:#bd93f9">7</span>..<span style="color:#bd93f9">85</span>.<span style="color:#bd93f9">6</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">IGNORE</span> <span style="color:#ff79c6">INDEX</span>(idx_fulltext_description)      <span style="color:#6272a4">-- Fulltext 인덱스 제거
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> <span style="color:#ff79c6">MATCH</span>(description) AGAINST(<span style="color:#f1fa8c">&#39;random&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> Filter: (<span style="color:#ff79c6">match</span> test_index.<span style="color:#ff79c6">`</span>description<span style="color:#ff79c6">`</span> against (<span style="color:#f1fa8c">&#39;random&#39;</span>))  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">11054</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">571</span>..<span style="color:#bd93f9">27</span>.<span style="color:#bd93f9">3</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> test_index  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">99495</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">568</span>..<span style="color:#bd93f9">19</span>.<span style="color:#bd93f9">5</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="검색-조회-like">검색 조회 (LIKE)</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> description <span style="color:#ff79c6">LIKE</span> <span style="color:#f1fa8c">&#39;%random%&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> Filter: (test_index.<span style="color:#ff79c6">`</span>description<span style="color:#ff79c6">`</span> <span style="color:#ff79c6">like</span> <span style="color:#f1fa8c">&#39;%random%&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">11054</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0473</span>..<span style="color:#bd93f9">62</span>.<span style="color:#bd93f9">9</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> test_index  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">99495</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0422</span>..<span style="color:#bd93f9">32</span>.<span style="color:#bd93f9">8</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>  
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> test_index 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">IGNORE</span> <span style="color:#ff79c6">INDEX</span>(idx_fulltext_description)       <span style="color:#6272a4">-- Fulltext 인덱스 제거
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">WHERE</span> description <span style="color:#ff79c6">LIKE</span> <span style="color:#f1fa8c">&#39;%random%&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> Filter: (test_index.<span style="color:#ff79c6">`</span>description<span style="color:#ff79c6">`</span> <span style="color:#ff79c6">like</span> <span style="color:#f1fa8c">&#39;%random%&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">11054</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">052</span>..<span style="color:#bd93f9">70</span>.<span style="color:#bd93f9">5</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> test_index  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">10102</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">99495</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0452</span>..<span style="color:#bd93f9">36</span>.<span style="color:#bd93f9">9</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">100000</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="결과-3">결과</h5>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>Fulltext 인덱스</th>
          <th>인덱스 X</th>
          <th>차이점</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>검색 조회 (MATCH)</strong></td>
          <td>89.4ms</td>
          <td>27.3ms</td>
          <td>3배 차이</td>
      </tr>
      <tr>
          <td><strong>검색 조회 (LIKE)</strong></td>
          <td>62.9ms</td>
          <td>70.5ms</td>
          <td>X</td>
      </tr>
  </tbody>
</table>
<ul>
<li>일단 LIKE 검색은 Fulltext 인덱스가 안먹는구나 확인</li>
<li>MATCH 검색은 왜 또 인덱스 뺀게 더 빠를까&hellip;.</li>
<li>으음&hellip; 또 데이터 문제일까? random 이라는 문구가 모든 row 에 존재해서 사실상 테이블 스캔이랑 동일하게 전체 검사를 하게 된건가?..</li>
</ul>
<h3 id="3-1-fulltext-index-테스트-데이터-변경">3-1. Fulltext Index 테스트: 데이터 변경</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> fulltext_test ( 
</span></span><span style="display:flex;"><span>    id <span style="color:#8be9fd;font-style:italic">INT</span> AUTO_INCREMENT <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span>, 
</span></span><span style="display:flex;"><span>    description <span style="color:#8be9fd;font-style:italic">TEXT</span>, 
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    FULLTEXT <span style="color:#ff79c6">INDEX</span> idx_fulltext_description (description)
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">INSERT</span> <span style="color:#ff79c6">INTO</span> fulltext_test (description)
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> CONCAT(
</span></span><span style="display:flex;"><span>        ELT(FLOOR(<span style="color:#bd93f9">1</span> <span style="color:#ff79c6">+</span> (RAND() <span style="color:#ff79c6">*</span> <span style="color:#bd93f9">5</span>)),
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;AI&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;Blockchain&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;Quantum Computing&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;Cybersecurity&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;Cloud Computing&#39;</span>
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>        <span style="color:#f1fa8c">&#39; is transforming the world of &#39;</span>,
</span></span><span style="display:flex;"><span>        ELT(FLOOR(<span style="color:#bd93f9">1</span> <span style="color:#ff79c6">+</span> (RAND() <span style="color:#ff79c6">*</span> <span style="color:#bd93f9">5</span>)),
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;finance&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;healthcare&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;automotive&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;retail&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">&#39;education&#39;</span>
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>        <span style="color:#f1fa8c">&#39;. Experts predict it will impact over &#39;</span>,
</span></span><span style="display:flex;"><span>        FLOOR(<span style="color:#bd93f9">10</span> <span style="color:#ff79c6">+</span> (RAND() <span style="color:#ff79c6">*</span> <span style="color:#bd93f9">90</span>)),
</span></span><span style="display:flex;"><span>        <span style="color:#f1fa8c">&#39; million users by &#39;</span>,
</span></span><span style="display:flex;"><span>        FLOOR(<span style="color:#bd93f9">2025</span> <span style="color:#ff79c6">+</span> (RAND() <span style="color:#ff79c6">*</span> <span style="color:#bd93f9">10</span>)),
</span></span><span style="display:flex;"><span>        <span style="color:#f1fa8c">&#39;.&#39;</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> information_schema.tables <span style="color:#ff79c6">AS</span> a
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">CROSS</span> <span style="color:#ff79c6">JOIN</span> information_schema.tables <span style="color:#ff79c6">AS</span> b
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">LIMIT</span> <span style="color:#bd93f9">1000000</span>;
</span></span></code></pre></div><h5 id="검색-조회-match-1">검색 조회 (MATCH)</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> fulltext_test 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> <span style="color:#ff79c6">MATCH</span>(description) AGAINST(<span style="color:#f1fa8c">&#39;Blockchain&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff79c6">-&gt;</span> Filter: (<span style="color:#ff79c6">match</span> fulltext_test.<span style="color:#ff79c6">`</span>description<span style="color:#ff79c6">`</span> against (<span style="color:#f1fa8c">&#39;Blockchain&#39;</span>))  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">35</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">5</span>.<span style="color:#bd93f9">64</span>..<span style="color:#bd93f9">35</span>.<span style="color:#bd93f9">9</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">22668</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Full</span><span style="color:#ff79c6">-</span><span style="color:#8be9fd;font-style:italic">text</span> <span style="color:#ff79c6">index</span> <span style="color:#ff79c6">search</span> <span style="color:#ff79c6">on</span> fulltext_test <span style="color:#ff79c6">using</span> idx_fulltext_description (description<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;Blockchain&#39;</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">35</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">5</span>.<span style="color:#bd93f9">63</span>..<span style="color:#bd93f9">34</span>.<span style="color:#bd93f9">3</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">22668</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span>   
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> fulltext_test 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">IGNORE</span> <span style="color:#ff79c6">INDEX</span>(idx_fulltext_description) 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> <span style="color:#ff79c6">MATCH</span>(description) AGAINST(<span style="color:#f1fa8c">&#39;Blockchain&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff79c6">-&gt;</span> Filter: (<span style="color:#ff79c6">match</span> fulltext_test.<span style="color:#ff79c6">`</span>description<span style="color:#ff79c6">`</span> against (<span style="color:#f1fa8c">&#39;Blockchain&#39;</span>))  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">11596</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">12572</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">046</span>..<span style="color:#bd93f9">48</span>.<span style="color:#bd93f9">7</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">22668</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Table</span> scan <span style="color:#ff79c6">on</span> fulltext_test  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">11596</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">113155</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0288</span>..<span style="color:#bd93f9">36</span>.<span style="color:#bd93f9">6</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">114244</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="결과-4">결과</h5>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>Fulltext 인덱스</th>
          <th>인덱스 X</th>
          <th>차이점</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>검색 조회 (MATCH)</strong></td>
          <td>35.9ms</td>
          <td>48.7ms</td>
          <td>1.35배 차이</td>
      </tr>
  </tbody>
</table>
<ul>
<li>성능 향상이 있긴 하지만, 위에 B-Tree 나 Hash 만큼 드라마틱 하지는 않은 것 같다.</li>
<li>문장이 더 다양하고, 긴 문장이면 효과가 더 나올라나?</li>
</ul>
<h2 id="4-b-tree-vs-hash-index">4. B-Tree VS Hash Index</h2>
<ul>
<li>생각보다 B-Tree 도 검색효율이 나오는 것 같아서 둘을 비교해보자</li>
</ul>
<h5 id="범위-조회-between-1">범위 조회 (BETWEEN)</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> btree_hash_test 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> btree_column <span style="color:#ff79c6">BETWEEN</span> <span style="color:#bd93f9">100000</span> <span style="color:#ff79c6">AND</span> <span style="color:#bd93f9">200000</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Index</span> range scan <span style="color:#ff79c6">on</span> btree_hash_test <span style="color:#ff79c6">using</span> idx_btree over (<span style="color:#bd93f9">100000</span> <span style="color:#ff79c6">&lt;=</span> btree_column <span style="color:#ff79c6">&lt;=</span> <span style="color:#bd93f9">200000</span>), <span style="color:#ff79c6">with</span> <span style="color:#ff79c6">index</span> condition: (btree_hash_test.btree_column <span style="color:#ff79c6">between</span> <span style="color:#bd93f9">100000</span> <span style="color:#ff79c6">and</span> <span style="color:#bd93f9">200000</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">4535</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">10078</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>.<span style="color:#bd93f9">62</span>..<span style="color:#bd93f9">11</span>.<span style="color:#bd93f9">9</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">10078</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> btree_hash_test 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> hash_column <span style="color:#ff79c6">BETWEEN</span> <span style="color:#bd93f9">100000</span> <span style="color:#ff79c6">AND</span> <span style="color:#bd93f9">200000</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Index</span> range scan <span style="color:#ff79c6">on</span> btree_hash_test <span style="color:#ff79c6">using</span> idx_hash over (<span style="color:#bd93f9">100000</span> <span style="color:#ff79c6">&lt;=</span> hash_column <span style="color:#ff79c6">&lt;=</span> <span style="color:#bd93f9">200000</span>), <span style="color:#ff79c6">with</span> <span style="color:#ff79c6">index</span> condition: (btree_hash_test.hash_column <span style="color:#ff79c6">between</span> <span style="color:#bd93f9">100000</span> <span style="color:#ff79c6">and</span> <span style="color:#bd93f9">200000</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">4475</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">9943</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">3</span>.<span style="color:#bd93f9">58</span>..<span style="color:#bd93f9">21</span>.<span style="color:#bd93f9">8</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">9943</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="검색-조회--3">검색 조회 (=)</h5>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> btree_hash_test 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> btree_column<span style="color:#ff79c6">=</span><span style="color:#bd93f9">146708</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Index</span> lookup <span style="color:#ff79c6">on</span> btree_hash_test <span style="color:#ff79c6">using</span> idx_btree (btree_column<span style="color:#ff79c6">=</span><span style="color:#bd93f9">146708</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">35</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0424</span>..<span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0454</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">ANALYZE</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> btree_hash_test 
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> hash_column<span style="color:#ff79c6">=</span><span style="color:#bd93f9">921585</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">Index</span> lookup <span style="color:#ff79c6">on</span> btree_hash_test <span style="color:#ff79c6">using</span> idx_hash (hash_column<span style="color:#ff79c6">=</span><span style="color:#bd93f9">921585</span>)  (cost<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">35</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>) (actual time<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0199</span>..<span style="color:#bd93f9">0</span>.<span style="color:#bd93f9">0213</span> <span style="color:#ff79c6">rows</span><span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span> loops<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>)
</span></span></code></pre></div><h5 id="결과-5">결과</h5>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>B-Tree</th>
          <th>Hash 인덱스</th>
          <th>차이점</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>범위 조회 (BETWEEN)</strong></td>
          <td>11.9ms</td>
          <td>21.8ms</td>
          <td>2배 차이</td>
      </tr>
      <tr>
          <td><strong>검색 조회 (=)</strong></td>
          <td>0.0454ms</td>
          <td>0.0213ms</td>
          <td>2배 차이</td>
      </tr>
  </tbody>
</table>
<ul>
<li>범위랑 검색 모두 2배 차이씩 나는 것 같다.</li>
<li>대충 범위 조회를 많이쓰면 B-Tree를 검색 조회를 많이 쓰면 Hash Index 를 사용하면 될 것 같다.</li>
</ul>
<h2 id="결과-6">결과</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>그냥 대충 B-Tree 써도 손해는 없어보인다.
</span></span><span style="display:flex;"><span>다만, 속도차이를 보니 속도를 타이트하게 잡아야한다면, 다른 Index 를 고려해볼 필요는 있을 것 같다.
</span></span></code></pre></div><h2 id="나중에-더-해볼-것">나중에 더 해볼 것</h2>
<ol>
<li>INSERT, UPDATE 시 속도 저하가 어느 정도인지 테스트 해보고싶다.</li>
<li>생각보다 Fulltext Index 테스트가 안좋았던 것 같다. 테스트 방식을 바꿔야 할 것 같다 =&gt; 크롤링으로 많은 데이터를 가져와서 Fulltext Index 걸어봐야 확실히 성능이 좋아지는지 알 수 있을 것 같다.</li>
</ol>
]]></content:encoded></item><item><title>MySQL 인덱스 기초 예제</title><link>https://jyukki.com/learning/examples/example-mysql-index-basics/</link><pubDate>Sat, 01 Feb 2025 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/examples/example-mysql-index-basics/</guid><description>WHERE, ORDER BY, JOIN에서 인덱스가 어떻게 사용되는지 간단한 예제로 정리</description><content:encoded><![CDATA[<h2 id="테이블--인덱스-준비">테이블 &amp; 인덱스 준비</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> users (
</span></span><span style="display:flex;"><span>    id <span style="color:#8be9fd;font-style:italic">BIGINT</span> <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span> AUTO_INCREMENT,
</span></span><span style="display:flex;"><span>    email <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">255</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>    name <span style="color:#8be9fd;font-style:italic">VARCHAR</span>(<span style="color:#bd93f9">100</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>    created_at DATETIME <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">INDEX</span> idx_users_email (email),
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">INDEX</span> idx_users_created_at (created_at)
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><h2 id="1-where-절에서-인덱스-사용">1. WHERE 절에서 인덱스 사용</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 인덱스 사용 (email = ?)
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">EXPLAIN</span> <span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users <span style="color:#ff79c6">WHERE</span> email <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;user@example.com&#39;</span>;
</span></span></code></pre></div><p><strong>체크 포인트</strong></p>
<ul>
<li><code>type: ref</code> 또는 <code>const</code> 인지 확인</li>
<li><code>possible_keys</code>, <code>key</code> 에 <code>idx_users_email</code> 이 나오는지 확인</li>
</ul>
<h2 id="2-범위-조회-between--">2. 범위 조회 (BETWEEN / &gt;=)</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- created_at 범위 조회
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">EXPLAIN</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> users
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-01-01&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&lt;</span>  <span style="color:#f1fa8c">&#39;2025-02-01&#39;</span>;
</span></span></code></pre></div><p><strong>실습 아이디어</strong></p>
<ul>
<li>인덱스 없는 상태에서 성능 비교</li>
<li><code>EXPLAIN ANALYZE</code> 로 실제 실행 시간 확인 (8.0+)</li>
</ul>
<h2 id="3-인덱스를-못-타는-패턴">3. 인덱스를 못 타는 패턴</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- ❌ 함수 사용 시 인덱스 사용 불가
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> <span style="color:#8be9fd;font-style:italic">DATE</span>(created_at) <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;2025-01-01&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">-- ✅ 범위 조건으로 변경
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">*</span> <span style="color:#ff79c6">FROM</span> users
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> created_at <span style="color:#ff79c6">&gt;=</span> <span style="color:#f1fa8c">&#39;2025-01-01 00:00:00&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">AND</span> created_at <span style="color:#ff79c6">&lt;</span>  <span style="color:#f1fa8c">&#39;2025-01-02 00:00:00&#39;</span>;
</span></span></code></pre></div><h2 id="4-join-에서-인덱스-사용">4. JOIN 에서 인덱스 사용</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">CREATE</span> <span style="color:#ff79c6">TABLE</span> orders (
</span></span><span style="display:flex;"><span>    id <span style="color:#8be9fd;font-style:italic">BIGINT</span> <span style="color:#ff79c6">PRIMARY</span> <span style="color:#ff79c6">KEY</span> AUTO_INCREMENT,
</span></span><span style="display:flex;"><span>    user_id <span style="color:#8be9fd;font-style:italic">BIGINT</span> <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>    amount <span style="color:#8be9fd;font-style:italic">DECIMAL</span>(<span style="color:#bd93f9">10</span>,<span style="color:#bd93f9">2</span>) <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>    created_at DATETIME <span style="color:#ff79c6">NOT</span> <span style="color:#ff79c6">NULL</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">INDEX</span> idx_orders_user_id (user_id)
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">EXPLAIN</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> u.id, u.email, o.amount
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> users u
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">JOIN</span> orders o <span style="color:#ff79c6">ON</span> u.id <span style="color:#ff79c6">=</span> o.user_id
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> u.email <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;user@example.com&#39;</span>;
</span></span></code></pre></div><p><strong>정리</strong></p>
<ul>
<li>JOIN 컬럼(<code>orders.user_id</code>) 에 인덱스가 있어야 <code>type: ref</code> 로 조회</li>
<li><code>rows</code> 값이 작을수록 효율적인 실행 계획</li>
</ul>
]]></content:encoded></item></channel></rss>