<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>천천히 가는 것을 걱정하지 말고 서있는 것을 걱정하라.</title>
    <link>https://soobarkbar.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 13 Aug 2026 04:53:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>기내식은수박바</managingEditor>
    <image>
      <title>천천히 가는 것을 걱정하지 말고 서있는 것을 걱정하라.</title>
      <url>https://tistory1.daumcdn.net/tistory/3076242/attach/d9ec458535e6467b967018217189ee98</url>
      <link>https://soobarkbar.tistory.com</link>
    </image>
    <item>
      <title>Oracle 정규표현식 함수 정리</title>
      <link>https://soobarkbar.tistory.com/265</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 데이터를 다루다 보면 단순한 &lt;code&gt;LIKE&lt;/code&gt;만으로는 처리하기 어려운 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 작업이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자열이 숫자로만 구성되어 있는지 검사&lt;/li&gt;
&lt;li&gt;이메일과 비슷한 형식인지 검사&lt;/li&gt;
&lt;li&gt;문자열에서 숫자만 제거하거나 추출&lt;/li&gt;
&lt;li&gt;쉼표로 구분된 문자열에서 특정 번째 값 추출&lt;/li&gt;
&lt;li&gt;특정 패턴이 문자열에 몇 번 등장하는지 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서는 이러한 작업을 위해 &lt;code&gt;REGEXP_LIKE&lt;/code&gt;, &lt;code&gt;REGEXP_REPLACE&lt;/code&gt;, &lt;code&gt;REGEXP_SUBSTR&lt;/code&gt;, &lt;code&gt;REGEXP_INSTR&lt;/code&gt;, &lt;code&gt;REGEXP_COUNT&lt;/code&gt;와 같은 정규표현식 함수를 제공한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. 정규표현식 기본 패턴&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규표현식은 일반 문자와 특별한 의미를 갖는 메타 문자를 조합해 문자열의 패턴을 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;[0-9]+&lt;/code&gt;는 &lt;b&gt;숫자가 1개 이상 연속되는 문자열&lt;/b&gt;을 의미하고,&lt;br /&gt;&lt;code&gt;^[0-9]+$&lt;/code&gt;는 &lt;b&gt;문자열 전체가 숫자로만 구성되어 있음&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;.&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임의의 문자 1개를 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;a.c&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 문자열이 매칭될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;abc
a1c
a-c&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;a&lt;/code&gt;와 &lt;code&gt;c&lt;/code&gt; 사이에 어떤 문자든 정확히 한 글자가 존재하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;^&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열의 시작을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;^A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 문자열은 매칭된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Apple
ABC
A123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 다음 문자열은 매칭되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;BA
123A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;A&lt;/code&gt;가 문자열의 첫 번째 위치에 있어야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;$&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열의 끝을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Z$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 &lt;code&gt;Z&lt;/code&gt;로 끝나는 문자열을 찾는다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ABCZ
123Z
Z&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;*&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 패턴이 &lt;b&gt;0번 이상&lt;/b&gt; 반복되는 것을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ab*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 문자열이 매칭될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;a
ab
abb
abbb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;*&lt;/code&gt;가 적용되는 대상은 바로 앞의 &lt;code&gt;b&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;b&lt;/code&gt;가 없어도 되고 여러 번 나타나도 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;+&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 패턴이 &lt;b&gt;1번 이상&lt;/b&gt; 반복되는 것을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ab+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 문자열은 매칭된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ab
abb
abbb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음 문자열은 매칭되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;a&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;*&lt;/code&gt;와 달리 최소 한 번은 나타나야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;?&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 패턴이 &lt;b&gt;0번 또는 1번&lt;/b&gt; 나타나는 것을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ab?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 두 형태가 가능하다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;a
ab&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;b&lt;/code&gt;가 두 번 이상 반복되는 &lt;code&gt;abb&lt;/code&gt;는 이 패턴 자체와는 일치하지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;{n}&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 패턴이 정확히 &lt;code&gt;n&lt;/code&gt;번 반복되는 것을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[0-9]{3}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자 3개가 연속되는 부분을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;123
007
999&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;{n,}&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 패턴이 &lt;code&gt;n&lt;/code&gt;번 이상 반복되는 것을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[0-9]{2,}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자가 최소 2개 이상 연속되는 부분을 찾는다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;12
123
123456&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;{n,m}&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 패턴이 최소 &lt;code&gt;n&lt;/code&gt;번, 최대 &lt;code&gt;m&lt;/code&gt;번 반복되는 것을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[0-9]{2,4}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자가 2개 이상 4개 이하 연속되는 패턴을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;12
123
1234&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;|&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OR 조건을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;A|B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;A&lt;/code&gt; 또는 &lt;code&gt;B&lt;/code&gt;에 매칭된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;A
B&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;()&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 패턴을 하나의 그룹으로 묶는다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;(ab)+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;+&lt;/code&gt;는 &lt;code&gt;b&lt;/code&gt;에만 적용되는 것이 아니라 &lt;code&gt;(ab)&lt;/code&gt;라는 그룹 전체에&lt;br /&gt;적용된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ab
abab
ababab&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;ab&lt;/code&gt;라는 문자열이 한 번 이상 반복되는 패턴이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자 집합을 의미하며 대괄호 안에 지정한 문자 중 &lt;b&gt;한 글자&lt;/b&gt;와 매칭된다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[abc]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 문자 중 하나와 매칭된다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;a
b
c&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범위를 지정할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[0-9]
[A-Z]
[a-z]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 숫자 한 글자, 영문 대문자 한 글자, 영문 소문자 한 글자를 의미한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;[^]&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대괄호 내부에서 &lt;code&gt;^&lt;/code&gt;를 사용하면 부정 문자 집합을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[^0-9]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자가 아닌 문자 한 글자와 매칭된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 문자열에서&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;ABC123&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, &lt;code&gt;C&lt;/code&gt;가 &lt;code&gt;[^0-9]&lt;/code&gt;에 해당한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;^&lt;/code&gt;는 위치에 따라 의미가 다르다. 정규표현식 맨 앞의 &lt;code&gt;^&lt;/code&gt;는 문자열의&lt;br /&gt;시작을 의미하지만, 문자 집합 &lt;code&gt;[]&lt;/code&gt; 내부의 첫 위치에서 사용한 &lt;code&gt;^&lt;/code&gt;는 해당&lt;br /&gt;문자 집합의 부정을 의미한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. 문자열 전체 검사와 부분 매칭의 차이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규표현식을 사용할 때 특히 주의해야 하는 부분이 &lt;b&gt;문자열 일부가 패턴에 맞는 것&lt;/b&gt;과 &lt;b&gt;문자열 전체가 패턴에 맞는 것&lt;/b&gt;의 차이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[0-9]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 패턴은 숫자 한 글자를 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음 문자열도 숫자 부분이 존재하므로 매칭된다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;ABC1DEF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 문자열 전체가 숫자로만 구성되어 있는지 검사하려면 시작과 끝을 함께&lt;br /&gt;지정해야 한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;^[0-9]+$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 나누어 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;^
│
│   [0-9]+
│      │
│      └─ 숫자가 1개 이상
│
└─ 문자열 시작

            $
            │
            └─ 문자열 끝&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음 문자열은 매칭된다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;123
