PostgreSQL에서 Index Only Scan은 heap을 읽지 않고 index만으로 결과를 반환하는 고성능 기법입니다.
그런데 shared_buffers 기준으로 실제로 얼마나 I/O가 발생하는지는 잘 알려져 있지 않습니다.

이 글에서는 다음 가정 하에 Index Only Scan이 정말 효율적으로 작동하는지,
그리고 shared buffer에서 몇 개의 logical read가 발생하는지 실험을 통해 확인해봅니다.



OS환경 : Rocky Linux 8.10 (64bit)
DB 환경 : PostgreSQL 17.4

 

 

 실험 가정

  • PostgreSQL 17 기준
  • B-Tree 인덱스 사용 (Root → Branch → Leaf 구조)
  • 리프 노드 하나에 5개 row 저장됨
  • visibility map 최신화 완료 (VACUUM 수행)
  • shared buffer에 모든 블록이 올라와 있음
  • VM block은 제외
  • %d% 검색조건으로 Index Only Scan 유도
  • planner 힌트: SET enable_seqscan = off;

✔ 실험 목표

SELECT c1 FROM t WHERE c1 LIKE '%d%';

 

✔ 실험 스크립트

 
-- 테이블 초기화
DROP TABLE IF EXISTS t;
CREATE TABLE t (c1 TEXT);

-- 인덱스 생성
CREATE INDEX idx_t_c1 ON t(c1);

-- 데이터 삽입 (한 leaf block에 들어가게 유도)
INSERT INTO t
SELECT 'ad' || i
FROM generate_series(1, 5) AS i;

-- 데이터 확인
select * from t;
 c1
-----
 ad1
 ad2
 ad3
 ad4
 ad5
(5 rows)

-- vacuum으로 VM 최신화
VACUUM t;

-- 실행계획 보려면 Sequential Scan방지
SET enable_seqscan = off;

-- Index Only Scan 실행
EXPLAIN (ANALYZE, BUFFERS)
SELECT c1 FROM t WHERE c1 LIKE '%d%';

 실행 결과 (예상&결과)

-- 예상
Index Only Scan using idx_t_c1 on t
Heap Fetches: 0
Buffers: shared hit=3



-- 결과
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Index Only Scan using idx_t_c1 on t  (cost=0.13..8.22 rows=5 width=32) (actual time=0.017..0.019 rows=5 loops=1)
   Filter: (c1 ~~ '%d%'::text)
   Heap Fetches: 0
   Buffers: shared hit=2
 Planning:
   Buffers: shared hit=57
 Planning Time: 0.348 ms
 Execution Time: 0.056 ms
(8 rows)

 

🤔 원인 분석

Root → Leaf 바로 연결되는 depth 2 (level 1) 구조

-- 원인 

SELECT * FROM bt_metap('idx_t_c1');
 magic  | version | root | level | fastroot | fastlevel | last_cleanup_num_delpages | last_cleanup_num_tuples | allequalimage
--------+---------+------+-------+----------+-----------+---------------------------+-------------------------+---------------
 340322 |       4 |    1 |     0 |        1 |         0 |                         0 |                      -1 | t
(1 row)

 

 

💡 해결 방법 (Branch도 보이게 하기)

-- 데이터 대량 삽입 (리프 페이지 증가 → depth 증가 유도)
INSERT INTO t
SELECT 'ad' || i
FROM generate_series(6 300000) AS i(i);

-- VACUUM으로 VM 최신화
VACUUM t;

-- depth 확인
SELECT * FROM bt_metap('idx_t_c1');
 magic  | version | root | level | fastroot | fastlevel | last_cleanup_num_delpages | last_cleanup_num_tuples | allequalimage
--------+---------+------+-------+----------+-----------+---------------------------+-------------------------+---------------
 340322 |       4 |  376 |     2 |      376 |         2 |                         0 |                      -1 | t
(1 row)


EXPLAIN (ANALYZE, BUFFERS)
SELECT c1 FROM t WHERE c1 LIKE '%d%';
                                                           QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using idx_t_c1 on t  (cost=0.42..11738.42 rows=299970 width=8) (actual time=0.017..35.364 rows=300000 loops=1)
   Filter: (c1 ~~ '%d%'::text)
   Heap Fetches: 0
   Buffers: shared hit=1401 read=212
 Planning:
   Buffers: shared hit=10
 Planning Time: 0.218 ms
 Execution Time: 45.477 ms
(8 rows)

 

✅ 분석 포인트 1: Heap Fetches = 0

