<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Mysql on kastori</title><link>http://blog.kastori.dev/tags/mysql/</link><description>Recent content in Mysql on kastori</description><generator>Hugo -- gohugo.io</generator><language>ko-kr</language><lastBuildDate>Wed, 03 Jun 2026 00:00:00 +0900</lastBuildDate><atom:link href="http://blog.kastori.dev/tags/mysql/index.xml" rel="self" type="application/rss+xml"/><item><title>[DB 완전 정복 #9] 어떤 DB를 왜 선택하는가 — MySQL, PostgreSQL, Redis, MongoDB 선택 기준</title><link>http://blog.kastori.dev/tech/2026-06-03-db-09-db-selection/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-06-03-db-09-db-selection/</guid><description>&lt;h2 id="왜-이-db를-쓰나요"&gt;&lt;a href="#%ec%99%9c-%ec%9d%b4-db%eb%a5%bc-%ec%93%b0%eb%82%98%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;왜 이 DB를 쓰나요?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;기술 면접에서 DB 관련 질문은 보통 여기서 끝난다. &amp;ldquo;MySQL 쓰셨다고 하셨는데, 왜 MySQL을 선택하셨나요?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&amp;ldquo;익숙해서요&amp;quot;는 답이 아니다. &amp;ldquo;팀에서 써왔어서요&amp;quot;도 부족하다. 트레이드오프를 설명할 수 있어야 한다. 이 시리즈 마지막 편에서는 각 DB를 선택하는 근거를 정리한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="sql-vs-nosql--먼저-이-구분부터"&gt;&lt;a href="#sql-vs-nosql--%eb%a8%bc%ec%a0%80-%ec%9d%b4-%ea%b5%ac%eb%b6%84%eb%b6%80%ed%84%b0" class="header-anchor"&gt;&lt;/a&gt;SQL vs NoSQL — 먼저 이 구분부터
&lt;/h2&gt;&lt;p&gt;NoSQL이 SQL보다 무조건 빠른 게 아니다. &lt;strong&gt;다른 문제를 풀기 위해 만들어진&lt;/strong&gt; 도구다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SQL을 선택할 때:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 데이터 간 관계가 명확하고 JOIN이 자주 필요한 경우
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 트랜잭션(ACID) 보장이 필수인 경우 (결제, 계좌)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 스키마가 안정적이고 자주 바뀌지 않는 경우
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 복잡한 집계·분석 쿼리가 필요한 경우
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NoSQL을 선택할 때:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 스키마가 유동적이거나 비정형 데이터 (로그, 이벤트)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 수평 확장(Sharding)이 필수인 대규모 트래픽
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 단순 키-값 조회 또는 단일 문서 읽기/쓰기 위주
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 최종 일관성(Eventual Consistency)으로 충분한 경우
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="mysql--웹-서비스의-검증된-선택"&gt;&lt;a href="#mysql--%ec%9b%b9-%ec%84%9c%eb%b9%84%ec%8a%a4%ec%9d%98-%ea%b2%80%ec%a6%9d%eb%90%9c-%ec%84%a0%ed%83%9d" class="header-anchor"&gt;&lt;/a&gt;MySQL — 웹 서비스의 검증된 선택
&lt;/h2&gt;&lt;p&gt;MySQL을 선택하는 진짜 이유:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;기술적 근거&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;InnoDB의 MVCC와 행 레벨 락으로 높은 동시성&lt;/li&gt;
&lt;li&gt;Clustered Index로 기본키 조회 최고 성능&lt;/li&gt;
&lt;li&gt;갭 락으로 Phantom Read 방지 (REPEATABLE READ 기본)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;실무적 근거&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Boot + JPA 조합의 레퍼런스 압도적&lt;/li&gt;
&lt;li&gt;AWS RDS, Aurora MySQL 등 클라우드 지원 완벽&lt;/li&gt;
&lt;li&gt;DBA, 운영 인력 시장에서 경험자가 가장 많음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;언제 MySQL이 부족한가: JSON 데이터를 쿼리하고 인덱스도 필요할 때, 지리 데이터를 다룰 때, AI 벡터 검색이 필요할 때.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="postgresql--기능과-확장성이-필요할-때"&gt;&lt;a href="#postgresql--%ea%b8%b0%eb%8a%a5%ea%b3%bc-%ed%99%95%ec%9e%a5%ec%84%b1%ec%9d%b4-%ed%95%84%ec%9a%94%ed%95%a0-%eb%95%8c" class="header-anchor"&gt;&lt;/a&gt;PostgreSQL — 기능과 확장성이 필요할 때
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;JSONB가 필요하다면 PostgreSQL&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 상품의 동적 속성을 JSON으로 저장하고 쿼리
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USING&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;@&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;color&amp;#34;: &amp;#34;red&amp;#34;, &amp;#34;size&amp;#34;: &amp;#34;L&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MySQL에서 같은 쿼리를 하려면 Generated Column + 인덱스 트릭이 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI/ML 연동이 필요하다면 PostgreSQL&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- pgvector: 텍스트 임베딩을 저장하고 유사도 검색
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[0.1, 0.2, ...]&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;LLM 기반 서비스를 만든다면 pgvector를 지원하는 PostgreSQL이 사실상 표준이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;지리 데이터라면 PostGIS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;위경도 거리 계산, 반경 내 검색, 지도 위에서의 공간 쿼리가 필요하면 PostGIS 확장이 있는 PostgreSQL이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="redis--인메모리의-다양한-역할"&gt;&lt;a href="#redis--%ec%9d%b8%eb%a9%94%eb%aa%a8%eb%a6%ac%ec%9d%98-%eb%8b%a4%ec%96%91%ed%95%9c-%ec%97%ad%ed%95%a0" class="header-anchor"&gt;&lt;/a&gt;Redis — 인메모리의 다양한 역할
&lt;/h2&gt;&lt;p&gt;Redis를 캐시로만 쓰면 절반만 쓰는 것이다.&lt;/p&gt;
&lt;h3 id="캐시"&gt;&lt;a href="#%ec%ba%90%ec%8b%9c" class="header-anchor"&gt;&lt;/a&gt;캐시
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;product&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;#id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;productRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;DB 조회가 많고 데이터 변경이 적은 경우 Redis 캐시로 DB 부하를 줄인다. TTL을 설정해 무효화를 자동화한다.&lt;/p&gt;
&lt;h3 id="세션-저장소"&gt;&lt;a href="#%ec%84%b8%ec%85%98-%ec%a0%80%ec%9e%a5%ec%86%8c" class="header-anchor"&gt;&lt;/a&gt;세션 저장소
&lt;/h3&gt;&lt;p&gt;다중 서버 환경에서 세션을 공유할 때. 서버가 재시작되거나 로드밸런서가 다른 서버로 요청을 보내도 세션이 유지된다.&lt;/p&gt;
&lt;h3 id="분산-락"&gt;&lt;a href="#%eb%b6%84%ec%82%b0-%eb%9d%bd" class="header-anchor"&gt;&lt;/a&gt;분산 락
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 다중 서버 환경에서 재고 차감을 안전하게&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;redissonClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;lock:product:&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TimeUnit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;단일 서버에서는 &lt;code&gt;synchronized&lt;/code&gt;로 충분하지만, 서버가 2대 이상이면 Redis 분산 락이 필요하다.&lt;/p&gt;
&lt;h3 id="실시간-랭킹"&gt;&lt;a href="#%ec%8b%a4%ec%8b%9c%ea%b0%84-%eb%9e%ad%ed%82%b9" class="header-anchor"&gt;&lt;/a&gt;실시간 랭킹
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZADD ranking 2300 &amp;#34;user:2&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ZREVRANGE ranking 0 9 // 상위 10명 — O(log N)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Sorted Set의 O(log N) 연산으로 수백만 명 랭킹도 실시간 처리 가능하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="mongodb--스키마가-유연해야-할-때"&gt;&lt;a href="#mongodb--%ec%8a%a4%ed%82%a4%eb%a7%88%ea%b0%80-%ec%9c%a0%ec%97%b0%ed%95%b4%ec%95%bc-%ed%95%a0-%eb%95%8c" class="header-anchor"&gt;&lt;/a&gt;MongoDB — 스키마가 유연해야 할 때
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;임베딩이 JOIN을 대체한다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;관계형 DB에서 3개 테이블을 JOIN해야 하는 상품 정보를, MongoDB에서는 단일 문서 조회 1번으로 처리한다. 계층 구조 데이터에서 읽기 성능이 좋다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;초기 개발의 스키마 유연성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;서비스 초기에 데이터 구조가 자주 바뀐다. 관계형 DB에서 컬럼 추가/삭제는 마이그레이션이 필요하다. MongoDB는 문서 구조가 자유로워 마이그레이션 없이 바꿀 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;단, 복잡한 관계와 트랜잭션이 많은 도메인은 MySQL이 맞다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="실무-조합-패턴"&gt;&lt;a href="#%ec%8b%a4%eb%ac%b4-%ec%a1%b0%ed%95%a9-%ed%8c%a8%ed%84%b4" class="header-anchor"&gt;&lt;/a&gt;실무 조합 패턴
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;일반 웹 서비스:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; MySQL ← 메인 데이터
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Redis ← 캐시, 세션, 분산 락
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;JSON 데이터가 많은 서비스:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; PostgreSQL (JSONB) ← 메인 데이터
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Redis ← 캐시, 세션
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;로그·이벤트 처리:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; MySQL / PostgreSQL ← 핵심 비즈니스 데이터
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; MongoDB ← 로그, 이벤트, 비정형 데이터
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Redis ← 실시간 집계, 캐시
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="cap-이론--왜-nosql은-최종-일관성인가"&gt;&lt;a href="#cap-%ec%9d%b4%eb%a1%a0--%ec%99%9c-nosql%ec%9d%80-%ec%b5%9c%ec%a2%85-%ec%9d%bc%ea%b4%80%ec%84%b1%ec%9d%b8%ea%b0%80" class="header-anchor"&gt;&lt;/a&gt;CAP 이론 — 왜 NoSQL은 최종 일관성인가
&lt;/h2&gt;&lt;p&gt;분산 데이터베이스는 세 가지를 동시에 보장할 수 없다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;C (Consistency) — 모든 노드가 같은 데이터를 봄
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;A (Availability) — 모든 요청이 응답을 받음
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;P (Partition Tolerance) — 네트워크 분리 상황에서도 동작
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;네트워크 분리(P)는 분산 시스템에서 항상 발생 가능한 사건이다. P를 포기할 수 없으니, 실제로는 C와 A 중 하나를 선택한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MySQL → CP (일관성 우선, 가용성 희생 가능)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Redis Cluster → AP (가용성 우선, 최종 일관성)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cassandra → AP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MongoDB → 기본 CP, 설정에 따라 AP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Redis가 캐시에 어울리는 이유도 여기 있다. 캐시는 약간 오래된 데이터를 써도 괜찮다(AP). 반면 결제는 항상 최신 데이터가 필요하다(CP, MySQL).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="면접-답변-정리"&gt;&lt;a href="#%eb%a9%b4%ec%a0%91-%eb%8b%b5%eb%b3%80-%ec%a0%95%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;면접 답변 정리
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;&amp;ldquo;어떤 DB를 선택하시겠어요?&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&amp;ldquo;서비스 특성에 따라 다릅니다. 관계형 데이터와 ACID 트랜잭션이 중요하면 MySQL을, JSON 데이터 쿼리나 지리 기능이 필요하면 PostgreSQL을 선택합니다. 캐시와 세션은 Redis를, 스키마가 유동적이고 계층 구조 데이터가 많으면 MongoDB를 고려합니다.&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;MySQL을 쓰는 이유가 뭔가요?&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&amp;ldquo;Spring + JPA 조합에서 레퍼런스가 가장 많고, InnoDB의 MVCC와 행 레벨 락으로 높은 동시성을 지원하며, AWS RDS와 Aurora MySQL을 통한 운영 편의성도 고려했습니다.&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id="시리즈를-마치며"&gt;&lt;a href="#%ec%8b%9c%eb%a6%ac%ec%a6%88%eb%a5%bc-%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;시리즈를 마치며
&lt;/h2&gt;&lt;p&gt;DB는 단순히 데이터를 저장하는 도구가 아니다. 각 DB는 특정 문제를 풀기 위해 설계됐고, 그 설계 철학이 성능과 트레이드오프를 결정한다.&lt;/p&gt;
&lt;p&gt;인덱스부터 시작해서 트랜잭션, 쿼리 최적화, 정규화, 커넥션 풀, MySQL vs PostgreSQL, Redis, MongoDB, 그리고 DB 선택 기준까지. 이 시리즈에서 다룬 내용이 면접과 실무에서 근거 있는 선택을 하는 데 도움이 되길 바란다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #6] MySQL과 PostgreSQL, 뭐가 다른가 — MVCC 구현부터 JSONB까지</title><link>http://blog.kastori.dev/tech/2026-05-31-db-06-mysql-vs-postgresql/</link><pubDate>Sun, 31 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-31-db-06-mysql-vs-postgresql/</guid><description>&lt;h2 id="그냥-mysql-쓰면-안-되나요"&gt;&lt;a href="#%ea%b7%b8%eb%83%a5-mysql-%ec%93%b0%eb%a9%b4-%ec%95%88-%eb%90%98%eb%82%98%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;그냥 MySQL 쓰면 안 되나요?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;사실 대부분의 경우 MySQL로 충분하다. 하지만 &amp;ldquo;왜 MySQL인가요?&amp;ldquo;라는 질문에 &amp;ldquo;익숙해서요&amp;quot;는 좋은 답이 아니다. 두 DB의 내부 구조 차이를 알면, 상황에 따른 선택 근거를 만들 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="mvcc-구현-방식--가장-근본적인-차이"&gt;&lt;a href="#mvcc-%ea%b5%ac%ed%98%84-%eb%b0%a9%ec%8b%9d--%ea%b0%80%ec%9e%a5-%ea%b7%bc%eb%b3%b8%ec%a0%81%ec%9d%b8-%ec%b0%a8%ec%9d%b4" class="header-anchor"&gt;&lt;/a&gt;MVCC 구현 방식 — 가장 근본적인 차이
&lt;/h2&gt;&lt;p&gt;앞서 MVCC가 트랜잭션의 일관된 읽기를 보장한다고 배웠다. 그런데 MySQL과 PostgreSQL은 이 MVCC를 다른 방식으로 구현한다.&lt;/p&gt;
&lt;h3 id="mysql-innodb-undo-log-방식"&gt;&lt;a href="#mysql-innodb-undo-log-%eb%b0%a9%ec%8b%9d" class="header-anchor"&gt;&lt;/a&gt;MySQL InnoDB: Undo Log 방식
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[테이블 페이지]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 행 데이터: TX_ID=200, name=&amp;#34;수정됨&amp;#34; ← 항상 최신 버전
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──(포인터)──▶ [Undo Log]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; TX_ID=100, name=&amp;#34;원본&amp;#34; ← 이전 버전
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──▶ [Undo Log]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; TX_ID=50, name=&amp;#34;초기값&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;현재 데이터 페이지에는 &lt;strong&gt;최신 버전만&lt;/strong&gt; 저장한다. 이전 버전은 별도 Undo Log 공간에 체인으로 보관한다. 읽기 시 자신의 스냅샷보다 최신 데이터면 Undo Log에서 이전 버전을 찾아 읽는다.&lt;/p&gt;
&lt;h3 id="postgresql-heap-방식"&gt;&lt;a href="#postgresql-heap-%eb%b0%a9%ec%8b%9d" class="header-anchor"&gt;&lt;/a&gt;PostgreSQL: Heap 방식
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[테이블 파일 (Heap)]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; t_xmin=100, t_xmax=200, name=&amp;#34;원본&amp;#34; ← 이전 버전 (dead tuple)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; t_xmin=200, t_xmax=0, name=&amp;#34;수정됨&amp;#34; ← 현재 버전 (live tuple)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;같은 테이블 파일 안에 &lt;strong&gt;여러 버전이 공존&lt;/strong&gt;한다. 각 행에 생성 트랜잭션(&lt;code&gt;t_xmin&lt;/code&gt;)과 삭제 트랜잭션(&lt;code&gt;t_xmax&lt;/code&gt;)을 기록한다. 읽기 시 내 트랜잭션 기준으로 어떤 버전을 볼지 visibility check로 판단한다.&lt;/p&gt;
&lt;h3 id="운영상-차이"&gt;&lt;a href="#%ec%9a%b4%ec%98%81%ec%83%81-%ec%b0%a8%ec%9d%b4" class="header-anchor"&gt;&lt;/a&gt;운영상 차이
&lt;/h3&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;항목&lt;/th&gt;
 &lt;th&gt;MySQL&lt;/th&gt;
 &lt;th&gt;PostgreSQL&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;이전 버전 위치&lt;/td&gt;
 &lt;td&gt;별도 Undo Log&lt;/td&gt;
 &lt;td&gt;같은 테이블 파일&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;정리 방법&lt;/td&gt;
 &lt;td&gt;Purge 스레드 자동&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;VACUUM&lt;/strong&gt; 필수&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;운영 주의점&lt;/td&gt;
 &lt;td&gt;긴 트랜잭션 (Undo 과다)&lt;/td&gt;
 &lt;td&gt;table bloat, autovacuum 튜닝&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;PostgreSQL은 오래된 dead tuple이 쌓이면 &lt;strong&gt;table bloat&lt;/strong&gt; 현상이 발생한다. autovacuum이 자동으로 정리하지만, 대용량 테이블에서는 VACUUM 튜닝이 운영의 핵심이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인덱스-구조--clustered-vs-heap"&gt;&lt;a href="#%ec%9d%b8%eb%8d%b1%ec%8a%a4-%ea%b5%ac%ec%a1%b0--clustered-vs-heap" class="header-anchor"&gt;&lt;/a&gt;인덱스 구조 — Clustered vs Heap
