PostgreSQL 성능 튜닝: 데이터베이스 서버 최적화 가이드

작성자: Vik Fearing
2024년 9월 20일


하드웨어에서 PostgreSQL 쿼리 최적화까지 데이터베이스 성능을 향상시키는 방법

이 문서는 PostgreSQL 및 EDB Postgres Advanced Server(EPAS) 10~13 버전의 성능 튜닝 방법을 소개합니다. 사용된 시스템은 RHEL 계열의 Linux 배포판 8버전이며, 여기서 제시하는 내용은 일반적인 가이드라인으로, 실제 튜닝 세부 사항은 워크로드에 따라 달라질 수 있습니다. 하지만 대부분의 배포 환경에서 시작점으로 활용할 수 있습니다.

튜닝은 하드웨어에서 시작해 스택 상단으로 진행하며, 최종적으로 애플리케이션의 SQL 쿼리 최적화를 다룹니다. 스택 상단으로 갈수록 워크로드에 따른 조정이 중요해지기 때문에, 가장 일반적인 부분부터 시작해 워크로드에 특화된 항목으로 점차 구체화합니다.

머신 설계하기


이 문서는 베어 메탈과 가상화된 머신에 초점을 맞추고 있습니다. 향후 버전에서는 클라우드 및 컨테이너화된 디자인도 포함될 예정입니다.

베어 메탈

PostgreSQL용 베어 메탈 서버를 설계할 때는 몇 가지 요소를 고려해야 합니다. CPU, RAM, 디스크, 그리고 경우에 따라 네트워크 카드가 포함됩니다.

CPU

적절한 CPU 선택은 PostgreSQL 성능에 매우 중요할 수 있습니다. 데이터 규모가 커질수록 CPU 속도가 중요하며, L3 캐시가 큰 CPU는 성능을 향상시킵니다. OLTP 성능을 위해서는 더 많고 빠른 코어가 운영 체제와 PostgreSQL의 효율성을 높이는 데 도움이 됩니다. 반면, L3 캐시가 큰 CPU는 대규모 데이터 세트를 처리하는 데 적합합니다.

L3 캐시란 무엇인가요?
CPU에는 최소 두 가지 캐시가 있습니다: L1(기본 캐시)과 L2(보조 캐시).

  • L1 캐시는 CPU 코어에 내장된 가장 작고 빠른 캐시입니다.
  • L2 캐시는 L1보다 크고 느리지만 L1에 데이터를 공급하는 역할을 합니다.

L1과 L2 캐시는 각각의 코어에 독립적으로 존재하는 반면, L3 캐시는 모든 코어가 공유합니다. L3는 L1과 L2보다 느리지만 여전히 RAM보다는 빠릅니다. L3 캐시가 클수록 대규모 데이터 세트를 처리할 때 CPU 성능이 향상되며, PostgreSQL의 병렬 쿼리에도 유리합니다.

RAM

RAM은 하드웨어 중에서 가장 저렴하면서도 PostgreSQL 성능에 가장 큰 영향을 미칩니다. 운영 체제는 사용 가능한 메모리를 최대한 활용해 데이터를 캐싱하려고 합니다. 캐싱이 많을수록 디스크 I/O가 줄어들고 쿼리 처리 속도가 빨라집니다. 새 하드웨어를 구입할 때는 가능한 많은 RAM을 추가하세요. 나중에 RAM을 확장하는 것은 비용적으로나 기술적으로 더 어려울 수 있습니다. 다운타임 없이 RAM을 교체할 수 있는 핫스왑 시스템이 아닌 이상, 확장을 위해 시스템 중단이 필요합니다. 사용 가능한 메모리에 따라 PostgreSQL의 여러 매개변수를 조정해야 하며, 이는 아래에서 다룹니다.

디스크

애플리케이션이 I/O 집약적(읽기 및 쓰기)이라면, 더 빠른 디스크 세트를 사용하는 것이 성능을 크게 개선할 수 있습니다. NVMe 및 SSD 드라이브 등 여러 옵션이 있습니다.

가장 기본적인 원칙은 WAL 디스크를 데이터 디스크와 분리하는 것입니다. WAL은 쓰기 집약적인 데이터베이스에서 병목현상이 될 수 있으므로, WAL을 별도의 빠른 드라이브에 두면 이 문제를 해결할 수 있습니다. 최소한 RAID 1을 사용해야 하며, 데이터베이스에서 쓰기 작업이 많은 경우 RAID 10이 필요할 수 있습니다.

인덱스와 데이터를 별도의 테이블스페이스와 드라이브에 저장하면 성능이 향상됩니다. 특히 PostgreSQL이 SATA 드라이브에서 실행되는 경우에 유용합니다. 그러나 SSD와 NVMe 드라이브에서는 일반적으로 필요하지 않습니다. 데이터에는 RAID 10 구성을 권장합니다.

드라이브 최적화에 대한 자세한 내용은 “파일 시스템 최적화” 섹션을 참고하세요. PostgreSQL에 사용할 수 있는 스토리지 및 RAID 옵션에 대한 자세한 내용은 관련 블로그를 참조하시기 바랍니다.

네트워크 카드

네트워크 카드는 PostgreSQL 성능과는 무관해 보일 수 있지만, 데이터 규모가 커지면 빠른 네트워크 카드나 본딩된 네트워크 카드를 사용하는 것이 기본 백업 속도를 높이는 데 도움이 될 수 있습니다.

가상 머신(Virtual Machines)

가상 머신은 가상화 레이어로 인해 베어 메탈 서버에 비해 약간의 성능 손실이 발생합니다. 또한 공유 리소스의 영향으로 사용 가능한 CPU와 디스크 I/O 성능이 감소할 수 있습니다.