✔️ Index Only Scan이 정상 작동 중
✔️ Visibility Map이 최신 상태
→ heap을 보지 않음
완전한 Index Only Scan

 

✅ 분석 포인트 2: Buffers

항목 개수 의미
shared hit 1401 이미 shared buffer에 올라와 있던 블록에서 읽음
shared read 212 디스크에서 읽은 후 shared buffer에 올림
합계 1613 총 logical read 횟수

💡 Index Only Scan을 하면서 index block들을 반복적으로 접근

 

✅ 분석 포인트 3: 왜 300,000 row인데 블록은 1613개만 읽었을까?

이게 PostgreSQL의 구조를 정확히 보여주는 부분이에요:

  • PostgreSQL은 row-by-row로 leaf block을 순차 탐색
  • 하지만!
    B-Tree 경로 상에서 동일한 block이 반복 접근되기 때문에
    → block 수(=relpages)는 적더라도
    shared hit은 row 수에 비례해 증가할 수 있음

여기선 leaf page 약 1200개 + branch/root 포함 1613개 정도 block을 순회

 

 

🎯 결론

실제 Index Only Scan에서 30만 개의 row를 탐색했음에도, 총 logical read는 약 1,600개에 불과했습니다. 이는 PostgreSQL이 leaf block을 한번에 sequential하게 읽는 구조가 아니라, 현재 위치를 유지하며 row-by-row로 순차 탐색하는 iterator 방식이기 때문입니다. 따라서 하나의 branch, root block이 수십만 row에 의해 반복적으로 hit되며, shared hit 수가 relpages보다 훨씬 많이 나올 수 있습니다.

 

 

🙌 댓글, 공감, 공유는 큰 힘이 됩니다! 😄

 

참조 : https://www.postgresql.org/docs/current/indexes-index-only-scans.html

 

 

 

Index Only Scan: shared hit가 relpages보다 많은 이유?
Index Only Scan방식은  leaf block을 sequential 하게 한번 읽는다?

Index Only Scan 시 shared hit이 row 수 × index depth만큼 커질 수 있다?



Oracle과 다른 PostgreSQL에서 Index Only Scan 방식에 대해서 알아보겠습니다. 



OS환경 : Rocky Linux 8.10 (64bit)
DB 환경 : PostgreSQL 17.4

 

 

🧪 실험 목적:

  1. Index Only Scan이 index depth에 따라 shared hit 수가 어떤 영향을 주는가?
  2. Index Only Scan 시에 numeric vs bigint 인덱스 성능 차이
  3. Index Only Scan 시 leaf만 sequential하게 읽는 지? 

🏗️ 실험 구성

1. 실험 테이블 생성

1,2,3번 실험목적을 달성하기 위해 테스트 테이블 하나 생성합니다.

DROP TABLE IF EXISTS test_index_io;
CREATE TABLE test_index_io (
    id serial PRIMARY KEY,
    col1 varchar(64),
    col2_numeric numeric(19,0),
    col2_bigint bigint
);

 

2. 100만건 데이터 삽입 (numeric과 bigint 동일한 값)

서로 비교하기 위해 numeric(19,0) 기준으로 bigint도 동일하게 값을 넣었습니다.

INSERT INTO test_index_io (col1, col2_numeric, col2_bigint)
SELECT
    'value_' || i,
    9223372036854775807 - i,  -- numeric 최대치 근처
    9223372036854775807 - i   -- bigint 최대치 근처
FROM generate_series(1, 1000000) AS i;

 

※ 값 확인하기 

3. 인덱스 생성 

-- 복합 인덱스: col1 + col2_numeric
CREATE INDEX idx_numeric ON test_index_io (col1, col2_numeric);

-- 복합 인덱스: col1 + col2_bigint
CREATE INDEX idx_bigint ON test_index_io (col1, col2_bigint);

 

4. 통계 정보 갱신

ANALYZE test_index_io;

 

 

5. 캐시 클리어(인덱스 캐시적재 후 클리어)  

-- extension 설치
CREATE EXTENSION IF NOT EXISTS pg_prewarm;

-- 특정 테이블 or 인덱스를 미리 캐시
SELECT pg_prewarm('idx_numeric');
SELECT pg_prewarm('idx_bigint');

-- shared_buffers 통계 초기화 (슈퍼유저용)
SELECT pg_stat_reset();

 

6. 테스트 쿼리 실행 – Index Only Scan 유도

-- BitmapScan 방지, Index Only Scan 유도
SET enable_seqscan TO off;

-- 1. numeric 인덱스 사용
EXPLAIN (ANALYZE, BUFFERS)
SELECT col1 FROM test_index_io WHERE col2_numeric BETWEEN 100 AND 200000;