007
123456&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 문자열은 매칭되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ABC123
123ABC
12A34&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 방식으로 문자열 전체의 문자 종류를 검사할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;^[0-9]+$       전체 문자열이 숫자로만 구성
^[A-Za-z]+$    전체 문자열이 영문자로만 구성
^[가-힣]+$      전체 문자열이 한글 음절로만 구성&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[가-힣]&lt;/code&gt;은 일반적인 완성형 한글 음절 범위를 검사할 때 유용하지만,&lt;br /&gt;모든 Unicode 한글 표현을 포괄하는 패턴은 아니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. REGEXP_LIKE&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;REGEXP_LIKE&lt;/code&gt;는 문자열이 특정 정규표현식 패턴과 매칭되는지 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;REGEXP_LIKE(문자열, 정규표현식, 옵션)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 &lt;code&gt;LIKE&lt;/code&gt;가 &lt;code&gt;%&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;를 이용한 단순 패턴 검색이라면 &lt;code&gt;REGEXP_LIKE&lt;/code&gt;는 반복 횟수, 문자 집합, 문자열 시작과 끝 등 훨씬 복 잡한 조건을 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;email&lt;/code&gt; 컬럼이 이메일 형식에 가까운 문자열인지 검사해보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM employees
WHERE REGEXP_LIKE(
    email,
    '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴을 나누어 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;^[A-Za-z0-9._%+-]+
        │
        └─ @ 앞부분

@
│
└─ @ 문자

[A-Za-z0-9.-]+
        │
        └─ 도메인 부분

\.
│
└─ 점(.) 문자

[A-Za-z]{2,}$
        │
        └─ 영문자 2개 이상으로 끝남&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;.&lt;/code&gt;은 원래 임의의 문자 한 개라는 특별한 의미를 가지므로, 실제 점 문자를 표현하기 위해 &lt;code&gt;\.&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션을 사용하면 대소문자 구분 등의 매칭 방식을 지정할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM employees
WHERE REGEXP_LIKE(ename, '^smith$', 'i');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;i&lt;/code&gt; 옵션은 대소문자를 구분하지 않도록 한다. 따라서 &lt;code&gt;SMITH&lt;/code&gt;, &lt;code&gt;Smith&lt;/code&gt;, &lt;code&gt;smith&lt;/code&gt; 등이 매칭될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. REGEXP_REPLACE&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;REGEXP_REPLACE&lt;/code&gt;는 정규표현식에 매칭되는 부분을 다른 문자열로 치환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;REGEXP_REPLACE(문자열, 패턴, 변경할 문자열)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 문자열에 공백이 여러 개 연속해서 들어 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT REGEXP_REPLACE(
    'abc   def     ghi',
    ' +',
    ' '
) AS result
FROM dual;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;abc def ghi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴 &lt;code&gt;' +'&lt;/code&gt;는 다음과 같이 해석할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;' '
 │
 └─ 공백

+
│
└─ 앞의 공백이 1번 이상 반복&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 연속된 여러 공백을 하나의 공백으로 치환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 자주 사용할 수 있는 패턴은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 숫자만 남기기
REGEXP_REPLACE(col, '[^0-9]', '')

-- 영문자만 남기기
REGEXP_REPLACE(col, '[^A-Za-z]', '')

-- 공백 문자 제거
REGEXP_REPLACE(col, '\s', '')

-- 연속된 일반 공백을 하나로 변경
REGEXP_REPLACE(col, ' +', ' ')

-- 영문/숫자/한글 음절을 제외한 문자 제거
REGEXP_REPLACE(col, '[^A-Za-z0-9가-힣]', '')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 전화번호에서 숫자만 남기고 싶다면 다음과 같이 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT REGEXP_REPLACE(
    '010-1234-5678',
    '[^0-9]',
    ''
) AS phone_no
FROM dual;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;01012345678&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[^0-9]&lt;/code&gt;가 숫자가 아닌 문자를 의미하므로 &lt;code&gt;-&lt;/code&gt;가 제거된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. REGEXP_SUBSTR&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;REGEXP_SUBSTR&lt;/code&gt;는 문자열에서 정규표현식에 매칭되는 &lt;b&gt;문자열 자체를 추출&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;REGEXP_SUBSTR(문자열, 패턴, 시작위치, 몇번째매칭)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문번호가 포함된 문자열에서 주문번호만 추출해보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT REGEXP_SUBSTR(
    'Order No: ABC-12345',
    '[A-Z]+-[0-9]+'
) AS order_no
FROM dual;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ABC-12345&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴은 다음과 같이 해석된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[A-Z]+
│
└─ 대문자 1개 이상

-
│
└─ 하이픈 문자

[0-9]+
│
└─ 숫자 1개 이상&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉼표로 구분된 문자열에서 두 번째 값을 가져오는 것도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT REGEXP_SUBSTR(
    'apple,banana,orange',
    '[^,]+',
    1,
    2
) AS second_value
FROM dual;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;banana&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 인수의 의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;apple,banana,orange
│
└─ 대상 문자열

[^,]+
│
└─ 쉼표가 아닌 문자가 1개 이상

1
│
└─ 첫 번째 문자부터 검색

2
│
└─ 두 번째 매칭 결과 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 매칭 과정은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;apple,banana,orange
───── ────── ──────
  1      2       3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 두 번째 매칭 결과인 &lt;code&gt;banana&lt;/code&gt;가 반환된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. REGEXP_INSTR&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;REGEXP_INSTR&lt;/code&gt;는 패턴에 매칭된 문자열 자체가 아니라 &lt;b&gt;매칭된 위치&lt;/b&gt;를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;REGEXP_INSTR(문자열, 패턴)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 문자열에서 숫자가 시작되는 위치를 찾아보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT REGEXP_INSTR(
    'abc123def',
    '[0-9]+'
) AS pos
FROM dual;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자 위치를 표시하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;문자 : a b c 1 2 3 d e f
위치 : 1 2 3 4 5 6 7 8 9
            &amp;uarr;
        숫자 시작 위치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[0-9]+&lt;/code&gt;에 처음 매칭되는 문자열은 &lt;code&gt;123&lt;/code&gt;이고 이 문자열은 네 번째 위치에서 시작한다. 따라서 &lt;code&gt;4&lt;/code&gt;가 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;REGEXP_SUBSTR&lt;/code&gt;와 비교하면 차이가 명확하다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;REGEXP_SUBSTR &amp;rarr; 무엇이 매칭되었는가?
REGEXP_INSTR  &amp;rarr; 어디에서 매칭되었는가?&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. REGEXP_COUNT&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;REGEXP_COUNT&lt;/code&gt;는 문자열에서 정규표현식 패턴이 몇 번 매칭되는지 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 형태는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;REGEXP_COUNT(문자열, 패턴)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 쉼표의 개수를 계산해보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT REGEXP_COUNT(
    'apple,banana,orange',
    ','
) AS comma_count
FROM dual;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자가 몇 개 있는지가 아니라 &lt;b&gt;숫자 그룹이 몇 번 등장하는지&lt;/b&gt; 계산할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT REGEXP_COUNT(
    'A12 B34 C567',
    '[0-9]+'
) AS number_group_count
FROM dual;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매칭되는 문자열은 다음 세 개다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;A12 B34 C567
 └┘  └┘  └─┘
 12  34   567&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;[0-9]&lt;/code&gt;가 아니라 &lt;code&gt;[0-9]+&lt;/code&gt;를 사용했다는 점이 중요하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT REGEXP_COUNT(
    'A12 B34 C567', 
    '[0-9]'
)
FROM dual;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 작성하면 각각의 숫자 한 글자가 별도의 매칭이므로 결과는&lt;br /&gt;&lt;code&gt;7&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;code&gt;[0-9]+&lt;/code&gt;는 연속된 숫자를 하나의 그룹으로 매칭하므로 결과는 &lt;code&gt;3&lt;/code&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. Oracle 정규표현식 함수 비교&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 함수의 차이는 결국 &lt;b&gt;패턴을 찾은 뒤 무엇을 반환하거나 수행하는가&lt;/b&gt;에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 역할 대표적인 사용 목적&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;REGEXP_LIKE&lt;/code&gt; : 패턴 매칭 여부 검사 형식 검증, 조건 검색&lt;br /&gt;&lt;code&gt;REGEXP_REPLACE&lt;/code&gt; : 매칭 문자열 치환 문자 제거, 데이터 정제&lt;br /&gt;&lt;code&gt;REGEXP_SUBSTR&lt;/code&gt; : 매칭 문자열 반환 특정 값 추출&lt;br /&gt;&lt;code&gt;REGEXP_INSTR&lt;/code&gt; : 매칭 위치 반환 특정 패턴의 위치 검색&lt;br /&gt;&lt;code&gt;REGEXP_COUNT&lt;/code&gt; : 매칭 횟수 반환 구분자, 패턴 등장 횟수 계산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 문자열이 있다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ABC-123-DEF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자 &lt;code&gt;123&lt;/code&gt;을 대상으로 생각하면 각 함수의 역할은 다음과 같이 구분할 수&lt;br /&gt;있다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;ABC-123-DEF
    └─┘
     │
     ├─ REGEXP_LIKE    &amp;rarr; 숫자 그룹이 존재하는가?
     ├─ REGEXP_REPLACE &amp;rarr; 숫자 그룹을 다른 값으로 바꾼다
     ├─ REGEXP_SUBSTR  &amp;rarr; &quot;123&quot;을 반환한다
     ├─ REGEXP_INSTR   &amp;rarr; 숫자 그룹의 시작 위치를 반환한다
     └─ REGEXP_COUNT   &amp;rarr; 숫자 그룹이 몇 번 등장하는지 반환한다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. 정규표현식 사용 시 주의할 점&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규표현식은 복잡한 문자열 조건을 간결하게 표현할 수 있지만, 단순 문자열 비교보다 처리 비용이 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 단순히 특정 접두어로 시작하는 데이터를 찾는 조건이라면 다음과 같이 &lt;code&gt;LIKE&lt;/code&gt;로 충분히 표현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WHERE col LIKE 'ABC%'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 굳이 다음처럼 정규표현식으로 작성할 필요는 없다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE REGEXP_LIKE(col, '^ABC')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 대량 데이터의 &lt;code&gt;WHERE&lt;/code&gt; 조건에서 컬럼에 정규표현식 함수를 적용하면 일반적인 B-Tree 인덱스를 그대로 활용하기 어려운 실행 계획이 나올 수 있으므로, 정규표현식은 &lt;b&gt;복잡한 패턴 검증이나 문자열 가공이 실제로 필요한 경우&lt;/b&gt;에 사용하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 정규표현식은 비슷해 보여도 앵커의 유무에 따라 의미가 크게 달라진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[0-9]+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 패턴은 문자열 중간에 숫자 그룹이 하나라도 있으면 매칭된다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;ABC123DEF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 다음 패턴은&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;^[0-9]+$&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열의 시작부터 끝까지 숫자로만 구성되어 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규표현식을 작성할 때는 항상 &lt;b&gt;부분 문자열을 찾는 것인지, 문자열 전체의 형식을 검사하는 것인지&lt;/b&gt; 먼저 구분하는 것이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 정규표현식 함수는 복잡한 문자열 검사와 가공을 SQL 내부에서&lt;br /&gt;처리할 수 있게 해준다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;REGEXP_LIKE
    └─ 패턴과 일치하는지 검사

REGEXP_REPLACE
    └─ 패턴에 일치하는 문자열 치환

REGEXP_SUBSTR
    └─ 패턴에 일치하는 문자열 추출

REGEXP_INSTR
    └─ 패턴에 일치하는 위치 반환

REGEXP_COUNT
    └─ 패턴이 등장하는 횟수 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규표현식을 사용할 때 가장 먼저 익혀두면 좋은 패턴은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;.            임의의 문자 1개
^            문자열 시작
$            문자열 끝
*            0번 이상 반복
+            1번 이상 반복
?            0번 또는 1번
{n}          정확히 n번
{n,}         n번 이상
{n,m}        n번 이상 m번 이하
|            OR
()           그룹
[]           문자 집합
[^]          부정 문자 집합&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;code&gt;[0-9]+&lt;/code&gt;처럼 &lt;b&gt;문자열 일부를 찾는 패턴&lt;/b&gt;과 &lt;code&gt;^[0-9]+$&lt;/code&gt;처럼 &lt;b&gt;문자열 전체를 검사하는 패턴&lt;/b&gt;의 차이를 이해해두면 정규표현식을 사용할 때 발생하는 많은 혼동을 줄일 수 있다.&lt;/p&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/265</guid>
      <comments>https://soobarkbar.tistory.com/265#entry265comment</comments>
      <pubDate>Sat, 8 Aug 2026 01:17:04 +0900</pubDate>
    </item>
    <item>
      <title>Oracle 실행 계획의 Sort 오퍼레이션 완전 정리</title>
      <link>https://soobarkbar.tistory.com/264</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 보다 보면 &lt;code&gt;SORT AGGREGATE&lt;/code&gt;, &lt;code&gt;SORT ORDER BY&lt;/code&gt;, &lt;code&gt;SORT GROUP BY&lt;/code&gt;, &lt;code&gt;SORT UNIQUE&lt;/code&gt;와 같은 Sort 오퍼레이션을 자주 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 이름에 &lt;b&gt;Sort&lt;/b&gt;가 들어가지만 역할은 서로 다르다. 어떤 오퍼레이션은 실제 정렬을 수행하고, 어떤 오퍼레이션은 이름과 달리 정렬을 수행하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 각 Sort 오퍼레이션이 언제 발생하는지와 내부 동작을 그림과 함께 정리해보자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;1. SORT AGGREGATE&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 로우를 대상으로 하나의 집계 결과를 만들 때 발생한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름에 Sort가 포함되어 있지만 실제 정렬은 수행하지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT SUM(sal),
       MAX(sal),
       MIN(sal)
FROM emp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 흐름&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;EMP
 │
 ▼
모든 Row 읽기
 │
 ▼
SUM / MAX / MIN 계산
 │
 ▼
결과 1건 반환&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. SORT ORDER BY&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORDER BY로 결과를 정렬할 때 발생한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM emp
ORDER BY sal DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 흐름&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;테이블 읽기
    │
    ▼

5000
3000
4000
1000
2000

    │
    ▼

SORT ORDER BY

    │
    ▼

5000
4000
3000
2000
1000&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. SORT GROUP BY&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소팅 알고리즘을 이용해 같은 그룹을 모은 뒤 집계를 수행한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT deptno,
       job,
       SUM(sal)
FROM emp
GROUP BY deptno, job
ORDER BY deptno, job;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 흐름&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;테이블 읽기
      │
      ▼

10 CLERK
20 MANAGER
10 CLERK
30 SALESMAN
20 MANAGER

      │
      ▼

(deptno, job) 기준 정렬

10 CLERK
10 CLERK
20 MANAGER
20 MANAGER
30 SALESMAN

      │
      ▼

연속된 같은 그룹 집계

10 CLERK    SUM
20 MANAGER  SUM
30 SALESMAN SUM&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HASH GROUP BY와 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10gR2부터는 ORDER BY가 없다면 HASH GROUP BY가 선택되는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SORT GROUP BY가 먼저 정렬한 뒤 그룹을 찾는다면, HASH GROUP BY는 Hash Bucket을 이용해 그룹을 바로 찾아간다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;입력 데이터

10 1000
20 2000
10 1500
30 3000
20 1800

        │
        ▼

   Hash Function

        │
        ▼

+----------------------+
| Bucket #1            |
| dept=10              |
| SUM=2500             |
+----------------------+

+----------------------+
| Bucket #2            |
| dept=20              |
| SUM=3800             |
+----------------------+

+----------------------+
| Bucket #3            |
| dept=30              |
| SUM=3000             |
+----------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 Row인 &lt;code&gt;(20, 500)&lt;/code&gt;을 읽으면 다음과 같이 처리된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(20, 500)
     │
     ▼

Hash(20)

     │
     ▼

Bucket #2 발견

     │
     ▼

SUM = 3800

     │
     ▼

SUM = 4300&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 같은 그룹을 찾기 위해 정렬하지 않고 Hash Bucket을 찾아 집계값을 계속 갱신하는 방식이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. SORT UNIQUE&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 제거가 필요한 경우 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 경우는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unnesting된 서브쿼리의 중복 제거&lt;/li&gt;
&lt;li&gt;UNION / MINUS / INTERSECT&lt;/li&gt;
&lt;li&gt;DISTINCT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PK, UNIQUE 제약 또는 UNIQUE Index로 중복이 이미 보장된다면 SORT UNIQUE는 생략될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT DISTINCT deptno
FROM emp
ORDER BY deptno;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SORT UNIQUE 내부 동작&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;                 테이블 읽기
                      │
                      ▼

      30 10 20 10 40 20 30

                      │
                      ▼

      PGA Sort Area에 데이터 적재

                      │
                      ▼

              오름차순 정렬

      10 10 20 20 30 30 40

                      │
                      ▼

      이전 Row와 비교하며 중복 제거

      10  ✓
      10  ✗
      20  ✓
      20  ✗
      30  ✓
      30  ✗
      40  ✓

                      │
                      ▼

          최종 결과

      10 20 30 40&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HASH UNIQUE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10gR2부터 ORDER BY가 없는 DISTINCT는 HASH UNIQUE가 선택되는 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;                 테이블 읽기
                      │
                      ▼

      30 10 20 10 40 20 30

                      │
                      ▼

          Hash Function 계산

                      │
                      ▼

+-------------------------+
| Bucket1 : 20           |
| Bucket2 : 10           |
| Bucket3 : 30           |
| Bucket4 : 40           |
+-------------------------+

      ▲
      │
이미 존재하면 버림
없으면 Bucket에 저장

      │
      ▼

      최종 결과

      20 10 30 40&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash Unique는 정렬 과정이 없기 때문에 결과 순서가 중요하지 않고, DISTINCT 대상 컬럼 수가 적으며 메모리가 충분한 경우 유리하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. SORT JOIN&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sort Merge Join을 수행할 때 발생한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT /*+ ordered use_merge(e) */ *
FROM dept d,
     emp e
WHERE d.deptno = e.deptno;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 흐름&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;DEPT 정렬
      │
      ├────────────┐
      │            │
EMP 정렬           │
      │            │
      ▼            ▼

    Merge Join&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. WINDOW SORT&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석 함수를 수행할 때 발생한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT empno,
       AVG(sal) OVER(PARTITION BY deptno)
FROM emp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 흐름&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;테이블 읽기
      │
      ▼

PARTITION 생성

      │
      ▼

PARTITION 내부 정렬

      │
      ▼

Window Function 계산&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;정리&lt;/h1&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SORT AGGREGATE&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;전체 집계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SORT ORDER BY&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;결과 정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SORT GROUP BY&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;정렬 후 그룹 집계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HASH GROUP BY&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Hash Bucket으로 그룹 집계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SORT UNIQUE&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;정렬 후 중복 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HASH UNIQUE&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Hash Table로 중복 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SORT JOIN&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Sort Merge Join&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WINDOW SORT&lt;/td&gt;
&lt;td&gt;대부분 ✅&lt;/td&gt;
&lt;td&gt;분석 함수 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/264</guid>
      <comments>https://soobarkbar.tistory.com/264#entry264comment</comments>
      <pubDate>Fri, 31 Jul 2026 01:31:35 +0900</pubDate>
    </item>
    <item>
      <title>IN 조건은 항상 인덱스 Access 조건으로 쓰는 것이 좋을까?</title>
      <link>https://soobarkbar.tistory.com/263</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 튜닝을 하다 보면 &lt;code&gt;IN&lt;/code&gt; 조건을 자주 사용한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE status IN ('PAID', 'READY', 'DONE')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로 보면 단순히 여러 값을 비교하는 조건처럼 보인다.&lt;br /&gt;하지만 실행계획 관점에서 보면 &lt;code&gt;IN&lt;/code&gt; 조건은 생각보다 중요한 차이를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Oracle에서는 &lt;code&gt;INLIST ITERATOR&lt;/code&gt;라는 실행 방식으로 나타날 수 있고, 이 경우 &lt;code&gt;IN&lt;/code&gt; 목록의 각 값을 기준으로 인덱스를 반복 탐색한다. 반대로 어떤 경우에는 &lt;code&gt;IN&lt;/code&gt; 조건이 인덱스 탐색 범위를 만드는 데 사용되지 않고, 이미 읽은 데이터에 대해 값이 맞는지 확인하는 &lt;code&gt;filter&lt;/code&gt; 조건으로 사용되기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 &lt;code&gt;IN&lt;/code&gt; 조건이 인덱스에서 어떻게 동작하는지, 그리고 왜 항상 &lt;code&gt;INLIST ITERATOR&lt;/code&gt;가 좋은 것은 아닌지 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. IN 조건의 두 가지 사용 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;IN&lt;/code&gt; 조건은 실행계획에서 크게 두 가지 성격으로 사용될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 &lt;b&gt;인덱스 Access 조건&lt;/b&gt;으로 사용하는 방식이다.&lt;br /&gt;두 번째는 &lt;b&gt;Filter 조건&lt;/b&gt;으로 사용하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 조건이 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE status IN ('PAID', 'READY', 'DONE')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건이 인덱스에서 읽을 범위를 직접 줄이는 데 사용되면 &lt;code&gt;access&lt;/code&gt; 조건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다른 조건으로 먼저 데이터를 읽은 뒤, 읽어온 데이터의 &lt;code&gt;status&lt;/code&gt; 값이 &lt;code&gt;'PAID'&lt;/code&gt;, &lt;code&gt;'READY'&lt;/code&gt;, &lt;code&gt;'DONE'&lt;/code&gt; 중 하나인지 확인하는 데 사용되면 &lt;code&gt;filter&lt;/code&gt; 조건이다.&lt;/p&gt;
&lt;table style=&quot;height: 77px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;의미&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;동작 방식&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Access 조건&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;인덱스에서 읽을 범위를 직접 결정&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;조건에 맞는 인덱스 구간을 찾아감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Filter 조건&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;읽은 데이터가 조건에 맞는지 확인&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;먼저 읽고 나중에 걸러냄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;INLIST ITERATOR&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;IN&lt;/code&gt; 값을 여러 개의 &lt;code&gt;=&lt;/code&gt; 조건처럼 반복 적용&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;IN&lt;/code&gt; 값 개수만큼 인덱스 탐색이 반복될 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로는 &lt;code&gt;access&lt;/code&gt; 조건이 &lt;code&gt;filter&lt;/code&gt; 조건보다 좋아 보인다.&lt;br /&gt;왜냐하면 &lt;code&gt;access&lt;/code&gt; 조건은 처음부터 읽을 범위를 줄이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;IN&lt;/code&gt; 조건에서는 항상 그렇지 않다.&lt;br /&gt;&lt;code&gt;INLIST ITERATOR&lt;/code&gt;는 &lt;code&gt;IN&lt;/code&gt; 값 개수만큼 인덱스 탐색을 반복할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. INLIST ITERATOR란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서 &lt;code&gt;INLIST ITERATOR&lt;/code&gt;는 &lt;code&gt;IN&lt;/code&gt; 절의 각 값을 equality 조건처럼 공급하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 조건이 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE status IN ('PAID', 'READY', 'DONE')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 개념적으로 다음과 비슷하게 동작할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;WHERE status = 'PAID'
OR
WHERE status = 'READY'
OR
WHERE status = 'DONE'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 실제 SQL이 이렇게 변환된다는 뜻은 아니다.&lt;br /&gt;개념적으로 &lt;code&gt;IN&lt;/code&gt; 목록의 각 값에 대해 인덱스 range scan이 반복될 수 있다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스가 다음과 같다고 하자.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(status)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM orders
WHERE status IN ('PAID', 'READY', 'DONE');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;INLIST ITERATOR&lt;/code&gt;가 사용되면 개념적으로 다음처럼 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;1. status = 'PAID' 구간 탐색
2. status = 'READY' 구간 탐색
3. status = 'DONE' 구간 탐색&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree 인덱스 관점에서는 각 값에 대해 루트 블록에서 시작해 브랜치 블록을 거쳐 리프 블록의 해당 위치를 찾는 과정이 반복될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;IN ('PAID', 'READY', 'DONE')

PAID  탐색: root &amp;rarr; branch &amp;rarr; leaf
READY 탐색: root &amp;rarr; branch &amp;rarr; leaf
DONE  탐색: root &amp;rarr; branch &amp;rarr; leaf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 말이 매번 물리 I/O가 발생한다는 뜻은 아니다.&lt;br /&gt;루트 블록이나 브랜치 블록은 버퍼 캐시에 올라와 있을 가능성이 높고, 실제 비용은 캐시 상태, leaf block 범위, table access 방식에 따라 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 &lt;b&gt;논리적으로 &lt;code&gt;IN&lt;/code&gt; 값 개수만큼 탐색 단위가 늘어날 수 있다&lt;/b&gt;는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. INLIST ITERATOR가 유리한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;INLIST ITERATOR&lt;/code&gt;는 &lt;code&gt;IN&lt;/code&gt; 값 개수가 적고, 각 값의 선택도가 낮을 때 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인덱스가 다음과 같다고 하자.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(status, created_at)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM orders
WHERE status IN ('FAILED', 'CANCELLED')
  AND created_at &amp;gt;= TIMESTAMP '2026-07-01 00:00:00'
  AND created_at &amp;lt;  TIMESTAMP '2026-08-01 00:00:00';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 인덱스 선두 컬럼인 &lt;code&gt;status&lt;/code&gt;에 &lt;code&gt;IN&lt;/code&gt; 조건이 걸려 있다.&lt;br /&gt;DB는 다음과 같은 범위를 탐색할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;scheme&quot;&gt;&lt;code&gt;(status = 'FAILED',    created_at 7월 범위)
(status = 'CANCELLED', created_at 7월 범위)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(status, created_at) 인덱스

CANCELLED  2026-07-01 ~ 2026-07-31  &amp;larr; 읽음
DONE       ...
FAILED     2026-07-01 ~ 2026-07-31  &amp;larr; 읽음
PAID       ...
READY      ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FAILED&lt;/code&gt;, &lt;code&gt;CANCELLED&lt;/code&gt; 데이터가 전체에서 적은 비율이라면 이 방식은 효율적이다.&lt;br /&gt;필요한 상태값의 구간만 찾아가고, 그 안에서 날짜 범위까지 적용할 수 있기 때문이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&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;IN&lt;/code&gt; 값 개수가 적음&lt;/td&gt;
&lt;td&gt;반복 탐색 횟수가 적다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;각 &lt;code&gt;IN&lt;/code&gt; 값의 데이터가 적음&lt;/td&gt;
&lt;td&gt;선택도가 낮다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IN&lt;/code&gt; 컬럼이 인덱스 선두에 있음&lt;/td&gt;
&lt;td&gt;인덱스 탐색 범위를 직접 만들 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;뒤쪽 컬럼에 추가 범위 조건이 있음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(IN 값, 범위 조건)&lt;/code&gt; 조합으로 더 좁게 읽을 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 &lt;code&gt;IN&lt;/code&gt; 조건이 &lt;code&gt;access&lt;/code&gt; 조건으로 사용되는 것이 유리할 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. INLIST ITERATOR가 불리할 수 있는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &lt;code&gt;IN&lt;/code&gt; 값이 많고, 각 값이 많은 데이터를 반환한다면 &lt;code&gt;INLIST ITERATOR&lt;/code&gt;가 불리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인덱스가 다음과 같다고 하자.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(status, created_at)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM orders
WHERE status IN (
    'PAID',
    'READY',
    'DONE',
    'SHIPPING',
    'DELIVERED',
    'CONFIRMED',
    'PACKED',
    'PICKED',
    'REQUESTED'
)
AND created_at &amp;gt;= TIMESTAMP '2026-07-01 00:00:00'
AND created_at &amp;lt;  TIMESTAMP '2026-08-01 00:00:00';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 전체 status 종류가 10개인데, 그중 9개를 &lt;code&gt;IN&lt;/code&gt; 조건으로 조회한다면 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건은 사실상 대부분의 데이터를 포함한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;전체 status 10개 중 9개 포함
&amp;rarr; status 조건으로 걸러지는 데이터가 거의 없음
&amp;rarr; 선택도가 높음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &lt;code&gt;INLIST ITERATOR&lt;/code&gt;로 실행되면 다음과 같이 여러 번 탐색이 반복될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;status = 'PAID'      탐색
status = 'READY'     탐색
status = 'DONE'      탐색
status = 'SHIPPING'  탐색
status = 'DELIVERED' 탐색
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 인덱스 탐색은 여러 번 반복되지만, 읽는 데이터는 전체와 크게 다르지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 비용 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;IN 값 개수 &amp;times; 인덱스 탐색 비용
+ 각 값별 leaf block scan 비용
+ 많은 rowid 기반 table access 비용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서는 &lt;code&gt;IN&lt;/code&gt;을 굳이 access 조건으로 쓰는 것이 오히려 비효율적일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. IN을 Filter 조건으로 쓰는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 인덱스가 다음과 같다고 하자.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(created_at, status)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM orders
WHERE created_at &amp;gt;= TIMESTAMP '2026-07-01 00:00:00'
  AND created_at &amp;lt;  TIMESTAMP '2026-08-01 00:00:00'
  AND status IN ('PAID', 'READY', 'DONE');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인덱스에서는 선두 컬럼이 &lt;code&gt;created_at&lt;/code&gt;이다.&lt;br /&gt;따라서 DB는 먼저 &lt;code&gt;created_at&lt;/code&gt;의 7월 범위를 읽을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;created_at 7월 범위 access
&amp;rarr; 읽은 데이터 중 status IN (...) 조건 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;status IN (...)&lt;/code&gt;은 인덱스에 포함된 컬럼이더라도 인덱스 탐색의 시작점과 끝점을 직접 만드는 핵심 조건이 아닐 수 있다.&lt;br /&gt;대신 &lt;code&gt;created_at&lt;/code&gt; 범위로 읽은 인덱스 엔트리 또는 테이블 row에 대해 &lt;code&gt;status&lt;/code&gt; 값이 조건에 맞는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림으로 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(created_at, status) 인덱스

2026-06-30  ...
2026-07-01  PAID      &amp;larr; 읽고 통과
2026-07-01  CANCELLED &amp;larr; 읽고 탈락
2026-07-02  READY     &amp;larr; 읽고 통과
2026-07-02  WAIT      &amp;larr; 읽고 탈락
...
2026-07-31  DONE      &amp;larr; 읽고 통과
2026-08-01  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;code&gt;status&lt;/code&gt; 값마다 인덱스를 반복 탐색하지 않는다.&lt;br /&gt;&lt;code&gt;created_at&lt;/code&gt; 범위를 한 번 읽으면서 &lt;code&gt;status&lt;/code&gt; 조건을 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. created_at이 선두 컬럼으로 고정된 상태에서 status를 인덱스에 포함할지 판단하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서는 &lt;code&gt;(created_at, status)&lt;/code&gt; 인덱스에서 &lt;code&gt;created_at&lt;/code&gt;으로 먼저 범위를 읽고, &lt;code&gt;status IN (...)&lt;/code&gt; 조건을 filter로 적용하는 방식을 설명했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 중요한 실무 판단이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;created_at&lt;/code&gt;이 인덱스 선두 컬럼으로 고정되어 있다면, &lt;code&gt;status&lt;/code&gt;를 인덱스에 포함하는 것이 의미가 있을까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면 &lt;b&gt;의미가 있을 수 있다.&lt;/b&gt;&lt;br /&gt;특히 &lt;code&gt;created_at&lt;/code&gt; 조건만으로는 읽는 데이터가 많고, &lt;code&gt;status&lt;/code&gt; 조건까지 적용했을 때 실제로 테이블에서 가져와야 하는 데이터가 매우 적다면 &lt;code&gt;status&lt;/code&gt;를 인덱스에 포함하는 것이 도움이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 인덱스가 다음과 같다고 하자.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(created_at)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM orders
WHERE created_at &amp;gt;= TIMESTAMP '2026-07-01 00:00:00'
  AND created_at &amp;lt;  TIMESTAMP '2026-08-01 00:00:00'
  AND status IN ('FAILED', 'CANCELLED');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 인덱스에는 &lt;code&gt;created_at&lt;/code&gt;만 있으므로 DB는 7월 범위의 인덱스 엔트리를 읽은 뒤, 각 rowid를 이용해 테이블에 접근해야 한다.&lt;br /&gt;그리고 테이블에서 &lt;code&gt;status&lt;/code&gt; 값을 확인한 뒤 &lt;code&gt;FAILED&lt;/code&gt;, &lt;code&gt;CANCELLED&lt;/code&gt;가 아닌 데이터는 버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(created_at) 인덱스

1. created_at 7월 범위 scan
2. 7월에 해당하는 rowid를 이용해 테이블 access
3. 테이블에서 status 확인
4. FAILED, CANCELLED만 통과&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 7월 데이터가 100만 건이고, 그중 &lt;code&gt;FAILED&lt;/code&gt;, &lt;code&gt;CANCELLED&lt;/code&gt;가 1만 건뿐이라면 어떻게 될까?&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;created_at 조건으로 100만 건 후보 발생
&amp;rarr; 테이블에 접근해서 status 확인
&amp;rarr; 실제 통과 데이터는 1만 건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 &lt;code&gt;status&lt;/code&gt;를 확인하기 위해 불필요한 테이블 access가 많이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 status를 인덱스에 포함하면 무엇이 달라질까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 인덱스가 다음과 같다고 하자.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;(created_at, status)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 쿼리를 다시 보자.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM orders
WHERE created_at &amp;gt;= TIMESTAMP '2026-07-01 00:00:00'
  AND created_at &amp;lt;  TIMESTAMP '2026-08-01 00:00:00'
  AND status IN ('FAILED', 'CANCELLED');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에도 선두 컬럼은 &lt;code&gt;created_at&lt;/code&gt;이다.&lt;br /&gt;따라서 &lt;code&gt;status&lt;/code&gt;가 &lt;code&gt;created_at&lt;/code&gt;보다 먼저 탐색 범위를 만드는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 동작은 여전히 다음에 가깝다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;created_at 7월 범위 access
&amp;rarr; status IN (...) filter&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 중요한 차이가 있다.&lt;br /&gt;&lt;code&gt;status&lt;/code&gt;가 인덱스에 포함되어 있으므로, DB는 테이블에 접근하기 전에 인덱스 엔트리에서 &lt;code&gt;status&lt;/code&gt; 값을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(created_at, status) 인덱스

1. created_at 7월 범위 scan
2. 인덱스 엔트리에서 status 확인
3. FAILED, CANCELLED인 rowid만 테이블 access
4. 나머지는 테이블에 접근하지 않고 제외&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;status&lt;/code&gt;가 access 조건으로 강하게 사용되지 않더라도, &lt;b&gt;인덱스 filter 조건으로 사용되면서 테이블 access를 줄일 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 정리하면 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;height: 96px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;인덱스&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;동작&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;문제 또는 장점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;code&gt;(created_at)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;code&gt;created_at&lt;/code&gt; 범위 scan 후 테이블에서 &lt;code&gt;status&lt;/code&gt; 확인&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;조건에 맞지 않는 row도 테이블 access가 발생할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;code&gt;(created_at, status)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;&lt;code&gt;created_at&lt;/code&gt; 범위 scan 후 인덱스에서 &lt;code&gt;status&lt;/code&gt; 확인&lt;/td&gt;
&lt;td style=&quot;height: 38px;&quot;&gt;조건에 맞는 row만 테이블 access할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;code&gt;status&lt;/code&gt;가 인덱스 선두 컬럼이 아니더라도 의미가 없지는 않다는 점이다.&lt;br /&gt;선두 컬럼이 아니기 때문에 &lt;code&gt;INLIST ITERATOR&lt;/code&gt;처럼 &lt;code&gt;status&lt;/code&gt; 값별로 찾아가는 방식은 아닐 수 있다.&lt;br /&gt;하지만 인덱스에 포함되어 있으면 &lt;code&gt;status&lt;/code&gt; 값을 테이블에 가지 않고도 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 언제 status를 인덱스에 포함하는 것이 유리할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 조건이 함께 만족될수록 &lt;code&gt;(created_at, status)&lt;/code&gt; 형태가 유리할 수 있다.&lt;/p&gt;
&lt;table style=&quot;height: 115px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;조건&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;created_at&lt;/code&gt; 범위로 읽는 데이터가 많음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;테이블 access 후보가 많다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;status IN (...)&lt;/code&gt; 조건을 적용하면 남는 데이터가 적음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;인덱스에서 먼저 걸러낼 가치가 크다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;조회 컬럼 때문에 테이블 access가 필요함&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;모든 row에 대해 테이블에 가는 비용을 줄일 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;status&lt;/code&gt; 컬럼 크기가 작음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;인덱스에 추가해도 인덱스 크기 증가 부담이 상대적으로 작다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;해당 쿼리가 자주 실행됨&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;인덱스 유지 비용보다 조회 이득이 클 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음 상황에서는 &lt;code&gt;status&lt;/code&gt;를 인덱스에 포함하는 것을 검토할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;created_at 7월 범위: 1,000,000건
status IN ('FAILED', 'CANCELLED'): 10,000건&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;(created_at)&lt;/code&gt; 인덱스만 있으면 100만 건에 대해 테이블에 접근한 뒤 &lt;code&gt;status&lt;/code&gt;를 확인할 수 있다.&lt;br /&gt;반면 &lt;code&gt;(created_at, status)&lt;/code&gt; 인덱스가 있으면 인덱스 단계에서 &lt;code&gt;status&lt;/code&gt;를 확인하고, 통과하는 1만 건에 대해서만 테이블에 접근할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;(created_at)
&amp;rarr; 100만 건 rowid로 테이블 access 가능성
&amp;rarr; 테이블에서 status 확인
&amp;rarr; 1만 건 통과

(created_at, status)
&amp;rarr; 100만 건 인덱스 엔트리 scan
&amp;rarr; 인덱스에서 status 확인
&amp;rarr; 1만 건만 테이블 access&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 경우에는 &lt;code&gt;status&lt;/code&gt;가 filter 조건이더라도 인덱스에 포함하는 것이 의미가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 주의할 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;code&gt;status&lt;/code&gt;를 인덱스에 포함한다고 항상 좋은 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스에 컬럼을 추가하면 인덱스 크기가 커지고, INSERT/UPDATE/DELETE 시 인덱스 유지 비용도 증가한다.&lt;br /&gt;또한 &lt;code&gt;created_at&lt;/code&gt; 범위 자체가 너무 넓다면, 결국 많은 인덱스 leaf block을 읽어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;(created_at, status)&lt;/code&gt; 인덱스는 다음을 기대하는 설계다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;created_at으로 대상 범위를 찾고
&amp;rarr; status를 인덱스에서 먼저 확인해서
&amp;rarr; 불필요한 테이블 access를 줄인다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음 상황이라면 효과가 작을 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&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;created_at&lt;/code&gt; 범위가 매우 넓음&lt;/td&gt;
&lt;td&gt;인덱스 scan 자체가 커진다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt; 조건으로 거의 걸러지지 않음&lt;/td&gt;
&lt;td&gt;테이블 access 감소 효과가 작다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;테이블 access가 원래 적음&lt;/td&gt;
&lt;td&gt;추가 인덱스 컬럼의 이점이 작다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DML이 매우 많은 테이블&lt;/td&gt;
&lt;td&gt;인덱스 유지 비용이 부담될 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조회 컬럼이 모두 인덱스에 있음&lt;/td&gt;
&lt;td&gt;이미 covering index처럼 동작할 수 있어 별도 판단 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;created_at&lt;/code&gt;이 선두 컬럼으로 고정된 상태에서 &lt;code&gt;status&lt;/code&gt;를 추가할지는 다음 기준으로 판단해야 한다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;status를 인덱스에서 먼저 걸러서 줄어드는 table access 비용
&amp;gt;
status를 인덱스에 추가하면서 늘어나는 index scan/저장/DML 비용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 왜 Filter 조건이 더 나을 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로는 filter보다 access가 좋아 보인다.&lt;br /&gt;하지만 &lt;code&gt;INLIST ITERATOR&lt;/code&gt;에서는 &lt;code&gt;IN&lt;/code&gt; 값 개수만큼 탐색이 반복될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식을 비교해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방식 A. INLIST ITERATOR&lt;/h3&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;status = 'PAID' 탐색
status = 'READY' 탐색
status = 'DONE' 탐색
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 구조는 대략 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;IN 값 개수 &amp;times; 인덱스 root/branch/leaf 탐색 비용
+ 각 값별 leaf range scan 비용
+ rowid를 통한 table access 비용&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방식 B. 다른 컬럼으로 Access 후 IN Filter&lt;/h3&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;created_at 7월 범위 한 번 scan
&amp;rarr; status IN (...) 조건 검사&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 구조는 대략 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;created_at 범위 scan 비용
+ status 값 비교 비용
+ 필요한 table access 비용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같은 상황에서는 방식 B가 더 유리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;IN 값별 반복 탐색 비용 &amp;gt; 넓은 범위 한 번 scan + filter 비용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, access 조건이 항상 filter 조건보다 좋은 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 말하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 access 조건은 읽는 범위를 충분히 줄여주는 조건이다.&lt;br /&gt;&lt;code&gt;IN&lt;/code&gt; 조건을 access로 사용하더라도 읽는 범위를 거의 줄이지 못하고 반복 탐색만 늘어난다면, filter 조건으로 사용하는 편이 더 나을 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 예시로 보는 판단 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 데이터 분포가 있다고 하자.&lt;/p&gt;
&lt;table style=&quot;height: 105px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;status&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;전체 데이터 비율&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;PAID&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;READY&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;DONE&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;CANCELLED&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;FAILED&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 조건은 선택도가 낮다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE status IN ('CANCELLED', 'FAILED')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체의 약 5%만 조회한다.&lt;br /&gt;이 경우 &lt;code&gt;(status, created_at)&lt;/code&gt; 인덱스를 사용해서 &lt;code&gt;INLIST ITERATOR&lt;/code&gt;로 접근하는 것이 유리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 아래 조건은 선택도가 높다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE status IN ('PAID', 'READY', 'DONE')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체의 약 95%를 포함한다.&lt;br /&gt;이 경우 &lt;code&gt;status&lt;/code&gt;를 access 조건으로 사용해도 읽는 데이터가 크게 줄어들지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 날짜 조건이 함께 있고, 날짜 조건이 충분히 강하다면 &lt;code&gt;(created_at, status)&lt;/code&gt; 인덱스로 날짜 범위를 먼저 읽고 &lt;code&gt;status&lt;/code&gt;는 filter로 처리하는 편이 더 나을 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&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;status IN ('CANCELLED', 'FAILED')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;를 access 조건으로 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status IN ('PAID', 'READY', 'DONE')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다른 조건으로 access 후 &lt;code&gt;status&lt;/code&gt;는 filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IN&lt;/code&gt; 값이 적고 선택도가 낮음&lt;/td&gt;
&lt;td&gt;INLIST ITERATOR 유리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IN&lt;/code&gt; 값이 많고 선택도가 높음&lt;/td&gt;
&lt;td&gt;Filter 조건 유리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;날짜 조건이 매우 강함&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(created_at, status)&lt;/code&gt; 형태가 유리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상태 조건이 매우 강함&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(status, created_at)&lt;/code&gt; 형태가 유리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 실행계획에서 확인해야 할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜닝할 때는 실행계획의 operation 이름만 보면 안 된다.&lt;br /&gt;특히 Oracle에서는 &lt;code&gt;Predicate Information&lt;/code&gt;에서 해당 조건이 &lt;code&gt;access&lt;/code&gt;인지 &lt;code&gt;filter&lt;/code&gt;인지 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음처럼 나오면 &lt;code&gt;status&lt;/code&gt;가 access 조건으로 사용된 것이다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;access(&quot;STATUS&quot;='PAID' OR &quot;STATUS&quot;='READY')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음처럼 나오면 &lt;code&gt;created_at&lt;/code&gt;이 access 조건이고, &lt;code&gt;status&lt;/code&gt;는 filter 조건이다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;access(&quot;CREATED_AT&quot;&amp;gt;=:FROM_DT AND &quot;CREATED_AT&quot;&amp;lt;:TO_DT)
filter(&quot;STATUS&quot;='PAID' OR &quot;STATUS&quot;='READY')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이가 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;status&lt;/code&gt;가 인덱스에 포함되어 있다고 해서 반드시 access 조건으로 사용되는 것은 아니다.&lt;br /&gt;인덱스 컬럼 순서, 조건의 형태, 데이터 분포, 통계 정보에 따라 access로 쓰일 수도 있고 filter로 쓰일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. MySQL에서는 어떻게 볼 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 Oracle처럼 &lt;code&gt;INLIST ITERATOR&lt;/code&gt;라는 이름이 실행계획에 직접 나타나지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 MySQL은 &lt;code&gt;IN()&lt;/code&gt; 조건을 여러 개의 equality range로 보고 range optimization 대상으로 처리할 수 있다.&lt;br /&gt;즉, 개념적으로는 다음과 같은 여러 index interval을 읽는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE status IN ('PAID', 'READY', 'DONE')&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;status = 'PAID' interval
status = 'READY' interval
status = 'DONE' interval&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 MySQL에서도 핵심 판단은 비슷하다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;IN 값이 적고 선택도가 낮으면 range access로 유리할 수 있음
IN 값이 많고 선택도가 높으면 반복 range 탐색의 이점이 줄어듦&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 DBMS마다 실행계획 표기 방식과 세부 최적화 방식은 다르므로, 실제 판단은 각 DBMS의 실행계획과 실제 수행 통계를 기준으로 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 튜닝 관점에서의 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;IN&lt;/code&gt; 조건을 튜닝할 때는 다음 순서로 판단하는 것이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.1 IN 값 개수를 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;IN&lt;/code&gt; 값이 적으면 반복 탐색 비용이 크지 않다.&lt;br /&gt;하지만 값이 많아질수록 반복 탐색 비용이 커질 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;IN 값 개수&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;/td&gt;
&lt;td&gt;access 조건으로 유리할 가능성이 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;많음&lt;/td&gt;
&lt;td&gt;반복 탐색 비용 확인 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;매우 많음&lt;/td&gt;
&lt;td&gt;parse 비용, plan 안정성, 임시 테이블 조인 방식도 검토 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전체 도메인 대부분&lt;/td&gt;
&lt;td&gt;조건으로서의 효과가 낮을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2 각 값의 선택도를 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;IN&lt;/code&gt; 값 개수보다 더 중요한 것은 선택도다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE status IN ('FAILED', 'CANCELLED')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 조건처럼 값은 2개뿐이고 전체 데이터의 5%만 포함한다면 좋은 조건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음 조건은 값이 3개뿐이어도 좋지 않을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;WHERE status IN ('PAID', 'READY', 'DONE')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 값이 전체 데이터의 95%를 차지한다면 선택도가 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;IN&lt;/code&gt; 값 개수만 보면 안 된다.&lt;br /&gt;각 값이 얼마나 많은 데이터를 포함하는지도 함께 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.3 다른 조건이 더 강한지 본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 쿼리를 보자.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WHERE created_at &amp;gt;= TIMESTAMP '2026-07-09 00:00:00'
  AND created_at &amp;lt;  TIMESTAMP '2026-07-10 00:00:00'
  AND status IN ('PAID', 'READY', 'DONE')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;created_at&lt;/code&gt; 하루 조건이 매우 강하다면, 날짜 범위를 먼저 읽고 &lt;code&gt;status&lt;/code&gt;를 filter하는 방식이 더 나을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음 쿼리는 다를 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WHERE created_at &amp;gt;= TIMESTAMP '2026-01-01 00:00:00'
  AND created_at &amp;lt;  TIMESTAMP '2027-01-01 00:00:00'
  AND status IN ('FAILED')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기간은 1년으로 넓고, &lt;code&gt;status = 'FAILED'&lt;/code&gt;가 매우 적다면 &lt;code&gt;status&lt;/code&gt;를 먼저 access 조건으로 사용하는 편이 유리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 최종 요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;INLIST ITERATOR / Access&lt;/th&gt;
&lt;th&gt;IN Filter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;동작&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IN&lt;/code&gt; 값별로 인덱스 탐색 반복&lt;/td&gt;
&lt;td&gt;다른 조건으로 읽은 뒤 값 검사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용 구조&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IN&lt;/code&gt; 개수 &amp;times; index range scan&lt;/td&gt;
&lt;td&gt;1차 access 범위 scan + 값 비교&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인덱스 예&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(status, created_at)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(created_at, status)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;/td&gt;
&lt;td&gt;특정 status 구간만 찾아갈 수 있음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;가 인덱스에 있으면 테이블 access 전에 걸러낼 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단점&lt;/td&gt;
&lt;td&gt;값이 많으면 반복 탐색 비용 증가&lt;/td&gt;
&lt;td&gt;먼저 읽는 범위가 넓으면 인덱스 scan 자체는 커질 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확인 포인트&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Predicate Information&lt;/code&gt;의 &lt;code&gt;access&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Predicate Information&lt;/code&gt;의 &lt;code&gt;filter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;IN 값이 적고 잘 걸러진다
&amp;rarr; INLIST ITERATOR / access 조건이 유리할 가능성이 높다

IN 값이 많고 대부분의 데이터를 포함한다
&amp;rarr; 반복 탐색 비용이 커질 수 있다
&amp;rarr; 다른 조건으로 access 후 IN은 filter가 나을 수 있다

복합 인덱스 설계에서는
&amp;rarr; IN 컬럼을 앞에 둘지
&amp;rarr; 다른 조건 컬럼을 앞에 두고 IN을 filter로 둘지
&amp;rarr; filter로 쓰더라도 인덱스에 포함해 table access를 줄일 수 있는지
&amp;rarr; 데이터 분포와 실행계획을 기준으로 판단해야 한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 기준은 이것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;IN&lt;/code&gt;을 access 조건으로 쓸 것인지는 문법적으로 가능한지가 아니라, 반복 탐색 비용보다 줄어드는 I/O가 더 큰지로 판단해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/263</guid>
      <comments>https://soobarkbar.tistory.com/263#entry263comment</comments>
      <pubDate>Fri, 10 Jul 2026 00:01:59 +0900</pubDate>
    </item>
    <item>
      <title>대규모 분산 시스템에서 고유 ID를 생성하는 방법들</title>
      <link>https://soobarkbar.tistory.com/261</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 개발하다 보면 거의 모든 데이터에 ID가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 데이터들은 대부분 고유한 식별자를 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원 ID&lt;/li&gt;
&lt;li&gt;주문 ID&lt;/li&gt;
&lt;li&gt;게시글 ID&lt;/li&gt;
&lt;li&gt;댓글 ID&lt;/li&gt;
&lt;li&gt;결제 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 서버와 단일 데이터베이스만 사용하는 환경에서는 ID 생성이 비교적 단순하다. 데이터베이스의 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;나 &lt;code&gt;SEQUENCE&lt;/code&gt;를 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;User Table

id | name
---|------
1  | Kim
2  | Lee
3  | Park&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 시스템 규모가 커지면 상황이 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 여러 대로 늘어나고, 데이터베이스가 샤딩되고, 여러 지역의 데이터센터에서 동시에 요청을 처리해야 한다면 단순히 숫자를 1씩 증가시키는 방식만으로는 충분하지 않을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A ─┐
Server B ─┼──&amp;gt; ID 생성
Server C ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템에서 ID를 생성할 때 중요한 문제는 단순히 &amp;ldquo;중복되지 않는 값&amp;rdquo;을 만드는 것만이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID가 시간순으로 정렬될 수 있는지, 데이터베이스 인덱스에 유리한지, 중앙 서버 장애에 영향을 받는지, 외부에 노출해도 안전한지, 초당 얼마나 많은 ID를 생성할 수 있는지도 함께 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 대규모 분산 시스템에서 고유 ID를 생성하는 대표적인 방법들을 간단히 정리한다. 각 방식의 내부 구조나 세부 구현은 이후 글에서 하나씩 더 자세히 살펴볼 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋은 ID는 어떤 조건을 만족해야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성 방식을 비교하기 전에 먼저 평가 기준을 정리할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 상황에 완벽한 ID 생성 방식은 없다. 어떤 방식은 구현이 단순하지만 확장성에 약하고, 어떤 방식은 분산 환경에 강하지만 운영 복잡도가 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 ID 생성 방식을 판단할 때는 보통 다음 기준을 본다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&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;/td&gt;
&lt;td&gt;서로 다른 데이터가 같은 ID를 가지면 안 된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성 성능&lt;/td&gt;
&lt;td&gt;많은 요청이 들어와도 빠르게 ID를 생성할 수 있어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분산 생성 가능 여부&lt;/td&gt;
&lt;td&gt;여러 서버가 동시에 ID를 만들어도 충돌이 없어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬 가능성&lt;/td&gt;
&lt;td&gt;ID만 보고 생성 순서나 대략적인 시간을 알 수 있는지 여부다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;저장 공간&lt;/td&gt;
&lt;td&gt;ID가 차지하는 크기가 작을수록 저장 공간과 인덱스 크기에 유리하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인덱스 효율&lt;/td&gt;
&lt;td&gt;데이터베이스 B-Tree 인덱스에서 삽입 성능이 좋은지 여부다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예측 가능성&lt;/td&gt;
&lt;td&gt;외부 사용자가 다음 ID를 쉽게 추측할 수 있는지 여부다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 복잡도&lt;/td&gt;
&lt;td&gt;별도의 중앙 서버, Worker ID 관리, 시간 동기화 등이 필요한지 여부다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;는 단순하고 인덱스 효율이 좋지만, 분산 환경에서는 확장성 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 UUID는 여러 서버에서 독립적으로 생성하기 쉽지만, UUID v4처럼 완전히 랜덤한 값은 데이터베이스 인덱스 관점에서 불리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Auto Increment / Sequence&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 방식은 데이터베이스가 ID 생성을 담당하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;, PostgreSQL의 &lt;code&gt;SEQUENCE&lt;/code&gt;가 대표적이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 데이터가 insert될 때마다 데이터베이스가 자동으로 증가하는 숫자 ID를 부여한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1, 2, 3, 4, 5, ...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Increment와 Sequence 방식의 장점은 명확하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현이 매우 단순하다.&lt;/li&gt;
&lt;li&gt;ID가 숫자라 저장 공간이 작다.&lt;/li&gt;
&lt;li&gt;순차적으로 증가하므로 정렬이 쉽다.&lt;/li&gt;
&lt;li&gt;B-Tree 인덱스에 유리하다.&lt;/li&gt;
&lt;li&gt;운영 복잡도가 낮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS 기반의 일반적인 CRUD 서비스라면 이 방식만으로도 충분한 경우가 많다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 시스템이 커졌을 때 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성을 데이터베이스가 담당하므로, 결국 데이터베이스가 ID 생성의 중심 지점이 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A ─┐
Server B ─┼──&amp;gt; Database Sequence
Server C ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 여러 대로 늘어나도 ID 생성은 데이터베이스에 의존한다. 트래픽이 커지면 데이터베이스가 병목이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 데이터베이스를 여러 개로 샤딩하면 더 복잡해진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Shard 1: id = 1
Shard 2: id = 1
Shard 3: id = 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 샤드가 독립적으로 Auto Increment를 사용하면 서로 같은 ID가 만들어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 피하기 위해 샤드별로 ID 범위를 나누거나, 증가 간격을 다르게 설정하거나, 별도의 ID 생성 서버를 두는 방법을 사용할 수 있다. 하지만 이 순간부터 단순했던 Auto Increment 방식도 운영 복잡도가 증가한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Increment나 Sequence는 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 데이터베이스 기반 서비스&lt;/li&gt;
&lt;li&gt;샤딩이 필요 없는 규모&lt;/li&gt;
&lt;li&gt;내부 관리자용 데이터&lt;/li&gt;
&lt;li&gt;순차적인 숫자 ID가 필요한 경우&lt;/li&gt;
&lt;li&gt;구현 단순성이 중요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 중앙 ID 생성 서버&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 방식은 ID 생성을 전담하는 중앙 서버를 두는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A ─┐
Server B ─┼──&amp;gt; ID Generator Server ──&amp;gt; 1000001
Server C ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버는 직접 ID를 만들지 않고, ID 생성 서버에 요청해서 새로운 ID를 받아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 방식은 다양하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis &lt;code&gt;INCR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;데이터베이스 Sequence 전용 서버&lt;/li&gt;
&lt;li&gt;ZooKeeper&lt;/li&gt;
&lt;li&gt;etcd&lt;/li&gt;
&lt;li&gt;별도 ID Generator API 서버&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 Auto Increment를 데이터베이스 테이블에 직접 묶어두는 대신, ID 생성 책임을 별도 컴포넌트로 분리한 형태라고 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중앙 ID 생성 서버를 사용하면 여러 애플리케이션 서버가 같은 규칙으로 ID를 발급받을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전역적으로 중복 없는 ID를 만들기 쉽다.&lt;/li&gt;
&lt;li&gt;순차 증가 ID를 만들기 쉽다.&lt;/li&gt;
&lt;li&gt;ID 생성 정책을 한 곳에서 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;여러 서비스가 같은 ID 생성 규칙을 공유할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 단점은 중앙 장애점이 생긴다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ID Generator Server 장애
        &amp;darr;
새로운 데이터 생성 불가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성 서버가 죽으면 새로운 주문, 게시글, 메시지를 만들 수 없게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 ID 생성 서버도 고가용성 구조로 만들 수 있다. 하지만 이 경우 시스템 복잡도는 다시 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 모든 ID 생성 요청이 네트워크 호출을 필요로 하므로, 애플리케이션 내부에서 직접 생성하는 방식보다 지연 시간이 늘어날 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중앙 ID 생성 서버는 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전역적으로 순차적인 ID가 필요한 경우&lt;/li&gt;
&lt;li&gt;여러 서비스가 동일한 ID 발급 정책을 사용해야 하는 경우&lt;/li&gt;
&lt;li&gt;ID 생성 정책을 중앙에서 통제해야 하는 경우&lt;/li&gt;
&lt;li&gt;중앙 컴포넌트를 고가용성으로 운영할 수 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. UUID&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID는 Universally Unique Identifier의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID는 128bit 식별자이며, 일반적으로 다음과 같은 문자열 형태로 표현된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;550e8400-e29b-41d4-a716-446655440000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID의 가장 큰 장점은 중앙 서버 없이 각 서버가 독립적으로 ID를 생성할 수 있다는 점이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A ──&amp;gt; UUID 생성
Server B ──&amp;gt; UUID 생성
Server C ──&amp;gt; UUID 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서버가 서로 통신하지 않아도 충돌 가능성이 매우 낮은 ID를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID에는 여러 버전이 있다. 이 글에서는 대표적으로 UUID v1, UUID v4, UUID v7을 간단히 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-1. UUID v1&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v1은 시간 정보와 노드 정보를 기반으로 ID를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순화하면 다음과 같은 구조다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Timestamp + Clock Sequence + Node&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Node 값에는 전통적으로 MAC Address가 사용될 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 기반이기 때문에 어느 정도 정렬 가능하다는 장점이 있지만, MAC Address 같은 하드웨어 식별 정보가 포함될 수 있다는 점 때문에 개인정보나 보안 측면에서 부담이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 신규 시스템에서 UUID v1을 적극적으로 선택하는 경우는 많지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-2. UUID v4&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v4는 랜덤 기반 UUID다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Random 122 bits + Version/Variant bits&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 개발자가 흔히 사용하는 UUID가 이 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서는 다음과 같이 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;UUID id = UUID.randomUUID();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v4의 장점은 단순하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중앙 서버가 필요 없다.&lt;/li&gt;
&lt;li&gt;여러 서버에서 동시에 생성해도 충돌 가능성이 극히 낮다.&lt;/li&gt;
&lt;li&gt;구현이 쉽다.&lt;/li&gt;
&lt;li&gt;외부에 노출해도 다음 ID를 예측하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단점도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v4는 랜덤 값이기 때문에 생성 순서와 ID 정렬 순서가 일치하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;생성 순서:
A &amp;rarr; B &amp;rarr; C &amp;rarr; D

UUID 정렬 결과:
C &amp;rarr; A &amp;rarr; D &amp;rarr; B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성은 데이터베이스 인덱스에서 문제가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree 인덱스는 정렬된 구조를 유지한다. 순차적으로 증가하는 ID는 인덱스의 끝에 계속 추가되기 때문에 비교적 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 UUID v4처럼 랜덤한 값은 인덱스의 여러 위치에 흩어져 삽입된다. 이로 인해 페이지 분할, 캐시 효율 저하, 인덱스 단편화가 발생할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v4는 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중앙 서버 없이 ID를 생성해야 하는 경우&lt;/li&gt;
&lt;li&gt;ID 예측 가능성을 낮춰야 하는 경우&lt;/li&gt;
&lt;li&gt;정렬 순서가 중요하지 않은 경우&lt;/li&gt;
&lt;li&gt;데이터 생성량이 크지 않거나 인덱스 성능 영향이 크지 않은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-3. UUID v7&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v7은 시간 정렬성을 고려한 UUID다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RFC 9562에 정의된 UUID v7은 Unix Epoch 기준 millisecond timestamp를 기반으로 한다. 즉, ID 앞부분에 시간 정보가 들어가고 나머지 부분에 랜덤성이 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;Timestamp + Random&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v7은 UUID v4의 장점인 분산 생성 가능성과 낮은 충돌 가능성을 유지하면서, 시간순 정렬에 더 유리하도록 설계되었다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;UUID v4: 랜덤 &amp;rarr; 정렬 불리
UUID v7: 시간 + 랜덤 &amp;rarr; 정렬 유리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 데이터베이스 Primary Key로 UUID를 사용해야 한다면 UUID v4보다 UUID v7이 더 나은 선택이 될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID v7은 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UUID 형식을 유지하고 싶은 경우&lt;/li&gt;
&lt;li&gt;분산 환경에서 각 서버가 독립적으로 ID를 생성해야 하는 경우&lt;/li&gt;
&lt;li&gt;시간순 정렬이 필요한 경우&lt;/li&gt;
&lt;li&gt;데이터베이스 인덱스 효율도 어느 정도 고려해야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. ULID&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID는 Universally Unique Lexicographically Sortable Identifier의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 그대로 사전식 정렬이 가능한 고유 ID다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;01ARZ3NDEKTSV4RRFFQ69G5FAV&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID는 128bit 구조이며, 크게 두 부분으로 나뉜다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;48bit Timestamp + 80bit Randomness&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞부분에 timestamp가 들어가기 때문에 문자열을 정렬했을 때 생성 시간순으로 정렬할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID와 비교하면 다음과 같은 특징이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UUID와 동일하게 128bit 수준의 식별자다.&lt;/li&gt;
&lt;li&gt;문자열 길이가 26자로 UUID 문자열 표현보다 짧다.&lt;/li&gt;
&lt;li&gt;시간순 정렬이 가능하다.&lt;/li&gt;
&lt;li&gt;URL-safe하다.&lt;/li&gt;
&lt;li&gt;사람이 읽기에는 UUID보다 조금 더 간결하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID는 로그, 이벤트, 메시지, 외부 공개 ID 등에 사용하기 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 UUID처럼 표준 라이브러리에 항상 기본 포함되어 있는 것은 아니므로, 사용하는 언어나 프레임워크에 따라 별도 라이브러리가 필요할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Snowflake&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake는 Twitter에서 사용한 것으로 유명한 분산 ID 생성 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake 계열의 핵심은 64bit 정수 하나에 시간, 서버 식별자, 시퀀스 번호를 나누어 담는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;64bit

1bit  : Sign
41bit : Timestamp
10bit : Worker ID
12bit : Sequence&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 필드의 의미는 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&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;Sign&lt;/td&gt;
&lt;td&gt;일반적으로 사용하지 않는 부호 비트다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamp&lt;/td&gt;
&lt;td&gt;기준 시각 이후 흐른 시간을 millisecond 단위로 저장한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker ID&lt;/td&gt;
&lt;td&gt;ID를 생성한 서버나 프로세스를 구분한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequence&lt;/td&gt;
&lt;td&gt;같은 millisecond 안에서 여러 ID를 만들기 위한 증가값이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 시각에 여러 서버가 동시에 ID를 생성하더라도 Worker ID가 다르면 충돌하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server A: Worker ID = 1
Server B: Worker ID = 2
Server C: Worker ID = 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 같은 서버에서 같은 millisecond 안에 여러 개의 ID를 생성하면 Sequence 값을 증가시켜 구분한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;timestamp = 1000, worker = 1, sequence = 0
timestamp = 1000, worker = 1, sequence = 1
timestamp = 1000, worker = 1, sequence = 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake의 장점은 명확하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;64bit 숫자 ID를 만들 수 있다.&lt;/li&gt;
&lt;li&gt;시간순 정렬이 가능하다.&lt;/li&gt;
&lt;li&gt;중앙 서버 없이 각 서버가 직접 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;UUID보다 저장 공간이 작다.&lt;/li&gt;
&lt;li&gt;데이터베이스 인덱스에 비교적 유리하다.&lt;/li&gt;
&lt;li&gt;초당 대량의 ID 생성이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 운영 시 주의할 점도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 Worker ID 관리다. 두 서버가 같은 Worker ID를 사용하면 같은 시간에 같은 Sequence를 생성할 수 있으므로 ID 충돌이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 서버 시간 문제다. Snowflake는 timestamp를 사용하기 때문에 서버 시간이 뒤로 돌아가면 중복 ID가 생성될 위험이 있다. 이를 Clock Rollback 문제라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째는 같은 millisecond 안에서 만들 수 있는 ID 수에 제한이 있다는 점이다. 예를 들어 Sequence가 12bit라면 한 Worker가 같은 millisecond 안에서 만들 수 있는 ID는 최대 4096개다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake는 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 트래픽을 처리해야 하는 경우&lt;/li&gt;
&lt;li&gt;숫자형 ID가 필요한 경우&lt;/li&gt;
&lt;li&gt;시간순 정렬이 필요한 경우&lt;/li&gt;
&lt;li&gt;데이터베이스 인덱스 효율이 중요한 경우&lt;/li&gt;
&lt;li&gt;Worker ID와 시간 동기화를 운영할 수 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. MongoDB ObjectId&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB를 사용하면 기본적으로 ObjectId를 자주 접하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectId는 12byte 식별자이며, 일반적으로 다음과 같은 정보를 포함한다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;Timestamp + Random Value + Counter&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjectId는 생성 시간 정보를 포함하므로 대략적인 시간순 정렬이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB 환경에서는 애플리케이션이 데이터베이스에 저장하기 전에 ObjectId를 미리 생성할 수도 있다. 즉, 중앙 DB에 insert한 뒤에야 ID를 알 수 있는 구조가 아니라, 클라이언트나 애플리케이션 레벨에서 먼저 ID를 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적합한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB ObjectId는 다음과 같은 상황에 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MongoDB를 사용하는 경우&lt;/li&gt;
&lt;li&gt;문서 생성 시점에 애플리케이션에서 ID를 미리 알고 싶은 경우&lt;/li&gt;
&lt;li&gt;대략적인 생성 시간 정보가 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. KSUID / Sonyflake&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KSUID와 Sonyflake는 Snowflake나 ULID와 비슷한 문제를 해결하기 위해 등장한 ID 생성 방식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KSUID&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KSUID는 K-Sortable Unique ID의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 정보를 포함하고 있어 정렬 가능한 ID를 만들 수 있다. 주로 이벤트, 로그, 분산 시스템의 리소스 ID 등에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ULID와 비슷하게 &amp;ldquo;시간순 정렬 가능한 문자열 ID&amp;rdquo;라는 관점에서 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sonyflake&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sonyflake는 Snowflake의 변형이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake와 마찬가지로 timestamp, machine id, sequence를 조합해서 ID를 생성한다. 다만 bit 배치나 시간 단위 등이 Snowflake와 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 변형 방식들은 공통적으로 다음 목표를 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중앙 서버 없이 ID 생성&lt;/li&gt;
&lt;li&gt;시간순 정렬 가능&lt;/li&gt;
&lt;li&gt;고성능 생성&lt;/li&gt;
&lt;li&gt;분산 환경에서 충돌 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황별 선택 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성 방식을 선택할 때는 &amp;ldquo;무엇이 가장 좋은가?&amp;rdquo;보다 &amp;ldquo;현재 시스템에서 어떤 특성이 중요한가?&amp;rdquo;를 기준으로 봐야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단일 DB 기반 서비스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 데이터베이스를 사용하고 있고, 트래픽이 크지 않다면 Auto Increment나 Sequence가 가장 단순하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;단순함 &amp;gt; 분산 확장성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 굳이 UUID나 Snowflake를 사용할 필요가 없을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 서버에서 독립적으로 ID를 생성해야 하는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버가 여러 대이고, 중앙 ID 생성 서버에 의존하고 싶지 않다면 UUID v4, UUID v7, ULID, Snowflake 계열을 고려할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;분산 생성 가능성 &amp;gt; 중앙 통제&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스 Primary Key로 사용할 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Primary Key는 인덱스 성능에 직접적인 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 랜덤한 UUID v4는 B-Tree 인덱스에서 불리할 수 있다. 시간순 정렬이 가능한 UUID v7, ULID, Snowflake가 더 적합할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부에 노출되는 ID가 필요한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 노출되는 URL이나 API 응답에 ID가 포함된다면 예측 가능성을 고려해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/users/1
/users/2
/users/3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 순차 ID는 다음 리소스의 ID를 쉽게 추측할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 UUID v4, UUID v7, ULID처럼 예측하기 어려운 ID가 더 적합할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;숫자형 ID가 필요한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 시스템에서는 문자열 ID보다 숫자형 ID가 더 편리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 데이터베이스 저장 공간, 인덱스 크기, 다른 시스템과의 호환성 때문에 &lt;code&gt;BIGINT&lt;/code&gt; ID를 선호할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Snowflake 계열이 좋은 선택지가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 분산 시스템에서 고유 ID를 생성하는 방식은 다양하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 방식은 해결하려는 문제가 다르다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;thead&gt;
&lt;tr&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;Auto Increment / Sequence&lt;/td&gt;
&lt;td&gt;단순하고 빠른 DB 중심 ID 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중앙 ID 서버&lt;/td&gt;
&lt;td&gt;전역적으로 통제되는 ID 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUID v4&lt;/td&gt;
&lt;td&gt;중앙 조정 없는 랜덤 ID 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUID v7&lt;/td&gt;
&lt;td&gt;시간 정렬 가능한 UUID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ULID&lt;/td&gt;
&lt;td&gt;문자열 정렬 가능한 시간 기반 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;64bit 숫자 기반 분산 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB ObjectId&lt;/td&gt;
&lt;td&gt;MongoDB 문서 중심 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KSUID / Sonyflake&lt;/td&gt;
&lt;td&gt;시간 정렬 가능한 Snowflake 계열 변형&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 완벽한 ID 생성 방식은 없다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 생성 방식을 선택할 때는 다음 질문을 먼저 해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ID가 반드시 숫자여야 하는가?&lt;/li&gt;
&lt;li&gt;시간순 정렬이 필요한가?&lt;/li&gt;
&lt;li&gt;여러 서버가 독립적으로 ID를 생성해야 하는가?&lt;/li&gt;
&lt;li&gt;중앙 ID 생성 서버를 둘 수 있는가?&lt;/li&gt;
&lt;li&gt;데이터베이스 인덱스 성능이 중요한가?&lt;/li&gt;
&lt;li&gt;외부 사용자가 ID를 예측하면 문제가 되는가?&lt;/li&gt;
&lt;li&gt;Worker ID나 서버 시간 동기화를 운영할 수 있는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 대한 답에 따라 선택지는 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 서비스라면 Auto Increment만으로 충분할 수 있다. 반대로 대규모 분산 시스템에서는 UUID v7, ULID, Snowflake 같은 방식을 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 분산 시스템에서 고유 ID를 생성하는 대표적인 방법들을 전체적으로 훑어보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RFC 9562 - Universally Unique IDentifiers(UUIDs): &lt;a href=&quot;https://datatracker.ietf.org/doc/rfc9562/&quot;&gt;https://datatracker.ietf.org/doc/rfc9562/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ULID Specification: &lt;a href=&quot;https://github.com/ulid/spec&quot;&gt;https://github.com/ulid/spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Twitter Snowflake 개념: &lt;a href=&quot;https://en.wikipedia.org/wiki/Snowflake_ID&quot;&gt;https://en.wikipedia.org/wiki/Snowflake_ID&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Server</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/261</guid>
      <comments>https://soobarkbar.tistory.com/261#entry261comment</comments>
      <pubDate>Sat, 27 Jun 2026 14:51:02 +0900</pubDate>
    </item>
    <item>
      <title>블룸 필터 (Bloom Filter)</title>
      <link>https://soobarkbar.tistory.com/260</link>
      <description>&lt;p&gt;어떤 값이 집합에 &amp;quot;있을 수도 있음 / 없음은 확실함&amp;quot;을 매우 빠르고 메모리 효율적으로 판단하기 위한 확률적 자료 구조이다.&lt;/p&gt;
&lt;h2&gt;1. 목적&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;존재 여부 검사를 빠르게 하기 위함&lt;/li&gt;
&lt;li&gt;정확성보다 속도와 메모리 효율을 우선&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;대표적인 사용 목적&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;캐시 Penetration 방지&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;존재 가능한 키만 Bloom Filter에 미리 등록&lt;/li&gt;
&lt;li&gt;요청 키가 Bloom Filter에서 &lt;code&gt;없음&lt;/code&gt;이면 Cache / DB 접근 자체를 차단&lt;/li&gt;
&lt;li&gt;존재하지 않는 ID에 대한 반복 DB 조회 방지 및 봇·악성 트래픽 방어&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;중복 요청 차단&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;이미 처리된 요청 ID를 Bloom Filter에 기록하여 요청이 다시 들어올 경우 &amp;quot;이미 처리됨&amp;quot;으로 간주&lt;/li&gt;
&lt;li&gt;멱등성 보조 수단 및 결제, 메세지 발송 중복 방지 (정확성이 중요한 경우 단독 사용은 부적합) &lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;크롤러의 URL 중복 방문 방지&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;방문한 URL을 Bloom Filter에 저장&lt;/li&gt;
&lt;li&gt;이미 방문했을 가능성이 있으면 스킵&lt;/li&gt;
&lt;li&gt;메모리 절약 및 대규모 URL 집합 관리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 핵심 특징&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;항목&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;False Positive&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;있다고 나올 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;False Negative&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;절대 발생하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;삭제&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;기본 Bloom Filter는 삭제 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;메모리&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;매우 적게 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;시간복잡도&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;O(k) (k = 해시 함수 개수)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;False Positive (거짓 양성) : 없는데 있다고 판단&lt;/p&gt;
&lt;p&gt;False Negative (거짓 음성) : 있는데 없다고 판단&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;3. 구조&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;비트 배열 (bit array): 크기 m&lt;/li&gt;
&lt;li&gt;해시 함수 k개: 서로 독립적인 해시 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 동작 방식&lt;/h2&gt;
&lt;h3&gt;값 추가 (insert)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;값을 k개의 해시 함수에 넣음&lt;/li&gt;
&lt;li&gt;각각의 해시 결과는 비트 배열의 인덱스를 의미&lt;/li&gt;
&lt;li&gt;해당 인덱스의 비트를 &lt;code&gt;1&lt;/code&gt;로 설정&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;값 조회 (lookup)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;값을 동일하게 k개의 해시 함수에 넣음&lt;/li&gt;
&lt;li&gt;나온 인덱스들의 비트 확인&lt;/li&gt;
&lt;li&gt;결과&lt;ul&gt;
&lt;li&gt;하나라도 &lt;code&gt;0&lt;/code&gt; → 없음을 의미 (절대 없음)&lt;/li&gt;
&lt;li&gt;전부 &lt;code&gt;1&lt;/code&gt; → 있을 수도 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;5. 왜 False Positive가 생기는가&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;서로 다른 값들이 같은 비트 인덱스를 공유할 수 있음.&lt;/li&gt;
&lt;li&gt;비트 배열이 점점 &lt;code&gt;1&lt;/code&gt;로 채워질수록 새로운 값도 &amp;quot;있는 것처럼&amp;quot; 보일 확률 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 번이라도 &lt;code&gt;0&lt;/code&gt;이면 바로 &amp;quot;없음&amp;quot; 판단 가능&lt;/li&gt;
&lt;li&gt;그래서 False Negative는 발생하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 장단점&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;장점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메모리 사용량이 매우 작음&lt;/li&gt;
&lt;li&gt;속도가 빠름 (CPU 캐시 친화적)&lt;/li&gt;
&lt;li&gt;대규모 트래픽에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;단점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;100% 정확하지 않음&lt;/li&gt;
&lt;li&gt;삭제 불가 (→ Counting Bloom Filter로 보완, 비트를 카운터로 바꿔 삭제 가능)&lt;/li&gt;
&lt;li&gt;데이터가 많아질수록 정확도 저하&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. 무엇을 우선?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;n (들어갈 원소 수) 을 먼저 추정&lt;/li&gt;
&lt;li&gt;목표 false positive (p) 를 결정&lt;/li&gt;
&lt;li&gt;그에 맞춰 비트 배열 (m) 과 해시 함수 개수 (k) 를 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대략적으로:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;최적 해시 개수는 k ≈ (m/n) * ln 2&lt;/li&gt;
&lt;li&gt;false positive는 대략 p ≈ (1 - e^{-kn/m})^k&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8. 다른 효율화 방안&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Counting Bloom Filter&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;비트를 카운터로 바꾸므로 삭제 가능&lt;/li&gt;
&lt;li&gt;단점은 메모리를 더 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Scalable Bloom Filter&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;n이 예상보다 커졌을 때 성능이 망가지는 문제 해결&lt;/li&gt;
&lt;li&gt;Bloom Filter를 하나 더 붙여가며 확장 (계층 구조)&lt;/li&gt;
&lt;li&gt;과소설계 리스트를 완화시킴&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Partitioned Bloom Filter&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;비트 배열을 k개의 구간으로 나누고 해시마다 구간을 고정&lt;/li&gt;
&lt;li&gt;비트 분포가 더 균등해져 성능이 안정적일 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;해시 함수 최적화 (더블 해싱)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;해시 2개로 k개를 파싱&lt;/li&gt;
&lt;li&gt;빠르고 구현이 단순&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/Structure</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/260</guid>
      <comments>https://soobarkbar.tistory.com/260#entry260comment</comments>
      <pubDate>Thu, 18 Jun 2026 23:17:12 +0900</pubDate>
    </item>
    <item>
      <title>Redis가 정말 싱글 쓰레드로 작동할까?</title>
      <link>https://soobarkbar.tistory.com/259</link>
      <description>&lt;p&gt;Redis는 클라이언트 요청을 이벤트 루프 기반으로 처리하고, Redis 자료구조를 직접 읽고 쓰는 명령 실행은 메인 쓰레드 중심으로 동작한다. 하지만 파일 닫기, AOF fsync, 메모리 해제 같은 일부 느린 작업은 백그라운드 쓰레드로 넘긴다.&lt;/p&gt;
&lt;p&gt;그래서 Redis를 정확히 이해하려면 다음 두 가지를 구분해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Redis 명령 실행 경로
   - GET, SET, INCR, HSET 같은 명령 실행
   - 메인 쓰레드 중심

2. Redis 프로세스 전체
   - 메인 쓰레드 외에도 백그라운드 쓰레드 존재
   - 파일 닫기, AOF fsync, lazy free 등 처리&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;1. Redis가 싱글 쓰레드라는 말의 의미&lt;/h2&gt;
&lt;p&gt;Redis가 싱글 쓰레드라고 할 때 핵심은 &lt;strong&gt;데이터를 읽고 쓰는 명령 실행 경로가 기본적으로 하나의 메인 쓰레드에서 처리된다&lt;/strong&gt;는 뜻이다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음과 같은 명령이 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SET user:1 &amp;quot;kim&amp;quot;
GET user:1
INCR view:post:100
LPUSH queue:order 1001&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 명령들은 Redis 내부의 메인 이벤트 루프에서 순차적으로 실행된다.&lt;/p&gt;
&lt;p&gt;즉, 여러 클라이언트가 동시에 요청을 보내더라도 Redis는 내부적으로 명령을 하나씩 꺼내 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client A -&amp;gt; SET user:1
Client B -&amp;gt; GET user:1
Client C -&amp;gt; INCR view:post:100

Redis Main Thread
1. SET user:1 처리
2. GET user:1 처리
3. INCR view:post:100 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조 덕분에 Redis는 명령 실행 중에 여러 쓰레드가 같은 자료구조를 동시에 수정하는 문제를 피할 수 있다.&lt;/p&gt;
&lt;p&gt;멀티 쓰레드 구조라면 다음과 같은 고민이 필요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Thread 1: user:1 수정
Thread 2: user:1 조회
Thread 3: user:1 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경우 동시성 제어를 위해 lock이 필요하다.&lt;/p&gt;
&lt;p&gt;하지만 Redis는 핵심 명령 실행을 단일 쓰레드에서 처리하기 때문에, 내부 자료구조 접근에 대해 복잡한 lock 경합을 크게 줄일 수 있다.&lt;/p&gt;
&lt;p&gt;이게 Redis가 단순하면서도 빠른 이유 중 하나다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 그럼 Redis 프로세스에는 쓰레드가 하나만 있을까?&lt;/h2&gt;
&lt;p&gt;그건 아니다.&lt;/p&gt;
&lt;p&gt;Redis에는 메인 쓰레드 외에도 백그라운드 작업을 처리하기 위한 쓰레드들이 존재한다.&lt;/p&gt;
&lt;p&gt;책에서 본 것처럼 Redis는 표면적으로는 싱글 쓰레드처럼 설명되지만, 실제로는 메인 쓰레드 외에 백그라운드 쓰레드가 함께 동작한다.&lt;/p&gt;
&lt;p&gt;대표적으로 Redis의 background I/O, 즉 BIO 쓰레드는 다음과 같은 작업을 담당한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread
- 클라이언트 연결 처리
- 명령 파싱
- 명령 실행
- 응답 생성

Background I/O Threads
- BIO_CLOSE_FILE
- BIO_AOF_FSYNC
- BIO_LAZY_FREE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 중요한 점은 백그라운드 쓰레드가 Redis의 핵심 데이터 명령을 병렬로 실행하는 것이 아니라는 점이다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;SET&lt;/code&gt;, &lt;code&gt;INCR&lt;/code&gt;, &lt;code&gt;HSET&lt;/code&gt; 같은 명령을 여러 쓰레드가 동시에 나눠 실행하는 구조가 아니다.&lt;/p&gt;
&lt;p&gt;백그라운드 쓰레드는 주로 메인 쓰레드가 직접 처리하면 오래 걸릴 수 있는 작업을 뒤로 넘겨서 처리한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Redis의 3가지 백그라운드 쓰레드&lt;/h2&gt;
&lt;p&gt;Redis의 백그라운드 I/O 쓰레드는 역할별로 나누어 볼 수 있다.&lt;/p&gt;
&lt;p&gt;대표적으로 다음 3가지가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. BIO_CLOSE_FILE
   - 파일 닫기 작업 처리

2. BIO_AOF_FSYNC
   - AOF 파일 fsync 처리

3. BIO_LAZY_FREE
   - 큰 객체의 메모리 해제 작업 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 쓰레드는 Redis 명령을 대신 실행하는 것이 아니라, &lt;strong&gt;메인 쓰레드가 오래 붙잡고 있으면 위험한 보조 작업을 비동기적으로 처리한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;3-1. BIO_CLOSE_FILE&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BIO_CLOSE_FILE&lt;/code&gt;은 이름 그대로 파일을 닫는 작업을 담당한다.&lt;/p&gt;
&lt;p&gt;일반적으로 파일을 닫는 &lt;code&gt;close()&lt;/code&gt; 작업은 매우 빠를 것처럼 보인다. 하지만 운영체제나 파일 시스템 상황에 따라 파일을 닫는 과정이 예상보다 오래 걸릴 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어 Redis가 AOF 파일을 교체하거나, 임시 파일을 사용한 뒤 닫아야 하는 상황이 있다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Main Thread: 파일 작업 완료
2. Main Thread: 파일 닫기 작업을 BIO_CLOSE_FILE 큐에 등록
3. BIO_CLOSE_FILE Thread: 실제 close 작업 수행
4. Main Thread: 클라이언트 요청 계속 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 메인 쓰레드가 직접 파일 닫기를 처리하다가 지연되면, Redis는 그동안 다른 클라이언트 명령을 처리하지 못한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;나쁜 흐름

Main Thread
1. SET user:1 처리
2. 파일 close 처리 시작
3. close 작업 지연
4. GET user:2 대기
5. INCR view:1 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그래서 Redis는 파일 닫기처럼 가끔 지연될 수 있는 작업을 백그라운드 쓰레드로 넘긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;개선된 흐름

Main Thread
1. SET user:1 처리
2. close 작업을 백그라운드 큐에 등록
3. GET user:2 처리
4. INCR view:1 처리

BIO_CLOSE_FILE Thread
1. 실제 파일 close 수행&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 파일 닫기가 Redis 데이터 명령 실행과 직접 관련된 작업은 아니라는 점이다. 따라서 백그라운드로 넘겨도 Redis의 명령 처리 일관성을 해치지 않는다.&lt;/p&gt;
&lt;h3&gt;3-2. BIO_AOF_FSYNC&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BIO_AOF_FSYNC&lt;/code&gt;는 AOF(Append Only File)의 &lt;code&gt;fsync&lt;/code&gt; 작업을 담당한다.&lt;/p&gt;
&lt;p&gt;Redis에서 AOF를 사용하면 쓰기 명령이 AOF 파일에 기록된다.&lt;/p&gt;
&lt;p&gt;예를 들어 다음 명령을 실행했다고 하자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SET user:1 &amp;quot;kim&amp;quot;
INCR view:post:100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AOF를 켜두면 Redis는 이 쓰기 명령들을 파일에 append 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appendonly.aof
SET user:1 &amp;quot;kim&amp;quot;
INCR view:post:100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런데 파일에 썼다는 것과 디스크에 안전하게 반영됐다는 것은 다르다.&lt;/p&gt;
&lt;p&gt;운영체제는 성능을 위해 파일 쓰기를 메모리 버퍼에 잠시 보관할 수 있다. 이때 &lt;code&gt;fsync&lt;/code&gt;는 “버퍼에 있는 내용을 실제 디스크에 반영해라”라고 요청하는 작업이다.&lt;/p&gt;
&lt;p&gt;문제는 &lt;code&gt;fsync&lt;/code&gt;가 디스크 I/O 작업이기 때문에 느려질 수 있다는 점이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread가 fsync까지 직접 처리하는 경우

1. SET user:1 처리
2. AOF 파일에 기록
3. fsync 수행
4. 디스크 응답 대기
5. 다음 명령 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 흐름에서는 디스크가 느려지는 순간 Redis 전체 응답 시간이 흔들릴 수 있다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;code&gt;appendfsync everysec&lt;/code&gt; 설정에서는 보통 fsync를 매 명령마다 하지 않고, 약 1초 단위로 백그라운드에서 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread
1. SET user:1 처리
2. AOF 버퍼 또는 파일에 append
3. 클라이언트에 응답
4. 다음 명령 처리

BIO_AOF_FSYNC Thread
1. 주기적으로 AOF 파일 fsync 수행
2. 디스크 반영 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식의 장점은 메인 쓰레드가 디스크 fsync 때문에 오래 멈추는 상황을 줄일 수 있다는 것이다.&lt;/p&gt;
&lt;p&gt;다만 트레이드오프도 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appendfsync always
- 매 쓰기마다 fsync
- 내구성 강함
- 성능 저하 가능성 큼

appendfsync everysec
- 보통 1초 단위 fsync
- 성능과 내구성의 균형
- 장애 시 최근 약 1초 데이터 유실 가능

appendfsync no
- fsync를 운영체제에 맡김
- 성능은 좋을 수 있음
- 내구성 보장은 약함&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, &lt;code&gt;BIO_AOF_FSYNC&lt;/code&gt;는 Redis의 명령 실행을 빠르게 유지하면서도 AOF 내구성을 확보하기 위한 백그라운드 작업이라고 볼 수 있다.&lt;/p&gt;
&lt;h3&gt;3-3. BIO_LAZY_FREE&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BIO_LAZY_FREE&lt;/code&gt;는 큰 객체의 메모리 해제를 백그라운드에서 처리한다.&lt;/p&gt;
&lt;p&gt;Redis에서 key를 삭제할 때 실제로는 두 가지 일이 발생한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. keyspace에서 key 제거
2. 해당 value가 사용하던 메모리 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;작은 문자열 key라면 이 작업은 매우 빠르다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL user:1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;하지만 value가 매우 큰 List, Set, Sorted Set, Hash라면 이야기가 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 &lt;code&gt;big:list&lt;/code&gt;에 수백만 개의 요소가 들어 있다면, key를 제거하는 것보다 내부 요소들의 메모리를 해제하는 작업이 오래 걸릴 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DEL big:list

Main Thread
1. keyspace에서 big:list 찾기
2. key 제거
3. list 내부 요소 메모리 해제
4. 해제가 끝날 때까지 다른 명령 대기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 문제를 줄이기 위해 Redis는 &lt;code&gt;UNLINK&lt;/code&gt;를 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UNLINK&lt;/code&gt;는 key를 먼저 keyspace에서 제거한다. 그러면 이후 Redis 명령에서는 해당 key에 접근할 수 없다.&lt;/p&gt;
&lt;p&gt;그리고 실제 메모리 해제 작업은 &lt;code&gt;BIO_LAZY_FREE&lt;/code&gt; 쓰레드로 넘긴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;UNLINK big:list

Main Thread
1. keyspace에서 big:list 제거
2. 메모리 해제 작업을 BIO_LAZY_FREE 큐에 등록
3. 바로 다음 명령 처리

BIO_LAZY_FREE Thread
1. big:list 내부 요소 메모리 해제
2. 사용하던 메모리 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조가 안전한 이유는 메인 쓰레드가 먼저 keyspace에서 key를 제거하기 때문이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Main Thread가 keyspace에서 key 제거
2. 이후 어떤 Redis 명령도 그 key에 접근할 수 없음
3. Background Thread가 실제 value 메모리 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 백그라운드 쓰레드가 Redis 자료구조를 마음대로 수정하는 것이 아니라, 이미 접근 불가능해진 객체의 메모리를 정리하는 역할을 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BIO_LAZY_FREE&lt;/code&gt;는 &lt;code&gt;UNLINK&lt;/code&gt;뿐 아니라 설정에 따라 만료, eviction, &lt;code&gt;FLUSHDB ASYNC&lt;/code&gt;, &lt;code&gt;FLUSHALL ASYNC&lt;/code&gt; 같은 작업에서도 사용될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:key
FLUSHDB ASYNC
FLUSHALL ASYNC&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;관련 설정으로는 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-conf&quot;&gt;lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 설정들은 eviction, expire, 서버 내부 삭제, replica flush 같은 상황에서 메모리 해제를 가능한 한 백그라운드로 넘길지 결정한다.&lt;/p&gt;
&lt;p&gt;정리하면 &lt;code&gt;BIO_LAZY_FREE&lt;/code&gt;는 Redis의 싱글 쓰레드 구조에서 가장 체감하기 쉬운 백그라운드 쓰레드다. 큰 key 삭제나 전체 flush 작업이 메인 쓰레드를 오래 막지 않도록 도와주기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. 왜 백그라운드 쓰레드가 필요할까?&lt;/h2&gt;
&lt;p&gt;Redis는 메인 쓰레드가 클라이언트 요청을 순차적으로 처리한다.&lt;/p&gt;
&lt;p&gt;이 구조에서는 하나의 작업이 오래 걸리면 뒤에 있는 요청들이 모두 기다려야 한다.&lt;/p&gt;
&lt;p&gt;예를 들어 아주 큰 key를 삭제한다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;만약 &lt;code&gt;big:list&lt;/code&gt;가 매우 큰 리스트라면, 삭제 과정에서 메모리를 해제하는 작업이 오래 걸릴 수 있다.&lt;/p&gt;
&lt;p&gt;Redis 메인 쓰레드가 이 작업을 끝까지 직접 처리하면 그동안 다른 요청 처리가 지연될 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. DEL big:list 처리 시작
2. 큰 메모리 해제 작업 수행
3. 작업이 끝날 때까지 다른 요청 대기
4. 이후 GET, SET 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 문제를 줄이기 위해 Redis는 &lt;code&gt;UNLINK&lt;/code&gt; 같은 명령을 제공한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UNLINK&lt;/code&gt;는 key를 논리적으로 제거한 뒤, 실제 메모리 해제 작업은 백그라운드에서 처리할 수 있게 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Main Thread: key를 keyspace에서 제거
2. BIO_LAZY_FREE Thread: 실제 메모리 해제
3. Main Thread: 다음 요청 계속 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, Redis가 백그라운드 쓰레드를 사용하는 이유는 핵심 명령 실행을 병렬화하기 위해서라기보다, &lt;strong&gt;메인 이벤트 루프가 오래 막히지 않도록 느린 작업을 분리하기 위해서&lt;/strong&gt;라고 보는 편이 맞다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Redis 6부터는 I/O Thread도 있다&lt;/h2&gt;
&lt;p&gt;Redis 6부터는 I/O Thread 기능이 추가되었다.&lt;/p&gt;
&lt;p&gt;여기서 다시 헷갈릴 수 있다.&lt;/p&gt;
&lt;p&gt;“Redis 6부터 멀티 쓰레드라면 이제 명령도 병렬로 실행되는 건가?”&lt;/p&gt;
&lt;p&gt;그렇지는 않다.&lt;/p&gt;
&lt;p&gt;Redis 6의 I/O Thread는 주로 네트워크 I/O를 병렬화하기 위한 기능이다.&lt;/p&gt;
&lt;p&gt;Redis 요청 처리를 단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. 클라이언트 요청 읽기
2. 명령 파싱
3. 명령 실행
4. 응답 쓰기&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기존에는 이 흐름 대부분을 메인 쓰레드가 처리했다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Main Thread
Read -&amp;gt; Parse -&amp;gt; Execute -&amp;gt; Write&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis 6 이후에는 설정에 따라 일부 네트워크 읽기/쓰기 작업을 I/O Thread가 도와줄 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;I/O Thread
- socket read
- socket write
- command parsing 일부

Main Thread
- command execute
- Redis data structure access
- reply 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, Redis 6 이후에도 핵심은 동일하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;네트워크 I/O 일부는 멀티 쓰레드 가능
하지만 Redis 자료구조를 변경하는 명령 실행은 메인 쓰레드 중심&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 Redis의 I/O Thread와 앞에서 설명한 BIO 쓰레드는 역할이 다르다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BIO Thread
- 파일 닫기
- AOF fsync
- lazy free
- 메인 쓰레드가 오래 막힐 수 있는 내부 작업 처리

I/O Thread
- 클라이언트 socket read/write 보조
- 네트워크 I/O 병목 완화&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, 둘 다 Redis 프로세스 내부의 추가 쓰레드지만 목적이 다르다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BIO Thread&lt;/code&gt;는 Redis 내부의 느린 작업을 백그라운드로 넘기기 위한 쓰레드이고, &lt;code&gt;I/O Thread&lt;/code&gt;는 클라이언트 네트워크 I/O 처리량을 높이기 위한 쓰레드다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. Redis가 싱글 쓰레드인데도 빠른 이유&lt;/h2&gt;
&lt;p&gt;Redis가 빠른 이유를 단순히 “메모리를 사용하기 때문”이라고만 보면 부족하다.&lt;/p&gt;
&lt;p&gt;물론 Redis는 대부분의 데이터를 메모리에 두기 때문에 디스크 기반 DB보다 빠른 접근이 가능하다.&lt;/p&gt;
&lt;p&gt;하지만 그 외에도 몇 가지 이유가 있다.&lt;/p&gt;
&lt;h3&gt;1) 명령 실행이 단순하다&lt;/h3&gt;
&lt;p&gt;Redis 명령은 대부분 짧고 빠르게 끝난다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;GET user:1
SET token:abc &amp;quot;...&amp;quot;
INCR view:post:1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 명령은 일반적인 RDBMS 쿼리처럼 복잡한 실행계획을 세우거나, 여러 테이블을 조인하거나, 디스크 블록을 많이 읽는 구조가 아니다.&lt;/p&gt;
&lt;p&gt;대부분 메모리의 자료구조에 직접 접근한다.&lt;/p&gt;
&lt;h3&gt;2) I/O Multiplexing을 사용한다&lt;/h3&gt;
&lt;p&gt;Redis는 하나의 쓰레드로도 여러 클라이언트 연결을 다룰 수 있다.&lt;/p&gt;
&lt;p&gt;이때 사용하는 방식이 I/O Multiplexing이다.&lt;/p&gt;
&lt;p&gt;쉽게 말하면, 클라이언트마다 쓰레드를 하나씩 만드는 것이 아니라, 하나의 이벤트 루프가 여러 소켓을 감시하다가 준비된 요청만 처리하는 방식이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client 1
Client 2
Client 3
Client 4
   ↓