다음은 가상 머신에서 PostgreSQL 성능을 향상시키기 위한 몇 가지 팁입니다:

  1. VM을 특정 CPU와 디스크에 고정
    VM을 호스트 머신에서 실행 중인 다른 VM으로 인해 발생할 수 있는 성능 병목현상을 제거하거나 제한하려면 특정 CPU와 디스크에 고정하세요.
  2. 설치 전에 디스크를 미리 할당
    데이터베이스 작업 중 호스트가 디스크 공간을 할당하는 것을 방지하려면 디스크를 사전에 할당하세요. 이 작업을 수행할 수 없는 경우, postgresql.conf 파일에서 다음 두 가지 매개변수를 조정하세요:
    • wal_recycle 매개변수 비활성화
      기본적으로 PostgreSQL은 WAL 파일을 재활용하기 위해 이름을 변경합니다. 그러나 Copy-On-Write(COW) 파일 시스템에서는 새로운 WAL 파일을 생성하는 것이 더 빠를 수 있으므로, 이 매개변수를 비활성화하면 VM 성능이 향상됩니다.
    • wal_init_zero 매개변수 비활성화
      기본적으로 WAL 공간은 WAL 레코드가 삽입되기 전에 미리 할당됩니다. 이는 COW 파일 시스템에서 WAL 작업을 느리게 만들 수 있습니다. 이 매개변수를 비활성화하면 이러한 기능이 비활성화되어 VM 성능이 향상됩니다. 설정을 끄면 파일 생성 시 마지막 바이트만 작성되어 예상 크기를 가지게 됩니다.

시스템 튜닝


PostgreSQL 운영 체제를 튜닝하면 성능을 더 향상시킬 수 있는 추가 기회를 제공합니다. 이 가이드는 Red Hat Enterprise Linux(RHEL) 계열에서 PostgreSQL 튜닝에 초점을 맞추고 있습니다.

tuned daemon

RHEL에서 대부분의 튜닝 작업은 tuned 데몬으로 수행됩니다. 이 데몬은 운영 체제를 워크로드에 맞게 최적화하여 더 나은 성능을 발휘할 수 있도록 조정합니다.

아래 명령은 RHEL 8을 기준으로 작성되었습니다. RHEL 7을 사용하는 경우, 예제에서 dnf 명령이 사용된 부분을 yum 명령으로 대체하면 됩니다.

tuned 데몬은 기본적으로 설치되어 있습니다. 만약 설치되어 있지 않다면(예: Kickstart 파일 설정 때문), 아래 명령어로 설치하세요:

dnf -y install tuned

이후, 아래 명령으로 활성화합니다:

systemctl enable --now tuned

tuned를 사용하면 시스템 관리자가 커널 설정을 빠르고 동적으로 변경할 수 있습니다. 따라서 /etc/sysctl 파일을 직접 수정할 필요가 없습니다. 이 작업은 tuned가 자동으로 처리합니다.

tuned에는 몇 가지 사전 정의된 프로파일이 포함되어 있습니다. tuned-adm list 명령을 사용하여 프로파일 목록을 확인할 수 있습니다. RHEL 설치 도구는 환경에 따라 적합한 기본값을 선택합니다. 베어 메탈의 기본값은 throughput-performance이며, 이는 처리량 증가를 목표로 합니다. 시스템을 분석한 후 tuned 데몬이 추천하는 값을 확인하려면 아래 명령을 실행하세요:

tuned-adm recommend

현재 활성화된 프로파일을 보려면 다음 명령을 사용하세요:

tuned-adm active
Current active profile: virtual-guest

기본값은 PostgreSQL 성능을 저하시킬 수 있습니다. 예를 들어, 전력 절약 모드는 CPU를 느리게 하고 네트워크 및 I/O 튜닝에서도 비슷한 문제가 발생할 수 있습니다. 이를 해결하기 위해 PostgreSQL 성능에 최적화된 새로운 프로파일을 만들어야 합니다.

새로운 프로파일을 만드는 과정은 비교적 간단합니다. 이 프로파일을 edbpostgres라고 명명하겠습니다. 다음 명령을 루트 사용자로 실행하세요:

#   This directory name will also be the
#  name of the profile:
mkdir /etc/tuned/edbpostgres
#  Create the profile file:
echo "
[main]
summary=Tuned profile for EDB PostgreSQL Instances
[bootloader]
cmdline=transparent_hugepage=never
[cpu]
governor=performance
energy_perf_bias=performance
min_perf_pct=100
[sysctl]
vm.swappiness = 10
vm.dirty_expire_centisecs = 500
vm.dirty_writeback_centisecs = 250
vm.dirty_ratio = 10
vm.dirty_background_ratio = 3
vm.overcommit_memory=0
net.ipv4.tcp_timestamps=0

[vm]
transparent_hugepages=never
" > /etc/tuned/edbpostgres/tuned.conf

[ ]로 둘러싸인 줄은 tuned 플러그인이라고 하며, 시스템의 특정 부분과 상호작용하는 데 사용됩니다.

매개변수와 값 설명

  • [main]: 프로파일 요약 정보를 포함하며, 다른 tuned 프로파일의 값을 포함할 수도 있습니다.
  • [cpu]: CPU 거버너와 전력 설정과 관련된 값을 포함합니다.
  • [sysctl]: procfs와 상호작용하는 값을 포함합니다.
  • [vm][bootloader]: 투명한 거대 페이지(Transparent Huge Pages)를 활성화하거나 비활성화합니다. [bootloader] 플러그인은 GRUB 명령줄 매개변수를 변경하는 데 사용됩니다.
이 설정으로 달성하고자 하는 목표

  • CPU가 전력 절약 모드에 들어가지 않아 PostgreSQL이 성능 저하를 겪지 않습니다.
  • Linux가 스와핑을 거의 하지 않도록 설정합니다.
  • 커널이 PostgreSQL의 더티 페이지를 플러시하도록 도와 bgwriter와 checkpointer의 부담을 줄입니다.
  • pdflush 데몬이 더 자주 실행되도록 설정합니다.
  • TCP 타임스탬프를 비활성화하여 타임스탬프 생성으로 인한 지연을 줄입니다.
  • 투명한 거대 페이지를 비활성화하여 PostgreSQL 성능을 크게 개선합니다.

아래 명령을 실행하여 새 프로파일을 활성화합니다:

tuned-adm profile edbpostgres

이 명령은 완료하는 데 약간 시간이 걸릴 수 있습니다.

투명한 거대 페이지를 완전히 비활성화하려면 아래 명령을 실행하세요:

grub2-mkconfig -o /boot/grub2/grub.cfg

이후 시스템을 재부팅합니다:

systemctl start reboot.target

파일 시스템 최적화

PostgreSQL은 데이터 파일에 대해 atime(파일이 마지막으로 액세스된 시간)을 사용하지 않으므로, 이를 비활성화하면 CPU 작업을 줄일 수 있습니다.

  1. /etc/fstab 파일을 열어 PostgreSQL 데이터와 WAL 파일이 저장된 드라이브에 대해 defaults 값 옆에 noatime을 추가하세요:
    • /dev/mapper/pgdata-01-data /pgdata xfs defaults,noatime 1 1
  2. 변경 사항을 즉시 적용하려면 다음 명령을 실행하세요:
    • mount -o remount,noatime,nodiratime /pgdata