-- 2. bigint 인덱스 사용
EXPLAIN (ANALYZE, BUFFERS)
SELECT col1 FROM test_index_io WHERE col2_bigint BETWEEN 100 AND 200000;

 

1. idx_numeric Explain

2. idx_bigint Explain2. idx_bigint Explain

Buffers: shared hit = ???" 비교해보면

bigint 쪽 shared hit이 적게 나오고, numeric 쪽은 훨씬 많게 나온걸 알 수 있습니다. 

 

7. B-Tree Depth 확인 쿼리 (pageinspect 필요)

CREATE EXTENSION IF NOT EXISTS pageinspect;

-- B-Tree level 확인: numeric 인덱스
SELECT * FROM bt_metap('idx_numeric');

-- B-Tree level 확인: bigint 인덱스
SELECT * FROM bt_metap('idx_bigint');

B-Tree Level은 동일하게 나옴

 

 

8. leaf page 수 비교

--pg_class.relpages 값을 이용해 정확한 블록 범위 계산
SELECT relname, relpages
FROM pg_class
WHERE relname IN ('idx_numeric', 'idx_bigint');
   relname   | relpages
-------------+----------
 idx_numeric |     6052
 idx_bigint  |     4954
(2 rows)

-- numeric 인덱스의 leaf page 수
SELECT count(*) AS leaf_pages_numeric
FROM generate_series(1, 6051) AS g(page)
JOIN LATERAL bt_page_stats('idx_numeric', g.page) AS stats
  ON stats.type = 'l';

relpages
 leaf_pages_numeric
--------------------
               6020
(1 row)


-- bigint 인덱스의 leaf page 수
SELECT count(*) AS leaf_pages_bigint
FROM generate_series(1, 4953) AS g(page)
JOIN LATERAL bt_page_stats('idx_bigint', g.page) AS stats
  ON stats.type = 'l';
    
 leaf_pages_bigint
-------------------
              4927
(1 row)

 

 

🔬 최종 실험 결과 요약

항목 idx_bigint idx_bigint
relpages 6052 4954
Leaf Page 수 6020 4927
bt_metap.level 2 2
Shared Hit (쿼리 기준) 6022 4929
Execution Time 150ms 57ms

 

🧠 분석 요점

✅ 1. B-Tree depth는 같지만 폭이 다르다

  • 둘 다 level = 2지만, numeric 인덱스는 leaf page 수가 1,093개 더 많음
  • 이는 인덱스 튜플이 variable-length (numeric) 이라서 페이지 밀도가 떨어지고, 더 많은 leaf page를 필요로 하기 때문

✅ 2. shared hit = 거의 leaf page 수와 비례

  • Index Only Scan에서 leaf traversal 시 page 단위 접근이 1:1로 거의 매핑됨
  • → numeric: 6020 leaf page → shared hit = 6022
  • → bigint: 4927 leaf page → shared hit = 4929

✅ 3. Execution Time 차이도 leaf page 수와 비례

  • 6020 → 150ms
  • 4927 → 57ms
  • 👉 단순 연산뿐 아니라, shared buffer traversal 횟수, CPU에서의 numeric 연산 처리 부하까지 모두 반영된 시간

 

✅ 실험 목적 vs 실험 결과 정밀 체크


실험 목적 달성 여부 증명된 근거
1. index depth에 따라 shared hit 수가 어떻게 증가하는가 🟡 부분 달성 bt_metap()로는 depth가 같았으나, 실제 shared hit 수는 leaf 수에 비례해 차이남 → “depth 차이” 자체는 이번 실험에선 없었음
2. numeric vs bigint 인덱스 차이 완전 달성 leaf page 수, shared hit, execution time 전부 명확하게 차이 났고, tuple size 차이도 확인 가능
3. Index Only Scan이 leaf만 sequential하게 읽는 게 아니라 경로 매번 탐색
(Index Scan도 동일하게 각 Row 마다 Index 탐색함)
완전 달성 shared hit 수 ≈ leaf page 수 → leaf를 한 번에 쭉 읽는 구조가 아닌 page-by-page traversal 구조로 증명됨

 

 

부분 달성 된 depth 차이를 증명하기 위한 추가 보완 실험 진행 

 

 

1. 실험 테이블 생성

DROP TABLE IF EXISTS test_index_depth;
CREATE TABLE test_index_depth (
    id serial PRIMARY KEY,
    col1 text,                      -- 길고 무거운 문자열
    col2_numeric numeric(38,0)      -- 최대 precision, variable-length
);

 

2.  데이터 800만건 INSERT