Event Loop
   ↓
Ready 된 요청만 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조는 Node.js의 이벤트 루프와도 비슷하게 이해할 수 있다.&lt;/p&gt;
&lt;h3&gt;3) Lock 경합이 적다&lt;/h3&gt;
&lt;p&gt;멀티 쓰레드로 명령을 동시에 실행하면 공유 데이터에 대한 lock이 필요하다.&lt;/p&gt;
&lt;p&gt;하지만 Redis는 핵심 명령 실행을 단일 쓰레드에서 순차적으로 처리하기 때문에 lock 경합 비용이 작다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;멀티 쓰레드 방식
- 동시에 실행 가능
- lock 필요
- context switching 비용 발생
- 동시성 버그 가능성 증가

Redis 방식
- 명령 실행은 순차 처리
- lock 경합 감소
- 구현 단순
- 예측 가능한 실행 흐름&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis는 이 단순한 구조를 선택한 대신, 오래 걸리는 작업을 피하거나 백그라운드로 넘기는 방식으로 성능을 유지한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 싱글 쓰레드 구조에서 조심해야 할 명령&lt;/h2&gt;
&lt;p&gt;Redis가 빠르다고 해서 모든 명령이 항상 안전한 것은 아니다.&lt;/p&gt;
&lt;p&gt;싱글 쓰레드 구조에서는 오래 걸리는 명령 하나가 전체 요청 처리에 영향을 줄 수 있다.&lt;/p&gt;
&lt;p&gt;대표적인 예가 &lt;code&gt;KEYS&lt;/code&gt;다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;KEYS *&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 명령은 전체 keyspace를 순회한다.&lt;/p&gt;
&lt;p&gt;운영 환경에서 key가 많다면 메인 쓰레드가 오랫동안 이 작업을 수행하게 되고, 그동안 다른 요청이 밀릴 수 있다.&lt;/p&gt;
&lt;p&gt;개선된 방식은 &lt;code&gt;SCAN&lt;/code&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SCAN 0 MATCH user:* COUNT 100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SCAN&lt;/code&gt;은 전체를 한 번에 훑는 것이 아니라 cursor 기반으로 조금씩 순회한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;KEYS
- 전체 key를 한 번에 조회
- 메인 쓰레드가 오래 점유될 수 있음
- 운영 환경에서 위험