이 설정은 초기 최적화 단계로 적합하며, 운영 체제와 PostgreSQL을 지속적으로 모니터링하여 추가적인 데이터를 수집하고 세부적인 튜닝을 진행하는 것이 중요합니다.

Huge Pages

기본적으로 Linux의 페이지 크기는 4KB입니다. 일반적인 PostgreSQL 인스턴스는 수십 GB의 메모리를 할당할 수 있으며, 작은 페이지 크기로 인해 성능 문제가 발생할 가능성이 있습니다. 또한, 이러한 페이지가 조각화되면 대규모 데이터 세트를 매핑하는 데 추가 시간이 소요될 수 있습니다.

Linux에서 Huge Pages를 활성화하면 PostgreSQL 성능이 향상됩니다. Huge Pages는 큰 블록으로 메모리를 한 번에 할당하기 때문입니다.

기본적으로 Linux에서는 Huge Pages가 비활성화되어 있으며, PostgreSQL의 기본 huge_pages 설정은 try로 되어 있습니다. 이는 운영 체제에서 Huge Pages가 사용 가능하면 이를 사용하고, 그렇지 않으면 사용하지 않는다는 의미입니다.

Huge Pages 설정 방법

Huge Pages를 설정하려면 운영 체제와 PostgreSQL 모두를 구성해야 합니다.

1. PostgreSQL에 필요한 Huge Pages 수 확인

PostgreSQL 인스턴스를 시작하면 postmaster$PGDATApostmaster.pid 파일을 생성합니다. 여기에서 메인 프로세스의 PID를 확인합니다:

$ head -n 1 $PGDATA/postmaster.pid
1991

다음으로, 이 인스턴스의 VmPeak 값을 확인합니다:

$ grep -i vmpeak /proc/1991/status
VmPeak: 8823028 kB

: 동일 서버에서 여러 PostgreSQL 인스턴스를 실행 중인 경우, 다음 단계에서 모든 VmPeak 값을 합산해야 합니다.

2. Huge Page 크기 확인




$ grep -i hugepagesize /proc/meminfo
Hugepagesize: 2048 kB

3. 필요한 Huge Pages 수 계산

8823028 / 2048 = 4308.12

이 값보다 약간 더 높은 수치를 설정하는 것이 이상적입니다. 그러나 값을 지나치게 높게 설정하면 운영 체제에서 작은 페이지와 공간을 필요로 하는 프로세스가 시작되지 못할 수 있습니다. 이는 운영 체제 부팅 실패나 동일 서버의 다른 PostgreSQL 인스턴스 시작 실패로 이어질 수 있습니다.

이제 위에서 생성한 tuned.conf 파일을 열고 [sysctl] 섹션에 다음 줄을 추가하세요:

vm.nr_hugepages=4500

그런 다음, 새로운 설정을 활성화하려면 아래 명령을 실행하세요:

tuned-adm profile edbpostgres

이제 postgresql.conf 파일에서 다음 설정을 추가하고 PostgreSQL을 (재)시작할 수 있습니다:

huge_pages=on

또한, tuned 서비스가 PostgreSQL 서비스보다 먼저 시작되도록 설정하고, 재부팅 후에도 이 순서가 유지되도록 보장해야 합니다. 이를 위해 PostgreSQL 서비스의 유닛 파일을 편집하세요:

systemctl edit postgresql-13.service

그리고 다음 두 줄을 추가하세요:

[Unit]
After=tuned.service

변경 사항을 적용하려면 아래 명령을 실행하세요:

systemctl daemon-reload

PostgreSQL 성능 튜닝 시작점


다음의 구성 옵션은 PostgreSQL에서 기본값을 변경해야 하는 항목입니다. 다른 값들도 성능에 큰 영향을 줄 수 있지만, 기본값이 이미 최적화되어 있기 때문에 여기서는 논의하지 않습니다.

구성 및 인증

max_connections

max_connections의 최적 값은 CPU 코어 수의 약 4배입니다. 이 공식은 최소한의 숫자를 제공하지만, 오류 발생 여지가 많지는 않습니다. 추천 값은 다음 공식에 따릅니다:
GREATEST(4 x CPU 코어 수, 100)

이 값을 초과하면 pgbouncer와 같은 연결 풀러를 사용하는 것이 좋습니다.

max_connections 값을 너무 높게 설정하면 Postgres의 다양한 데이터 구조 크기가 커져 CPU 사이클이 낭비됩니다. 반대로, 필요한 워크로드를 지원할 충분한 리소스가 할당되었는지도 확인해야 합니다.

리소스 사용

shared_buffers

이 매개변수는 가장 다양한 경우에 따라 다릅니다. 일부 워크로드는 대규모 데이터베이스에서도 최소 값(1GB 또는 2GB)으로 잘 작동합니다. 다른 워크로드는 더 큰 값을 요구합니다.
LEAST(RAM/2, 10GB)는 합리적인 시작점입니다.

이 공식은 PostgreSQL 커뮤니티의 오랜 경험에 기반을 둔 것이며, 정확한 이유를 설명하기는 어렵지만 일반적으로 좋은 결과를 제공합니다.

work_mem

work_mem의 권장 시작 값은 다음 공식으로 계산됩니다:
((총 RAM - shared_buffers) / (16 x CPU 코어 수))

이 공식은 메모리 고갈 위험이 있는 여러 쿼리를 실행하는 경우 시스템이 이미 CPU 용량에 의해 제한될 것이라는 가정을 기반으로 합니다. 이 공식은 일반적인 경우에 상대적으로 큰 메모리 한도를 제공합니다.

값을 높게 설정하고 싶을 수 있지만, 이는 피해야 합니다. work_mem에 지정된 메모리 양은 단일 쿼리 계획 내의 각 노드에서 사용될 수 있기 때문입니다. 예를 들어, 중첩된 해시 조인 문자열에서는 단일 쿼리가 여러 work_mem을 사용할 수 있습니다.

maintenance_work_mem

이 매개변수는 VACUUM, CREATE INDEX, ALTER TABLE ADD FOREIGN KEY, 데이터 로드 작업과 같은 유지 관리 작업에 사용되는 최대 메모리 양을 결정합니다. 이러한 작업은 데이터베이스 서버의 I/O를 증가시킬 수 있지만, 더 많은 메모리를 할당하면 작업이 더 빨리 완료될 수 있습니다. 1GB는 좋은 시작 값입니다.