-- col1: 아주 긴 문자열, col2_numeric: 큰 수
INSERT INTO test_index_depth (col1, col2_numeric)
SELECT
    repeat('A', 500),                     -- 500 byte string
    9999999999999999999999999999 - i     -- 최대 길이 numeric
FROM generate_series(1, 3000000) AS i;


-- level 3이 되지않아 한차례 더 INSERT
INSERT INTO test_index_depth (col1, col2_numeric)
SELECT
    repeat('A', 500),                     -- 500 byte string
    9999999999999999999999999999 - i     -- 최대 길이 numeric
FROM generate_series(1, 5000000) AS i;

 

3. 인덱스 생성 & 통계정보수집

CREATE INDEX idx_depth_numeric ON test_index_depth (col2_numeric);
ANALYZE test_index_depth;

 

4. B-Tree level 확인

Level 3확인

SELECT * FROM bt_metap('idx_depth_numeric');
 magic  | version | root  | level | fastroot | fastlevel | last_cleanup_num_delpages | last_cleanup_num_tuples | allequalimage
--------+---------+-------+-------+----------+-----------+---------------------------+-------------------------+---------------
 340322 |       4 | 29283 |     3 |    29283 |         3 |                         0 |                      -1 | f
(1 row)

 

5. 캐시 클리어 & Index Only Scan 실행 & shared hit 측정

-- shared_buffers 초기화 (가능 시)
SELECT pg_stat_reset();
SELECT pg_prewarm('idx_depth_numeric');


-- Index Only Scan 유도
SET enable_seqscan = off;
SET enable_indexscan = off;
SET enable_seqscan = off;


-- VACUUM으로 visibility map Setting
-- Index Only Scan은 visibility map이 설정된 경우에만 가능하므로 꼭 필요함.
VACUUM test_index_depth;

-- 데이터 조회: 어떤 값이 들어가 있는지 확인
SELECT MIN(col2_numeric), MAX(col2_numeric)
FROM test_index_depth;
             min              |             max
------------------------------+------------------------------
 9999999999999999999994999999 | 9999999999999999999999999998
(1 row)


--EXPLAIN 수행
EXPLAIN (ANALYZE, BUFFERS)
SELECT col2_numeric
FROM test_index_depth
WHERE col2_numeric BETWEEN 9999999999999999999994999999 AND 9999999999999999999999999998;

 

 

📦 Visibility Map이 뭐냐?

PostgreSQL은 테이블의 각 블록이 VACUUM 이후 변경되지 않았는지를 추적하기 위해
visibility map이라는 메타데이터 파일을 사용합니다.

상태 의미
all-visible 이 블록의 모든 row가 visible (확인할 필요 없음)
not visible 최신성 보장 못함 → heap 블록 접근 필요

 

6. pg_class relpages 조회 

SELECT relpages
FROM pg_class
WHERE relname='test_index_depth';
 relpages
----------
   571429
(1 row)

 

항목 설명
relpages 인덱스에 존재하는 distinct 블록 수 (정적)
shared hit 쿼리 수행 중 shared buffer에 접근한 총 횟수 (동적)
shared hit > relpages 되는 이유 row 마다 B-Tree 경로 탐색 → 중복된 block에도 반복 접근 (여러 row에서 같은 block을 탐색)

  PostgreSQL의 shared hit은 "Block count"가 아니다.

 

7. row당 average block 접근 수

-- shared hit / 8,000,000 row
SELECT 6062041.0 / 8000000.0;  -- 약 0.76

 

 

✅ 실험 목표: 달성 요약

목적 항목 달성 여부  증명된 근거
 Index depth가 커지면 shared hit 수가 증가하는가? ✅ 완전 달성 shared hit 6,062,041 📈 (vs 이전 실험 수천 단위)

 

🔍 PostgreSQL vs Oracle 비교표

항목 PostgreSQL Oracle
MVCC 방식 Heap + visibility map Undo 로그 기반
인덱스만으로 쿼리 가능? ✅ 단, VM 필요 ✅ 무조건 가능 (조건만 맞으면)
heap 접근 판단 방식 visibility map 기반 안 함 (ROWID 기반)
Index-only scan fallback? 있음 (heap 접근) 없음
VACUUM 필요 여부 ✅ 필요 ❌ 없음

 

✅ 정리

이번 실험은 PostgreSQL의 Index Only Scan이 단순한 sequential leaf scan이 아님을 명확히 보여줍니다.

단순히 리프 노드를 순서대로 읽는 게 아니라, MVCC 일관성 유지를 위해 visibility map 체크와 조건 평가를 포함한 동작을 합니다.