SCAN
- cursor 기반으로 나눠 조회
- 한 번에 처리하는 양을 조절 가능
- 운영 환경에서 상대적으로 안전&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;큰 key 삭제도 마찬가지다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;DEL big:key&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;큰 객체를 삭제할 때는 다음처럼 &lt;code&gt;UNLINK&lt;/code&gt;를 고려할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;UNLINK big:key&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DEL
- key 삭제와 메모리 해제를 동기적으로 처리할 수 있음
- 큰 key에서는 지연 발생 가능

UNLINK
- keyspace에서는 빠르게 제거
- 실제 메모리 해제는 BIO_LAZY_FREE가 백그라운드 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AOF를 사용하는 환경에서는 &lt;code&gt;appendfsync&lt;/code&gt; 설정도 중요하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-conf&quot;&gt;appendonly yes
appendfsync everysec&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;보통은 &lt;code&gt;everysec&lt;/code&gt;가 성능과 내구성의 균형점으로 많이 사용된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;appendfsync always
- 쓰기마다 디스크 동기화
- 안전하지만 느려질 수 있음

appendfsync everysec
- 약 1초 단위로 fsync
- BIO_AOF_FSYNC가 백그라운드에서 처리
- 일반적인 운영 환경에서 많이 사용

appendfsync no
- 운영체제 정책에 맡김
- 빠를 수 있지만 장애 시 유실 범위가 커질 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;8. Redis를 “싱글 쓰레드”라고만 말하면 부족한 이유&lt;/h2&gt;
&lt;p&gt;Redis를 이해할 때는 관점을 나눠야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. 명령 실행 관점
   - 대부분 싱글 쓰레드
   - Redis 자료구조 접근은 메인 쓰레드 중심