autovacuum_work_mem

maintenance_work_mem 값을 높게 설정하면 autovacuum 작업자가 해당 메모리를 사용할 수 있습니다. vacuum 작업자는 청소하려는 각 죽은 튜플에 대해 6바이트의 메모리를 사용합니다. 8MB의 값은 약 140만 개의 죽은 튜플을 처리할 수 있도록 합니다.

effective_io_concurrency

이 매개변수는 특정 작업 동안 선읽기(read-ahead)에 사용됩니다. 데이터가 저장된 디스크 수로 설정하는 것이 좋습니다. 원래는 RAID 배열에서 디스크가 회전하는 동안 발생하는 병렬 읽기를 고려해 설계되었습니다. SSD의 경우, 이 값을 200으로 설정하면 성능이 개선됩니다.

Write-Ahead Log (WAL)

wal_compression

이 매개변수를 활성화하면 full_page_writes 또는 기본 백업 중 PostgreSQL 서버가 WAL에 기록되는 전체 페이지 이미지를 압축합니다. 대부분의 데이터베이스 서버는 CPU보다는 I/O 병목이 발생하기 때문에, 이 값을 on으로 설정하세요.

wal_log_hints

pg_rewind를 사용하려면 이 매개변수가 필요합니다. on으로 설정하세요.

wal_buffers

이 매개변수는 백엔드가 메모리에서 WAL 데이터를 쓰는 데 사용할 수 있는 공간을 제어합니다. 기본적으로 WAL 세그먼트는 16MB이므로, 이를 버퍼링하는 것은 메모리 측면에서 비용이 적습니다. 64MB로 설정하면 성능 테스트에서 긍정적인 효과를 볼 수 있습니다.

checkpoint_timeout

긴 타임아웃은 WAL 볼륨을 줄이지만, 충돌 복구가 느려질 수 있습니다. 최소 15분 이상을 추천하며, 비즈니스 요구사항의 RPO에 따라 설정하세요.

checkpoint_completion_target

PostgreSQL이 체크포인트를 완료하려고 목표하는 시간을 결정합니다. 체크포인트가 I/O 급증을 유발하지 않도록 설정하며, 체크포인트 시간을 분산하려고 시도합니다. 권장 값은 0.9입니다.

max_wal_size

WAL 재활용을 위해 특정 값에 도달하면 체크포인트가 트리거되도록 설정해야 합니다. 권장 값은 WAL이 저장된 디스크 공간의 절반에서 3분의 2입니다.

archive_mode

이 매개변수를 변경하려면 재시작이 필요합니다. WAL 아카이빙을 절대 사용하지 않을 계획이 아니라면 on으로 설정하세요.

archive_command

archive_mode가 활성화되어 있다면 유효한 archive_command가 필요합니다. 설정 준비가 완료될 때까지 기본 값으로 : to be configured를 사용할 수 있습니다.

:는 POSIX 시스템(Windows 포함)에서 성공을 반환하며, Postgres에 WAL 세그먼트를 재활용하거나 삭제할 수 있음을 알립니다.

PostgreSQL 쿼리 최적화

random_page_cost

이 매개변수는 PostgreSQL 옵티마이저에 디스크에서 랜덤 페이지를 읽는 비용에 대한 힌트를 제공하여 인덱스 스캔과 순차 스캔 중 어느 것을 사용할지 결정하도록 합니다.

  • SSD 디스크를 사용하는 경우, 권장 값은 1.1입니다.
  • 회전 디스크에서는 기본값이 적합한 경우가 많습니다.

이 매개변수는 전역적으로 설정하거나 테이블스페이스별로 설정해야 합니다. 예를 들어, 테이프 드라이브에 저장된 히스토리컬 데이터를 포함하는 테이블스페이스가 있다면 이 값을 매우 높게 설정해 랜덤 액세스를 억제하고 순차 스캔과 필터링이 더 빠르게 작동하도록 할 수 있습니다.

effective_cache_size

이 값은 총 RAM 용량의 0.75배 또는 free 명령 출력에서 buff/cache, free RAM, shared_buffers의 합 중 더 작은 값으로 설정해야 합니다. 이는 PostgreSQL에 사용 가능한 총 캐시 공간에 대한 힌트를 제공하기 위한 값이며, 여기서의 캐시는 CPU 캐시가 아닌 메인 메모리의 캐시를 의미합니다.

이 예시에서는 effective_cache_size 값이 (64304 * 0.75, 58113 + 5808 + 8192) 중 더 작은 값으로 설정됩니다. 여기서 shared_buffers가 8GB(8192MB)라고 가정하면, 결과 값은 48228MB가 됩니다.

cpu_tuple_cost

쿼리 처리 중 각 행을 처리하는 상대 비용을 지정합니다. 기본값은 0.01이지만, 이는 최적의 값보다 낮은 경향이 있습니다. 경험적으로 0.03으로 설정하는 것이 더 현실적인 비용으로 간주됩니다.

보고 및 로깅

logging_collector

log_destinationstderr 또는 csvlog가 포함되어 있으면 이 매개변수를 on으로 설정해 로그 출력을 파일로 수집합니다.

log_directory

logging_collector가 활성화되어 있다면, 로그 디렉토리를 데이터 디렉토리 외부로 설정하세요. 이렇게 하면 로그가 기본 백업의 일부로 포함되지 않습니다.

log_checkpoints

미래의 진단을 위해 이 매개변수를 on으로 설정합니다. 특히 체크포인트가 checkpoint_timeout에 의해 발생했는지 max_wal_size에 의해 발생했는지 확인할 수 있습니다.

log_line_prefix

로그 파일의 각 줄 앞에 붙는 접두사 형식을 정의합니다. 진단을 돕기 위해 시간, 프로세스 ID, 줄 번호, 사용자 및 데이터베이스, 애플리케이션 이름을 포함해야 합니다.

추천 값:

'%m [%p-%l] %u@%d app=%a '

접미사 끝의 공백을 잊지 마세요!

log_lock_waits

이 매개변수를 on으로 설정하면 느린 쿼리 진단에 필수적입니다.

log_statement

ddl로 설정하세요. 기본 감사 로그를 제공할 뿐만 아니라, 잘못된 테이블 삭제와 같은 심각한 사용자 오류가 발생한 시점을 확인할 수 있습니다.