"각 row마다 index 경로를 다시 타고, shared buffer를 많이 hit한다"

라는 구조적 특성은 shared hit 수가 relpages보다 몇 배 이상 많을 수 있다는 현상을 설명하는 핵심입니다.

 

 

 

  PostgreSQL의 shared hit은 "Block count"가 아니다.

  PostgreSQL의 Index Only Scan은 Oracle처럼 leaf block들을 한 번에 sequential하게 읽어들이는 구조가 아니라,
row-by-row로 인덱스를 탐색하는 iterator 기반 구조입니다. 탐색 도중 현재 위치는 유지되지만, 각 row를 순차적으로 처리하면서 B-Tree의 내부 노드와 leaf block에 반복적으로 접근하게 되며, 이로 인해 shared hit 수는 relpages보다 훨씬 많아질 수 있습니다.

PostgreSQL의  numeric vs bigint 인덱스 성능 차이에서 bigint가 훨씬 뛰어남.

 



 

 

🙌 댓글, 공감, 공유는 큰 힘이 됩니다! 😄

  

 

 

참조

Index-Only Scans 참고문헌  

https://www.postgresql.org/docs/current/indexes-index-only-scans.html

https://www.postgresql.org/docs/17/btree.html

https://www.interdb.jp/pg/pgsql07/02.html


PostgreSQL 소스코드 내부에서 Index Only Scan 처리하는 핵심 모듈

https://github.com/postgres/postgres/blob/master/src/backend/executor/nodeIndexonlyscan.c

(DBA 입장에서 바라 본)
PostgreSQL 패스워드 관리와 테이블 권한부여 및 권한 분리 대한 정리



OS환경 : Rocky 8.10 (64bit)
DB 환경 : PostgreSQL 17.4

 

 

 

Oracle DB 처럼 개발자 계정, 서비스 계정 분리를 하기 위해 다음과 같이 방법을 고려해서 작성하였습니다.

해당 내용을 이해하기 위해서는 Schema, Role, User, search_path 개념을 먼저 익히면 좋을 것 같습니다.

 

 

예시) A 개발 업체의 특정 서비스 개발로 여러 개의 계정생성이 필요한 상황이며 불필요한 DDL권한은 DBA에게 요청하여  관리할 목적임

 

 

🎯 개발 프로젝트 용도 계정생성 

유저 이름 사번
대상 DB mydb
사용 시작일 2025-03-31
사용 기간(계정 만료일) 1년 (~ 2026-03-30)
권한 범위 테이블 DML만 (SELECT, INSERT, UPDATE, DELETE)
권한 적용 범위 스키마 dev
권한 부여 방식 그룹 ROLE을 통해 간접 위임

 

😀 1. 사용자 생성

##영어 대문자로 계정을 만들어도, 소문자로 생성됨
CREATE ROLE A12345678 WITH LOGIN PASSWORD'1q2w3e4r!'VALID UNTIL '2026-03-31 23:59:59';

 

😢 2. 현재 계정 만료일 확인

SELECT usename, valuntil FROM pg_user WHERE usename = 'a12345678'

 

⭐오라클처럼 패스워드를 변경한다고 계정 만료일이 자동으로 연장되지 않음.

 

 

👍 계정 자동 연장 스크립트 (30일 이내 만료 계정, 자동 1년 연장)  

DO $$
DECLARE
    r record;
BEGIN
  FOR r IN
    SELECT usename FROM pg_user
    WHERE valuntil < now() + interval '30 days'
      AND valuntil IS NOT NULL
  LOOP
    EXECUTE format('ALTER ROLE %I VALID UNTIL %L', r.usename, (now() + interval '1 year')::text);
  END LOOP;
END$$;

 

 

 

 

🧷 1. DML 전용 ROLE 생성 

CREATE ROLE mydb_dml_role;
 

 

🛡️ 2. DML 전용 ROLE에 권한부여 

-- 현재 있는 테이블들에 대해
GRANT SELECT, INSERT, UPDATE, DELETE
  ON ALL TABLES IN SCHEMA dev
  TO mydb_dml_role;

-- Sequences부여
GRANT USAGE ON ALL SEQUENCES IN SCHEMA dev TO mydb_dml_role;

-- 앞으로 생성되는 테이블에도 자동으로 권한 부여
-- 새로운 테이블 생성 시 해당 테이블 접근 불가
ALTER DEFAULT PRIVILEGES IN SCHEMA dev
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO mydb_dml_role;