2. Redis 프로세스 관점
   - 메인 쓰레드 외 백그라운드 쓰레드 존재
   - BIO_CLOSE_FILE, BIO_AOF_FSYNC, BIO_LAZY_FREE 등 처리

3. Redis 6 이후 네트워크 I/O 관점
   - I/O Thread 사용 가능
   - socket read/write 일부를 병렬 처리 가능

4. Persistence 관점
   - AOF fsync는 백그라운드 쓰레드가 도울 수 있음
   - RDB snapshot, AOF rewrite는 별도 프로세스가 관여할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;따라서 정확히 표현하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Redis는 클라이언트 명령 실행과 데이터 접근은 주로 싱글 쓰레드로 처리하지만, 백그라운드 I/O 작업과 네트워크 I/O 최적화를 위해 여러 쓰레드 또는 별도 프로세스를 사용할 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;9. 실제 운영에서는 어떻게 봐야 할까?&lt;/h2&gt;
&lt;p&gt;Redis를 운영할 때는 “싱글 쓰레드니까 CPU 코어 하나만 보면 된다”라고 생각하면 안 된다.&lt;/p&gt;
&lt;p&gt;물론 명령 실행 자체는 메인 쓰레드 병목이 될 수 있다.&lt;br&gt;그래서 Redis는 보통 하나의 인스턴스가 하나의 CPU 코어를 강하게 사용하는 형태로 나타날 수 있다.&lt;/p&gt;
&lt;p&gt;하지만 다음 요소들도 함께 봐야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;- 메인 쓰레드 CPU 사용률
- 네트워크 I/O 병목
- 큰 key 삭제 여부
- AOF fsync 지연
- RDB snapshot 또는 AOF rewrite 시점
- slowlog
- latency monitor
- lazy free pending object 증가 여부&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 Redis가 느려졌을 때는 단순히 CPU만 볼 게 아니라 다음 명령들을 같이 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;SLOWLOG GET 10&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO stats&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO commandstats&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO persistence&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;INFO memory&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;INFO memory&lt;/code&gt;에서는 lazy free와 관련된 지표를 확인할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;lazyfree_pending_objects&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 값이 계속 증가한다면 백그라운드에서 해제해야 할 객체가 많이 쌓이고 있다는 의미로 볼 수 있다.&lt;/p&gt;
&lt;p&gt;또 AOF를 사용하는 경우에는 &lt;code&gt;INFO persistence&lt;/code&gt;를 통해 AOF rewrite, fsync, persistence 관련 상태를 함께 확인하는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. 마무리&lt;/h2&gt;
&lt;p&gt;Redis는 흔히 싱글 쓰레드라고 설명된다.&lt;/p&gt;
&lt;p&gt;이 말은 틀린 말은 아니지만, 정확히는 &lt;strong&gt;Redis의 핵심 명령 실행 경로가 싱글 쓰레드에 가깝다&lt;/strong&gt;는 의미로 이해해야 한다.&lt;/p&gt;
&lt;p&gt;Redis 프로세스 전체를 보면 메인 쓰레드 외에도 백그라운드 쓰레드가 존재하고, Redis 6 이후에는 네트워크 I/O를 위한 I/O Thread도 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Redis는 완전한 의미의 단일 쓰레드 프로그램은 아니다.