log_temp_files

0으로 설정하세요. 이는 생성된 모든 임시 파일을 기록하며, work_mem이 잘못 조정되었음을 시사할 수 있습니다.

timed_statistics (EPAS)

Dynamic Runtime Instrumentation Tools Architecture(DRITA) 기능을 위한 타이밍 데이터 수집을 제어합니다. on으로 설정하세요.

Autovacuum

log_autovacuum_min_duration

autovacuum 활동을 모니터링하면 이를 튜닝하는 데 도움이 됩니다. 권장 값은 0으로, 모든 autovacuum 활동을 기록합니다.

autovacuum_max_workers

autovacuum 작업자의 수를 설정합니다. 기본값은 3이며, 업데이트하려면 데이터베이스 서버를 재시작해야 합니다. 각 테이블은 하나의 작업자만 사용할 수 있으므로, 작업자 수를 늘리면 테이블 간 병렬 작업 및 더 빈번한 vacuum에 유리합니다. 기본값이 낮으므로 5로 시작하는 것이 좋습니다.

autovacuum_vacuum_cost_limit

autovacuum으로 인해 데이터베이스 서버에 과도한 부하가 걸리지 않도록 Postgres는 I/O 할당량을 설정합니다. 기본값이 낮으므로 이 값을 3000으로 늘리는 것을 권장합니다.

클라이언트 연결 기본값

idle_in_transaction_session_timeout

트랜잭션에서 유휴 상태인 세션은 잠금을 유지하며 vacuum을 방해할 수 있습니다. 이 타이머는 트랜잭션에서 너무 오래 유휴 상태로 유지된 세션을 종료합니다. 애플리케이션이 이를 복구할 준비가 되어 있어야 합니다.

권장 값: 10분

lc_messages

로그 분석기는 번역되지 않은 메시지만 이해할 수 있습니다. 이 매개변수를 C로 설정해 번역을 방지하세요.

shared_preload_libraries

pg_stat_statements를 추가하면 오버헤드는 낮으면서도 높은 가치를 제공합니다. 이는 권장되지만 선택 사항입니다.

워크로드 분석에 기반한 PostgreSQL 성능 튜닝

느린 쿼리 찾기

느린 쿼리를 찾는 방법에는 크게 두 가지가 있습니다:

  • log_min_duration_statement 매개변수, 그리고
  • pg_stat_statements 모듈 및 확장.


log_min_duration_statement 매개변수는 쿼리가 로그 파일에 기록되기 전에 얼마나 오래 실행되어야 하는지를 밀리초 단위로 설정합니다. 모든 쿼리를 기록하려면 이 값을 0으로 설정할 수 있지만, 이렇게 하면 상당한 I/O가 발생할 수 있으니 주의해야 합니다.

일반적으로 이 값을 1초로 설정한 뒤, 아래 설명된 방식으로 쿼리를 최적화합니다. 이후 점진적으로 임계값을 낮추어 적절한 값에 도달할 때까지 과정을 반복한 후, 지속적인 최적화를 위해 해당 값을 유지합니다. 적절한 임계값은 워크로드에 따라 달라질 수 있습니다.

이 방법은 느린 쿼리를 찾는 데 효과적이지만, 완벽한 해결책은 아닙니다. 예를 들어, 실행 시간이 1분이고 10분마다 한 번 실행되는 쿼리와, 실행 시간이 20ms이지만 초당 20번 실행되는 쿼리가 있다고 가정해 봅시다.

  • 첫 번째 쿼리는 10분 동안 서버의 1분을 소비합니다.
  • 두 번째 쿼리는 10분 동안 서버의 4분을 소비합니다.

따라서 두 번째 쿼리를 최적화하는 것이 더 중요하지만, 이 쿼리는 log_min_duration_statement 설정으로는 쉽게 감지되지 않을 가능성이 높습니다.

이제 pg_stat_statements 모듈을 살펴보겠습니다. 이 모듈의 단점은 shared_preload_libraries에 설정해야 하며, 이를 위해 서버를 다시 시작해야 한다는 점입니다. 하지만 이 모듈은 오버헤드가 매우 낮고, 얻을 수 있는 이점이 크기 때문에 항상 프로덕션 환경에 설치할 것을 권장합니다.

이 모듈은 서버에서 실행된 모든 완료된 쿼리를 기록하고, 상수를 매개변수로 대체하는 등 여러 방식으로 정규화합니다. 또한, 동일한 쿼리를 하나의 데이터 요소로 집계하여 다음과 같은 유용한 통계를 제공합니다:

  • 총 실행 시간
  • 호출 횟수
  • 최대 및 최소 실행 시간
  • 반환된 총 행 수
  • 생성된 임시 파일의 총 크기 등

파싱 후 정규화된 내부 구조가 동일하다면 두 쿼리는 동일한 쿼리로 간주됩니다. 예를 들어:

  • SELECT * FROM t WHERE pk = 42;
  • SeLeCt * FrOm T wHeRe Pk=56;

이 두 쿼리는 pk 값이 다르더라도 동일한 쿼리로 간주됩니다.

pg_stat_statements 모듈에서 수집된 통계를 확인하려면, 먼저 다음 명령어로 확장을 설치하고 pg_stat_statements 뷰를 생성해야 합니다:


보안 관련

이 모듈은 실행된 사용자/데이터베이스 조합에 관계없이 모든 쿼리에 대한 통계를 수집합니다. 필요하면 이 확장을 특정 데이터베이스나 여러 데이터베이스에 설치할 수 있습니다.

  • 기본적으로, 모든 사용자는 이 뷰에서 자신의 쿼리만 볼 수 있습니다(pg_stat_activity 뷰와 동일).
  • 슈퍼유저 및 pg_read_all_stats 또는 pg_monitor 역할이 부여된 사용자는 전체 내용을 확인할 수 있습니다.

EDB의 PostgreSQL Enterprise Manager(PEM)는 이 데이터를 깔끔하게 시각화하는 SQL 프로파일러를 제공합니다.

쿼리 재작성

때로는 쿼리의 일부를 재작성하는 것만으로도 성능을 크게 향상시킬 수 있습니다.

“나체” 컬럼(Naked Columns)

자주 발생하는 실수 중 하나는 아래와 같은 방식으로 쿼리를 작성하는 것입니다:

SELECT * FROM t
WHERE t.a_timestamp + interval ‘3 days’ < CURRENT_TIMESTAMP