-- 자동으로 시퀀스 권한 부여
ALTER DEFAULT PRIVILEGES IN SCHEMA dev grant usage on sequences TO mydb_dml_role;

  
-- 계정 DB 접속 권한 조회
SELECT datname,
       has_database_privilege('a12345678', datname, 'connect') AS can_connect
FROM pg_database;

 

 

※ 만약 can_connect가 t로 되어 있다면 public schema revoke 진행, template0는 원래 접속이 불가능 하여 revoke 할 필요가 없음.  ( datallowconn = false ) 

-- 초기 DB 설치 시 Public schema는 revoke처리 
REVOKE CONNECT ON DATABASE mydb FROM PUBLIC;
REVOKE CONNECT ON DATABASE postgres FROM PUBLIC;
REVOKE CONNECT ON DATABASE template1 FROM PUBLIC;
REVOKE CONNECT ON DATABASE khg FROM PUBLIC;

 

 

🔗 3. 개발자 계정에  DML ROLE 권한 부여 

GRANT mydb_dml_role TO a12345678;
 

 

📦 4. 개발자 계정 접속 권한 부여 및 스키마 권한 부여

GRANT CONNECT ON DATABASE mydb TO a12345678;
CREATE SCHEMA DEV;
--해당 권한이 없으면 테이블 접근 불가
GRANT USAGE ON SCHEMA DEV TO mydb_dml_role;

--SEARCH PATH 설정
ALTER DATABASE mydb SET SERACH_PATH TO dev, public, "$user";

 

Serach Path 확인

 

 

 

dev schema 테이블 리스트 조회 

 

 

 

PUBLIC 스키마 테이블 조회 시도 시  불가능 확인. 목록 리스트는 확인 가능함  

 

 

 

 

 

TIP :

  1. VALID UNTIL은 패스워드 만료일이지만, 사실상 계정 만료일과 다름 없다.
    (사용자가 패스워드 변경해도 유효기간은 변함없음)  
  2. 테이블 접근 권한 부여를 하지만, 새로운 테이블 생성 시  ALTER DEFAULT PRIVILEGES IN 권한을 부여해줘야
    여러 번  작업을 안 할 수 있음.

 

 

 

내용적으로 추가로 궁금한 부분이나 새로운 아이디어 질문해주시면 답변 드리겠습니다.  

 

 

참조 : 없음 

 

 

1️⃣ Client & Backend Process 관련 파라미터

구성 요소 설명 관련파라미터 
Client 사용자 애플리케이션이 DB에 접속 listen_addresses, port, max_connections
Postmaster
(Daemon Process)
DB 프로세스를 관리하는 메인 프로세스 max_connections, superuser_reserved_connections
Backend Process 각 클라이언트 연결을 처리하는 개별 프로세스 work_mem, maintenance_work_mem
listen_addresses = '*'
port = 5432
max_connections = 100
superuser_reserved_connections = 3
  • listen_addresses: PostgreSQL이 수신할 IP 주소
  • port: PostgreSQL이 사용할 포트
  • max_connections: 동시에 연결 가능한 최대 클라이언트 수
  • superuser_reserved_connections: 슈퍼유저를 위한 예약된 연결 수

 

2️⃣ Shared Memory 관련 파라미터

구성 요소 설명 관련 파라미터
Shared Buffers 데이터 페이지를 캐싱하는 메모리 영역 shared_buffers
WAL Buffers 트랜잭션 로그(WAL) 저장을 위한 버퍼 wal_buffers
CLOG Buffers 트랜잭션 상태(Commit/Abort) 관리 버퍼 commit_delay, commit_siblings
Temp Buffers 임시 테이블 및 정렬 공간 temp_buffers
Other Buffers 기타 내부적인 버퍼 work_mem, maintenance_work_mem
shared_buffers = 4GB
wal_buffers = 16MB
temp_buffers = 8MB
work_mem = 64MB
maintenance_work_mem = 512MB
commit_delay = 100
commit_siblings = 5
  • shared_buffers: PostgreSQL이 사용하는 공유 메모리 버퍼 크기
  • wal_buffers: WAL(Log) 데이터를 저장하는 버퍼 크기
  • temp_buffers: 임시 테이블을 위한 버퍼 크기
  • work_mem: 정렬/해시 조인 등 개별 작업을 위한 메모리 크기
  • maintenance_work_mem: VACUUM, CREATE INDEX 등의 작업을 위한 메모리
  • commit_delay, commit_siblings: 트랜잭션 커밋 지연을 조절하는 옵션

 

3️⃣ Utility Processes 관련 파라미터