하지만 Redis의 핵심 데이터 명령 실행은 여전히 메인 쓰레드 중심으로 동작한다.

BIO_CLOSE_FILE은 파일 닫기 작업을 백그라운드에서 처리한다.

BIO_AOF_FSYNC는 AOF fsync 작업을 백그라운드에서 처리한다.

BIO_LAZY_FREE는 큰 객체의 메모리 해제를 백그라운드에서 처리한다.

Redis 6 이후의 I/O Thread도 명령 실행을 병렬화하는 것이 아니라 네트워크 I/O 병목을 줄이기 위한 기능이다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결국 Redis의 싱글 쓰레드 모델은 단순한 제약이 아니라, 빠른 메모리 접근과 이벤트 루프, lock 없는 명령 실행을 조합한 설계 선택이라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;Redis를 사용할 때 중요한 것은 “싱글 쓰레드냐, 멀티 쓰레드냐”를 외우는 것이 아니라, &lt;strong&gt;어떤 작업이 메인 쓰레드를 막고 어떤 작업이 백그라운드로 분리될 수 있는지 이해하는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Redis 공식 문서 - Latency optimization&lt;/li&gt;
&lt;li&gt;Redis 공식 문서 - INFO command&lt;/li&gt;
&lt;li&gt;Redis 공식 블로그 - The little-known feature of Redis 4.0 that will speed up your applications&lt;/li&gt;
&lt;li&gt;Redis 공식 블로그 - Redis 8.0-M03 is out. Even more performance &amp;amp; new features&lt;/li&gt;
&lt;li&gt;Redis GitHub source - &lt;code&gt;src/bio.c&lt;/code&gt;, &lt;code&gt;src/bio.h&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/259</guid>
      <comments>https://soobarkbar.tistory.com/259#entry259comment</comments>
      <pubDate>Sun, 7 Jun 2026 17:44:48 +0900</pubDate>
    </item>
    <item>
      <title>인덱스 풀스캔(Index Full Scan) 훑어보기</title>
      <link>https://soobarkbar.tistory.com/258</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 보다 보면 &lt;code&gt;range&lt;/code&gt;, &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;ALL&lt;/code&gt; 같은 접근 방식과 함께&lt;br /&gt;DBMS에 따라 &lt;b&gt;인덱스 풀스캔&lt;/b&gt;에 가까운 동작이 등장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름만 보면 비효율적으로 느껴질 수 있지만, 실제로는 상황에 따라 꽤 합리적인 선택이 된다.&lt;br /&gt;중요한 건 &lt;b&gt;&amp;ldquo;인덱스를 일부만 읽는 스캔이 아니라, 인덱스 전체를 읽는 스캔&amp;rdquo;&lt;/b&gt; 이라는 점이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 인덱스 풀스캔이 무엇이며, 어떻게 동작하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 풀스캔은 말 그대로 &lt;b&gt;인덱스의 처음부터 끝까지 전체를 읽는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 인덱스는 특정 값을 빠르게 찾기 위해 사용한다고 배운다.&lt;br /&gt;예를 들어 &lt;code&gt;where id = 10&lt;/code&gt; 같은 조건에서는 인덱스의 일부만 읽으면 된다.&lt;br /&gt;그런데 어떤 쿼리는 조건 검색보다도 &lt;b&gt;정렬된 순서&lt;/b&gt;나 &lt;b&gt;인덱스에 담긴 컬럼 자체&lt;/b&gt;가 더 중요하다.&lt;br /&gt;이럴 때 옵티마이저는 테이블 전체를 읽는 대신 &lt;b&gt;인덱스 전체를 순서대로 읽는 방식&lt;/b&gt;을 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 왜 인덱스 전체를 읽는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 단순하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스는 보통 테이블보다 &lt;b&gt;폭이 더 좁다&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 건을 저장하는 데이터 크기가 더 작아서 같은 양을 읽을 때 필요한 블록 수가 더 적을 수 있다. 용량을 많이 차지하는 데이터 타입 (varchar(1000), blob, ...) 이 있다면 하나의 블록에 들어갈 수 있는 로우 수가 적어지기 때문에 테이블의 폭이 더 넓을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B-Tree 인덱스는 키 순서대로 정렬되어 있다&lt;/li&gt;