이 대신 아래처럼 작성해야 합니다:

SELECT * FROM t
WHERE t.a_timestamp < CURRENT_TIMESTAMP - interval ‘3 days’

이 두 쿼리의 결과는 동일하며, 의미적으로는 차이가 없습니다. 하지만 두 번째 쿼리는 t.a_timestamp에 인덱스를 사용할 수 있는 반면, 첫 번째 쿼리는 사용할 수 없습니다.
테이블 컬럼은 항상 왼쪽에 “나체” 상태로 두고, 모든 연산식은 오른쪽에 작성하세요.

서브쿼리와 함께 NOT IN 사용 금지

IN 술어에는 두 가지 형태가 있습니다:

  • x IN (a, b, c)
  • x IN (SELECT …)

양의 경우에는 두 가지 형태 중 어떤 것을 사용해도 괜찮습니다. 그러나 **음의 경우(NOT IN)**에는 첫 번째 형태를 사용하세요. 이는 NULL 처리 방식 때문입니다.

예를 들어:

demo=# select 1 in (1, 2); 
?column?
---------- 
t 
(1 row) 
demo=# select 1 in (1, null);
?column? 
---------- 
t 
(1 row) 
demo=# select 1 in (2, null);
?column? 
---------- 
(null) 
(1 row)

이 예시는 NULL 값이 포함된 경우, IN 술어는 참(True) 또는 NULL만 반환하며, 거짓(False)을 반환하지 않는다는 점을 보여줍니다. 따라서 NOT IN은 거짓(False) 또는 NULL만 반환하며, 절대 참(True)을 반환하지 않습니다.

상수 목록을 제공하는 경우 NULL 값이 있는지 쉽게 파악할 수 있고, 쿼리가 원하는 결과를 제공하지 않을 것임을 알 수 있습니다. 하지만 서브쿼리 형태를 사용하면 이를 쉽게 확인하기 어렵습니다.

더 중요한 점은, 서브쿼리 결과에 NULL이 없다고 보장되더라도 Postgres는 이를 **안티 조인(Anti Join)**으로 최적화하지 않습니다. 따라서 NOT EXISTS를 사용하는 것이 좋습니다.

쿼리 성능 최적화를 위한 EXPLAIN (ANALYZE, BUFFERS) 사용

Using EXPLAIN (ANALYZE, BUFFERS)

쿼리가 종료되지 않거나 실행 시간이 너무 길어 인내심을 잃고 포기하게 되는 경우, 간단한 EXPLAIN 플랜을 분석하거나 전문가에게 도움을 요청하는 것이 좋습니다. 하지만 쿼리가 정상적으로 종료되는 경우라면, 항상 ANALYZE 옵션을 사용해 쿼리를 최적화해야 합니다.

잘못된 추정 (Bad Estimates)

성능 저하의 가장 흔한 원인은 잘못된 추정치입니다. 테이블 통계가 최신 상태가 아니면 PostgreSQL은 실제로 200개의 행이 반환될 경우에도 2개의 행만 반환될 것으로 예측할 수 있습니다.

단순 스캔의 경우, 이는 큰 문제가 되지 않습니다. 실행 시간이 예상보다 약간 길어질 뿐입니다. 하지만 진짜 문제는 나비 효과입니다.

PostgreSQL이 스캔 결과 2개의 행만 반환될 것으로 판단하면, 조인 시 **중첩 루프(Nested Loop)**를 선택할 수 있습니다. 하지만 실제로 200개의 행이 반환되면 쿼리가 매우 느려집니다.
만약 PostgreSQL이 반환될 행 수를 정확히 알고 있었다면, 해시 조인(Hash Join) 또는 **병합 조인(Merge Join)**을 선택했을 것입니다.

이 문제는 ANALYZE 명령을 사용해 통계를 업데이트함으로써 해결할 수 있습니다.

또한, Planner가 알지 못하는 강하게 상관된 데이터가 있다면, 이를 해결하기 위해 CREATE STATISTICS를 사용할 수 있습니다.

외부 정렬 (External Sorts)

정렬 작업에 사용할 work_mem이 충분하지 않으면 PostgreSQL이 디스크에 데이터를 기록하게 됩니다. RAM은 디스크(SSD 포함)보다 훨씬 빠르기 때문에, 이는 쿼리 속도를 저하시킬 수 있습니다. 이러한 문제가 발생한다면 work_mem 값을 늘리는 것을 고려해 보세요.

demo=# create table t (c bigint);
CREATE TABLE
demo=# insert into t select generate_series(1, 1000000); 
INSERT 0 1000000 demo=# explain (analyze on, costs off) table t order by c;
                         QUERY PLAN
---------------------------------------------------------------------- 
Sort (actual time=158.066..245.686 rows=1000000 loops=1) 
 Sort Key: c 
 Sort Method: external merge Disk: 17696kB 
 -> Seq Scan on t (actual
time=0.011..51.972 rows=1000000 loops=1)
Planning Time: 0.041 ms
Execution Time: 273.973 ms
(6 rows)

demo=# set work_mem to '100MB';
SET
demo=# explain (analyze on, costs off) table t order by c;
                         QUERY PLAN
----------------------------------------------------------------------
Sort (actual time=183.841..218.555 rows=1000000 loops=1)
 Sort Key: c
 Sort Method: quicksort Memory: 71452kB
 -> Seq Scan on t (actual time=0.011..56.573 rows=1000000 loops=1)
Planning Time: 0.043 ms
Execution Time: 243.031 ms
(6 rows)

이 예에서는 데이터셋이 작아 차이가 크게 드러나지 않습니다. 하지만 실제 환경의 쿼리에서는 훨씬 더 큰 성능 차이가 나타날 수 있습니다. 경우에 따라, 정렬 자체를 피하기 위해 인덱스를 추가하는 것이 최선일 수 있습니다.

비정상적으로 많은 임시 파일을 생성하는 쿼리를 방지하려면 temp_file_limit 매개변수를 설정하세요. 이 매개변수를 초과하는 임시 파일을 생성하는 쿼리는 자동으로 취소됩니다.

해시 배치(Hash Batches)

work_mem 값이 너무 낮게 설정되었음을 나타내는 또 다른 징후는 해싱 작업이 여러 배치로 나뉘어 수행되는 경우입니다.

다음 예제에서는 쿼리를 실행하기 전에 work_mem을 가능한 가장 낮은 값으로 설정하고 실행 계획을 확인합니다. 그런 다음, work_mem을 재설정하고 쿼리를 다시 실행해 실행 계획을 비교합니다.