구성 요소 설명 관련 파라미터 
BG Writer 공유 버퍼를 디스크에 기록 bgwriter_delay, bgwriter_lru_maxpages, bgwriter_lru_multiplier
Checkpointer Checkpoint 생성 및 WAL 정리 checkpoint_timeout, checkpoint_completion_target
WAL Writer WAL 버퍼를 WAL 파일로 저장 wal_writer_delay, wal_writer_flush_after
Archiver WAL을 보관(Archiving) archive_mode, archive_command
Logger 로그 파일 관리 logging_collector, log_directory, log_statement
Stats Collector DB 통계를 수집 track_activities, track_counts, stats_temp_directory
Autovacuum Launcher 자동 VACUUM 실행 autovacuum, autovacuum_vacuum_threshold, autovacuum_analyze_threshold
bgwriter_delay = 200ms
bgwriter_lru_maxpages = 100
bgwriter_lru_multiplier = 2.0

checkpoint_timeout = 5min
checkpoint_completion_target = 0.7

wal_writer_delay = 200ms
wal_writer_flush_after = 1MB

archive_mode = on
archive_command = 'cp %p /postgres_archive/%f'

logging_collector = on
log_directory = 'pg_log'
log_statement = 'all'

track_activities = on
track_counts = on
stats_temp_directory = '/postgres_data/pg_stat_tmp'

autovacuum = on
autovacuum_vacuum_threshold = 50
autovacuum_analyze_threshold = 50
  • bgwriter_*: 공유 버퍼 페이지를 디스크로 미리 기록하는 백그라운드 프로세스 설정
  • checkpoint_*: 체크포인트 발생 주기 조절
  • wal_writer_*: WAL 데이터를 디스크로 저장하는 빈도 조절
  • archive_mode, archive_command: WAL 파일을 아카이브하는 설정
  • logging_*: 로그 수집 설정
  • track_*: 통계 수집 활성화
  • autovacuum_*: 자동 VACUUM 활성화 및 튜닝

4️⃣ Physical Files 관련 파라미터

구성 요소 설명 관련 파라미터 
Data Files 실제 테이블 및 인덱스가 저장되는 파일 data_directory
WAL Files 트랜잭션 로그가 저장되는 파일 wal_level, wal_keep_size, max_wal_size, min_wal_size
Log Files PostgreSQL 운영 로그 파일 log_directory, log_filename
Archive Files 백업 및 복구용 WAL 아카이브 파일 archive_mode, archive_command, archive_timeo
  • data_directory: PostgreSQL 데이터 파일이 저장되는 경로
  • wal_level: WAL의 기록 수준 (minimal, replica, logical)
  • wal_keep_size: PostgreSQL이 보관할 WAL 파일 크기
  • max_wal_size, min_wal_size: WAL 파일 최대/최소 크기
  • log_directory, log_filename: 로그 파일 저장 위치 및 파일명 패턴
  • archive_mode, archive_command: WAL 파일 아카이빙 활성화

 

 

 

 

참조 : 1. https://blog.ex-em.com/1645

           2. ChatGPT

 

DB 인사이드 | PostgreSQL Architecture - 1. Process, Memory

엑셈은 창사 이래 꾸준히 축적해온 IT 시스템 성능 관리 경험과 연구 역량을 토대로 전문적이고 차별화된 컨설팅 서비스를 제공하고 있습니다. DBMS 전문가 엑셈에서 새롭게 선보이는 ‘DB인사이

blog.ex-em.com

 

 

 

 

 

 

 

Virtualbox에서 Rocky Linux 설치하기

2개의 프로그램은 전부 무료입니다.

 

 

Rocky Linux 다운로드 링크

https://rockylinux.org/ko-KR/download

 

 

 

VirtualBox 다운로드 링크

https://www.virtualbox.org/

 

 

 

아래 OS에 맞게 설치하는데, 대부분 Windows 환경으로 생각되어 Windows로 다운받고 설치하시면 됩니다.

 

 

 

최초 설치시에 아래 환경과 동일하게 출력됩니다.

 

 

 

Ctrl + G 단축키를 이용하거나, File - Preferences 들어가서

 

 

Language에서 한국어로 변경합니다.

 

 

 

변경 후에 새로 만들기로 들어갑니다.

 

 

아까 다운받은 Rocky8을 ISO 파일로 불러오기 하고, Virtualbox 파일들을 저장할 폴더의 경로를 지정합니다.

※ 용량이 넉넉한 디스크 경로로 지정해주셔야 됩니다.

 

 

무인 설치는 넘어가고 하드웨어 항목에서 기본 메모리와 프로세스를 설정합니다.