&lt;li&gt;필요한 컬럼이 인덱스 안에 있으면 테이블까지 안 가도 되는 경우가 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;전체를 읽더라도 테이블보다 인덱스가 비용이 더 싸다&amp;rdquo;라고 판단되면 인덱스 풀스캔이 나올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 동작 방식&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인덱스 루트/브랜치 블록을 따라 첫 리프 블록으로 이동한다&lt;/li&gt;
&lt;li&gt;리프 블록을 처음부터 끝까지 읽는다&lt;/li&gt;
&lt;li&gt;필요한 컬럼이 인덱스에 다 있으면 결과를 바로 반환한다&lt;/li&gt;
&lt;li&gt;필요한 컬럼이 인덱스에 없으면 테이블을 추가 조회한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE employees (
    emp_id INT,
    dept_id INT,
    emp_name VARCHAR(100),
    hire_date DATE,
    salary INT
);

CREATE INDEX idx_emp_hire_date ON employees(hire_date);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT hire_date
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 hire_date만 필요하고, 정렬도 hire_date 기준이다.&lt;br /&gt;이 경우 테이블을 전부 읽고 정렬하는 것보다, idx_emp_hire_date 인덱스를 처음부터 끝까지 읽는 편이 더 유리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 인덱스 풀스캔이 유리한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 풀스캔은 &amp;ldquo;풀스캔&amp;rdquo;이라는 이름 때문에 무조건 나쁜 방식처럼 보이지만, 실제로는 꽤 유용한 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 필요한 컬럼이 인덱스에 다 들어 있는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 가장 유리하다.&lt;br /&gt;인덱스만 읽고 결과를 만들 수 있으니 테이블 접근을 줄일 수 있다. MySQL에서는 이런 경우 Using index가 붙는 covering index 형태가 될 수 있고, Oracle에서도 인덱스만으로 결과를 만들 수 있을 때 빠른 경로를 선택할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE INDEX idx_emp_dept_salary ON employees(dept_id, salary);

SELECT dept_id, salary
FROM employees
ORDER BY dept_id, salary;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 컬럼도 인덱스에 있고, 정렬 순서도 인덱스 순서와 맞는다.&lt;br /&gt;이런 경우 인덱스를 전체로 읽는 방식이 꽤 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 결과를 이미 정렬된 상태로 가져오고 싶은 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree 인덱스는 키 순서대로 저장된다.&lt;br /&gt;그래서 ORDER BY가 인덱스 순서와 맞아떨어지면 별도의 정렬 작업을 줄일 수 있다. MySQL은 인덱스를 이용해 정렬을 피할 수 있고, Oracle의 일반적인 index full scan도 인덱스 키 순서를 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT hire_date
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 전체를 읽고 sort 하는 것보다, hire_date 인덱스를 순서대로 읽는 편이 더 나을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 테이블보다 인덱스가 훨씬 가벼운 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블은 컬럼이 많고 행 크기가 큰 반면, 인덱스는 상대적으로 얇다.&lt;br /&gt;그래서 &amp;ldquo;어차피 많이 읽어야 한다면 차라리 인덱스를 읽자&amp;rdquo;라는 판단이 가능하다. MySQL 문서도 covering index의 경우 인덱스만 스캔하는 것이 보통 테이블 전체를 읽는 것보다 빠를 수 있다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;employees 테이블에 큰 TEXT, JSON, BLOB 컬럼이 많다고 가정하자.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT dept_id
FROM employees
ORDER BY dept_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 dept_id 인덱스만 훑는 편이 테이블 전체를 읽는 것보다 부담이 적을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 인덱스 풀스캔이 불리한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 인덱스 풀스캔이 비효율적인 경우도 분명하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 대부분 행에 대해 다시 테이블 접근이 필요한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스에는 키 값만 있고 실제 필요한 컬럼은 테이블에 있는 경우가 많다.&lt;br /&gt;이때 인덱스를 전체로 읽은 뒤, 각 행마다 다시 테이블을 찾아가면 랜덤 I/O가 많이 생길 수 있다.&lt;br /&gt;이 경우는 차라리 테이블 풀스캔이 더 나을 수 있다. Oracle도 다른 접근 경로보다 비용이 높으면 full table scan을 선택한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hire_date 인덱스는 정렬에는 도움이 되지만, SELECT * 이므로 결국 대부분 행을 다시 테이블에서 읽어야 한다.&lt;br /&gt;데이터 양이 많으면 비효율적일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 조건으로 일부만 읽어도 되는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 대상이 일부 구간에 불과하다면 인덱스 전체를 읽는 것보다 레인지 스캔이 더 낫다.&lt;br /&gt;MySQL 문서도 range access는 인덱스 값의 일부 구간만 읽는 방식이라고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT emp_id, hire_date
FROM employees
WHERE hire_date BETWEEN '2025-01-01' AND '2025-01-31';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우는 전체 인덱스를 다 볼 필요가 없다.&lt;br /&gt;조건에 맞는 구간만 읽는 것이 훨씬 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 인덱스 정렬 순서가 쿼리와 맞지 않는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 전체로 읽더라도 원하는 정렬이나 필터링 조건과 맞지 않으면 이점이 줄어든다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE INDEX idx_emp_dept_id ON employees(dept_id);