demo=# create table t1 (c) as select generate_series(1, 1000000);
SELECT 1000000
demo=# create table t2 (c) as select generate_series(1, 1000000, 100);
SELECT 10000
demo=# vacuum analyze t1, t2;
VACUUM
demo=# set work_mem to '64kB';
SET
demo=# explain (analyze on, costs off, timing off)
demo-# select * from t1 join t2 using (c);
                         QUERY PLAN
------------------------------------------------------------------
Gather (actual rows=10000 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Hash Join (actual rows=3333 loops=3)
   Hash Cond: (t1.c = t2.c)
-> Parallel Seq Scan on t1 (actual rows=333333 loops=3)
-> Hash (actual rows=10000 loops=3)
   Buckets: 2048 Batches: 16 Memory Usage: 40kB
-> Seq Scan on t2 (actual rows=10000 loops=3)
Planning Time: 0.077 ms
Execution Time: 115.790 ms
(11 rows)

demo=# reset work_mem;
RESET
demo=# explain (analyze on, costs off, timing off)
demo-# select * from t1 join t2 using (c);
                         QUERY PLAN
------------------------------------------------------------------
Gather (actual rows=10000 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Hash Join (actual rows=3333 loops=3)
   Hash Cond: (t1.c = t2.c)
-> Parallel Seq Scan on t1 (actual rows=333333 loops=3)
-> Hash (actual rows=10000 loops=3)
   Buckets: 16384 Batches: 1 Memory Usage: 480kB
-> Seq Scan on t2 (actual rows=10000 loops=3)
Planning Time: 0.081 ms
Execution Time: 63.893 ms
(11 rows)

단일 배치로 작업을 수행함으로써 실행 시간이 절반으로 줄어들었습니다.

힙 페치(Heap Fetches)

쿼리를 실행하는 트랜잭션에서 행이 보이는지 여부는 테이블의 해당 행에 저장됩니다. **가시성 맵(Visibility Map)**은 페이지의 모든 행이 모든 트랜잭션에 대해 보이는지 여부를 나타내는 비트맵입니다.

따라서, 인덱스 스캔은 일치하는 행을 찾았을 때 해당 행이 보이는지 확인하기 위해 테이블(여기서는 힙이라고도 함)을 검사해야 합니다.

**인덱스 전용 스캔(Index-Only Scan)**은 가시성 맵을 사용하여 힙에서 행을 가져오지 않고도 작업을 수행할 수 있습니다. 하지만 가시성 맵이 최신 상태가 아니면, 인덱스 전용 스캔이 불필요한 I/O를 수행하게 되고, 최악의 경우 일반 인덱스 스캔으로 완전히 전환됩니다.

EXPLAIN 실행 계획은 가시성 맵이 최신 상태가 아니어서 테이블(힙)을 몇 번이나 조회해야 했는지를 보여줍니다.

demo=# create table t (c bigint)
demo-# with (autovacuum_enabled = false);
CREATE TABLE
demo=# insert into t select generate_series(1, 1000000);
INSERT 0 1000000
demo=# create index on t (c);
CREATE INDEX
demo=# analyze t;
ANALYZE
demo=# explain (analyze on, costs off, timing off, summary off)
demo-# select c from t where c <= 2000; 
                          QUERY PLAN 
---------------------------------------------------------------
Index Only Scan using t_c_idx on t (actual rows=2000 loops=1) 
Index Cond: (c <=2000) 
Heap Fetches: 2000
(3 rows)

이 값은 이상적으로는 0이 되어야 하지만, 이는 테이블에서의 작업량에 따라 달라집니다. 동일한 페이지를 지속적으로 수정하고 쿼리하는 경우, 해당 값이 증가하는 것을 볼 수 있습니다. 만약 그렇지 않다면, 가시성 맵을 업데이트해야 하며, 이를 위해 VACUUM을 실행해야 합니다. (이 데모에서 autovacuum을 비활성화한 이유가 바로 이것입니다.)

demo=# vacuum t; 
VACUUM demo=# explain
(analyze on, costs off, timing off, summary off) 
demo-# select c from t where c <=2000; 
                        QUERY PLAN
--------------------------------------------------------------- 
Index Only Scan using t_c_idx on t (actual rows=2000 loops=1) 
Index Cond: (c <=2000) 
Heap Fetches: 0 
(3 rows)

손실 비트맵 스캔(Lossy Bitmap Scans)

데이터가 여기저기 흩어져 있는 경우, PostgreSQL은 **비트맵 인덱스 스캔(Bitmap Index Scan)**을 수행합니다. 이 과정에서 일치하는 모든 행의 페이지와 페이지 내 오프셋에 대한 비트맵을 생성한 후, 테이블(힙)을 스캔하여 각 페이지에서 한 번의 페치로 모든 행을 가져옵니다.

그러나 이러한 작업은 **충분한 work_mem**이 제공되는 경우에만 가능합니다. work_mem이 부족하면 PostgreSQL은 오프셋 정보를 “잊어버리고” 해당 페이지에 적어도 하나의 일치하는 행이 있다는 정보만 기억합니다. 이 경우, 힙 스캔은 페이지의 모든 행을 확인하고, 일치하지 않는 행을 필터링해야 하므로 추가적인 작업이 발생하게 됩니다.

demo=# create table t (c1, c2) as
demo-# select n, n::text from generate_series(1, 1000000) as g (n) 
demo-# order by random(); 
SELECT 1000000 
demo=# create index on t (c1);
CREATE INDEX 
demo=# analyze t; 
ANALYZE 
demo=# explain (analyze on, costs off, timing off) 
demo-# select * from t
where c1 <=200000; 
                         QUERY PLAN 
------------------------------------------------------------------ 
Bitmap Heap Scan on t (actual rows=200000 loops=1) 
Recheck Cond: (c1 <=200000) 
Heap Blocks: exact=5406 
-> Bitmap Index Scan on t_c1_idx (actual rows=200000 loops=1)
   Index Cond: (c1 <= 200000) 
Planning Time: 0.065 ms 
Execution Time: 48.800 ms 
(7 rows)

demo=# set work_mem to '64kB' ; 
SET 
demo=# explain (analyze on, costs off, timing off) 
demo-# select * from t where c1 <=200000;
                         QUERY PLAN 
------------------------------------------------------------------ 
Bitmap Heap Scan on t (actual rows=200000 loops=1) 
Recheck Cond: (c1 <=200000) 
Rows Removed by Index Recheck: 687823 
Heap Blocks: exact=752 lossy=4654 
-> Bitmap Index Scan on t_c1_idx (actual rows=200000 loops=1)
   Index Cond: (c1 <= 200000) 
Planning Time: 0.138 ms 
Execution Time: 85.208 ms 
(8 rows)

잘못된 실행 계획 형태

이 문제는 감지하기 가장 어려운 문제 중 하나이며, 경험을 통해서만 파악할 수 있습니다. 이전에 설명했듯이, work_mem이 부족하면 해시 작업이 여러 배치로 나뉠 수 있습니다. 하지만 PostgreSQL이 해시 조인을 사용하는 것보다 중첩 루프(Nested Loop)를 사용하는 것이 더 저렴하다고 판단하면 어떻게 될까요?

이 경우, 이전 섹션에서 다룬 다른 문제들처럼 명확히 드러나는 부분이 없을 수 있습니다. 하지만 work_mem을 늘리면 다시 해시 조인을 선택하도록 유도할 수 있습니다.

쿼리가 특정한 실행 계획 형태를 가져야 하는 상황을 이해하고, 실제 실행 계획이 달라졌을 때 이를 감지하는 능력을 키우면 PostgreSQL 최적화 기회를 발견하는 데 큰 도움이 됩니다.

파티셔닝

파티셔닝을 사용하는 주요 이유는 유지 관리병렬 처리 두 가지입니다.

유지 관리

테이블 크기가 매우 커질수록 기본 autovacuum 설정에 따라 허용되는 죽은 행(dead rows)의 수도 증가합니다.

  • 예를 들어, 행이 10억 개인 테이블의 경우, 2억 개의 행이 업데이트되거나 삭제되기 전까지는 정리가 시작되지 않습니다.
  • 대부분의 워크로드에서는 이러한 상태가 되기까지 시간이 걸리며, 그 시점이 오면 – 또는 더 나쁘게는 랩어라운드(wraparound)가 발생하면 – autovacuum 작업자는 테이블 전체를 스캔하며 죽은 행 목록을 수집해야 합니다.

이 목록은 죽은 행당 6바이트를 사용하므로, 약 1.2GB의 RAM이 필요합니다.


이후, 테이블의 각 인덱스를 하나씩 스캔하고 해당 목록에서 항목을 찾아 제거하며, 마지막으로 테이블을 다시 스캔해 죽은 행 자체를 제거합니다.

  • **1.2GB의 autovacuum_work_mem**을 사용할 수 없거나 설정하지 않았다면, 이 작업은 배치(batch)로 반복됩니다.
  • 이 작업 중 쿼리가 autovacuum과 충돌하는 락을 요청하면, autovacuum은 작업을 중단하고 처음부터 다시 시작합니다.
  • 단, 랩어라운드를 방지하기 위한 autovacuum인 경우, 쿼리는 대기해야 합니다.

autovacuum은 가시성 맵(visibility map)을 사용해 마지막 vacuum 이후 변경되지 않은 테이블의 큰 영역을 건너뛸 수 있습니다.

PostgreSQL 9.6 이후에는 반랩어라운드(anti-wraparound) vacuum에도 이 기능이 적용되었습니다.


그러나 인덱스에는 이러한 최적화가 없습니다. 모든 인덱스는 매번 완전히 스캔됩니다.

또한, 테이블에 남은 공간은 이후 삽입/업데이트로 채워질 수 있지만, 인덱스의 경우 값이 정렬되어 있으므로 빈 공간을 재사용하기가 훨씬 더 어렵습니다.
vacuum 작업이 줄어들수록 인덱스 성능을 유지하기 위해 인덱스를 더 자주 재인덱싱(reindexing)해야 합니다.

  • PostgreSQL 11까지는 테이블 쓰기를 잠그고 재인덱싱해야 했지만, PostgreSQL 12부터는 동시 재인덱싱이 가능합니다.
파티셔닝의 효과

데이터를 더 작은 청크로 파티셔닝하면 각 파티션과 인덱스를 별도의 작업자가 처리할 수 있습니다. 각 작업자는 처리해야 할 작업량이 줄어들고 더 자주 작업을 수행합니다.

특정 상황에서는 vacuum을 없앨 수도 있음

테이블이 시간 시계열 데이터와 같이 “삽입 후 수정하지 않는(insert and forget)” 데이터라면 위의 문제는 크게 발생하지 않습니다.

  • 오래된 행이 고정(frozen)되면 autovacuum은 다시는 해당 행을 검사하지 않습니다(PostgreSQL 9.6 이후).

다만, 이 경우 데이터 보존 정책이 문제입니다.

  • 데이터를 10년 동안만 유지하고 이후 콜드 스토리지에 보관하거나 삭제하는 경우, 테이블에 남는 공간이 새 데이터로 채워지면서 테이블이 조각화될 수 있습니다.
  • 이는 BRIN 인덱스를 완전히 쓸모없게 만듭니다.
해결 방법

이를 해결하기 위해 월 단위(또는 원하는 세분화 단위)로 파티셔닝하는 것이 일반적입니다.

이후 프로세스는 다음과 같습니다:

  1. 오래된 파티션을 분리(detach).
  2. 해당 데이터를 아카이브로 덤프.
  3. 테이블 삭제.

이 방법으로 vacuum 작업 자체가 필요 없게 됩니다.

병렬 처리

랜덤 액세스가 많은 대규모 테이블(예: 멀티테넌트 구조)에서는 테이블을 테넌트별로 파티셔닝해 각각의 테넌트를 별도의 테이블스페이스에 배치하면 I/O 성능을 개선할 수 있습니다.

잘못된 이유

파티셔닝에 대한 흔히 잘못된 믿음 중 하나는 작은 테이블 여러 개가 하나의 큰 테이블보다 쿼리 성능이 좋다는 것입니다.


그러나 이는 성능을 저하시키는 경우가 더 많습니다.

OLTP 성능 최적화

이 가이드는 대부분의 OLTP 워크로드에 적합한 시작점을 제공합니다. PostgreSQL에서 특정 워크로드에 맞는 최고의 성능을 얻으려면, 이 설정과 기타 설정을 지속적으로 모니터링하고 조정하는 것이 필수적입니다.

본문: PostgreSQL Performance Tuning: Optimize Your Database Server

이메일: salesinquiry@enterprisedb.com