&lt;/h2&gt;&lt;p&gt;MySQL의 기본키 인덱스는 &lt;strong&gt;Clustered Index&lt;/strong&gt;다. 리프 노드에 실제 행 데이터가 저장되고, 기본키 순서로 물리적으로 정렬된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MySQL Secondary Index 조회:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 인덱스에서 PK 찾기 → PK로 Clustered Index 조회 (2단계)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PostgreSQL 모든 인덱스 조회:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 인덱스에서 heap tuple pointer 찾기 → heap에서 행 조회 (1단계)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MySQL의 Secondary Index는 2단계를 거치지만, 기본키 조회는 Clustered Index 덕분에 최고 성능이다. PostgreSQL은 모든 인덱스가 동등한 구조를 가진다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="jsonb--postgresql이-mysql을-압도하는-부분"&gt;&lt;a href="#jsonb--postgresql%ec%9d%b4-mysql%ec%9d%84-%ec%95%95%eb%8f%84%ed%95%98%eb%8a%94-%eb%b6%80%eb%b6%84" class="header-anchor"&gt;&lt;/a&gt;JSONB — PostgreSQL이 MySQL을 압도하는 부분
&lt;/h2&gt;&lt;p&gt;반정형 데이터(JSON)를 DB에 저장해야 한다면, PostgreSQL의 JSONB가 압도적으로 유리하다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- MySQL JSON: 텍스트로 저장, 조회마다 파싱
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COLUMN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 인덱스를 걸려면 Generated Column 트릭 필요
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COLUMN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta_color&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GENERATED&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ALWAYS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_UNQUOTE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$.color&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_color&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta_color&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- PostgreSQL JSONB: 바이너리 파싱 후 저장, GIN 인덱스 직접 가능
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COLUMN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USING&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 풍부한 연산자
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;@&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;#34;color&amp;#34;: &amp;#34;red&amp;#34;}&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 포함 여부
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;color&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 키 존재 여부
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PostgreSQL JSONB는 바이너리로 저장해 파싱 없이 빠르고, GIN 인덱스로 JSON 내부 키에 직접 인덱싱할 수 있다. MySQL은 이 수준의 JSON 쿼리 지원이 없다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="postgresql의-확장-생태계"&gt;&lt;a href="#postgresql%ec%9d%98-%ed%99%95%ec%9e%a5-%ec%83%9d%ed%83%9c%ea%b3%84" class="header-anchor"&gt;&lt;/a&gt;PostgreSQL의 확장 생태계
&lt;/h2&gt;&lt;p&gt;PostgreSQL은 확장(Extension)으로 기능을 추가할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PostGIS → 지리 데이터, 위경도 거리 계산, 공간 인덱스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pgvector → AI 벡터 임베딩 저장 및 유사도 검색 (LLM 연동)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_trgm → 문자열 유사도 검색 (LIKE &amp;#39;%검색어%&amp;#39; 인덱스)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TimescaleDB → 시계열 데이터 최적화
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;LLM, AI 서비스와 연동하거나 지리 기능이 필요하다면 PostgreSQL이 사실상 유일한 선택이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="라이선스-차이"&gt;&lt;a href="#%eb%9d%bc%ec%9d%b4%ec%84%a0%ec%8a%a4-%ec%b0%a8%ec%9d%b4" class="header-anchor"&gt;&lt;/a&gt;라이선스 차이
&lt;/h2&gt;&lt;p&gt;MySQL은 GPL 라이선스다. 오픈소스 프로젝트에서 MySQL을 수정해 배포하면 소스를 공개해야 한다. MariaDB 포크가 생긴 배경이기도 하다.&lt;/p&gt;
&lt;p&gt;PostgreSQL은 BSD 계열(PostgreSQL License)로 &lt;strong&gt;상업용 포함 완전 자유&lt;/strong&gt;다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="선택-기준-정리"&gt;&lt;a href="#%ec%84%a0%ed%83%9d-%ea%b8%b0%ec%a4%80-%ec%a0%95%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;선택 기준 정리
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MySQL을 선택하는 경우:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - Spring Boot + JPA 조합 (레퍼런스와 생태계 압도적)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 단순 CRUD 위주의 일반 웹 서비스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 팀에 MySQL 운영 경험이 있는 경우
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - AWS Aurora MySQL 사용 계획
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PostgreSQL을 선택하는 경우:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - JSON 데이터를 쿼리하고 인덱스도 필요한 경우 (JSONB)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 지리 데이터 (PostGIS)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - AI/ML 벡터 검색 (pgvector)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 복잡한 분석 쿼리 위주 서비스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 오픈소스 라이선스 자유도가 중요한 경우
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;MySQL은 웹 서비스의 검증된 선택이고, PostgreSQL은 확장성과 기능 다양성에서 강하다. 어느 쪽이 낫다는 게 아니라, &lt;strong&gt;사용하는 이유를 설명할 수 있느냐&lt;/strong&gt;가 중요하다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 관계형 DB와 전혀 다른 구조로 설계된 Redis의 내부 동작 원리를 다룬다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #4] 정규화는 언제 깨야 하나 — 1NF부터 3NF, 실무 비정규화까지</title><link>http://blog.kastori.dev/tech/2026-05-29-db-04-normalization/</link><pubDate>Fri, 29 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-29-db-04-normalization/</guid><description>&lt;h2 id="테이블-하나에-다-넣으면-안-되나요"&gt;&lt;a href="#%ed%85%8c%ec%9d%b4%eb%b8%94-%ed%95%98%eb%82%98%ec%97%90-%eb%8b%a4-%eb%84%a3%ec%9c%bc%eb%a9%b4-%ec%95%88-%eb%90%98%eb%82%98%ec%9a%94" class="header-anchor"&gt;&lt;/a&gt;테이블 하나에 다 넣으면 안 되나요?
&lt;/h2&gt;&lt;p&gt;DB를 처음 설계할 때 이런 생각을 한 적이 있을 것이다. 그냥 필요한 정보를 테이블 하나에 다 넣으면 조인도 없고 편하지 않을까?&lt;/p&gt;
&lt;p&gt;실제로 해보면 금방 문제가 생긴다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="이상-현상--설계가-나쁠-때-생기는-일"&gt;&lt;a href="#%ec%9d%b4%ec%83%81-%ed%98%84%ec%83%81--%ec%84%a4%ea%b3%84%ea%b0%80-%eb%82%98%ec%81%a0-%eb%95%8c-%ec%83%9d%ea%b8%b0%eb%8a%94-%ec%9d%bc" class="header-anchor"&gt;&lt;/a&gt;이상 현상 — 설계가 나쁠 때 생기는 일
&lt;/h2&gt;&lt;p&gt;이런 테이블이 있다고 해보자.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;주문_정보 (비정규화)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| order_id | member_id | member_name | member_email | product_id | product_name | price |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|----------|-----------|-------------|---------------|------------|--------------|-------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 1 | 100 | 홍길동 | hong@test.com | 200 | 노트북 | 1500 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 2 | 100 | 홍길동 | hong@test.com | 201 | 마우스 | 30 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 3 | 101 | 김철수 | kim@test.com | 200 | 노트북 | 1500 |
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;삽입 이상&lt;/strong&gt;: 주문이 없는 신규 회원을 등록하고 싶다. &lt;code&gt;order_id&lt;/code&gt;가 없으니 행을 추가할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;삭제 이상&lt;/strong&gt;: 주문 1번을 삭제한다. 그런데 홍길동의 이메일 정보가 주문 2번에만 남는 게 아니라, 만약 주문이 1번 하나뿐이었다면 회원 정보 자체가 사라진다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;수정 이상&lt;/strong&gt;: 홍길동이 이메일을 변경했다. &lt;code&gt;order_id = 1&lt;/code&gt;과 &lt;code&gt;order_id = 2&lt;/code&gt; 두 군데를 모두 수정해야 한다. 하나만 고치면 같은 사람의 이메일이 두 개가 된다.&lt;/p&gt;
&lt;p&gt;이것이 **이상 현상(Anomaly)**이다. 정규화는 이 문제를 해결하기 위한 설계 원칙이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="1nf--원자값-원칙"&gt;&lt;a href="#1nf--%ec%9b%90%ec%9e%90%ea%b0%92-%ec%9b%90%ec%b9%99" class="header-anchor"&gt;&lt;/a&gt;1NF — 원자값 원칙
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;모든 컬럼의 값이 더 이상 분리할 수 없는 원자적인 값이어야 한다.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❌ 1NF 위반
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| order_id | products |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|----------|------------------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 1 | 노트북, 마우스 | ← 여러 값이 한 컬럼에
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✅ 1NF 준수
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| order_id | product_id | product_name |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|----------|------------|--------------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 1 | 200 | 노트북 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 1 | 201 | 마우스 |
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;컬럼에 여러 값을 콤마로 넣거나, 배열처럼 넣는 것이 대표적인 위반이다. 이렇게 하면 &lt;code&gt;WHERE products LIKE '%마우스%'&lt;/code&gt;처럼 검색해야 하고, 인덱스도 제대로 못 탄다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="2nf--부분-함수-종속-제거"&gt;&lt;a href="#2nf--%eb%b6%80%eb%b6%84-%ed%95%a8%ec%88%98-%ec%a2%85%ec%86%8d-%ec%a0%9c%ea%b1%b0" class="header-anchor"&gt;&lt;/a&gt;2NF — 부분 함수 종속 제거
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;기본키가 복합키일 때, 모든 일반 컬럼이 복합키 전체에 종속되어야 한다. (기본키의 일부에만 종속되면 안 된다)&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;order_items 테이블 (1NF)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;기본키: (order_id, product_id)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| order_id | product_id | product_name | qty |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|----------|------------|--------------|-----|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 1 | 200 | 노트북 | 1 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 1 | 201 | 마우스 | 2 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;문제: product_name은 product_id에만 종속 (order_id와는 무관)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 노트북 이름이 바뀌면 여러 주문 행을 모두 수정해야 함
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✅ 2NF 준수 (분리)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;order_items: (order_id, product_id, qty)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;products: (product_id, product_name, price)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="3nf--이행적-함수-종속-제거"&gt;&lt;a href="#3nf--%ec%9d%b4%ed%96%89%ec%a0%81-%ed%95%a8%ec%88%98-%ec%a2%85%ec%86%8d-%ec%a0%9c%ea%b1%b0" class="header-anchor"&gt;&lt;/a&gt;3NF — 이행적 함수 종속 제거
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;기본키가 아닌 일반 컬럼 간의 종속이 없어야 한다.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;members 테이블 (2NF)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;기본키: member_id
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| member_id | zip_code | city | district |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;|-----------|----------|--------|----------|
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 1 | 06130 | 서울 | 강남구 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| 2 | 06130 | 서울 | 강남구 |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;문제: city와 district는 zip_code에 종속 (member_id → zip_code → city/district)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 06130 우편번호의 시/구가 바뀌면 모든 행을 수정해야 함
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✅ 3NF 준수 (분리)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;members: (member_id, zip_code)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zip_codes: (zip_code, city, district)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="정규화-vs-비정규화--트레이드오프"&gt;&lt;a href="#%ec%a0%95%ea%b7%9c%ed%99%94-vs-%eb%b9%84%ec%a0%95%ea%b7%9c%ed%99%94--%ed%8a%b8%eb%a0%88%ec%9d%b4%eb%93%9c%ec%98%a4%ed%94%84" class="header-anchor"&gt;&lt;/a&gt;정규화 vs 비정규화 — 트레이드오프
&lt;/h2&gt;&lt;p&gt;정규화를 하면 데이터 무결성이 좋아지지만, 조회 시 조인이 많아진다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;정규화의 장점:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ✅ 데이터 중복 없음 → 수정 이상 없음
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ✅ 삽입/삭제 이상 없음
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ✅ 저장 공간 절약
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;정규화의 단점:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ❌ 조회 시 JOIN 필요 → 쿼리 복잡
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ❌ 성능이 중요한 읽기 위주 서비스에서 조인 비용
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;실무에서는 &lt;strong&gt;의도적으로 정규화를 깨는&lt;/strong&gt; 경우도 있다.&lt;/p&gt;
&lt;h3 id="비정규화가-정당한-경우"&gt;&lt;a href="#%eb%b9%84%ec%a0%95%ea%b7%9c%ed%99%94%ea%b0%80-%ec%a0%95%eb%8b%b9%ed%95%9c-%ea%b2%bd%ec%9a%b0" class="header-anchor"&gt;&lt;/a&gt;비정규화가 정당한 경우
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;1. 집계 결과를 미리 저장&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 매번 COUNT 집계 쿼리 실행
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 비정규화: members 테이블에 post_count 컬럼 추가
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 게시글 INSERT/DELETE 시 post_count를 함께 업데이트
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;post_count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 빠름
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;2. 자주 함께 조회되는 컬럼 중복 저장&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- orders 테이블에 member_name을 중복 저장
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 주문 목록 조회 시 members 테이블 JOIN 불필요
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;주문 당시의 회원명을 보존한다는 의미도 있다(회원이 탈퇴해도 주문 이력 유지).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 읽기 전용 분석 테이블&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OLAP(분석 쿼리) 용도의 테이블은 처음부터 비정규화로 설계한다. 수십 개 테이블을 조인하는 분석 쿼리보다 평탄화된 넓은 테이블이 훨씬 빠르다.&lt;/p&gt;
&lt;h3 id="jpa와-정규화의-관계"&gt;&lt;a href="#jpa%ec%99%80-%ec%a0%95%ea%b7%9c%ed%99%94%ec%9d%98-%ea%b4%80%ea%b3%84" class="header-anchor"&gt;&lt;/a&gt;JPA와 정규화의 관계
&lt;/h3&gt;&lt;p&gt;JPA 연관관계(&lt;code&gt;@OneToMany&lt;/code&gt;, &lt;code&gt;@ManyToOne&lt;/code&gt;)는 정규화된 테이블을 객체로 매핑하는 방식이다. 정규화가 잘 되어 있으면 JPA 설계도 자연스럽게 따라온다.&lt;/p&gt;
&lt;p&gt;비정규화를 하면 JPA 매핑이 어색해질 수 있다. 그래서 비정규화는 신중하게, 이유가 명확할 때만 해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;정규화는 목표가 아니라 도구다. &lt;strong&gt;이상 현상을 없애기 위해 필요한 만큼&lt;/strong&gt; 하고, 성능이 중요하고 이유가 명확한 경우에만 의도적으로 비정규화를 선택한다. &amp;ldquo;정규화를 깨는 이유가 뭔가요?&amp;ldquo;라는 질문에 답할 수 있다면 충분하다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 DB 연결 자체의 비용을 다루는 커넥션 풀(HikariCP)을 다룬다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #3] 느린 쿼리를 잡아라 — EXPLAIN 읽는 법과 페이징 최적화</title><link>http://blog.kastori.dev/tech/2026-05-28-db-03-query-optimization/</link><pubDate>Thu, 28 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-28-db-03-query-optimization/</guid><description>&lt;h2 id="쿼리가-느리다-어디서부터-봐야-할까"&gt;&lt;a href="#%ec%bf%bc%eb%a6%ac%ea%b0%80-%eb%8a%90%eb%a6%ac%eb%8b%a4-%ec%96%b4%eb%94%94%ec%84%9c%eb%b6%80%ed%84%b0-%eb%b4%90%ec%95%bc-%ed%95%a0%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;쿼리가 느리다. 어디서부터 봐야 할까
&lt;/h2&gt;&lt;p&gt;개발 환경에서는 데이터가 몇 백 건이라 빠르다. 그런데 운영 환경에 올라가면 특정 페이지가 버벅인다. 로그를 보면 DB 쿼리가 범인이다.&lt;/p&gt;
&lt;p&gt;이때 시작점은 하나다. &lt;strong&gt;EXPLAIN&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="explain--옵티마이저의-속마음을-보는-방법"&gt;&lt;a href="#explain--%ec%98%b5%ed%8b%b0%eb%a7%88%ec%9d%b4%ec%a0%80%ec%9d%98-%ec%86%8d%eb%a7%88%ec%9d%8c%ec%9d%84-%eb%b3%b4%eb%8a%94-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;EXPLAIN — 옵티마이저의 속마음을 보는 방법
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;은 MySQL 옵티마이저가 쿼리를 어떻게 실행할지 보여주는 명령이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PENDING&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;출력에서 봐야 할 컬럼 네 가지:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;컬럼&lt;/th&gt;
 &lt;th&gt;의미&lt;/th&gt;
 &lt;th&gt;좋은 값&lt;/th&gt;
 &lt;th&gt;나쁜 값&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;테이블 접근 방식&lt;/td&gt;
 &lt;td&gt;const, ref, range&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;ALL&lt;/strong&gt; (풀스캔)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;실제 사용한 인덱스&lt;/td&gt;
 &lt;td&gt;인덱스명&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;NULL&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;rows&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;예상 스캔 행 수&lt;/td&gt;
 &lt;td&gt;작을수록&lt;/td&gt;
 &lt;td&gt;클수록&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Extra&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;부가 정보&lt;/td&gt;
 &lt;td&gt;Using index&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Using filesort&lt;/strong&gt;, &lt;strong&gt;Using temporary&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="type-값--빠른-순서"&gt;&lt;a href="#type-%ea%b0%92--%eb%b9%a0%eb%a5%b8-%ec%88%9c%ec%84%9c" class="header-anchor"&gt;&lt;/a&gt;type 값 — 빠른 순서
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;PK로&lt;/span&gt; &lt;span class="err"&gt;단&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;개&lt;/span&gt; &lt;span class="err"&gt;행&lt;/span&gt; &lt;span class="err"&gt;조회&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;가장&lt;/span&gt; &lt;span class="err"&gt;빠름&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;eq_ref&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;조인에서&lt;/span&gt; &lt;span class="n"&gt;PK로&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;개&lt;/span&gt; &lt;span class="err"&gt;행&lt;/span&gt; &lt;span class="err"&gt;조회&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;일반&lt;/span&gt; &lt;span class="err"&gt;인덱스로&lt;/span&gt; &lt;span class="err"&gt;여러&lt;/span&gt; &lt;span class="err"&gt;행&lt;/span&gt; &lt;span class="err"&gt;조회&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;range&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;인덱스&lt;/span&gt; &lt;span class="err"&gt;범위&lt;/span&gt; &lt;span class="err"&gt;스캔&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BETWEEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;인덱스&lt;/span&gt; &lt;span class="err"&gt;풀&lt;/span&gt; &lt;span class="err"&gt;스캔&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;테이블보다는&lt;/span&gt; &lt;span class="err"&gt;낫지만&lt;/span&gt; &lt;span class="err"&gt;느림&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ALL&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;테이블&lt;/span&gt; &lt;span class="err"&gt;풀&lt;/span&gt; &lt;span class="err"&gt;스캔&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;가장&lt;/span&gt; &lt;span class="err"&gt;느림&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;즉시&lt;/span&gt; &lt;span class="err"&gt;개선&lt;/span&gt; &lt;span class="err"&gt;필요&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;type: ALL&lt;/code&gt;이 보이면 인덱스가 없거나 무력화된 것이다.&lt;/p&gt;
&lt;h3 id="extra-해석"&gt;&lt;a href="#extra-%ed%95%b4%ec%84%9d" class="header-anchor"&gt;&lt;/a&gt;Extra 해석
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using index → 커버링 인덱스. 테이블 접근 없음 (좋음)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using where → 인덱스 후 WHERE 필터링 (보통)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using filesort → ORDER BY를 정렬 버퍼에서 처리 (느림, 인덱스로 해결 가능)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using temporary → 임시 테이블 사용 (느림, GROUP BY/DISTINCT 최적화 필요)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using index condition → ICP (인덱스 레벨에서 조건 필터링, 개선)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Using filesort&lt;/code&gt;와 &lt;code&gt;Using temporary&lt;/code&gt;가 함께 나오면 위험 신호다. ORDER BY나 GROUP BY 컬럼에 인덱스를 추가하는 것을 검토해야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="슬로우-쿼리-로그--운영에서-느린-쿼리-찾기"&gt;&lt;a href="#%ec%8a%ac%eb%a1%9c%ec%9a%b0-%ec%bf%bc%eb%a6%ac-%eb%a1%9c%ea%b7%b8--%ec%9a%b4%ec%98%81%ec%97%90%ec%84%9c-%eb%8a%90%eb%a6%b0-%ec%bf%bc%eb%a6%ac-%ec%b0%be%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;슬로우 쿼리 로그 — 운영에서 느린 쿼리 찾기
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;은 알고 있는 쿼리를 분석할 때 쓴다. 운영에서 어떤 쿼리가 느린지 모를 때는 슬로우 쿼리 로그를 켠다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 슬로우 쿼리 로그 설정
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GLOBAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slow_query_log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ON&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GLOBAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;long_query_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 1초 이상 걸리는 쿼리 기록
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GLOBAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slow_query_log_file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/var/log/mysql/slow.log&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;로그에 쌓인 쿼리를 &lt;code&gt;mysqldumpslow&lt;/code&gt;로 집계하면 가장 자주 느린 쿼리 순으로 정렬할 수 있다.&lt;/p&gt;
&lt;p&gt;Spring Boot에서는 &lt;code&gt;p6spy&lt;/code&gt; 라이브러리를 추가하면 JPA가 실행하는 쿼리의 실행 시간을 콘솔에서 볼 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="조인-최적화--드라이빙-테이블이-결과를-결정한다"&gt;&lt;a href="#%ec%a1%b0%ec%9d%b8-%ec%b5%9c%ec%a0%81%ed%99%94--%eb%93%9c%eb%9d%bc%ec%9d%b4%eb%b9%99-%ed%85%8c%ec%9d%b4%eb%b8%94%ec%9d%b4-%ea%b2%b0%ea%b3%bc%eb%a5%bc-%ea%b2%b0%ec%a0%95%ed%95%9c%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;조인 최적화 — 드라이빙 테이블이 결과를 결정한다
&lt;/h2&gt;&lt;p&gt;MySQL은 조인 시 **드라이빙 테이블(Driving Table)**을 먼저 읽고, 그 결과로 드리븐 테이블(Driven Table)을 검색한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PENDING&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;옵티마이저는 어떤 테이블을 드라이빙으로 할지 결정한다. 일반적으로 &lt;strong&gt;결과 행 수가 적은 테이블&lt;/strong&gt;이 드라이빙이 된다.&lt;/p&gt;
&lt;p&gt;핵심 원칙: &lt;strong&gt;드리븐 테이블의 조인 조건 컬럼에는 반드시 인덱스가 있어야 한다.&lt;/strong&gt; 없으면 드라이빙 테이블의 행마다 드리븐 테이블 풀스캔이 반복된다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- members.id에 인덱스가 없으면
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- orders 행 수만큼 members 풀스캔 반복 → O(N*M)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- members.id에 인덱스 있으면
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 각 orders 행에서 members를 O(log M)으로 조회 → O(N*log M)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="offset-페이징의-함정"&gt;&lt;a href="#offset-%ed%8e%98%ec%9d%b4%ec%a7%95%ec%9d%98-%ed%95%a8%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;OFFSET 페이징의 함정
&lt;/h2&gt;&lt;p&gt;가장 많이 쓰는 페이징 쿼리다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OFFSET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;문제는 &lt;code&gt;OFFSET 100000&lt;/code&gt;이다. MySQL은 첫 번째 행부터 100,020번째 행까지 읽어서, 앞 100,000개를 버리고 20개만 반환한다. 페이지 번호가 높아질수록 버리는 데이터가 늘어난다.&lt;/p&gt;
&lt;p&gt;페이지 1: 20개 읽기&lt;br&gt;
페이지 5000: 100,020개 읽고 100,000개 버리기&lt;/p&gt;
&lt;h3 id="해결책-1-커버링-인덱스--서브쿼리"&gt;&lt;a href="#%ed%95%b4%ea%b2%b0%ec%b1%85-1-%ec%bb%a4%eb%b2%84%eb%a7%81-%ec%9d%b8%eb%8d%b1%ec%8a%a4--%ec%84%9c%eb%b8%8c%ec%bf%bc%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;해결책 1: 커버링 인덱스 + 서브쿼리
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 1단계: 인덱스만으로 PK 목록 조회 (테이블 접근 없음)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OFFSET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 2단계: PK로 실제 데이터 조회
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OFFSET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;서브쿼리는 인덱스만 읽으니 빠르고, 최종 데이터는 PK 20개로만 조회한다.&lt;/p&gt;
&lt;h3 id="해결책-2-커서-기반-페이징-no-offset"&gt;&lt;a href="#%ed%95%b4%ea%b2%b0%ec%b1%85-2-%ec%bb%a4%ec%84%9c-%ea%b8%b0%eb%b0%98-%ed%8e%98%ec%9d%b4%ec%a7%95-no-offset" class="header-anchor"&gt;&lt;/a&gt;해결책 2: 커서 기반 페이징 (No Offset)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 첫 페이지
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 마지막으로 받은 created_at이 &amp;#39;2026-05-01 12:00:00&amp;#39;이라면
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 다음 페이지 (OFFSET 없이 조건으로)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2026-05-01 12:00:00&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;OFFSET 없이 항상 인덱스 범위 스캔으로 처리된다. 몇 백만 페이지가 되어도 속도가 일정하다. 단, 임의 페이지 이동(페이지 번호 클릭)이 필요하면 쓸 수 없다. 무한 스크롤에 최적이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;쿼리 최적화의 순서:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. EXPLAIN으로 실행 계획 확인
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. type: ALL → 인덱스 추가
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. Using filesort → ORDER BY 컬럼에 인덱스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. 조인: 드리븐 테이블 조인 조건에 인덱스
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. 페이징: OFFSET 높으면 커버링 인덱스 또는 커서 기반으로 전환
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;다음 편에서는 테이블 설계의 기초인 정규화와, 실무에서 의도적으로 정규화를 깨는 경우를 다룬다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #2] MySQL이 동시 요청을 처리하는 방법 — MVCC, 갭 락, 데드락</title><link>http://blog.kastori.dev/tech/2026-05-26-db-02-transaction-lock/</link><pubDate>Tue, 26 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-26-db-02-transaction-lock/</guid><description>&lt;h2 id="동시에-100명이-같은-행을-읽고-있다면"&gt;&lt;a href="#%eb%8f%99%ec%8b%9c%ec%97%90-100%eb%aa%85%ec%9d%b4-%ea%b0%99%ec%9d%80-%ed%96%89%ec%9d%84-%ec%9d%bd%ea%b3%a0-%ec%9e%88%eb%8b%a4%eb%a9%b4" class="header-anchor"&gt;&lt;/a&gt;동시에 100명이 같은 행을 읽고 있다면
&lt;/h2&gt;&lt;p&gt;서버에 동시 요청이 들어오면 여러 트랜잭션이 같은 데이터를 동시에 읽고 쓴다. 이때 두 가지 문제가 생긴다.&lt;/p&gt;
&lt;p&gt;첫 번째는 &lt;strong&gt;일관성&lt;/strong&gt;. A가 읽는 도중 B가 데이터를 수정하면, A는 수정 전을 봐야 할까 수정 후를 봐야 할까?&lt;/p&gt;
&lt;p&gt;두 번째는 &lt;strong&gt;성능&lt;/strong&gt;. 읽기마다 락을 걸면 안전하지만, 100개 스레드가 모두 대기 상태가 된다.&lt;/p&gt;
&lt;p&gt;MySQL InnoDB는 &lt;strong&gt;MVCC&lt;/strong&gt;로 이 두 문제를 동시에 해결한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="mvcc--락-없이-일관된-읽기"&gt;&lt;a href="#mvcc--%eb%9d%bd-%ec%97%86%ec%9d%b4-%ec%9d%bc%ea%b4%80%eb%90%9c-%ec%9d%bd%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;MVCC — 락 없이 일관된 읽기
&lt;/h2&gt;&lt;p&gt;MVCC(Multi-Version Concurrency Control)는 &lt;strong&gt;각 트랜잭션이 자신이 시작한 시점의 스냅샷을 본다&lt;/strong&gt;는 개념이다.&lt;/p&gt;
&lt;p&gt;InnoDB는 모든 행에 숨겨진 컬럼을 둔다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DB_TRX_ID : 이 행을 마지막으로 수정한 트랜잭션 ID
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DB_ROLL_PTR : Undo Log 포인터 (이전 버전으로 가는 링크)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;트랜잭션이 시작되면 &lt;strong&gt;스냅샷 ID&lt;/strong&gt;가 부여된다. 이후 데이터를 읽을 때, 해당 행의 &lt;code&gt;DB_TRX_ID&lt;/code&gt;가 자신의 스냅샷 ID보다 크면(= 내가 시작한 이후에 수정됨) Undo Log에서 이전 버전을 찾아 읽는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[현재 데이터] TX_ID=200, name=&amp;#34;수정됨&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──▶ [Undo Log] TX_ID=100, name=&amp;#34;원본&amp;#34; ← TX 150이 읽는 버전
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──▶ [Undo Log] TX_ID=50, name=&amp;#34;초기값&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;트랜잭션 150은 TX 200이 수정한 내용을 보지 않는다. 락 없이도 일관된 데이터를 읽는다. 이것이 MVCC의 핵심이다.&lt;/p&gt;
&lt;p&gt;덕분에 &lt;strong&gt;읽기는 쓰기를 방해하지 않고, 쓰기는 읽기를 방해하지 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="행-레벨-락--필요한-행만-잠근다"&gt;&lt;a href="#%ed%96%89-%eb%a0%88%eb%b2%a8-%eb%9d%bd--%ed%95%84%ec%9a%94%ed%95%9c-%ed%96%89%eb%a7%8c-%ec%9e%a0%ea%b7%bc%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;행 레벨 락 — 필요한 행만 잠근다
&lt;/h2&gt;&lt;p&gt;읽기는 MVCC로, 쓰기는 &lt;strong&gt;행 레벨 락&lt;/strong&gt;으로 처리한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;S락 (Shared Lock) : 읽기 락. 여러 트랜잭션이 동시에 획득 가능
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X락 (Exclusive Lock): 쓰기 락. 하나의 트랜잭션만 획득 가능
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;S + S → 허용
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;S + X → 대기
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X + X → 대기
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;InnoDB는 테이블 전체가 아닌 &lt;strong&gt;특정 행&lt;/strong&gt;에만 락을 건다. &lt;code&gt;id = 1&lt;/code&gt;에 락이 걸려 있어도 &lt;code&gt;id = 2&lt;/code&gt;는 다른 트랜잭션이 자유롭게 수정할 수 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- id=1에만 X락
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- id=2, 3, 4 ... 다른 트랜잭션이 자유롭게 접근 가능
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="갭-락--phantom-read를-막는-방법"&gt;&lt;a href="#%ea%b0%ad-%eb%9d%bd--phantom-read%eb%a5%bc-%eb%a7%89%eb%8a%94-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;갭 락 — Phantom Read를 막는 방법
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;REPEATABLE READ&lt;/code&gt; 격리 수준에서는 같은 쿼리를 두 번 실행해도 결과가 같아야 한다. 하지만 락 없이 범위 조회를 하면, 그 사이 다른 트랜잭션이 새 행을 삽입(INSERT)할 수 있다. 이것이 &lt;strong&gt;Phantom Read&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;InnoDB는 이를 **갭 락(Gap Lock)**으로 막는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- orders에 id: 10, 20, 30이 있다고 가정
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BETWEEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 10~30 범위의 &amp;#34;빈 공간(갭)&amp;#34;에도 락 → 이 범위에 INSERT 불가
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;갭 락은 인덱스 레코드 사이의 공간에 걸린다. 행이 없어도 &amp;ldquo;그 자리&amp;quot;를 잠가서 새 INSERT를 막는다.&lt;/p&gt;
&lt;p&gt;실무에서 갭 락 주의사항: 인덱스가 없는 컬럼으로 UPDATE하면 범위를 특정할 수 없어 테이블 전체에 갭 락이 걸린다. 다른 트랜잭션의 INSERT가 전부 막힌다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="데드락--서로를-기다리는-교착-상태"&gt;&lt;a href="#%eb%8d%b0%eb%93%9c%eb%9d%bd--%ec%84%9c%eb%a1%9c%eb%a5%bc-%ea%b8%b0%eb%8b%a4%eb%a6%ac%eb%8a%94-%ea%b5%90%ec%b0%a9-%ec%83%81%ed%83%9c" class="header-anchor"&gt;&lt;/a&gt;데드락 — 서로를 기다리는 교착 상태
&lt;/h2&gt;&lt;p&gt;두 트랜잭션이 서로 상대방의 락이 풀리기를 기다리면 데드락이 발생한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- TX A -- TX B
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- A가 id=1 X락 획득 -- B가 id=2 X락 획득
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- id=2 락 대기 (B가 가지고 있음) -- id=1 락 대기 (A가 가지고 있음)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- → 영원히 대기 = 데드락
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;InnoDB는 데드락을 자동으로 감지해서 &lt;strong&gt;한쪽 트랜잭션을 강제 롤백&lt;/strong&gt;한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR 1213 (40001): Deadlock found when trying to get lock;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;try restarting transaction
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="데드락-예방-4가지"&gt;&lt;a href="#%eb%8d%b0%eb%93%9c%eb%9d%bd-%ec%98%88%eb%b0%a9-4%ea%b0%80%ec%a7%80" class="header-anchor"&gt;&lt;/a&gt;데드락 예방 4가지
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 모든 트랜잭션에서 테이블/행 접근 순서를 일관되게 유지
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; (항상 id 오름차순으로 락을 잡으면 교차가 발생하지 않는다)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 트랜잭션을 짧게 유지 — 락 보유 시간 최소화
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 인덱스를 통한 정확한 행 접근 — 갭 락 범위 최소화
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. @Transactional(timeout = 5) — 일정 시간 후 자동 롤백
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="낙관적-락-vs-비관적-락--언제-뭘-써야-하나"&gt;&lt;a href="#%eb%82%99%ea%b4%80%ec%a0%81-%eb%9d%bd-vs-%eb%b9%84%ea%b4%80%ec%a0%81-%eb%9d%bd--%ec%96%b8%ec%a0%9c-%eb%ad%98-%ec%8d%a8%ec%95%bc-%ed%95%98%eb%82%98" class="header-anchor"&gt;&lt;/a&gt;낙관적 락 vs 비관적 락 — 언제 뭘 써야 하나
&lt;/h2&gt;&lt;p&gt;충돌이 드문지 잦은지에 따라 전략이 달라진다.&lt;/p&gt;
&lt;h3 id="낙관적-락--충돌이-드물다"&gt;&lt;a href="#%eb%82%99%ea%b4%80%ec%a0%81-%eb%9d%bd--%ec%b6%a9%eb%8f%8c%ec%9d%b4-%eb%93%9c%eb%ac%bc%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;낙관적 락 — &amp;ldquo;충돌이 드물다&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;읽을 때는 락을 걸지 않고, 수정할 때 version 조건을 추가한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 읽기 (락 없음)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 결과: stock=10, version=5
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 수정 (version 조건 추가)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 영향받은 행이 0이면 → 다른 트랜잭션이 먼저 수정 → 애플리케이션에서 재시도
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;JPA에서는 &lt;code&gt;@Version&lt;/code&gt; 어노테이션으로 자동 처리된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;적합한 케이스&lt;/strong&gt;: 게시글 수정, 사용자 프로필 변경 — 동시 수정이 드문 경우&lt;/p&gt;
&lt;h3 id="비관적-락--충돌이-잦다"&gt;&lt;a href="#%eb%b9%84%ea%b4%80%ec%a0%81-%eb%9d%bd--%ec%b6%a9%eb%8f%8c%ec%9d%b4-%ec%9e%a6%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;비관적 락 — &amp;ldquo;충돌이 잦다&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;읽을 때부터 X락을 걸어 다른 트랜잭션의 접근 자체를 막는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- X락
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- 커밋 시 락 해제
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;적합한 케이스&lt;/strong&gt;: 재고 차감, 선착순 쿠폰, 포인트 사용 — 동시 수정이 잦고 정확성이 중요한 경우&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;MVCC는 읽기와 쓰기가 서로를 방해하지 않게 해준다. 행 레벨 락은 필요한 부분만 잠근다. 갭 락은 Phantom Read를 막는다. 그리고 데드락은 락 순서를 통일하면 예방할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@Transactional&lt;/code&gt;을 쓸 때 격리 수준을 설정하는 이유, JPA의 &lt;code&gt;@Version&lt;/code&gt;이 하는 일 — 모두 여기서 나온다. 다음 편에서는 느린 쿼리를 진단하는 EXPLAIN을 다룬다.&lt;/p&gt;</description></item><item><title>[DB 완전 정복 #1] 쿼리가 느린 이유는 대부분 인덱스에 있다</title><link>http://blog.kastori.dev/tech/2026-05-24-db-01-index/</link><pubDate>Sun, 24 May 2026 00:00:00 +0900</pubDate><guid>http://blog.kastori.dev/tech/2026-05-24-db-01-index/</guid><description>&lt;h2 id="인덱스-걸었는데-왜-느리죠"&gt;&lt;a href="#%ec%9d%b8%eb%8d%b1%ec%8a%a4-%ea%b1%b8%ec%97%88%eb%8a%94%eb%8d%b0-%ec%99%9c-%eb%8a%90%eb%a6%ac%ec%a3%a0" class="header-anchor"&gt;&lt;/a&gt;&amp;ldquo;인덱스 걸었는데 왜 느리죠?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;개발하다 보면 이런 상황을 만난다. 분명히 인덱스를 걸었는데 쿼리가 여전히 느리다. &lt;code&gt;EXPLAIN&lt;/code&gt;을 돌려보면 &lt;code&gt;type: ALL&lt;/code&gt;이 찍혀 있다. 풀스캔이다.&lt;/p&gt;
&lt;p&gt;인덱스를 걸었는데 왜 안 탈까? 이 글에서는 그 이유를 포함해서, 인덱스가 실제로 어떻게 동작하는지 처음부터 짚어본다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인덱스는-책의-목차다"&gt;&lt;a href="#%ec%9d%b8%eb%8d%b1%ec%8a%a4%eb%8a%94-%ec%b1%85%ec%9d%98-%eb%aa%a9%ec%b0%a8%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;인덱스는 책의 목차다
&lt;/h2&gt;&lt;p&gt;테이블에서 특정 행을 찾는 방법은 두 가지다. 첫 번째는 처음부터 끝까지 전부 읽는 것(Full Table Scan), 두 번째는 목차를 먼저 보고 해당 페이지로 바로 가는 것(Index Scan)이다.&lt;/p&gt;
&lt;p&gt;100만 건짜리 테이블에서 &lt;code&gt;member_id = 123&lt;/code&gt;인 주문을 찾는다고 해보자.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 인덱스 없음: 100만 건 전부 스캔
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 인덱스 있음: B-Tree로 O(log N) 탐색
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;인덱스는 &lt;strong&gt;조회를 빠르게&lt;/strong&gt; 하지만 &lt;strong&gt;쓰기를 느리게&lt;/strong&gt; 한다. 행을 삽입·수정·삭제할 때마다 인덱스도 같이 업데이트되기 때문이다. 트레이드오프를 이해하고 써야 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="b-tree--mysql-인덱스의-기본-자료구조"&gt;&lt;a href="#b-tree--mysql-%ec%9d%b8%eb%8d%b1%ec%8a%a4%ec%9d%98-%ea%b8%b0%eb%b3%b8-%ec%9e%90%eb%a3%8c%ea%b5%ac%ec%a1%b0" class="header-anchor"&gt;&lt;/a&gt;B-Tree — MySQL 인덱스의 기본 자료구조
&lt;/h2&gt;&lt;p&gt;MySQL InnoDB의 기본 인덱스는 **B-Tree(Balanced Tree)**다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; [ 루트 노드: 50 ]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; [ 내부 노드: 20, 35 ] [ 내부 노드: 65, 80 ]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; / | \ / | \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; [리프] [리프] [리프] [리프] [리프] [리프]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 10,15 20,25 35,40 50,55 65,70 80,90
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;리프 노드끼리는 &lt;strong&gt;이중 연결 리스트&lt;/strong&gt;로 연결되어 있다. 덕분에 범위 검색(&lt;code&gt;BETWEEN&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;)이 효율적이다. 50 이상을 찾으면 50이 있는 리프 노드에서 오른쪽으로 쭉 따라가면 된다.&lt;/p&gt;
&lt;h3 id="clustered-index-vs-secondary-index"&gt;&lt;a href="#clustered-index-vs-secondary-index" class="header-anchor"&gt;&lt;/a&gt;Clustered Index vs Secondary Index
&lt;/h3&gt;&lt;p&gt;InnoDB에는 두 가지 인덱스가 있다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Clustered Index (기본키 인덱스)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 리프 노드에 실제 행 데이터 저장
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 테이블당 1개, 기본키 기준으로 물리적 정렬
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Secondary Index (일반 인덱스)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 리프 노드에 기본키 값만 저장
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 조회 시: 인덱스에서 PK 찾기 → PK로 다시 Clustered Index 조회 (2단계)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Secondary Index 조회는 2단계를 거친다. 이 2단계 과정이 &amp;ldquo;인덱스 랜덤 I/O&amp;quot;다. 후술할 커버링 인덱스가 이 비용을 없애는 방법이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="복합-인덱스--순서가-전부다"&gt;&lt;a href="#%eb%b3%b5%ed%95%a9-%ec%9d%b8%eb%8d%b1%ec%8a%a4--%ec%88%9c%ec%84%9c%ea%b0%80-%ec%a0%84%eb%b6%80%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;복합 인덱스 — 순서가 전부다
&lt;/h2&gt;&lt;p&gt;여러 컬럼을 하나의 인덱스로 묶을 때, &lt;strong&gt;컬럼 순서가 핵심&lt;/strong&gt;이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_member_status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;인덱스는 &lt;strong&gt;왼쪽부터 순서대로&lt;/strong&gt; 사용된다. 이걸 &amp;ldquo;선두 컬럼 규칙&amp;quot;이라 한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- ✅ member_id + status 둘 다 인덱스 사용
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DONE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- ✅ member_id만 인덱스 사용 (선두 컬럼)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- ❌ 인덱스 사용 불가 (선두 컬럼 없음)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DONE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;컬럼 순서 결정 기준&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;카디널리티 높은 것 먼저&lt;/strong&gt; — 값이 다양한 컬럼. &lt;code&gt;member_id&lt;/code&gt;(수백만 종류)가 &lt;code&gt;status&lt;/code&gt;(5종류)보다 앞에&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;등호(&lt;code&gt;=&lt;/code&gt;) 먼저, 범위(&lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;) 나중&lt;/strong&gt; — 범위 조건이 앞에 오면 뒤 컬럼은 인덱스를 못 씀&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id="커버링-인덱스--테이블을-아예-안-보는-방법"&gt;&lt;a href="#%ec%bb%a4%eb%b2%84%eb%a7%81-%ec%9d%b8%eb%8d%b1%ec%8a%a4--%ed%85%8c%ec%9d%b4%eb%b8%94%ec%9d%84-%ec%95%84%ec%98%88-%ec%95%88-%eb%b3%b4%eb%8a%94-%eb%b0%a9%eb%b2%95" class="header-anchor"&gt;&lt;/a&gt;커버링 인덱스 — 테이블을 아예 안 보는 방법
&lt;/h2&gt;&lt;p&gt;쿼리에 필요한 모든 컬럼이 인덱스 안에 있으면, 테이블 본체를 읽지 않아도 된다. 이것이 &lt;strong&gt;커버링 인덱스&lt;/strong&gt;다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;idx_cover&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 커버링 인덱스 적용
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- SELECT 절의 member_id, status, amount가 모두 인덱스에 있음
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DONE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;에서 &lt;code&gt;Extra: Using index&lt;/code&gt;가 보이면 커버링 인덱스가 작동 중이다. 테이블 랜덤 I/O가 없으니 성능이 극적으로 좋아진다.&lt;/p&gt;
&lt;p&gt;JPA + QueryDSL을 쓴다면 페이징 COUNT 쿼리에서 커버링 인덱스를 활용할 수 있다. &lt;code&gt;COUNT(*)&lt;/code&gt;는 실제 행 데이터가 필요 없으니 인덱스만으로 처리 가능하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인덱스가-무력화되는-6가지-패턴"&gt;&lt;a href="#%ec%9d%b8%eb%8d%b1%ec%8a%a4%ea%b0%80-%eb%ac%b4%eb%a0%a5%ed%99%94%eb%90%98%eb%8a%94-6%ea%b0%80%ec%a7%80-%ed%8c%a8%ed%84%b4" class="header-anchor"&gt;&lt;/a&gt;인덱스가 무력화되는 6가지 패턴
&lt;/h2&gt;&lt;p&gt;인덱스를 걸었는데도 안 타는 경우가 있다. 실수하기 쉬운 패턴들이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 1. 컬럼에 함수 적용
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;YEAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ❌ 인덱스 무력화
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2026-01-01&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ✅ 범위로 변환
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 2. 앞쪽 와일드카드 LIKE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;%홍길동&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ❌
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;홍길동%&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ✅ 뒤 와일드카드는 인덱스 사용
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 3. 컬럼이 다른 OR 조건
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DONE&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ❌ (보통 풀스캔)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- → UNION으로 분리하거나 각각 인덱스 조회 후 합치는 방식 검토
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 4. 데이터 타입 불일치
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;123&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ❌ INT 컬럼에 문자열 → 묵시적 형변환
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;member_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ✅
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 5. 부정 조건
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DONE&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ❌ 대부분 풀스캔
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PENDING&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PROCESSING&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- ✅ 이 패턴으로 변환
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 6. 카디널리티 낮은 컬럼
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- status가 &amp;#39;Y&amp;#39;/&amp;#39;N&amp;#39; 두 가지뿐이면 옵티마이저가 인덱스 대신 풀스캔 선택
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- (50% 이상 행을 어차피 읽어야 하니 풀스캔이 더 효율적이라 판단)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="인덱스를-걸어야-할-때-피해야-할-때"&gt;&lt;a href="#%ec%9d%b8%eb%8d%b1%ec%8a%a4%eb%a5%bc-%ea%b1%b8%ec%96%b4%ec%95%bc-%ed%95%a0-%eb%95%8c-%ed%94%bc%ed%95%b4%ec%95%bc-%ed%95%a0-%eb%95%8c" class="header-anchor"&gt;&lt;/a&gt;인덱스를 걸어야 할 때, 피해야 할 때
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✅ 인덱스를 걸어야 하는 경우
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - WHERE 절에 자주 등장하는 컬럼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - JOIN 조건으로 쓰이는 컬럼 (FK)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - ORDER BY, GROUP BY에 사용되는 컬럼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 카디널리티가 높은 컬럼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;❌ 인덱스를 피해야 하는 경우
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 카디널리티가 낮은 컬럼 (성별, Y/N 등)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 자주 변경되는 컬럼 (INSERT/UPDATE/DELETE 비용 증가)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 테이블 데이터가 아주 적은 경우 (풀스캔이 오히려 빠름)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 인덱스가 너무 많으면 쓰기 성능 전체 저하
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;쿼리가 느리다&amp;quot;는 증상의 원인 대부분은 인덱스에 있다. 인덱스가 없거나, 있어도 무력화되거나, 복합 인덱스 순서가 잘못됐거나.&lt;/p&gt;
&lt;p&gt;느린 쿼리를 만나면 가장 먼저 &lt;code&gt;EXPLAIN&lt;/code&gt;을 돌려보자. &lt;code&gt;type: ALL&lt;/code&gt;이 보이면 인덱스 문제다. 다음 편에서는 EXPLAIN을 읽는 방법과 트랜잭션·락 동작 원리를 다룬다.&lt;/p&gt;</description></item></channel></rss>