<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>PostgreSQL.KR 강좌 RSS</title>
        <link>http://postgresql.kr/blog/</link>
        <description>postgresql.kr 홈페이지의 강좌 게시물에 대한 최신 기사 20건입니다.</description>
        <language>ko-kr</language>
        <lastBuildDate>Thu, 01 Oct 2026 04:36:59 +0900</lastBuildDate>
        <item>
            <title>Huge Page 이야기</title>
            <link>http://postgresql.kr/blog/huge_page_for_pg.html</link>
            <description>&lt;h1 data-path-to-node=&quot;2&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;Huge Pages로 161만 번의 Page Fault를 잡고 TPS 15.5% 올린 이야기&lt;/h1&gt;&lt;p data-path-to-node=&quot;3&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;개발 환경이나 DB 성능을 튜닝하다 보면 &quot;설정 하나 바꿨는데 시스템이 날아다닌다&quot;는 식의 유혹적인 글을 자주 접하곤 한다. 하지만 엔지니어의 세계에서 이유 없는 성능 향상은 없다. &apos;왜 좋아졌는가&apos;를 눈으로 직접 확인하지 못하면 그것은 최적화가 아니라 운에 가깝다.&lt;/p&gt;&lt;p data-path-to-node=&quot;4&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;이번에는 Apple Silicon(M 시리즈) 위에서 구동되는 Lima VM(Rocky Linux ARM64) 환경의 PostgreSQL 18 버전을 대상으로, OS의 &lt;b data-path-to-node=&quot;4&quot; data-index-in-node=&quot;94&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;Huge Pages&lt;/b&gt;가 DB 성능과 커널 내부 동작에 어떤 미학적인 변화를 만들어내는지 직접 실험하고 검증해 본 과정을 기록으로 남긴다.&lt;/p&gt;&lt;h2 data-path-to-node=&quot;6&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;1. 4KB라는 작은 조각과 2MB라는 거대한 판자: 공유 버퍼와 Huge Page&lt;/h2&gt;&lt;p data-path-to-node=&quot;7&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;PostgreSQL은 쿼리 성능을 높이기 위해 디스크의 데이터를 메모리에 상주시키는 &lt;code data-path-to-node=&quot;7&quot; data-index-in-node=&quot;47&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;shared_buffers&lt;/code&gt;라는 거대한 공유 메모리 공간을 사용한다. 이번 실험에서 내가 설정한 공유 버퍼의 크기는 &lt;b data-path-to-node=&quot;7&quot; data-index-in-node=&quot;111&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;4GB&lt;/b&gt;였다.&lt;/p&gt;&lt;p data-path-to-node=&quot;8&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;Linux 커널의 기본 메모리 관리 단위는 &lt;b data-path-to-node=&quot;8&quot; data-index-in-node=&quot;24&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;4KB&lt;/b&gt;다. 즉, 커널 입장에서 4GB짜리 공유 버퍼를 관리하려면 무려 1,048,576개(약 104만 개)의 조각을 만들어 일일이 주소 매핑 테이블(Page Table)에 올려두어야 한다.&lt;/p&gt;&lt;p data-path-to-node=&quot;9&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;문제는 CPU 내부의 주소 변환 캐시 메모리인 TLB(Translation Lookaside Buffer)의 용량이 지극히 제한적이라는 점이다. 수십 개의 DB 커넥션이 동시에 들어와 4GB 공간을 뒤집고 다닐 때, CPU는 104만 개나 되는 조각들의 주소를 찾느라 TLB 미스를 내며 헉헉거리게 된다. &quot;주소 찾다가 시간을 다 보내는 상태&quot;가 발생하는 것이다.&lt;/p&gt;&lt;p data-path-to-node=&quot;10&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;여기서 등장하는 구원투수가 바로 Huge Pages(2MB)다. 메모리를 4KB가 아닌 2MB 단위의 큼직한 판자로 쪼개는 기술이다. 2MB를 적용하면 4GB 공간은 단 &lt;b data-path-to-node=&quot;10&quot; data-index-in-node=&quot;95&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;2,048개의 조각&lt;/b&gt;으로 축소된다. CPU가 관리해야 할 주소 엔트리가 &lt;b data-path-to-node=&quot;10&quot; data-index-in-node=&quot;134&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;1/512&lt;/b&gt;로 급감하면서, CPU는 주소 찾기 연산을 멈추고 온전히 DB 쿼리를 처리하는 데 전념할 수 있게 된다.&lt;/p&gt;&lt;h2 data-path-to-node=&quot;12&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;2. 성능 개선을 위해 OS와 PostgreSQL에 준비한 3가지 작업&lt;/h2&gt;&lt;p data-path-to-node=&quot;13&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;4GB 공유 버퍼에 Huge Pages를 입히기 위해 OS 커널과 DB 레이어에서 3가지 설정을 진행했다.&lt;/p&gt;&lt;h3 data-path-to-node=&quot;14&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;① 커널 파라미터 예약 (&lt;code data-path-to-node=&quot;14&quot; data-index-in-node=&quot;14&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;/etc/sysctl.d/99-hugepages.conf&lt;/code&gt;)&lt;/h3&gt;&lt;p data-path-to-node=&quot;15&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;4GB(&lt;code data-path-to-node=&quot;15&quot; data-index-in-node=&quot;4&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;shared_buffers&lt;/code&gt;)보다 약간 넉넉하게 4.2GB 정도를 Huge Pages 공간으로 미리 확보하도록 지정했다. 2MB 조각 기준으로 2,150개를 잡았다.&lt;/p&gt;&lt;response-element class=&quot;no-md&quot; ng-version=&quot;0.0.0-PLACEHOLDER&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;code-block _nghost-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-429 enable-luminous-code-block ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block ng-tns-c951812425-429 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot; jslog=&quot;223238;track:impression,attention;BardVeMetadataKey:W1sicl8zNTBlN2Q4NmI5ZTg4OTFjIiwiY19mYWFmYmZjNWE5NmQwOTFmIixudWxsLCJyY19lNDIyOTk1Y2VjNTM2MmFjIixudWxsLG51bGwsImtvIixudWxsLDEsbnVsbCxudWxsLDEsMF1d&quot; data-hveid=&quot;0&quot; decode-data-ved=&quot;1&quot; data-ved=&quot;0CAAQhtANahgKEwi9z6qm8vqWAxUAAAAAHQAAAAAQ5QY&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;!----&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;formatted-code-block-internal-container ng-tns-c951812425-429&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;animated-opacity ng-tns-c951812425-429&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c951812425-429 ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;vm.nr_hugepages = 2150&lt;/div&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c951812425-429 ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;br&gt;&lt;/div&gt;&lt;!----&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/code-block&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/response-element&gt;&lt;h3 data-path-to-node=&quot;17&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;② 메모리 고정 제한 해제 (&lt;code data-path-to-node=&quot;17&quot; data-index-in-node=&quot;16&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;systemd postgresql service 설정&lt;/code&gt;)&lt;/h3&gt;&lt;p data-path-to-node=&quot;18&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;PostgreSQL 프로세스가 Huge Pages 메모리를 RAM 상에 튕겨 나가지 않게 단단히 고정(Lock)시킬 수 있도록 자원 제한을 풀어주었다.&lt;/p&gt;&lt;response-element class=&quot;no-md&quot; ng-version=&quot;0.0.0-PLACEHOLDER&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code-block _nghost-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-430 enable-luminous-code-block ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block ng-tns-c951812425-430 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot; jslog=&quot;223238;track:impression,attention;BardVeMetadataKey:W1sicl8zNTBlN2Q4NmI5ZTg4OTFjIiwiY19mYWFmYmZjNWE5NmQwOTFmIixudWxsLCJyY19lNDIyOTk1Y2VjNTM2MmFjIixudWxsLG51bGwsImtvIixudWxsLDEsbnVsbCxudWxsLDEsMF1d&quot; data-hveid=&quot;0&quot; decode-data-ved=&quot;1&quot; data-ved=&quot;0CAAQhtANahgKEwi9z6qm8vqWAxUAAAAAHQAAAAAQ5gY&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;formatted-code-block-internal-container ng-tns-c951812425-430&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;animated-opacity ng-tns-c951812425-430&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c951812425-430 ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;buttons ng-tns-c951812425-430 ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;gem-icon-button _ngcontent-ng-c951812425=&quot;&quot; tabindex=&quot;-1&quot; type=&quot;onSurface&quot; size=&quot;small&quot; theme=&quot;lm&quot; arialabel=&quot;코드 복사&quot; gemtooltip=&quot;코드 복사&quot; data-test-id=&quot;gem-copy-button&quot; class=&quot;mat-mdc-tooltip-trigger copy-button ng-tns-c951812425-430 gem-button gem-button-badge-size-small gem-button-size-small gem-button-type-on-surface lm-enabled ng-star-inserted&quot; _nghost-ng-c1730857670=&quot;&quot; aria-describedby=&quot;cdk-describedby-message-ng-1-22&quot; cdk-describedby-host=&quot;ng-1&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;/gem-icon-button&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;&lt;!----&gt;&lt;pre _ngcontent-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-430&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code _ngcontent-ng-c951812425=&quot;&quot; role=&quot;text&quot; data-test-id=&quot;code-content&quot; class=&quot;code-container formatted ng-tns-c951812425-430&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span class=&quot;hljs-section&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;[Service]&lt;/span&gt;
&lt;span class=&quot;hljs-attr&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;LimitMEMLOCK&lt;/span&gt;=infinity&amp;nbsp;&lt;/code&gt;&lt;/pre&gt;&lt;!----&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/code-block&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/response-element&gt;&lt;h3 data-path-to-node=&quot;17&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;③ Transparent Huge Pages (THP) 비활성화&lt;/h3&gt;&lt;h3 data-path-to-node=&quot;20&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;18&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;font size=&quot;3&quot;&gt;&lt;span style=&quot;font-weight: 400;&quot;&gt;Linux 커널의 THP 기능은 PostgreSQL의 Shared Memory 및 Huge Pages와 충돌하여 메모리 단편화 및 Latency Spike(갑작스러운 성능 저하)를 유발하므로 반드시 disabled 했다.&lt;/span&gt;&lt;/font&gt;&lt;/p&gt;&lt;response-element class=&quot;no-md&quot; ng-version=&quot;0.0.0-PLACEHOLDER&quot; style=&quot;font-size: medium; font-weight: 400; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code-block _nghost-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-430 enable-luminous-code-block ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block ng-tns-c951812425-430 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot; jslog=&quot;223238;track:impression,attention;BardVeMetadataKey:W1sicl8zNTBlN2Q4NmI5ZTg4OTFjIiwiY19mYWFmYmZjNWE5NmQwOTFmIixudWxsLCJyY19lNDIyOTk1Y2VjNTM2MmFjIixudWxsLG51bGwsImtvIixudWxsLDEsbnVsbCxudWxsLDEsMF1d&quot; data-hveid=&quot;0&quot; decode-data-ved=&quot;1&quot; data-ved=&quot;0CAAQhtANahgKEwi9z6qm8vqWAxUAAAAAHQAAAAAQ5gY&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;formatted-code-block-internal-container ng-tns-c951812425-430&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;animated-opacity ng-tns-c951812425-430&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c951812425-430 ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;buttons ng-tns-c951812425-430 ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;gem-icon-button _ngcontent-ng-c951812425=&quot;&quot; tabindex=&quot;-1&quot; type=&quot;onSurface&quot; size=&quot;small&quot; theme=&quot;lm&quot; arialabel=&quot;코드 복사&quot; gemtooltip=&quot;코드 복사&quot; data-test-id=&quot;gem-copy-button&quot; class=&quot;mat-mdc-tooltip-trigger copy-button ng-tns-c951812425-430 gem-button gem-button-badge-size-small gem-button-size-small gem-button-type-on-surface lm-enabled ng-star-inserted&quot; _nghost-ng-c1730857670=&quot;&quot; aria-describedby=&quot;cdk-describedby-message-ng-1-22&quot; cdk-describedby-host=&quot;ng-1&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;/gem-icon-button&gt;&lt;/div&gt;&lt;/div&gt;&lt;pre _ngcontent-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-430&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code _ngcontent-ng-c951812425=&quot;&quot; role=&quot;text&quot; data-test-id=&quot;code-content&quot; class=&quot;code-container formatted ng-tns-c951812425-430&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;# root 권한으로 실행
echo &apos;w /sys/kernel/mm/transparent_hugepage/enabled - - - - never&apos; | tee /etc/tmpfiles.d/disable-thp.conf
echo &apos;w /sys/kernel/mm/transparent_hugepage/defrag - - - - never&apos; | tee -a /etc/tmpfiles.d/disable-thp.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code-block&gt;&lt;/response-element&gt;&lt;/h3&gt;&lt;h3 data-path-to-node=&quot;20&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;④ PostgreSQL 파라미터 적용 (&lt;code data-path-to-node=&quot;20&quot; data-index-in-node=&quot;22&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;postgresql.conf&lt;/code&gt;)&lt;/h3&gt;&lt;p data-path-to-node=&quot;21&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;공유 버퍼 크기를 4GB로 지정하고, Huge Pages가 제대로 할당되지 않으면 아예 DB가 켜지지 않도록 &apos;강제(on)&apos; 옵션을 주었다.&lt;/p&gt;&lt;response-element class=&quot;no-md&quot; ng-version=&quot;0.0.0-PLACEHOLDER&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code-block _nghost-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-431 enable-luminous-code-block ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block ng-tns-c951812425-431 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot; jslog=&quot;223238;track:impression,attention;BardVeMetadataKey:W1sicl8zNTBlN2Q4NmI5ZTg4OTFjIiwiY19mYWFmYmZjNWE5NmQwOTFmIixudWxsLCJyY19lNDIyOTk1Y2VjNTM2MmFjIixudWxsLG51bGwsImtvIixudWxsLDEsbnVsbCxudWxsLDEsMF1d&quot; data-hveid=&quot;0&quot; decode-data-ved=&quot;1&quot; data-ved=&quot;0CAAQhtANahgKEwi9z6qm8vqWAxUAAAAAHQAAAAAQ5wY&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;formatted-code-block-internal-container ng-tns-c951812425-431&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;animated-opacity ng-tns-c951812425-431&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block-decoration header-formatted gds-emphasized-body-m ng-tns-c951812425-431 ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;buttons ng-tns-c951812425-431 ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;gem-icon-button _ngcontent-ng-c951812425=&quot;&quot; tabindex=&quot;-1&quot; type=&quot;onSurface&quot; size=&quot;small&quot; theme=&quot;lm&quot; arialabel=&quot;코드 복사&quot; gemtooltip=&quot;코드 복사&quot; data-test-id=&quot;gem-copy-button&quot; class=&quot;mat-mdc-tooltip-trigger copy-button ng-tns-c951812425-431 gem-button gem-button-badge-size-small gem-button-size-small gem-button-type-on-surface lm-enabled ng-star-inserted&quot; _nghost-ng-c1730857670=&quot;&quot; aria-describedby=&quot;cdk-describedby-message-ng-1-22&quot; cdk-describedby-host=&quot;ng-1&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;!----&gt;&lt;!----&gt;&lt;/gem-icon-button&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;&lt;!----&gt;&lt;!----&gt;&lt;/div&gt;&lt;!----&gt;&lt;pre _ngcontent-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-431&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code _ngcontent-ng-c951812425=&quot;&quot; role=&quot;text&quot; data-test-id=&quot;code-content&quot; class=&quot;code-container formatted ng-tns-c951812425-431&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span class=&quot;hljs-attr&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;shared_buffers&lt;/span&gt; = &lt;span class=&quot;hljs-number&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;4&lt;/span&gt;GB
&lt;span class=&quot;hljs-attr&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;huge_pages&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;!----&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/code-block&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/response-element&gt;&lt;h2 data-path-to-node=&quot;24&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;3. 숫자와 수치로 증명하는 현장의 기록: pgbench, perf, 그리고 /proc/meminfo&lt;/h2&gt;&lt;p data-path-to-node=&quot;25&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;모든 준비를 마치고, Huge Pages를 켰을 때(ON)와 껐을 때(OFF)를 비교하는 본격적인 벤치마크(&lt;code data-path-to-node=&quot;25&quot; data-index-in-node=&quot;60&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;pgbench&lt;/code&gt;)와 커널 추적(&lt;code data-path-to-node=&quot;25&quot; data-index-in-node=&quot;76&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;perf&lt;/code&gt;)에 들어갔다.&lt;/p&gt;&lt;h3 data-path-to-node=&quot;26&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;① pgbench 성능 비교: &quot;더 많이, 더 빠르게&quot;&lt;/h3&gt;&lt;p data-path-to-node=&quot;27&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;64개 클라이언트 환경에서 Read-Only 쿼리 부하를 5분간 가했을 때의 결과다.&lt;/p&gt;&lt;table data-path-to-node=&quot;28&quot; style=&quot;margin-bottom: 32px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;thead style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;tr style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;strong style=&quot;line-height: 1.15 !important; margin: 0px !important;&quot;&gt;측정 지표&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;strong style=&quot;line-height: 1.15 !important; margin: 0px !important;&quot;&gt;Huge Pages OFF (4KB)&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;strong style=&quot;line-height: 1.15 !important; margin: 0px !important;&quot;&gt;Huge Pages ON (2MB)&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;strong style=&quot;line-height: 1.15 !important; margin: 0px !important;&quot;&gt;수치 변화 / 효과&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;tr style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span data-path-to-node=&quot;28,1,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;28,1,0,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;TPS (초당 트랜잭션)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span data-path-to-node=&quot;28,1,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;112,942&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span data-path-to-node=&quot;28,1,2,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;28,1,2,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;130,459&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span data-path-to-node=&quot;28,1,3,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;28,1,3,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;+15.5% 향상&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span data-path-to-node=&quot;28,2,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;28,2,0,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;Latency (평균 응답속도)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span data-path-to-node=&quot;28,2,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;0.567 ms&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span data-path-to-node=&quot;28,2,2,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;28,2,2,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;0.491 ms&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;td style=&quot;border: 1px solid rgb(196, 199, 197); padding: 8px 12px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;span data-path-to-node=&quot;28,2,3,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;28,2,3,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;-13.4% 단축&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-path-to-node=&quot;29&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;하드웨어 스펙을 단 1MB도 늘리지 않고, 오직 커널 메모리 매핑 방식을 바꾼 것만으로 &lt;b data-path-to-node=&quot;29&quot; data-index-in-node=&quot;49&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;처리량은 15.5% 늘어나고 응답 속도는 0.4ms 대&lt;/b&gt;로 단축되었다.&lt;/p&gt;&lt;h3 data-path-to-node=&quot;31&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;② perf 추적 결과: &quot;161만 번의 비명이 5천 번의 침묵으로&quot;&lt;/h3&gt;&lt;p data-path-to-node=&quot;32&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;TPS가 왜 올라갔는지 기술적 원인을 입증하기 위해 &lt;code data-path-to-node=&quot;32&quot; data-index-in-node=&quot;29&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;perf&lt;/code&gt;로 60초간 커널의 &lt;code data-path-to-node=&quot;32&quot; data-index-in-node=&quot;44&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;page-faults&lt;/code&gt; 이벤트를 채록했다. 결과는 그야말로 극적이었다.&lt;/p&gt;&lt;ul data-path-to-node=&quot;33&quot; style=&quot;padding-inline-start: 32px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;33,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;33,0,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;Huge Pages OFF 부하 중:&lt;/b&gt; &lt;code data-path-to-node=&quot;33,0,0&quot; data-index-in-node=&quot;21&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;1,616,577&lt;/code&gt; page-faults (약 161만 건)&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;33,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;33,1,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;Huge Pages ON 부하 중:&lt;/b&gt; &lt;code data-path-to-node=&quot;33,1,0&quot; data-index-in-node=&quot;20&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;5,042&lt;/code&gt; page-faults (약 5천 건)&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p data-path-to-node=&quot;34&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;Huge Pages가 꺼져 있을 때는 CPU가 60초 동안 161만 번이나 &quot;이 4KB 주소 물리 메모리 어디 있어?&quot; 하고 커널을 호출하며 비명을 질렀다. 반면 Huge Pages를 켜자 이 주소 재매핑 오버헤드가 &lt;b data-path-to-node=&quot;34&quot; data-index-in-node=&quot;121&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;99.69% 폭락&lt;/b&gt;하여 불과 5,042건으로 줄어들었다. 부하가 강하게 걸리고 있음에도 거의 대기(Idle) 상태 수준의 유려함을 보여준 것이다.&lt;/p&gt;&lt;h3 data-path-to-node=&quot;36&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;③ /proc/meminfo 비교: &quot;실제로 그 많은 페이지는 어디로 갔나?&quot;&lt;/h3&gt;&lt;p data-path-to-node=&quot;37&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;DB 가동 직후(부하 전)와 &lt;code data-path-to-node=&quot;37&quot; data-index-in-node=&quot;16&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;pgbench&lt;/code&gt; 실행 중(부하 후)에 커널 메모리 상태(&lt;code data-path-to-node=&quot;37&quot; data-index-in-node=&quot;46&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;grep Huge /proc/meminfo&lt;/code&gt;)를 출력해 메모리 내부의 움직임을 관찰했다.&lt;/p&gt;&lt;h4 data-path-to-node=&quot;38&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;[부하 전: DB 시동 직후]&lt;/h4&gt;&lt;response-element class=&quot;no-md&quot; ng-version=&quot;0.0.0-PLACEHOLDER&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code-block _nghost-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-432 enable-luminous-code-block ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block ng-tns-c951812425-432 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot; jslog=&quot;223238;track:impression,attention;BardVeMetadataKey:W1sicl8zNTBlN2Q4NmI5ZTg4OTFjIiwiY19mYWFmYmZjNWE5NmQwOTFmIixudWxsLCJyY19lNDIyOTk1Y2VjNTM2MmFjIixudWxsLG51bGwsImtvIixudWxsLDEsbnVsbCxudWxsLDEsMF1d&quot; data-hveid=&quot;0&quot; decode-data-ved=&quot;1&quot; data-ved=&quot;0CAAQhtANahgKEwi9z6qm8vqWAxUAAAAAHQAAAAAQ6gY&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;formatted-code-block-internal-container ng-tns-c951812425-432&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;animated-opacity ng-tns-c951812425-432&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;pre _ngcontent-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-432&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code _ngcontent-ng-c951812425=&quot;&quot; role=&quot;text&quot; data-test-id=&quot;code-content&quot; class=&quot;code-container formatted ng-tns-c951812425-432&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;HugePages_Total:    2150&lt;br&gt;HugePages_Free:     2087
HugePages_Rsvd:     2064
&lt;/code&gt;&lt;/pre&gt;&lt;!----&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/code-block&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/response-element&gt;&lt;ul data-path-to-node=&quot;40&quot; style=&quot;padding-inline-start: 32px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;40,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;40,0,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;해석:&lt;/b&gt; 총 2,150개(약 4.3GB)의 Huge Pages 중, PostgreSQL이 4GB 공유 버퍼를 위해 &lt;b data-path-to-node=&quot;40,0,0&quot; data-index-in-node=&quot;63&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;2,064개&lt;/b&gt;를 사전에 딱 찜해놓은(&lt;code data-path-to-node=&quot;40,0,0&quot; data-index-in-node=&quot;82&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;Rsvd&lt;/code&gt;, Reserved) 상태다. 아직 실제 쿼리가 대량으로 들어오지 않아 메모리에 물리적으로 모두 터치되지는 않았다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-path-to-node=&quot;41&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;[부하 후: pgbench 64 클라이언트 휘몰아친 후]&lt;/h4&gt;&lt;response-element class=&quot;no-md&quot; ng-version=&quot;0.0.0-PLACEHOLDER&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code-block _nghost-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-433 enable-luminous-code-block ng-star-inserted&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;code-block ng-tns-c951812425-433 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation&quot; jslog=&quot;223238;track:impression,attention;BardVeMetadataKey:W1sicl8zNTBlN2Q4NmI5ZTg4OTFjIiwiY19mYWFmYmZjNWE5NmQwOTFmIixudWxsLCJyY19lNDIyOTk1Y2VjNTM2MmFjIixudWxsLG51bGwsImtvIixudWxsLDEsbnVsbCxudWxsLDEsMF1d&quot; data-hveid=&quot;0&quot; decode-data-ved=&quot;1&quot; data-ved=&quot;0CAAQhtANahgKEwi9z6qm8vqWAxUAAAAAHQAAAAAQ6wY&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;formatted-code-block-internal-container ng-tns-c951812425-433&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;div _ngcontent-ng-c951812425=&quot;&quot; class=&quot;animated-opacity ng-tns-c951812425-433&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;pre _ngcontent-ng-c951812425=&quot;&quot; class=&quot;ng-tns-c951812425-433&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;code _ngcontent-ng-c951812425=&quot;&quot; role=&quot;text&quot; data-test-id=&quot;code-content&quot; class=&quot;code-container formatted ng-tns-c951812425-433&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;HugePages_Total:    2150&lt;br&gt;HugePages_Free:       40
HugePages_Rsvd:       17
&lt;/code&gt;&lt;/pre&gt;&lt;!----&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/code-block&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;!----&gt;&lt;/response-element&gt;&lt;ul data-path-to-node=&quot;43&quot; style=&quot;padding-inline-start: 32px; line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;43,0,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;43,0,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;해석:&lt;/b&gt; 64개의 클라이언트가 4GB 공유 버퍼 전체를 맹렬하게 읽고 쓰면서, 예약되어 있던 메모리 페이지들이 실제로 물리 RAM에 100% 매핑되어 완전히 소진되었다. (&lt;code data-path-to-node=&quot;43,0,0&quot; data-index-in-node=&quot;95&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;Free&lt;/code&gt; 수치가 2,087개에서 &lt;b data-path-to-node=&quot;43,0,0&quot; data-index-in-node=&quot;113&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;40개&lt;/b&gt;로 급감)&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;43,1,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;커널이 할당해 둔 2MB짜리 대형 공간 2,100여 개를 PostgreSQL이 완벽하게 장악하고 100% 효율로 활용하고 있음을 보여주는 진풍경이다.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h2 data-path-to-node=&quot;45&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;4. 결론: 원인을 아는 최적화가 주는 쾌감&lt;/h2&gt;&lt;p data-path-to-node=&quot;46&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;이번 트러블슈팅과 성능 검증을 관통하는 결론은 깔끔하다.&lt;/p&gt;&lt;blockquote data-path-to-node=&quot;47&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;47,0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;47,0&quot; data-index-in-node=&quot;0&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;&quot;Huge Pages 적용을 통해 커널의 주소 재매핑 부담(Page Faults)을 161만 건에서 5천 건으로 99.7% 제거하였고, 그 결과 얻어진 CPU 여유 자원이 그대로 DB로 이관되어 TPS 15.5% 상승이라는 가시적인 성능 이득을 만들어냈다.&quot;&lt;/b&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p data-path-to-node=&quot;48&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;대용량 &lt;code data-path-to-node=&quot;48&quot; data-index-in-node=&quot;4&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;shared_buffers&lt;/code&gt;(4GB 이상)를 운영하는 PostgreSQL 환경이라면 Huge Pages는 &apos;선택&apos;이 아니라 &apos;필수&apos;다.&lt;/p&gt;&lt;p data-path-to-node=&quot;49&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;로그와 커널 지표 속에 숨어있는 숫자의 의미를 읽어내고, 그것이 성능이라는 결과로 일치되어 증명될 때 엔지니어로서 얻는 쾌감은 이루 말할 수 없다. 데이터베이스 환경 설정을 통한 성능 튜닝이란 결국 시스템과 커널이 나누는 대화를 귀담아듣는 작업이 아닐까.&lt;/p&gt;&lt;p data-path-to-node=&quot;49&quot; style=&quot;line-height: 1.15 !important; margin-top: 0px !important; margin-left: 0px !important; margin-right: 0px !important;&quot;&gt;(그럼에도 불구하고, 쿼리 튜닝이 먼저인 것은 어쩔 수 없다.)&lt;/p&gt;</description>
            <pubDate>Sun, 20 Sep 2026 03:15:47 +0900</pubDate>
            <guid>http://postgresql.kr/blog/huge_page_for_pg.html</guid>
        </item>
        <item>
            <title>tobgem3_client 확장 모듈 개발 이야기</title>
            <link>http://postgresql.kr/blog/tobgem3_client.html</link>
            <description>&lt;h1&gt;tobgem3_client 확장 모듈 개발 이야기&lt;/h1&gt;&lt;h2&gt;들어가며&lt;/h2&gt;&lt;div&gt;LLM 등장과 함께 급부상한 vector 검색에 대한 이야기입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;PostgreSQL은 오래전부터 vector 자료에 대한 처리를 참 잘 해왔습니다. 그 대표적인 것이 textsearch 와, postgis 쪽이죠.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그런데, 요즘의 vector 이야기는 이런 vector 를 구성하는 그 값과 그 수학적인 연산에서 벗어나서 &apos;의미적 유사성&apos; 이라는 것에 더 집중되고 있더군요. 즉, 이 자료가 내가 찾고자 하는 자료랑 얼마나 유사하니? 그래서, 가장 유사한 것 다섯개만 찾는다. 이 &apos;유사하다, 비슷하다.&apos; 는 어떤 문장 일 수 있고, 어떤 소리 일 수 있고, 어떤 사진 일 수도 있다. 여기서 vector 진가가 나타납니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;데이터베이스의 판도를 바꾸었다고 봐도 과하지 않은 발상의 전환이었습니다.&lt;/div&gt;&lt;div&gt;그 vector 이야기의 중심에 pgvector가 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;물론 인공지능 분야의 활성화로 거의 모든 데이터베이스가 이 vector 자료 처리에 대한 기능을 제공합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;굳이 왜 꼭 PostgreSQL 로 vector 자료를 처리해야하니? 라는 질문에 저는 아직까지는 그럴싸한 대답을 못 찾았습니다. 단지, 기존 자료들이 PostgreSQL 안에 있고, 그것들을 가장 손쉽게 vector 자료로 추가 저장하고, 기존 방식 대로 처리할 수 있는 익숙함, 그것 뿐입니다.&amp;nbsp; 또 다른 하나는 어떤 대형 벤더의 기술에 기대지 않고, 오픈소스로 널리 세상을 이롭게라는 아주 거창한 모토를 따르겠다는 어쭙잖은 오기도 한몫을 하겠죠.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;pgvector 확장 모듈&lt;/h2&gt;&lt;div&gt;먼저 이 &apos;의미 기반 검색&apos;을 PostgreSQL에서 구현하려면, pgvector 확장 모듈이 필요합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;물론 이것도 단지 현재 상황에서는 실험적일 뿐입니다. vector 기술의 부흥으로 PostgreSQL에서 vector 자료 처리를 지원하는 확장 모듈이 pgvector만 있는 것은 아니니까요. 다들 제각기, 우리 모듈은 pgvector 보다 이런 점에서 좋다면서 자랑을 하니까, 오히려, 도대체 저 pgvector가 뭔지 더 깊게 살펴보고싶어서 pgvector를 보기 시작했습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 모듈 설치는 여느 확장 모듈 설치방법과 크게 다르지 않기 때문에,&amp;nbsp;&lt;/div&gt;&lt;div&gt;해당 소스를 구하고, 그 소스 안에서 설명하고 있는 설치 방법 대로 설치하면 끝납니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;현재 pgvector 는 &lt;a href=&quot;https://github.com/pgvector/pgvector&quot;&gt;https://github.com/pgvector/pgvector&lt;/a&gt; 에서 관리되고 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;기술적인 문서는 github 문서 이상을 제가 쓸 수 없는 관계로 생략합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 글을 쭉 읽어 &apos;아, 이렇게 돌아가는 구나!&apos; 라고 어느 정도 방향성이 정해지면,&lt;/div&gt;&lt;div&gt;github 문서를 보면서 기술적인 부분을 참고하시면 됩니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;모든 설치가 끝나고, CREATE EXTENSION 작업까지 되었다면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;다음과 같이 pgvector 확장 모듈을 살펴 볼 수 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# \dx
                                          설치된 확장기능 목록
      이름      | 버전  | 기본 버전 |   스키마   |                         설명
----------------+-------+-----------+------------+------------------------------------------------------
 plpgsql        | 1.0   | 1.0       | pg_catalog | PL/pgSQL procedural language
 vector         | 0.8.2 | 0.8.2     | public     | vector data type and ivfflat and hnsw access methods

postgres=# \dx+ vector
            &quot;vector&quot; 확장 기능 안에 포함된 객체들
                          개체 설명
--------------------------------------------------------------
 *(halfvec,halfvec) 연산자
 *(vector,vector) 연산자
 +(halfvec,halfvec) 연산자
 +(vector,vector) 연산자
 -(halfvec,halfvec) 연산자
 -(vector,vector) 연산자
 &amp;lt;#&amp;gt;(halfvec,halfvec) 연산자
...중략...
 vector_to_float4(vector,integer,boolean) 함수
 vector_to_halfvec(vector,integer,boolean) 함수
 vector_to_sparsevec(vector,integer,boolean) 함수
 vector_typmod_in(cstring[]) 함수
 ||(halfvec,halfvec) 연산자
 ||(vector,vector) 연산자
&lt;/pre&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;간단히 정리하면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;vector 라는 자료형을 이제부터 사용할 수 있고,&amp;nbsp;&lt;/li&gt;&lt;li&gt;그 자료형과 관련된 연산(&amp;lt;=&amp;gt;, &amp;lt;-&amp;gt; 거리 계산),&amp;nbsp;&lt;/li&gt;&lt;li&gt;그 자료를 보다 빠르게 접근하기 위한 hnsw, ivfflat 인덱스 접근 방법(index access method - 인덱스 종류를 지정하는 이름)&amp;nbsp;&lt;/li&gt;&lt;li&gt;기타 집계함수 (벡터 자료의 중심점 찾기)&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;이런 것들을 이제 사용할 수 있습니다.&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;vector 임베딩&lt;/h2&gt;&lt;div&gt;pgvetor 문서에 나오는 예제를 보면서 의문이 들었습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그 예제들이랑 &apos;의미 기반 검색, Semantic Search&apos;랑 무슨 관계가 있는거지?&lt;/div&gt;&lt;div&gt;[1,1], [2,2], [3,4] 세 개의 점들이 있을 때, [2,5] 점에서 제일 가까운 점을 찾는 것은 이미 postgis 모듈로도 충분히 할 수 있는데 말이죠.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;핵심은 &apos;벡터 임베딩&apos; 이라는 기술에 있었습니다.&lt;/div&gt;&lt;div&gt;어떤 자료를 어떻게 임베딩할 것인가? 이것에 대한 연구가 인공지능이랑 만나면서 폭발적으로 다양한 사례들이 등장하고, 그 꽃은 문장을 벡터화하고, 그 벡터의 유사도를 분석하면 의미 기반 검색에 이용하는 것이였습니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;문장을 벡터로&lt;/h3&gt;&lt;div&gt;그럼 어떻게 어떤 기법으로 문장을 벡터화 할 것인가?&lt;/div&gt;&lt;div&gt;현재 가장 보편적인 기술은 임베딩 모델을 사용해서, 문자열을 인코딩(내부 라이브러리에서는 encode라고 하고, 일반적인 단어로는 벡터화(vectorize)라고도 합니다)하고 그 vector를 자료로 저장하는 것을 embed 합니다. 흔히 encoder 모듈, 또는 embedding 모델을 이용한다고 합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;그럼 이런 인코더들은 어떤 것이 있는가? 를 찾아보니, 엄청나게 많더군요. 이 글에서는 알리바바에서 만든 &lt;a href=&quot;https://huggingface.co/BAAI/bge-m3&quot;&gt;BAAI/bge-m3&lt;/a&gt; 모델과 python &lt;a href=&quot;https://pypi.org/project/sentence-transformers/&quot;&gt;sentence-transformers&lt;/a&gt;&amp;nbsp;패키지의 SentenceTransformer 모듈을 이용했습니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;그 외 자료의 벡터 임베딩은?&lt;/h3&gt;&lt;div&gt;이제, 생각을 조금 바꿔, 문장 아닌 다른 것들은? 이 생각이 들었을 때, bge-m3 모델처럼 분명 다른 모델들도 많을거야라는 생각으로 이어졌고, 예상 대로 엄청나게 다양한 모델들이 있었습니다. 즉, 내가 생각하고 있는, 내가 가지고 있는 자료를 벡터화 하면 어떻게 사용할 수 있겠다는 생각을 해야할 것이고, 그것을 어떻게 벡터화하면 되겠다는 전략이 필요해졌습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;예를 들어, 가끔 소개하고 있는 대한민국 구석구석 자료를 기반으로 해서 그 자료들의 벡터 임베딩 자료를 더 추가한다면, 사용자 여행 취향 기반 추천 시스템을 만들 수도 있겠죠. 뭐, 이런 작업을 하는데, 벡터화 임베딩 모델을 찾을 수 없다면, 자신이 직접 벡터 임베딩 설계를 하면 될 것입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(하지만, 인터넷을 뒤지면 대부분 제가 생각하고 있는 임베딩 모델들은 이미 다 있더군요. 역시 형님들은 ...)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;자료입출력&lt;/h2&gt;&lt;div&gt;이 글에서는 bge-m3 모델을 이용한 문장의 벡터 임베딩 자료 입출력과 그 검색으로 한정 짓습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;테스트 자료는 당연히 문장의 꽃인 언론사 기사.&lt;/div&gt;&lt;div&gt;다행히 우리나라는 오래전부터 이 분야의 공익성을 보고, 매년 국립국어원에서 한 해가 지난 언론사 기사들 가운데, 공개할 수 있는 것들을 깔끔하게 다듬어 &apos;모두의 말뭉치&apos; 사이트에 공개하고 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글은 바로 이 자료인 &apos;23년 신문기사 말뭉치&apos; 자료를 대상으로 했습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;자료는 총 24개 언론사의 약 120만건 기사를 대상으로 했습니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;데이터 청킹&lt;/h3&gt;&lt;div&gt;자료를 구하고, 이제 PostgreSQL 데이터베이스에 입력하려고 보니, 그 기사의 길이가 bge-m3가 감당할 수 있는 길이가 아니였습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;여기서 드디어 &apos;청킹, chunking&apos; 이라는 용어가 등장합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;대부분의 벡터 인코딩 모델들은 벡터 차원(Dimension)이 미리 정해져있습니다.&lt;/div&gt;&lt;div&gt;bge-m3 모델은 1024 차원입니다. 즉, 어떤 문장을 입력해도, 인코딩 결과는&amp;nbsp; [1,2,...1024] 이런 1024 차원의 벡터를 만든다는 뜻입니다. 또 다시 말하면, 인코딩할 문장이 아주 긴 경우는 그 변별성이 떨어짐을 의미합니다.&lt;/div&gt;&lt;div&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;결국 &apos;기사의 문단 나누기&apos; 작업이 필요해졌습니다.&lt;br&gt;1024개의 숫자로 변환하면 한국어인 경우는 약 500자 정도라고 하네요.&lt;br&gt;(왜 이 값인지에 대해서 저도 모릅니다. 이 문단 나누기 영역도 또 하나의 엄청난 기술영역이더군요. 파면 팔 수록 그 깊이가 ... 아무튼 단순하게 500자 기준으로 하면서 하나의 청크가 이전, 이후 청크의 맥락을 조금 포함 하기 위해서 overlap 값으로 50 정도 해서 나눴습니다. 이것도 python langchain-text-splitters 패키지의 RecursiveCharacterTextSplitter을 사용했습니다. 문단 나누기 - 그냥 글자수로 나누면 되는 것 아닌가? 아니더군요. -.-)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;INSERT&lt;/h3&gt;&lt;div&gt;자료를 저장하기 위해서는 일단 PostgreSQL pgvector 모듈에서 제공하는 vector 자료형 칼럼을 사용하는 테이블 하나를 만듭니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# \d news_chunks
                                 &quot;public.news_chunks&quot; 테이블
    필드명     |     형태     | 정렬규칙 | NULL허용 |                 초기값
---------------+--------------+----------+----------+-----------------------------------------
 id            | integer      |          | not null | nextval(&apos;news_chunks_id_seq&apos;::regclass)
 article_id    | integer      |          |          |
 chunk_content | text         |          | not null |
 chunk_index   | integer      |          |          |
 embedding     | vector(1024) |          |          |
인덱스들:
    &quot;news_chunks_pkey&quot; PRIMARY KEY, btree (id)
    &quot;idx_news_chunks_article_id&quot; btree (article_id)
    &quot;news_chunks_embedding_idx&quot; hnsw (embedding vector_cosine_ops) WITH (m=&apos;16&apos;, ef_construction=&apos;64&apos;)
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 news_chunks.embedding 칼럼에 앞에서 이야기한 문장을 벡터로 바꾼 그 자료가 저장됩니다. 자료 입력은 당연히&lt;/div&gt;&lt;div&gt;&lt;pre&gt;INSERT INTO news_chunks VALUES (default, 1, &apos;안녕하세요.&apos;, 0, &apos;[0.1, 0.2, ..., 0.1]&apos;)&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이런식이 되겠죠. 그런데, 저 벡터값은 python 응용 프로그램에서 만들 수 있으니, 결국, insert 작업은 psql에서 간단하게 처리할 수 없게 되었습니다. python에서 PostgreSQL을 쓰기 위해서, psycopg 모듈 사용법도 알아야하고, 여간 성가신 일이 아니더군요.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;약 5일에 걸처 모든 자료가 입력되었습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# select count(*) from news_chunks;
  count
---------
 3163508
(1개 행)

postgres=# \dt+ news_chunks
                                테이블 목록
 스키마 |    이름     |  형태  | 소유주 | 지속성 | 접근 방법 | 크기  | 설명
--------+-------------+--------+--------+--------+-----------+-------+------
 public | news_chunks | 테이블 | ioseph | 영구   | heap      | 19 GB |
(1개 행)

postgres=# \di+ news_chunks_embedding_idx
                                              인덱스 목록
 스키마 |           이름            |  형태  | 소유주 |   테이블    | 지속성 | 접근 방법 | 크기  | 설명
--------+---------------------------+--------+--------+-------------+--------+-----------+-------+------
 public | news_chunks_embedding_idx | 인덱스 | ioseph | news_chunks | 영구   | hnsw      | 24 GB |
(1개 행)
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;순수 기사의 크기만 계산하면 약 150만건의 기사로 약 2GB 정도의 텍스트 자료인데, 약 300만건의 자료로 나눠서(하나의 기사당 두개의 청크로 나뉘어졌네요.) 19GB 까지 커진 것을 보면, 4kb(하나의 임베딩 자료, 4byte 부동소수형 1024개)의 약 300만건, 12GB 정도가 embedding 칼럼의 자료이고 나머지가 일반 데이터네요.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;SELECT&lt;/h3&gt;&lt;div&gt;&lt;div&gt;이제 이것을 가지고, 의미 기반 검색을 시도합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;간단합니다. 신문 기사니까, embedding 칼럼 기반 조회를 하면 됩니다.&lt;br&gt;여기서 새로운 사실 하나를 배웁니다.&lt;/div&gt;&lt;br&gt;기존 방식으로 생각하면,&amp;nbsp;&lt;/div&gt;&lt;pre&gt;select * from news_chunks where embedding = &apos;[0.1...]&apos;&lt;/pre&gt;&lt;div&gt;또는&lt;/div&gt;&lt;pre&gt;select * from news_chunks where embedding &amp;gt;= &apos;[0.1...]&apos; and embedding &amp;lt;= &apos;[0.2...]&apos;&lt;/pre&gt;&lt;div&gt;이런 식으로 쓰면 되겠지 생각하겠지만,&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: red&quot;&gt;이런 쿼리를 사용하지 않습니다.&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;벡터 자료의 검색은 기존 검색과 다릅니다.&lt;/div&gt;&lt;div&gt;한국어 문장으로 풀면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&quot;news_chunks.embedding 자료 가운데, 다음 벡터(질문을 벡터로 바꾼 자료)와 유사한 순서대로 찾아서 그 중에 제일 유사한 다섯개만 뽑아라.&quot;&lt;/div&gt;&lt;div&gt;이렇게 사용합니다. 이렇게 해야 위에서 만든 hnsw 인덱스를 사용할 수 있게 되고, 그래야 검색 속도를 높일 수 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기서 중요한 사실 하나를 기억해야합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&apos;유사하다&apos; 라는 뜻이 pgvector에서는 참 여러가지로 구분한 것입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;궁금하신 분은 더 깊게, LLM에게 물어보면서 살펴보시면 될 것 같고, 여기서는 &amp;lt;-&amp;gt;(유클리드 거리)와 &amp;lt;=&amp;gt;(코사인 유사도) 연산을 통해서 유사도 평가를 다룰 것입니다.&lt;br&gt;&lt;br&gt;news_chunks 테이블이 RAG(Retrieval-Augmented Generation, 검색 증강 생성)시스템 구축 용도로 사용된다면, 최적의 연산은 코사인 유사도입니다.&amp;nbsp;&lt;br&gt;즉, 쿼리는&amp;nbsp;&lt;/div&gt;&lt;pre&gt;select * from news_chunks order by embedding &amp;lt;=&amp;gt; &apos;[0.1,0.2....]&apos; limit 5&lt;/pre&gt;&lt;div&gt;이런식으로 사용됩니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;문제는 매번 테스트를 하기 위해서는 결국 python 프로그램으로 또 검색 테스트를 위한 프로그램을 만들어 사용자 검색어를 같은 bge-m3 모델을 이용해서 벡터로 만들어서, 그것을 select 구문으로 DB에 물어봐야합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;tobgem3 client 확장 모듈&lt;/h2&gt;&lt;div&gt;그래서 함수 하나를 만들기로 했습니다.&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;select * from news_chunks
order by embedding &amp;lt;=&amp;gt; tobgem3vector(&apos;벚꽃 구경 가기 좋은 곳&apos;) limit 5&lt;/pre&gt;&lt;div&gt;이런식으로 tobgem3vector 함수가 하나 있고, 이 함수의 반환값이 기사의 청크를 벡터로 임베딩할 때 사용했던 bge-m3 모델로 인코딩한 벡터면 딱이죠.&lt;br&gt;그래서, 바로 plpython으로 만들었습니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;결과는 처참했습니다.&lt;/div&gt;&lt;div&gt;왜냐하면, 이 함수가 호출될 때 마다 매번 2GB 가까이 되는 모델 파일을 불러오는데 시간이 많이 걸렸습니다. 그래서,&amp;nbsp;tobgem3vector 함수는 bge-m3 모델 인코딩을 담당하는 서버에게 요청을 하고, 서버의 응답을 받아서 사용하는 방식으로, 그리고, 서버는 unix domain socket 으로 서비스해서, 서버가 실행되면 먼저 해당 모델을 불러 놓고, 클라이언트의 요청에 대해서 간단하게 응답하는 방식으로.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 결과도 처참했습니다.&lt;/div&gt;&lt;div&gt;tobgem3vector 함수가 plpython 이다 보니, db 세션이 새로 연결 되고, plpython 모듈을 로딩하는데 비용이 너무 컸습니다. 또한 4kb 응답만 받으면 될 것을 그 벡터를 문자열로 처리해서 다시 이진 자료로 변환하는 비용까지 포함되어 최적화가 필요했습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;최종 모습은&lt;/div&gt;&lt;div&gt;PostgreSQL 데이터베이스 서버가 운영 되고 있는 호스트에 bge-m3 모델을 미리 다운로드 받아 두고, python async 기법의 unix domain socket 통신하고, 반환은 vector 바이너리를 그대로 클라이언트로 보내는 서버를 하나 만들고, tobgem3vector() 함수는 C로 다시 만들었습니다. 해당 코드는 &lt;a href=&quot;https://github.com/i0seph/to_bge_m3_vector&quot;&gt;https://github.com/i0seph/to_bge_m3_vector&lt;/a&gt; 페이지에 공개했습니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;(당연히 모든 코딩은 요즘 유행하는 바이브코딩이었음은 안비밀이긴합니다. - 세상 너무 좋아졌어요!)&lt;br&gt;&lt;br&gt;이 최종 꿈 꿨던 모습은 십여전 만들었던 &lt;a href=&quot;https://github.com/i0seph/textsearch_ko&quot;&gt;textsearch_ko&lt;/a&gt; 와 컨셉이 아주 유사합니다.&lt;/div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;예제로 살펴본 성능&lt;/h2&gt;&lt;pre&gt;postgres=# select id from news_chunks
order by embedding &amp;lt;=&amp;gt; tobgem3vector(&apos;벚꽃 구경 가기 좋은 곳&apos;) limit 5;
   id
---------
 1680103
  729450
 1679422
 2610779
 2023529
(5개 행)

작업시간: 76.396 ms&lt;/pre&gt;&lt;div&gt;실행 계획&lt;/div&gt;&lt;pre&gt;postgres=# explain (analyze, buffers) select id from news_chunks
order by embedding &amp;lt;=&amp;gt;  tobgem3vector(&apos;벚꽃 구경 가기  좋은 곳&apos;) limit 5;

QUERY PLAN&lt;br&gt;--------------------------------------------------
 Limit  (cost=3536.42..3573.39 rows=5 width=12)
        (actual time=0.737..0.760 rows=5.00 loops=1)
   Buffers: shared hit=1003
   -&amp;gt;  Index Scan using news_chunks_embedding_idx on news_chunks  (cost=3536.42..23417699.78 rows=3166389 width=12)
                                                                 (actual time=0.736..0.759 rows=5.00 loops=1)
         Order By: (embedding &amp;lt;=&amp;gt; &apos;[-0.0385437,0.015029907,...중략..., -0.034942627,-0.011772156,0]&apos;::vector)
         Index Searches: 1
         Buffers: shared hit=1003
 Planning:
   Buffers: shared hit=1
 Planning Time: 73.626 ms
 Execution Time: 0.772 ms
(10개 행)

작업시간: 74.814 ms&lt;/pre&gt;&lt;p&gt;노파심에서 하는 이야기&lt;br&gt;이럴 일은 없을 것이라 믿지만,&lt;br&gt;거리값을 알고싶어서&lt;/p&gt;&lt;p&gt;select embedding &amp;lt;=&amp;gt; tobgem3vector() ... from news_chunks&amp;nbsp;&lt;/p&gt;&lt;p&gt;이런식으로 이 함수를 사용하는 사람이 없길 바랍니다. 왜 쓰면 안되는지에 대해서는 너무 이야기가 길어져 생락합니다.&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;h2&gt;vector 인덱스&lt;/h2&gt;&lt;h3&gt;explain에서의 buffers&lt;/h3&gt;&lt;div&gt;자료가 3백만건 가량 되기 때문에, 인덱스 기반 조회를 하지 않고, 자료 전체를 검색해서 서비스하겠다는 것은 아주 위험한 생각입니다. 쿼리 매번 20GB 가량의 자료를 읽겠다는 것이 되기 때문입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;윗 예제에서 보면 하나의 벡터 인덱스 검색에는 평균적으로 1000개 정도의 블록을 읽습니다.&amp;nbsp; 운영 환경에서는 이 숫자가 말하는 의미를 잘 파악해야합니다.&lt;/div&gt;&lt;div&gt;공유 메모리에 모든 인덱스 블록이 미리 복사되어 있다면, 크게 상관 없겠으나, 이게 가능하려면, 공유 메모리가 인덱스 크기만큼(24GB)은 할당 되어야 가능한 이야기가 됩니다. 열악한 운영 환경에서는 이 숫자도 부담되는 숫자이긴합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;통상 btree 인덱스인 경우는 인덱스를 이용한 검색에서는 많아도 100 블록 이상을 잘 읽지 않으며, 인덱스 크기도 커봤자 수GB 수준인데, 이 벡터 인덱스는 지금까지 상황과는 사뭇 다릅니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;뭐, 쿼리가 인덱스를 잘 쓰고 있네, 그럼 서비스하면 되지. 이런 짧은 생각은 위험합니다. 공유 버퍼가 적은 환경이라면, 인덱스 검색을 위해 디스크를 의도하지 않게 많이 읽게 됩니다. SATA 기반 구닥다리 하드디스크에 인덱스가 있다면, 매번 쿼리에 1000블록을 랜덤 억세스 하는 것은 큰 비용입니다. 문제는 btree 인덱스와 달리, 사용자들의 질문에 대한 벡터 자료는 아주 랜덤하기 때문에, 사용자들이 자주쓰는 인덱스 블록은 공유 메모리에 올라와 있겠지, 이런 생각도 위험합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;결국 인덱스를 잘 쓴다고 하더라도, 공유메모리가 적은 환경 (이 글의 예제 상황으로 본다면, 1000블록 이상을 매번 쿼리에서 디스크 읽기를 하는 상황)에서는 결국 인덱스가 저장된 디스크의 읽기 쓰기 속도가 거의 메모리 접근 속도를 비슷해야 인덱스 사용의 효과를 볼 수 있습니다. 결국 지금 하드웨어 환경에서는 NVMe SSD 여야한다는 결론이 나오게됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 부분은 제가 느끼는 pgvector의 태생적 한계로 보입니다.&amp;nbsp;&lt;br&gt;(요즘 누가 운영환경 DB로 SATA 디스크를 쓰니? 라고 하면 할 말은 없습니다. -.-)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;hnsw vs ivfflat&lt;/h3&gt;&lt;div&gt;pgvector 모듈 관련 문서를 읽다 보면, 인덱스를 hnsw 형 인덱스와, ivfflat 형 인덱스를 만들 수 있다고 하는데, 이 부분에 대한 심도 있는 글들은 이번 이야기의 범위를 벗어나 생략합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;2025년 pgday seoul 행사에서 이 부분에 대한 재미난 내용이 발표되었는데, 그것을 참조하면 될 것 같습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;단지 이 글에서 추가하고싶은 이야기는 그 말 많은 hnsw 인덱스도 NVMe SSD 환경에서는 충분히 쓸만하다. 더 깊게 생각하는 것은 정신 건강에 도움이 안된다. 이 정도.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;나오며&lt;/h2&gt;&lt;div&gt;결론은 이 긴 글을 Gemini에게 보내서 글의 결론 부분을 작성해 달라고 요청한 것을 대로 인용하고, 앞으로 더 살펴볼 것들에 대해서 추가하고 마칩니다.&amp;nbsp;&lt;br&gt;(즉, 아주 구닥다리 방식으로 이 긴글은 타이핑 했다는 말입니다. 흑흑. 이런 것도 내 옆에서 계속 보고 있다가, &apos;다 했다. 멋진 문서 만들어줘!&apos; 이러면 딱 만들어주면 정말 좋을텐데.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;hr&gt;&lt;div _ngcontent-ng-c2373463799=&quot;&quot; class=&quot;markdown markdown-main-panel stronger enable-updated-hr-color&quot; style=&quot;--animation-duration: 400ms; --fade-animation-function: linear; font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot; id=&quot;model-response-message-contentr_e8cb7b07f4a1ad1b&quot; aria-live=&quot;polite&quot; aria-busy=&quot;false&quot; dir=&quot;ltr&quot;&gt;&lt;p data-path-to-node=&quot;3&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;이번 &lt;code data-path-to-node=&quot;3&quot; data-index-in-node=&quot;3&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;tobgem3_client&lt;/code&gt; 확장 모듈 개발 과정을 통해 PostgreSQL 환경에서 고성능 의미 기반 검색(Semantic Search)을 구현하기 위한 실질적인 해법을 확인했습니다. 단순히 &lt;code data-path-to-node=&quot;3&quot; data-index-in-node=&quot;110&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;pgvector&lt;/code&gt;를 설치하는 수준을 넘어, 실제 대규모 말뭉치(120만 건의 기사, 300만 개의 청크)를 다루며 얻은 주요 교훈은 다음과 같습니다.&lt;/p&gt;&lt;ol start=&quot;1&quot; data-path-to-node=&quot;4&quot; style=&quot;padding-inline-start: 32px; font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;li style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;4,0,0&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;4,0,0&quot; data-index-in-node=&quot;0&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;임베딩 모델 관리의 최적화&lt;/b&gt;: 2GB에 달하는 BGE-M3 모델을 데이터베이스 세션마다 로딩하는 것은 성능상 불가능에 가깝습니다. 이를 별도의 유닉스 도메인 소켓 서버로 분리하고, C 언어로 작성된 클라이언트 확장 모듈을 통해 통신하는 구조는 지연 시간(Latency)을 획기적으로 줄이는 핵심 전략이 되었습니다.&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;4,1,0&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;4,1,0&quot; data-index-in-node=&quot;0&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;인덱스와 하드웨어의 상관관계&lt;/b&gt;: HNSW 인덱스는 강력하지만, 데이터 규모가 커질수록 공유 버퍼와 디스크 I/O 성능에 매우 민감해집니다. 대규모 벡터 데이터를 실시간으로 조회하기 위해서는 NVMe SSD와 같은 고성능 스토리지와 충분한 메모리 할당이 뒷받침되어야 인덱스의 진가를 발휘할 수 있습니다.&lt;/p&gt;&lt;/li&gt;&lt;li style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;p data-path-to-node=&quot;4,2,0&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;&lt;b data-path-to-node=&quot;4,2,0&quot; data-index-in-node=&quot;0&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;PostgreSQL의 확장성 확인&lt;/b&gt;: 기존의 관계형 데이터와 벡터 데이터를 하나의 SQL 쿼리(&lt;code data-path-to-node=&quot;4,2,0&quot; data-index-in-node=&quot;52&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;tobgem3vector&lt;/code&gt;) 내에서 자연스럽게 결합할 수 있다는 점은, PostgreSQL이 AI 시대의 통합 데이터 플랫폼으로서 충분한 경쟁력을 갖추고 있음을 증명합니다.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p data-path-to-node=&quot;5&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;앞으로는 검색 성능을 한 단계 더 높이기 위한 인덱스 파라미터 튜닝뿐만 아니라, RAG(검색 증강 생성) 시스템의 완성도를 높이기 위한 하이브리드 검색(Full-text Search + Vector Search) 도입 등을 추가로 검토해볼 계획입니다. 십여 년 전 &lt;code data-path-to-node=&quot;5&quot; data-index-in-node=&quot;148&quot; style=&quot;font-family: Google Sans Text, sans-serif !important; line-height: 1.15 !important; margin-top: 0px !important;&quot;&gt;textsearch_ko&lt;/code&gt;를 만들었을 때와 마찬가지로, 이번 모듈이 PostgreSQL을 활용해 AI 서비스를 구축하려는 분들에게 실질적인 도움이 되기를 바랍니다.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;</description>
            <pubDate>Tue, 31 Mar 2026 17:29:09 +0900</pubDate>
            <guid>http://postgresql.kr/blog/tobgem3_client.html</guid>
        </item>
        <item>
            <title>pg_createsubscriber 써 논리 복제 구독 서버 만들기</title>
            <link>http://postgresql.kr/blog/pg_createsubscriber.html</link>
            <description>&lt;h1&gt;pg_createsubscriber 써 논리 복제 구독 서버 만들기&lt;/h1&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;운영중인 용량이 큰 데이터베이스 전체를 논리 복제로 구축하는 일이 까다로웠습니다.&lt;/li&gt;&lt;ul&gt;&lt;li&gt;단순하게 그냥 깡통 테이블을 만들고, 원본 테이블에서 모든 데이터를 테이블 별로 다 가져 오든가,&amp;nbsp;&lt;/li&gt;&lt;li&gt;발행 쪽에서 논리 복제 슬롯을 만들고, 만들 때 알려준 그 lsn 까지 지정 시점 복구(Point in time recovery)를 한 뒤 구독을 copy_data 옵션을 끄고 만들든가&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;17버전에서 pg_createsubscriber 프로그램이 새로 나왔습니다.&lt;/li&gt;&lt;ul&gt;&lt;li&gt;사용법은 물리 복제(replica) 노드를 만들어 primary 쪽과 계속 동기화 하고 있다가&lt;/li&gt;&lt;li&gt;replica를 중지하고,&lt;/li&gt;&lt;li&gt;pg_createsubscriber 명령을 이용해서 지정 데이터베이스 전체를 논리 복제 환경으로 바꿉니다. 참 쉽죠.&lt;/li&gt;&lt;li&gt;이 작업이 끝나면, replica 노드는 promote 작업을 해서 독립된 인스턴스가 되됩니다.&lt;/li&gt;&lt;li&gt;해당 논리복제 구독 인스턴스를 실행하면 그때부터 pg_createsubscriber 때부터 구독 인스턴스가 실행될 때까지 변경된 내용만 가져와서 논리복제 환경을 만듭니다.&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;pg_createsubscriber -D replica의_PGDATA -P 기존replica의_primary_conninfo -d 논리복제로_사용할_데이터베이스이름&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;</description>
            <pubDate>Fri, 05 Sep 2025 16:38:36 +0900</pubDate>
            <guid>http://postgresql.kr/blog/pg_createsubscriber.html</guid>
        </item>
        <item>
            <title>PostgreSQL 읽기 전용 부하 분산에 대해서</title>
            <link>http://postgresql.kr/blog/postgresql_readonly_load_balancing.html</link>
            <description>PostgreSQL 읽기 전용 부하 분산에 대해서&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;1. 읽기 전용 부하 분산을 위한 서버 환경 매개 변수 설정&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;hot_standby = on&lt;/div&gt;&lt;div&gt;# 트랜잭션 로그를 전달 받아, 그것을 재실행하는 과정 중에도 해당 인스턴스에 접속해서 읽기 전용 쿼리를 사용할 수 있도록 하는 옵션&lt;br&gt;# 기본값이 on 이기 때문에 따로 변경할 일은 없다&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;2.&amp;nbsp;libpq의 load_balance_hosts=random&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;3.&amp;nbsp;jdbc의 loadBalanceHosts=true&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;4. 부하 분산을 위한 프록시 이용&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;5. 응용 프로그램이 연결 풀을 쓰는 경우&lt;/div&gt;</description>
            <pubDate>Fri, 11 Jul 2025 16:21:53 +0900</pubDate>
            <guid>http://postgresql.kr/blog/postgresql_readonly_load_balancing.html</guid>
        </item>
        <item>
            <title>patroni로 구현하는 PostgreSQL 고가용성</title>
            <link>http://postgresql.kr/blog/patroni.html</link>
            <description>&lt;h1&gt;patroni로 구현하는 PostgreSQL 고가용성&lt;/h1&gt;&lt;h2&gt;들어가며&lt;/h2&gt;&lt;div&gt;PostgreSQL 가용성을 높이는 방법은 단지 데이터베이스 차원에서만 구현 할 수 없다.&lt;br&gt;(가용성 = auto failover로 운영 DB 접속이 안될 때, 대기 서버가 운영 DB로 그 역할을 대신 하는 것&amp;nbsp; + 부하분산 - PostgreSQL은 읽기 전용 서버에 대해서만 부하분산이 가능하다)&lt;/div&gt;&lt;div&gt;어떤 서비스의 가용성을 높이는 것은 결국 그 서비스를 제공하고 있는 호스트 문제와도 연결되며, 더 나아가 그 호스트와 연결되어있는 네트워크 장비와도 연결되어있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그래서, PostgreSQL 개발 그룹에서는 이 가용성 문제는 데이터베이스 영역을 벗어나기 때문에 다루지는 않는다.&lt;/div&gt;&lt;div&gt;지금까지도 그렇다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 가용성 문제를 푸는 가장 보편적인 방법은 OS 클러스터링 솔루션을 이용하는 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;오픈소스로 대표적인 것은&amp;nbsp;Pacemaker이다. 하지만, pacemaker 구축과 관리 비용이 상당히 비싸다. (배우기 어렵다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여러개의 읽기 전용 복제 서버를 만들고, 그것들을 이용해서 읽기 전용 작업에 대한 부하 분산을 하는 것은 오래전부터 표준화 되었기 때문에 따로 언급하지는 않겠지만,&amp;nbsp;&lt;/div&gt;&lt;div&gt;읽기-쓰기 인스턴스(Primary, Master, Leader 같은 용어로 지칭한다.)의 auto failover, 더 나아가 편한 switchover (안정적인 상태에서 다른 호스트로 읽기-쓰기 인스턴스를 옮기는 일) 기능을 제공하는 것에는 아직까지는 표준화 된 것이 없다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;전통적인 방법으로는 공유 디스크를 이용해서 Active 호스트가 문제가 있을 때, Standby 호스트가 Active 호스트에서 사용하던 데이터베이스 자료 디스크를 마운트해서 데이터베이스 인스턴스를 실행하는 방법이 이용되었다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 방법은 failover, switchover 작업 비용이 거의 없다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;해당 공유 디스크가 물리적으로 문제만 없다면, 자료 손실도 없다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;하지만, IT 인프라 환경이 클라우드 기반으로 옮겨가고, 이 공유 디스크를 사용할 수 없는 환경에서 failover 문제를 풀어야 한다면, 물리적인 스트리밍 복제 기능을 이용해서 읽기 전용 인스턴스를 읽기-쓰기 인스턴스로 바꿔 그것을 운영 디비로 쓰는 방식을 사용할 수 밖에 없다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글은 이부분에 관한 이야기다.&lt;/div&gt;&lt;div&gt;patroni 프로그램이 auto failover와 switchover를 담당하고, 그 내부 작업에 대해서는 운영자가 신경을 최대한 안쓰게 하기 위함이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;또한 OS 클러스터 솔루션과 달리 네트워크 환경, OS 환경은 전혀 고려하지 않고, 데이터베이스 서비스에만 집중에서 auto failover를 구현한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(그래서, pacemaker 구축 보다 쉽다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 이야기의 시작은 EnterpriseDB사에서 소개하고 있는 patroni 이야기다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;해당 글은&lt;/div&gt;&lt;div&gt;https://www.enterprisedb.com/docs/supported-open-source/patroni/rhel8_quick_start/&lt;/div&gt;&lt;div&gt;페이지에서 읽을 수 있다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;구성도&lt;/h2&gt;&lt;div&gt;&lt;img src=&quot;/images/patroni.drawio.png&quot;&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;(draw.io 이용)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;필요한 것들&lt;/h2&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;Rocky Linux 8 Virtual Machine 3대 (쌍방간 네트워크 가능한 상태)&lt;/li&gt;&lt;li&gt;각 호스트는 hostname -s 값으로 서로 통신 가능해야함 /etc/hosts 있든 dns에 등록되든&amp;nbsp;&lt;/li&gt;&lt;li&gt;etcd :&amp;nbsp;https://etcd.io/&lt;/li&gt;&lt;li&gt;patroni :&amp;nbsp;https://github.com/patroni/patroni&lt;/li&gt;&lt;li&gt;postgresql : https://postgresql.org/&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;etcd&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;https://download.postgresql.org/pub/repos/yum/common/pgdg-rhel8-extras/redhat/&lt;/div&gt;&lt;div&gt;에서 해당 플랫폼에 맞게 rpm 파일을 받아서 그냥 설치했다.&lt;/div&gt;&lt;div&gt;다른 패키지 의존성이 전혀 없는 프로그램이기 때문에, 서비스 설정 관련을 편하게 하기 위해서 rpm 파일을 이용했다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;patroni&lt;/h3&gt;&lt;div&gt;python 가상환경에서 pip으로 설치했다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;patroni 부터는 postgres 계정에서 전적으로 관리하기 때문에,&amp;nbsp;&lt;/div&gt;&lt;div&gt;OS root와 관계 없이 실행될 수 있도록 했다.&lt;/div&gt;&lt;div&gt;root 권한으로 필요한 것은 python3 패키지를 설치하는 것 뿐.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;su - postgres
mkdir patroni
cd patroni
python3 -m venv .
pip install patroni[etcd,psycopg3]
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;pip install 작업이 성공하려면, pip build 작업을 할 수 있는 각종 개발 패키지들이 필요하다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;작업 과정에서 오류 메시지를 잘 읽어보고 몇번의 시행착오만 거치면 쉽게 마칠 수 있다.&lt;/div&gt;&lt;div&gt;pip 명령을 사용할 수 없는 패쇄망 환경이라면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;인터넷 망에서 윗 작업을 하고,&amp;nbsp;&lt;/div&gt;&lt;div&gt;/home/postgres/patroni 디렉토리만 패쇄망으로 옮겨 같은 위치에서 사용하면 될 것이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;PostgreSQL&lt;/h3&gt;&lt;div&gt;설치 관련해서는 설명 생략.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;etcd 구성&lt;/h2&gt;&lt;div&gt;etcd 프로그램은 key-value 멀티마스터 데이터베이스다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;최소 3대로 묶여야 클러스터링 환경에서 그들 간의 auto failover를 진행하기 때문에,&amp;nbsp;&lt;/div&gt;&lt;div&gt;번거롭지만 각 호스트 마다 설치를 하고, 설정하고, 하나의 클러스터로 묶는다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 데이터베이스에는 patroni에서 사용하는 각종 PostgreSQL 클러스터링 정보를 저장한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;방화벽 포트&lt;/h3&gt;&lt;div&gt;이러기 위해서는 당연히 각 호스트에서 etcd가 사용하는 포트가 열려있어야한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;OS 방화벽이나, 기타 네트워크 방화벽 설정이 되어있다면, 사전에 2379, 2380 포트 통신이 가능한 상태로 만들어야 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(이런 작업을 해야할 상황이라면, 추가로&amp;nbsp; patroni가 쓰는 8008, postgresql이 쓰는 5432 포트도 함께 여는 작업도 한다.)&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;etcd 서비스 설정&lt;/h3&gt;&lt;div&gt;클러스터로 묶어둔 etcd 세트가 망가지면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;patroni가 postgresql 클러스터 재구성 작업을 하다가 실패하면서 멀쩡히 잘 쓰고 있는 PostgreSQL 인스턴스가 중지될 수 있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그래서, etcd 프로그램은 OS 서비스로 등록하고, 중지되면 무조건 재실행한다는 설정까지 필요하다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;운영환경에서는 최악의 경우라도 두개의 etcd 인스턴스는 정상 실행되어 클러스터링 되고, patroni가&amp;nbsp; etcd를 통해서 postgresql 클러스터 정보를 받을 수 있도록 운영 되어야 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;rpm파일로 설치하고 나면&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;/usr/lib/systemd/system/etcd.service 파일에서&amp;nbsp; Restart 설정을 always로 바꾼다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Restart=always&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음 서비스 변경분을 반영하고, etcd 서비스를 활성화 한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;systemctl daemon-reload &amp;amp;&amp;amp; systemctl enable etcd&lt;/pre&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음 /etc/etcd/etcd.conf 파일을 다음과 같이 만든다.&lt;/div&gt;&lt;div&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;#[Member]
# LISTEN 관련 설정은 포트 바인딩 이기 때문에 반드시 IP로 지정되어야 한다.
ETCD_LISTEN_PEER_URLS=&quot;http://172.30.1.161:2380&quot;
ETCD_LISTEN_CLIENT_URLS=&quot;http://localhost:2379,http://172.30.1.161:2379&quot;
ETCD_NAME=&quot;vm1&quot;
#[Clustering]
#여기서부터는 바인딩과 관계없는 호스트 정보들임으로 호스트 이름으로 설정해도 괜찮다.
ETCD_INITIAL_ADVERTISE_PEER_URLS=&quot;http://vm1:2380&quot;
ETCD_INITIAL_CLUSTER=&quot;vm1=http://vm1:2380,vm2=http://vm2:2380,vm3=http://vm3:2380&quot;
ETCD_ADVERTISE_CLIENT_URLS=&quot;http://vm1:2379&quot;
ETCD_INITIAL_CLUSTER_TOKEN=&quot;etcd-cluster-1&quot;
ETCD_INITIAL_CLUSTER_STATE=&quot;new&quot;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;IP 주소는 모두 적당히 바꿔야 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;위의 예제는 vm1의 경우이기 때문에, vm1이라는 글자와, 172.30.1.161&amp;nbsp; IP 주소가 사용되었다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;vm2, vm3 도 적당히 알맞게 바꿔야 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;ETCD_INITIAL_CLUSTER 설정은 세 호스트 모두 같다. 규칙은 etcd 인스턴스이름(ETCD_NAME)과 해당 IP로 구성된다.&lt;/div&gt;&lt;div&gt;준비가 다 되었으면, 세 호스트 모두 etcd 서비스를 실행한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;systemctl start etcd&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 3 호스트 모두 etcd 설정을 마치고 서비스를 시작했다면,&lt;/div&gt;&lt;div&gt;etcdctl member list 명령 결과로&amp;nbsp; 3 줄 목록이 나와야한다.&lt;/div&gt;&lt;div&gt;ETCD_INITIAL_CLUSTER 에서 지정한 각 호스트들이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;root@vm1:~# etcdctl member list
7b6a22d036981d50, started, vm2, http://172.30.1.162:2380, http://vm2:2379, false
a2cb95237c4b2bf3, started, vm1, http://172.30.1.161:2380, http://vm1:2379, false
a9cfb614665e6b05, started, vm3, http://172.30.1.163:2380, http://vm3:2379, false&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;watchdog 설정&lt;/h2&gt;&lt;div&gt;patroni 에서 /dev/watchdog 장치를 사용하면 좀 더 OS 장애 상황을 빠르게 파악한다고 해서 설정하기를 권장한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 부분은 EnterpriseDB사에서 소개하고 있는 글을 그대로 따랐다.&lt;/div&gt;&lt;div&gt;kernel 모듈을 로딩하고, 파일 소유주를 바꾸는 작업을 OS차원에서 자동화하는 작업이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;pre&gt;[root@vm1 ~]# cat /etc/udev/rules.d/99-watchdog.rules
KERNEL==&quot;watchdog&quot;, OWNER=&quot;postgres&quot;, GROUP=&quot;postgres&quot;
[root@vm4 ~]# cat /etc/modules-load.d/softdog.conf
softdog&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;최종 모습은 OS가 리부팅 되고 난 뒤에도 항상&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;ls -l /dev/watchdog
crw-rw----. 1 postgres postgres 10, 130  1월 28 13:41 /dev/watchdog&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이런 모습이어야 한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;patroni 실행&lt;/h2&gt;&lt;div&gt;postroni 프로그램은 postgresql 인스턴스를 자기 상황에 알맞게 primary 인스턴스로 실행할 것인지, 읽기 전용 복제 인스턴스로 실행할 것인지를 판단해서 postgresql 인스턴스를 실행하는 프로그램이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;또한 postgresql 인스턴스를 모니터링 하고 있다가 해당 인스턴스가 문제가 생기면 etcd 쪽으로 인스턴스 상황을 보고해서, auto failover (다른 복제 인스턴스가 promote 작업을 해서 주서버(primary server)로 변경하는 일)을 하는 것이 주 목적이다.&lt;/div&gt;&lt;div&gt;또한 자신이 관리하는 postgresql을 새롭게 실행해야 하는 상황에서 복제 서버로 실행되어야 하는 경우 - 이미 주서버가 잘 작동되고 있는 상황 - pg_rewind 명령을 이용해서 최대한 빠르게 복제 서버를 구축해서 실행하는 일도 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, patroni 프로그램을 사용하게 되면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;DB 인스턴스는 pg_ctl 명령이 아니라,&amp;nbsp;&lt;/div&gt;&lt;div&gt;patroni 명령을 실행해서 그 프로그램이 DB 인스턴스를 성격에 알맞게 실행하게 하고,&amp;nbsp;&lt;/div&gt;&lt;div&gt;patronictl 명령으로 DB인스턴스의 모니터링과 관리자가 임의로 각 인스턴스를 자격 변경을 하게된다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;patroni, patronictl 명령을 사용하기 위해서는,&lt;/div&gt;&lt;div&gt;이 글과 같은 구성 환경에서는 postgres 계정 대상으로&lt;/div&gt;&lt;div&gt;몇가지 OS 환경 변수 설정과 patroni 환경 구성 파일을 미리 지정해야 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 작업은 3 호스트 모두 해야 하며, 각 호스트에 맞게 설정을 바꿔야 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;다른 클러스링 도구들과 달리 설정을 각각 해야 하는 것이 조금은 성가시다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(etcd도, patroni도 다 각 호스트에 맞게 설정해야 한다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;.bash_profile 내용&amp;nbsp;&lt;/h3&gt;&lt;div&gt;&lt;pre&gt;export PGDATA=/data/demo
export PATH=/postgres/16/bin:$PATH
export PATRONICTL_CONFIG_FILE=/home/postgres/patroni.yml
. $HOME/patroni/bin/activate&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;윗 설정에서 지정했듯이, /home/postgres/patroni.yml 파일은 다음과 같다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;patroni.yml 내용&amp;nbsp;&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;scope: demo-cluster #클러스터이름
namespace: /db/
name: vm1 #인스턴스 노드 이름(호스트이름과 동일)

# patroni 내부 각종 정보 조회및 조작 서버 설정 각 호스트별로 자신의 호스트를 사용한다.
restapi:
  listen: &quot;0.0.0.0:8008&quot;
  connect_address: &quot;172.30.1.161:8008&quot;
  authentication:
    username: patroni
    password: mySupeSecretPassword

# etcd 클러스터로 묶여 있는 모든 호스트 정보를 나열 
# (호스트 이름이 ip로 바뀔 수 있는 경우는 아래처럼 호스트 이름으로
# 그렇지 않으면, 각 IP 주소로 지정)
etcd3:
  hosts:
  - vm1:2379
  - vm2:2379
  - vm3:2379

# postgresql 인스턴스를 실행하려고 했는데, 해당 인스턴스가 전혀 준비가 안된 상태라면
# 아에 처음부터 시작하는데, 그 설정
bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        archive_mode: &quot;on&quot;
        archive_command: &quot;/bin/true&quot;

  initdb:
  - encoding: UTF8
  - lc-collate: C
  - lc-ctype: ko_KR.utf8
  - data-checksums
  - auth-local: peer
  - auth-host: scram-sha-256

  pg_hba:
  - host replication replicator 0.0.0.0/0 scram-sha-256
  - host all all 0.0.0.0/0 scram-sha-256

# PostgreSQL 실행할 때의 환경 설정
postgresql:
  listen: &quot;0.0.0.0:5432&quot;
  connect_address: &quot;172.30.1.161:5432&quot;
  data_dir: /data/demo
  bin_dir: /postgres/16/bin
  bin_name:
    postgres: /postgres/16/bin/postgres
  pgpass: /home/postgres/patroni-pgpass
  authentication:
    replication:
      username: replicator
      password: confidential
    superuser:
      username: postgres
      password: my-super-password
    rewind:
      username: rewind_user
      password: rewind_password
  parameters:
    unix_socket_directories: &quot;/tmp&quot;
    timezone: &quot;Asia/Seoul&quot;
    log_timezone: &quot;Asia/Seoul&quot;
    patroni.member: &quot;vm1&quot;
# 추가로 가상 IP 설정을 테스트 하기위해 임의 스크립트를 추가했음
  callbacks:
    on_role_change: &quot;/home/postgres/on_role_change.sh&quot;

watchdog:
  mode: required
  device: /dev/watchdog
  safety_margin: 5

tags:
  nofailover: false
  noloadbalance: false
  clonefrom: false
  nosync: false
&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;patroni 프로그램을 실행할 때는 해당 설정 파일을 지정해야한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;patroni patroni.yml &amp;gt; patroni.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 작업을 OS가 부팅 되고 난 뒤에 자동으로 실행되기를 원한다면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;patroni 실행 작업을 서비스로 등록해 두면 된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(각 OS에서 사용자 정의 서비스 등록에 대한 이야기는 여기서는 생략한다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;patroni는 Pacemaker 같은 OS 클러스터 관리 도구가 아니다!&lt;/div&gt;&lt;div&gt;오직 PostgreSQL 인스턴스 관리에만 촛점이 맞춰있어,&lt;/div&gt;&lt;div&gt;Virtual IP를 주서버(Primary Server) 기준으로 설정하고,&lt;/div&gt;&lt;div&gt;failover가 발생하면, 가상 IP를 바꾸고 하는 것은 patroni 기본 기능이 아니다.&lt;/div&gt;&lt;div&gt;그래서, OS 관리자와&amp;nbsp; DB 관리자가 분리되어 있는 운영 환경이라면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;patroni 관련 운영은 DB 관리자가 전적으로 맡는 것이 좋을 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;여기서는 이 정책에 맞춰, 실행도, 모니터링도, 로그 분석도 postgres 계정으로 모두 할 수 있도록 구성했다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;patronictl 과 모니터링&lt;/h2&gt;&lt;div&gt;데이터베이스 인스턴스는 두 대만 있어도 되지만,&amp;nbsp;&lt;/div&gt;&lt;div&gt;etcd 서비스를 어차피 3 대로 하기에 vm1, vm2, vm3 모두 같은 환경의 데이터베이스를 운영하는 것으로 테스트했다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;또한 아래에서도 이야기하지만,&amp;nbsp;&lt;/div&gt;&lt;div&gt;auto failover 나 switchover 가 끝난 뒤, 옛날 주서버를 새로운 주서버의 리플리카를 만드는 사이에 또 failover 나 switchover가 일어날 경우 예상치 못한 클러스터 전면 재구축 작업을 해야하는 불상사를 막기 위함이기도 하다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;트랜잭션 로그를 스트리밍 기법으로 전달하고, 그것을 복제 인스턴스에서 그대로 반복 반영해서 복제 인스턴스를 관리하는 기법은 이 주서버와 복제 서버간 동기화 기법이 비동기식(async)인 경우는 운영환경에서는 두 대의 복제 인스턴스를 두는 것이 안전한 것 같다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(동기식 복제라면 복제 서버가 하나여도 최악의 사태는 막을 수 있을 것 같다. 이 부분은 테스트를 해보질 못했다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;모든 작업이 끝나면,&amp;nbsp; patronictl 명령으로 클러스터 상태를 살펴본다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;(patroni) postgres@vm1:~$ patronictl list
+ Cluster: demo-cluster   (7464125923163850844) ---+-----------+
| Member | Host         | Role    | State     | TL | Lag in MB |
+--------+--------------+---------+-----------+----+-----------+
| vm1    | 172.30.1.161 | Replica | streaming | 84 |         0 |
| vm2    | 172.30.1.162 | Leader  | running   | 84 |           |
| vm3    | 172.30.1.163 | Replica | streaming | 84 |         0 |
+--------+--------------+---------+-----------+----+-----------+&lt;/pre&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Replica들은 모두 상태가 &apos;streaming&apos; 상태여야하고, Leader는 &apos;running&apos; 상태여야한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이런 상황이 아니면 비정상 상태다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;리플리카가 &apos;running&apos; 상태로 계속 되고 있는 경우는&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;완전 재초기화가 필요해서 아주 오랫동안 리플리카를 구축하고 있거나,&lt;/li&gt;&lt;li&gt;스트리밍 복제 쪽에 문제가 있는 상황이다.&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;스트리밍 복제 문제가 있는 경우는 가장 손쉽게 푸는 방법은 해당 member 를 patronictl reinit 명령으로 재초기화하는 것이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;모니터링은 이것이 전부다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;문제는 이런 스트리밍 복제 쪽에 문제가 생긴 상황을 방치해 두면, PostgreSQL 스트리밍 복제 환경에서 발생하는 다양한 문제를 그대로 직면하게 될 수 있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;예를 들어, 위 list&amp;nbsp; 결과로 3개의 인스턴스가 나오고, 리플리카가 2 인스턴스라면, 주서버에는 2개의 복제 슬롯이 만들어진다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;윗 예제 기준으로 vm3 호스트를 아에 shutdown 해서 더이상 사용하지 않는다고 했을 때,&amp;nbsp;&lt;/div&gt;&lt;div&gt;patronictl list 에서&amp;nbsp; vm3 관련 정보가 사라질 때까지 vm3 관련 복제 슬롯은 주서버에서 사라지지 않는다.&amp;nbsp; (정말 중요한 이야기다!)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;앞에서 이야기했듯이, 리플리카인데, streaming 상태를 유지하지 않는 것이 보인다면 최대한 빠른 시간 안에 해당 문제를 풀어야한다. 그렇지 않으면, 복제 슬롯 때문에 트랜잭션 로그 적체가 일어나고, 주서버 인스턴스쪽 디스크 공간 관리에 문제가 발생할 수 있다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;아무 생각 없이 그냥 방치 해 둔다고 알아서 모든 상황을 patroni가 클러스터 각 DB 인스턴스들을 잘 관리하는 것은 아니다!&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;patronictl reinit&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;(patroni) postgres@vm2:~$ patronictl topology
+ Cluster: demo-cluster (7464125923163850844) ----------+
| Member | Host | Role    | State     |  TL | Lag in MB |
+--------+------+---------+-----------+-----+-----------+
| vm3    | vm3  | Leader  | running   | 169 |           |
| + vm1  | vm1  | Replica | streaming | 169 |         0 |
| + vm2  | vm2  | Replica | running   | 169 |        16 |
+--------+------+---------+-----------+-----+-----------+&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;patroni가 관리하는 클러스터의 해당 인스턴스 계층 구조를 본 화면이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;앞에서 설명한 Replica 인스턴스인데, streaming 상태가 아닌 vm2 인스턴스가 발견되었다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;vm2 인스턴스가 리플리카로 정상적으로 실행되고 있지 못하는 상태를 뜻한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;vm2 인스턴스를 로그를 보면,&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;2025-02-01 22:07:26.554 KST [727] LOG:  started streaming WAL from primary at 0/9D000000 on timeline 169
2025-02-01 22:07:26.554 KST [727] FATAL:  could not receive data from WAL stream: ERROR:  requested WAL segment 000000A9000000000000009D has already been removed
2025-02-01 22:07:26.554 KST [515] LOG:  waiting for WAL to become available at 0/9D0002E8&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이런 로그가 계속 찍히고 있다. 물리 복제 오류 메시지 가운데 흔히 보는 메시지다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;주서버에 000000A9000000000000009D 트랜잭션 로그 조각 파일이 이미 지워져 버려 vm2 리플리카를 못하고 있는 상황이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이때 이것을 방치하면, vm3(주서버)의 vm2용 복제 슬롯이 활성화 되어 있기 때문에, vm3 쪽에서 자료 변경 작업이 계속 일어난다면, vm3의 pg_wal 디렉터리의 여유 공간은 점점 줄어들게 될 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;해결 방법은&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;vm2의 patroni 프로세스를 중지해서 아에 vm2 인스턴스를 사용하지 않거나,&amp;nbsp;&lt;/li&gt;&lt;li&gt;vm2 인스턴스를 새로 만들어야 한다.&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;새로 만들 때 사용하는 명령이 reinit 명령이다.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;(patroni) postgres@vm2:~$ patronictl reinit --help
Usage: patronictl reinit [OPTIONS] CLUSTER_NAME [MEMBER_NAMES]...

  Reinitialize cluster member

Options:
  --group INTEGER  Citus group
  --force          Do not ask for confirmation at any point
  --wait           Wait until reinitialization completes
  --help           Show this message and exit.
(patroni) postgres@vm2:~$ patronictl reinit --wait demo-cluster vm2
+ Cluster: demo-cluster (7464125923163850844) ----------+
| Member | Host | Role    | State     |  TL | Lag in MB |
+--------+------+---------+-----------+-----+-----------+
| vm1    | vm1  | Replica | streaming | 169 |         0 |
| vm2    | vm2  | Replica | running   | 169 |        16 |
| vm3    | vm3  | Leader  | running   | 169 |           |
+--------+------+---------+-----------+-----+-----------+
Are you sure you want to reinitialize members vm2? [y/N]: y
Success: reinitialize for member vm2
Waiting for reinitialize to complete on: vm2
Reinitialize is completed on: vm2
(patroni) postgres@vm2:~$ patronictl topology
+ Cluster: demo-cluster (7464125923163850844) ----------+
| Member | Host | Role    | State     |  TL | Lag in MB |
+--------+------+---------+-----------+-----+-----------+
| vm3    | vm3  | Leader  | running   | 169 |           |
| + vm1  | vm1  | Replica | streaming | 169 |         0 |
| + vm2  | vm2  | Replica | streaming | 169 |         0 |
+--------+------+---------+-----------+-----+-----------+&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;테스트&lt;/h2&gt;&lt;div&gt;failover 관련 테스트는 전통적으로 failover가 자동으로 발생되어야 하는 모든 가능성을 테스트하는 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;앞에서 이야기했듯이 이 failover는 전통적인 OS 클러스터 솔루션이 담당했던 Virtual IP 전환에 대한 부분은 빠져있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;호스트가 power off 되었을 때&lt;/li&gt;&lt;li&gt;호스트가 멈췄을 때, hang on 또는 stuck 이라고는 하는 현상&lt;/li&gt;&lt;li&gt;호스트의 과부화&lt;/li&gt;&lt;li&gt;etcd 데몬 프로세스 문제&lt;/li&gt;&lt;li&gt;patroni 데몬 프로세스 문제&lt;/li&gt;&lt;li&gt;postgres 데몬 프로세스 문제&lt;/li&gt;&lt;li&gt;네트워크 인터페이스 down&amp;nbsp;&lt;/li&gt;&lt;li&gt;...&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;이런 식으로 모든 가능성을 나열하고 하나씩 테스트 한다.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;지금까지 테스트 한 것으로는 etcd 클러스터가 망가졌을 때가 가장 치명적이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;최악의 사태를 대비해서 etcd 자료 백업 복구 연습을 충분히 두어야 할 것 같다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;failover를 고려한 클라이언트&lt;/h3&gt;&lt;div&gt;자동 failover는 서비스 연속성 분야에서 아주 중요한 부분이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;virtual ip나, haproxy 나, pgbouncer, pgpool 같은 추가 도구나 설정 없이, etcd + patroni 만으로 서비스 연속성을 보장하려면, 이 postgresql 데이터베이스를 쓰는 클라이언트에서 추가 작업이 필요하다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h4&gt;libpq 기반 클라이언트들&lt;/h4&gt;&lt;div&gt;psql이나, pg_dump, 같은 기본 PostgreSQL 클라이언트 도구들이이나, python 프로그래밍에서 사용하는 psycopg 라이브러리를 사용하는 클라이언트들을 말한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기서는 데이터베이스 서버 연결 문자열을 사용할때, target_session_attrs=read-write 설정을 추가하면 지정한 여러 인스턴스 가운데, 주서버로 자동으로 접속하는 기능을 제공한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;(patroni) postgres@vm1:~$ psql &quot;host=vm1,vm2,vm3 target_session_attrs=read-write&quot;

postgres=# show patroni.member;
 patroni.member
----------------
 vm3
(1개 행)&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;윗 설정은 항상 read-write 인스턴스로만 접속하는 것이고, 반대로 read-only (윗 경우라면,&amp;nbsp; vm1, vm2) 인스턴스를 임의로 선택해서 접속하려면,&amp;nbsp;target_session_attrs=read-only load_balance_hosts=random 설정을 추가하면 된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;서비스 연속성 테스트&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;테이블 하나 만들기&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# \d t
                                     &quot;public.t&quot; 테이블
 필드명 |            형태             | 정렬규칙 | NULL허용 |            초기값
--------+-----------------------------+----------+----------+------------------------------
 a      | timestamp without time zone |          |          |
 b      | integer                     |          | not null | nextval(&apos;t_b_seq&apos;::regclass)
 c      | text                        |          |          |
인덱스들:
    &quot;t_pkey&quot; PRIMARY KEY, btree (b)&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;read-write 연속성 테스트&lt;/div&gt;&lt;div&gt;1초 간격으로 계속 insert 작업하기&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;(patroni) postgres@vm1:~/test$ cat psql_insert.sh
while true
do
psql &quot;host=vm1,vm2,vm3 target_session_attrs=read-write&quot; \
     -c &quot;insert into t values (current_timestamp, default, current_setting(&apos;patroni.member&apos;)) returning *&quot;
sleep 1
done

(patroni) postgres@vm1:~/test$ sh psql_insert.sh
             a              |  b   |  c
----------------------------+------+-----
 2025-01-28 18:09:34.206879 | 6780 | vm2
(1개 행)

INSERT 0 1
             a              |  b   |  c
----------------------------+------+-----
 2025-01-28 18:09:35.263846 | 6781 | vm2
(1개 행)

INSERT 0 1&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 계속 insert 작업을 하고 있는 중에 다른 호스트에서 patronictl switchover 작업을 진행한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;(patroni) postgres@vm3:~$ patronictl switchover demo-cluster --candidate vm1 --scheduled now --leader vm2 --force
Current cluster topology
+ Cluster: demo-cluster   (7464125923163850844) ---+-----------+
| Member | Host         | Role    | State     | TL | Lag in MB |
+--------+--------------+---------+-----------+----+-----------+
| vm1    | 172.30.1.161 | Replica | streaming | 94 |         0 |
| vm2    | 172.30.1.162 | Leader  | running   | 94 |           |
| vm3    | 172.30.1.163 | Replica | streaming | 94 |         0 |
+--------+--------------+---------+-----------+----+-----------+
2025-01-28 18:13:19.63234 Successfully switched over to &quot;vm1&quot;
+ Cluster: demo-cluster   (7464125923163850844) -+-----------+
| Member | Host         | Role    | State   | TL | Lag in MB |
+--------+--------------+---------+---------+----+-----------+
| vm1    | 172.30.1.161 | Leader  | running | 94 |           |
| vm2    | 172.30.1.162 | Replica | stopped |    |   unknown |
| vm3    | 172.30.1.163 | Replica | running | 94 |         0 |
+--------+--------------+---------+---------+----+-----------+&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;정상적으로 vm1으로 switchover 가 될 때, psql_insert.sh 실행 결과를 살펴본다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;             a              |  b   |  c
----------------------------+------+-----
 2025-01-28 18:13:16.026525 | 6844 | vm2
(1개 행)

INSERT 0 1
             a              |  b   |  c
----------------------------+------+-----
 2025-01-28 18:13:17.079092 | 6845 | vm2
(1개 행)

INSERT 0 1
psql: 오류: &quot;vm1&quot; (172.30.1.161), 5432 포트로 서버 접속 할 수 없음: 세션이 읽기 전용임
&quot;vm2&quot; (172.30.1.162), 5432 포트로 서버 접속 할 수 없음: 연결이 거부됨
	해당 호스트에 서버가 실행 중이고, TCP/IP 접속을 허용하는지 확인하세요.
&quot;vm3&quot; (172.30.1.163), 5432 포트로 서버 접속 할 수 없음: 세션이 읽기 전용임
psql: 오류: &quot;vm1&quot; (172.30.1.161), 5432 포트로 서버 접속 할 수 없음: 세션이 읽기 전용임
&quot;vm2&quot; (172.30.1.162), 5432 포트로 서버 접속 할 수 없음: 연결이 거부됨
	해당 호스트에 서버가 실행 중이고, TCP/IP 접속을 허용하는지 확인하세요.
&quot;vm3&quot; (172.30.1.163), 5432 포트로 서버 접속 할 수 없음: 세션이 읽기 전용임
             a              |  b   |  c
----------------------------+------+-----
 2025-01-28 18:13:20.170898 | 6848 | vm1
(1개 행)

INSERT 0 1
             a              |  b   |  c
----------------------------+------+-----
 2025-01-28 18:13:21.196725 | 6849 | vm1
(1개 행)

INSERT 0 1
             a              |  b   |  c
----------------------------+------+-----
 2025-01-28 18:13:22.239153 | 6850 | vm1
(1개 행)

INSERT 0 1&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;3초 걸렸다. vm2 에서 vm1으로 잘 넘어갔다. (내 목구멍으로 소주 한잔도 잘 넘어갔다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기까지 테스트는&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;데이터베이스 작업을 하기 전에 데이터베이스로 접속을 하고,&amp;nbsp;&lt;/li&gt;&lt;li&gt;작업 SQL을 실행하고,&amp;nbsp;&lt;/li&gt;&lt;li&gt;데이터베이스 연결을 끊는 작업을&amp;nbsp;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div&gt;반복한 것인데, 이런 반복 작업은 운영환경에서는 거의 일어나지 않는다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;대부분의 반복작업은&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;처음 한 번 데이터베이스로 연결하고,&amp;nbsp;&lt;/li&gt;&lt;li&gt;SQL 작업을 반복하고,&amp;nbsp;&lt;/li&gt;&lt;li&gt;연결을 끊는 방식을&amp;nbsp;&amp;nbsp;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div&gt;사용한다.&lt;/div&gt;&lt;div&gt;아쉽게도 psql 에서는 이 부분을 지원하지 않는다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;psql reconnection 기능을 제공하기는 하지만, 다중 호스트, target_session_attrs=read-write 설정을 고려해서 잘 작동하지는 않는다. (개선되면 좋을 듯)&lt;/div&gt;&lt;div&gt;그래서, python psycopg 모듈을 이용한 간단한 테스트 스크립트를 만들어서 테스트 해 보았다.&lt;br&gt;(추가로 pip install&amp;nbsp;psycopg-pool 도 필요하다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;(patroni) postgres@vm1:~/test$ cat conn_pool.py
import psycopg
import psycopg_pool
import time

dsn =&quot;host=vm1,vm2,vm3 target_session_attrs=read-write connect_timeout=5&quot;
pool = psycopg_pool.ConnectionPool(conninfo=dsn)

def myconnect():
  while True:
    try:
      conn = pool.getconn()
      if not conn.closed:
        return conn
        break
    except Exception as e:
      print(e)
      time.sleep(1)

conn = myconnect()

while True:
  try:
    cur = conn.cursor()
    cur.execute(&quot;insert into t values (current_timestamp, default, current_setting(&apos;patroni.member&apos;)) &quot;\
                &quot;returning *,pg_backend_pid()&quot;)
    conn.commit()
    row = cur.fetchone()
    cur.close()
    print(row[0], row[1], row[2], row[3])
    time.sleep(1)
  except Exception as e:
    print(&quot;error!&quot;)
    cur.close()
    conn.close()
    pool.close()
    pool = psycopg_pool.ConnectionPool(conninfo=dsn)
    conn = myconnect()
    time.sleep(1)

(patroni) postgres@vm1:~/test$ python conn_pool.py
2025-01-28 18:39:07.348516 6963 vm3 1205
2025-01-28 18:39:08.357589 6964 vm3 1205
2025-01-28 18:39:09.367712 6965 vm3 1205&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;마지막 1205는 해당 세션의 backend 프로세스 pid이다. 같다면, 그 세션이 끊기지 않고 작업하고 있음을 의미한다.&lt;/div&gt;&lt;div&gt;여기서도 patroni switchover 테스트를 해 보면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;2025-01-28 18:44:02.586864 7030 vm3 1214
2025-01-28 18:44:03.598007 7031 vm3 1214
2025-01-28 18:44:04.608094 7032 vm3 1214
error!
error connecting in &apos;pool-2&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: FATAL:  the database system is shutting down
error connecting in &apos;pool-2&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: FATAL:  the database system is shutting down
error connecting in &apos;pool-2&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: FATAL:  the database system is shutting down
error connecting in &apos;pool-2&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: FATAL:  the database system is shutting down
error connecting in &apos;pool-2&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
error connecting in &apos;pool-2&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
error connecting in &apos;pool-2&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
error connecting in &apos;pool-2&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
2025-01-28 18:44:09.438773 7065 vm1 2124
2025-01-28 18:44:10.450736 7066 vm1 2124
2025-01-28 18:44:11.461425 7067 vm1 2124&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;잘 넘어가고 잘 작동했다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;patroni를 이용한 auto failover 기능을 사용하고, 기타 다른 proxy 소프트웨어를 사용하지 않겠다면, 데이터베이스 접속 문자열 설정을 잘 해야하는 것과,&amp;nbsp;&lt;/div&gt;&lt;div&gt;해당 접속이 끊겼을 때, 다시 재접속하는 작업을 응용 프로그램에서 반드시 구현해야 한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;실재 pool는 미리 몇개의 연결을 해 놓고, 그것 가운데 하나를 가져와서 DB 쿼리를 하고, 쿼리가 끝나면 그 연결을 풀러에게 반환하는 방식을 사용하기 때문에, 이 부분도 윗 코드를 조금 바꿔 다시 테스트 해 봤다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;(patroni) postgres@vm1:~/test$ cat conn_pool2.py
import psycopg
import psycopg_pool
import time

dsn =&quot;host=vm1,vm2,vm3 target_session_attrs=read-write connect_timeout=5&quot;
pool = psycopg_pool.ConnectionPool(conninfo=dsn)

while True:
  with pool.connection() as conn:
    try:
      with conn.execute(&quot;insert into t values (current_timestamp, default, current_setting(&apos;patroni.member&apos;)) &quot;\
                &quot;returning *,pg_backend_pid()&quot;) as cur:
        conn.commit()
        row = cur.fetchone()
        print(row[0], row[1], row[2], row[3])
        time.sleep(1)
    except Exception as e:
      print(e)
      time.sleep(0.1)
(patroni) postgres@vm1:~/test$ python conn_pool2.py
2025-02-01 18:23:11.585164 9405 vm3 637
2025-02-01 18:23:12.594700 9406 vm3 636
2025-02-01 18:23:13.607007 9407 vm3 638
2025-02-01 18:23:14.621884 9408 vm3 639
2025-02-01 18:23:15.631616 9409 vm3 637
2025-02-01 18:23:16.643578 9410 vm3 636
2025-02-01 18:23:17.653505 9411 vm3 638
2025-02-01 18:23:18.664466 9412 vm3 639
terminating connection due to administrator command
discarding closed connection: &lt;psycopg.connection [bad]=&quot;&quot; at=&quot;&quot; 0xffffa47ad790=&quot;&quot;&gt;
terminating connection due to administrator command
error connecting in &apos;pool-1&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
discarding closed connection: &lt;psycopg.connection [bad]=&quot;&quot; at=&quot;&quot; 0xffffa47accd0=&quot;&quot;&gt;
terminating connection due to administrator command
error connecting in &apos;pool-1&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
discarding closed connection: &lt;psycopg.connection [bad]=&quot;&quot; at=&quot;&quot; 0xffffa47c0850=&quot;&quot;&gt;
terminating connection due to administrator command
error connecting in &apos;pool-1&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
discarding closed connection: &lt;psycopg.connection [bad]=&quot;&quot; at=&quot;&quot; 0xffffa47c1450=&quot;&quot;&gt;
error connecting in &apos;pool-1&apos;: connection failed: connection to server at &quot;172.30.1.163&quot;, port 5432 failed: server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
2025-02-01 18:23:20.804151 9439 vm2 1319
2025-02-01 18:23:21.813625 9440 vm2 1320
2025-02-01 18:23:22.833537 9441 vm2 1321
2025-02-01 18:23:23.844478 9442 vm2 1322
2025-02-01 18:23:24.860096 9443 vm2 1319&lt;/psycopg.connection&gt;&lt;/psycopg.connection&gt;&lt;/psycopg.connection&gt;&lt;/psycopg.connection&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;vm3의 637..639 백엔드 세션을 쓰고 반복해서 쓰고 있다가 vm2의 1319..1322 세션으로 잘 넘어갔다. 2초 걸렸다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기서 haproxy 를 이용한 접속 정보 고정 하는 부분을 다루지 않는 것은 결국 haproxy에 대한 고가용성도 또 고려해야하기 때문이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h4&gt;jdbc 기반 클라이언트들&lt;/h4&gt;&lt;div&gt;jdbc 에서는 단순히 connection url 설정에 대해서만 언급한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;read-write 호스트로 자동 접속하는 옵션:&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; jdbc:postgresql://vm1,vm2,vm3/dbname?targetServerType=primary&lt;/div&gt;&lt;div&gt;read-only 호스트들 가운데 임의 접속하는 옵션:&lt;/div&gt;&lt;div&gt;&amp;nbsp;&amp;nbsp;jdbc:postgresql://vm1,vm2,vm3/dbname?targetServerType=secondary&amp;amp;loadBalanceHosts=true&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;failover, switchover 와 응용 프로그램의 pooler 문제&lt;/h3&gt;&lt;div&gt;기억해야 할 사실은,&lt;/div&gt;&lt;div&gt;읽기-쓰기 인스턴스가 읽기전용 인스턴스로 바뀔 때는 인스턴스가 재실행 되기 때문에, 연결 되어 있던 모든 클라이언트의 backend 세션들이 모두 정리가 된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이에 맞춰 응용 프로그램을 위에서 소개한 것처럼 연결이 끊기게 되면 재접속을 하는 예외처리가 되어있다면, 별 문제 없이 연결이 정리가 된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;문제는,&lt;/div&gt;&lt;div&gt;읽기전용 인스턴스가 읽기-쓰기 인스턴스로 바뀔 때에는 그 인스턴스가 재실행되지 않는다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그래서, 읽기전용 인스턴스 상태에 연결 되어 있던 클라이언트들의 backend 세션들이 정리가 되질 않는다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;아주 낙천적으로 생각하면, 어차피 idle timeout 설정 때문에 언젠가는 다 끊어지겠지 생각할 수도 있겠지만, 읽기 전용 인스턴스의 부하가 심한 상태였다면, 운영상 치명적인 문제가 될 수도 있을 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;읽기전용 인스턴드들의 부하 분산과 그 인스턴스를 사용하고 있는 응용 프로그램이 pooler를 사용하고 있는 환경이라면, 좀 더 고민을 해 봐야할 문제로 보인다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(아직 답을 못 찾았다. ChatGPT는 Spring Boot 인경우는 사용자 정의 데이터소스 라우팅 기법으로 해결 하라고 한다. 아무튼 이 영역(읽기전용 인스턴스들이 여럿 있고, 응용 프로그램은 pooler를 쓰는 경우에 구현하는 부하분산)은 응용 프로그램을 왕창 고치든, haproxy 같은 부하분산를 처리하는 인스턴스가 있어야 할 것 같다. )&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;Virtual IP 설정&lt;/h2&gt;&lt;div&gt;이 부분은 일반적인 네트워크 환경에서는&amp;nbsp;&lt;/div&gt;&lt;div&gt;PostgreSQL 공식 빌드 저장소 가운데, extra 저장소를 이용해서,&amp;nbsp;&lt;/div&gt;&lt;div&gt;vip-manager 패키지를 이용하면 손쉽게 설정할 수 있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;아래 글은 이 사실을 모르고 혼자서 삽질한 결과물이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그냥 기록 삼아 남겨둔다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;vip-manager 사용법에 대해서는&amp;nbsp;&lt;/div&gt;&lt;div&gt;https://github.com/cybertec-postgresql/vip-manager&lt;/div&gt;&lt;div&gt;페이지 참조.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;-------&lt;/div&gt;&lt;div&gt;OS 클러스터 솔루션을 사용하지 않고, etcd + patroni 만으로 클러스터 대표 IP 주소를 지정하고, 응용 프로그램이 그 대표 IP를 사용해서 서비스 하는 것에 대한 고민이였는데,&amp;nbsp;&lt;/div&gt;&lt;div&gt;결론은 앞에서 계속 다루웠듯이 클러스터 소속 모든 IP를 응용프로그램에서 지정하고,&lt;/div&gt;&lt;div&gt;데이터베이스 접속을 담당하는 라이브러리가 알아서 원하는 데이터베이스로 접속하도록 하는 것이 제일 안전한 방법인 듯하다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;하지만, 윗 patroni.yml 파일에서 테스트 하려고 설정했던 on_role_change.sh 스크립트가 있으니, 이것을 테스트한 기록도 남긴다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;virtaul ip 로 윗 환경에서는 172.30.1.170 으로 정했다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그리고 on_role_change.sh 스크립트에서&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 virtual ip 할당 작업을 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;스크립트 내용은 다음과 같다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;(patroni) [postgres@vm4 ~]$ cat on_role_change.sh
#!/bin/sh

VIRTUALIP=&quot;172.30.1.170&quot;
ETHDEV=&quot;enp0s1&quot;
GATEWAY=&quot;172.30.1.254&quot;
CLUSTERNM=`curl -s http://localhost:8008 | jq -r .patroni.scope`

sudo ifconfig $ETHDEV:1 down
sudo arp -d $VIRTUALIP

PGPRIMARY=`etcdctl get --print-value-only=true /db/$CLUSTERNM/leader`
MYHOSTNAME=`hostname -s`

if [ &quot;$PGPRIMARY&quot; = &quot;$MYHOSTNAME&quot; ]; then
  sudo ifconfig $ETHDEV:1 $VIRTUALIP/24
  # debian 계열 OS에서는 아래 -s 옵션을 -S 로 바꿔야 한다.
  sudo arping -c 3 -w 5 -I $ETHDEV -U $GATEWAY -s $VIRTUALIP
fi&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 스크립트가 실행이 되면 (해당 patroni 에서 역할이 바뀌기 시작하면 - primary 에서 standby 가 되든가, standby 에서 primary로 바뀌든가 -)&amp;nbsp;&lt;/div&gt;&lt;div&gt;먼저 vip가 할당된 이더넷 인터페이스를 down하고,&amp;nbsp;&lt;/div&gt;&lt;div&gt;해당 vip를 arp 캐시에서 지우고,&lt;/div&gt;&lt;div&gt;etcd 를 통해 leader가 누군지 찾고, (이때, patroni.yml 에서 지정한 namespace와 scope 설정을 지정한다. /db/demo-cluster/)&lt;/div&gt;&lt;div&gt;leader와 자신이 같으면,&amp;nbsp; vip 설정하고,&lt;/div&gt;&lt;div&gt;arping 명령으로 vip의 mac 주소가 바뀌었음을 알린다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;ifconfig, arp 작업들은 모두 root 작업이기 때문에,&lt;/div&gt;&lt;div&gt;sudo 명령으로 진행하며 sudoers 설정도 필요하다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;vip 바꾸기 작업을 이렇게 하면 어떻게든 되기는 한다.&lt;/div&gt;&lt;div&gt;그런데 많이 번거롭다.&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;(patroni 구축 자체가 많이 번거롭기는 하다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;----------------&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;하나 더.&lt;/div&gt;&lt;div&gt;위와 같은 이더넷 장치에 별칭 IP를 추가하는 것으로 VIP를 구현해도 복잡한 클라우드 가상 네트워크 환경인 경우에는 문제가 안풀리는 경우가 있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이때는 iptables의 포트 포워딩을 이용해서, primary 인스턴스에 대해서만 5432 포트가 열리는 식으로 설정하고, (가상화 되어있든 말든) L4 장비에 이 DB인스턴스들의 호스트와 5432 포트를 지정해 auto failover를 구현할 수 있을 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 하기 위해서는 먼저 모든 데이터베이스의 서비스 포트를 다른 포트(6432)로 바꿔 설정을 하고, 위 on_role_change.sh 스크립트에서 iptables 설정을 하는 방식도 있을 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;#!/bin/sh

CLUSTERNM=`curl -s http://localhost:8008 | jq -r .patroni.scope`

# 일단 5432 포트 포워딩 설정을 지우고,
sudo iptables -D PREROUTING -t nat -i enp0s1 -p tcp --dport 5432 -j REDIRECT --to-port 6432

PGPRIMARY=`etcdctl get --print-value-only=true /db/$CLUSTERNM/leader`
MYHOSTNAME=`hostname -s`

if [ &quot;$PGPRIMARY&quot; = &quot;$MYHOSTNAME&quot; ]; then
   # 내가 포트 포워딩을 해야 하는 경우면 설정을 추가하고.
   sudo iptables -A PREROUTING -t nat -i enp0s1 -p tcp --dport 5432 -j REDIRECT --to-port 6432
fi&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이정도면, 일단 데이터베이스 인스턴스 측에서는 할 것은 다했다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;------&lt;/div&gt;&lt;div&gt;이랬는데, patroni 에서 이 문제를 미리 알고,&amp;nbsp;&lt;/div&gt;&lt;div&gt;/primary API를 제공하고 있었다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;http://vm1:8008/primay&amp;nbsp;&lt;/div&gt;&lt;div&gt;호출했을 때 HTTP header 응답 코드가 200이면 primary 다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그렇지 않은 500을 던진다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, L4 설정에서 DB 인스턴스 health check 를 이걸로 하면 된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;L4 그룹에 모든 DB 인스턴스를 등록하고, 각 인스턴스가 사용가능한지를 검사하는 것은&amp;nbsp;&lt;/div&gt;&lt;div&gt;윗 8008 웹서버의 /primary 페이지를 호출해서 검사하는 식으로 구성하면 된다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;나머지는 L4에게 맡긴다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;------&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;응용프로그램을 수정할 수 없는 상황으로 데이터베이스 접속 IP 주소를 하나 밖에 지정할 수 없다면,&amp;nbsp; 부득이 이렇게 vip 설정을 할 수는 있겠지만,&amp;nbsp;&lt;/div&gt;&lt;div&gt;이런 저런 테스트를 해본 결과 데이터베이스 운영하는 입장에서는&lt;/div&gt;&lt;div&gt;위에서 언급한 것 처럼 응용프로그램에서 데이터베이스 연결하는 방식을&amp;nbsp;&lt;/div&gt;&lt;div&gt;다중 호스트 지정를 지정하고, 그 가운데 primary를 찾아서 접속하라고 하고,&amp;nbsp;&lt;/div&gt;&lt;div&gt;작업 도중 접속이 끊어지면, 다시 접속하는 예외처리를 포함 해서 응용프로그램을 구현하는 것이 제일 좋을 듯하다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;마치며&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;운영환경에서는 당연히 postgres.yml 파일 내용을 좀 더 섬세하게 설정할 필요가 있다.&amp;nbsp;&lt;/li&gt;&lt;li&gt;etcd 클러스터링 문제 상황에 대한 대처 능력이 필요하다. (백업, 복구)&lt;/li&gt;&lt;li&gt;각 소프트웨어 업그레이드나 패치에 대한 안정적인 운영 지침이 필요하다.&lt;/li&gt;&lt;li&gt;기존 단일 postgresql 서비스를 이 etcd + patroni + postgres 구성으로 구축하는 방법도 준비해야 할 것&lt;/li&gt;&lt;li&gt;psycopg_pool 모듈에서 DB 다중 읽기 전용 인스턴스의 부하분산된 pooling 구현 해킹도 필요하다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;올해 설 명절 긴 연휴, 이렇게 신나게 놀고 있다.&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;</description>
            <pubDate>Sat, 08 Feb 2025 22:10:15 +0900</pubDate>
            <guid>http://postgresql.kr/blog/patroni.html</guid>
        </item>
        <item>
            <title>PostgreSQL 파티션 테이블 2부</title>
            <link>http://postgresql.kr/blog/postgresql_partition_table_2.html</link>
            <description>&lt;h1&gt;PostgreSQL 파티션 테이블 2부&lt;/h1&gt;&lt;h2&gt;파티션 테이블&lt;/h2&gt;&lt;div&gt;PostgreSQL에서는 마치 하나의 테이블인 것 처럼 보이지만, 내부적으로는 물리적으로 특정 조건에 따라 나뉘어 있는 테이블을 파티션 테이블이라고 합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;엄밀하게 표현하면 그 하나의 테이블은 파티션된 테이블이고, 그 부분 테이블은 파티션 테이블이라고 영어에서는 표현합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글에서는 상위 테이블, 하위 테이블 이 단어을 사용합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(부모 테이블, 자식 테이블 이런 표현도 썼으나, 요즘은 잘 안씁니다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;파티션 테이블을 만들어 쓰는 까닭은&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;어느 특정 부분만 찾을 때 찾지 않아도 되는 다른 부분을 아에 찾지 않고, 원하는 부분만 꼭 집어 찾아 작업 비용을 줄이는 것과,&lt;/li&gt;&lt;li&gt;그 특정 부분을 쉽고 빠르게 지우고,&lt;/li&gt;&lt;li&gt;아주 큰 단일 테이블을 autovacuum이 트랜잭션ID 겹침 방지 작업을 아주 오랫동안 하다가 결국 데이터베이스를 단일 사용자 모드로 접속해서 하세월 vacuum 해야하는 데이터베이스 최악의 사고를 막기&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;위함입니다.&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;파티션 테이블 만들기&lt;/h2&gt;&lt;div&gt;공식 설명서의 예제를 그대로 가져오면,&lt;/div&gt;&lt;div&gt;먼저 상위 테이블을 만들고,&lt;/div&gt;&lt;div&gt;&lt;pre&gt;CREATE TABLE measurement (
    city_id         int not null,
    logdate         date not null,
    peaktemp        int,
    unitsales       int
) PARTITION BY RANGE (logdate);&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;다음 하위 테이블을 만들어서 사용합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;CREATE TABLE measurement_y2006m02 PARTITION OF measurement
    FOR VALUES FROM (&apos;2006-02-01&apos;) TO (&apos;2006-03-01&apos;);

CREATE TABLE measurement_y2006m03 PARTITION OF measurement
    FOR VALUES FROM (&apos;2006-03-01&apos;) TO (&apos;2006-04-01&apos;);&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;핵심 구문은 상위 테이블에서 사용하는 &apos;partition by&apos; 와 하위 테이블에서 사용하는 &apos;partition of&apos; 입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;또 하나 기억해야 할 것은 파티션 테이블의 기본키에는 반드시 파티션키가 포함되어야합니다. 유니크 제약조건(유니크 인덱스 포함)도 마찬가지입니다.&lt;/div&gt;&lt;div&gt;파티션 테이블 전체를 통틀어 하나의 칼럼으로 유일성을 보장할 수 없다는 뜻입니다.&lt;/div&gt;&lt;div&gt;이는 테이블 설계 할 때 꽤 주의를 기우려야하는 문제점(?)을 안고 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;현재로써는 단일 칼럼 값을 유니크하게 처리할 수 있는 방법은 없습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;유니크 자료만 담는 부가 테이블을 만들고,&amp;nbsp; 응용프로그램의 힘을 빌려야할 것 같네요.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;파티션 테이블 조회하기&lt;/h2&gt;&lt;div&gt;PostgreSQL에서 파티션 테이블을 조회 할 때 꼭 기억해야하는 것은&amp;nbsp;&lt;/div&gt;&lt;div&gt;원하는 하위 테이블만 조회하기 위한 선택 작업(Partition Pruning)이 실행계획 전에 먼저 일어난다는 것입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;그래서, 단일 테이블에서 성능이 잘 나오던 SQL이 어처구니 없이 느려지는 경우도 발생합니다. 그 원인을 찾기 위해 실행 계획을 보면, 거의 대부분의 문제가 위에서 이야기한 하위 테이블 선택 작업에서 모든 하위테이블을 대상으로 선택되어 발생합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;특히 파티션 테이블과 파티션 테이블의 join 작업에 빈번하게 예상치 않게 비효율적인 실행 계획이 만들어집니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;방법은 아주 단순합니다. 파티션 테이블을 사용하는 SQL 구문을 작성할 때,&amp;nbsp;&lt;/div&gt;&lt;div&gt;꼭 의도한 대로 하위 테이블만 참조하는지 explain 명령으로 꼭 확인하는 것입니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;원하는 하위 테이블만 참조하도록 SQL을 작성하는 방법은 &lt;a href=&quot;/blog/postgresql_partition_table.html&quot;&gt;1부&lt;/a&gt;에서도 언급했듯이, where 조건절에 반드시 해당 파티션키 선택 조건을 추가해야합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;위 테이블을 예로 들면,&amp;nbsp;&lt;/div&gt;&lt;pre&gt;select * from measurement where logdate &amp;gt;= &apos;2006-02-01&apos; and logdate &amp;lt; &apos;2006-03-01&apos; ......&lt;/pre&gt;&lt;div&gt;이런식으로 지정해야&amp;nbsp;measurement_y2006m02 테이블만 선택하게 됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이는 update, delete 쿼리에서도 같이 적용됩니다. 자료 변경 쿼리를 짤 때도 꼭 신경 써야 합니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;운영 환경에서의 파티션 테이블 조작&lt;/h2&gt;&lt;div&gt;운영 환경에서 하위 파티션 테이블을 새로 추가하거나, 삭제 하는 작업은 생각보다 신경 쓸 거리가 많습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;왜냐하면, 이 작업들은 상위 테이블의 배타적 잠금을 유발하기 때문입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;생각 하기를 멈추고, 사용 설명서 예제로 나와 있으니, 그거 믿고, 그냥 작업 하면 되겠지 했다가 운영 장애 상황을 만드는 경우를 종종 봅니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;첫번째 작업: 아주 오래 걸리는 파티션 테이블 조회 작업&lt;/div&gt;&lt;div&gt;두번째 작업: create table partition of 구문으로 하위 테이블 만드는 작업&lt;br&gt;이 두번째 작업은 첫번째 작업이 끝나야 자신이 상위 테이블을 배타적 잠금을 하고, 하위 파티션 테이블 추가 작업을 진행합니다. 그런데, 첫번째 작업이 오래 걸린다고 가정하면, 두번째 작업은 대기 상태로 빠지고,&amp;nbsp;&lt;/div&gt;&lt;div&gt;세번째, 네번째 작업들도 모두 대기 상태로 빠집니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;두번째 create 작업 뒤에 이어 오는 작업들이 그냥 단순 아주 빠른 select 쿼리 일지라도 두번째 작업의 대기 때문에 모두 대기 상태로 빠집니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;아주 치명적인 상황이 발생될 수 있습니다.&lt;/div&gt;&lt;div&gt;이 문제를 피할 수 있는 방법은 alter table attach 구문으로 하위 파티션 테이블을 추가하는 것입니다. 이렇게 하려면, 먼저 해당 하위 테이블을 만들어야겠죠.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;하위 테이블을 지우는 작업에서도 이 전체 잠금 문제는 똑 같이 일어납니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그냥 하위 테이블을 drop 하는 것은 아주 위험합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다행히 14버전부터 alter table detach ... concurrently 구문이 지원되면서, 하위 파티션 테이블을 삭제하는 작업에서 부담 덜하지만, 13버전 이하에서는 주의가 많이 필요합니다. 하위 테이블 삭제 작업도 detach concurrently -&amp;gt; drop 으로 진행해야 많은 사람들이 편합니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음은 지금까지 이야기를 바탕으로 실무에서 사용할 수 있는 파티션 테이블 추가, 삭제에 대한 SQL 구문들입니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;-- 하위 테이블이 없는 상위 파티션 테이블 만들기
create table p (a int, b int, c int , d int) partition by range (b);

-- 테이블 전체 잠금을 하지 않는 하위 테이블 만들어 상위 테이블에 붙이기
create table sub1 (like p including all, check (b &amp;gt;= 0 and b &amp;lt; 10));
alter table p attach partition sub1 for values from (&apos;0&apos;) to (&apos;10&apos;);
alter table sub1 drop constraint if exists sub1_b_check;

-- 자료 넣고, 조회하는 쿼리가 오래 걸리도록 만들기
insert into p values (1,1,1,1);
select a,b,c,d,pg_sleep(60) from p where a=1 and b = 1;

-- 윗 상황에서 다른 연결에서 기본키를 만드는데 테이블 전체 잠금 최소화 하는 방법
create unique index concurrently sub1_pkey on sub1 (a,b);
create unique index concurrently sub2_pkey on sub2 (a,b);
-- 이 두 작업은 위 오래 걸리는 select 쿼리가 끝나기를 기다리겠지만 테이블 전체 잠금은 일어나지 않음
alter table sub1 add primary key  using index sub1_pkey;
alter table sub2 add primary key  using index sub2_pkey;

-- 이제 상위 테이블 기본키 만들기
alter table only p add primary key (a,b);

-- 하지만 이 기본키는 아직 완전한 기본키가 아님
\d p

-- 하위 테이블의 기본키들이 상위 테이블의 기본키 소속임을 지정하기
alter index p_pkey attach partition sub1_pkey;
alter index p_pkey attach partition sub2_pkey;
\d p

-- 14 버전부터 쓸 수 있는 테이블 전체 잠금을 최소화한 하위 테이블 분리
alter table p detach partition sub2 concurrently;

&lt;/pre&gt;&lt;/div&gt;</description>
            <pubDate>Fri, 19 Jul 2024 13:07:46 +0900</pubDate>
            <guid>http://postgresql.kr/blog/postgresql_partition_table_2.html</guid>
        </item>
        <item>
            <title>한글과 LC_COLLATE</title>
            <link>http://postgresql.kr/blog/collate_for_pg.html</link>
            <description>아주 지루한 이야기를 시작합니다.&lt;div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;1. 문자세트&lt;/h2&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;% locale&lt;/div&gt;&lt;div&gt;LANG=&quot;ko_KR.CP949&quot;&lt;/div&gt;&lt;div&gt;LC_COLLATE=&quot;ko_KR.CP949&quot;&lt;/div&gt;&lt;div&gt;LC_CTYPE=&quot;ko_KR.CP949&quot;&lt;/div&gt;&lt;div&gt;LC_MESSAGES=&quot;ko_KR.CP949&quot;&lt;/div&gt;&lt;div&gt;LC_MONETARY=&quot;ko_KR.CP949&quot;&lt;/div&gt;&lt;div&gt;LC_NUMERIC=&quot;ko_KR.CP949&quot;&lt;/div&gt;&lt;div&gt;LC_TIME=&quot;ko_KR.CP949&quot;&lt;/div&gt;&lt;div&gt;LC_ALL=&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;% /pgsql/16/bin/initdb -D cp949-cluster&lt;/div&gt;&lt;div&gt;데이터베이스 클러스터는 &quot;ko_KR.CP949&quot; 로케일으로 초기화될 것입니다.&lt;/div&gt;&lt;div&gt;initdb: 오류: &quot;ko_KR.CP949&quot; 로케일은 지원하지 않는 &quot;UHC&quot; 인코딩을 필요로 함&lt;/div&gt;&lt;div&gt;initdb: 상세정보: &quot;UHC&quot; 인코딩을 서버측 인코딩으로 사용할 수 없습니다.&lt;/div&gt;&lt;div&gt;initdb: 힌트: 다른 로케일을 지정해서 initdb 작업을 다시 하세요.&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;OS 언어환경이 확장 완성형 (cp949, uhc) 인 경우는 로케일 관련 옵션 없이는 PostgreSQL 데이터베이스를 초기화 할 수 없습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;% export LANG=ko_KR.eucKR&lt;/div&gt;&lt;div&gt;% /pgsql/16/bin/initdb -D euckr-cluster&lt;/div&gt;&lt;div&gt;데이터베이스 클러스터는 &quot;ko_KR.eucKR&quot; 로케일으로 초기화될 것입니다.&lt;/div&gt;&lt;div&gt;기본 데이터베이스 인코딩은 &quot;EUC_KR&quot; 인코딩으로 설정되었습니다.&lt;/div&gt;&lt;div&gt;데이터베이스 클러스터가 잘 만들어졌습니다.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이제 데이터베이스 인스턴스를 만들겠습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;% /pgsql/16/bin/pg_ctl -D euckr-cluster start&lt;/div&gt;&lt;div&gt;서버 시작됨&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;잘 시작되었습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이제 접속해서 한글 관련 테스트를 하겠습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# select &apos;툝롲&apos;;&lt;/div&gt;&lt;div&gt;오류:&amp;nbsp; &quot;EUC_KR&quot; 인코딩에서 사용할 수 없는 문자가 있음: 0xb8 0x94&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;아무런 사전 조치를 하지 않은 euc-kr 환경에서는 그냥 데이터베이스를 만들면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;euc-kr 문자세트에 포함되지 않은 문자를 처리할 수 없습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL은 철저하게 예외를 두지 않습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그래서, 해당 문자세트에 포함되지 않은 문자를 처리하려고 할 때는 원천 차단됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;자료를 저장하고자 한다면 저장 자체가 안됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;지금까지 결론&lt;/div&gt;&lt;div&gt;모든 한글을 담을 수 있는 locale은 ko_KR.eucKR, ko_KR.CP949 로 모국어 환경으로는 안된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;결국 ko_KR.UTF-8 설정을 합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;% locale -a | grep ko_KR&lt;/div&gt;&lt;div&gt;ko_KR&lt;/div&gt;&lt;div&gt;ko_KR.eucKR&lt;/div&gt;&lt;div&gt;ko_KR.UTF-8&lt;/div&gt;&lt;div&gt;ko_KR.CP949&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;% export LANG=ko_KR.UTF-8&lt;/div&gt;&lt;div&gt;% date&lt;/div&gt;&lt;div&gt;2023년 11월&amp;nbsp; 5일 일요일 01시 51분 50초 KST&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;% /pgsql/16/bin/initdb -D utf8-cluster&lt;/div&gt;&lt;div&gt;&lt;div&gt;% /pgsql/16/bin/pg_ctl -D utf8-cluster start&lt;/div&gt;&lt;div&gt;서버를 시작하기 위해 기다리는 중.... 완료&lt;/div&gt;&lt;div&gt;서버 시작됨&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# select &apos;툝롲&apos;;;&lt;/div&gt;&lt;div&gt;&amp;nbsp;?column?&lt;/div&gt;&lt;div&gt;----------&lt;/div&gt;&lt;div&gt;&amp;nbsp;툝롲&lt;/div&gt;&lt;div&gt;(1개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이제 잘 됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# show server_encoding ;&lt;/div&gt;&lt;div&gt;&amp;nbsp;server_encoding&lt;/div&gt;&lt;div&gt;-----------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;UTF8&lt;/div&gt;&lt;div&gt;(1개 행)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;postgres=# show client_encoding ;&lt;/div&gt;&lt;div&gt;&amp;nbsp;client_encoding&lt;/div&gt;&lt;div&gt;-----------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;UTF8&lt;/div&gt;&lt;div&gt;(1개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;결국 utf8 문자세트 환경이어야 한글 모든 글자를 처리할 수 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;2. LC_COLLATE&lt;/h2&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 만들어진 데이터베이스의 문자 관련 설정은 다음과 같습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# \l&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;데이터베이스 목록&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;이름&amp;nbsp; &amp;nbsp; | 소유주 | 인코딩 | 로케일 제공자 |&amp;nbsp; &amp;nbsp;Collate&amp;nbsp; &amp;nbsp;|&amp;nbsp; &amp;nbsp; Ctype&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;-----------+--------+--------+---------------+------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;postgres&amp;nbsp; | ioseph | UTF8&amp;nbsp; &amp;nbsp;| libc&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | ko_KR.UTF-8 | ko_KR.UTF-8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;collate 관련 설정을 특별히 지정하지 않으면, LANG 환경설정을 따르게됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이것 기반으로 데이터베이스를 만들때도 그대로 사용되어 데이터베이스의 Collate값은 ko_KR.UTF-8 로 지정됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;OS LC_COLLATE 환경 설정 변수는 문자열 정렬과 관계됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&apos;가&apos; 가 &apos;나&apos;보다 앞에 있다는 것에 대한 정의입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;간단하게 생각하면 그냥 각 문자의 코드를 순서대로 하고, 그냥 그 코드 값을 따르면 될 것 처럼 보이지만, 실세계에서는 그리 간단한 문제가 아닙니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&apos;abcde&apos; 인경우에,&amp;nbsp;&apos;à&apos;, &apos;ã&apos; 문자를 사용하는 나라에서는 각 고유의 순서를 임의로 정합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이런식으로 libc 로케일 제공자가 정의한 utf8 문자셋을 사용하는 ko_KR (Korean_Korea) 문자 정렬은 그 libc 제작 주체에 따라 달라집니다. Microsoft, Apple, GNU, .....&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;한글, 간체, 히라가나로만 확인해보면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;GNU: 간체, 히라가나, 한글&lt;/div&gt;&lt;div&gt;Microsoft :&amp;nbsp; 히라가나, 간체,&amp;nbsp; 한글&lt;/div&gt;&lt;div&gt;Apple: 간체, 한글, 히라가나&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이런식으로 제각각입니다.&lt;/div&gt;&lt;div&gt;전세계 다양한 문자들을 다 고려하고, 각 문자들을 사용하는 나라들의 환경을 고려하면, 더 복잡해집니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;거기에, 각종 다양한 프로그래밍 언어까지 더해지면, 완전 엉망이 되어버리죠. 그래서,&amp;nbsp;&lt;/div&gt;&lt;div&gt;모든 문자에 대한 정렬에 관련한 표준이 필요해집니다. 그래서 등장한 것이&amp;nbsp;&lt;/div&gt;&lt;div&gt;ICU -&amp;nbsp;International Components for Unicode&lt;/div&gt;&lt;div&gt;이 ICU 이야기는 그 자체만으로도 꽤 긴 이야기가 되기 때문에, 이 글에서는 다루지 않습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;3. 한글을 사용하는 칼럼의 인덱스&lt;/div&gt;&lt;div&gt;&lt;div&gt;mydb=# \l&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;데이터베이스 목록&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;이름&amp;nbsp; &amp;nbsp; |&amp;nbsp; 소유주&amp;nbsp; | 인코딩 | 로케일 제공자 |&amp;nbsp; Collate&amp;nbsp; &amp;nbsp;|&amp;nbsp; &amp;nbsp;Ctype&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;-----------+----------+--------+---------------+-----------&lt;/div&gt;&lt;div&gt;&amp;nbsp;mydb&amp;nbsp; &amp;nbsp; &amp;nbsp; | postgres | UTF8&amp;nbsp; &amp;nbsp;| libc&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | ko_KR.utf8 | ko_KR.utf8&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;mydb=# create table koword (a text);&lt;/div&gt;&lt;div&gt;CREATE TABLE&lt;/div&gt;&lt;div&gt;mydb=# \copy koword from &apos;kodic.txt&apos;&lt;/div&gt;&lt;div&gt;COPY 5966&lt;/div&gt;&lt;div&gt;mydb=# create index koword_a_i on koword (a);&lt;/div&gt;&lt;div&gt;CREATE INDEX&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;mydb=# explain select a from koword where a like &apos;사랑%&apos;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;QUERY PLAN&lt;/div&gt;&lt;div&gt;--------------------------------------------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;Seq Scan on koword&amp;nbsp; (cost=0.00..105.58 rows=1 width=9)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Filter: (a ~~ &apos;사랑%&apos;::text)&lt;/div&gt;&lt;div&gt;(2개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;인덱스를 사용하지 않습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;여느 다른 관계형 데이터베이스에서 이렇지 않는데 말이죠.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL에서는 독특한 규칙 하나가 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그 데이터베이스의 문자 정렬 규칙이 C 가 아닌 경우, like 연산에서 인덱스를 사용하려면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;그 인덱스를 만들 때 like 연산이 가능하도록 연산자 클래스를 지정해 주어야 합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;mydb=# create index koword_a_i2 on koword (a text_pattern_ops);&lt;/div&gt;&lt;div&gt;CREATE INDEX&lt;/div&gt;&lt;div&gt;mydb=# explain select a from koword where a like &apos;사랑%&apos;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; QUERY PLAN&lt;/div&gt;&lt;div&gt;-------------------------------------------------------------------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;Index Only Scan using koword_a_i2 on koword&amp;nbsp; (cost=0.28..4.30 rows=1 width=9)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Index Cond: ((a ~&amp;gt;=~ &apos;사랑&apos;::text) AND (a ~&amp;lt;~ &apos;사랒&apos;::text))&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Filter: (a ~~ &apos;사랑%&apos;::text)&lt;/div&gt;&lt;div&gt;(3개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 연산자 클래스를 지정하지 않은 인덱스를 사용해서 자료를 탐색하려면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;범위 연산만 허용합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;mydb=# explain select a from koword where a &amp;gt;= &apos;사랑&apos; and a &amp;lt; &apos;사랒&apos;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; QUERY PLAN&lt;/div&gt;&lt;div&gt;------------------------------------------------------------------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;Index Only Scan using koword_a_i on koword&amp;nbsp; (cost=0.28..4.30 rows=1 width=9)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Index Cond: ((a &amp;gt;= &apos;사랑&apos;::text) AND (a &amp;lt; &apos;사랒&apos;::text))&lt;/div&gt;&lt;div&gt;(2개 행)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;mydb=# explain select a from koword where a between &apos;사랑&apos; and &apos;사랑힣&apos;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; QUERY PLAN&lt;/div&gt;&lt;div&gt;------------------------------------------------------------------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;Index Only Scan using koword_a_i on koword&amp;nbsp; (cost=0.28..4.30 rows=1 width=9)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Index Cond: ((a &amp;gt;= &apos;사랑&apos;::text) AND (a &amp;lt;= &apos;사랑힣&apos;::text))&lt;/div&gt;&lt;div&gt;(2개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 두가지 검색방법 - 범위연산, like 연산 모두 같은 인덱스를 사용해서 탐색할 수 있는 방법은&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;1. 데이터베이스 collate 가 C 가 아닌 경우에는&amp;nbsp;&lt;/div&gt;&lt;div&gt;인덱스를 만들 때 해당 칼럼의 정렬 규칙을 C로 임의로 지정하고,&lt;/div&gt;&lt;div&gt;탐색 쿼리에서 찾을 문자열 뒤에 그 정렬 규칙이 C라로 일일히 지정하는 방법입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;mydb=# drop index koword_a_i;&lt;/div&gt;&lt;div&gt;DROP INDEX&lt;/div&gt;&lt;div&gt;mydb=# drop index koword_a_i2;&lt;/div&gt;&lt;div&gt;DROP INDEX&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;mydb=# create index koword_a_i on koword (a collate &quot;C&quot;);&lt;/div&gt;&lt;div&gt;CREATE INDEX&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;mydb=# explain select a from koword where a &amp;gt;= &apos;사랑&apos; collate &quot;C&quot; and a &amp;lt; &apos;사랒&apos; collate &quot;C&quot;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;QUERY PLAN&lt;/div&gt;&lt;div&gt;------------------------------------------------------------------------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;Index Only Scan using koword_a_i on koword&amp;nbsp; (cost=0.28..4.30 rows=1 width=9)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Index Cond: ((a &amp;gt;= &apos;사랑&apos;::text COLLATE &quot;C&quot;) AND (a &amp;lt; &apos;사랒&apos;::text COLLATE &quot;C&quot;))&lt;/div&gt;&lt;div&gt;(2개 행)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;mydb=# explain select a from koword where a like &apos;사랑&apos; collate &quot;C&quot;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; QUERY PLAN&lt;/div&gt;&lt;div&gt;------------------------------------------------------------------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;Index Only Scan using koword_a_i on koword&amp;nbsp; (cost=0.28..4.30 rows=1 width=9)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Index Cond: (a = &apos;사랑&apos;::text)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Filter: (a ~~ &apos;사랑&apos;::text COLLATE &quot;C&quot;)&lt;/div&gt;&lt;div&gt;(3개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;2. 다른 방법은 데이터베이스의 collate 값을 처음부터 C로 지정하는 방법입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;div&gt;mydb=# \c postgres&lt;/div&gt;&lt;div&gt;접속정보: 데이터베이스=&quot;postgres&quot;, 사용자=&quot;postgres&quot;.&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# \l&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;데이터베이스 목록&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;이름&amp;nbsp; &amp;nbsp; |&amp;nbsp; 소유주&amp;nbsp; | 인코딩 | 로케일 제공자 |&amp;nbsp; Collate&amp;nbsp; &amp;nbsp;|&amp;nbsp; &amp;nbsp;Ctype&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;-----------+----------+--------+---------------+-----------&lt;/div&gt;&lt;div&gt;&amp;nbsp;postgres&amp;nbsp; | postgres | UTF8&amp;nbsp; &amp;nbsp;| libc&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | C&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | ko_KR.utf8&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# create table koword (a text);&lt;/div&gt;&lt;div&gt;CREATE TABLE&lt;/div&gt;&lt;div&gt;postgres=# \copy koword from &apos;kodic.txt&apos;&lt;/div&gt;&lt;div&gt;COPY 5966&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# create index koword_a_i on koword (a);&lt;/div&gt;&lt;div&gt;CREATE INDEX&lt;/div&gt;&lt;div&gt;postgres=# explain select a from koword where a &amp;gt;= &apos;사랑&apos; and a &amp;lt; &apos;사랒&apos;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; QUERY PLAN&lt;/div&gt;&lt;div&gt;------------------------------------------------------------------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;Index Only Scan using koword_a_i on koword&amp;nbsp; (cost=0.28..4.30 rows=1 width=9)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Index Cond: ((a &amp;gt;= &apos;사랑&apos;::text) AND (a &amp;lt; &apos;사랒&apos;::text))&lt;/div&gt;&lt;div&gt;(2개 행)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;postgres=# explain select a from koword where a like &apos;사랑%&apos;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; QUERY PLAN&lt;/div&gt;&lt;div&gt;------------------------------------------------------------------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;Index Only Scan using koword_a_i on koword&amp;nbsp; (cost=0.28..4.30 rows=1 width=9)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Index Cond: ((a &amp;gt;= &apos;사랑&apos;::text) AND (a &amp;lt; &apos;사랒&apos;::text))&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;Filter: (a ~~ &apos;사랑%&apos;::text)&lt;/div&gt;&lt;div&gt;(3개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;3. SQL_ASCII 문자세트&lt;/h2&gt;&lt;div&gt;가끔, 나는 이런 것 신경 안쓰고 그냥 SQL_ASCII 문자세트로 지정해서 잘 쓰고 있는데,&amp;nbsp;&lt;/div&gt;&lt;div&gt;SQL_ASCII를 사용하면, 한글 뿐만 아니라, 모든 문자들들 다 잘 처리할 수 있고, 인덱스도 아주 잘 사용할 수 있고, 게다가 문자열의 저장공간도 utf-8 문자세트를 쓰지 않아, 한글인 경우는 1바이트도 줄일 수 있어 아주 좋은데 왜 SQL_ASCII 문자세트 사용에 대해서는 말하지 않는가?&amp;nbsp;&lt;/div&gt;&lt;div&gt;라는 질문을 받습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;문자세트의 시작은 문자를 저장할 때 &apos;무엇을 문자라 할 것인가?&apos; 에서 출발합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&apos;안녕하세요&apos; 는 한글로 다섯글자입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이게 가장 기본입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;SQL_ASCII 문자세트는 8비트 정보를 주고받기 위한 128개의 컴퓨터 기준의 문자세트일 뿐입니다. 그 안에는 한글도, 간체도, 히라가나도 고려하지 않았습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;뭐 알아서 잘 쓰겠다면, 뭐라 할 말은 없지만, 요즘처럼 전세계의 모든 문자를 처리해야하는 상황에서 이것을 고집하는 것은 위험한 것임에는 분명합니다.&amp;nbsp;&lt;/div&gt;&lt;h2&gt;4. 결론&lt;/h2&gt;&lt;div&gt;여느 다른 관계형 데이터베이스에서처럼 between 연산이나, like 연산을 사용하는데 있어 문자열 칼럼을 사용하는 인덱스를 편하게 사용하려면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;PostgreSQL 데이터베이스의 lc_collate 값은 C 여야합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;문제는 initdb 작업부터 이것을 염두해 두지 않았다면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;사용할 데이터베이스를 만들 때 template 데이터베이스로 template0 를 사용해야합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;create database mydb template template0 lc_collate &quot;C&quot;;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이런 귀찮음도 피하고자 한다면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;initdb 작업 전 그 명령을 실행할&amp;nbsp; OS 환경 설정이 문자세트는 utf8 로 문자정렬은 C로 하는 것이 정신건강에 좋습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;export LANG=ko_KR.UTF-8&lt;/div&gt;&lt;div&gt;export LC_COLLATE=C&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 있으면 되겠죠.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;</description>
            <pubDate>Mon, 06 Nov 2023 01:05:11 +0900</pubDate>
            <guid>http://postgresql.kr/blog/collate_for_pg.html</guid>
        </item>
        <item>
            <title>PostgreSQL이 사용하는 대표 Scan과 그 Cost</title>
            <link>http://postgresql.kr/blog/pg_scan_cost.html</link>
            <description>&lt;h1&gt;PostgreSQL이 사용하는 대표 Scan과 그 Cost&lt;/h1&gt;&lt;div&gt;explain 명령으로 쿼리 실행 계획을 보면 각 세부 작업으로 어떤 것을 한다는 식으로 그 단계에서 진행되는 작업을 설명하고 있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;그 작업 설명은 크게 Scan, Join, Sort, Aggregate, ...... 등 쿼리의 각 요소들이 하는 일입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글은 저 작업들 가운데 가장 많이 보이는 Scan과 그 가운데 대표적인 Scan들의 작업비용을 실행계획기는 어떻게 추측하는가에 대한 이야기입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;explain 명령 결과로 보이는 Scan들&lt;/h2&gt;&lt;div&gt;PostgreSQL explain 명령 결과는 읽고 이해하기가 많이 힘듭니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;특히 psql 같은 텍스트 기반 툴에서는 거의 암호문 수준입니다.&lt;/div&gt;&lt;div&gt;처음 접하는 사람은 대부분 당혹감을 감출 수 없습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글의 대표 사진은 그나마 explain을 시각화 하는데 비교적 충실한 pgAdmin으로 출력한&amp;nbsp;&lt;br&gt;&lt;/div&gt;&lt;div&gt;explain select * from pg_tables 명령 결과입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;해당 작업을 psql에서 하면 다음과 같이 보입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;# explain select * from pg_tables where tablename = &apos;pg_class&apos;;
                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------
 Nested Loop Left Join  (cost=0.27..10.43 rows=1 width=260)
   Join Filter: (t.oid = c.reltablespace)
   -&amp;gt;  Nested Loop Left Join  (cost=0.27..9.38 rows=1 width=140)
         Join Filter: (n.oid = c.relnamespace)
         -&amp;gt;  Index Scan using pg_class_relname_nsp_index on pg_class c  (cost=0.27..8.29 rows=1 width=80)
               Index Cond: (relname = &apos;pg_class&apos;::name)
               Filter: (relkind = ANY (&apos;{r,p}&apos;::&quot;char&quot;[]))
         -&amp;gt;  Seq Scan on pg_namespace n  (cost=0.00..1.04 rows=4 width=68)
   -&amp;gt;  Seq Scan on pg_tablespace t  (cost=0.00..1.02 rows=2 width=68)&lt;/pre&gt;&lt;div&gt;여기서 &apos;Index Scan&apos;, &apos;Seq Scan&apos; 이렇게 보이는 것들에는 어떤 종류가 있는지 살펴보기 위해 소스 코드를 뒤졌습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;src/backend/commands/explain.c 에 있더군요.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음은 PostgreSQL에서 다루는 Scan 종류입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;Seq Scan : 힙 순차 검색&lt;/li&gt;&lt;li&gt;Sample Scan : tablesample 절이 있을 때&lt;/li&gt;&lt;li&gt;Index Scan : 인덱스로 찾아 힙 자료를 출력할 때&lt;/li&gt;&lt;li&gt;Index Only Scan : 인덱스 만으로 자료를 출력할 때&lt;/li&gt;&lt;li&gt;Bitmap Index Scan : 인덱스 찾기 결과를 비트맵으로 만들고&lt;/li&gt;&lt;li&gt;Bitmap Heap Scan : 만들어진 비트맵을 힙 ctid랑 비트맵 연산으로 자료를 뽑을 때&lt;/li&gt;&lt;li&gt;Tid Scan : where ctid = .... 일때&lt;/li&gt;&lt;li&gt;Tid Range Scan : where ctiid between ... 일때&lt;/li&gt;&lt;li&gt;Subquery Scan : 서브쿼리과 관계된 scan&lt;/li&gt;&lt;li&gt;Function Scan : 집합 반환 함수 사용 때&lt;/li&gt;&lt;li&gt;Table Function Scan : 테이블 반환 함수 사용 때&lt;/li&gt;&lt;li&gt;Values Scan : 말그대로&lt;/li&gt;&lt;li&gt;CTE Scan : 말그대로&lt;/li&gt;&lt;li&gt;Named Tuplestore Scan : 아직 안 찾아봤고&lt;/li&gt;&lt;li&gt;WorkTable Scan : 아직 안 찾아봤고&lt;/li&gt;&lt;li&gt;Foreign Scan : 외부 테이블 참조 때&lt;/li&gt;&lt;li&gt;Custom Scan : 아에 모르겠고&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;이들 중 가장 많이 사용되고, 기초가 되는 것은 Seq, Index, Bitmap Index &amp;amp; Bitmap Heap Scan 정도입니다. 이 글은 이들 설명과 explain 에서 보이는 그 cost를 설명해 볼까 합니다.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;explain의 cost&lt;/h2&gt;&lt;div&gt;explain 명령은 해당 쿼리를 실행할 때, 각 세부 실행 계획 (일반적인 용어로 쿼리 스탭이라고 합니다.) 의 순서를 보여줍니다. 이 순서는 실행계획기가 여러가지 조건으로 총 비용을 계산해서 가장 싼 비용이 드는 순서입니다. 이 비용이 말 그대로 cost라고 하며,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 비용의 기준이 되는 것이 &apos;데이터 페이지 (8kb씩 나눠진 테이블의 한 영역 - 흔히 다른 관계형 데이터페이스에서는 이것을 데이터 블록이라고 합니다.) 하나를 디스크에서 메모리로 가져오는 작업을 1이라고 하자.&apos; 라는 것입니다.&lt;/div&gt;&lt;div&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;그리고 이 cost 계산을 위한 데이터베이스 환경 설정 매개 변수를 쭉 지정합니다.&amp;nbsp;&lt;/div&gt;&lt;pre&gt;# select name,setting from pg_settings where name like &apos;%cost&apos;;
          name           | setting
-------------------------+---------
 cpu_index_tuple_cost    | 0.005
 cpu_operator_cost       | 0.0025
 cpu_tuple_cost          | 0.01
 jit_above_cost          | 100000
 jit_inline_above_cost   | 500000
 jit_optimize_above_cost | 500000
 parallel_setup_cost     | 1000
 parallel_tuple_cost     | 0.1
 random_page_cost        | 4
 seq_page_cost           | 1&lt;/pre&gt;&lt;div&gt;pg_settings에서 cost로 끝나는 것을 찾으면 이렇습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;지면 관계상, short_desc 칼럼은 생략했습니다. 이 칼럼을 포함면 각 변수들의 설명을 간단하게 읽을 수 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;맨 밑에 있는 seq_page_cost가 바로 앞에서 설명한 그것입니다.&lt;/div&gt;&lt;div&gt;윗 값은 PostgreSQL 기본값입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;아주 간단합니다.&amp;nbsp;&lt;br&gt;인덱스를 사용해서 자료를 찾고, 그 실 자료가 있는 테이블에서 자료를 뽑는(random_page_cost) 는 seq_page_cost보다 4배 비싸다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;메모리에 가져온 데이터페이지에서 자료를 하나 처리하는 작업(cpu_tuple_cost)는 1/100 정도 싸다.&lt;/div&gt;&lt;div&gt;이런 식입니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;Seq Scan 테이블 전체 순차 탐색&lt;/h2&gt;&lt;div&gt;이것은 테이블 또는 구체화된 뷰를 그냥 처음부터 끝까지 쭉 읽는 것을 뜻합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;즉 cost는 해당 객체의 페이지수가 될 것입니다.&lt;br&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;경우는 두가지겠죠.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;하나는 조회 조건을 보니, 인덱스를 사용할 수 없는 경우이거나,&amp;nbsp;&lt;/li&gt;&lt;li&gt;자료량이 많지 않아 데이터 페이지가 네 개보다 작아서 인덱스를 사용하면 비용이 더 드는 경우.&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;(여기서 주의해야할 것은 실행계획기는 그 테이블의 실제 페이지가 몇개인지를 직접 계산하지 않고, pg_class.relpages 값으로 판단합니다. 그런데, 이 값은 테이블 통계 정보가 수집되기 전까지는 마지막 수집했을 때의 값이고, 한번도 통계 수집 작업이 일어나지 않았다면, 0 입니다. 0인 경우는 내부적으로 기본 비용 계산으로 진행해서 실행계획을 만들어버립니다. 즉, 자료가 갑자기 많이 변경된 경우라면, 그것도 용량이 큰 테이블이라면, 통계 수집기가 그 변경분을 반영하기 전까지는 엉뚱한 실행계획을 짤 가능성이 있다는 것을 기억해야합니다. 통계수집기가 사라진 15버전에서는 어떤 상황이 벌어질지 아직 경험해 보지는 못했습니다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;실세계 자료라면 대부분 테이블은 인덱스를 사용하면 비용이 더 들 정도로 자료량이 많지 않은 경우는 잘 없습니다. 그래서, 쿼리 튜닝을 할 때 제일 먼저 살펴보는 부분은 인덱스를 사용 사용하는 것이 좋은데, 이 테이블 전체 순차 탐색을 하고 있는 부분이 있는지를 살펴보는 것입니다.&lt;/div&gt;&lt;pre&gt;-- 먼저 pg_class 테이블의 통계 정보를 수집합니다.
ioseph=# analyze pg_class;
ANALYZE
ioseph=# select relpages from pg_class where relname = &apos;pg_class&apos;;
 relpages
----------
       14
(1개 행)
-- pg_class 테이블은 총 14 페이지입니다.
-- 이제 테이블 전체 순차 탐색을 하도록 인덱스 탐색과 비트맵 탐색을 사용하지 않겠다고 설정합니다.
ioseph=# set enable_indexscan = off;
SET
ioseph=# set enable_bitmapscan = off;
SET
-- 실행 계획을 봅니다.
ioseph=# explain select * from pg_class where relname = &apos;pg_class&apos;;
                        QUERY PLAN
-----------------------------------------------------------
 Seq Scan on pg_class  (cost=0.00..19.19 rows=1 width=265)
   Filter: (relname = &apos;pg_class&apos;::name)
(2개 행)&lt;/pre&gt;&lt;div&gt;pg_class 라는 테이블에서 relname 칼럼 값이 &apos;pg_class&apos;인 자료를 보여달라고 했을 때, 실행 계획기는 테이블 전체 순차 탐색을 할 것이고, 이 계획을 짠 근거는 총 비용이 19.19이기 때문이다고 합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그리고 이 쿼리의 결과는 자료가 하나 나올 것이다(rows=1)고 예측하고, 그 출력 자료의 크기는 265 바이트일 것이다(width=265) 라고 예측했습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;저 19.19 는&lt;/div&gt;&lt;pre&gt;# select (reltuples * 0.01) + (reltuples * 0.0025) + (relpages * 1) from pg_class where relname = &apos;pg_class&apos;;
 ?column?
----------
  19.1875&lt;/pre&gt;&lt;div&gt;입니다.&lt;/div&gt;&lt;div&gt;0.01 과, 0.0025, 1 상수 값은 이 글 위에서 찾아보세요.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;Index Scan 인덱스를 이용한 임의 힙 페이지 탐색&lt;/h2&gt;&lt;div&gt;드디어 힙 페이지라는 생소한 단어가 나옵니다. 별거 없습니다. 이 글에서 이야기한 테이블의 데이터 페이지로 이해하면 됩니다.&lt;/div&gt;&lt;div&gt;즉, 인덱스 탐색은 인덱스 객체도 검색하고, 그 테이블의 데이터 페이지도 읽는 작업을 말합니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL에서 다루는 자료는 가장 저수준으로 ctid라는 단위로&amp;nbsp; 처리됩니다. 다른 RDBMS처럼 인스턴스 전체를 통틀어 이 자료의 고유 식별값이 없습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;즉, 어느 데이터베이스에 어느 테이블에 ctid.&lt;/div&gt;&lt;div&gt;이렇게 처리되어야 고유 식별값이 됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 ctid 값이 인덱스의 key-value 관점에서 value가 됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;즉 PostgreSQL 에서 사용하는 일반적인 인덱스(btree 인덱스 - 정확하게는 b+tree입니다)는 &apos;이 자료는 테이블의 어느 페이지의 몇 번째 자료다&apos; 라는 정보를 따로 모아둔 것입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;인덱스 내부는 pageinspect 라는 확장 모듈로 살펴 볼 수 있습니다.&amp;nbsp;&lt;/div&gt;&lt;pre&gt;-- pageinspect 확장 모듈을 설치합니다.
ioseph=# create extension pageinspect ;
CREATE EXTENSION
-- pg_class 테이블은 어떤 인덱스를 사용하는지 살펴봤습니다.
ioseph=# \d pg_class
                   &quot;pg_catalog.pg_class&quot; 테이블
       필드명        |     형태     | 정렬규칙 | NULL허용 | 초기값
---------------------+--------------+----------+----------+--------
 oid                 | oid          |          | not null |
 relname             | name         |          | not null |
 relnamespace        | oid          |          | not null |
 relfrozenxid        | xid          |          | not null |
......
 relminmxid          | xid          |          | not null |
 relacl              | aclitem[]    |          |          |
 reloptions          | text[]       | C        |          |
 relpartbound        | pg_node_tree | C        |          |
인덱스들:
    &quot;pg_class_oid_index&quot; PRIMARY KEY, btree (oid)
    &quot;pg_class_relname_nsp_index&quot; UNIQUE CONSTRAINT, btree (relname, relnamespace)
    &quot;pg_class_tblspc_relfilenode_index&quot; btree (reltablespace, relfilenode)
-- 릴레이션 이름에 대한 인덱스가 pg_class_relname_nsp_index 인 것을 보고,
-- 이 인덱스의 두번째 페이지에는 어떤 자료가 들어있는지 살펴봅니다.
-- (인덱스의 첫번째 페이지는 테이블의 페이지와 달리 인덱스 그 자체에 대한 정보가 담겨 있어 자료가 없습니다.
ioseph=# select itemoffset, htid,
  convert_from(
    decode(
      replace(substr(data, 1, position(&apos; 00&apos; in data)), &apos; &apos; , &apos;&apos;), 
      &apos;hex&apos;),
    &apos;utf8&apos;) from bt_page_items(&apos;pg_class_relname_nsp_index&apos;, 1) limit 10;
 itemoffset |  htid   |           convert_from
------------+---------+-----------------------------------
          1 |         | pg_range
          2 | (10,8)  | _pg_foreign_data_wrappers
          3 | (10,11) | _pg_foreign_servers
          4 | (10,5)  | _pg_foreign_table_columns
          5 | (10,14) | _pg_foreign_tables
          6 | (10,17) | _pg_user_mappings
          7 | (4,25)  | a
          8 | (8,19)  | administrable_role_authorizations
          9 | (8,18)  | applicable_roles
         10 | (8,21)  | attributes
(10개 행)&lt;/pre&gt;&lt;div&gt;첫번째 자료는 다음 페이지는 &apos;pg_range&apos; 에서 시작한다는 다음 페이지 힌트고 실 자료는 릴레이션 이름이 &apos;_pg_foreign_data_wrappers&apos; 인 자료는 pg_class 테이블의 10번 페이지 8번째 자료다고 알려줍니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, select * from pg_class where relname = &apos;_pg_foreign_data_wrappers&apos; 이렇게 쿼리한다면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp;pg_class_relname_nsp_index 인덱스에서 key가 &apos;_pg_foreign_data_wrappers&apos; 인 htid (힙테이블ID)를 구해서, 드디어 임의 페이지 접근(random page)을 해서 거기서 8번째 자료를 보여줍니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, 인덱스 탐색은 아무리 최소로 진행한다고 해도,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;이 인덱스가 어떤 인덱스인지 알아야 하고, (인덱스 객체의 첫번째 블록은 읽어야하고)&lt;/li&gt;&lt;li&gt;그 인덱스에서 원하는 자료를 찾아야하고, (루트 - 브랜치 - 리프)&amp;nbsp;&lt;/li&gt;&lt;li&gt;그래서 찾은 테이블의 블록을 읽어야 합니다.&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;그래서 random_page_cost를 4 정도로 하는 것이 기본값입니다.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음은 pg_class 테이블에서 relname이 pg_class인 자료를 찾는 쿼리의 실행 계획입니다.&amp;nbsp;&lt;/div&gt;&lt;pre&gt;--이제 인덱스 탐색을 할 수 있도록 설정을 바꿉니다.
ioseph=# set enable_indexscan = on;
SET
ioseph=# explain select * from pg_class where relname = &apos;pg_class&apos;;
                                         QUERY PLAN
---------------------------------------------------------------------------------------------
 Index Scan using pg_class_relname_nsp_index on pg_class  (cost=0.27..8.29 rows=1 width=265)
   Index Cond: (relname = &apos;pg_class&apos;::name)&lt;/pre&gt;&lt;div&gt;예상 했던 대로 pg_class_relname_nsp_index 라는 인덱스를 사용해서 인덱스 탐색을 하겠다고 알려줍니다. 그 비용은 총 8.29, 자료는 한개, 그 한 자료의 크기는 265바이트.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;인덱스를 사용하지 않았다가, 인덱스를 사용하면,&lt;/div&gt;&lt;div&gt;전체 비용은 19.19에서 8.29로 거의 반 정도 비용이 싸진다고 실행계획기는 예측합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(random_page_cost 값을 세배로 늘린다면, 실행 계획기는 seq scan을 선택하겠죠. 이런 식입니다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 인덱스 탐색은 기억해야 하는 것이, 인덱스로 해당 자료의 tid를 찾으면 바로 그곳으로 가서 원하는 자료를 준비한다는 것입니다. 그래서, order by 를 지정하지 않아도 그 인덱스의 순서대로 자료가 출력됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;즉, between 연산 처럼 여러 자료를 뽑을 경우라면, 여러 데이터 페이지를 왔다 갔다 하면서 읽었던 페이지를 또 읽는 일이 벌어집니다. 이런 비용도 줄여보고자 등장한 것이 비트맴 스캔입니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;Bitmap Scan 비트맵을 이용한 탐색&lt;/h2&gt;&lt;div&gt;PostgreSQL의 아주 독특한 탐색 방법입니다.&lt;/div&gt;&lt;div&gt;일단 인덱스를 탐색하면서 조건에 맞는 자료는 1, 아닌 것은 0 이렇게 페이지 단위 비트맵을 만들고(Bitmap Index Scan),&lt;/div&gt;&lt;div&gt;이 비트맵과 테이블의 각 페이지 tid 정보가 담긴 부분과 AND 연산을 한 결과를 뽑아 그 실 자료를 뽑는(Bitmap Heap Scan) 방식입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;테이블의 실 자료의 정렬도가 낮은데 (시계열 자료가 아니라는 말이죠), 그 칼럼 기준으로 검색은 해야겠는데, 굳이 인덱스 탐색(인덱스와 테이블 페이지를 왔다 갔다 하는 일)을 안해도 될 어느 정도(?)의 총 자료량이여서 쿼리 중에 임시로 메모리에 비트맵을 만들어될 만큼의 자료라고 예측 되면 이 비트맴 스캔을 실행계획기가 선택합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;(글이 너무 어렵네요. 아직도 정확하게 글 쓴 사람도 제대로 이해하지 못했다는 것입니다. - 사기는 말 많음에서 시작합니다. 음...)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;아무튼 다 만들어진 비트맵 기준으로 데이터 페이지와 비교하기 때문에, 자료 결과는 데이터 페이지 순서로 자료가 나오게 됩니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음은 index scan 과, bitmap scan&amp;nbsp; 실행 계획 모습입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;pre&gt;ioseph=# explain select relname,relpages from pg_class where relname &amp;gt;= &apos;pg_a&apos; and relname &amp;lt; &apos;pg_aa&apos;;
                                         QUERY PLAN
--------------------------------------------------------------------------------------------
 Index Scan using pg_class_relname_nsp_index on pg_class  (cost=0.27..8.29 rows=1 width=68)
   Index Cond: ((relname &amp;gt;= &apos;pg_a&apos;::name) AND (relname &amp;lt; &apos;pg_aa&apos;::name))
(2개 행)

-- 윗 쿼리를 비트맵 스캔으로 유도하려면, 임시로 인덱스 스캔을 사용하지 않는다고 설정합니다.
-- 그러면, 비트맵 스캔을 선택하던가 시퀀스 스캔을 선택하겠죠.
ioseph=# set enable_indexscan = off;
SET
ioseph=# explain select relname,relpages from pg_class where relname &amp;gt;= &apos;pg_a&apos; and relname &amp;lt; &apos;pg_aa&apos;;
                                       QUERY PLAN
-----------------------------------------------------------------------------------------
 Bitmap Heap Scan on pg_class  (cost=4.28..8.30 rows=1 width=68)
   Recheck Cond: ((relname &amp;gt;= &apos;pg_a&apos;::name) AND (relname &amp;lt; &apos;pg_aa&apos;::name))
   -&amp;gt;  Bitmap Index Scan on pg_class_relname_nsp_index  (cost=0.00..4.28 rows=1 width=0)
         Index Cond: ((relname &amp;gt;= &apos;pg_a&apos;::name) AND (relname &amp;lt; &apos;pg_aa&apos;::name))
(4개 행)&amp;nbsp;&lt;/pre&gt;&lt;div&gt;이렇게 0.01 차이로 왔다갔다합니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;마치며&lt;/h2&gt;&lt;div&gt;관계형 데이터베이스의 실행계획 비용 계산 영역은 정말 어렵습니다.&lt;/div&gt;&lt;div&gt;게다가 이 실행계획을 결정하는 각종 환경 변수 값의 기본값은 2023년을 살아가고 있는 지금에는 현실적이지 않는 부분이 있습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;random_page_cost가 정말 seq_page_cost의 네 배나 될까?&amp;nbsp; 라고 하면, 고속 비휘발성 메모리(&amp;nbsp;NVMe - Non-Volatile Memory Express)를 주 저장장치로 쓰는 환경에서는 현실적인 값이 아님이 분명합니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;아무튼 어설프게나마 PostgreSQL 실행계획에서 Scan 관련해서 살펴보면서 그 실행계획을 짜는데 기준이 되는 비용에 대해서 살펴봤습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;</description>
            <pubDate>Thu, 24 Aug 2023 03:59:21 +0900</pubDate>
            <guid>http://postgresql.kr/blog/pg_scan_cost.html</guid>
        </item>
        <item>
            <title>pgbouncer 이야기</title>
            <link>http://postgresql.kr/blog/pgbouncer.html</link>
            <description>&lt;h1&gt;pgbouncer 이야기&lt;/h1&gt;&lt;h2&gt;PostgreSQL 세션 프로세스 연결 시간 관리&lt;/h2&gt;&lt;div&gt;PostgreSQL 데이터베이스 서버는 클라이언트가 서버로 접속하면 그에 상응하는 백엔드 프로세스라는 것을 만들어서 그 백엔드 프로세스가 클라이언트의 요청에 응답하는 방식으로 운영됩니다. 흔히 이 백엔드 프로세스를 세션 프로세스라고 합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, 클라이언트가 100개가 각각 100개의 연결을 하고 있다면, 100개의 이 백엔드 프로세스가 데이터베이스 서버가 운영되고 있는 OS에 만들어져 돌아가는 샘이죠. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 백엔드 프로세스는 pg_stat_activity 뷰나, 리눅스 환경이라면, OS ps 명령이나 top 명령으로 살펴보기도 합니다. pg_stat_activity 뷰로는 그 백엔드 프로세스의 각 개별 OS 자원 사용률을 살펴볼 수 없지만, OS에서 각 프로세스별 자원 사용률을 살펴보면 상식적으로는 이해가 되지 않는 현상을 가끔 마주하기도 합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;세션이 오랫동안 연결해 있으면, 해당 백엔드 프로세스의 RSS(메모리 점유량) 값이, 이 보다 연결 시간이 적은 백엔드 프로세스보다 많고, 이 값이 접속 시간에 비례해서 계속 커진다는 것이죠. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이는 리눅스 환경에서 이 RSS 값은 그 프로세스가 사용하는 공유 메모리 영역도 함께 포함되어 계산되고 보여지기 때문입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;다행이 14버전부터 &lt;code class=&quot;function&quot;&gt;pg_log_backend_memory_contexts()&lt;/code&gt;함수로 이 백엔드 프로세스의 메모리 내부를 볼 수 있어서, 이 값과 RSS 차이가 있는 것을 설명할 수 있어서 다행이지만, 그 이전 버전에서 이 부분을 PostgreSQL을 잘 모르는 사람들에게 설명하기가 쉽지 않았습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이런 긴 이야기 끝에 나온 말이 &quot;PostgreSQL에서는 주기적인 연결 세션의 재접속이 필요하다&quot; 는 것입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;어떤 서비스에서는 1년을 계속 연결해 있어도 괜찮은데, 어떤 서비스는 하루만 지나면, OS의 OOM Killer가 백엔드 프로세스를 중지시키버려 수시로 데이터베이스가 재초기화 되는 일이 일어나기도 합니다.&amp;nbsp; 이런 문제를 피할 수 있는 방법은 RSS 값이 큰 백엔드 프로세스 기준으로 그 연결을 끊고 재연결하는 것이 가장 단순한 해결 방법입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 방법으로 가장 무식한 방법은 DB를 사용할 때만 응용 프로그램이 DB로 접속하고, DB 작업이 끝나면 DB 연결을 끊는 것이겠죠. 하지만, 이 방법은 과도한 연결 비용을 감수해야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;연결 관리자(connection pooler for database)의 필요성&lt;/h2&gt;&lt;div&gt;앞부분에서 다뤘듯이 PostgreSQL은 다중 프로세스 기반 서버이며, 각 세션 연결은 그 연결 요청이 받아들여질 때, 그 때 실시간으로 클라이언트와 대응하는 백엔드 프로세스를 만들어 처리합니다. 이 때문에, 갑자기 연결을 원하는 클라이언트들이 많아지면, 데이터베이스 서버는 이 백엔드 프로세스를 만드는 비용이 갑자기 커지게 되고 이것이 데이터베이스 성능을 저하시키는 결정적인 요인으로 작용합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;아파치 웹 서버처럼 미리 몇개의 준비된 백엔드 프로세스를 만들어 두고, 클라이언트 요청에 맞춰 백엔드 프로세스를 할당하는 방식이 아닙니다. 아직까지는 이 방식으로 연결 처리하는 것은 고려대상이 아닌 것 같습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;그래서, 초창기부터 PostgreSQL은 엔터프라이즈 환경에서 사용한다면, 3rd party 연결 관리 솔루션을 사용하기를 권고하고 있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 데이터베이스 연결 관리자는 대부분 응용프로그램을 구현하는 프로그래밍 언어에 종속적입니다. 응용 프로그램이 데이터베이스 연결을 하고 그 연결 객체를 이용해서 쿼리를 보내고 결과를 받아 처리하고 하는 것들이 대부분 프로그래밍 언어에서는 표준화 되어있고, 각 데이터베이스별로 이 표준안을 지켜 각 데이터베이스에 맞는 연결 방법을 구현하기 때문입니다.&amp;nbsp; 단순하게 생각하면, 그냥 내가 쓰는 프로그래밍 언어에서 쓸 수 있는 연결 관리자를 쓰면 문제가 없을 것이다고 생각하고, 별 탈 없이 쓰고 있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;하지만, PostgreSQL은 앞에서 언급했듯이 다중 프로세스 기반 백엔드 처리 기법의 한계로 이 연결 관리자를 쓸 때 꼭 고려해야할 사항이 하나 더 있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;지금 놀고 있는 연결이 일정 시간 이상 연결 되어있었다면, 그 연결을 끊고, 다른 새 연결을 준비할 수 있는가? 입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;일반적으로 idle timeout 이라고 합니다. 즉, 개발을 시작하면서 데이터베이스 연결 솔루션을 사용한다면, 제일 먼저 확인해 봐야할 것이 일정 시간이 지난 백엔드 세션이 있는가입니다. 만일 있다면 그 오래된 백엔드 세션이 자동으로 정리 될 수 있도록 그 연결 솔루션 설정을 조정하는 것입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;몇몇 연결 솔루션에서는 아에 이런 기능을 제공하지 않는 것도 있으니, 꼭 살펴보아야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;하지만, 놀고 있는 연결을 잘 정리한다고 하더라도 문제가 하나 더 남아 있습니다.&lt;/div&gt;&lt;div&gt;바로 놀고 있지 않고, 너무도 열심히 잘 쓰고 있는 연결을 어떻게 버리고 새 연결을 사용할 수 있는가? 인데. 바로 연결의 최대 수명 주기를 제어하는 것입니다. 어떤 이유로도 이 연결은 1시간을 넘길 수 없다. 이런 식의 설정입니다. 일반적인 용어로는 life timeout 입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;대부분의 연결 솔루션은 이런 PostgreSQL 특성을 고려하지 않기 때문에, life timeout에 대해서는 제어할 수 있는 방법을 제공하고 있지 않습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;그래서, 결국 pgbouncer가 등장하게 됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;pgbouncer는&lt;/h2&gt;&lt;div&gt;pgbouncer는 PostgreSQL 서버가 엄청 많은 (대략 80 이상) 연결 상태를 유지하고 있게 되면 성능이 급격하게 떨어지는 초창기 버전들의 한계를 극복하기 위해서&amp;nbsp; 만들어졌습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;요즘 사용하는 최신 PostgreSQL 서버들은 이 문제를 많이 개선했지만, 앞에서 이야기한 엔터프라이즈 환경에서 근본적인 연결 관리 솔루션이 필요하다는 것은 여전히 유효하기 때문에, 요즘은 연결 관리 솔루션 본연의 목적인 미리 연결 확보해 두고 클라이언트가 요청하면 준비된 연결을 제공하고, 다 쓴 연결을 다시 연결 대기 상태로 두고, 각 설정에 맞춰 timeout 작업을 해서 데이터베이스 서버쪽 부담을 줄이는 역할에 보다 충실하도록 계속 개선되고 있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;딱 여기까지입니다. 목적이 분명하고 기타 다른 기능들을 최대한 도입하지 않는 연결 관리자 본연의 일만하는 솔루션입니다. pgbouncer에는 auto failover 기능이 없습니다. load balance 기능도 없습니다. 쿼리 결과 집합 캐싱 기능도 없습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;pgbouncer의 최대 장점은 응용프로그램의 기존 DB 연결 설정을 하나도 바꾸지 않아도 된다는 점입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;최대 단점은 또 새로운 pgbouncer 서버(디비 운영자 관점에서 보면, 또 하나의 DB 서버가 운영 되는 식이거든요)를 운영해야한다는 점이겠죠.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;또 새로운 솔루션을 운영해야하는 부담감&lt;/h2&gt;&lt;div&gt;서비스 운영에서 문제가 생겼을 때, 살펴보아야 할 부분이 하나 더 늘었다는 것은 꽤 부담입니다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;특히나 그것이 오픈소스라면, 그 소프트웨어의 사용권과 사용할 버전과 문제 상황에서 기대 커뮤니티도 살펴야합니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;pgbouncer는 단일 버전 새 기능과 버그 픽스로 버전 관리를 하기 때문에, 무조건 가장 마지막 정식 배포된 버전을 사용하는 것이 무난합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;하지만, pgbouncer도 하나의 서비스를 제공하는 서버이며, 그 서버 동작이 이상해지면, 그 클라이언트 전체에 영향을 끼치기에, 서비스 장애 상황이나, 품질이 떨어지는 상황일 때 반드시 pgbouncer가 잘 동작하고 있는지를 확인해야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, 관리자는 결국 또 하나의 서버 관리 능력을 키워야 함을 의미합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;앞에서 다뤘듯이 pgbouncer는 pooler 기능에 집중한 서버입니다. 그래서, 초기 도입 안정화 작업만 잘 했다면, &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;클라이언트들이 잘 접속하는지와,&amp;nbsp;&lt;/li&gt;&lt;li&gt;자기가 클라이언트가 되는 디비 서버 쪽으로 잘 접속하는지, &lt;/li&gt;&lt;li&gt;현재 상태가 어떤지&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;이것만 잘 살펴보고 문제 상황을 잘 해결 할 수 있다면 운영 부담은 다른 서버군 소프트웨어보다 관리 비용이 적을 수 있습니다. &lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;br&gt;&lt;h2&gt;새로운 솔루션이 도입되어 응용프로그램을 바꿔야하는 부담감&lt;/h2&gt;&lt;div&gt;앞에서 이야기한 대로 pgbouncer 도입의 최대 장점은 응용프로그램 쪽에서 바꾸어야 할 부분이 전혀 없다는 것입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;물론 클라이언트와 pgbouncer 간 연결은 끊기지 않았지만, pgbouncer 내부 설정에 따라 pgbouncer와 db 인스턴스간 연결이 재 초기화 되는 설정이 있는데, 응용 프로그램이 그것을 고려하지 않은 db 기능을 사용하는 경우는 당연히 문제가 생기겠죠. &lt;br&gt;&lt;/div&gt;&lt;div&gt;대표적인 예가, prepare 구문을 사용하는 서버측 준비된 구문과, declare 구문을 사용하는 서버측 커서 같은 것들이겠죠. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이런 것을 사용한다면, pgbouncer 설정에서 응용 프로그램 - pgbouncer - db server 연결을 항상 보장하는 방식으로 운영해야할 것입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이런 응용프로그램이 db 인스턴스를 어떻게 쓰는지 모른다면, 도입 검증용 환경에서 충분히 검증을 해야할 것입니다. 이 때는 pgbouncer 로그와 db 인스턴스의 클라이언트 접속 로그를 부지런히 비교해 보아야합니다. 그리고 문제 상황들을 차근히 푼뒤 운영 환경에 적용해야할 것 같네요. (당연한 이야기지만)&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;pgbouncer 설명&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;만들기 &amp;amp; 설치&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;소스 구하고, https://pgbouncer.org&lt;/div&gt;&lt;div&gt;configure&amp;nbsp;&lt;/div&gt;&lt;div&gt;make &amp;amp;&amp;amp; make install&lt;/div&gt;&lt;div&gt;여느 오픈소스 빌드 작업과 크게 다르지는 않습니다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;pgbouncer는 네트워크 패킷 처리를 위해 event 라이브러리를 사용합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;그래서, libevent 패키지 관련 개발 패키지 (header 파일이 포함되어 있는 패키지)가 설치 되어 있어야 컴파일이 됩니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;환경 설정&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;make install 작업으로 만들어진 share/doc/pgbouncer 폴더 안에 샘플 환경 설정 파일이 있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이것을 적당한 곳으로 옮기고 적당하게 편집합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;su - postgres&lt;br&gt;&lt;/div&gt;&lt;div&gt;cd $PGDATA&lt;/div&gt;&lt;div&gt;cp /usr/local/share/doc/pgbouncer/pgbouncer.ini .&lt;/div&gt;&lt;div&gt;vim pgbouncer.ini&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;환경설정은 크게 다음과 같습니다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;응용프로그램이 pgbouncer로 접속하는 데이터베이스 정의 [databases]&lt;br&gt;곧 사용할 데이터베이스 정의가 되겠죠. 이 데이터베이스로 응용프로그램이 접속하면 pgbouncer는 내부적으로 db 인스턴스 어느 데이터베이스를 사용할 것인지를 맵핑하는 설정입니다. &lt;br&gt;&lt;/li&gt;&lt;li&gt;사용자별 개별 설정 [users]&lt;br&gt;응용프로그램이 pgbouncer로 접속하는 사용자별 개별 설정인데, 일반적으로 비워둡니다. (대부분 설정은 아래 pgbouncer 서버 전역 설정을 사용하기에 딱히 따로 지정할 상황을 고려하지 않아도 될 것 같습니다.)&lt;/li&gt;&lt;li&gt;pgbouncer 설정 [pgbouncer]&lt;br&gt;connection pooler 의 일반 설정이 이 부분에서 합니다. 일반적으로 다음 몇가지를 빼고는 기본값을 그대로 사용해도 무난합니다.&lt;br&gt;&lt;/li&gt;&lt;ol&gt;&lt;li&gt;auth_type, auth_file : 클라이언트 인증 관련 정보&lt;br&gt;auth_type = md5&lt;br&gt;auth_file = 패스워드파일 (scram-sha-256 비밀번호라면, PostgreSQL 서버의 pg_authid.rolpassword 값을 가져와서 사용하면 된다.)&lt;br&gt;&lt;/li&gt;&lt;li&gt;admin_users : pgbouncer 서버 각종 정보를 보거나 기능을 SQL로 제어하려고 할 때 접속하는 pgbouncer 데이터베이스로 접속 할 수 있는 사용자 이름&lt;/li&gt;&lt;li&gt;stats_users : pgbouncer 사용 통계 정보를 볼 수 있는 사용자 이름&lt;br&gt;&lt;/li&gt;&lt;li&gt;pool_mode : 서비스 성격에 따라 잘 지정해야 합니다.&lt;br&gt;session: 한 번 접속하면 클라이언트가 끊기기 전까지 연결 유지, 기본값&lt;br&gt;transaction: 연결을 트랜잭션 단위로&lt;br&gt;statement: SQL 한 구문 단위로&lt;br&gt;일반적인 웹 서비스라면, transaction 단위도 무난합니다.&lt;br&gt;&lt;/li&gt;&lt;li&gt;클라이언트 접속 제한을 위한 각종 설정 max_client_conn, min_pool_size, reserve_pool_size, max_db_connections&lt;br&gt;각 설정을 도움말을 보면서 적당하게 조정해서 사용해서 쓰면 됩니다. 기억해야할 것은 윗 네가지 설정을 기본값이 실운영환경에는 적합하지 않습니다. 서비스 성격에 맞게 조정하는 것이 바람직합니다. &lt;br&gt;예를 들어, DB 인스턴스가 실행되고 있는 호스트의 CPU 코어가 8개라면, max_db_connections 값을 그 값의 8배 이상을 잡지 않는 것이 좋습니다. 물론 이 가정은 pool_mode 가 transaction 이고, idle in transaction 상태가 거의 없는 작업만 있다는 가정 아래일 뿐입니다. 무조건 &apos;max_db_connections 값은 cpu 코어수의 8배다&apos; 이런 생각은 아주 위험합니다. 나머지 설정도 마찬가지입니다. &lt;br&gt;&lt;/li&gt;&lt;li&gt;다음 pgbouncer의 로그 관련 설정이 있는데, 기본 로그 설정이 많은 정보를 남깁니다. 그래서, 메인 DB 인스턴스 운영과 달리 pgbouncer는 대부분 운영 담당자에게는 방치형 서버가 될 가능성이 큽니다. 안정화 작업을 충분히 거쳤 더 이상 안봐도 되는 로그는 남기지 않겠다고 설정해야, 로그 분석시 편할 것입니다.&amp;nbsp; 도입 초기는 기본값으로 쓰고 안정화 되었다면, 이 로그 관련 설정을 바꾸어 운영할 필요가 있습니다. 안그러면, pgbouncer 서버 로그 관리도 해야하는 상황이 발생할 수도 있습니다.&lt;/li&gt;&lt;li&gt;각종 timeout 관련 설정, server_lifetime, server_idle_timeout 이 두 설정은 신경을 써야합니다. 앞에서 다룬 오래된 연결 세션 프로세스들이 많아서 발생하는 out of memory killer 작동으로 인한 데이터베이스 재초기화 문제를 피하는 결정적인 설정입니다. &lt;br&gt;client_idle_timeout, idle_transaction_timeout, query_timeout 같은 설정은 서비스의 성격에 따라 너무도 다양해서 이 글에서는 언급하지 않겠습니다. &lt;br&gt;&lt;/li&gt;&lt;li&gt;그외 나머지 저수준 tcp/ip 연결에 대한 각종 설정들인데, 이 부분은 대부분 OS 기본값을 사용하는것이 무난합니다. 물론 pgbouncer와, PostgreSQL 사이가 원거리 네트워크 환경이라면, 상황이 또 달라지겠지만, 일반적인 구성상으로는 이 두 소프트웨어 연결은 대부분 근거리 네트워크 환경이거나 같은 호스트내에서 운영 되기 때문에 신경을 안쓰는 것으로.&lt;/li&gt;&lt;/ol&gt;&lt;/ol&gt;&lt;div&gt;정리하면, &lt;br&gt;[databases] 영역에서 사용할 DB 설정하고, &lt;br&gt;&lt;/div&gt;&lt;div&gt;[pgbouncer] 영역에서 admin , 사용자 인증, pooler 개수, 연결 최대수, timeout 설정해서&lt;/div&gt;&lt;div&gt;쓰면 됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;실행&lt;/h3&gt;&lt;div&gt;pgbouncer 실행에 앞서, pgbouncer.ini에서 언급한 userlist.txt 파일을 준비해야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글에서는 auth_query 를 이용한 PostgreSQL 서버에서 사용자 정보를 가져와 인증하는 방식을 사용하지 않고, 그냥 일반 텍스트 파일에 &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&quot;사용자이름&quot; &quot;비밀번호&quot;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;양식의 사용자 목록 파일(userlist.txt)을 사용자 인증용으로 사용하려고 합니다. 그래서, &lt;br&gt;&lt;/div&gt;&lt;div&gt;이 파일을 하나 만들어야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&quot;postgres&quot; &quot;SCRAM-SHA-256$......&quot;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;뒤부분의 비밀번호 문자열은 &lt;br&gt;&lt;/div&gt;&lt;div&gt;DB 서버에서 &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;ALTER ROLE postgres PASSWORD &apos;mysecret&apos; &lt;br&gt;SQL을 실행하고, 그 결과가 저장된, pg_authid.rolpassword 값을 그대로 사용하면 됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음 pgbouncer 실행&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;nohup ./pgbouncer pgbouncer.ini &amp;gt; pgbouncer.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;일반적인 서버 실행방법과 다르지 않지만, pgbouncer.ini 파일 지정은 꼭 해야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;로그 파일 관리 부분이 고려되지 않았는데, &lt;br&gt;&lt;/div&gt;&lt;div&gt;윗 방식으로 실행하게 되면, 자꾸만 커져가는 로그파일 정리가 힘들어집니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;OS 기본 패키지인 logrotate 같은 도구로 로그파일 돌려쓰기를 하려면, pgbouncer.ini에서 로그파일 설정을 하고, pgbouncer는 데몬모드로 실행하고, pgbouncer 프로세스에 HUP 신호를 보내서 환경설정을 다시 반영하는 방식으로 풀어야할 것 같습니다.&amp;nbsp; postrotate 로 kill -HUP `cat pgbouncer.pid` 같은 명령을 등록해 주면 될 것 같네요.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;pgbouncer 데몬모드(-d 옵션 추가)가 충분히 안정적인가?에 대한 대답은 피합니다. &lt;br&gt;소프트웨어 안정성에 대한 검증은 쓰는 사람 몫이니까요.&lt;br&gt;(데몬 모드의 신뢰성을 의심한다면, OS 서비스 등록으로 푸는 방법도 있습니다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;나름대로 고민해본 최적의 도입 방안&lt;/h2&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;pgbouncer를 운영 할 때 기억해야할 것은&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;그 프로세스&lt;/h3&gt;&lt;p&gt;pgbouncer 프로세스가 어느 호스트에서 실행될 것인가입니다.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;응용프로그램 서버 쪽에 두어 pgbouncer를 병렬화 하는 방법&lt;/li&gt;&lt;li&gt;DB 인스턴스가 있는 쪽에 두어 전통적인 DB 인스턴스의 연결관리자 역할만 하는 방법&lt;/li&gt;&lt;li&gt;pgbouncer 인스턴스가 실행되는 전용 호스트를 두는 방법&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;각 구성 방법에는 각각 장단점이 있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글에서는 두번째 구성 방법을 소개합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이유는 다음과 같습니다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;pgbouncer 운영 주체가 DB 인스턴스 운영 주체와 같은 경우가 많습니다.&lt;/li&gt;&lt;li&gt;pgbouncer의 고가용성은 auto failover 정도만으로 충분하다고 판단 되었습니다. &lt;br&gt;&lt;/li&gt;&lt;li&gt;보안성을 높이는 측면에서 봐도 응용프로그램 서버 쪽에 있는 것보다 안전할 것입니다. &lt;br&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;이렇게 되면 응용프로그램 환경 변경 최소화를 목표로 하고 있을 때, PostgreSQL 리슨 포트와 pgbouncer의 리슨 포트가 충돌하게 됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;피하는 방법&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;PostgreSQL 인스턴스는 unix domain socket 접속만 허용해서, tcp listen 포트는 아에 쓰지 않는 설정을 합니다.&lt;br&gt;postgresql.conf 에서 listen_addresses = &apos;&apos;&lt;/li&gt;&lt;li&gt;pgbouncer 에서는 반대로 tcp listen 포트만 열고, unix domain socket을 사용하지 않는 설정을 합니다. &lt;br&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;이렇게 하면, &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;psql 명령만 실행하면, PostgreSQL 인스턴스로 접속하고, &lt;br&gt;&lt;/div&gt;&lt;div&gt;psql -h 127.0.0.1 로 실행하면, pgbouncer 인스턴스로 접속하는 전략입니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;모든 포트는 기본값 5432 입니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;pgbouncer 프로세스 소유주와, postgres 프로세스 소유주에 대한 OS 계정 문제도 있습니다.&amp;nbsp; 이 부분에 대한 견해도 각각인데, OS 계정을 분리할지, 같은 계정으로 할 것인지에 대한 견해도 각각이지만, 일단은 관리자가 같다면, OS 계정도 같은 것이 편할 것 같습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;환경 설정&lt;/h3&gt;&lt;div&gt;아래 설정은 웹 서비스용 - 서버측 prepare 구문도 없고, 커서도 없고, 2PC도 쓰지 않는 아주 일반적인 DB 인스턴스를 대상으로 작성되었습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;[databases]&lt;br&gt;mydb = host=/tmp user=ioseph&lt;/div&gt;&lt;div&gt;[users]&lt;br&gt;[pgbouncer]&lt;br&gt;logfile = /data/pg16/pgbouncer.log&lt;br&gt;pidfile = /data/pg16/pgbouncer.pid&lt;br&gt;listen_addr = *&lt;br&gt;listen_port = 5432&lt;br&gt;unix_socket_dir =&lt;br&gt;auth_type = md5&lt;br&gt;auth_file = /data/pg16/userlist.txt&lt;br&gt;admin_users = pgbouncer&lt;br&gt;stats_users = pgbouncer&lt;br&gt;pool_mode = transaction&lt;/div&gt;&lt;div&gt;ignore_startup_parameters = extra_float_digits&lt;br&gt;&lt;/div&gt;&lt;div&gt;min_pool_size = 5&lt;br&gt;reserve_pool_size = 3&lt;br&gt;log_stats = 0&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;윗 userlist.txt 파일에는 &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&quot;webuser&quot; &quot;비밀번호&quot;&lt;/div&gt;&lt;div&gt;&quot;pgbouncer&quot; &quot;pgbouncer관리자비밀번호&quot;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 두줄이 있습니다. webuser는 기존 응용프로그램이 사용하는 계정이름입니다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;응용 프로그램이 pgbouncer를 사용할 때는 달라지는 것은 pool_mode를 transaction으로 했기 때문에, 앞에서 다뤘듯이 연결이 보장된 상태에서 사용했던 기능들을 사용하지 않기 때문에, jdbc driver를 사용한다면, jdbc connection url에 prepareThreshold=0 옵션을 추가로 지정해야할 필요도 있습니다. 기본값이 5입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;관리 방법&lt;/h3&gt;&lt;div&gt;pgbouncer 관리자와, PostgreSQL 관리자가 같다면, 위와 같은 환경에서 꼭 기억해야하는 것은 pgbouncer를 통해서 db로 접속했는지, 직접 db로 접속했는지를 헷갈리지 말아야합니다. 이 헷갈림을 피하기 위해, pgbouncer 서버 관리자 이름은 postgres을 사용하지 않는 것도 좋은 방법입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;psql -U postgres mydb&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; # PostgreSQL 서버로 접속&lt;br&gt;&lt;/div&gt;&lt;div&gt;psql -h 127.0.0.1 -U pgbouncer pgbouncer&amp;nbsp;&amp;nbsp; # pgbouncer 관리DB로 접속&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;위 환경이라면, pgbouncer로 접속할 때는 어떤 방법으로 db 슈퍼유저 권한으로 접속할 방법이 없습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;데이터베이스 관리자 권한으로 작업이 필요할 때는 반드시 db 인스턴스가 실행된 호스트로 쉘 접속을 해서, unix domain socket으로 해당 데이터베이스에 접속해야한다는 것입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;pgbouncer를 통해서 슈퍼유저 권한 접속이 필요하다면, &lt;br&gt;&lt;/div&gt;&lt;div&gt;윗 설정 기준이라면, &lt;br&gt;&lt;/div&gt;&lt;div&gt;mydb 처럼 mydbdba 같은 새로운 데이터베이스 하나를 더 등록하고, 그 접속 정보에 슈퍼유저 정보를 지정하는 것입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;mydbdba = host=/tmp user=postgres&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;물론 윗 설정이 localhost의 unix domain socket 접속에 대한 인증이 trust 이기 때문에, pgbouncer를 통한 슈퍼유저 접속은 보안상 많이 위험합니다. 이 부분은 hba 설정으로 어느정도는 보완할 수는 있지만 아에 접속자체를 막는것이 더 안전할 것입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;div&gt;$ psql -h 127.0.0.1 -U pgbouncer pgbouncer&lt;br&gt;pgbouncer 사용자의 암호:&lt;br&gt;psql(16beta2, 1.20.0/bouncer 서버)&lt;br&gt;경고: psql 메이저 버전 16, 서버 메이저 버전 1.20.&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 일부 psql 기능이 작동하지 않을 수도 있습니다.&lt;br&gt;도움말을 보려면 &quot;help&quot;를 입력하십시오.&lt;br&gt;&lt;br&gt;pgbouncer=# show help;&lt;br&gt;NOTICE:&amp;nbsp; Console usage&lt;br&gt;상세정보:&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SHOW HELP|CONFIG|DATABASES|POOLS|CLIENTS|SERVERS|USERS|VERSION&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SHOW PEERS|PEER_POOLS&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SHOW FDS|SOCKETS|ACTIVE_SOCKETS|LISTS|MEM|STATE&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SHOW DNS_HOSTS|DNS_ZONES&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SHOW STATS|STATS_TOTALS|STATS_AVERAGES|TOTALS&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET key = arg&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; RELOAD&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; PAUSE [&lt;db&gt;]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; RESUME [&lt;db&gt;]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; DISABLE &lt;db&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ENABLE &lt;db&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; RECONNECT [&lt;db&gt;]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; KILL &lt;db&gt;&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SUSPEND&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SHUTDOWN&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WAIT_CLOSE [&lt;db&gt;]&lt;br&gt;SHOW&lt;br&gt;&lt;/db&gt;&lt;/db&gt;&lt;/db&gt;&lt;/db&gt;&lt;/db&gt;&lt;/db&gt;&lt;/db&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;pgbouncer 데이터베이스에 접속하고, 각종 pgbouncer 관련 SQL 작업을 할 수 있습니다. 시작은 show help;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;주로 쓰는 SQL 구문은 첫줄에 있는&lt;br&gt;&lt;/div&gt;&lt;div&gt;SHOW HELP|CONFIG|DATABASES|POOLS|CLIENTS|SERVERS|USERS|VERSION&lt;/div&gt;&lt;div&gt;명령과,&lt;/div&gt;&lt;div&gt;SHOW STATS|STATS_TOTALS|STATS_AVERAGES|TOTALS&lt;/div&gt;&lt;div&gt;정도입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;각 명령의 자세한 설명은 pgbouncer 홈페이지를 참조하세요.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;이 글에서 못다한 이야기&lt;/h2&gt;&lt;div&gt;도입을 고민하고 있다면, pgbouncer의 성능보다 안정성을 먼저 고민해야할 것 같습니다. 왜냐하면, 데이터베이스 관리자로 데이터베이스 인스턴스 보기도 바쁜데, pgbouncer 때문에 끙끙거리는 일이 최대한 없어야 하니까요.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;안정성 테스트는 &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;아주 잦은 클라이언트와 pgbouncer 간 연결에서 잘 버티는지,&lt;/li&gt;&lt;li&gt;아주 오랫 동안 클라이언트가 연결 되어 있음에도 db 인스턴스의 세션 관리를 잘 하는지&lt;/li&gt;&lt;li&gt;전체적으로 오랫동안 운영되어도 메모리 누수가 없는지&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;이정도 일 것 같네요. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;pgbouncer를 포기해야하는 상황에서 클라이언트가 직접 DB로 접속하는 원래 상태로 돌리는 작업을 어떻게 깔끔하게 할지에 대한 계획도 잘 마련해 두어야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;pgbouncer 소프트웨어의 패치 작업에 대한 계획도 필요합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;데이터베이스 관리자가 모니터링 도구를 통해서 데이터베이스 인스턴스를 모니터링 하듯, pgbouncer 모니터링 도구도 마련해야할 것 같습니다.&lt;/div&gt;&lt;div&gt;pgbouncer용 grafana 대시보드가 있긴하던데, 안 써봐서 모르겠습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL 데이터베이스 접속 관련 문제로 고민 하고 분이 pgbouncer 도입을 고려할 때 이 글이 조금이나마 도움이 되었으면 하는 마음으로 이 글을 마칩니다.&lt;br&gt;&lt;/div&gt;</description>
            <pubDate>Fri, 04 Aug 2023 16:29:05 +0900</pubDate>
            <guid>http://postgresql.kr/blog/pgbouncer.html</guid>
        </item>
        <item>
            <title>테이블 크기 알기</title>
            <link>http://postgresql.kr/blog/table_size_for_postgres.html</link>
            <description>&lt;h1&gt;테이블 크기 알기&lt;/h1&gt;&lt;div&gt;PostgreSQL은 각 객체가 OS 저장 공간을 얼마나 사용하는지 확인할 때는 함수를 사용해야한다. 데이터베이스 정보를 보여주는 테이블이나, 뷰를 조회해서 그 값을 찾는게 아니라, pg_*_size() 함수를 사용해서 각 객체들의 크기를 확인한다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 함수가 호출 될 때, 이 함수는 내부적으로 그 객체에 해당하는 OS 파일시스템의 파일 크기를 구한다. - 데이터베이스나 테이블스페이스라면, 해당 디렉터리 안에 있는 모든 파일의 크기 총 합을 알려준다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기서 기억해야 할 것은 그 함수의 호출 자체가 해당 객체를 access share 모드로 잠근다는 것이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, 이 모드 잠금을 할 수 없는 상황 - 먼저 다른 쪽에서 access exclusive 모드로 잠군 상황 - 이라면 기다리게 된다(!).&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;크기 조회 함수들&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;각 객체의 크기를 조회하는 함수는&lt;/div&gt;&lt;div&gt;https://postgresql.kr/docs/current/functions-admin.html#FUNCTIONS-ADMIN-DBOBJECT&lt;/div&gt;&lt;div&gt;문서에서 소개하고 있다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글에서 이것을 다시 정리하면,&lt;/div&gt;&lt;div&gt;(여기서 소개하고 있는 쿼리는 언제든지 바로 그 결과를 확인할 수 있다고 믿으면 안된다. 앞에서 이야기했듯이 이 함수가 호출되는 대상 객체가 잠겨 있으면 쿼리 자체가 대기 상태가 된다.) &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;select datname, pg_database_size(oid) from pg_database&lt;/li&gt;&lt;ul&gt;&lt;li&gt;해당 데이터베이스 디렉터리($PGDATA/base/숫자) 이하 있는 모든 파일 크기의 총합&lt;/li&gt;&lt;li&gt;즉, 그 디렉터리를 대상으로 일어나는 중간 과정의 모든 임시 파일들도 그 총합에 포함된다 (concurrently 옵션을 사용해서 만들고 있는 임시 인덱스들의 크기도 포함된다)&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;select spcname, pg_tablespace_size(oid) from pg_tablespace&lt;/li&gt;&lt;ul&gt;&lt;li&gt;설명 생략&lt;br&gt;&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;select oid::regclass, relkind, pg_indexes_size(oid) from pg_class where relkind in (&apos;r&apos;, &apos;m&apos;, &apos;t&apos;)&lt;br&gt;&lt;/li&gt;&lt;ul&gt;&lt;li&gt;이 인덱스 크기를 구하는 함수의 인자는 인덱스가 아니라, 그 인덱스를 사용하는 객체다.&lt;/li&gt;&lt;li&gt;그 객체에 딸린 모든 인덱스의 총합이다.&lt;/li&gt;&lt;li&gt;각 개별 인덱스의 크기를 살펴보려면, 아래 pg_relation_size() 함수를 이용한다.&lt;br&gt;&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;select oid::regclass, relkind, pg_relation_size(oid) from pg_class where relkind not in (&apos;S&apos;, &apos;v&apos;, &apos;c&apos;, &apos;f&apos;,&apos;p&apos;,&apos;I&apos;)&lt;/li&gt;&lt;ul&gt;&lt;li&gt;&amp;nbsp;여기서 재미난 것이 숨어 있는데, 이 함수는 입력 인자로 하나만 쓰면 그 객체 그 자체의 크기를 반환하고, 두번째 입력인자로 &apos;main&apos;, &apos;fsm&apos;, &apos;vm&apos;, &apos;init&apos; 중 하나를 지정하면 그에 맞는 크기를 반환한다. &lt;br&gt;&lt;/li&gt;&lt;ul&gt;&lt;li&gt;main: 그 객체 자체(토스트 테이블 제외)&lt;br&gt;&lt;/li&gt;&lt;li&gt;fsm: 테이블, 토스트, 구체화된 뷰인 경우 그 객체의 빈터 지도 파일 크기&lt;/li&gt;&lt;li&gt;vm: 테이블, 토스트, 구체화된 뷰인 경우 그 객체의 실자료 지도 파일 크기&lt;/li&gt;&lt;li&gt;init: unlogged 옵션을 사용하는 테이블의 init 파일 크기 (https://postgresql.kr/docs/current/storage-init.html 참조)&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;이렇기 때문에, 이 pg_relation_size() 함수는 정확한 이해 없이 사용하게 되면 의도하지 않게 부정확한 크기를 구하게 된다.&lt;/li&gt;&lt;li&gt;테이블만을 고려대상으로 한다면, 아래에서 소개하는 pg_table_size(), pg_total_relation_size() 함수를 사용하는 것이 더 간편하다.&lt;br&gt;&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;select oid::regclass, relkind, pg_table_size(oid) from pg_class where relkind in (&apos;r&apos;, &apos;m&apos;)&lt;/li&gt;&lt;ul&gt;&lt;li&gt;인덱스를 제외한 테이블에 관계된 모든 파일의 총합(main + fsm + vm + init + toast의 그것들도 함께)&lt;/li&gt;&lt;li&gt;즉, 저수준으로 pg_table_size 함수를 구현한다면, pg_relation_size 함수가 여덟번 호출한 결과의 총합이다.&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;select oid::regclass, relkind, pg_total_relation_size(oid) from pg_class where relkind in (&apos;r&apos;, &apos;m&apos;)&lt;/li&gt;&lt;ul&gt;&lt;li&gt;대망의 데이터베이스 관리자가 관심 있는 용량 관리용으로 제일 많이 사용하는 함수다 - 이 테이블을 지우면 얼마의 공간을 확보할 수 있어요? 질문의 답을 찾는 함수다.&lt;/li&gt;&lt;li&gt;pg_table_size() + pg_indexes_size() 값이다.&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;div&gt;아쉽게도 해당 스키마 소속 모든 객체의 총합을 구하는 함수는 아직 없다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;지금까지 설명을 기반으로 잘 만들어서 써봅시다. 숙제&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;</description>
            <pubDate>Thu, 12 Jan 2023 15:59:30 +0900</pubDate>
            <guid>http://postgresql.kr/blog/table_size_for_postgres.html</guid>
        </item>
        <item>
            <title>9.x 파티션 테이블을 10.x. 이상 버전으로 업그레이드 하기</title>
            <link>http://postgresql.kr/blog/upgrade_partition_tables_to_ver10.html</link>
            <description>&lt;h1&gt;파티션 테이블 업그레이드 하기&lt;/h1&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;들어가며&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;9.x 대 버전까지는&amp;nbsp; 다른 관계형 데이터베이스와 달리 파티션 테이블에 대한 명시적인 설명과 방법이 없었습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;왜냐하면 PostgreSQL은 초창기부터 테이블 상속이라는 개념이 있어, 파티션 테이블은 이 상속 기법으로 처리할 수 있었으니까요.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;create table 엄마 ();&lt;/div&gt;&lt;div&gt;create table 아빠 ();&lt;/div&gt;&lt;div&gt;create table 첫째 () inherits (엄마,&amp;nbsp; 아빠);&lt;/div&gt;&lt;div&gt;&lt;div&gt;create table 둘째 () inherits (엄마,&amp;nbsp; 아빠);&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이런 아주 가족적인 테이블 구성을 할 수 있습니다.&lt;/div&gt;&lt;div&gt;(윗 네줄을 복사해서 psql 안에서 실행하면 정상적으로 sql 구문이 실행됩니다. - 심심하면 한 번 해보세요 . 다른 관계형 데이터베이스만 알고 있는 사람들이 본다면, 아이들 교육에 도움이 되겠네, 라고 말할 기능이죠.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;이 방식으로 파티션 테이블 기능을 구현했습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;하지만, 이 기법이 주류가 아닐뿐더러 편하지도 않고, 성능 문제도 있고, 그밖에 다른 문제들도 있고 해서 결국 10버전부터 주류 관계형 데이터베이스에서 사용하는 파티션 테이블이라는 아주 구체적인 개념을 도입했고, 구현했습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(테이블을 탁자라는 우리말로 옮기지 못했고, 파티션 테이블도 결국 나눠진 탁자로 옮기지 못해 그냥 파티션 테이블이라고 합니다.&lt;/div&gt;&lt;div&gt;하지만, 영어에서는 partitioned table : 상위 테이블, partition tables : 하위 테이블로 명확하게 구분해서 사용합니다. 영문 사용 설명서를 읽을 때 참고하세요. - 뱀발)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;세월은 흘러 이제 9.x 버전은 더 이상 엔진 코드 변경을 안하겠다고 개발 그룹에서 발표하고, 그저 이 데이터베이스를 쓰는 사람 처지에서는 이런 저런 이유로 10 버전 이상으로 데이터베이스 업그레이드를 생각하는데, 이 업그레이드 작업에서 이 파티션 테이블 업그레이드가 아주 껄꺼러운 문제가 되어 버립니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;왜냐하면 PostgreSQL 메이져버전 업그레이드는 pg_upgrade라는 명령을 이용해서 하는데, 이 명령의 내부작업에서 이 문제(하위 버전의 inherits로 만든 테이블을 새로운 버전에서 제공하는 파티션 테이블로 쉽게 바꾸는 것)를 깔끔하게 처리하지 못하기 때문입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL 데이터베이스 관리자 용어로 이야기하면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;pg_upgrade .....&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 명령이 잘 작동하는 것처럼 보이는데,&amp;nbsp;&lt;/div&gt;&lt;div&gt;단 기존 데이터베이스 안에 테이블 상속 기법을 이용한 파티션 테이블이 있다면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;이것을 알아서 잘 10 이상에서 제공하는 새로운 파티션 테이블로 바꾸지 않습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(10버전에서는 이 새로운 파티션 기법을&amp;nbsp;declarative table partitioning 이라고 합니다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글은 이 파티션 테이블 업그레이드에 대한 이야기입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;10버전에서 달라지는 파티션 테이블 특성&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;상위 테이블은 반드시 하나의 테이블이여야합니다.&lt;/li&gt;&lt;li&gt;하위 테이블의 구조는 상위 테이블과 똑 같아야합니다. (상속에서는 달라도 되었죠)&lt;br&gt;&lt;/li&gt;&lt;li&gt;다른 관계형 데이터베이스와 달리 상위 테이블과 하위 테이블을 각각의 DDL 구문으로 만들어야합니다.&lt;/li&gt;&lt;li&gt;기본키가 있는 경우 그 기본키가 참조하는 칼럼 가운데 하나는 반드시 파티션 키여야합니다.&lt;/li&gt;&lt;li&gt;상속 기반으로 작성되었던 파티션 테이블 흉내냈던 기능들은 모두 내장 기능으로 바뀌었기 때문에 변환을 한다면, 후속 작업이 필요합니다.&lt;/li&gt;&lt;ul&gt;&lt;li&gt;상위 테이블에 지정했던 자료 처리용 트리거는 사용하지 않습니다. 이제 자동으로 됨&lt;/li&gt;&lt;li&gt;하위 테이블에 지정했던 check 제약 조건은 파티션 범위나 항목으로 바뀌기 때문에 이것도 사용하지 않습니다. &lt;br&gt;&lt;/li&gt;&lt;li&gt;하위 테이블 추가, 삭제 구문이 바뀝니다. (inherit -&amp;gt; attach, detach)&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h3&gt;상속 기능을 이용하는 상위 테이블과 하위 테이블을 파티션 테이블로 바꾸기&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;여기서 다루는 예제 테이블은 지난 &lt;a href=&quot;https://postgresql.kr/blog/postgresql_partition_table.html&quot;&gt;&quot;9.6 이하 버전에서의 파티션 테이블 이야기&quot;&lt;/a&gt; 글에서 사용했던 테이블을 대상으로 합니다. &lt;br&gt;&lt;/div&gt;&lt;pre&gt;postgres=# \dt
              List of relations
 Schema |      Name       | Type  |  Owner
--------+-----------------+-------+----------
 public | wwwlog          | table | postgres
 public | wwwlog_20180111 | table | postgres
 public | wwwlog_20180112 | table | postgres
 public | wwwlog_20180113 | table | postgres
 public | wwwlog_20180114 | table | postgres
(5 rows)

postgres=# \d wwwlog
                                       Table &quot;public.wwwlog&quot;
 Column |            Type             | Collation | Nullable |               Default
--------+-----------------------------+-----------+----------+-------------------------------------
 seq    | integer                     |           | not null | nextval(&apos;wwwlog_seq_seq&apos;::regclass)
 ctime  | timestamp without time zone |           | not null | CURRENT_TIMESTAMP
 node   | bigint                      |           | not null |
 data   | jsonb                       |           | not null |
Indexes:
    &quot;wwwlog_pkey&quot; PRIMARY KEY, btree (seq)
    &quot;wwwlog_ctime_i&quot; brin (ctime)
    &quot;wwwlog_data_i&quot; gin (data)
Triggers:
    tr_wwwlog_insert BEFORE INSERT ON wwwlog FOR EACH ROW EXECUTE FUNCTION wwwlog_insert_dynamic()
Number of child tables: 4 (Use \d+ to list them.)

postgres=# \d wwwlog_20180111
                                  Table &quot;public.wwwlog_20180111&quot;
 Column |            Type             | Collation | Nullable |               Default
--------+-----------------------------+-----------+----------+-------------------------------------
 seq    | integer                     |           | not null | nextval(&apos;wwwlog_seq_seq&apos;::regclass)
 ctime  | timestamp without time zone |           | not null | CURRENT_TIMESTAMP
 node   | bigint                      |           | not null |
 data   | jsonb                       |           | not null |
Indexes:
    &quot;wwwlog_20180111_pkey&quot; PRIMARY KEY, btree (seq)
    &quot;wwwlog_20180111_ctime_idx&quot; brin (ctime)
    &quot;wwwlog_20180111_data_idx&quot; gin (data)
Check constraints:
    &quot;wwwlog_20180111_ctime_check&quot; CHECK (ctime &amp;gt;= &apos;2018-01-11 00:00:00&apos;::timestamp without time zone AND ctime &amp;lt; &apos;2018-01-12 00:00:00&apos;::timestamp without time zone)
Inherits: wwwlog
&lt;/pre&gt;&lt;div&gt;위와 같이 전형적인 상속 기능을 이용한 파티션 테이블 있을 때, 이것은 새 파티션 테이블로 바꾸는 작업을 합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;전체 과정은 다음과 같습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;필요하다면, 기존 테이블의 모든 기본키에 파티션 키를 추가해서 기본키를 바꾸고&lt;/li&gt;&lt;li&gt;새 파티션 상위 테이블을 만들고,&lt;/li&gt;&lt;li&gt;옛 파티션 하위 테이블을 떼어내어 새 파티션 테이블에 붙이고,&lt;/li&gt;&lt;li&gt;그 하위 테이블에 있는 check 제약 조건을 지웁니다.&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;1.&lt;br&gt;&lt;/div&gt;&lt;div&gt;운영 환경 중에 기본키를 바꾸는 전략은 일반적으로&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;새 유니크 인덱스를 concurrently 옵션으로 하나 만들고, (파티션 키를 포함하는 인덱스가 되겠죠)&lt;br&gt;&lt;/li&gt;&lt;li&gt;기존 기본키를 지우고,&lt;/li&gt;&lt;li&gt;새로 만든 유니크 인덱스를 기본키로 지정합니다. &lt;br&gt;&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;이론상으로 별로 어렵지 않은 작업 같아보이지만, PostgreSQL 유니크 인덱스는 null 자료에 대해서는 중복을 허용합니다. 하지만, 기본키로 사용한다면, 그 소속 칼럼 값은 null을 허용하지 않습니다. 이런 문제 때문에, 필요하다면, 이 null 문제부터 풀어야합니다. 아무 생각 없이 파티션 키로 사용하는 칼럼에 null 허용 속성이 부여되어있다면, 먼저 not null 속성으로 바꾸는 일부터 해야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;슬프게도, 이 작업은 alter 작업이고, 테이블의 그 칼럼 값을 전부 조회하는 일을 하게 됩니다. 자료가 많다면, 꽤 오랜 시간 테이블 전체가 잠기게 됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;윗 테이블 구조 상황이라면, 다음과 같은 방식으로 진행합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# create unique index concurrently if not exists wwwlog_new_pk on wwwlog (seq, ctime);
CREATE INDEX
postgres=# begin;
           alter table wwwlog drop constraint wwwlog_pkey;
           alter index wwwlog_new_pk rename to wwwlog_pkey;
           alter table wwwlog add primary key using index wwwlog_pkey;
           end;
COMMIT&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이 기본키 바꾸기 작업을 하위테이블도 모두 해야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;2.&lt;/div&gt;&lt;div&gt;10버전 이상용 새 파티션 테이블 만드는 작업은 비교적 단순합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# create table if not exists p_wwwlog (like wwwlog including all) partition by range (ctime);
CREATE TABLE
postgres=# \d p_wwwlog
                                  &quot;public.p_wwwlog&quot; 파티션 테이블
 필드명 |            종류             | Collation | NULL허용 |               초기값
--------+-----------------------------+-----------+----------+-------------------------------------
 seq    | integer                     |           | not null | nextval(&apos;wwwlog_seq_seq&apos;::regclass)
 ctime  | timestamp without time zone |           | not null | CURRENT_TIMESTAMP
 node   | bigint                      |           | not null |
 data   | jsonb                       |           | not null |
파티션 키: RANGE (ctime)
인덱스들:
    &quot;p_wwwlog_pkey&quot; PRIMARY KEY, btree (seq, ctime)
    &quot;p_wwwlog_ctime_idx&quot; brin (ctime)
    &quot;p_wwwlog_data_idx&quot; gin (data)
파티션 테이블 수: 0&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;including all 옵션을 사용해서 만들었습니다. 편하죠.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;3.&lt;/div&gt;&lt;div&gt;옛날 파티션 테이블에서 하위 테이블을 떼어내어, 새 파티션 테이블에 끼워넣는 작업은 하나의 트랜잭션으로 처리하는 것이 바람직합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;또한 운영 환경 상태라면, 응용 프로그램이 사용하는 쿼리들이 어떤 결과를 보일 것인지도 예측하고 작업을 진행해야겠죠. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# begin;
     alter table wwwlog_20180111 no inherit wwwlog;
     alter table p_wwwlog attach partition wwwlog_20180111 for values from (&apos;2018-01-11 00:00:00&apos;) to (&apos;2018-01-12 00:00:00&apos;);
     end;
COMMIT&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이 작업도 모든 하위 테이블을 대상으로 진행합니다. 이때 attach partition 옵션 뒤에 사용하는 파티션이 이름이 바로 떼어낸 하위 테이블 이름입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;파티션 attach 작업 때 기억해야하는 것은 그 테이블에 있는 자료가 for values 로 지정한 허용 범위안의 자료인지 테이블 전체 자료를 검사한다는 것입니다. 단 예외가 있는데, 그 끼워넣을 하위 테이블에 check 제약조건이 for values 조건과 같다면, 이 자료 전체 검사를 생략합니다. 그래서, 끼워넣는 작업 전에 하위 테이블에 있는 check 제약조건을 먼저 지우면 작업 시간이 길어지게됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;4.&lt;/div&gt;&lt;div&gt;체크 제약조건 없애는 것은 늘 하던 대로&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# alter table wwwlog_20180111 drop constraint wwwlog_20180111_ctime_check;
ALTER TABLE&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이 작업도 당연히 모든 하위테이블을 대상으로 해야겠죠. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;h3&gt;운영 환경 데이터베이스 업그레이드 때 한 번 더 생각해야 하는 것들&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;먼저 고려해야할 것은 &lt;br&gt;&lt;/div&gt;&lt;ul&gt;&lt;li&gt;바뀐 파티션 테이블 특성을 잘 파악에서 기존 테이블의 형상
 변경이 필요한지를 파악해야합니다. - 대표적으로 상위 테이블의 기본키 정의에 반드시 파티션 키가 포함되어야 하는지 
살펴보는거겠죠. 만들어야 한다면, 먼저 기본키 변경 작업부터 해야합니다.&amp;nbsp; 이 작업을 운영 환경에서 한다는 것은 꽤 큰 부담을 
안고 진행하는 일입니다. 특히나 자료가 많은 경우라면 더더욱. 작업 영향도를 잘 파악해야합니다.&lt;br&gt;&lt;/li&gt;&lt;li&gt;alter 
table ... no inherits 구문을 사용해서 상속 관계를 끊는 작업, 새 상위 파티션 테이블에 떼어낸 하위 테이블을 
alter table ... attach 구문으로 포함 시키는 작업, 그 하위 테이블에 있는 check 제약 조건을 없애는 
작업까지 모두 하나의 트랜잭션으로 처리하는 것이 안전하고, 이 작업들이 DDL 작업으로 테이블 잠금이 일어난다는 것입니다. 즉, 
작업 영향도를 반드시 파악해야합니다. &lt;/li&gt;&lt;li&gt;운영 환경에서는 이 글 예제처럼 파티션 테이블이 몇 개로 구성되는 경우는 거의 없습니다. 적어도 월단위 1년치니까, 한 파티션 테이블의 하위 테이블이 적어도 12개 이상은 됩니다. 그런데, 이 여러개 하위 테이블의 반복 작업이 꽤 성가신 작업입니다. 즉, 반복 작업을 위한 SQL 구문 자동 생성을 하든, SQL 구문을 자동으로 생성해서 실행까지 자동화 하든 어떤 방법으로든 어느 정도의 자동화가 필요합니다. 완전 자동화 방법은 그 작업 과정 중에 발생하는 테이블 잠김 상황 때 대처가 쉽지 않기 때문에 여기서는 SQL 구문 자동 생성 하고, 그것을 관리자가 차례로 실행하는 방식을 소개합니다. &lt;br&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;1. 상속 기반 상위 테이블과 하위 테이블 찾기&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# select inhrelid::regclass, inhparent::regclass 
from pg_inherits where inhparent::regclass = &apos;wwwlog&apos;::regclass;
    inhrelid     | inhparent
-----------------+-----------
 wwwlog_20180111 | wwwlog
 wwwlog_20180112 | wwwlog
 wwwlog_20180113 | wwwlog
 wwwlog_20180114 | wwwlog
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;2. 해당 테이블의 기본키 찾기&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# select conname,pg_get_constraintdef(oid) 
from pg_constraint where conrelid::regclass = &apos;wwwlog&apos;::regclass and
 contype = &apos;p&apos;;
   conname   |   pg_get_constraintdef
-------------+--------------------------
 wwwlog_pkey | PRIMARY KEY (seq, ctime)&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;3. 상속된 하위 테이블의 check 제약조건 찾기&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# select conrelid::regclass, conname,pg_get_constraintdef(oid) 
from pg_constraint where conrelid 
  in (select inhrelid from pg_inherits 
      where inhparent::regclass = &apos;wwwlog&apos;::regclass)
and contype = &apos;c&apos; ;
    conrelid     |           conname           |                                                           pg_get_constr
aintdef
-----------------+-----------------------------+------------------------------------------------------------------------
------------------------------------------------------------------
 wwwlog_20180112 | wwwlog_20180112_ctime_check | CHECK (((ctime &amp;gt;= &apos;2018-01-12 00:00:00&apos;::timestamp without time zone) A
ND (ctime &amp;lt; &apos;2018-01-13 00:00:00&apos;::timestamp without time zone)))
 wwwlog_20180113 | wwwlog_20180113_ctime_check | CHECK (((ctime &amp;gt;= &apos;2018-01-13 00:00:00&apos;::timestamp without time zone) A
ND (ctime &amp;lt; &apos;2018-01-14 00:00:00&apos;::timestamp without time zone)))
 wwwlog_20180114 | wwwlog_20180114_ctime_check | CHECK (((ctime &amp;gt;= &apos;2018-01-14 00:00:00&apos;::timestamp without time zone) A
ND (ctime &amp;lt; &apos;2018-01-15 00:00:00&apos;::timestamp without time zone)))&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;4. 파티션 테이블 관련 쿼리&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# select parentrelid,relid,pg_get_partition_constraintdef(relid) 
from (
  select (pg_partition_tree(oid)).* 
  from pg_class where relkind = &apos;p&apos;
) as t;
 parentrelid |      relid      |                                                              pg_get_partition_constrain
tdef
-------------+-----------------+----------------------------------------------------------------------------------------
------------------------------------------------------------------
             | p_wwwlog        |
 p_wwwlog    | wwwlog_20180111 | ((ctime IS NOT NULL) AND (ctime &amp;gt;= &apos;2018-01-11 00:00:00&apos;::timestamp without time zone)
AND (ctime &amp;lt; &apos;2018-01-12 00:00:00&apos;::timestamp without time zone))&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;아쉽게도 check 제약조건 내용이나, 파티션 for values 값 정의 부분이 그냥 문자열로 뽑을 수 밖에 없습니다. 그래서, 필요한 부분만 딱 뽑아서 SQL을 쉽게 만들 수가 없습니다. 물론 이런 문제는 regexp_replace() 함수로 충분히 정규식을 이용한 문자열 조작으로 풀 수 있기는 합니다. 이 부분은 그때 그때 상황에 맞게 만들어야할 것 같습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;마지막으로 고려해야할 것은 운영 환경에서 이 형상 변경 작업으로 발생하는 서비스 중지 시간 최소화 방안입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;지난 &lt;a href=&quot;https://postgresql.kr/blog/online_partitioning_for_pgv11.html&quot;&gt;일반 테이블을 파티션 테이블로 만들기&lt;/a&gt; 문서에서도 언급했듯이 &lt;br&gt;&lt;/div&gt;&lt;div&gt;이런 형상 변경 작업을 사용자들이 눈치 채지 못하게 아주 유연하게 바꾸는 것은 쉽지 않은 작업입니다. 이 부분은 결국 관리자와 응용 프로그램 개발자 사이 긴밀한 도움으로 최적 방안을 그 환경에 맞게 만드는 것 뿐입니다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;나가며&lt;/h3&gt;&lt;div&gt;다 쓰고 나니 그다지 중요하지도 어렵지도 않는 간단한 작업을 뭘 이리 주절주절 썼나싶은데, 그냥 단순하게 &apos;나는 상속 기반 파티션 테이블을 이런 쿼리로 바꿨다&apos; 이렇게 쿼리만 달랑 기록해 두면, 분명 운영환경에서 고생할 것이 예상이 되어 노파심에서 주절주절했습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;아무튼 테이블 형상 변경 작업은 생각보다 예민한 작업입니다. 잘 풀어가시길 빕니다!&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;</description>
            <pubDate>Thu, 16 Jun 2022 14:32:31 +0900</pubDate>
            <guid>http://postgresql.kr/blog/upgrade_partition_tables_to_ver10.html</guid>
        </item>
        <item>
            <title>hunspell과 postgresql text search</title>
            <link>http://postgresql.kr/blog/hunspell_postgresql.html</link>
            <description>&lt;h1&gt;hunspell과 한글 text search&lt;/h1&gt;&lt;h2&gt;포스트그레스큐엘 텍스트 찾기 소개&lt;/h2&gt;&lt;div&gt;이 글은&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;a href=&quot;https://postgresql.kr/docs/13/textsearch.html&quot;&gt;https://postgresql.kr/docs/current/textsearch.html&lt;/a&gt;&lt;/div&gt;&lt;div&gt;문서의 소개글이며,&amp;nbsp;&lt;/div&gt;&lt;div&gt;이것을 기반으로 한글 환경에서는 과연 이 기능을 어떻게 쓸 수 있는가를 검토한 글이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;바쁜 사람들을 위한 결론, 현재로서는&amp;nbsp;&lt;br&gt;2014년(헉 10년이 되어가는군!)에 작업한 &lt;a href=&quot;https://postgresql.kr/blog/korean_full_textsearch.html&quot;&gt;textsearch_ko 확장모듈&lt;/a&gt;이 그나마 최적의 대안이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글은 저 글의 확장판으로 확장 모듈을 만들고, 설정하고, 이런 낯선 작업들 없이, 기본적으로 지원하는 hunspell 형태소 분석을 이용하는 방법과 그 한계를 소개한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;텍스트 검색 구성 &amp;amp; 사전 &amp;amp; 구문분석기 &amp;amp; 템플릿&lt;/h3&gt;&lt;div&gt;PostgreSQL에서는 텍스트 검색을 위해 네개의 객체를 제공한다.&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;CREATE TEXT SEARCH CONFIGURATION: 최종 검색 환경 구성&lt;/li&gt;&lt;li&gt;CREATE TEXT SEARCH DICTIONARY: 사전&lt;/li&gt;&lt;li&gt;CREATE TEXT SEARCH PARSER: 구문분석기&lt;/li&gt;&lt;li&gt;CREATE TEXT SEARCH TEMPLATE: 사전 템플릿&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;관계를 설명하면,&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;사전 템플릿을 만들고, 또 그것을 이용해 사전을 정의를 하고, 구문분석기를 정의하고, 최종적으로 사용할 구문분석기와 그 구문분석으로 나뉘어진 토큰들의 의미소를 뽑기 위해 사용할 사전을 연결하는 하나의 검색 환경을 만든다.&lt;/div&gt;&lt;div&gt;이 검색 환경 이름은 to_tsvector() - 문자열을 형태소 분석을 끝내고 의미소들만 모은 백터 자료로 바꾸는 함수와, to_tsquery() - 문자열을 형태소 분석과 기호를 통해 검색 조건을 만드는 함수에서 사용되며, 이들 함수에서 이 검색 환경 이름을 빼면,&amp;nbsp;default_text_search_config DB 환경 설정 매개 변수 값을 사용한다. (초기값은 simple이다)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;예제로 살펴 보는 검색 환경 구성&lt;/h3&gt;&lt;div&gt;PostgreSQL 기본 배포판에는 각 언어별 기본 검색 환경이 미리 구성되어있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그 중 하나를 예로 앞에서 설명한 것이 어떻게 구현되었는지 확인해 본다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;무조건 띄워쓰기 단위로 구분 하는 simple 환경 구성 보다는 조금 복잡한 Snowball 프로젝트에서 사용한&amp;nbsp; 그 알고리즘을 기반으로 몇 언어들이 미리 설정 되어있다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;한 예로 한국어와 비슷한 터키어가 있어 살펴본다.&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# \dF
                        텍스트 검색 구성 목록
   스키마   |      이름       |                 설명                  
------------+-----------------+---------------------------------------
 pg_catalog | arabic          | configuration for arabic language
 pg_catalog | armenian        | configuration for armenian language
 pg_catalog | basque          | configuration for basque language
... (중간 생략) ...
 pg_catalog | turkish         | configuration for turkish language

postgres=# \dF+ turkish
텍스트 검색 구성 &quot;pg_catalog.turkish&quot;
파서: &quot;pg_catalog.default&quot;
      토큰       |     사전     
-----------------+--------------
 asciihword      | turkish_stem
 asciiword       | turkish_stem
 email           | simple
 file            | simple
 float           | simple
 host            | simple
 hword           | turkish_stem
 hword_asciipart | turkish_stem
 hword_numpart   | simple
 hword_part      | turkish_stem
 int             | simple
 numhword        | simple
 numword         | simple
 sfloat          | simple
 uint            | simple
 url             | simple
 url_path        | simple
 version         | simple
 word            | turkish_stem
-- default 구문분석기를 이용해 쪼개진 각 토큰은 그 유형에 따라
-- 해당하는 사전을 이용해 기본형, 또는 어근 추출을 한다. 
-- 윗 예제라면, 아스키 문자들로 구성된 단어는 turkish_stem 사전을 이용해서 
-- 어근 추출을 한다.
postgres=# \dFd+ turkish_stem
                                                         텍스트 검색 사전 목록
   스키마   |     이름     |       템플릿        |                 초기화 옵션                 |                 설명                  
------------+--------------+---------------------+---------------------------------------------+---------------------------------------
 pg_catalog | turkish_stem | pg_catalog.snowball | language = &apos;turkish&apos;, stopwords = &apos;turkish&apos; | snowball stemmer for turkish language
(1개 행)

postgres=# \dFt+ snowball
                           텍스트 검색 템플릿 목록
   스키마   |   이름   |     초기화     |      Lexize      |       설명       
------------+----------+----------------+------------------+------------------
 pg_catalog | snowball | dsnowball_init | dsnowball_lexize | snowball stemmer
(1개 행)
-- 사전과 그 사전을 구성하는 템플릿을 살펴봤다.
-- 다음은 ts_debug() 함수를 이용해서 turkish 검색 환경 구성이
-- 어떻게 내부적으로 작동했는지 살펴본 것이다.
postgres=# select * from  ts_debug(&apos;turkish&apos;, &apos;&lt;span style=&quot;font-family: arial&quot;&gt;Sana bir masal okuyacağım&lt;/span&gt;.&apos;);
   alias   |    description    |   token    |  dictionaries  |  dictionary  |  lexemes   
-----------+-------------------+------------+----------------+--------------+------------
 asciiword | Word, all ASCII   | Sana       | {turkish_stem} | turkish_stem | {sa}
 blank     | Space symbols     |            | {}             |              | 
 asciiword | Word, all ASCII   | bir        | {turkish_stem} | turkish_stem | {bir}
 blank     | Space symbols     |            | {}             |              | 
 asciiword | Word, all ASCII   | masal      | {turkish_stem} | turkish_stem | {masal}
 blank     | Space symbols     |            | {}             |              | 
 word      | Word, all letters | &lt;span style=&quot;font-family: arial&quot;&gt;okuyacağım&lt;/span&gt; | {turkish_stem} | turkish_stem | {okuyacak}
 blank     | Space symbols     | .          | {}             |              | 
(8개 행)
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;lexemes 칼럼에 최종 의미소 단위의 자료가 추출되는 것이다. 이것을 to_tsvector()를 이용하면, lexemes 칼럼값과 그 위치 값을 쌍으로 하는 벡터자료로 만들 수 있고, 이것을 저장하고, 인덱스를 만들어 검색 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# select to_tsvector(&apos;turkish&apos;, &apos;&lt;span style=&quot;font-family: arial&quot;&gt;Sana bir masal okuyacağım.&lt;/span&gt;&apos;) @@ to_tsquery(&apos;turkish&apos;, &apos;masallar&apos;);
 ?column? 
----------
 t
(1개 행)

postgres=# select to_tsvector(&apos;turkish&apos;, &apos;&lt;span style=&quot;font-family: arial&quot;&gt;Sana bir masal okuyacağım.&lt;/span&gt;&apos;) @@ to_tsquery(&apos;turkish&apos;, &apos;okudum&apos;);
 ?column? 
----------
 f
(1개 행)

postgres=# select to_tsvector(&apos;turkish&apos;, &apos;okudum&apos;) ;
 to_tsvector 
-------------
 &apos;okudu&apos;:1
(1개 행)

postgres=# select to_tsvector(&apos;turkish&apos;, &apos;okumak&apos;) ;
 to_tsvector 
-------------
 &apos;okumak&apos;:1
(1개 행)
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이렇게 그럭저럭 의도한 찾기는 가능하다. 그럭저럭이라고 한 이유는 &apos;읽겠다&apos;(&lt;span style=&quot;font-family: arial;&quot;&gt;okuyacağım)&lt;/span&gt;와 &apos;읽다&apos;(okumak)와 &apos;읽었다&apos;(okudum)을 모두 다른 단어로 처리했지만, &apos;책&apos;(masal)과 &apos;책들&apos;(masallar)는 같은 단어로 처리했다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;찾아보니, snowball 프로젝트의 내부 알고리즘이 따로 사전을 두지 않고, 각 언어별 어미 변화에 대한 규칙들만 지정해서 그냥 그 어미들을 버리는 방식을 택했기 때문이었다.&amp;nbsp;&lt;br&gt;(언어학적인 문장으로 표현하면, snowball 프로젝트는 굴절어 형식의 언어에서는 어느 정도는 유용할지 모르나, 한국어나 터키어(위에서 본것 처럼) 교착어 형식의 언어에서는 그다지 유용하지 않다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;그래서, hunspell로 넘어갔다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;hunspell 과 텍스트 검색&lt;/h2&gt;&lt;div&gt;hunspell 프로젝트는 그 출발점이 검색을 위한 것이라기 보다는 맞춤범 검사 기능에 초점이 맞춰진 프로젝트다.&amp;nbsp; 이 프로젝트 또한 굴절어를 쓰는 동네에서 개발되었기에 한국어 환경에서 쓰기 위해서는 엄청난 삽질이 필요하다.&amp;nbsp;&lt;br&gt;그 삽질을 이미 누군가가 했다. (그 엄청난 노고에 고마운 마음을 전한다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;한국어 hunspell 사용하기&lt;/h3&gt;&lt;div&gt;한국어 hunspell은 &lt;a href=&quot;https://spellcheck-ko.github.io/&quot;&gt;한국어 맞춤법 검사&lt;/a&gt;&amp;nbsp;홈페이지에서 프로젝트로 운영중이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL에서는 그저 hunspell 사전과 변화규칙 파일을 이용하면 된다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;최신 파일은&amp;nbsp;https://github.com/spellcheck-ko/hunspell-dict-ko/releases&lt;/div&gt;&lt;div&gt;페이지에서 받을 수 있으며,&amp;nbsp; 그냥 최종 파일 (ko-aff-dic-xxx.zip) 만 받아서 바로 사용하면 된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;wget&amp;nbsp;https://github.com/spellcheck-ko/hunspell-dict-ko/releases/download/0.7.92/ko-aff-dic-0.7.92.zip&lt;/li&gt;&lt;li&gt;unzip ko-aff-dic-0.7.92.zip&lt;/li&gt;&lt;li&gt;cd&amp;nbsp;ko-aff-dic-0.7.92&lt;/li&gt;&lt;li&gt;cp ko.dic {PostgreSQL엔진설치디렉터리}/share/tsearch_data/hunspell_korean.dict&lt;/li&gt;&lt;li&gt;cp ko.aff {PostgreSQL엔진설치디렉터리}/share/tsearch_data/hunspell_korean.affix&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;파일 다운로드, 복사 작업은 끝났다. 주의할 것은 파일 확장자를 위와 같이 바꿔야한다.&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음 &lt;a href=&quot;https://postgresql.kr/docs/current/textsearch-dictionaries.html#TEXTSEARCH-ISPELL-DICTIONARY&quot;&gt;공식 설명서&lt;/a&gt;에 나오는 대로 사전을 만든다.&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# CREATE TEXT SEARCH DICTIONARY korean_hunspell (
    TEMPLATE = ispell,
    DictFile = hunspell_korean,
    AffFile = hunspell_korean);
CREATE TEXT SEARCH DICTIONARY
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이때 DictFile과, AffFile에 지정한 이름이 바로 앞에서 복사한 dict, affix 확자자로 지정한 파일 이름 앞부분이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음 구문분석기는 기본 분석기(default)를 쓰고, 검색 환경 구성도 &lt;a href=&quot;https://postgresql.kr/docs/current/textsearch-configuration.html&quot;&gt;공식 설명서&lt;/a&gt;를 참고해서 만든다.&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# create text search configuration korean_hunspell (copy=english);
CREATE TEXT SEARCH CONFIGURATION
postgres=# alter text search configuration korean_hunspell alter mapping for word with korean_hunspell, simple;
ALTER TEXT SEARCH CONFIGURATION
postgres=# \dF+ korean_hunspell
텍스트 검색 구성 &quot;public.korean_hunspell&quot;
파서: &quot;pg_catalog.default&quot;
      토큰       |          사전          
-----------------+------------------------
 asciihword      | english_stem
 asciiword       | english_stem
 email           | simple
 file            | simple
 float           | simple
 host            | simple
 hword           | english_stem
 hword_asciipart | english_stem
 hword_numpart   | simple
 hword_part      | english_stem
 int             | simple
 numhword        | simple
 numword         | simple
 sfloat          | simple
 uint            | simple
 url             | simple
 url_path        | simple
 version         | simple
 word            | korean_hunspell,simple
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이렇게 영어 환경을 복사해서 한국어 환경(korean_hunspell)을 만들고, 영어(ascii 문자열로만 구성된 단어)는 snowball 영어 사전으로, 한국어는 아까 만든 korean_hunspell 사전에서 원형을 찾고 못찾으면, 그냥 그대로 사용하는 식으로 사용하는 것으로 설정했다. (alter mapping에서 korean_hunspell, simple 이렇게 지정한 순서가 중요하다)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;테스트&lt;/h3&gt;&lt;div&gt;&lt;pre&gt;postgres=# select * from ts_debug(&apos;korean_hunspell&apos;, &apos;무궁화 꽃이 피었습니다&apos;);
 alias |    description    |   token    |       dictionaries       | dictionary |   lexemes    
-------+-------------------+------------+--------------------------+------------+--------------
 word  | Word, all letters | 무궁화     | {korean_hunspell,simple} | simple     | {무궁화}
 blank | Space symbols     |            | {}                       |            | 
 word  | Word, all letters | 꽃이       | {korean_hunspell,simple} | simple     | {꽃이}
 blank | Space symbols     |            | {}                       |            | 
 word  | Word, all letters | 피었습니다 | {korean_hunspell,simple} | simple     | {피었습니다}
(5개 행)
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;의도한 대로 의미소 단위로 처리되지 못했다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;NFD &amp;amp; NFC&lt;/h3&gt;&lt;div&gt;이유는 한국어 hunspell 처리방식이 한글 글자를 자모로 모두 분리하고, 그것 기반으로 형태소 분석을 하기 때문이었다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;즉, 의미소 분석 작업 전에, 한글 문자열을 자모단위로 분리해서 넘겨주어야한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;찾아보니, python unicodedata 모듈이 그일을 손쉽게 해서, 다음과 같이 간단하게 함수하나를 만들었다. python 프로시져 언어를 이용하기에, plpython3u 확장 모듈도 추가로 설치해야한다.&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# create extension plpython3u;
CREATE EXTENSION
postgres=# CREATE OR REPLACE FUNCTION public.to_nfd(s text)
 RETURNS text
 LANGUAGE plpython3u
 IMMUTABLE PARALLEL SAFE
AS $function$
import unicodedata
return unicodedata.normalize(&apos;NFD&apos;, s)
$function$;
CREATE FUNCTION&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;다시 테스트&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# select * from ts_debug(&apos;korean_hunspell&apos;, to_nfd(&apos;무궁화 꽃이 피었습니다&apos;));
 alias |    description    |       token       |       dictionaries       |   dictionary    |   lexemes    
-------+-------------------+-------------------+--------------------------+-----------------+--------------
 word  | Word, all letters | 무궁화        | {korean_hunspell,simple} | korean_hunspell | {무궁화}
 blank | Space symbols     |                   | {}                       |                 | 
 word  | Word, all letters | 꽃이           | {korean_hunspell,simple} | korean_hunspell | {꽃}
 blank | Space symbols     |                   | {}                       |                 | 
 word  | Word, all letters | 피었습니다 | {korean_hunspell,simple} | korean_hunspell | {피다,피}
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;성공했다!&lt;/div&gt;&lt;div&gt;리눅스 그놈 터미널 환경에서는 이 자소로 분리된 문자열을 보기 좋게 보여주지만, putty와 같은 환경에서는 token과, lexemes 값이 화면에서는 이상하게 보일 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;to_nfd() 함수를 사용하지 않으려면, defaut 파서 대신에 nfd 내장된 파서를 따로 하나 만들어야한다. 시작 부분은 nfd 작업을 하고, 끝부분에서는 nfc 작업을 하는 파서여야할 것이다. 물론 textsearch_ko 모듈에서 처럼 전각 문자들을 반각 문자로 바꾸고, 한자를 한국어로 바꾸고, 등등 한국어 형태소 분석을 위한 전처리 작업이 필요하기 때문에, 한국어 hunspell 용 구문분석기(성능 좋은!)를 만들 필요는 있어보인다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;한계&lt;/h3&gt;&lt;div&gt;사전 기반 형태소 분석에 따른 텍스트 검색 방식의 공통된 한계점이기도 하지만, 사전에 없는 비속어, 유행어, 신조어 처리 부분은 이 hunspell 방식에서도 여전히 그 한계를 보여준다. 또한 앞에서 보는바와 같이(피었습니다 -&amp;gt; 피, 피다) hunspell 태생 자체가 맞춤법 교정을 목적으로 설계되었기 때문에, 의도하지 않는 단어가 색인에 들어올 수 있게 된다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;나머지&amp;nbsp;&lt;/h2&gt;&lt;div&gt;색인을 만들고, 검색 쿼리를 하고, highlighting 하고, 이런 검색 서비스의 일반적인 이야기는 공식 문서를 참고하면 될 것이다.&lt;/div&gt;&lt;div&gt;결론, 확장 모듈을 만들지 않고, hunspell 사전으로도 한국어 형태소 분석기 기반 검색 서비스를 만들 수 있다. 물론 여러 한계들은 있지만.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;</description>
            <pubDate>Thu, 07 Apr 2022 10:37:07 +0900</pubDate>
            <guid>http://postgresql.kr/blog/hunspell_postgresql.html</guid>
        </item>
        <item>
            <title>PostgreSQL 14 새기능 이야기</title>
            <link>http://postgresql.kr/blog/pg14_new_features.html</link>
            <description>1.&lt;div&gt;&lt;div&gt;postgres=# CREATE PROCEDURE p_test(OUT a integer)&lt;/div&gt;&lt;div&gt;LANGUAGE plpgsql&lt;/div&gt;&lt;div&gt;AS $$&lt;/div&gt;&lt;div&gt;begin&lt;/div&gt;&lt;div&gt;a := 1;&lt;/div&gt;&lt;div&gt;end;&lt;/div&gt;&lt;div&gt;$$;&lt;/div&gt;&lt;div&gt;CREATE PROCEDURE&lt;/div&gt;&lt;div&gt;postgres=# call p_test(1);&lt;/div&gt;&lt;div&gt;&amp;nbsp;a&amp;nbsp;&lt;/div&gt;&lt;div&gt;---&lt;/div&gt;&lt;div&gt;&amp;nbsp;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;2.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# create table tree (id int, link int, data text);&lt;/div&gt;&lt;div&gt;CREATE TABLE&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# copy tree from stdin;&lt;/div&gt;&lt;div&gt;한 줄에 한 레코드씩 데이터를 입력하고&lt;/div&gt;&lt;div&gt;자료입력이 끝나면 backslash 점 (\.) 마지막 줄 처음에 입력하는 EOF 시그널을 보내세요.&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt; 100&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;200&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;data100&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt; 55&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;400&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;data55&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt; 10&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;200&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;data10&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt; 200&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;\N&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;data200&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt; 1000&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;\N&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;data1000&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt; 70&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;200&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;data70&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt; 5&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;10&lt;span style=&quot;white-space:pre&quot;&gt;	&lt;/span&gt;data5&lt;/div&gt;&lt;div&gt;&amp;gt;&amp;gt; \.&lt;/div&gt;&lt;div&gt;COPY 7&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# select * from tree;&lt;/div&gt;&lt;div&gt;&amp;nbsp; id&amp;nbsp; | link |&amp;nbsp; &amp;nbsp;data&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;------+------+----------&lt;/div&gt;&lt;div&gt;&amp;nbsp; 100 |&amp;nbsp; 200 | data100&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;55 |&amp;nbsp; 400 | data55&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;10 |&amp;nbsp; 200 | data10&lt;/div&gt;&lt;div&gt;&amp;nbsp; 200 |&amp;nbsp; &amp;nbsp; &amp;nbsp; | data200&lt;/div&gt;&lt;div&gt;&amp;nbsp;1000 |&amp;nbsp; &amp;nbsp; &amp;nbsp; | data1000&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;70 |&amp;nbsp; 200 | data70&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; 5 |&amp;nbsp; &amp;nbsp;10 | data5&lt;/div&gt;&lt;div&gt;(7개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# WITH RECURSIVE search_tree(id, link, data) AS (&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; SELECT t.id, t.link, t.data&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; FROM tree t where link is null&lt;/div&gt;&lt;div&gt;&amp;nbsp; UNION ALL&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; SELECT t.id, t.link, t.data&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; FROM tree t, search_tree st&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; WHERE t.link = st.id&lt;/div&gt;&lt;div&gt;) SELECT * FROM search_tree;&lt;/div&gt;&lt;div&gt;&amp;nbsp; id&amp;nbsp; | link |&amp;nbsp; &amp;nbsp;data&amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;------+------+----------&lt;/div&gt;&lt;div&gt;&amp;nbsp; 200 |&amp;nbsp; &amp;nbsp; &amp;nbsp; | data200&lt;/div&gt;&lt;div&gt;&amp;nbsp;1000 |&amp;nbsp; &amp;nbsp; &amp;nbsp; | data1000&lt;/div&gt;&lt;div&gt;&amp;nbsp; 100 |&amp;nbsp; 200 | data100&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;10 |&amp;nbsp; 200 | data10&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;70 |&amp;nbsp; 200 | data70&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; 5 |&amp;nbsp; &amp;nbsp;10 | data5&lt;/div&gt;&lt;div&gt;(6개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# WITH RECURSIVE search_tree(id, link, data) AS (&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; SELECT t.id, t.link, t.data&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; FROM tree t where link is null&lt;/div&gt;&lt;div&gt;&amp;nbsp; UNION ALL&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; SELECT t.id, t.link, t.data&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; FROM tree t, search_tree st&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; WHERE t.link = st.id&lt;/div&gt;&lt;div&gt;) SEARCH DEPTH FIRST BY id SET ordercol SELECT * FROM search_tree&amp;nbsp;&lt;/div&gt;&lt;div&gt;order by ordercol;&lt;/div&gt;&lt;div&gt;&amp;nbsp; id&amp;nbsp; | link |&amp;nbsp; &amp;nbsp;data&amp;nbsp; &amp;nbsp;|&amp;nbsp; &amp;nbsp; &amp;nbsp;ordercol&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;------+------+----------+------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp; 200 |&amp;nbsp; &amp;nbsp; &amp;nbsp; | data200&amp;nbsp; | {(200)}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;10 |&amp;nbsp; 200 | data10&amp;nbsp; &amp;nbsp;| {(200),(10)}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; 5 |&amp;nbsp; &amp;nbsp;10 | data5&amp;nbsp; &amp;nbsp; | {(200),(10),(5)}&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp;70 |&amp;nbsp; 200 | data70&amp;nbsp; &amp;nbsp;| {(200),(70)}&lt;/div&gt;&lt;div&gt;&amp;nbsp; 100 |&amp;nbsp; 200 | data100&amp;nbsp; | {(200),(100)}&lt;/div&gt;&lt;div&gt;&amp;nbsp;1000 |&amp;nbsp; &amp;nbsp; &amp;nbsp; | data1000 | {(1000)}&lt;/div&gt;&lt;div&gt;(6개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;3.&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=#&amp;nbsp; SELECT (&apos;{ &quot;postgres&quot;: { &quot;release&quot;: 14 }}&apos;::jsonb)[&apos;postgres&apos;][&apos;release&apos;];;&lt;/div&gt;&lt;div&gt;&amp;nbsp;jsonb&amp;nbsp;&lt;/div&gt;&lt;div&gt;-------&lt;/div&gt;&lt;div&gt;&amp;nbsp;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=#&amp;nbsp; SELECT (&apos;{ &quot;postgres&quot;: { &quot;release&quot;: 14 }}&apos;::jsonb)[&apos;postgres&apos;][&apos;release&apos;];;&lt;/div&gt;&lt;div&gt;&amp;nbsp;jsonb&amp;nbsp;&lt;/div&gt;&lt;div&gt;-------&lt;/div&gt;&lt;div&gt;&amp;nbsp;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;4.&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# SELECT int4multirange(int4range(1, 14), int4range(20, 25), int4range(16,19));&lt;/div&gt;&lt;div&gt;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; int4multirange&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;--------------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;{[1,14),[16,19),[20,25)}&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;5.&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# \h create statistics&amp;nbsp;&lt;/div&gt;&lt;div&gt;명령: CREATE STATISTICS&lt;/div&gt;&lt;div&gt;설명: 새 확장 통계정보 만들기&lt;/div&gt;&lt;div&gt;문법:&lt;/div&gt;&lt;div&gt;CREATE STATISTICS [ IF NOT EXISTS ] 통계정보_이름&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; ON ( 표현식 )&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; FROM 테이블이름&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;CREATE STATISTICS [ IF NOT EXISTS ] 통계정보_이름&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; [ ( 통계정보_종류 [, ... ] ) ]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; ON { 칼럼이름 | ( 표현식 ) }, { 칼럼이름 | ( 표현식 ) } [, ...]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; FROM 테이블이름&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;6.&lt;/div&gt;&lt;div&gt;&lt;div&gt;root@ddb23215b67c:/# psql --version&lt;/div&gt;&lt;div&gt;psql (PostgreSQL) 9.6.21&lt;/div&gt;&lt;div&gt;root@ddb23215b67c:/# psql -h 172.19.0.2 -U postgres&lt;/div&gt;&lt;div&gt;postgres 사용자의 암호:&amp;nbsp;&lt;/div&gt;&lt;div&gt;psql(9.6.21, 14.0 (Debian 14.0-1.pgdg110+1) 서버)&lt;/div&gt;&lt;div&gt;경고: psql 메이저 버전 9.6, 서버 메이저 버전 14.&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;일부 psql 기능이 작동하지 않을 수도 있습니다.&lt;/div&gt;&lt;div&gt;도움말을 보려면 &quot;help&quot;를 입력하십시오.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;postgres=# \q&lt;/div&gt;&lt;/div&gt;&lt;div&gt;---&lt;/div&gt;&lt;div&gt;&lt;div&gt;root@42b8260b3623:/# psql --version&lt;/div&gt;&lt;div&gt;psql (PostgreSQL) 9.5.5&lt;/div&gt;&lt;div&gt;root@42b8260b3623:/# psql -h 172.19.0.2 -U postgres&lt;/div&gt;&lt;div&gt;psql: 10 인증 방법이 지원되지 않음&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;7.&lt;/div&gt;&lt;div&gt;&lt;div&gt;psql (13.3 (Debian 13.3-1.pgdg100+1))&lt;/div&gt;&lt;div&gt;도움말을 보려면 &quot;help&quot;를 입력하십시오.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;postgres=# select extract(hour from current_date);&lt;/div&gt;&lt;div&gt;&amp;nbsp;date_part&amp;nbsp;&lt;/div&gt;&lt;div&gt;-----------&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;0&lt;/div&gt;&lt;div&gt;(1개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;vs.&lt;/div&gt;&lt;div&gt;&lt;div&gt;psql (14.0 (Debian 14.0-1.pgdg110+1))&lt;/div&gt;&lt;div&gt;도움말을 보려면 &quot;help&quot;를 입력하십시오.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;postgres=# select extract(hour from current_date);&lt;/div&gt;&lt;div&gt;오류:&amp;nbsp; date units &quot;hour&quot; not supported&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;8.&lt;/div&gt;&lt;div&gt;&lt;div&gt;psql (13.3 (Debian 13.3-1.pgdg100+1))&lt;/div&gt;&lt;div&gt;도움말을 보려면 &quot;help&quot;를 입력하십시오.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;postgres=# select factorial(-1);&lt;/div&gt;&lt;div&gt;&amp;nbsp;factorial&amp;nbsp;&lt;/div&gt;&lt;div&gt;-----------&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;1&lt;/div&gt;&lt;div&gt;(1개 행)&lt;/div&gt;&lt;/div&gt;&lt;div&gt;vs.&lt;/div&gt;&lt;div&gt;&lt;div&gt;psql (14.0 (Debian 14.0-1.pgdg110+1))&lt;/div&gt;&lt;div&gt;도움말을 보려면 &quot;help&quot;를 입력하십시오.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;postgres=# select factorial(-1);&lt;/div&gt;&lt;div&gt;오류:&amp;nbsp; factorial of a negative number is undefined&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;9.&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;font color=&quot;#0d0a0b&quot; face=&quot;monospace, monospace&quot;&gt;&lt;span style=&quot;font-size: 14.4px; background-color: rgb(248, 249, 250);&quot;&gt;postgres=# show vacuum_cleanup_index_scale_factor;&lt;/span&gt;&lt;/font&gt;&lt;/div&gt;&lt;div&gt;&lt;font color=&quot;#0d0a0b&quot; face=&quot;monospace, monospace&quot;&gt;&lt;span style=&quot;font-size: 14.4px; background-color: rgb(248, 249, 250);&quot;&gt;오류:&amp;nbsp; 알 수 없는 환경 매개 변수 이름: &quot;vacuum_cleanup_index_scale_factor&quot;&lt;/span&gt;&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;10.&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# \h vacuum&lt;/div&gt;&lt;div&gt;명령: VACUUM&lt;/div&gt;&lt;div&gt;설명: 물리적인 자료 정리 작업 - 쓰레기값 청소&lt;/div&gt;&lt;div&gt;문법:&lt;/div&gt;&lt;div&gt;VACUUM [ ( 옵션 [, ...] ) ] [ 테이블과_칼럼 [, ...] ]&lt;/div&gt;&lt;div&gt;VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ 테이블과_칼럼 [, ...] ]&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;옵션 사용법:&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; FULL [ boolean ]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; FREEZE [ boolean ]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; VERBOSE [ boolean ]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; ANALYZE [ boolean ]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; DISABLE_PAGE_SKIPPING [ boolean ]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; SKIP_LOCKED [ boolean ]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; INDEX_CLEANUP { AUTO | ON | OFF }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; PROCESS_TOAST [ boolean ]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; TRUNCATE [ boolean ]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; PARALLEL 정수&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;테이블과_칼럼 사용법:&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; 테이블이름 [ ( 칼럼이름 [, ...] ) ]&lt;/div&gt;&lt;div&gt;URL: https://www.postgresql.org/docs/14/sql-vacuum.html&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;11.&lt;/div&gt;&lt;div&gt;\h alter table&lt;/div&gt;&lt;div&gt;&lt;div&gt;ALTER TABLE [ IF EXISTS ] 이름&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; DETACH PARTITION 파티션_이름 [ CONCURRENTLY | FINALIZE ]&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;12. 답답함을 풀어줍니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# select * from pg_stat_progress_copy ;&lt;/div&gt;&lt;div&gt;&amp;nbsp;pid | datid | datname | relid | command | type | bytes_processed | bytes_total | tuples_processed | tuples_excluded&amp;nbsp;&lt;/div&gt;&lt;div&gt;-----+-------+---------+-------+---------+------+-----------------+-------------+------------------+-----------------&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# select * from pg_stat_wal \gx&lt;/div&gt;&lt;div&gt;-[ RECORD 1 ]----+-----------------------------&lt;/div&gt;&lt;div&gt;wal_records&amp;nbsp; &amp;nbsp; &amp;nbsp; | 12052&lt;/div&gt;&lt;div&gt;wal_fpi&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | 266&lt;/div&gt;&lt;div&gt;wal_bytes&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | 6289054&lt;/div&gt;&lt;div&gt;wal_buffers_full | 1763&lt;/div&gt;&lt;div&gt;wal_write&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | 1896&lt;/div&gt;&lt;div&gt;wal_sync&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;| 132&lt;/div&gt;&lt;div&gt;wal_write_time&amp;nbsp; &amp;nbsp;| 17.65&lt;/div&gt;&lt;div&gt;wal_sync_time&amp;nbsp; &amp;nbsp; | 3705.877&lt;/div&gt;&lt;div&gt;stats_reset&amp;nbsp; &amp;nbsp; &amp;nbsp; | 2021-10-05 12:54:01.75027+09&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# select pg_stat_reset_shared(&apos;wal&apos;);&lt;/div&gt;&lt;div&gt;&amp;nbsp;pg_stat_reset_shared&amp;nbsp;&lt;/div&gt;&lt;div&gt;----------------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;(1개 행)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;postgres=# select * from pg_stat_wal \gx&lt;/div&gt;&lt;div&gt;-[ RECORD 1 ]----+------------------------------&lt;/div&gt;&lt;div&gt;wal_records&amp;nbsp; &amp;nbsp; &amp;nbsp; | 0&lt;/div&gt;&lt;div&gt;wal_fpi&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | 0&lt;/div&gt;&lt;div&gt;wal_bytes&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | 0&lt;/div&gt;&lt;div&gt;wal_buffers_full | 0&lt;/div&gt;&lt;div&gt;wal_write&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | 0&lt;/div&gt;&lt;div&gt;wal_sync&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;| 0&lt;/div&gt;&lt;div&gt;wal_write_time&amp;nbsp; &amp;nbsp;| 0&lt;/div&gt;&lt;div&gt;wal_sync_time&amp;nbsp; &amp;nbsp; | 0&lt;/div&gt;&lt;div&gt;stats_reset&amp;nbsp; &amp;nbsp; &amp;nbsp; | 2021-10-05 16:22:36.509571+09&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;postgres=# show in_hot_standby ;&lt;/div&gt;&lt;div&gt;&amp;nbsp;in_hot_standby&amp;nbsp;&lt;/div&gt;&lt;div&gt;----------------&lt;/div&gt;&lt;div&gt;&amp;nbsp;off&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;명령: CREATE TRIGGER&lt;/div&gt;&lt;div&gt;설명: 새 트리거 만들기&lt;/div&gt;&lt;div&gt;문법:&lt;/div&gt;&lt;div&gt;CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER 이름 { BEFORE | AFTER | INSTEAD OF } { 이벤트 [ OR ... ] }&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;</description>
            <pubDate>Tue, 05 Oct 2021 16:26:49 +0900</pubDate>
            <guid>http://postgresql.kr/blog/pg14_new_features.html</guid>
        </item>
        <item>
            <title>자료 분산 처리 Sharding 이야기</title>
            <link>http://postgresql.kr/blog/postgresql-sharding.html</link>
            <description>&lt;h1&gt;자료 분산 처리 Sharding 이야기&lt;/h1&gt;&lt;div&gt;&lt;h2&gt;파티션 테이블과 뭐가 다르지?&lt;/h2&gt;&lt;div&gt;하나의 테이블에 많은 자료가 담기면 여러 문제가 생긴다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;테이블 자료를 다 살펴봐야 끝나는 ALTER TABLE 작업이 아주 오래 걸린다.&lt;br&gt;최신 버전에서는 ALTER TABLE 작업에서 이렇게 테이블 자료를 모두 살펴보는 일이 많지는 않지만, 그래도 자료가 많은 테이블의 ALTER TABLE 작업은 언제나 부담이 된다.&lt;br&gt;&lt;/li&gt;&lt;li&gt;부분 인덱스를 사용하지 않는 경우라면, 그 자료량만큼 인덱스도 커진다. &lt;br&gt;&lt;/li&gt;&lt;li&gt;테이블 청소를 신경 써야한다. (아직까지 로봇 청소기(autovacuum)가 그리 똑똑하게 작동하지 않는다.)&lt;br&gt; &lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;이런 여러 문제들 때문에, 한 테이블에 담을 자료량이 많으면, 자료가 담기지 않는 상위 테이블과 자료가 담기는 여러 개의 하위 테이블을 만들고, 사용자는 상위 테이블만 사용하는 파티션 테이블 개념이 등장했다.&amp;nbsp;&lt;/p&gt;&lt;p&gt;이 파티션 테이블 이야기는 많은 사용자들이 잘 알고 있을 것이다.&amp;nbsp;&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;이 글에서는 이 파티션 테이블 확장을 이야기한다. 그 부분 자료를 이제는 하나의 데이터베이스 서버가 아닌 다른 데이터베이스 서버에 저장하고, 여러 데이터베이스 서버를 마치 하나의 데이터베이스 서버처럼, 아니, 하나의 테이블인것 처럼 사용하는 방법을 이야기한다.&lt;/p&gt;&lt;p&gt;greenplum 같은 분산 병렬 이야기를 단지 PostgreSQL 만으로 다루는 것이다.&lt;br&gt;&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;h2&gt;멀티마스터와 샤딩&lt;/h2&gt;&lt;div&gt;하나의 데이터베이스 서버가 하나의 서비스에 사용 될 때는 그냥 &apos;서버&apos;라고 하는데, 이 서버들이 여러개 있고, 그것들이 모두 하나의 서비스에 사용 될 때는 요즘은 &apos;노드&apos;라는 말을 한다.&amp;nbsp; 여러 노드(부분 데이터베이스 서버)가 모여 하나의 데이터베이스 서버가 되는 것이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 때 그 부분 데이터베이스 서버에서 다루는 자료가 모두 같은 자료인지, 아니면 전체 자료의 부분 자료인지에 따라 구분할 필요가 있다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;흔히 &apos;멀티마스터 데이터베이스&apos; 이야기를 할 때 이 부분을 생각해야한다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;오라클 RAC인 경우는 각 노드에 있는 자료가 모두 같다.&lt;/div&gt;&lt;div&gt;몽고디비나 엘라스틱서치의 경우는 각 노드가 전체 자료의 부분 자료만 다룬다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;요즘 추세는 후자다. (물론 아직까지 RAC는 넘사벽이다)&lt;/div&gt;&lt;div&gt;이 각 노드는 자신의 자료만 저장하는 것을 샤딩이라고 한다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;샤딩을 이야기 할 때는 그 응용 프로그램이 접근하고자 하는 자료가 있는 노드로 찾아가는 일을 어디서 할 것인가를 잘 살펴봐야한다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;샤딩 기능이 있는 요즘 데이터베이스 서버들은 대부분 이 분산 처리를 위한 또 하나의 서버를 둔다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글에서는 이 두가지 모두를 구현하는 방법을 다룬다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;논리 복제로 구현하는 멀티마스터&lt;/h2&gt;&lt;p&gt;이 글에서 다루는 멀티마스터는 오라클의 RAC를 뜻하지 않는다.&amp;nbsp; 고작 테이블 수준의 비동기식 하위 테이블 복제 수준이다. PostgreSQL 13 버전에서 파티션 테이블의 하위 테이블이 논리 복제를 할 수 있게 되었다.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;/images/logical_replication_partition.png&quot; style=&quot;width: 100%; height: auto&quot;&gt;&lt;/p&gt;&lt;p&gt;위 그림과 같이 &apos;거래내역&apos; 테이블은 3개의 노드에 그 구조가 똑같은 파티션 테이블이 있고, 하위 테이블도 똑 같이 있을 때, 개별 하위 테이블은 각각의 노드에서 읽기/쓰기가 가능하고, 나머지는 다른 노드에서 논리 복제로 기능으로 복제된 읽기 전용 하위 테이블로 구성한다.&amp;nbsp;&lt;/p&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;위 그림의 경우라면, 2020년 자료는 초록색 테이블이 있는 노드에서 DML 작업을 하고, 그 자료 변경 결과는 각각 노란색과 파란색 테이블로 논리 복제 된다.&lt;/div&gt;&lt;div&gt;이런식으로 다른 노드들도 자기 노드에서 DML 작업이 가능한 하위 테이블과 읽기 전용으로 사용할 하위 테이블을 구분해서 구성한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;최종적으로는 세 파티션 테이블의 모든 자료는 동일하게 세 곳에 저장된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;다음은 PostgreSQL 13 버전 기준 위 그림을 구현한 DDL 구문이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;호스트 이름: node1, node2, node3&amp;nbsp;&lt;/div&gt;&lt;div&gt;테이블 소유주(롤):&amp;nbsp;&lt;/div&gt;&lt;div&gt;논리 복제 롤:&amp;nbsp;&lt;/div&gt;&lt;h2&gt;외부 자료 싸개&lt;/h2&gt;&lt;div&gt;PostgreSQL 12 버전에서 파티션 테이블의 하위 테이블로 외부 테이블을 사용할 수 있게 되었다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;자료가 담기지 않는 상위 테이블이 있는 서버와, 부분 자료들이 각각 담기는 하위 테이블이 있는 서버들을 이용해서 분산 처리하는 방법이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;또 다른 방법으로, &lt;br&gt;&lt;/div&gt;&lt;div&gt;각 노드는 모두 똑 같은 상위 파티션 테이블이 있고, &lt;br&gt;&lt;/div&gt;&lt;div&gt;각 노드별로 자기 노드에 저장되는 하위 테이블과 다른 노드에 저장되는 여러 외부 테이블로 구성할 수도 있다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;숙제&lt;/h2&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
            <pubDate>Wed, 16 Jun 2021 12:59:53 +0900</pubDate>
            <guid>http://postgresql.kr/blog/postgresql-sharding.html</guid>
        </item>
        <item>
            <title>WSL2 + Docker Desktop + PostgreSQL</title>
            <link>http://postgresql.kr/blog/wsl_docker_postgresql.html</link>
            <description>&lt;h1&gt;WSL2 + Docker Desktop + PostgreSQL&lt;/h1&gt;&lt;div&gt;오랜만에 짧은 글을 쓴다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;윈도우즈 10 환경에서 PostgreSQL 쓰는 한 방법의 이야기다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;1. wsl2&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Windows 10 버전에서는 Linux용 Windows 하위 시스템, Windows Subsystem Linux, WSL 이라는 기능을 제공한다. 최신 버전으로 업그레이드하면, WSL2 기능을 사용할 수 있다. 옛날 hyper-v (마이크로소프트의 가상화 기술) 기반 위에, 마이크로소프트가 공식 제공하는 리눅스 가상 호스트인데, 이게 딱히 가상호스트가 아닌, cmd 창에서 그냥 리눅스 환경으로 전환할 수 있는 얄궂은 기능이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;초기 WSL (버전1)은 리눅스 텍스트 기반 명령어를 실행할 수 있어요. 정도로 많이 어눌했다. 처음 썼을 때 느낌이 &apos;이걸 왜 만들었지?&apos; 할 정도로 볼품이 없었다. 하지만, 새 WSL(버전2)이 나오면서 &apos;이 놈 물건인데?&apos; 할 정도로 확 바뀌었고, 꽤 많이 리눅스스러워졌다. WSL2에서는 X-Window 응용 프로그램도 실행된다!&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;일단 자신의 PC, 노트북 OS가 윈도우즈 10이라면 최신 버전으로 업그레이드한다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;그리고, WSL 기능을 활성화 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 이야기는 인터넷에 널려있으니, 그 문서를 참고하면 된다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다음 윈도우즈 앱 스토어에서 &apos;WSL&apos;로 검색한 결과 앱들을 하나씩, 만만한 것들을 설치해서 사용해 본다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;한 번도 사용해 보지 않은 이라면, 딱 이 말을 할 것이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&apos;아, 이제 wmware player도 oracle virtualbox 도 필요없겠네!&apos;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기서 기억해야할 것은, wsl (내장 리눅스 가상 호스트 베이스 이미지) 커널 패치가 필요하다.&amp;nbsp; 이 부분은 사용하다보면 어느 사이트에서 이런 파일을 받아서 패치하라고 친절하게 안내해준다. 그게 나오면 하라는 대로 하면 된다. (요즘 wsl2 는 이부분이 패치되어 설치될지도 모르겠다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;2. docker desktop&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기서 더 나아가, wsl 리눅스 커널을 마치 윈도우즈의 커널처럼 쓸 수 있는 환경이니, Docker Desktop 소프트웨어는 그 환경 설정안에, wsl 기반에서 어떤 가상 호스트의 설치 없이 도커 서비스를 이용할 수 있는 기능을 제공한다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 관련 정보도 인터넷에 엄청나게 많다.&amp;nbsp; &apos;wsl docker desktop&apos; 검색어&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기까지 잘 되었다면, docker desktop 응용 프로그램을 실행한 뒤, &lt;br&gt;&lt;/div&gt;&lt;div&gt;cmd 창에서 docker 명령을 사용할 수 있어, 리눅스 환경에서 도커를 쓰는 것과 완벽하게 똑같게 된다. (물론 디스크 마운트 관련은 약간 다르겠지만)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;3. 도커의 한 컨테이너인 PostgreSQL&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 부분은 더 할 말이 없다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;알아서 잘 쓰면 될 뿐. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;윈도우즈용 마이크로소트프 런타임 C 라이브러리로 윈도우즈 환경에 최적화되어 컴파일된 실행파일보다 당연히 성능이 떨어지겠지만, 그래도 개발환경에서는 꽤 쓸모가 있을 것이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;4. 사족, TMI&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;응용 프로그램 개발자에게, 마이너 버전 패치, 메이저 버전 업그레이드 업무 영향도를 파악하는데 분명 보다 편한 길을 열어줄 것이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;리눅스 쉘 환경에서의 명령행 편집 글쇠에 익숙한 이들이라면, cmd 창의 명령행 편집기능이 답답할 수 있을 것이다. 이것도 파워셀로 바꾸고, 파워쉘의 emacs 글쇠 호환 기능 구현을 찾아보면, 이미 우리의 형님들은 친절하게 다 설명하고 있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이젠 리눅스와 윈도우즈의 경계가 거의 허물어진듯하다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;옛날 유행어가 생각난다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;뭣이 중헌디!&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;</description>
            <pubDate>Thu, 03 Jun 2021 01:34:22 +0900</pubDate>
            <guid>http://postgresql.kr/blog/wsl_docker_postgresql.html</guid>
        </item>
        <item>
            <title>트랜잭션 격리 이야기에서 팬텀 읽기 현상</title>
            <link>http://postgresql.kr/blog/pg_phantom_read.html</link>
            <description>&lt;h1&gt;트랜잭션 격리 이야기에서 팬텀 읽기 현상&lt;/h1&gt;&lt;div&gt;트랜잭션 격리 이야기를 하면 빠지지 않고 나오는 것이 격리 수준과 그 수준이 어느 현상까지를 허용하는가?에 대한 이야기다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;간단하게 이야기하면,&amp;nbsp; 일단 트랜잭션의 격리 수준을 지정하는 SET TRANSACTION 명령 도움말을 통해 그 사용법을 보면,&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre class=&quot;synopsis&quot; style=&quot;box-shadow: rgb(223, 223, 223) 3px 3px 5px; border-color: rgb(207, 207, 207); background-color: rgb(247, 247, 247); border-width: 1px; border-style: solid; padding: 2ex; margin-top: 2ex; margin-bottom: 2ex; margin-left: 2ex; overflow: auto; border-radius: 8px;&quot;&gt;SET TRANSACTION &lt;em class=&quot;replaceable&quot;&gt;transaction_mode&lt;/em&gt; [, ...]
SET TRANSACTION SNAPSHOT &lt;em class=&quot;replaceable&quot;&gt;snapshot_id&lt;/em&gt;
SET SESSION CHARACTERISTICS AS TRANSACTION &lt;em class=&quot;replaceable&quot;&gt;transaction_mode&lt;/em&gt; [, ...]

&lt;span class=&quot;phrase&quot;&gt;where &lt;em class=&quot;replaceable&quot;&gt;&lt;code&gt;transaction_mode&lt;/code&gt;&lt;/em&gt; is one of:&lt;/span&gt;

    ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }
    READ WRITE | READ ONLY
    [ NOT ] DEFERRABLE&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기서 격리 수준을 지정하는&amp;nbsp;&lt;em class=&quot;replaceable&quot; style=&quot;background-color: rgb(247, 247, 247);&quot;&gt;transaction_mode&amp;nbsp;&amp;nbsp;&lt;/em&gt;순서 대로 뒤로 갈 수록 트랜잭션의 격리엄격성이 느슨해진다. 즉,&amp;nbsp; serializable 이 제일 엄격하고, read uncommitted 가 제일 덜 엄격하다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;즉 제일 엄격한 격리를 원하면, serializable로 지정하면 되고, 최소한의 격리가 필요하면, read uncommitted 를 지정하면 된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;그런데, PostgerSQL은 read uncommitted 설정은 read committed 설정으로 간주한다. ANSI 문법 호환성 때문에 read uncommitted 문법을 허용할 뿐 내부적으로 read committed 격리 수준으로 작동한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;또한, 이 read committed 격리 수준이 PostgreSQL 기본 격리 수준이기 때문에,&amp;nbsp; set transaction 명령으로 사용자가 임의로 지정하지 않는 이상 read committed 격리 수준으로 작동한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;정리하면, read commited 격리 수준이 어느 수준인지를 알고, 그 외 보다 엄격한 수준이 어떻게 구분되는지를 알아야 적당한 트랜잭션 격리 수준을 지정해서 바르게 사용할 수 있을 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;트랜잭션 격리&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;관계형 데이터베이스에 대한 이론적인 이야기를 하다보면, 트랜잭션의 성질 - 트랜잭션은 이러 이러한 모습이어야지 이것을 트랜잭션이라고 할 수 있고, 이런 규칙을 데이터베이스 서버가 지원해야 관계형 데이터베이스라 할 수 있다고 한다.&amp;nbsp; 시험 때 맨날 나오는 바로 그 &lt;a href=&quot;https://ko.wikipedia.org/wiki/ACID&quot;&gt;ACID&lt;/a&gt;다. &lt;span style=&quot;text-decoration:line-through&quot;&gt;(지겹지도 않는지)&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 트랜잭션의 성질들 가운데 하나인 영문자 I에 해당하는 Isolation - 우리말로 옮기면, 고립, 격리에 해당하는&amp;nbsp; - 이 있는데, 이 성질은 여러 사용자가 동시에 같은 서버를 사용할 때 동시 처리하는 성능과 직결 되기 때문에, 각 데이터베이스마다 그 격리 수준을 조정할 수 있는 유연성을 가진다. (트랜잭션 고립도가 제일 적당한 용어인듯한데, 잘 안쓴다. 요즘은 다들 격리 수준이라고 하는 듯) 모든 관계형 데이터베이스는 이 수준을 트랜잭션 작업 전에 지정할 수 있도록 해서 그 수준을 바꿀 수 있게한다. 그 표준 명령어가 SET TRANSACTION 명령이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL에서는 SET TRANSACTION 명령은 트랜잭션 블럭 내에서 사용할 수 있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;$ psql
psql (13.2 (Debian 13.2-1.pgdg100+1))
도움말을 보려면 &quot;help&quot;를 입력하십시오.

postgres=# SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
경고:  SET TRANSACTION 명령은 트랜잭션 블럭에서만 사용될 수 있음
SET
postgres=# show transaction_isolation ;
 transaction_isolation 
-----------------------
 read committed
(1개 행)&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이처럼 트랜잭션 블럭 밖에서 이 명령을 사용하면, 격리수준이 바뀌지 않는다. 그래서, 기본 격리 수준 자체를 바꾸고자 한다면,&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;postgres=# SET SESSION CHARACTERISTICS 
AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET
postgres=# show transaction_isolation ;
 transaction_isolation 
-----------------------
 serializable
(1개 행)&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;SET SESSION CHARACTERISTICS AS TRANSACTION ... 명령을 사용한다. 물론 이 설정은 현재 세션에 한정된다. 접속을 끊었다가 다시 접속한다면, 기본 격리 수준으로 바뀐다. 데이터베이스 전역으로 이 격리 수준을 바꾸고자 한다면, transaction_isolation 환경 설정 매개 변수값을 바꾸면 된다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;유령 읽기 PHANTOM READ&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;앞에서 이야기했듯이, PostgreSQL에서는 이 수준을&amp;nbsp;serializable 이나,&amp;nbsp;repeatable read 두 수준으로 임의로 설정한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 두 수준의 차이는 트랜잭션 커밋 순서를 직렬화로 보장하느냐이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 말은 아주 단순하다. 먼저 커밋한 놈만 정상 트랜잭션으로 간주하겠다는 것이다.&amp;nbsp;&lt;br&gt;(물론 각 트랜잭션 내에서 자료 읽기 독립성이 보장된 상태에 한정 해서 말이다. 자료 읽기 독립성 - 한 트랜잭션이 한번 읽은 자료는 그 자료를 그 트랜잭션 내에서 바꾸지 않는한 다른 세션이 그 자료를 조작해서 커밋을 했더라도, 항상 같은 값으로 보여야한다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;문제는 PostgreSQL 공식 문서에 소개하고는 repeatable read 격리 수준 상태에서의 phantom read 현상 이야기가 많이 친절하지 못하다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;(그래서, 한국어 페이지 번역도 포기한 상태였다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글은 이 유령 읽기에 대한 꼼꼼한 기록이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;먼저 위키피디아에서 소개하고 있는 유령 읽기 이야기를 소개하면,&amp;nbsp; &lt;a href=&quot;https://en.wikipedia.org/wiki/Isolation_(database_systems)#Phantom_reads&quot;&gt;[링크]&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 현상이 일어나는 이유는 내가 조회한 자료 값을 내가 바꾸지 않는 이상 트랜잭션 내에서 언제나 조회해도 항상 같은 값을 제공한다는 repeatable read 격리 수준에서 그 조회한 값이 만일 어떤 범위라면, 그 범위 전체를 보관하지 못해서 발생하는 문제라고 소개하고 있다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;다른 인터넷 문서에서 소개하고 있는 유령 읽기 현상은 항상 다른 세션에서 insert 를 했다거나, delete 를 했다거나 해서 자신의 세션에서 조회한 자료 세트 범위가 다른 세션에 의해서 변경 될 경우 그것이 자신의 자료 세트가 자기가 조작하지 않았음에도 불구하고, 유령처럼 바뀌는 것이다고 소개하고 있다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;가장 쉬운 표현으로 내가 딱히 새로 만들거나 지운 것도 아닌데,&amp;nbsp; 없던 놈이 보이고, 있던 놈이 사라지는 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;아쉽게도 PostgreSQL repeatable read 격리 수준에서 그 현상을 볼 수가 없다.&amp;nbsp; read committed 수준에서는 이 현상이 나타난다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, PostgreSQL에서는 자신이 조회한 자료가 하나의 로우 자료가 되든 여러 로우 세트가 되든 repeatable read 격리 수준에서는 자신이 그 자료를 변경하지 않는 이상 처음 조회한 그 형상 그대로를 유지한다. 설령 다른 세션에서 그 자료를 조작하고 커밋을 했다고 하더라도 말이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;처음부터 PostgreSQL을 써왔고, 딱히 트랜잭션 격리 수준을 바꾸지 않고 사용했왔던 사용자에게는 이 &apos;유령 읽기&apos; 현상과 이 보다 좀 더 느슨한 격리 수준에서 보이는 &apos;Nonrepeatable Read - 반복되지 않는 읽기(마이크로소프트사 번역)&apos; 현상을 명확하게 구분하기가 힘들다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;(처음부터 구분이 안되었으니, 정확하게 설명하기가 힘들었다. - 글쓴이 핑계, 시간 날 때 꼭 격리 수준 관련 공식 설명서 번역본을 가장 읽기 편하도록 바꿔 놓겠다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;MariaDB 10.5와 PostgreSQL 13의 격리수준에 따른 격리 현상 비교&lt;/h2&gt;&lt;h3&gt;1. MariaDB의 커밋되지 않는 자료 읽기 read uncommited 격리 수준에서 허용되는 dirty read&lt;/h3&gt;&lt;div&gt;&lt;table cellspacing=&quot;1&quot; cellpadding=&quot;1&quot; border=&quot;1&quot;&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;시간흐름&lt;/td&gt;
			&lt;td&gt;첫번째 세션&lt;/td&gt;
			&lt;td&gt;두번째 세션&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;1&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;begin;&lt;/pre&gt;&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;2&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;begin;&lt;/pre&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;3&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;show variables like &apos;tx_isolation&apos;;
+---------------+------------------+
| Variable_name | Value            |
+---------------+------------------+
| tx_isolation  | READ-UNCOMMITTED |
+---------------+------------------+&lt;/pre&gt;&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;4&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;show variables like &apos;tx_isolation&apos;;
+---------------+------------------+
| Variable_name | Value            |
+---------------+------------------+
| tx_isolation  | READ-UNCOMMITTED |
+---------------+------------------+&lt;/pre&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;5&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;select * from t;
Empty set (0.000 sec)&lt;/pre&gt;&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;6&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;insert into t values (1,1);
Query OK, 1 row affected (0.000 sec)&lt;/pre&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;7&lt;/td&gt;
			&lt;td style=&quot;background-color: #ffff00&quot;&gt;&lt;pre&gt;select * from t;
+---+------+
| a | b    |
+---+------+
| 1 |    1 |
+---+------+
1 row in set (0.000 sec)&lt;/pre&gt;&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;8&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;rollback;
Query OK, 0 rows affected (0.046 sec)&lt;/pre&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;9&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;select * from t;
Empty set (0.000 sec)&lt;/pre&gt;&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;노란색 칠한 부분이 바로 커밋 되지 않은 자료 읽기 dirty read&amp;nbsp; 현상이다.&lt;/div&gt;&lt;div&gt;PostgreSQL에서는 이것을 재현할 수 있는 방법이 없다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h3&gt;2. read committed 격리 수준에서 허용되는&amp;nbsp;nonrepeatable read 현상 비교&lt;/h3&gt;&lt;div&gt;기본적으로 다른 세션에서 커밋한 영향(insert, update, delete)을 자신의 트랜잭션 상태 안에서 반영된다는 점은 MariaDB나 PostgreSQL 둘 다 같다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;기억해야 할 것은 MariaDB는 격리 수준의 기본값이 repeatable read 이고, PostgreSQL은 read committed 라는 점이다. 그래서, MariaDB의 기본 트랜잭션 환경에서는 앞에서 이야기한 다른 세션의 commit 영향을 확인할 수 없다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;비교를 하려면 set transaction 명령(정확히는 set session transaction isolation level ... 구문이다)을 먼저 사용해서 격리 수준을 바꾸고 비교해 보아야한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;read committed 격리 수준에서 재미난 비교는 어느 시점에서 작업을 시작했고, 그에 따라 그 대상이 어느 자료가 되느냐라는 부분이 두 데이터베이스가 다르다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h4&gt;2.1 MariaDB&lt;/h4&gt;&lt;/div&gt;&lt;div&gt;&lt;table cellspacing=&quot;1&quot; cellpadding=&quot;1&quot; border=&quot;1&quot;&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;시간흐름&lt;/td&gt;
			&lt;td&gt;첫번째 세션&lt;/td&gt;
			&lt;td&gt;두번째 세션&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;1&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; begin;
&amp;gt; show variables like &apos;tx_iso%&apos;;
+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| tx_isolation  | READ-COMMITTED |
+---------------+----------------+&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;2&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; begin;
&amp;gt; show variables like &apos;tx_iso%&apos;;
+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| tx_isolation  | READ-COMMITTED |
+---------------+----------------+&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;3&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; select * from t;
+---+------+
| a | b    |
+---+------+
| 1 |    1 |
| 2 |    2 |
+---+------+&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;4&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;&amp;gt; update t set b = b + 1;
Query OK, 2 rows affected (0.000 sec)
Rows matched: 2  Changed: 2  Warnings: 0&lt;/pre&gt;&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;5&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; delete from t where b = 2;&lt;/pre&gt;
			여기서 이 작업은 멈춘다&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;6&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; commit;&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;7&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;Query OK, 1 row affected (5.139 sec)&lt;/pre&gt;
			첫번째 세션의 update 작업이 commit되면 delete 작업이 실행된다.&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;8&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; select * from t;
+---+------+
| a | b    |
+---+------+
| 2 |    3 |
+---+------+&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;두번째 세션의 delete 작업 결과를 주목해야한다. 작업 결과는 a = 1 자료가 첫번째 세션 작업으로 b 값이 2로 바뀌었고, 그것이 커밋되었기에, 커밋된 자료 (read committed) 기준으로 a = 1, b = 2 자료가 삭제되었다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h4&gt;2.2 PostgreSQL&lt;/h4&gt;&lt;/div&gt;&lt;div&gt;&lt;table cellspacing=&quot;1&quot; cellpadding=&quot;1&quot; border=&quot;1&quot;&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;시간흐름&lt;/td&gt;
			&lt;td&gt;첫번째 세션&lt;/td&gt;
			&lt;td&gt;두번째 세션&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;1&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; begin;
&amp;gt; show transaction_isolation ;
 transaction_isolation
-----------------------
 read committed&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;2&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; begin;
&amp;gt; show transaction_isolation ;
 transaction_isolation
-----------------------
 read committed&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;3&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; select * from t;
 a | b
---+---
 1 | 1
 2 | 2&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;4&lt;/td&gt;
			&lt;td&gt;
                        &lt;pre&gt;&amp;gt; update t set b = b + 1;
UPDATE 2&lt;/pre&gt;&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;5&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; delete from t where b = 2;&lt;/pre&gt;
			여기서 이 작업은 멈춘다&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;6&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; commit;&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;7&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;DELETE 0&lt;/pre&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;8&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; select * from t;
 a | b
---+---
 1 | 2
 2 | 3&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div style=&quot;color: red&quot;&gt;PostgreSQL에서는 a=1 자료가 지워지지 않는다!&lt;/div&gt;&lt;div&gt;delete 작업의 시작 시점을 해당 작업을 시작하는 시점 (delete 작업을 시작하는 시점)으로 보고, 작업이 실제로 시작되는 시점은 자기가 잠금을 획득하는 시점(첫번째 세션이 commit 한 뒤)으로 보기 때문이다. 즉, delete 작업을 시작하는 시점에는 b=2 자료인 a=2 자료를 작업대상으로 했는데,&amp;nbsp; 첫번째 세션이 commit 되면서 a=2 자료는 b=3으로 바뀌어버렸기 때문에, 삭제할 자료가 사라진 샘이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;read committed 격리 수준에서 read 의 타이밍이 그 작업의 시작 시점이 아니라, 정말 자기가 그 작업을 시작할 수 있는 시점(잠금을 할 수 있는 시점)으로 한다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;독특한 모습이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;아마 undo 로그에 옛 버전 자료를 보관하는 것과, 해당 테이블에 옛 버전을 그대로 두는 MVCC 처리 방식에서 생긴 차이같다. 더 깊게 안 살펴봐서 모른다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;아무튼 PostgreSQL을 쓴다면, 기억해야하는 부분이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;3. repeatable read 수준에서 유령 읽기 실제 현상&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;이 현상은 PostgreSQL에서는 나타나지 않기 때문에, MariaDB 경우를 먼저 소개하고, 똑 같은 작업을 PostgreSQL에서 실행할 경우 어떻게 서버가 반응하는지를 소개한다.&lt;/div&gt;&lt;div&gt;&lt;h4&gt;3.1 MariaDB&lt;/h4&gt;&lt;/div&gt;&lt;div&gt;&lt;table cellspacing=&quot;1&quot; cellpadding=&quot;1&quot; border=&quot;1&quot;&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;시간흐름&lt;/td&gt;
			&lt;td&gt;첫번째 세션&lt;/td&gt;
			&lt;td&gt;두번째 세션&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;1&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;begin;&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;2&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;begin;&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;3&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; show variables like &apos;tx_iso%&apos;;
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;4&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; show variables like &apos;tx_iso%&apos;;
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;5&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; insert into t values (3,3);
Query OK, 1 row affected (0.000 sec)&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;6&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; select * from t;
+---+------+
| a | b    |
+---+------+
| 1 |    1 |
| 2 |    2 |
+---+------+
2 rows in set (0.000 sec)

&amp;gt; update t set b = b + 1 ;&lt;/pre&gt;자료가 두 개 있는 것을 확인하고(아직 첫번째 세션에서 입력한 자료가 커밋되지 않았기 때문에, 두 개의 자료가 나온다), 그 전체를 업데이트했으나, 첫번째 세션의 커밋이 되지 않아 기다리게 된다.&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;7&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;commit;&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;8&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;첫번째 세션이 커밋되면, 업데이트 작업 처리 결과가 나온다.
			&lt;pre&gt;&lt;span style=&quot;color: red; background-color: yellow&quot;&gt;Query OK, 3 rows affected&lt;/span&gt; (31.775 sec)
Rows matched: 3  Changed: 3  Warnings: 0&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;9&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; select * from t;
+---+------+
| a | b    |
+---+------+
| 1 |    2 |
| 2 |    3 |
&lt;span style=&quot;color: red; background-color: yellow&quot;&gt;| 3 |    4 |&lt;/span&gt;
+---+------+&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;두번째 세션의 update 이후 없던 자료(a=3)가 갑자기 나타난다. 이 현상은 오류가 아니다. 표준 규약에서 repeatable read 수준에서 이런 유령 읽기를 허용하기 때문이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;delete와 update 복합 상황에서는 보다 유령스러운(?) 현상이 나타난다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;table cellspacing=&quot;1&quot; cellpadding=&quot;1&quot; border=&quot;1&quot;&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;시간흐름&lt;/td&gt;
			&lt;td&gt;첫번째 세션&lt;/td&gt;
			&lt;td&gt;두번째 세션&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;1&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;begin;&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;2&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;begin;&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;3&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; show variables like &apos;tx_iso%&apos;;
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;4&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; show variables like &apos;tx_iso%&apos;;
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;5&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; select  * from t;
+---+------+
| a | b    |
+---+------+
| 1 |    2 |
| 2 |    3 |
| 3 |    4 |
+---+------+
3 rows in set (0.000 sec)

&amp;gt; delete from t where a= 1;
Query OK, 1 row affected (0.005 sec)&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;6&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;&amp;gt; select  * from t;
+---+------+
| a | b    |
+---+------+
| 1 |    2 |
| 2 |    3 |
| 3 |    4 |
+---+------+
3 rows in set (0.000 sec)

&amp;gt; delete from t where a= 2;
Query OK, 1 row affected (0.005 sec)&lt;/pre&gt;
			각 세션에서 각자의 자료를 지웠다. (row exclusive lock 작업이기 때문에, 다른 세션이 커밋하지 않아도 자신의 작업은 기다림 없이 실행된다.)&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;7&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;commit;&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;8&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;&amp;gt; select * from t;
+---+------+
| a | b    |
+---+------+
| 1 |    2 |
| 3 |    4 |
+---+------+
2 rows in set (0.000 sec)

&amp;gt; update t set b = b + 1 where a=1;
&lt;span style=&quot;color: red; background-color: yellow&quot;&gt;Query OK, 0 rows affected&lt;/span&gt; (0.000 sec)
Rows matched: 0  Changed: 0  Warnings: 0

&amp;gt; select * from t;
+---+------+
| a | b    |
+---+------+
&lt;span style=&quot;color: red; background-color: yellow&quot;&gt;| 1 |    2 |&lt;/span&gt;
| 3 |    4 |
+---+------+
2 rows in set (0.000 sec)&lt;/pre&gt;남은 자료를 확인하고, 첫번째 세션에서 지운(이미 커밋한) a=1 자료를 업데이트 했으나 반영 되지 않고, 유령으로 남아있는다.
			&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 유령 읽기를 허용하는 경우, 자료의 정합성 문제는 없겠지만, 이런 현상을 잘 이해하고 있지 않는다면, 많이 황당할 것 같다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h4&gt;3.2 PostgreSQL&lt;/h4&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL 서버는 앞에서 설명했듯이 repeatable read 수준에서도 이런 유령 읽기를 허용하지 않는다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;table cellspacing=&quot;1&quot; cellpadding=&quot;1&quot; border=&quot;1&quot;&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;시간흐름&lt;/td&gt;
			&lt;td&gt;첫번째 세션&lt;/td&gt;
			&lt;td&gt;두번째 세션&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;1&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=# begin;
BEGIN
postgres=*# set transaction 
isolation level repeatable read;
SET&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;2&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=# begin;
BEGIN
postgres=*# set transaction 
isolation level repeatable read;
SET&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;3&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=*# insert into t values (3,3);
INSERT 0 1&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;4&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=*# select * from t;
 a | b
---+---
 1 | 1
 2 | 2
(2개 행)

postgres=*# update t set b = b + 1;
UPDATE 2
postgres=*# select * from t;
 a | b
---+---
 1 | 2
 2 | 3
(2개 행)&lt;/pre&gt;첫번째 세션의 insert 작업이 커밋되지 않았기에, 두 개의 자료만 보이고, 그것을 update 하면, MariaDB랑 달리 기다림 없이 작업이 바로 성공한다.
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;5&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=*# commit;
COMMIT&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;6&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=*# commit;
COMMIT&lt;/pre&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;7&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;&lt;pre&gt;postgres=# select * from t;
 a | b
---+---
 3 | 3
 1 | 2
 2 | 3
(3개 행)&lt;/pre&gt;&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;&lt;div&gt;각 세션의 각각 자료에 대한 각각 처리였기 때문에, 오류 없이, 처리 되었고, 두번째 세션의 트랜잭션이 끝나기 전까지 이미 첫번째 세션에서 추가한 a=3 자료는 보이지도 않으면, update 작업에 영향을 받지도 않는다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;문제는 다른 세션에서 update, delete 작업을 하고, 커밋한 자료에 대해서 자기가 자료를 조작하려고 할 때, PostgreSQL은 오류를 낸다.&lt;/div&gt;&lt;div&gt;&lt;table cellspacing=&quot;1&quot; cellpadding=&quot;1&quot; border=&quot;1&quot;&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;시간흐름&lt;/td&gt;
			&lt;td&gt;첫번째 세션&lt;/td&gt;
			&lt;td&gt;두번째 세션&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;1&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=# begin;
BEGIN
postgres=*# set transaction 
isolation level repeatable read;
SET&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;2&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=# begin;
BEGIN
postgres=*# set transaction 
isolation level repeatable read;
SET&lt;/pre&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;3&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=*# select * from t;
 a | b
---+---
 3 | 3
 1 | 2
 2 | 3
(3개 행)

postgres=*# delete from t where a = 1;
DELETE 1&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;4&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=*# select * from t;
 a | b
---+---
 3 | 3
 1 | 2
 2 | 3
(3개 행)

postgres=*# delete from t where a = 2;
DELETE 1&lt;/pre&gt;각자의 자료를 지웠기 때문에, 별 기다림 없이 작업은 진행된다.
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;5&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=*# commit;
COMMIT&lt;/pre&gt;
			&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;6&lt;/td&gt;
			&lt;td&gt;&amp;nbsp;&lt;/td&gt;
			&lt;td&gt;
			&lt;pre&gt;postgres=*# select * from t;
 a | b
---+---
 3 | 3
 1 | 2
(2개 행)

postgres=*# update t set b = b + 1 where a = 1;
&lt;span style=&quot;color: red; background-color: yellow&quot;&gt;오류:  동시 삭제 작업 때문에 순차적 액세스가 불가능합니다&lt;/span&gt;&lt;/pre&gt;첫번째 세션에서 커밋한 뒤에 그 자료를 건드리면, 오류를 낸다.&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL에서는 repeatable read 이상의 격리 수준을 사용하는 경우는 위와 같은 오류에 대한 대비책을 반드시 응용프로그램에서 준비해야한다. 아주 중요한 이야기다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글은 유령 읽기에 대한 이야기이기 때문에, serializable 격리 수준에 대한 구체적인 상황과 그 오류들에 대해서는 생략한다. (글 쓰다가 지쳐서, 이 부분은 숙제로 남겨둔다)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;마치며&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;이상 PostgreSQL과 유령 읽기 이야기를 꼼꼼하게 하려고 마음은 먹었으나, 글 쓰다 지쳐 엉성하게 마무리 한다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;실무 상황에서는 이보다 훨씬 다양한 동시 트랜잭션 작업이 발생하며, 다양한 오류를 만들 수 있을 것이다. 이 때, 여기서 소개하고 있는 기본 개념만 잘 기억해 두면, 그 상황을 해결하는데 별로 어렵지는 않을 것이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL에서는 앞에서 이야기 했듯이, repeatable read, serializable 격리 수준 상황에서는 반드시 응용프로그램 차원에서 동시 작업 위배 오류에 대한 예외 처리가 꼭 필요하다는 것을 기억두자.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;</description>
            <pubDate>Wed, 07 Apr 2021 19:06:01 +0900</pubDate>
            <guid>http://postgresql.kr/blog/pg_phantom_read.html</guid>
        </item>
        <item>
            <title>도커 공식 포스트그레스큐엘 이미지를 쓸 때</title>
            <link>http://postgresql.kr/blog/when_useing_docker_official_postgres_image.html</link>
            <description>이 글은 PostgreSQL 서버를 보다 쉽게 설치하고, 사용하기 위한 사용자들(국내에서는 흔히 개발자라고 하죠)을 위한 글입니다. &lt;br&gt;&lt;div&gt;실무 운영 환경에서는 그다지 도움이 되지는 않습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;간단히 버전별 비교를 하거나, &lt;br&gt;&lt;/div&gt;&lt;div&gt;잠깐 자료 정리를 하기 위해서 데이터베이스가 필요할 때&lt;/div&gt;&lt;div&gt;참고할만한 글입니다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;PostgreSQL 컨테이너 만들기&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL 공식 Docker 이미지 이름은 postgres 입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;도커 공식 PostgreSQL 이미지는 기본값으로 데이터클러스터 영역을 도커 볼륨을 사용하도록 설정되어있습니다. 즉, 
컨테이너를 만들 때, 따로 이 영역으로 사용할 도커 볼륨을 만들지 않았다면, 임의로 도커 볼륨을 만들어 사용합니다. 이때, 임의 이름 컨테이너가 만들어지는데, 이 때, 임의의 볼륨도 함께 만들어집니다.&lt;br&gt;&lt;/div&gt;&lt;pre&gt;$ docker run -d -e POSTGRES_PASSWORD=password postgres
2c73aac0c3af2474e32405787b0d87ee06ca8cd82f27057e7fbcf4fb387081e9
$ docker ps
CONTAINER ID   IMAGE      COMMAND                  CREATED         STATUS         PORTS      NAMES
2c73aac0c3af   postgres   &quot;docker-entrypoint.s…&quot;   8 seconds ago   Up 7 seconds   5432/tcp   quizzical_poincare
$ docker volume ls
DRIVER    VOLUME NAME
local     6a3f3cc38d6a232b0029f649e7a0697ab936dc992f530707e48ba7ebea5f378f&lt;/pre&gt;&lt;div&gt;postgres
 이미지 기본 entrypoint는 docker-entrypoint.sh 스크립트로 하는 일은 데이터클러스터가 없으면, 
initdb 명령을 실행합니다. 이 때 기본값으로 사용하는 데이터디렉터리는 /var/lib/postgresql/data 입니다.&amp;nbsp;
 또한 이 initdb 작업이 필요한 상황이라면, 컨테이너를 만들 때, -e 옵션으로 반드시 POSTGRES_PASSWORD 환경
 변수 값을 지정해야합니다. (이미 만들어진 데이터클러스터를 쓴다면 굳이 POSTGRES_PASSWORD 설정이 꼭 필요하지는 
않습니다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;도커는 이렇게 임의의 볼륨을 사용하는 컨테이너가 만들어졌을 때, 이 컨테이너를 더 이상 쓰지 않아, 삭제될 경우 (--rm 옵션으로 컴테이너가 종료되면 자동으로 그 컨테이너를 지울 경우도 포함 해서) 이 때 같이 만들어진 임의의 볼륨을 삭제하지 않습니다. 이렇게 해서 데이터클러스터를 다시 사용할 수 있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이 볼륨도 지워야한다면, docker volume rm 명령으로 수동으로 지워야합니다.&lt;br&gt;&lt;/div&gt;&lt;pre&gt;$ docker stop quizzical_poincare
quizzical_poincare
$ docker rm quizzical_poincare
quizzical_poincare
$ docker run -d -v 6a3f3cc38d6a232b0029f649e7a0697ab936dc992f530707e48ba7ebea5f378f:/var/lib/postgresql/data  postgres
0a35b2d912b111b32976bdb7ccaf25f5fca05e8d4c51981cef1c456ff9d1343a
$ docker ps
CONTAINER ID   IMAGE      COMMAND                  CREATED         STATUS         PORTS      NAMES
0a35b2d912b1   postgres   &quot;docker-entrypoint.s…&quot;   5 seconds ago   Up 5 seconds   5432/tcp   quirky_feynman
$ docker volume ls
DRIVER    VOLUME NAME
local     6a3f3cc38d6a232b0029f649e7a0697ab936dc992f530707e48ba7ebea5f378f&lt;/pre&gt;&lt;div&gt;이렇기 때문에, postgres 도커 이미지를 실행할 때는 -v 옵션으로 볼륨을 지정하는 것이 좋습니다. 문제는 그 볼륨 이름이 임의의 문자열이기 때문에, 이미지와 컨테이너와 볼륨의 조합이 어떤지 기억해 두지 않는다면, 해당 컨테이너를 지우고, (지워졌거나) 다시 그 볼륨을 사용하려고 할 때 힘듭니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;그래서, -v 옵션을 지정하기 전에 사용자가 미리 볼륨을 만듭니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;운영환경이라면, 이 볼륨은 당연히 소프트웨어 정의 저장공간으로 도커 호스트와 별개로 운영되는 영구 저장공간이겠지만, 일반 개발환경에서는 단순히 로컬 디스크를 쓸 것이고, 이 때 이 데이터베이스를 위해 사용할 볼륨을 간단하게 만들고 그 볼륨을 컨테이너를 만들 때 사용합니다.&lt;br&gt;&lt;/div&gt;&lt;pre&gt;$ docker volume create postgres13-data
postgres13-data
$ docker volume ls
DRIVER    VOLUME NAME
local     postgres13-data
$ docker run -d -e POSTGRES_PASSWORD=password -v postgres13-data:/var/lib/postgresql/data postgres
2a5284255846bdc80aa3093b52eff4357ade24216b42aad3faa4e064ea61b174
$ docker ps -a
CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS                    PORTS      NAMES
2a5284255846   postgres                   &quot;docker-entrypoint.s…&quot;   5 seconds ago    Up 4 seconds              5432/tcp   adoring_dhawan&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이렇게 만든 데이터클러스터용 볼륨을 사용할 때 기억해야할 것은 PostgreSQL 메이저 버전 단위로 그 볼륨을 재활용할 수 있다는 것입니다. 그래서, 볼륨 이름에 그 메이저 버전을 지정해 놓는 것이 좋습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;태그 선택&lt;br&gt;&lt;/h2&gt;&lt;div&gt;&lt;h3&gt;Docker postgres 태그 설명&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;postgres 이미지의 태그는 버전입니다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;태그 없음: 가장 마지막 공식 배포판&lt;/li&gt;&lt;li&gt;9.6, 10, 11, 12, 13: 메이저 버전의 마지막 배포판&lt;/li&gt;&lt;li&gt;9.6.x, 10.x, 11.x, 12.x, 13.x: 각 메이저별 패치 버전 배포판&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;이런식입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h3&gt;무슨 태그를 쓸까?&lt;br&gt;&lt;/h3&gt;&lt;/div&gt;&lt;div&gt;가장 최신 버전을 잠깐 살펴 볼 때는 태그를 사용하지 않고 그냥 이미지 이름만 사용해서 컨테이너를 만드는 것이 한 방법이겠지만, 이렇게 만들어진 컨테이너가 사용하는 데이터 클러스터 영역을 재활용할 때, 도커 이미지가 메이저 버전 단위로 업그레이드 되었을 때, 손이 많이 가게 됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL은 메이저 버전이 바뀌게 되면, pg_upgrade 명령으로 사용하고 있던 데이터클러스터를 기반으로 새 메이저 버전용 데이터클러스터를 만들어 업그레이드해야 사용할 수 있습니다.&amp;nbsp; 아쉽게도 아직 이런 작업은 사용자가 직접해야합니다. 그렇기 때문에, 통상 응용 프로그램 개발용 PostgreSQL 도커 컨테이너를 쓴다면, 패치 버전까지 미리 딱 정할 것인지, 메이저버전까지만 정할 것인지를 선택해야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;1. 메이저버전&lt;/div&gt;&lt;div&gt;postgres:13 이렇게 태그를 메이저버전으로만 선택하는 경우 도커 이미지 관리자에 의해 언제든지 임의로 docker pull 작업으로 그 이미지들이 업그레이드 될 수 있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, 해당 메이저 버전의 패치 작업은 단순히 컨테이너 중지, 삭제, 재생성 작업을 함으로 도커 서비스 전체 형상 관리가 좀 더 단순해 질 수 있습니다.&amp;nbsp; 단점은 docker ps 명령으로 해당 컨테이너가 정확하게 무슨 버전을 쓰고 있지는 알 수 없다는 점입니다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;2. 패치버전&lt;/div&gt;&lt;div&gt;일반적으로 이렇게 고정된 태그를 쓰는 경우는 그 패치 버전별 비교를 위해서 이거나, 패치 작업 뒤 기타 문제로 긴급하게 하위 패치 버전으로 다운그래이드를 염두해 둘 때입니다. 단점은 도커 호스트에서 관리해야할 postgres 이미지들이 다양해 진다는 것입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;결국 태그 선택은 사용자의 몫입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;기억해야할 것은 docker 는 pull 명령으로 이미지를 자동 업데이터할 수 있고, 이런 장점을 잘 이용하면, 그 이미지를 사용하는 컨테이너들의 버전 패치는 자동화 할 수 있다는 것입니다. (물론 postgres 이미지와 볼륨을 따로 지정하는 경우에 한정 되어서 말이죠)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;데이터베이스 기본 설정값&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;도커 공식 이미지로 PostgreSQL을 실행할 경우, 실행되는 데이터베이스의 기본 설정값이 너무 보수적입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;a href=&quot;https://postgresql.kr/blog/docker_postgresql.html&quot;&gt;지난 글&lt;/a&gt;에서도 언급했듯이, &lt;br&gt;&lt;/div&gt;&lt;div&gt;데이터베이스가 처음 실행되고 접속이 확인되면 응용 프로그램을 사용하기 전에 먼저 어느 정도의 서버 설정이 필요하기는 합니다.&amp;nbsp; 특히나 메모리 관련해서 설정을 하지 않으면 성능이 많이 떨어집니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;기본적으로 살펴보아야할 설정값들은 다음과 같습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;effective_cache_size&lt;/li&gt;&lt;li&gt;maintenance_work_mem&lt;/li&gt;&lt;li&gt;max_wal_size&lt;/li&gt;&lt;li&gt;min_wal_size&lt;/li&gt;&lt;li&gt;shared_buffers&lt;/li&gt;&lt;li&gt;wal_buffers&lt;/li&gt;&lt;li&gt;work_mem&lt;br&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;div&gt;alter system SQL 명령으로 변경하고, 해당 컨테이너를 다시 실행하고 사용하는 것이 좋습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;여기서 고민해볼말한 것이, logging_collector 설정인데, 기본값은 off 입니다. 이말은 데이터베이스 서버가 기록하는 모든 서버 로그는 표준 출력(stdout)으로 보냅니다. 즉, 이 로그는 docker logs 명령으로 살펴볼 수 있으며, 도커에서 특별히 로그 관리를 따로 지정하지 않는다면, 최대 10MB의 마지막 로그만 보관합니다. 서버 로그를 서버 독립적으로 관리하고싶다면, log* 관련 환경 설정도 변경이 필요합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;또 한 부분은 한국어 관련 설정입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;도커 공식 postgres 이미지는 영어 환경을 기반으로 합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;시간은 UTC 기반입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 한국, 한국어 기반으로 변경 하고자 한다면, 데이터베이스 서버의 환경 설정을 바꾸기 보다, 그 컨테이너가 실행되는 OS 환경 설정을 한국, 한국어 환경으로 설정하고, 그 기반 위에 데이터베이스 서버가 실행되게 하는 것이 좋습니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 말은 공식 postgres 이미지를 다시 한번 수정해서 새로운 한국어 postgres 이미지를 만들어 사용하겠다는 것을 의미합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;통상 이 작업은 Dockerfile을 만들고, docker build 명령으로 한국어 환경에 특화된 postgres 이미지를 만들어서 사용합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;예제 Dockerfile 내용은 다음과 같습니다. &lt;br&gt;&lt;/div&gt;&lt;pre&gt;FROM postgres:13.1
RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime &amp;amp;&amp;amp; \
    sed -i &apos;s/# ko_KR.UTF-8 UTF-8/ko_KR.UTF-8 UTF-8/&apos; /etc/locale.gen &amp;amp;&amp;amp; \
    locale-gen
ENV LANG=ko_KR.utf8 \
    LC_COLLATE=C \
    POSTGRES_INITDB_ARGS=--data-checksums&lt;/pre&gt;&lt;div&gt;나머지 사용법은 동일합니다. &lt;br&gt;&lt;/div&gt;&lt;pre&gt;$ docker build -t postgres-ko:13.1 .
$ docker run -d -e POSTGRES_PASSWORD=password -v postgres13-data:/var/lib/postgresql/data postgres-ko:13.1
&lt;/pre&gt;&lt;div&gt;문제는 이런 식으로 공식 이미지를 기반으로 또 새로운 이미지를 독자적으로 만든다면, 계속 바뀌는 공식 이미지에 맞춰 계속 해당 한국어용 이미지를 계속 관리해야한다는 부담이 생기게됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;컨테이너 패치하기&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;이미 사용중인 12.4 데이터베이스 서버를 12.5로 패치하고자 하는데, 그 서버가 컨테이너 기반으로 운영 되고 있는 경우 작업 방법을 소개합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;간단합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;그 컨테이너를 지우고 다시 만든다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;끝&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;즉, 다시 만들 때, 예전에 처음 만들 때 사용했던 그 옵션들을 그대로 사용해야한다는 것입니다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;사용했던 태그가 메이저 버전이었다면, 다음과 같은 작업으로 진행됩니다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;docker pull postgres:12&lt;/div&gt;&lt;div&gt;docker stop pg12&lt;/div&gt;&lt;div&gt;docker rm pg12&lt;/div&gt;&lt;div&gt;docker run -d --name pg12 -v pg12:/var/lib/postgresql/data postgres:12&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;패치 버전까지 있는 태그를 사용하는 경우도 마찬가지겠죠.&lt;/div&gt;&lt;div&gt;물론 network 관련도 기존에 쓰던 것과 같은 환경으로 재구축해야하는 것도 빼먹지 말아야합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;(이 글은 docker network 관련 글이 아니여서 여기서는 생략합니다.)&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;마치며&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;이 글에는 쿠버네티스 같은 통합 컨테이너 관리 솔루션을 사용할 때의 이야기는 빠져있습니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글에서도 소개하고 있는 그 볼륨 문제를 아직 깔끔하게 해결하지 못했기 때문입니다. &lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
            <pubDate>Mon, 28 Dec 2020 04:26:58 +0900</pubDate>
            <guid>http://postgresql.kr/blog/when_useing_docker_official_postgres_image.html</guid>
        </item>
        <item>
            <title>자료 목록 페이징 처리</title>
            <link>http://postgresql.kr/blog/pg_paging.html</link>
            <description>&lt;h1&gt;자료 목록 페이징 처리&lt;/h1&gt;&lt;div&gt;(ORDER BY ... OFFSET 10 FETCH FIRST 10 ROWS WITH TIES 이야기)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글은 응용 프로그램 개발자들을 위한 글입니다.&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;h2&gt;DECLARE &amp;amp;&amp;amp; FETCH&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;전통적으로 클라이언트/서버 환경에서 사용해 오던 방식이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;트랜잭션이 유지된다는 것을 전제한 기능이다.&lt;/div&gt;&lt;div&gt;웹 응용프로그램처럼 관련 작업에서 클라이언트와 서버 연결이 항상 같을 수 없는 경우에는 효율적이지 못하다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;하지만 사용자 정의 프로시져나, 함수 안에서 아주 큰 레코드 집합을 대상을 뽑아 차례로 어떤 작업을 진행하는 경우 이 기법은 해당 백엔드 세션 메모리를 효율적으로 사용하는 점에서 여전히 쓸모 있는 기능이기에 적당한 곳에 적당히 잘 쓸 수 있어야 한다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;흔히 서버 측 커서 server side cursor 라는 개념으로 설명하며, declare 라는 SQL 명령으로 커서를 지정하고, fetch 라는 명령으로 자료를 뽑는다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;기억해야 할 것은 &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;declare 명령은 begin 명령으로 시작하는 트랜잭션 내에서만 사용할 수 있으며&lt;/li&gt;&lt;li&gt;declare 명령으로 만든 커서는 반드시 그 집합을 다 사용한 시점에 트랜잭션 블럭 내에서 close 명령으로 닫아야하며&lt;/li&gt;&lt;li&gt;limit offset 과 마찬가지로 자료의 끝부분은 그 만큼 차례로 건너 뛰어야 뽑을 수 있다는 것이다. - 아주 많은 자료 집합을 대상으로 커서를 만들고 바로 fetch last 명령으로 바로 가장 마지막 자료를 뽑는다면, 그 전체 자료를 끝까지 다 뒤지고 난 다음 자료를 보여준다. 이런 형태로 사용하는 것은 아주 비효율적이다. 이런 업무 요건이라면, 커서를 만들 때 정렬을 반대로 하고, fetch first로 뽑아야한다.&lt;br&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;OFFET LIMIT&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;표준이 확정되기 전 옛 문법이다.&lt;/div&gt;&lt;div&gt;offset 다음에 숫자, limit 다음에 숫자, 예를들어 offset 10 limit 10 이런 형태로 사용되며, 해당 자료의 처음부터 열 개를 건너 뛰고,&amp;nbsp; 그 다음부터 10개를 뽑는 식이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 구문을 사용해야 할 때 기억해야 할 것은&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;이 옵션 만으로 자료의 순서를 보장 받을 수 없다. 항상 동일한 자료를 뽑으려면 반드시 order by 구문과 함께 사용해서 자료 순서를 지정해야하며,&lt;/li&gt;&lt;li&gt;앞에서 이야기 한 것처럼 자료가 많으면 많을 수록, offset 값이 크면 클 수록 그 만큼 건너뛰는 작업이 포함 됨으로 작업량은 많아진다는 점이다. - offset 1억 limit 1 명령은 1억 개 건너 뛰는 작업을 하고 한 개를 뽑는다는 것을 기억해야한다. &lt;br&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;OFFSET FETCH FIRST ROWS ONLY&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;SQL:2008에 채택되고, PostgreSQL에서도 8.4 버전부터 사용가능 했다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;현재 표준 구문이다. 즉 offset limit 구문을 사용하고 있다면, 차근히 이 구문으로 바꾸는 것이 SQL 호환성을 높이는데 도움을 줄 것이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;문법 정의는 다음과 같다.&lt;/div&gt;&lt;div&gt;&lt;pre&gt;[ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY ]&lt;/pre&gt;&lt;div&gt;FIRST, NEXT / ROW, ROWS는 호환성을 높이기 위해서 같이 지원하며 하는 일은 똑같다.&amp;nbsp; 그냥 단순하게, FETCH FIRST 출력로우수 ROWS ONLY로 기억해두면 된다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 구문도 offset limit 구문에서 설명한 것과 똑 같은 제약사항이 있기 때문에, order by 구문과 함께 사용하는 것이 안전하며, offset 값이 커지는 것에 주의를 해야한다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;완전한 구문은 다음과 같은 모습이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre class=&quot;programlisting&quot;&gt;SELECT * FROM distributors ORDER BY 2 OFFSET 10 FETCH FIRST 10 ROWS ONLY;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;ORDER BY ... OFFSET 10 FETCH FIRST 10 ROWS WITH TIES&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;이번 13버전에서 WITH TIES 옵션이 추가되었다. SQL:2008 규약에 정의되어 있었으나, 이번에 구현되었다. order by 정렬 조건에 따라 마지막에 뽑힌 자료랑 같은 자료가 더 있다면, 짤린 나머지들도 모두 함께 뽑는 옵션이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;ioseph=&amp;gt; select sid,sname from kostore order by sname offset 80 fetch first 10 rows only;
   sid    |     sname
----------+----------------
 20417775 | 007라이브
 24927957 | 007마트
  5065157 | 007마트
 25995200 | 007마트
  8626174 | 007마트
 24546504 | 007마트
 25250807 | 007마트
 24572759 | 007마트
 22375536 | 007마트 노원점
 24576761 | 007마트 대덕점
(10개 행)
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이런 자료일 때 앞에서 두 개만 뽑으면&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;ioseph=&amp;gt; select sid,sname from kostore&lt;br&gt;         order by sname offset 80 fetch first 2 rows only;
   sid    |   sname
----------+-----------
 20417775 | 007라이브
 24927957 | 007마트
(2개 행)
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이렇게 나오고, WITH TIES 옵션을 사용하면 다음과 같이 나온다.&lt;/div&gt;&lt;div&gt;&lt;pre&gt;ioseph=&amp;gt; select sid,sname from kostore &lt;br&gt;         order by sname offset 80 fetch first 2 rows with ties;
   sid    |   sname
----------+-----------
 20417775 | 007라이브
 24927957 | 007마트
  5065157 | 007마트
 25995200 | 007마트
  8626174 | 007마트
 24546504 | 007마트
 25250807 | 007마트
 24572759 | 007마트
(8개 행)
&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;여기서 주의할 점은 딸려서 뽑힐 자료량이 얼마나 되는지 예측할 수 없다는 점이다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;ORDER BY ... OFFSET 10 FETCH FIRST 10 PERCENT ROWS WITH TIES&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;오라클에서 제공하는 PERCENT는 아직 지원하지 않는다. 자료량의 10%만 뽑는 옵션이다. 현실적인 업무 내에서는 이 옵션이 아주 유용한 옵션이긴 하지만, 이 옵션을 구현하려면 먼저 표준으로 모두가 합의한 규칙이 있어야하는데, 아직까지는 없는 상황이기에 PostgreSQL에서는 수용되고 있지 않는 것 같다. (몇 해전부터 이 관련 패치 코드가 올라왔지만, 계속 채택 보류 상태로 있는 상황이다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;현재로써는 이런 방식의 자료 뽑기는 window 함수인 percent_rank() 함수를 사용하는 방법 밖에는 없는 것 같다. (실무에서 이런 자료 뽑기를 하면서 고민해 본적이 없어서 이것밖에 모르겠다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;대용량 자료 페이징&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;자료량이 많으면 OFFSET 값 처리를 위한 자료 건너 뛰는 비용이 커진다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;다른 방법을 찾아야한다.&lt;/div&gt;&lt;div&gt;전통적인 방법은 건너 뛰는 작업을 할 때 인덱스를 사용하는 것이다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL의 인덱스 접근은 쿼리 최적화기에서 알아서 순방향/역방향 인덱스 순차 검색을 선택한다.&amp;nbsp; 따로 역방향 인덱스를 만들 필요도 없으며, 역방향으로 순차 검색 하라고 힌트를 지정하지도 않는다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;ioseph=&amp;gt; \d kostore
                    &quot;public.kostore&quot; 테이블
 필드명 |         종류          | Collation | NULL허용 | 초기값 
--------+-----------------------+-----------+----------+--------
 sid    | integer               |           | not null | 
 sname  | text                  |           |          | 
 scate  | text                  |           |          | 
 lng    | numeric               |           |          | 
 lat    | numeric               |           |          | 
 p      | geography(Point,4326) |           |          | 
인덱스들:
    &quot;kostore_pkey&quot; PRIMARY KEY, btree (sid)
    &quot;kostore_p_i&quot; gist (p)
    &quot;kostore_p_sname_i&quot; btree (sname)

ioseph=&amp;gt; explain select sid,sname from kostore 
         order by sid offset 0 fetch first 10 rows only;
                                          QUERY PLAN                                          
------------------------------------------------------------------------------------------
 Limit  (cost=0.43..1.70 rows=10 width=23)
   -&amp;gt;  Index Scan using kostore_pkey on kostore  
       (cost=0.43..298257.94 rows=2356915 width=23)
(2개 행)

ioseph=&amp;gt; explain select sid,sname from kostore 
         order by sid desc offset 0 fetch first 10 rows only;
                                              QUERY PLAN                                               
------------------------------------------------------------------------------------------
 Limit  (cost=0.43..1.70 rows=10 width=23)
   -&amp;gt;  Index Scan Backward using kostore_pkey on kostore  
       (cost=0.43..298257.94 rows=2356915 width=23)
(2개 행)

&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;이처럼 실행계획기가 인덱스를 순차 검색 할지, 거꾸로 검색 할지를 알아서 한다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;이 특성을 이용해서, 페이징 처리를 개념적으로 정리하면&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;목록 순서를 결정한다 (게시물이라면 게시물번호 기준 내림 차순 - 최신 게시물이 먼저 보이는 정렬)&lt;/li&gt;&lt;li&gt;게시물 번호 제일 큰 값을 구하고&lt;/li&gt;&lt;li&gt;그 값보다 작거나 같은 자료를 게시물 번호 내림 차순으로 처음부터 목록 수 + 1 만큼 구하고,&amp;nbsp;&lt;/li&gt;&lt;li&gt;화면에서는 목록수 만큼 보여주고, + 1 되는 자료는 [다음] 링크로 처리한다&lt;/li&gt;&lt;/ol&gt;&lt;div&gt;이런 식으로 처리한다. [이전]도 마찬가지다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;한편, 이것보다 좀 더 복잡하게, 1,2,3,4 ...... 이런식으로 목록&amp;nbsp; 아래에 [다음] 이 아니, 여러 페이지로 바로 가는 기능을 제공한다면,&amp;nbsp; 자료가 하나씩 추가 될 때마다 미리 계산한 전체 자료수를 참조해서 전체 페이지수를 알고 그에 맞는 페이징 처리를 한다.&amp;nbsp; 실무에서 쓰는 구체적인 예는 &lt;a href=&quot;http://database.sarang.net/list.phps&quot;&gt;데이터베이스 사랑넷 게시판 시스템의 페이징 처리 소스&lt;/a&gt;를 참조하면 된다.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;올 해 pgday.seoul에서 때마침 이 관련 쿼리 튜닝 기법에 대한 발표가 있었다.&lt;/div&gt;&lt;div&gt;해당 자료: &lt;a href=&quot;https://www.slideshare.net/pgday_seoul/pgdayseoul-2020-sql-tuning&quot;&gt;PostgreSQL SQL Tuning&lt;/a&gt;&lt;/div&gt;&lt;div&gt;해당 자료 중간 부분에서 여기서 이야기하는 대량 자료 페이징 처리에 대한 이야기를 이 글보다 더 자세히 다루고 있다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;마무리&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;이 글은 올 해 구월에 나온 PostgreSQL 13 새기능 가운데,&amp;nbsp;OFFSET FETCH FIRST WITH TIES 부분을 소개하기 위한 것이였다.&amp;nbsp; 글을 쓰다가 오래된 이야기를 함께 하는 것이 더 도움이 될 것 같다. 글이 많이 산만해지기는 했다.&amp;nbsp;&lt;/div&gt;&lt;div&gt;지금까지 자료 목록 보기 쿼리의 표준 구문과 대량 자료에서는 어떻게 처리하는 것이 효율적인가에 대해서&amp;nbsp; 다뤘다.&lt;/div&gt;&lt;div&gt;요즘처럼 다양한 소프트웨어 설계가 사용되고 있는 상황에서 여기서 소개한 방법만이 좋은 방법이다고 이야기하는 것은 좁은 생각이다. 단지, SQL 표준 구문으로 목록 페이징은 이렇게 처리한다는 것을 알고 표준 호환성이 높은 쿼리를 작성하는데 도움이 되면 좋겠다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;</description>
            <pubDate>Fri, 20 Nov 2020 22:38:50 +0900</pubDate>
            <guid>http://postgresql.kr/blog/pg_paging.html</guid>
        </item>
        <item>
            <title>프로메테우스와 포스트그레스큐엘</title>
            <link>http://postgresql.kr/blog/prometheus_for_postgres.html</link>
            <description>&lt;div&gt;&lt;h1&gt;PostgreSQL과 Prometheus와의 만남&lt;/h1&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;Prometheus 간단 소개&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;홈페이지: https://prometheus.io/&lt;/div&gt;&lt;div&gt;하는 일:&lt;/div&gt;&lt;div&gt;&lt;ol&gt;&lt;li&gt;여러 exporter (정보 수집 하는 놈) 들을 지정해서 일정 간격으로 정보를 수집하라고 시키고, &lt;br&gt;&lt;/li&gt;&lt;li&gt;그 수집한 정보를 자기 데이터베이스(기본값: tsdb)에 저장하고, &lt;/li&gt;&lt;li&gt;다른 놈(일반적으로 시각화 도구들)이 그 정보를 보고자 하면 제공한다.&amp;nbsp;&lt;/li&gt;&lt;/ol&gt;통신방법: HTTP&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;Prometheus에서 사용하는 각종 exporter들&lt;/div&gt;&lt;div&gt;홈페이지: https://prometheus.io/docs/instrumenting/exporters/&lt;/div&gt;&lt;div&gt;어마어마하다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;모든 exporter들의 실행 방법은 모두 제각각이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;통상 데이터베이스라면, 해당 데이터베이스 정보와 그 데이터베이스가 실행되는 운영체제 정도가 전부일 것이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;운영체제 수집기는 node_exporter, PostgreSQL 수집기는 postgres_exporter 가 사람들이 제일 많이 쓰는 것 같다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;필요하다면, 직접 만들어서 쓰는 수고로움을 즐기는 것도 나쁘지는 않을 것 같지만, 여기서는 postgres_exporter 를 소개한다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;postgres_exporter &lt;/h2&gt;&lt;/div&gt;&lt;div&gt;홈페이지: https://github.com/wrouesnel/postgres_exporter&lt;/div&gt;&lt;div&gt;사용법은 해당 홈페이지에서 소개하고 있다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;프로메테우스 쪽 프로젝트들을 보면 다들 미리 static으로 컴파일 된 아주 큰 실행파일을 제공한다. postgres_exporter도 예외는 아니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;그냥 그 프로그램이 실행된 운영체제에 맞는 실행 파일을 다운로드 받아서 실행하기만 하면 된다.&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;데이터베이스 관리자 시각에서 보면, 그냥 하나의 데이터베이스 클라이언트다.&lt;/div&gt;&lt;div&gt;프로메테우스가 되든 다른 무엇인가가 되든, 이 postgres_expoter 에게 정보를 요청하면, (이 놈도 하나의 웹서버이기 때문에, 일반적으로 HTTP URL 호출로 요청한다) 해당 데이터베이스에 접속해 있다가 그 데이터베이스의 각종 정보를 수집해서 요청자에게 HTTP 프로토콜을 이용해서 보낸다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;postgresql 접속 방법 - postgres_exporter 실행방법&lt;/h2&gt;&lt;div&gt;접속에 관련 모든 설정은 해당 운영체제 환경 변수 설정으로 한다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;직접 OS 콘솔에서 실행한다. 그에 맞에 설정하고 실행하면 된다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;리눅스라면,&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;DATA_SOURCE_NAME=postgresql://postgres_exporter:password@localhost:5432/postgres?sslmode=disable&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;이런식이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;실행되면, 기본 웹서버 포트인 9187번 포트가 리슨 상태로 웹 서버 하나가 실행된다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;웹 서버니까 curl 같은 거로 확인해서 자료가 보인다면, 정상 진행된 것이다.&lt;/div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;curl http://localhost:9187/metrics&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;프로메테우스가 아니더라도 윗 url을 주기적으로 호출하고, 그 값을 처리해서 시계열 자료에 넣어두고 시각화 해서 보면 된다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글은 PostgreSQL 데이터베이스 성능 자료들을 프로메테우스에 활용하는 방법에 대한 글임으로 이제 프로메테우스로 넘어간다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;prometheus.yml 파일&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;scrape_configs: 섹션에서 다음처럼 expoter를 하나 더 추가해주고 실행하면 된다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;  - job_name: &apos;pgexporter&apos;
    static_configs:
    - targets: [&apos;pgexporter:9187&apos;]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;job_name은 임의의 문자열이고, targets 값은 해당 postgres_exporter 웹서버의 호스트이름(또는 호스트IP)과 그 포트다.&lt;/div&gt;&lt;div&gt;설정하고, 프로메테우스를 실행하면 이번에는 9090 포트로 웹서버가 하나 실행된다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;테스트 URL은 &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;curl http://localhost:9090/targets &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div&gt;여기서 해당 job으로 등록한 pgexporter가 나오면 된다.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;기억해야 할 것들&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;&lt;ul&gt;&lt;li&gt;postgres_exporter는 현 시점의 데이터베이스 성능 지표값을 프로메테우스로 보낸다. &lt;br&gt;&lt;/li&gt;&lt;li&gt;프로메테우스는 prometheus.yml 환경설정 파일에서 그 지표값을 어떤 주기로 수집할 것인지를 지정한다. &lt;br&gt;&lt;/li&gt;&lt;li&gt;프로메테우스로 수집된 자료는 따로 지정하지 않으면, prometheus tsdb로 저장된다.&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;숙제&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;보다 간편하게 설정하고, 사용하는 docker 환경 이야기는 일부러 뺐다. (이 글을 쓰면서 테스트한 것은 모두 docker 환경이었지만)&lt;/div&gt;&lt;div&gt;이 부분은 실무에서 사용될 때는 컨테이너 통합 환경에서 어떻게 활용될지를 같이 고민해야하기에, 겪어보지 못한 부분이기에 더 자세히 다룰 수 없었다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;프로메테우스 tsdb가 다중 사용자 환경에서 그다지 좋은 성능을 내지 못한다는 이야기가 있다. (경험해 보지 못한 사실) 이 문제를 해결 하려면, tsdb 대신에 보다 시계열 자료를 전문적으로 다루는 데이터베이스를 사용하는 것도 도전해볼 만한 숙제다 .&lt;/div&gt;&lt;div&gt;이왕이면, PostgreSQL 데이터베이스를 쓰겠다면, timescaledb 확장 모듈과, pg_prometheus 확장 모듈을 이용해서, prometheus 기본 데이터베이스를 PostgreSQL로 바꾸는 것도 한 방법일 것이다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이 글에서는 prometheus 까지만 다뤘다. 실무에서는 prometheus 만으로는 편한 모니터링 도구가 되지 못한다.&amp;nbsp; 즉, 프로메테우스에 저장된 자료를 어떻게 시각화 할 것이냐가 최대 관심사가 될 것이다.&amp;nbsp; 이 부분도 숙제로 남겨둔다.&amp;nbsp; grafana dashbard가 대세이긴하나, 좀더 깊이 있게 들여다 보면 많은 생각들이 왔다 갔다 해서 그냥 숙제로 남겨둔다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;부록&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;도커 작업 내역&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;pre&gt;&lt;code&gt;# db 실행 
docker run -d --name postgres -e POSTGRES_PASSWORD=password postgres
# postgres_exporter 실행
docker run -d --link postgres --name pgexporter \
-e DATA_SOURCE_URI=&quot;postgres/postgres?application_name=exporter&amp;amp;sslmode=disable&quot; \
-e DATA_SOURCE_USER=postgres -e DATA_SOURCE_PASS=password \
wrouesnel/postgres_exporter
# prometheus 실행
docker run  -d --link pgexporter --name prometheus prom/prometheus
# prometheus.yml 편집
docker exec -it prometheus vi /etc/prometheus/prometheus.yml
# 위에서 설명한 postgres_exporter 항목 추가
# prometheus 재실행
docker stop prometheus
docker start prometheus
# grafana
docker run -d --link prometheus --name grafana -p 3000:3000 grafana/grafana&lt;br&gt;# 최종확인, http://그라파나가실행된호스트IP:3000/ 페이지를 웹블로우져로 열고&lt;br&gt;# 화면왼쪽아래쪽 환경 설정 메뉴 -&amp;gt; Data Sourses 에서 Prometheus를 만들고&lt;br&gt;# 화면왼쪽위에 + 메뉴 -&amp;gt; Import 에서 grafana.com 에서 가져오기 항목에 9628 입력&lt;br&gt;# PostgreSQL Database 대쉬보드가 만들어졌으면 열어서 화면 위쪽 가운데 인스턴스 부분을 바꿈&lt;br&gt;# 끝&lt;br&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;/div&gt;</description>
            <pubDate>Mon, 10 Aug 2020 18:19:06 +0900</pubDate>
            <guid>http://postgresql.kr/blog/prometheus_for_postgres.html</guid>
        </item>
        <item>
            <title>PostgreSQL 버전 이야기</title>
            <link>http://postgresql.kr/blog/pg_version.html</link>
            <description>&lt;div&gt;&lt;h2&gt;소프트웨어의 버전 이야기&lt;/h2&gt;&lt;div&gt;대부분의 소프트웨어는 숫자나 단어로 된 버전을 붙여 이야기합니다. 이는 그 버전이 언제적 버전이고, 지금도 그 소프트웨어를 만든, 관리하는 쪽에서 그것을 관리하고 있는지가 아주 중요한 부분이기 때문입니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;h2&gt;PostgreSQL 6.0&lt;/h2&gt;&lt;div&gt;이 데이터베이스에 좀 더 관심을 가져본 사람이라면, 왜 PostgreSQL 6.0 버전부터 시작하는지 궁금할 것입니다. 참고로 지금은 PostgreSQL 12 입니다.&amp;nbsp; 6.0 부터 시작하는 이유는 &lt;a href=&quot;https://dsf.berkeley.edu/oldpost/&quot;&gt;https://dsf.berkeley.edu/oldpost/&lt;/a&gt; 홈페이지를 살펴보면 설명이 됩니다. 언제까지 이 홈페이지를 유지할지는 모르겠지만, PostgreSQL은 이 홈페이지에서 시작합니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;그 페이지에 보면, postgres-v2, postgres-v3, postgres-v4, postgres95 이렇게 네 버전의 PostgreSQL 전신 버전이 있습니다. (postgres-v1 은 어디있는지 모르겠습니다. - 공식문서를 읽어보면 아마 첫 버전은 비공개였는 듯합니다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 해서, postgres 프로젝트의 정통성을 잇는다는 뜻으로 postgres95 다음 PostgreSQL 6.0 버전이 나오게 됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;PostgreSQL 12 까지&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;이렇게 6부터 시작해서, 7.1 버전까지는 그냥 시간 순서 대로 단일 마스터 버전에서 쭉 작업 순서에 따라 버전을 매겨 갑니다.&amp;nbsp; 64bit 리눅스를 본격적으로 지원하는 7.2 버전부터 다중 버전 관리 체계로 바뀝니다. (7.1부터 64bit 리눅스에서 쓸 수 있기는 하나, 여러 모로 준비과정이었습니다.)&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;이렇게 세월은 흘렀는데, 문제가 생깁니다. 워낙 오랫동안 개발 되고 있는 소프트웨어이다보니, 버전 번호를 다 써버렸습니다. 다섯자리 숫자로 처음 한자리가 버전 첫번째 숫자로 9를 다 써버려서, 9.6 상황에서 결국, 9.99 버전 형태로 버전이 계속 바뀌는 꼴이 되어버렸습니다. 그래서, 10버전을 출시하면서 버전 할당 규칙을 바꾸는 대공사를 하게됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;그래서 지금의 여섯자리 숫자 버전체계를 가지게 되었습니다. (show server_version_num)&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;PostgreSQL 버전과, PG_VERSION 비교&lt;/h2&gt;&lt;div&gt;PostgreSQL에서는 초기부터 아주 중요한 OS 환경 변수가 있는데, 자료가 저장되는 공간의 최상위 위치인 $PGDATA 입니다. 이것은 PostgreSQL에서는 데이터 카탈로그 디렉터리, 또는 그냥 피지데이터 디렉터리라고 부릅니다. 모든 버전의 데이터 카탈로그 디렉터리 안에는 정상적인 경우라면, 반드시 PG_VERSION 파일이 있습니다.&amp;nbsp; cat 같은 명령으로 그 파일의 내용을 보면, 그냥 숫자만 보입니다. 예를 들어 PostgreSQL 12.2 버전으로 사용하는 데이터 디렉터리라면, 12가 보입니다.&amp;nbsp; 이 숫자가 PostgreSQL 메이져 버전과 같다고 간주해도 좋습니다. (최근 10년 사이 버전들에 대해서는) 즉, PostgreSQL 버전 가운데 맨 마지막 숫자를 제외한 버전이 같다면 그 데이터 디렉터리를 사용할 수 있음을 의미합니다. (물론 OS, CPU 비트수도 같아야합니다.)&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;pg_control 버전&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;PostgreSQL에는 숨어 있는 버전 번호가 또 하나 있는데, pg_controldata 명령으로 살펴 볼 수 있는 pg_control 버전과, 카탈로그 버전입니다. 하나는 PG_VERSION 과 비슷한 숫자로 되어있고, 다른 하나는 날짜로 되어있습니다. 실재로 이 값이 postgres 서버 프로그램과 PGDATA 디렉터리 사이 실행 가능한 조합을 검사하는 값으로 사용됩니다. 또한 이 값 기준으로 pg_upgrade 작업이 진행됩니다. &lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;&lt;h2&gt;보너스&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;준비중 :(&lt;br&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</description>
            <pubDate>Sat, 21 Mar 2020 16:10:03 +0900</pubDate>
            <guid>http://postgresql.kr/blog/pg_version.html</guid>
        </item>
    </channel>
</rss>