<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>아는 개발자</title>
    <link>https://kwony91.tistory.com/</link>
    <description>새롭게 배우는 것을 담아내는 공간입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 20:43:35 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kwony</managingEditor>
    <image>
      <title>아는 개발자</title>
      <url>https://tistory1.daumcdn.net/tistory/1709853/attach/63791b5cb53a40019d674bf120aaebaf</url>
      <link>https://kwony91.tistory.com</link>
    </image>
    <item>
      <title>Virtual Thread in JAVA</title>
      <link>https://kwony91.tistory.com/entry/Virtual-Thread-in-JAVA</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;작업마다 Java Thread 를 생성하면 OS 단위에서 Thread 를 생성하기 때문에 비싸고 시간도 오래 걸린다. 그래서 Java 진영에선 기존에 생성하던 Java Thread 를 Platform Thread 라고 정의하고 Virtual Thread 라는 것을 새로 만들어서 Platform Thread 위에서 실행되는 구조를 잡았다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKD4Bf/dJMcacBQvy5/8NMN93OLda3mXNdRAipeg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKD4Bf/dJMcacBQvy5/8NMN93OLda3mXNdRAipeg0/img.png&quot; data-alt=&quot;출처: 우아한기술블로그 (https://techblog.woowahan.com/15398/)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKD4Bf/dJMcacBQvy5/8NMN93OLda3mXNdRAipeg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKD4Bf%2FdJMcacBQvy5%2F8NMN93OLda3mXNdRAipeg0%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;1280&quot; height=&quot;541&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 우아한기술블로그 (https://techblog.woowahan.com/15398/)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Virtual Thread 는 Heap 메모리에 있는 객체다. Virtual 스레드는 생성 작업이 JVM 위에서 이뤄지기 때문에 Platform Thread 에 비해서 생성 비용도 싸다. Stack 이나 IP 같은 저장공간도 JVM 내에서 컨텍스트 스위칭 되는 형태이기 때문에 불필요한 시스템 콜이 없어지게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java Thread 에 비해서 커널레벨까지 내려가서 실행하는 시스템 작업이 없어 효율적이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 Virtual Thread 의 강점은 Blocking I/O 를 Non blocking I/O 로 실행하게 된다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Java Thread 를 사용할 경우 I/O 작업이 오래 걸리면 끝까지 기다리게 된다. 반면에 Virtual Thread 에서는 JVM 이 Blocking 을 감지하면 실행하고 있던 Virtual Thread 를 중지하고 다른 Virtual Thread 를 실행한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 하나의 Java Thread 를 사용해도 두개의 작업을 동시에 실행하는 효과를 내게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768358479241&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TEST {
    private static final int TASKS = 1000;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        ioBoundTask();
        System.out.printf(&quot;Time %dms\n&quot;, System.currentTimeMillis() - start);
    }

    private static void ioBoundTask() {
        try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {

            for (int i = 0; i &amp;lt; TASKS; i++) {
                executorService.submit(() -&amp;gt; {
                    for (int j = 0; j &amp;lt; 100; j++) {
                        System.out.println(&quot;Executing Blocking Task: &quot; + Thread.currentThread());
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
            }
        }
    }

 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Virtual Thread 를 sleep 코드에서 blocking 이 발생하지 않아서 새로운 쓰레드 생성을 계속한다.&amp;nbsp; 그래서 1000개의 쓰레드를 생성하는데 1233ms 만 소요된다&lt;/p&gt;
&lt;pre id=&quot;code_1768358526007&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Executing Blocking Task: VirtualThread[#686]/runnable@ForkJoinPool-1-worker-5
Executing Blocking Task: VirtualThread[#482]/runnable@ForkJoinPool-1-worker-2
Executing Blocking Task: VirtualThread[#688]/runnable@ForkJoinPool-1-worker-2
Executing Blocking Task: VirtualThread[#635]/runnable@ForkJoinPool-1-worker-4
Executing Blocking Task: VirtualThread[#645]/runnable@ForkJoinPool-1-worker-8
Executing Blocking Task: VirtualThread[#937]/runnable@ForkJoinPool-1-worker-1
Executing Blocking Task: VirtualThread[#972]/runnable@ForkJoinPool-1-worker-6
Tasks took 1233ms to complete&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;newCachedThreadPool 을 사용하는 경우 Java Thread 를 생성해야 하기 때문에 시간이 더 소요되고 경우에 따라선 하드웨어 자원이 부족해 crash 가 발생하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1768358706001&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Executing Blocking Task: Thread[#286,pool-1-thread-259,5,main]
Executing Blocking Task: Thread[#1006,pool-1-thread-979,5,main]
Executing Blocking Task: Thread[#286,pool-1-thread-259,5,main]
Executing Blocking Task: Thread[#1006,pool-1-thread-979,5,main]
Tasks took 1760ms to complete&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/기술</category>
      <category>blocking</category>
      <category>java</category>
      <category>non-blocking</category>
      <category>virtual thread</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/413</guid>
      <comments>https://kwony91.tistory.com/entry/Virtual-Thread-in-JAVA#entry413comment</comments>
      <pubDate>Wed, 14 Jan 2026 11:46:00 +0900</pubDate>
    </item>
    <item>
      <title>java volatile</title>
      <link>https://kwony91.tistory.com/entry/java-volatile</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 클래스가 있다고 해보자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767851394966&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static class SharedClass {
    private int x = 0;
    private int y = 0;

    public  void increment() {
        x++;
        y++;
    }

    public  void checkForDataRace() {
        if (y &amp;gt; x) {
            System.out.println(&quot;y &amp;gt; x - Data Race is detected&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;x에 대한 증가가 y에 대한 증가보다 먼저 작성되었기 때문에 이론상으로 x&amp;gt;=y 인 상황만 발행해야하고 checkForDataRace 에서 if 문에 걸릴 일은 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 멀티 쓰레드 환경에서 실행해보면 y&amp;gt;x 인 케이스가 발생한다. 놀랍게도&lt;/p&gt;
&lt;pre id=&quot;code_1767851529419&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        SharedClass sharedClass = new SharedClass();
        Thread thread1 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; Integer.MAX_VALUE; i++) {
                sharedClass.increment();
            }
        });

        Thread thread2 = new Thread(() -&amp;gt; {
            for (int i = 0; i &amp;lt; Integer.MAX_VALUE; i++) {
                sharedClass.checkForDataRace();
            }

        });

        thread1.start();
        thread2.start();
    }
 //

y &amp;gt; x - Data Race is detected
y &amp;gt; x - Data Race is detected
y &amp;gt; x - Data Race is detected
y &amp;gt; x - Data Race is detected&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러와 CPU는 성능 최적화를 위해서 종종 연산의 순서를 바꾸기도 한다. 논리적인 정확성을 유지하면서 순서를 바꾸는데 멀티 쓰레드환경에서는 규칙이 지켜지지 않는 문제가 발생한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예제 코드에선 y++ 를 CPU가 먼저 실행하고 thread2 에서 이때 상태로 check를 걸었기 때문에 문제가 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책은 두가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째는 SharedClass 의 함수에다가 synchronized 를 걸어주는 방법이다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767851780766&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static class SharedClass {
    private int x = 0;
    private int y = 0;

    public synchronized void increment() {
        x++;
        y++;
    }

    public synchronized void checkForDataRace() {
        if (y &amp;gt; x) {
            System.out.println(&quot;y &amp;gt; x - Data Race is detected&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관적이긴 하지만 이 방법은 Core Grained Locking 이 되어서 성능이 덜어진다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째는 volatile 을 사용하는 방법이다. volatile 로 선언된 변수에 대해선 실행의 재정렬을 막아주고 변수의 변경을 쓰레드가 즉시 볼 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 클래스에서 volatile 을 붙여주면 y&amp;gt;x 인 케이스가 발생하지 않게 된다&lt;/p&gt;
&lt;pre id=&quot;code_1767853122406&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public static class SharedClass {
        private volatile int x = 0;
        private volatile int y = 0;

        public void increment() {
            x++;
            y++;
        }

        public void checkForDataRace() {
            if (y &amp;gt; x) {
                System.out.println(&quot;y &amp;gt; x - Data Race is detected&quot;);
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단 연산의 원자성을 보장하진 않는다. 멀티쓰레드 환경에서 여러 쓰레드가 동시에 연산을 실행하면 결과 값이 예상과 달라질 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Volatile 의 경우 쓰레드 하나만 값을 업데이트하고 나머지는 읽기 연산을 수행할 때 쓰는게 좋다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 Single Writer Multiple Reader 패턴이라고 Chat GPT 는 알려줬다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념 자체는 알겠는데 실전에서 쓰려면 기억이 안나서 실수가 잦는 키워드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 double, long 타입에 대한 reference operation 에서 원자성을 보장한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 자바에서는 int, boolean 같은 primitive 타입에선 특별한 동기화 작업을 걸지 않아도 원자성을 보장하는데 long, double 타입의 경우에는 예외였다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768134034239&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private int getter() {
	return intVal;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 volatile 을 사용하면 reference 오퍼레이션에 대해서도 원자성을 보장한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768134152673&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static class Metrics {
    private long count = 0;
    private volatile double average = 0.0; 

    public synchronized void addSample(long sample) {
		// 여기에선 volatile 로 선언 안해도 괜춘함
		// synchronized 에서 쓰레드가 하나씩만 접근하게 되므로
        double currentSum = average * count; 
        count++;
        average = (currentSum + sample) / count;
    }

    public double getAverage() {
        // average 변수를 volatile 로 선언해줘야
        // reference 가 atomic operation 임이 보장된다
		// double, long 타입은 volatile 을 반드시 붙여줘야함
        return average;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발</category>
      <category>java</category>
      <category>multi thread</category>
      <category>volatile</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/412</guid>
      <comments>https://kwony91.tistory.com/entry/java-volatile#entry412comment</comments>
      <pubDate>Thu, 8 Jan 2026 15:21:22 +0900</pubDate>
    </item>
    <item>
      <title>logi options+ 무한로딩 ㅡㅡ</title>
      <link>https://kwony91.tistory.com/entry/logi-options-%EB%AC%B4%ED%95%9C%EB%A1%9C%EB%94%A9-%E3%85%A1%E3%85%A1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 아침에 로지텍 마우스로 커스텀 설정해둔 버튼이 동작하지 않아서 logi options+ 를 실행해보니 동글뱅이가 멈출생각을 하지 않았다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-07 오후 2.18.35.png&quot; data-origin-width=&quot;3016&quot; data-origin-height=&quot;1700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X9Ufj/dJMcah4aKya/qLjaX729ipKTrOqpCZKDQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X9Ufj/dJMcah4aKya/qLjaX729ipKTrOqpCZKDQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X9Ufj/dJMcah4aKya/qLjaX729ipKTrOqpCZKDQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX9Ufj%2FdJMcah4aKya%2FqLjaX729ipKTrOqpCZKDQ0%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;3016&quot; height=&quot;1700&quot; data-filename=&quot;스크린샷 2026-01-07 오후 2.18.35.png&quot; data-origin-width=&quot;3016&quot; data-origin-height=&quot;1700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 잘못된 것 같아서 삭제하고 재실행해봐도 동일하고 chat GPT 가 시키는걸 했는데도 그대로였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.reddit.com/r/logitech/comments/1f7myq8/logi_options_stuck_at_loading_on_mac_need_help/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;레딧&lt;/a&gt;에 들어가보니 나와 같은 장애를 겪고 있는 사람들이 올린 댓글이 다수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람들 추측으론 동시에 많은 사람들이 안되는로 보아 백엔드쪽이 맛이 간것 같고 맥 시스템 날짜를 변경하면 된다는 글을 보니까 인증서 문제로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매해 1월마다 많은 서비스들이 인증서 교체작업을 하는데 logi options+ 앱도 마찬가지인듯&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-01-07 오후 2.22.56.png&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;984&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQIjOj/dJMcadHq4tR/JQkFIAkI9h6N2N1x18Z7p1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQIjOj/dJMcadHq4tR/JQkFIAkI9h6N2N1x18Z7p1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQIjOj/dJMcadHq4tR/JQkFIAkI9h6N2N1x18Z7p1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQIjOj%2FdJMcadHq4tR%2FJQkFIAkI9h6N2N1x18Z7p1%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;1486&quot; height=&quot;984&quot; data-filename=&quot;스크린샷 2026-01-07 오후 2.22.56.png&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;984&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.clien.net/service/board/cm_mac/18722033&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;클리앙&lt;/a&gt;에도 뜬걸로 보아 한국 사용자들도 같은 문제를 겪고 있는듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 하드웨어 기기를 인터넷이 되는 환경에서만 사용할 수 있다는게 말이 되는건가 싶다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;p data-ke-size=&quot;size16&quot;&gt;로지텍도 참 실망스럽다. 최소 7시간째 장애를 겪고 있는데 아직도 대응을 안하고 있다니. 그것도 제품을 구매한 사용자들이 쓰는 기능인데 말이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레딧 댓글처럼 담당자가 휴가가서 발생하는 이슈라면 부담당자라도 얼른 대처해야하는거 아닌가.&amp;nbsp;&lt;/p&gt;</description>
      <category>Logi Options+</category>
      <category>로지텍</category>
      <category>무한로딩</category>
      <category>버그</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/411</guid>
      <comments>https://kwony91.tistory.com/entry/logi-options-%EB%AC%B4%ED%95%9C%EB%A1%9C%EB%94%A9-%E3%85%A1%E3%85%A1#entry411comment</comments>
      <pubDate>Wed, 7 Jan 2026 14:34:57 +0900</pubDate>
    </item>
    <item>
      <title>Python GIL</title>
      <link>https://kwony91.tistory.com/entry/Python-GIL</link>
      <description>&lt;h1&gt;CPython&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이선 스크립트 엔진중에 가장 메인. 공식 배포판에서도 이걸 쓴다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C 언어로 작성되었고 공식 배포판이다&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;터미널에서 python 명령어를 실행하면 CPython 엔진으로 파이선 파일을 실행하게 된다&lt;/p&gt;
&lt;h1&gt;GIL&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 인터프리터 락 (Global Interpreter Lock). 쓰레드간에 동시성을 제어하기 위해 사용되는 메커니즘. GIL 은 동시에 하나의 쓰레드만 바이트코드를 실행할 수 있도록 허용하며 메모리 관리와 객체 모델의 일관성을 보장한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 파이썬은 하나의 스레드만 바이트 코드를 실행하게 했는지 궁금할 수 있는데 자세히 들어가면 복잡하니 C 언어 생태계에서 단일스레드 성능을 높이기 위함이였다 정도로 짧게 이해하고가자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락의 획득과 해제: CPython 인터프리터가 파이선 코드 실행시 GIL 을 획득하고 다른 쓰레드는 GIL 을 실행할 때까지 대기. 인터프리터는 주기적으로 GIL 을 해제하여 다른 쓰레드가 실행될 수 있도록 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 전환: GIL 은 바이트코드 실행 주기 또는 I/O 작업이 발생할 때마다 다른 쓰레드로 전환된다. 단일 코어에서 다중 쓰레드가 실행되는것처럼 보이지만 실제로는 하나의 쓰레드만 실행된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 관리 간소화: 쓰레드가 동시에 메모리 객체에 접근해도 GIL 은 일관성을 유지할 수 있게 해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안전성: GIL 덕분에 파이썬 내부 데이터 구조가 쓰레드 안정성을 유지한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티코어 CPU 활용 제한: 파이썬 쓰레드는 동시에 하나의 쓰레드만 실행되므로 병렬처리가 제한된다. CPU 바운드 작업에서 성능 저하를 초래한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰레드 성능 저하: I/O 바운드 작업에서는 큰 문제가 안되지만 CPU 바운드 작업에선 멀티쓰레딩의 이점을 누리지 못함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음코드가 GIL 병목의 대표적인 예제 코드다&lt;/p&gt;
&lt;pre id=&quot;code_1766911771513&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import threading

def work():
    x = 0
    for _ in range(10**8):
        x += 1

threads = [threading.Thread(target=work) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;x+=1 단순한 코드를 돌리는데 멀티 스레드 환경이지만 한번에 하나의 스레드만 실행이 가능하다. 실질적으로 하나의 스레드로 돌리는 것과 비슷한 속도가 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 바운드 작업이 높은 서비스의 경우에는 c++ 을 이용한 내장 라이브러리를 적극 활용해야 GIL 병목에 걸리지 않는다 -&amp;gt; 암호 해체 관련된 작업의 경우 락에서 보내는 시간이 많아질수도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 I/O 관련 작업이 많다면 GIL에서 걸리는 시간이 많지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;Django&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장고도 CPython 으로 실행하면 GIL 때문에 쓰레드 동시성 문제를 겪게됨. 그래서 wsgi 를 이용해 worker 프로세스를 여러개 둬서 멀티 프로세싱을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 쓰레드도 두긴 하는데 이건 I/O 바운드 작업을 최적화 하기 위함임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <category>cpython</category>
      <category>Gil</category>
      <category>Python</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/410</guid>
      <comments>https://kwony91.tistory.com/entry/Python-GIL#entry410comment</comments>
      <pubDate>Sun, 28 Dec 2025 17:53:23 +0900</pubDate>
    </item>
    <item>
      <title>넷플릭스가 클릭하우스를 최적화시킨 방법</title>
      <link>https://kwony91.tistory.com/entry/%EB%84%B7%ED%94%8C%EB%A6%AD%EC%8A%A4%EA%B0%80-%ED%81%B4%EB%A6%AD%ED%95%98%EC%9A%B0%EC%8A%A4%EB%A5%BC-%EC%B5%9C%EC%A0%81%ED%99%94%EC%8B%9C%ED%82%A8-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1763461953491&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;How Netflix optimized its petabyte-scale logging system with ClickHouse&quot; data-og-description=&quot;&amp;ldquo;To make our logging system work, we had to make a lot of choices. The key is how you simplify things in order to do the least amount of work.&amp;rdquo; Daniel Muino, Software Engineer&quot; data-og-host=&quot;clickhouse.com&quot; data-og-source-url=&quot;https://clickhouse.com/blog/netflix-petabyte-scale-logging&quot; data-og-url=&quot;https://clickhouse.com/blog/netflix-petabyte-scale-logging&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mZgKj/hyZNYBN3D6/kZCB33WYceA7tiukc5K3lK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/zdFpq/hyZNzujcq7/20MtXrtMHSBG3qxaXONKk0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://clickhouse.com/blog/netflix-petabyte-scale-logging&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://clickhouse.com/blog/netflix-petabyte-scale-logging&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mZgKj/hyZNYBN3D6/kZCB33WYceA7tiukc5K3lK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/zdFpq/hyZNzujcq7/20MtXrtMHSBG3qxaXONKk0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How Netflix optimized its petabyte-scale logging system with ClickHouse&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;To make our logging system work, we had to make a lot of choices. The key is how you simplify things in order to do the least amount of work.&amp;rdquo; Daniel Muino, Software Engineer&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;clickhouse.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클릭하우스 공식 블로그를 번역 + 개인적인 의견을 첨가한 포스팅&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Safe to say, this is a scale that would overwhelm most logging platforms. Making that kind of interactivity possible&amp;mdash;logs that are searchable within seconds, queries that feel instant&amp;mdash;took not just the right database (ClickHouse!) but a series of carefully engineered optimizations.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clickhouse 는 시스템의 중심에 있으며 hot tier 로 동작한다. 속도가 생명인 최신 로그를 저장하고 빠른 쿼리와 상호작용하는 디버깅을 제공한다. 클릭하우스 덕분에 최신 데이터를 빨리 보내줄 수 있다고 관계자는 말한다 (Daniel)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClickHouse sits at the heart of the system as the hot tier. It stores recent logs where speed is critical, powering fast queries and interactive debugging. &amp;ldquo;Thanks to ClickHouse, we&amp;rsquo;re able to serve this data very fresh,&amp;rdquo; Daniel says. &amp;ldquo;All the buffering we do along the way doesn&amp;rsquo;t really affect us much.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn2qYZ/dJMcagDTIY2/GKns6I8r94xDvSiX2iHxd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn2qYZ/dJMcagDTIY2/GKns6I8r94xDvSiX2iHxd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn2qYZ/dJMcagDTIY2/GKns6I8r94xDvSiX2iHxd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn2qYZ%2FdJMcagDTIY2%2FGKns6I8r94xDvSiX2iHxd0%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;1564&quot; height=&quot;824&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템이 거의 동시에 일어나고 있는것처럼 느껴진다. 로그는 발생한지 20초만에 조회가 가능하고 종종 2초 latency 만에 라이브 로그를 전달받기도 한다고함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;The result is a system that feels almost instantaneous. Logs are usually searchable within 20 seconds of being generated, far faster than Netflix&amp;rsquo;s 5-minute SLA. In some cases, engineers can even stream live logs with two-second latency. They can click into events, expand JSON payloads, group millions of messages by fingerprint hash, or drill into surrounding logs, all without waiting for queries to churn.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 여기까지가 넷플릭스 성능에 대한 자랑이었고 다음부터는 최적화 기법에 대해서 설명하고 있다.&lt;/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;Ingestion - Fingerprinting&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 사용성을 높이기 위해서 우선 비슷한 메시지끼리 묶어주는 처리를 함. Fingerpinting 이라고 부르는 기법인데 요 방식이 수백만개의 비슷한 요소들을 하나의 패턴으로 묶어서 노이즈를 줄이게 해줌. 이게 없으면 로그에서 조회하는 작업은 매우 압도적이라고 할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 머신러닝 모델을 이용해서 로그를 그룹화하는 시도를 했음. 이론적으로는 동작했으나 실전에서는 모델을 돌리기에는 컴퓨팅 리소스가 부족했음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고 나서 정규표현식 패턴 매칭 방식을 사용했고 generic token 을 swap out 했음. 동작은 했으나 정규표현식은 초당 천만데이터는 따라잡지 못했음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀은 Fingerprinting 을 다시 조정했고 자바로 구현된 tokenizer JFlex Lexer 라는 것을 만들어냈음. 런타임에 복잡한 정규표현식을 평가하는거 대신 이 시스템은 패턴을 효율적인 코드로 컴파일했음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷플릭스처럼 돈많은 회사들도 비용절감을 위해 상당히 로우레벨로 까지 내려가고있다는 점. 이래서 회사에서 알고리즘을 면접때 보는 것일지도 모르겠다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hub - Serialization&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 fingerprint 했으면 클릭하우스에 작성돼야함. 다른 병목이 있는데 바로 직렬화임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 JDBC 배치 inserts 방식에 의존했음. 간단하고 친숙했지면 효과적이지 않았다. 모든 준비된 구문들이 클라이언트가 스키마와 직렬화에 대해서 협상해야했고 오버헤드가 있었음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상화 스택을 포기하고 클릭하우스 low level 자바 클라이언트에게 노출된 RowBinary 포맷을 사용하기로함. 칼럼 단위로 수동으로 직렬화를 시켜줌. 성능이 매우 증가했으나 충분하지는 않았다고함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RowBinary 포맷을 사용하면 불필요한 인코딩 데이터들이 포함되지 않기 때문에 데이터 양도 적고 속도도 빨라지게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;So the team dropped down a level in the abstraction stack, using the&amp;nbsp;&lt;a href=&quot;https://clickhouse.com/docs/interfaces/formats/RowBinary&quot;&gt;RowBinary&lt;/a&gt;&amp;nbsp;format exposed by ClickHouse&amp;rsquo;s low-level Java client. This meant manually serializing data column by column&amp;mdash;writing map lengths, encoding&amp;nbsp;&lt;a href=&quot;https://clickhouse.com/docs/sql-reference/data-types/datetime64&quot;&gt;DateTime64&lt;/a&gt;&amp;nbsp;as nanoseconds since the epoch, and handling other quirks. It gave them a &amp;ldquo;huge performance boost,&amp;rdquo; Daniel says, but it still wasn&amp;rsquo;t enough.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;input 포맷별 벤치마크에 대한 블로그 글을 읽고 혁신적인 방식을 고안함. native protocol 이 RowBinary 보다 더 나은 성능을 보였으나 Java client 는 지원하지 않았고 Go client 만 지원했음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;1138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7F54x/dJMcacnXEAR/cU5F911gH4gGVNBu3yLFWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7F54x/dJMcacnXEAR/cU5F911gH4gGVNBu3yLFWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7F54x/dJMcacnXEAR/cU5F911gH4gGVNBu3yLFWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7F54x%2FdJMcacnXEAR%2FcU5F911gH4gGVNBu3yLFWK%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;1610&quot; height=&quot;1138&quot; data-origin-width=&quot;1610&quot; data-origin-height=&quot;1138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Beyond the ClickHouse command-line client, the native interface is currently supported only by the C++ and Go clients, with Rust support planned. All main language clients, except C++, support the HTTP interface.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬화까지 내려간거면 정말 낮은 시스템 레벨에서 성능을 올리기 위해 고민해본거라 할 수 있을것 같다. 일반적인 서비스에서는 고민하기 힘든 일임. 그래서 넷플릭스 개발자들이 돈을 많이 받는 이유일지도 모르겠다 ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 일을 하면서 이정도까지 깊이 있게 파본적은 없는거 같은데. 아무튼 대단하고 동기부여가 된다&lt;/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;Queries - Custom Tag&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingestion, Serialization 은 쓰기가 오래 걸리는 작업이라면 세번째 병목은 읽는 부분이었음. 엔지니어들은 tag 에 강하게 의존하고 있는데 요 태그가 마이크로서비스, 요청 id, 그외 다른 속성들을 걸러주는 역할을 함. 유용할 수록 태그는 시스템에서 가장 머리아픈 구석이 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그는 원래 &lt;a href=&quot;https://clickhouse.com/docs/sql-reference/data-types/map&quot;&gt;Map(String, String)&lt;/a&gt; 타입으로 저장되어 있었음. 내부적으로 클릭하우스는 키와 값으로 구성된 두개의 병렬 배열 로 표현함. Lookup 할 때마다 특정 키를 찾기 위해선 배열을 linear 하게 스캔해야함. 넷플릭스 환경에선 시간당 25,000 개의 unique key 와 천만개의 unique values 가 쿼리 성능 저하시키는 요인임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클릭하우스 창업자와 얘기한 결과 LowCardinality types 라는 걸 추천 받았고 키 값에는 효과가 있었으나 값은 너무 다양했기 떄문에 완전히 해결하진 못함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책은 의외로 간단했는데 map 을 샤드하는 방식이다. 태그 키들을 31개의 작은 맵으로 쪼개고 나니 쿼리가 모든 키 값을 확인할 필요 없이 바로 다음 샤드를 확인하게 됐따.&lt;/p&gt;</description>
      <category>개발/데이터베이스</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/409</guid>
      <comments>https://kwony91.tistory.com/entry/%EB%84%B7%ED%94%8C%EB%A6%AD%EC%8A%A4%EA%B0%80-%ED%81%B4%EB%A6%AD%ED%95%98%EC%9A%B0%EC%8A%A4%EB%A5%BC-%EC%B5%9C%EC%A0%81%ED%99%94%EC%8B%9C%ED%82%A8-%EB%B0%A9%EB%B2%95#entry409comment</comments>
      <pubDate>Tue, 18 Nov 2025 19:38:56 +0900</pubDate>
    </item>
    <item>
      <title>Clickhouse Materialized views</title>
      <link>https://kwony91.tistory.com/entry/Clickhouse-Materialized-views</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;테이블에 데이터가 추가될 때마다 특정한 쿼리를 실행하고 결과를 target 테이블에 전송하는 테이블이다. 데이터가 추가될수록 결과가 타겟 테이블에 전송되고 중간 result 는 업데이트되고 병합된다. 병합된 결과는 기존 데이터에 대한 쿼리와 동등하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;target 테이블에 미리 계산된 결과를 전달하고 싶을때 사용하는 테이블이라고 보면됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 up_down_votes_per_day_mv 테이블은 up_down_votes_per_day 테이블에 일별 데이터를 모아서 전송해주는 역할을 하는 materialized 테이블이다&lt;/p&gt;
&lt;pre id=&quot;code_1762504828169&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE MATERIALIZED VIEW up_down_votes_per_day_mv TO up_down_votes_per_day AS
SELECT toStartOfDay(CreationDate)::Date AS Day,
       countIf(VoteTypeId = 2) AS UpVotes,
       countIf(VoteTypeId = 3) AS DownVotes
FROM votes
GROUP BY Day&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리얼타입에 데이터를 유입시킬 수 있으며 점진적으로 업데이트되는 인덱스와 비슷한 역할을 한다고 볼 수 있다. 다른 디비의 경우 materialized view 가 특정 상태의 snapshot 을 저장하고 새로고침을 해줘야 하는 것에 비해서 요건 클릭하우스 고유한 특징이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 애플리케이션 돌릴 필요 없이 테이블을 이용해서 데이터 전처리를 해줄 수 있는 테이블이라고 볼 수 있다. 여기에 Aggregate Engine 같은걸 이용하면 효율을 더 높일수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;1244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIEt1L/dJMcahCJvPw/sKOV2sHLwlPJGHHJ2SbEi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIEt1L/dJMcahCJvPw/sKOV2sHLwlPJGHHJ2SbEi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIEt1L/dJMcahCJvPw/sKOV2sHLwlPJGHHJ2SbEi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIEt1L%2FdJMcahCJvPw%2FsKOV2sHLwlPJGHHJ2SbEi1%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;1124&quot; height=&quot;1244&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;1244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/데이터베이스</category>
      <category>clickhouse</category>
      <category>MATERIALIZED</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/408</guid>
      <comments>https://kwony91.tistory.com/entry/Clickhouse-Materialized-views#entry408comment</comments>
      <pubDate>Fri, 7 Nov 2025 22:41:10 +0900</pubDate>
    </item>
    <item>
      <title>Clickhouse Projections</title>
      <link>https://kwony91.tistory.com/entry/Clickhouse-Projections</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Projections&lt;/p&gt;
&lt;figure id=&quot;og_1762504571630&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Projections | ClickHouse Docs&quot; data-og-description=&quot;Page describing what projections are, how they can be used to improve query performance, and how they differ from materialized views.&quot; data-og-host=&quot;clickhouse.com&quot; data-og-source-url=&quot;https://clickhouse.com/docs/data-modeling/projections&quot; data-og-url=&quot;https://clickhouse.com/docs/data-modeling/projections&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Hoi2W/hyZM51LEGq/8GFRHPkPOtmy1DG5KVGEPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/lJWNM/hyZMCkMsP0/qAHbEkqzNTakpiKS9Z2I8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://clickhouse.com/docs/data-modeling/projections&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://clickhouse.com/docs/data-modeling/projections&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Hoi2W/hyZM51LEGq/8GFRHPkPOtmy1DG5KVGEPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/lJWNM/hyZMCkMsP0/qAHbEkqzNTakpiKS9Z2I8k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Projections | ClickHouse Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Page describing what projections are, how they can be used to improve query performance, and how they differ from materialized views.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;clickhouse.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Projection 은 클릭하우스에서 속도를 올리기 위한 기술중 하나다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실질적으로 Projection 은 기존 테이블에 숨겨진 추가 테이블이라고 볼 수 있다. projection 은 기존 테이블과 행의 순서도 다르고 primary index 도 다르다. 데이터가 들어올 때마다 자동으로 그리고 점진적으로 aggregate values 들을 구해준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Practically, a Projection can be thought of as an additional, hidden table to the original table. The projection can have a different row order, and therefore a different primary index, to that of the original table and it can automatically and incrementally pre-compute aggregate values&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Projection 은 여러개의 행도 가지고 있고 삽입 시점에 미리 Aggregation 을 해준다는 점에서 Materialized Views 랑 비슷하다. 단 Materialized View 랑은 달리 기존 테이블 데이터를 주기적으로 싱크를 맞추고 있다는 점에서 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Original Table 에 대한 쿼리 요청이 들어오면 클릭하우스에서는 동일한 결과를 낼 수 있는 Projection 테이블을 선택하는데 로직은 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;1110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIJPPc/dJMcakzssr3/IOc7WFVNCZaLYx9z8xkGqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIJPPc/dJMcakzssr3/IOc7WFVNCZaLYx9z8xkGqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIJPPc/dJMcakzssr3/IOc7WFVNCZaLYx9z8xkGqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIJPPc%2FdJMcakzssr3%2FIOc7WFVNCZaLYx9z8xkGqk%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;1234&quot; height=&quot;1110&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;1110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;25.5 버전부터 projection 에 두가지 방식중 하나를 선택할 수 있는데 기존처럼 모든 칼럼을 저장할지 아니면 sortingkey +&lt;span style=&quot;background-color: #000000; color: #eb5757;&quot; data-token-index=&quot;1&quot;&gt;_part_offset&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 같이 이용하는 방법이 있다. 후자의 경우 기존 테이블에서 데이터를 읽어오는 방식이기 때문에 용량을 차지하지는 않지만 I/O 리소스가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;p data-ke-size=&quot;size16&quot;&gt;PK 가 아닌 칼럼에 대해서 필터링을 걸고 싶을때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762504649646&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
  tip_amount,
  trip_id,
  dateDiff('minutes', pickup_datetime, dropoff_datetime) AS trip_duration_min
FROM nyc_taxi.trips WHERE tip_amount &amp;gt; 200 AND trip_duration_min &amp;gt; 0
ORDER BY tip_amount, trip_id ASC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 쿼리에서는 trip_duration_min 칼럼에 대해서 조건문을 걸어주고 있는데 이게 PK 가 아니기 때문에 성능이 좋지 않다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주쓰이는 쿼리라면 여기에 projection 을 걸어줄 수 있다. 새로운 테이블을 만든건 아니지만 dateDiff 함수를 사용할대 아래 프로젝션 로직을 사용하게 된다&lt;/p&gt;
&lt;pre id=&quot;code_1762504674484&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE nyc_taxi.trips_with_projection
ADD PROJECTION prj_tip_amount
(
    SELECT *
    ORDER BY tip_amount, dateDiff('minutes', pickup_datetime, dropoff_datetime)
)

ALTER TABLE nyc.trips_with_projection MATERIALIZE PROJECTION prj_tip_amount&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Projection 을 사용하고 나면 query_log 에 projection 을 사용했다고 나온다. 반복되는 쿼리 요청이 있다면 projection 사용해서 간편하게 처리해줄 수 있을듯하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762504726103&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   ┌─query─────────────────────────────────────────────────────────────────────────┬─projections──────────────────────┐
   │ SELECT                                                                       ↴│ ['default.trips.prj_tip_amount'] │
   │↳  tip_amount,                                                                ↴│                                  │
   │↳  trip_id,                                                                   ↴│                                  │
   │↳  dateDiff('minutes', pickup_datetime, dropoff_datetime) AS trip_duration_min↴│                                  │
   │↳FROM trips WHERE tip_amount &amp;gt; 200 AND trip_duration_min &amp;gt; 0                   │                                  │
   └───────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발/데이터베이스</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/407</guid>
      <comments>https://kwony91.tistory.com/entry/Clickhouse-Projections#entry407comment</comments>
      <pubDate>Fri, 7 Nov 2025 20:39:15 +0900</pubDate>
    </item>
    <item>
      <title>Clickhouse Skip Index</title>
      <link>https://kwony91.tistory.com/entry/Clickhouse-Skip-Index</link>
      <description>&lt;figure id=&quot;og_1762500481559&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Understanding ClickHouse Data Skipping Indexes | ClickHouse Docs&quot; data-og-description=&quot;Skip indexes enable ClickHouse to skip reading significant chunks of data that are guaranteed to have no matching values.&quot; data-og-host=&quot;clickhouse.com&quot; data-og-source-url=&quot;https://clickhouse.com/docs/optimize/skipping-indexes&quot; data-og-url=&quot;https://clickhouse.com/docs/optimize/skipping-indexes&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/JfdfC/hyZM3Qq0in/OlixVx5oc5X7wmdi10kAKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/7BdTm/hyZMz9ogBS/nAUNWEDbNAkRbVhjrOV5B1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://clickhouse.com/docs/optimize/skipping-indexes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://clickhouse.com/docs/optimize/skipping-indexes&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/JfdfC/hyZM3Qq0in/OlixVx5oc5X7wmdi10kAKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/7BdTm/hyZMz9ogBS/nAUNWEDbNAkRbVhjrOV5B1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Understanding ClickHouse Data Skipping Indexes | ClickHouse Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Skip indexes enable ClickHouse to skip reading significant chunks of data that are guaranteed to have no matching values.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;clickhouse.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 방식에서는 B-tree 구조로 데이터베이스가 행을 log(n) 시간에 찾을 수 있게 했다. 하지만 클릭하우스의 경우 개별 row 가 존재하는게 아니기 때문에 작동하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 클릭하우스에선 skip 인덱스라는 걸 사용해서 불필요한 데이터 chunk 를 읽지 않아도 되는 방식을 제공한다. 각 구조마다 Skip 인덱스 표시가 되어 있어서 청크 단위로 읽지 않아도 되는 데이터를 표시를 해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Instead, ClickHouse provides a different type of index, which in specific circumstances can significantly improve query speed. These structures are labeled &quot;Skip&quot; indexes because they enable ClickHouse to skip reading significant chunks of data that are guaranteed to have no matching values.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 data skipping index 를 만들면 data part 경로에 파일이 두개가 추가된다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;skp_idx_{index_name}.idx, which contains the ordered expression values&lt;/li&gt;
&lt;li&gt;skp_idx_{index_name}.mrk2, which contains the corresponding offsets into the associated data column files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;where 조건문에 index 가 걸린 칼럼에 대해서 접근하면 clickhouse 에서는 index 파일 데이터를 이용해서 처리해야하는 관련된 블록을 결정하고 걸러야 할 건 bypass 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;block 의 크기는 granularity 로 관리를 하는데 모든 색인된 블록은 별도의 granularity 를 갖고 있다. 볼지 말지 결정하는 chunk 의 크기이며 granularity 가 클수록 필터링하는 단위가 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nZcUI/dJMcaeeWheu/DepN1fBmTzXWwnAnzRlVpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nZcUI/dJMcaeeWheu/DepN1fBmTzXWwnAnzRlVpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nZcUI/dJMcaeeWheu/DepN1fBmTzXWwnAnzRlVpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnZcUI%2FdJMcaeeWheu%2FDepN1fBmTzXWwnAnzRlVpk%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;1176&quot; height=&quot;1034&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;1034&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;pre id=&quot;code_1762500591030&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE skip_table
(
  my_key UInt64,
  my_value UInt64
)
ENGINE MergeTree primary key my_key
SETTINGS index_granularity=8192;

INSERT INTO skip_table SELECT number, intDiv(number,4096) FROM numbers(100000000);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 테이블에서 my_value 칼럼에 대해서 데이터를 조회하려고 하면 100m 개의 데이터를 모두 스캔해야하는 문제가 있다. 그래서 0.079 초가 걸림&lt;/p&gt;
&lt;pre id=&quot;code_1762500609799&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM skip_table WHERE my_value IN (125, 700)

┌─my_key─┬─my_value─┐
│ 512000 │      125 │
│ 512001 │      125 │
│    ... |      ... |
└────────┴──────────┘

8192 rows in set. Elapsed: 0.079 sec. Processed 100.00 million rows, 800.10 MB (1.26 billion rows/s., 10.10 GB/s.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 인덱스를 추가하고 다시 스캐닝 해보면 속도가 0.02s 단축된 것을 확인할 수 있다. 데이터가 얼마 없어서 그리 차이는 나지 않은듯&lt;/p&gt;
&lt;pre id=&quot;code_1762500629360&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE skip_table ADD INDEX vix my_value TYPE set(100) GRANULARITY 2;

SELECT * FROM skip_table WHERE my_value IN (125, 700)

┌─my_key─┬─my_value─┐
│ 512000 │      125 │
│ 512001 │      125 │
│    ... |      ... |
└────────┴──────────┘

8192 rows in set. Elapsed: 0.051 sec. Processed 32.77 thousand rows, 360.45 KB (643.75 thousand rows/s., 7.08 MB/s.)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;skip index 종류도 여러가지가 있어서 요구사항에 맞게 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VT0VQ/dJMcaa4F8SQ/jVsmDItaLU5Ev6Lnt4OZqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VT0VQ/dJMcaa4F8SQ/jVsmDItaLU5Ev6Lnt4OZqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VT0VQ/dJMcaa4F8SQ/jVsmDItaLU5Ev6Lnt4OZqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVT0VQ%2FdJMcaa4F8SQ%2FjVsmDItaLU5Ev6Lnt4OZqk%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;1182&quot; height=&quot;596&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;minmax 의 경우 차지하는 용량도 많지 않고 가장 간단하게 구현할 수 있는 방식이라고 한다. 최소랑 최대만 확인해주면 되기 때문에 부담이 적은듯하다. 블룸필터 같은거 쓰려면 용량도 그렇고 자료구조도 복잡해질 것 같긴 하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시에서는 vix 타입의 skip index 를 사용했는데 문서상에는 vix 에 대한 설명은 나와있지 않아서 당황스럽다;&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;Best Practice&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL, PG 처럼 row 기반 DB 에서 index 는 읽어야 하는 값을 찾아주는 용도로 사용된다. 반면에 Clickhouse 는 granule 단위로 스캔하지 않아도 되는 범위를 찾는데 효과가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timestamp 처럼 row 별로 저장되는 값이 다른 경우 SKIP 할 수 있는 데이터가 많으나 gender 처럼 비슷한 데이터가 전반에 분포되어 있는 경우 SKIP 하는 granule 이 별로 없다 (block 단위로 male, female 값이 모두 존재할 것이기 때문에)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 데이터가 고르게 분포되어 있는 경우에는 index의 효과를 보기 어렵다고 볼 수 없다. 아래 그림처럼 대부분의 데이터에 1001 값이 존재한다면 인덱스를 걸어도 모든 granule 을 읽게 되기 때문에 효과를 보기 어렵다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVJ2hs/dJMcaiVWCbu/0uTOZ37CtUCxVTKf8aJdL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVJ2hs/dJMcaiVWCbu/0uTOZ37CtUCxVTKf8aJdL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVJ2hs/dJMcaiVWCbu/0uTOZ37CtUCxVTKf8aJdL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVJ2hs%2FdJMcaiVWCbu%2F0uTOZ37CtUCxVTKf8aJdL0%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;1188&quot; height=&quot;848&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clickhouse 속도를 올리기 위해 index 를 사용하는 유즈케이스의 경우 대부분은 잘못사용하는 경우라고 한다. 그래서 대안요소들을 먼저 조사하고 pk 를 바꿔보고 projection 이나 materialized view 까지 써본 다음에 최종적으로 사용하는게 좋다.&lt;/p&gt;</description>
      <category>개발/데이터베이스</category>
      <category>clickhouse</category>
      <category>Index</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/406</guid>
      <comments>https://kwony91.tistory.com/entry/Clickhouse-Skip-Index#entry406comment</comments>
      <pubDate>Fri, 7 Nov 2025 20:33:43 +0900</pubDate>
    </item>
    <item>
      <title>Clickhouse JSON 칼럼</title>
      <link>https://kwony91.tistory.com/entry/Clickhouse-JSON-%EC%B9%BC%EB%9F%BC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1761551425143&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;How we built a new powerful JSON data type for ClickHouse&quot; data-og-description=&quot;We&amp;rsquo;re excited to introduce our new and significantly enhanced JSON data type, purpose-built to deliver high-performance handling of JSON data. Our core engineer, Pavel Kruglov, dives into how we built this feature on top of ClickHouse's columnar storage.&quot; data-og-host=&quot;clickhouse.com&quot; data-og-source-url=&quot;https://clickhouse.com/blog/a-new-powerful-json-data-type-for-clickhouse&quot; data-og-url=&quot;https://clickhouse.com/blog/a-new-powerful-json-data-type-for-clickhouse&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lTrcb/hyZMcsTBPW/rEXoGIuso4UZtkQZUeRHvK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/98CqR/hyZMmoI6r1/fwjSTE5hNiqKRsyo511dX0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bqMgUC/hyZMHFdCO8/8jt9E6EEGzRVtAYF78tjLK/img.png?width=3890&amp;amp;height=996&amp;amp;face=0_0_3890_996&quot;&gt;&lt;a href=&quot;https://clickhouse.com/blog/a-new-powerful-json-data-type-for-clickhouse&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://clickhouse.com/blog/a-new-powerful-json-data-type-for-clickhouse&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lTrcb/hyZMcsTBPW/rEXoGIuso4UZtkQZUeRHvK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/98CqR/hyZMmoI6r1/fwjSTE5hNiqKRsyo511dX0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bqMgUC/hyZMHFdCO8/8jt9E6EEGzRVtAYF78tjLK/img.png?width=3890&amp;amp;height=996&amp;amp;face=0_0_3890_996');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;How we built a new powerful JSON data type for ClickHouse&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;We&amp;rsquo;re excited to introduce our new and significantly enhanced JSON data type, purpose-built to deliver high-performance handling of JSON data. Our core engineer, Pavel Kruglov, dives into how we built this feature on top of ClickHouse's columnar storage.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;clickhouse.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 블로그 글을 읽고 공부했던 내용을 정리했다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Variant&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Variant 칼럼이란게 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구조에서는 칼럼에는 하나의 데이터 타입만 둘 수 있었다면 Variant Type 에서는 여러가지 데이터 타입을 가질 수 있는 기능. 하나의 칼럼에 여러가지 데이터 타입의 값을 저장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 사진을 보면 하나의 칼럼에 Int64, String, Array (String or Int64) 이렇게 다양한 타입을 저장할 수 있도록 선언했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-27 오후 4.51.25.png&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;1688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mis2T/dJMcaesqcs1/vSiMIU8BaKLpr6eleGWUp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mis2T/dJMcaesqcs1/vSiMIU8BaKLpr6eleGWUp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mis2T/dJMcaesqcs1/vSiMIU8BaKLpr6eleGWUp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmis2T%2FdJMcaesqcs1%2FvSiMIU8BaKLpr6eleGWUp0%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;1362&quot; height=&quot;1688&quot; data-filename=&quot;스크린샷 2025-10-27 오후 4.51.25.png&quot; data-origin-width=&quot;1362&quot; data-origin-height=&quot;1688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Variant Type 은 하나의 칼럼으로 보이지만 실제로 저장될 때는 같은 데이터 타입인 값들 끼리 별도의 서브 칼럼 형태로 저장된다. 위의 예시에서 C.Int64 는 정수형 데이터, C.string 은 문자열이 저장되는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;For such a column, ClickHouse stores all values with the same concrete data type in separate subcolumns (type variant column data files, which by themselves look almost identical to the column data files in the&amp;nbsp;&lt;a href=&quot;https://clickhouse.com/blog/a-new-powerful-json-data-type-for-clickhouse#traditional-data-storage-in-clickhouse&quot;&gt;previous&lt;/a&gt;&amp;nbsp;example)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Variant 칼럼의 경우 타입에 해당하는 값만 읽어오도록 할 수 있다. Int64 타입의 데이터만 읽어오고 싶은 경우에는 아래처럼 선언해주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761551527321&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT C.Int64 -- Int64 타입의 데이터만 읽어오는 방법
FROM test;

   ┌─C.Int64─┐
1. │      42 │
2. │    ᴺᵁᴸᴸ │
3. │    ᴺᵁᴸᴸ │
4. │      43 │
5. │    ᴺᵁᴸᴸ │
6. │    ᴺᵁᴸᴸ │
7. │      44 │
8. │    ᴺᵁᴸᴸ │
9. │    ᴺᵁᴸᴸ │
   └─────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dynamic&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Variant 타입의 진화된 버전. 칼럼에 다양한 데이터 타입을 저장하면서 동시에 사용 가능한 데이터 타입의 수도 저장이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-27 오후 4.52.32.png&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;1154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuXO75/dJMcabbovff/HnN0eeTeIahcqJifktzdv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuXO75/dJMcabbovff/HnN0eeTeIahcqJifktzdv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuXO75/dJMcabbovff/HnN0eeTeIahcqJifktzdv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuXO75%2FdJMcabbovff%2FHnN0eeTeIahcqJifktzdv1%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;1384&quot; height=&quot;1154&quot; data-filename=&quot;스크린샷 2025-10-27 오후 4.52.32.png&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;1154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Variant Type 에서는 데이터 타입을 지정해줘야 했던 반면에 Dynamic 에선 별도의 데이터 타입을 지정하지 않고도 여러가지 타입의 데이터를 저장할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 형태에 저장되는 방식은 Variant column 과 동일하고 추가적인 정보가 같이 저장된다. 위 그림에서 C.dynamic_structure.bin 파일이 있는 점이 Variant type 과 다른 점이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynamic type 에서는 추가할 수 있는 타입의 개수에 제한을 둘 수 있다. 하지만 제한을 초과한다고 저장을 못하는건 아니다. 제한을 초과한 데이터들은 공유 칼럼에 하나의 데이터로 모두 들어가게된다. 아래 그림에서 C.SharedVariant.bin 여기에 정보가 저장된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-27 오후 4.53.48.png&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;976&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0tH8G/dJMcacOUl68/9HZoMUKhNKCtZgE4plVRt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0tH8G/dJMcacOUl68/9HZoMUKhNKCtZgE4plVRt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0tH8G/dJMcacOUl68/9HZoMUKhNKCtZgE4plVRt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0tH8G%2FdJMcacOUl68%2F9HZoMUKhNKCtZgE4plVRt1%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;1408&quot; height=&quot;976&quot; data-filename=&quot;스크린샷 2025-10-27 오후 4.53.48.png&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;976&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;Clickhouse JSON Type&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 데이터 타입은 Variant 와 Dynamic 타입을 조합해서 칼럼 데이터 타입이다. JSON 타입이 다양한 자료구조를 지원하고 있기 때문에 Variant, Dynamic 타입을 조합하는 식으로 구현한 모양이다 (이렇게 안하면 다른 방법이 없기도 하고)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 칼럼 선언 방법은 아래와 같다&lt;/p&gt;
&lt;pre id=&quot;code_1761551708495&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;column_name&amp;gt; JSON(
  max_dynamic_paths=N, 
  max_dynamic_types=M, 
  some.path TypeName, 
  SKIP path.to.skip, 
  SKIP REGEXP 'paths_regexp')&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;max_dynamic_paths: 서브 칼럼으로 저장할 수 있는 json key 의 개수. 만약에 제한을 초과하면 key 들은 동일한 서브 칼럼에 저장되게 된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;max_dynamic_types: 같은 키인데 서로 다른 데이터 타입가 서브 칼럼으로 저장될 수 있는 수. 제한이 초과되면 마찬가지로 싱글 칼럼으로 저장되게 된다.&lt;/li&gt;
&lt;li&gt;some.path TypeName 특정 JSON 경로에 대한 타입 힌트를 의미한다. 해당 경로로 저장되는 데이터는 특정 서브 칼럼으로 분류되기 때문에 성능이 보장된다&lt;/li&gt;
&lt;li&gt;skip path.to.skip 특정 JSON 경로에 대해서는 저장하지 않는다. 무시할 경로에 대해서 사용하면 편함&lt;/li&gt;
&lt;li&gt;skip regex 'path_regexp' 특정 정규식 조건의 JSON 에 대해서는 저장하지 않도록 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 칼럼 Storage 를 다이어그램으로 아래와 같이 표현할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-27 오후 4.55.55.png&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;962&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/erFxpA/dJMcaj8kRYE/WATRQN9JsTiQuIIKY8hSp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/erFxpA/dJMcaj8kRYE/WATRQN9JsTiQuIIKY8hSp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/erFxpA/dJMcaj8kRYE/WATRQN9JsTiQuIIKY8hSp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FerFxpA%2FdJMcaj8kRYE%2FWATRQN9JsTiQuIIKY8hSp0%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;1390&quot; height=&quot;962&quot; data-filename=&quot;스크린샷 2025-10-27 오후 4.55.55.png&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;962&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a.b, a.c path 의 경우에는 타입힌트로 분류되었기 때문에 별도의 서브 칼럼으로 저장된다. a.d, a.d, a.e 의 경우에는 타입 힌트로 분류되지 않았으나 현재 칼럼이 max_dynamic_paths 조건을 초과하지 않았기 때문에 서브 칼럼으로 분류가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 서브 칼럼의 존재를 눈여겨볼 필요가 있는데 타입힌트의 경우 dynamic 보다 데이터를 빠르게 조회할 수 있는 이유가 서브칼럼 때문이다. 개발자 시각에선 하나의 칼럼에 데이터를 저장하는것처럼 보이지만 실제론 path 별로 해당하는 데이터를 별도의 서브 칼럼에 저장했고 path 별로 스캔할 양이 달라지기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max_dynamic_paths 에서도 서브 칼럼이 사용되며 max_dynamic_paths 범위를 벗어나는 경우에는 공유 칼럼을 사용하고 성능의 이점도 떨어지게 된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-27 오후 5.08.16.png&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;1506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFbmEc/dJMcae0f9sp/wMa02XOIQkXLsN6I2SRu80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFbmEc/dJMcae0f9sp/wMa02XOIQkXLsN6I2SRu80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFbmEc/dJMcae0f9sp/wMa02XOIQkXLsN6I2SRu80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFbmEc%2FdJMcae0f9sp%2FwMa02XOIQkXLsN6I2SRu80%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;1402&quot; height=&quot;1506&quot; data-filename=&quot;스크린샷 2025-10-27 오후 5.08.16.png&quot; data-origin-width=&quot;1402&quot; data-origin-height=&quot;1506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;c.f path 에 해당하는 데이터의 경우 dynamic paths 를 초과하는 케이스인데 저장하는 데이터 칼럼을 공유하며 내부에 저장되는 데이터 타입도 같이 저장된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 칼럼의 경우에는 python, javascript 처럼 &amp;lsquo;.&amp;rsquo; 을 이용해서 json 경로 데이터에 접근하고 읽을수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1761552524942&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT C.a.b
FROM test;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 데이터의 서브 칼럼 타입도 같이 확인할 수 있는데 Type hint 로 따로 지정하지 않았다면 아래처럼 Dynamic 으로 표시가 된다. 다른 타입에선 어떻게 나올지는 나중에 테이블을 만들어봐야 할듯함&lt;/p&gt;
&lt;pre id=&quot;code_1761552545825&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
    C.a.d,
    toTypeName(C.a.d)
FROM test;

   ┌─C.a.d───┬─toTypeName(C.a.d)─┐
1. │ 42      │ Dynamic           │
2. │ 43      │ Dynamic           │
3. │ ᴺᵁᴸᴸ    │ Dynamic           │
4. │ foo     │ Dynamic           │
5. │ [23,24] │ Dynamic           │
6. │ ᴺᵁᴸᴸ    │ Dynamic           │
   └─────────┴───────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최적화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 시나리오에선 dynamic json 경로가 동일한 타입을 갖고 있을 것이라고 예상할 수 있는데 이런 경우 Dynamic 타입 구분자 파일이 동일한 값을 갖게 될거고 별도의 압축이 없다면 불필요한 저장공간을 차지하게 될 것이다. discriminator 는 아래 그림처럼 행별로 타입을 구분해주는 칼럼을 의미함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-27 오후 5.11.59.png&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O7oXj/dJMcajtJkYB/jsSXokrbUTVSNfQUDKKyR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O7oXj/dJMcajtJkYB/jsSXokrbUTVSNfQUDKKyR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O7oXj/dJMcajtJkYB/jsSXokrbUTVSNfQUDKKyR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO7oXj%2FdJMcajtJkYB%2FjsSXokrbUTVSNfQUDKKyR0%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;1582&quot; height=&quot;766&quot; data-filename=&quot;스크린샷 2025-10-27 오후 5.11.59.png&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;766&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷하게 유니크 하지만 다양한 json paths 를 저장하게 되면 각 경로의 discriminator 파일은 대부분 255 (NULL 을 의미) 값을 갖게 될 것임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두가지 경우 모두 discriminators 파일은 압축될 것이지만 대부분의 행이 같은 값을 가지기 때문에 redundant 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 최적화시키기 위해서 컴팩트하게 discriminator 직렬화 시키기 위한 포맷을 만들었다. discriminator 를 보통의 UInt8 로 작성하기보다 discriminator 가 target 수치와 동일하다면 세가지 데이터만 직렬화를 시킨다고 한다&lt;/p&gt;</description>
      <category>개발/데이터베이스</category>
      <category>clickhouse</category>
      <category>json</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/405</guid>
      <comments>https://kwony91.tistory.com/entry/Clickhouse-JSON-%EC%B9%BC%EB%9F%BC#entry405comment</comments>
      <pubDate>Mon, 27 Oct 2025 17:12:56 +0900</pubDate>
    </item>
    <item>
      <title>클릭하우스 업데이트 빠르게 만들기</title>
      <link>https://kwony91.tistory.com/entry/%ED%81%B4%EB%A6%AD%ED%95%98%EC%9A%B0%EC%8A%A4-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1759453077611&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;클릭하우스 업데이트는 왜 느릴까?&quot; data-og-description=&quot;How we built fast UPDATEs for the ClickHouse column store &amp;ndash; Part 1: Purpose-built enginesClickHouse is a column store, but that doesn&amp;rsquo;t mean updates are slow. In this post, we explore how purpose-built engines like ReplacingMergeTree deliver fast, effi&quot; data-og-host=&quot;selfish-developer.com&quot; data-og-source-url=&quot;https://selfish-developer.com/entry/%ED%81%B4%EB%A6%AD%ED%95%98%EC%9A%B0%EC%8A%A4-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EB%8A%94-%EC%99%9C-%EB%8A%90%EB%A6%B4%EA%B9%8C&quot; data-og-url=&quot;https://selfish-developer.com/entry/%ED%81%B4%EB%A6%AD%ED%95%98%EC%9A%B0%EC%8A%A4-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EB%8A%94-%EC%99%9C-%EB%8A%90%EB%A6%B4%EA%B9%8C&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c8305u/hyZKrb0BdM/3LRWt9gU9l3NJyBlIkJFCk/img.png?width=800&amp;amp;height=438&amp;amp;face=0_0_800_438,https://scrap.kakaocdn.net/dn/bXTWT0/hyZKtgBhsM/xCyAIAaG8ilRiyUgpDgxKK/img.png?width=800&amp;amp;height=438&amp;amp;face=0_0_800_438,https://scrap.kakaocdn.net/dn/bMExbt/hyZKt1Yak5/Xn4oy2NKsNez1AdYaZwxtK/img.png?width=1544&amp;amp;height=846&amp;amp;face=173_207_397_431&quot;&gt;&lt;a href=&quot;https://selfish-developer.com/entry/%ED%81%B4%EB%A6%AD%ED%95%98%EC%9A%B0%EC%8A%A4-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EB%8A%94-%EC%99%9C-%EB%8A%90%EB%A6%B4%EA%B9%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://selfish-developer.com/entry/%ED%81%B4%EB%A6%AD%ED%95%98%EC%9A%B0%EC%8A%A4-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EB%8A%94-%EC%99%9C-%EB%8A%90%EB%A6%B4%EA%B9%8C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c8305u/hyZKrb0BdM/3LRWt9gU9l3NJyBlIkJFCk/img.png?width=800&amp;amp;height=438&amp;amp;face=0_0_800_438,https://scrap.kakaocdn.net/dn/bXTWT0/hyZKtgBhsM/xCyAIAaG8ilRiyUgpDgxKK/img.png?width=800&amp;amp;height=438&amp;amp;face=0_0_800_438,https://scrap.kakaocdn.net/dn/bMExbt/hyZKt1Yak5/Xn4oy2NKsNez1AdYaZwxtK/img.png?width=1544&amp;amp;height=846&amp;amp;face=173_207_397_431');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;클릭하우스 업데이트는 왜 느릴까?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;How we built fast UPDATEs for the ClickHouse column store &amp;ndash; Part 1: Purpose-built enginesClickHouse is a column store, but that doesn&amp;rsquo;t mean updates are slow. In this post, we explore how purpose-built engines like ReplacingMergeTree deliver fast, effi&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;selfish-developer.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스트에서 클릭하우스 업데이트가 느린 이유를 설명했고 이번 포스트에서는 업데이트 속도를 올리기 위한 클릭하우스의 트릭(?) 여러가지를 소개함. 이것도 클릭하우스 &lt;a href=&quot;https://clickhouse.com/blog/updates-in-clickhouse-2-sql-style-updates&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;블로그&lt;/a&gt; 글을 읽어보고 중요한 부분들만 요약 발췌했다 .&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 업데이트 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트가 발생하면 클릭하우스는 뒤에서 mutation 을 실행하고 세가지 단계로 진행된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 업데이트에 대해서 새로운 block number 가 발행됨&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. mutated part 가 디스크에 저장되고 새로운 버전이 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. mutation 은 기존 버전보다 낮은 part 에 대해서 적용된다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btKdBl/btsQ2xswuGz/Xezr6KFmzWdKkDj6rJxV0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btKdBl/btsQ2xswuGz/Xezr6KFmzWdKkDj6rJxV0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btKdBl/btsQ2xswuGz/Xezr6KFmzWdKkDj6rJxV0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtKdBl%2FbtsQ2xswuGz%2FXezr6KFmzWdKkDj6rJxV0k%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;521&quot; height=&quot;898&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트된 칼럼에 대해서는 새로운 part 랑 링크가 추가되고 그렇지 않은 컬럼에 대해서는 hard linked 로 관리된다. 이런 작업이 백그라운드에서 진행되며 최종적으로 mutation 이 끝나고나면 데이터가 visible 하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;On the Fly updates&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트 결과물을 빠르게 보기 위한 방법. 실제 데이터가 rewrite 되기 전에 업데이트를 보여주게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;To reduce the latency between issuing an UPDATE and seeing the result, ClickHouse introduced&amp;nbsp;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://clickhouse.com/docs/guides/developer/on-the-fly-mutations&quot; data-token-index=&quot;1&quot;&gt;&lt;span&gt;on-the-fly mutations&lt;/span&gt;&lt;/a&gt;, an optimization that makes updates visible immediately, even before any part is rewritten.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csMmsv/btsQ10Wjtaf/LiWCQXKC6itJzmSUQWSEMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csMmsv/btsQ10Wjtaf/LiWCQXKC6itJzmSUQWSEMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csMmsv/btsQ10Wjtaf/LiWCQXKC6itJzmSUQWSEMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsMmsv%2FbtsQ10Wjtaf%2FLiWCQXKC6itJzmSUQWSEMK%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;606&quot; height=&quot;792&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트 결과물을 메모리에 저장하고 데이터를 로딩할 때 메모리를 거쳐서 가져간다. 이렇게하면 mutation 완료 전에도 데이터를 빠르게 볼 수 있다는 장점이 있지만 백그라운드 rewrite 작업을 피할 수 없고 update 데이터 양이 많으면 속도가 느려질 우려도 존재한다. 메모리 공간도 유한하기 계속 의지할 수 없기도 하다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Patch part&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그에서 중점적으로 설명하는 기술. fast insert 랑 백그라운드 머지 방법을 generalize 해서 사용하는 방식임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Patch parts borrow two proven ideas from our&amp;nbsp;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://clickhouse.com/blog/updates-in-clickhouse-1-purpose-built-engines&quot; data-token-index=&quot;1&quot;&gt;&lt;span&gt;specialized engines&lt;/span&gt;&lt;/a&gt;,&amp;nbsp;&lt;span data-token-index=&quot;3&quot;&gt;fast inserts&lt;/span&gt;&amp;nbsp;and&amp;nbsp;&lt;span data-token-index=&quot;5&quot;&gt;background merges&lt;/span&gt;, and generalize them, fully encapsulated for flexible, SQL-style updates:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 mutation 과 다르게 클릭하우스가 전체 칼럼 part 를 재작성하는게 아니라 수정된 부분에 대해서만 patch part 를 만듬&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unlike classic mutations, ClickHouse doesn&amp;rsquo;t rewrite the entire column or part. Instead, it creates a new, compact&amp;nbsp;&lt;span data-token-index=&quot;1&quot;&gt;patch part&lt;/span&gt;&amp;nbsp;that contains only:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buQfAY/btsQ3BA2OWV/IXK0WRoqKKGyFKWSy3Zedk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buQfAY/btsQ3BA2OWV/IXK0WRoqKKGyFKWSy3Zedk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buQfAY/btsQ3BA2OWV/IXK0WRoqKKGyFKWSy3Zedk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuQfAY%2FbtsQ3BA2OWV%2FIXK0WRoqKKGyFKWSy3Zedk%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;588&quot; height=&quot;844&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Patch part 는 백그라운드에서 진행하는 작업에 편승하는 작업이며 오버헤드는 거의 없다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Patch parts&amp;nbsp;&lt;span data-token-index=&quot;1&quot;&gt;piggyback on&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://clickhouse.com/blog/updates-in-clickhouse-1-purpose-built-engines#merges-happen-in-the-background&quot; data-token-index=&quot;2&quot;&gt;&lt;span&gt;merges already happening in the background&lt;/span&gt;&lt;/a&gt;, they hook into the process ClickHouse already runs continuously, with almost zero overhead:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 칼럽 (_part offset, _block_offset) 을 활용해서 정렬 작업을 최적화한다. 기존 데이터와 Patch part 데이터가 호환이 잘 될수 있도록 만들기 위한 작업인듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;1452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ct5cT5/btsQ3Dlkmm3/adEVoNl52n1mzMnpzufwkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ct5cT5/btsQ3Dlkmm3/adEVoNl52n1mzMnpzufwkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct5cT5/btsQ3Dlkmm3/adEVoNl52n1mzMnpzufwkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fct5cT5%2FbtsQ3Dlkmm3%2FadEVoNl52n1mzMnpzufwkK%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;559&quot; height=&quot;485&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;1452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;patch part 로직을 활용하기 위해 모든 행은 세가지 시스템 칼럼을 사용한다. 칼럼을 이용해서 merge 시 인덱싱 속도를 높이는데 자세한 내용까지는 알 필요는 없을 것 같고 필요하다면 &lt;a href=&quot;https://clickhouse.com/blog/updates-in-clickhouse-2-sql-style-updates#system-columns-in-original-rows&quot;&gt;&lt;span&gt;여기&lt;/span&gt;&lt;/a&gt;&amp;nbsp;읽어보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0EV3H/btsQ2UVuUrX/MRkZvY0QtOUZ2KNCMIquk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0EV3H/btsQ2UVuUrX/MRkZvY0QtOUZ2KNCMIquk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0EV3H/btsQ2UVuUrX/MRkZvY0QtOUZ2KNCMIquk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0EV3H%2FbtsQ2UVuUrX%2FMRkZvY0QtOUZ2KNCMIquk0%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;592&quot; height=&quot;332&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트 자체는 merge 를 기다리지 않는 non blocking 방식임: 백그라운드에서 merge 가 진행 되더라도 외부에 보여주는 데이터 자체는 이미 머지가 진행된 상태. 기존 방식에서는 백그라운드 머지가 완료된 상태여야 했다. 각각의 업데이트는 업데이트가 시작될 때의 데이터 스냅샷 기준으로 실행된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClickHouse updates are&amp;nbsp;&lt;span data-token-index=&quot;1&quot;&gt;non-blocking&lt;/span&gt;: they don&amp;rsquo;t wait for merges to finish. Instead, each update runs against a&amp;nbsp;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://clickhouse.com/blog/clickhouse-release-25-06#single-snapshot-for-select&quot; data-token-index=&quot;3&quot;&gt;&lt;span&gt;snapshot&lt;/span&gt;&lt;/a&gt;&amp;nbsp;of the parts that exist when the UPDATE begins.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Featherweight Deletes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 작업에서도 동일한 로직을 적용할 수 있다. Patch Part 방식처럼 삭제가 적용되는 일부 row 에다가만 업뎃을 해준다. 그래서 삭제 작업은 더이상 ALTER 명령이 아니게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In&amp;nbsp;&lt;a href=&quot;https://clickhouse.com/blog/updates-in-clickhouse-2-sql-style-updates#stage-15-lightweight-deletes-still-mutations-but-faster&quot;&gt;Stage 1.5&lt;/a&gt;,&amp;nbsp;&lt;b&gt;lightweight DELETEs&lt;/b&gt;&amp;nbsp;already gave us a win: they rewrote only the&amp;nbsp;_row_exists&amp;nbsp;deletion mask via an ALTER UPDATE, avoiding full-row rewrites.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L6DyC/btsQ1poz3sT/cZIKhmUGQWy50fyEWy33k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L6DyC/btsQ1poz3sT/cZIKhmUGQWy50fyEWy33k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L6DyC/btsQ1poz3sT/cZIKhmUGQWy50fyEWy33k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL6DyC%2FbtsQ1poz3sT%2FcZIKhmUGQWy50fyEWy33k1%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;588&quot; height=&quot;814&quot; data-origin-width=&quot;1590&quot; data-origin-height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Patch part 에서 _part_offset 1 에 해당하는 row 에 대하여 _row_exists = 0 으로 설정했고 Merged 일 때 mouse 가 삭제된 체로 업데이트 되는것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;How patch-on-read works&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능에 미치는 영향을 최소화해서 만들었다고함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WfpRW/btsQ1zkDSv4/DFLi5Sibe5kK1xxRXL9Wb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WfpRW/btsQ1zkDSv4/DFLi5Sibe5kK1xxRXL9Wb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WfpRW/btsQ1zkDSv4/DFLi5Sibe5kK1xxRXL9Wb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWfpRW%2FbtsQ1zkDSv4%2FDFLi5Sibe5kK1xxRXL9Wb0%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;665&quot; height=&quot;682&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리에서 고른 데이터는 날짜에 따라서 여러개의 data 영역 (data range) 으로 나눠짐. 요 영역들은 쿼리 엔진에 따라서 별개의 parallel stream stages 로 나눠지고 병렬 처리 절차를 따르며 결과가 나오게됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Usually, the data selected for a query (after&amp;nbsp;&lt;a href=&quot;https://clickhouse.com/docs/primary-indexes&quot;&gt;index analysis&lt;/a&gt;) is located in several&amp;nbsp;&lt;b&gt;data ranges&lt;/b&gt;&amp;nbsp;(consecutive blocks of rows) in several data parts. These ranges are dynamically spread by the&amp;nbsp;&lt;a href=&quot;https://clickhouse.com/docs/academic_overview#4-query-processing-layer&quot;&gt;query engine&lt;/a&gt;&amp;nbsp;across ① separate and&amp;nbsp;&lt;b&gt;parallel stream stages&lt;/b&gt;&amp;nbsp;(data streams) and then processed by ②&amp;nbsp;&lt;a href=&quot;https://clickhouse.com/docs/optimize/query-parallelism#distributing-work-across-processing-lanes&quot;&gt;&lt;b&gt;parallel processing lanes&lt;/b&gt;&lt;/a&gt;&amp;nbsp;that filter, aggregate, sort, and limit the data into its final result:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Patch Part 는 특별한 요소처럼 보이지만 결국에는 클릭하우스 regular part 의 일종이며 다른 patch part 와 최종적으로 머지됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 테크닉을 써서 성능에 영향을 미치지 않았다고 하지만 실제로 이렇게 동작하는지는 주의깊게 살펴볼 필요는 있다고 생각함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Patch parts may seem special, but under the hood, they&amp;rsquo;re just regular parts in ClickHouse. That means:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;They are&amp;nbsp;&lt;b&gt;merged with other patch parts&lt;/b&gt;&amp;nbsp;using the&amp;nbsp;&lt;a href=&quot;https://clickhouse.com/blog/updates-in-clickhouse-1-purpose-built-engines#replacingmergetree-replace-rows-by-inserting-new-ones&quot;&gt;ReplacingMergeTree&lt;/a&gt;&amp;nbsp;algorithm, with&amp;nbsp;_data_version&amp;nbsp;as a&amp;nbsp;&lt;a href=&quot;https://clickhouse.com/docs/engines/table-engines/mergetree-family/replacingmergetree#replacingmergetree-parameters&quot;&gt;version column&lt;/a&gt;. This ensures each patch part stores only the latest version of each updated row.&lt;/li&gt;
&lt;li&gt;They&amp;rsquo;re&amp;nbsp;&lt;b&gt;automatically cleaned up&lt;/b&gt;&amp;nbsp;once their changes are fully materialized into all affected data parts, or when merged with another patch part. Background cleanup threads handle this safely.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://clickhouse.com/docs/operations/settings/merge-tree-settings#parts_to_throw_insert&quot;&gt;&lt;b&gt;They count toward the TOO_MANY_PARTS threshold&lt;/b&gt;&lt;/a&gt;, which applies&amp;nbsp;&lt;b&gt;per partition&lt;/b&gt;. To mitigate this, patch parts are stored in&amp;nbsp;&lt;b&gt;separate partitions based on the set of updated columns&lt;/b&gt;. So if you run multiple UPDATE statements that affect different columns, like&amp;nbsp;SET x = &amp;hellip;,&amp;nbsp;SET y = &amp;hellip;, and&amp;nbsp;SET x = &amp;hellip;, y = &amp;hellip;, you&amp;rsquo;ll get separate patch partitions, each with its own part count.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;This design keeps patch parts fast, efficient, and deeply integrated with MergeTree mechanics.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;part 가 늘어나면 일반적으로는 읽어야할 파일의 수가 늘어나기 때문에 성능에 무리가 가게 될텐데 업데이트가 많아질수록 patch part 데이터가 늘어나면서 문제가 생길 수 있는게 아닐까 싶다.&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/데이터베이스</category>
      <author>kwony</author>
      <guid isPermaLink="true">https://kwony91.tistory.com/404</guid>
      <comments>https://kwony91.tistory.com/entry/%ED%81%B4%EB%A6%AD%ED%95%98%EC%9A%B0%EC%8A%A4-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry404comment</comments>
      <pubDate>Fri, 3 Oct 2025 10:19:51 +0900</pubDate>
    </item>
  </channel>
</rss>