본인 PC 사양에 맞춰서 작업관리자에서 성능 탭 메모리와 CPU를 확인합니다.

작업관리자는 Ctrl+Alt+Delete(del) 키를 눌러 활성화하면 가능합니다.

 

 

 

아래와 같이 나온다면 더블클릭하면 위 사진과 동일하게 나옵니다.

 

 

 

저는 여유 메모리가 많기 때문에 10240MB(10GB)로 설정합니다. 1GB당 1024MB 계산하면 됩니다.

최소 메모리는 4GB로 설정해주셔야 됩니다.

프로세스는 1개로 하겠습니다.

 

 

본인 PC의 볼륨을 생각하고 저는 E드라이브로 정하였습니다.

본인이 설치할 S/W에 따라서 지정하면 좋습니다

나중에 추가도 가능하기 때문에(귀찮지만...) 100GB로 설정하고 완료 버튼을 눌러줍니다.

 

 

추가로 가상머신 환경에서는 마우스가 안나거나 하는 경우가 발생합니다.

파일-환경설정 들어가서 아래와 같이 동일하게 설정하고 Ctrl+Alt 를 누르면 마우스가 탈출할 수 있게 변경합니다.

 

 

F12를 눌러 다음으로 이동합니다.

 

 

여기서 Hard disk 항목인 숫자 1 눌러줍니다. 

 

 

아까 다운로드 받은 Rocky8 ISO 파일을 지정하고 마운트 후 부트 재시도를 합니다.

 

 

 

방향키를 ↑ 눌러서 Install Rocky Linux 8.10 으로 이동하고 엔터합니다.

 

 

 

 

 

 

기다리다보면 다음과 같은 화면이 출력됩니다. Continue 합니다.

 

 

 

 

Keyboard로 들어갑니다.

 

 

+ 버튼을 눌러서 KOREAN 추가합니다.

 

 

 

 

 

 

Language Support도 마찬가지로 KOREA 추가합니다.

 

 

 

체크하는 것 잊지 마시고 체크하고 DONE 눌러줍니다.

 

 

 

 

Time & Date로 들어갑니다.

 

 

 

지도에 핀을 찍어도 되고, Asia Seoul로 해도 됩니다. 평양만... 피해서 조심해서 핀 찍어주세요😂

 

 

 

여기서 실수로 Installaction Source 들어가신분은...

아래 새로고침 누르고 Done으로 빠져나옵니다. 여기서 수정할 내용은 없습니다.

 

 

 

Software Selection 으로 들어갑니다.

 

 

 

Minimal Install로 설정하고 Done 눌러줍니다.

 

 

 

 

Installation Destination 눌러줍니다.

 

 

 

 

아래 볼륨을 클릭해서 ✅ 모양이 되고 난 이후 Custom으로 누르고 나서 Done 눌러줍니다.

 

 

 

+ 버튼을 눌러줍니다.

 

 

 

 

다음과 같은 순서로 추가합니다.
Swap = 최초 설정했던 메모리 용량과 동일하게 10240MB

/boot 1024MB

/ 나머지

 

마지막 / 루트는 아무것도 입력하지 않고 Add mount point 해줍니다.

 

 

 

다음과 같이 설정이 완료가 되었으면 Done 눌러줍니다.

 

 

아래와 같이 Accept Changes 클릭합니다.

 

 

KDUMP 들어갑니다.

 

 

 

체크 해제 후 Done

 

 

 

 

 

Network & Host Name 설정을 위해 눌러줍니다.

 

 

 

 

 

아래와 같이 빨간박스 확인하고 Done 눌러줍니다.

Hostname은 꼭 Apply 확인후 우측에 Current host name 을 꼭 확인해줍니다.

 

 

 

Security Policy 들어갑니다.

 

 

 

 

OFF로 꺼주시고 Done 눌러줍니다.

 

 

 

 

Root Password 눌러줍니다.

 

 

 

 

Root Password 입력 후 Done 눌러줍니다.

 

 

 

 

Begin Installation 까지 하면 설치는 끝납니다.

 

 

 

 

설치 기다리고 Reboot System 버튼이 활성화되면 클릭해줍니다.

 

 

Reboot 눌러줍니다.

 

 

여기서 그냥 3초 기다리시면 됩니다.

 

 

 

login : root

pw : (입력 내용이 보이지 않습니다. 그대로 입력 후 엔터)

 

 

 

 

df -h 눌러서 아까 설정한 볼륨 용량 확인합니다.

 

 

서버 종료는 init 0 하시면 종료됩니다.

 

여기까지 설치는 마무리 되었습니다. 감사합니다😀

+ Recent posts