SELECT salary
FROM employees
ORDER BY hire_date;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인덱스는 dept_id 기준인데, 쿼리는 hire_date 기준으로 정렬한다.&lt;br /&gt;이런 경우 idx_emp_dept_id를 풀스캔해도 정렬 이점을 얻기 어렵다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. MySQL과 Oracle에서 인덱스 풀스캔이 어떻게 다르게 동작하는지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 &amp;ldquo;인덱스를 많이 읽는다&amp;rdquo;는 점은 비슷하지만, 실행 계획에서의 의미와 세부 동작은 다르게 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) Oracle은 INDEX FULL SCAN과 INDEX FAST FULL SCAN을 구분한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle은 인덱스 전체를 읽는 방식도 두 가지로 나눠서 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;INDEX FULL SCAN&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스를 키 순서대로 읽는다&lt;/li&gt;
&lt;li&gt;ORDER BY를 만족시키는 데 유리하다&lt;/li&gt;
&lt;li&gt;보통 단일 블록 I/O 성격으로 이해한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;INDEX FAST FULL SCAN&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 전체를 정렬 순서와 무관하게 빠르게 읽는다&lt;/li&gt;
&lt;li&gt;멀티블록 I/O, 병렬 처리 활용이 가능하다&lt;/li&gt;
&lt;li&gt;물리적으로 디스크에 저장된 순서대로 블록을 읽기 때문에 결과가 인덱스 순서로 나오지 않을 수 있다&lt;/li&gt;
&lt;li&gt;인덱스에 필요한 컬럼이 모두 있을 때 full table scan의 대안이 될 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) MySQL은 보통 type=index와 Using index를 같이 해석해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 실행 계획에서는 Oracle처럼 INDEX FULL SCAN, INDEX FAST FULL SCAN이라는 이름으로 세분화해서 보여주지 않는다.&lt;br /&gt;대신 EXPLAIN 결과에서 type=index가 나오면 인덱스를 처음부터 끝까지 읽는 스캔에 가깝게 해석하는 경우가 많다.&lt;br /&gt;그리고 Extra에 Using index가 있으면 인덱스만으로 결과를 처리하는 covering index 가능성이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) Oracle은 &amp;ldquo;정렬 보장 여부&amp;rdquo;까지 구분해서 보고, MySQL은 실행 계획 컬럼 조합으로 해석하는 편이다&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Oracle&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;표현 방식&lt;/td&gt;
&lt;td&gt;INDEX FULL SCAN, INDEX FAST FULL SCAN 등으로 구분&lt;/td&gt;
&lt;td&gt;보통 type=index, Extra=Using index 등 조합으로 해석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬 순서 활용&lt;/td&gt;
&lt;td&gt;INDEX FULL SCAN은 키 순서 활용 가능&lt;/td&gt;
&lt;td&gt;인덱스 순서를 활용할 수 있지만 Oracle처럼 명시적으로 scan 이름이 나뉘진 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인덱스만 읽는 빠른 전체 스캔&lt;/td&gt;
&lt;td&gt;INDEX FAST FULL SCAN 존재&lt;/td&gt;
&lt;td&gt;covering index scan 형태로 해석&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 풀스캔은 단순히 &amp;ldquo;인덱스를 썼으니 좋은 것&amp;rdquo;, 또는 &amp;ldquo;풀스캔이니 무조건 나쁜 것&amp;rdquo;으로 보면 안 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조건 일부만 찾는 스캔이 아니라 인덱스 전체를 읽는 방식이다&lt;/li&gt;
&lt;li&gt;정렬 이점이나 covering index가 있으면 유리할 수 있다&lt;/li&gt;
&lt;li&gt;반대로 테이블 재접근이 많아지면 비효율적일 수 있다&lt;/li&gt;
&lt;li&gt;Oracle은 INDEX FULL SCAN과 INDEX FAST FULL SCAN을 구분해서 봐야 하고,&lt;br /&gt;MySQL은 type=index, Using index 등을 조합해서 해석해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 볼 때는 &amp;ldquo;인덱스를 탔는가&amp;rdquo;만 보지 말고, 인덱스를 어떻게 탔는가까지 같이 봐야 한다.&lt;/p&gt;</description>
      <category>Database</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/258</guid>
      <comments>https://soobarkbar.tistory.com/258#entry258comment</comments>
      <pubDate>Sat, 18 Apr 2026 16:57:51 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] MacOS에서 카프카 매니저 설치</title>
      <link>https://soobarkbar.tistory.com/256</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. github에서 tar.gz 파일 다운로드 (&lt;a href=&quot;https://github.com/yahoo/CMAK&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/yahoo/CMAK&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2312&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MZKBf/btsGrW9BOEb/zHJGyiMHUMJgQNSevDX750/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MZKBf/btsGrW9BOEb/zHJGyiMHUMJgQNSevDX750/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MZKBf/btsGrW9BOEb/zHJGyiMHUMJgQNSevDX750/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMZKBf%2FbtsGrW9BOEb%2FzHJGyiMHUMJgQNSevDX750%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2312&quot; height=&quot;1114&quot; data-origin-width=&quot;2312&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 압축 해제 후 설치한 폴더 내에서 ./sbt clean dist 명령어 실행&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;Cannot use JVMCI compiler: No JVMCI compiler found&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;명령어 실행 시 위 에러가 발생한 경우, &lt;a href=&quot;https://github.com/yahoo/CMAK/issues/927&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. target/universal 경로에 생성된 .zip 파일을 원하는 위치에 압축 해제&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4. conf/application.conf 파일 수정 (로컬에 주키퍼를 띄웠기 때문에 아래와 같이 수정)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nqPxL/btsGpKXqiXR/uNZJ1NEpLhVKhSsL3cRH70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nqPxL/btsGpKXqiXR/uNZJ1NEpLhVKhSsL3cRH70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nqPxL/btsGpKXqiXR/uNZJ1NEpLhVKhSsL3cRH70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnqPxL%2FbtsGpKXqiXR%2FuNZJ1NEpLhVKhSsL3cRH70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;113&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5.  ./bin/cmak 명령어 실행 후, localhost:9000 접속&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3340&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AkO6m/btsGprqkxz5/5wl7mrfkvQC1Mwcp7hH4Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AkO6m/btsGprqkxz5/5wl7mrfkvQC1Mwcp7hH4Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AkO6m/btsGprqkxz5/5wl7mrfkvQC1Mwcp7hH4Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAkO6m%2FbtsGprqkxz5%2F5wl7mrfkvQC1Mwcp7hH4Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3340&quot; height=&quot;604&quot; data-origin-width=&quot;3340&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;!&amp;nbsp;@7jn3njc8f&amp;nbsp;-&amp;nbsp;Internal&amp;nbsp;server&amp;nbsp;error,&amp;nbsp;for&amp;nbsp;(GET)&amp;nbsp;[/assets/dataTables/javascripts/dataTables.bootstrap4.js]&lt;/li&gt;
&lt;li&gt;카프카 매니저 접속 시에 JS, CSS를 불러오지 못하는 위 에러가 발생하는 경우, &lt;a href=&quot;https://github.com/yahoo/CMAK/issues/844&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kafka</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/256</guid>
      <comments>https://soobarkbar.tistory.com/256#entry256comment</comments>
      <pubDate>Sat, 6 Apr 2024 14:54:32 +0900</pubDate>
    </item>
    <item>
      <title>기수 정렬 (Radix Sort)</title>
      <link>https://soobarkbar.tistory.com/156</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;기수 정렬 (Radix Sort) ?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기수 정렬은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;각 자리 위치를 하나씩 증가시키면서&lt;/b&gt;&lt;/span&gt; &lt;b&gt;숫자들을 정렬&lt;/b&gt;하는 방법이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정 정렬&lt;/b&gt;에 속하며, &lt;a href=&quot;https://soobarkbar.tistory.com/101&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카운팅 정렬&lt;/a&gt;과 마찬가지로 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;값의 비교연산 없이&lt;/span&gt; 정렬&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;기수 정렬의 단점은 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제자리 정렬이 아니기 때문에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;추가적인 메모리가 필요&lt;/b&gt;&lt;/span&gt;하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;동일한 길이를 가진&lt;/span&gt; 숫자나 문자열&lt;/b&gt;이여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정렬하는 숫자 자릿수 \(k\) 에 따라 \(O(kn)\) 의 시간 복잡도를 가진다.&lt;/li&gt;
&lt;li&gt;기수 정렬 과정은 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1의 자리 숫자를 비교해서 오름차순 또는 내림차순으로 정렬한 뒤, 큐 버킷에 삽입한 뒤 순서대로 뽑아 정렬한다.&lt;/li&gt;
&lt;li&gt;그 다음, 10의 자리 숫자를 비교해서 동일하게 수행한다.&lt;/li&gt;
&lt;li&gt;가장 큰 숫자의 자릿 수만큼 반복해서 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cF5fiB/btqCgSllX0Z/kECRdI1FQquPfMjBbkoZEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cF5fiB/btqCgSllX0Z/kECRdI1FQquPfMjBbkoZEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cF5fiB/btqCgSllX0Z/kECRdI1FQquPfMjBbkoZEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcF5fiB%2FbtqCgSllX0Z%2FkECRdI1FQquPfMjBbkoZEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1057&quot; height=&quot;419&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Code&lt;/h3&gt;
&lt;pre id=&quot;code_1696321370732&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void radixSort(int[] arr, int maxSize) {
    int jarisu = 1, size = 0;
    int len = arr.length;
    int[] output = new int[len];

    while (size &amp;lt; maxSize) {
        int[] bucket = new int[10];

        for (int i = 0; i &amp;lt; len; ++i) {
            ++bucket[arr[i] / jarisu % 10];
        }

        for (int i = 1; i &amp;lt; 10; ++i) {
            bucket[i] += bucket[i - 1];
        }

        for (int i = len - 1; i &amp;gt;= 0; --i) {
            int idx = arr[i] / jarisu % 10;

            output[bucket[idx] - 1] = arr[i];
            --bucket[idx];
        }

        for (int i = 0; i &amp;lt; len; ++i) {
            arr[i] = output[i];
        }

        jarisu *= 10;
        ++size;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;colorscripter-code&quot; style=&quot;color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; position: relative !important; overflow: auto;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;div class=&quot;colorscripter-code&quot; style=&quot;color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; position: relative !important; overflow: auto;&quot;&gt;
&lt;table class=&quot;colorscripter-code-table&quot; style=&quot;margin: 0; padding: 0; border: none; background-color: #272727; border-radius: 4px;&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding: 6px; border-right: 2px solid #4f4f4f;&quot;&gt;
&lt;div style=&quot;margin: 0; padding: 0; word-break: normal; text-align: right; color: #aaa; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; line-height: 130%;&quot;&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;1&lt;/div&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;2&lt;/div&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;3&lt;/div&gt;
&lt;div style=&quot;line-height: 130%;&quot;&gt;4&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;padding: 6px 0; text-align: left;&quot;&gt;
&lt;div style=&quot;margin: 0; padding: 0; color: #f0f0f0; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important; line-height: 130%;&quot;&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;기수&amp;nbsp;정렬&amp;nbsp;1&amp;nbsp;단계:&lt;/div&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;[82,&amp;nbsp;43,&amp;nbsp;3,&amp;nbsp;15,&amp;nbsp;35,&amp;nbsp;27,&amp;nbsp;7,&amp;nbsp;38,&amp;nbsp;18,&amp;nbsp;9]&lt;/div&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;기수&amp;nbsp;정렬&amp;nbsp;2&amp;nbsp;단계:&lt;/div&gt;
&lt;div style=&quot;padding: 0 6px; white-space: pre; line-height: 130%;&quot;&gt;[3,&amp;nbsp;7,&amp;nbsp;9,&amp;nbsp;15,&amp;nbsp;18,&amp;nbsp;27,&amp;nbsp;35,&amp;nbsp;38,&amp;nbsp;43,&amp;nbsp;82]&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;td style=&quot;vertical-align: bottom; padding: 0 2px 4px 0;&quot;&gt;&lt;a style=&quot;text-decoration: none; color: white;&quot; href=&quot;http://colorscripter.com/info#e&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-size: 9px; word-break: normal; background-color: #4f4f4f; color: white; border-radius: 10px; padding: 1px;&quot;&gt;cs&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://lktprogrammer.tistory.com/48&quot;&gt;https://lktprogrammer.tistory.com/48&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://namu.wiki/w/정렬%20알고리즘#s-2.2.2&quot;&gt;https://namu.wiki/w/정렬%20알고리즘#s-2.2.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Algorithm/Sort</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/156</guid>
      <comments>https://soobarkbar.tistory.com/156#entry156comment</comments>
      <pubDate>Tue, 3 Oct 2023 17:23:20 +0900</pubDate>
    </item>
    <item>
      <title>Forward / Reverse Proxy</title>
      <link>https://soobarkbar.tistory.com/248</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Proxy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;프록시&amp;nbsp;서버를&amp;nbsp;통해서&amp;nbsp;다른&amp;nbsp;네트워크&amp;nbsp;서비스에&amp;nbsp;간접적으로&amp;nbsp;접속할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;해주는&amp;nbsp;컴퓨터&amp;nbsp;시스템&amp;nbsp;또는&amp;nbsp;응용&amp;nbsp;프로그램&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 (Proxy) : 서버와 클라이언트 사이에서 대리로 통신을 수행하는 것&amp;nbsp;&lt;/li&gt;
&lt;li&gt;프록시 서버 (Proxy Server) : 중계 기능을 수행하는 서버&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;1. Forward Proxy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;인터넷에&amp;nbsp;접근하는&amp;nbsp;것이&amp;nbsp;아니라&amp;nbsp;프록시&amp;nbsp;서버가&amp;nbsp;요청을&amp;nbsp;받고&amp;nbsp;인터넷에&amp;nbsp;연결하여&amp;nbsp;결과를&amp;nbsp;클라이언트에&amp;nbsp;전달&amp;nbsp;(Forward)&amp;nbsp;해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;장점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;보안 : 프록시 서버에서 In / Out Bound 패킷에 대한 보안 정책 (Content Filtering 등) 을 적용할 수 있다.&amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;성능 : 프록시 서버 내부에 캐시를 유지하며 한 번 통신한 외부 서버의 이미지, 파일 등을 저장할 수 있다. 캐시에 데이터가 있으면 프록시 서버가 데이터를 바로 제공할 수 있어 빠른 통신을 지원한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkfkRx/btrA0XzX0Or/3MGQUCeH1LIiZaJ2G4KrZ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkfkRx/btrA0XzX0Or/3MGQUCeH1LIiZaJ2G4KrZ0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkfkRx/btrA0XzX0Or/3MGQUCeH1LIiZaJ2G4KrZ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkfkRx%2FbtrA0XzX0Or%2F3MGQUCeH1LIiZaJ2G4KrZ0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;283&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;2. Reverse Proxy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가&amp;nbsp;인터넷에&amp;nbsp;데이터를&amp;nbsp;요청하면&amp;nbsp;리버스&amp;nbsp;프록시가&amp;nbsp;이&amp;nbsp;요청을&amp;nbsp;받아&amp;nbsp;내부&amp;nbsp;서버에서&amp;nbsp;데이터를&amp;nbsp;받은&amp;nbsp;후&amp;nbsp;클라이언트에&amp;nbsp;전달한다.&lt;br /&gt;&lt;br /&gt;클라이언트는&amp;nbsp;내부&amp;nbsp;서버에&amp;nbsp;대한&amp;nbsp;정보를&amp;nbsp;알&amp;nbsp;필요&amp;nbsp;없이&amp;nbsp;리버스&amp;nbsp;프록싱만&amp;nbsp;요청하면&amp;nbsp;된다.&lt;br /&gt;&lt;br /&gt;내부&amp;nbsp;서버&amp;nbsp;(WAS)&amp;nbsp;에&amp;nbsp;직접&amp;nbsp;접근할&amp;nbsp;경우,&amp;nbsp;DB에&amp;nbsp;접근이&amp;nbsp;가능하기&amp;nbsp;때문에&amp;nbsp;중간에&amp;nbsp;리버스&amp;nbsp;프록시를&amp;nbsp;두고&amp;nbsp;클라이언트와&amp;nbsp;내부&amp;nbsp;서버&amp;nbsp;사이의&amp;nbsp;통신을&amp;nbsp;담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;장점&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;보안 : 모든 접속은 리버스 프록시 서버에게 들어오고 요청에 매핑되는 내부 서버에 요청 정보를 넘겨주기 때문에 외부 사용자는 실제 내부망에 있는 서버의 존재를 모른다.&lt;/li&gt;
&lt;li&gt;로드밸런싱 : 프록시 서버가 내부 서버의 정보를 알고 있기 때문에 로드 밸런싱을 통해 부하 여부에 따라 요청을 분배할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZDP0i/btrA33Gzlfe/79jDfwoNKKzYWL7qkR63Hk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZDP0i/btrA33Gzlfe/79jDfwoNKKzYWL7qkR63Hk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZDP0i/btrA33Gzlfe/79jDfwoNKKzYWL7qkR63Hk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZDP0i%2FbtrA33Gzlfe%2F79jDfwoNKKzYWL7qkR63Hk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;810&quot; height=&quot;287&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;차이점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 요청하는 End Point&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;Forward&amp;nbsp;Proxy&amp;nbsp;:&amp;nbsp;&lt;b&gt;실제 서버 도메인&lt;/b&gt;&lt;br /&gt;Reverse&amp;nbsp;Proxy&amp;nbsp;:&amp;nbsp;&lt;b&gt;프록시&amp;nbsp;서버&amp;nbsp;도메인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;감춰지는 대상&amp;nbsp;&amp;nbsp;&lt;br /&gt;Forward Proxy : &lt;b&gt;클라이언트 &lt;/b&gt;(요청 받는 서버는 포워드 프록시 서버를 통해서 요청을 받기 때문에 클라이언트의 정보를 알 수 없다.)&lt;br /&gt;Reverse Proxy : &lt;b&gt;서버&amp;nbsp;&lt;/b&gt;(클라이언트는 리버스 프록시 서버에게 요청하기 때문에 실제 서버의 정보를 알 수 없다.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://bcp0109.tistory.com/194&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://bcp0109.tistory.com/194&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/sjk5766/nginx-reverse-proxy-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-e11e18fcf843&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/sjk5766/nginx-reverse-proxy-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-e11e18fcf843&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Server</category>
      <author>기내식은수박바</author>
      <guid isPermaLink="true">https://soobarkbar.tistory.com/248</guid>
      <comments>https://soobarkbar.tistory.com/248#entry248comment</comments>
      <pubDate>Mon, 2 May 2022 12:19:13 +0900</pubDate>
    </item>
  </channel>
</rss>