※ PostgreSQL: work_mem.
안녕하세요. 듀스트림입니다.
오늘은 PostgreSQL 성능 관점에서 가장 중요한 메모리 파라미터 중 하나인 work_mem에 대해 자세히 알아보겠습니다.
동작 방식과 적용 방식에 대해 오해하시는 분들이 많은 거 같아서 작성해 봤습니다.
1. work_mem이란?
work_mem은 PostgreSQL에서 정렬(Sort), 해시 테이블 빌드(Hash Join, Hash Aggregate), 정렬 기반 연산(WindowAgg, SetOp) 등과 같은 Plan 노드 단위의 작업에 사용되는 세션 로컬 임시 메모리의 상한값입니다.
각 노드마다 독립적으로 할당되며, 초과 시 PostgreSQL은 자동으로 디스크 임시 파일로 스필(spill) 처리하지만, 일부 표현식 평가(ExprContext) 등은 spill을 지원하지 않아 OOM이 발생할 수 있습니다.
# postgresql.conf
work_mem = 4MB # 기본값
# postgresql.conf 파일에서 work_mem 값을 변경한 후, pg_ctl reload 명령을 실행하면 설정 변경 사항이 반영됩니다.
# 이 설정은 세션 단위로 적용되기 때문에 기존 연결 세션에는 적용되지 않으며, 이후 새롭게 연결되는 세션부터 적용됩니다.
- 할당 단위: Plan 노드 1개당 1번씩
- 적용 단위: 세션 (SET 명령으로 적용 가능)
- 초과 시 동작: Sort, Hash는 Disk Spill → external sort, batch hash join 등으로 전환
2. Plan 노드 단위로 할당된다는 의미
공식 문서에 따르면, work_mem은 쿼리 전체에 대해 하나만 할당되는 것이 아니라, 실행 계획의 각 Plan 노드(Sort, Hash Join, Aggregate 등)마다 독립적인 별도 메모리로 할당됩니다.
Summary.
“This value limits the amount of memory used by internal sort operations and hash tables before writing to temporary disk files. Note that for a complex query, several sort or hash operations might be running in parallel; each one will be allowed to use as much memory as this value before it starts to write data into temporary files.”
3. 실제 메모리 사용량 계산식
이 계산식은 Plan 노드가 동시에 실행될 경우의 최대 메모리 사용량을 기준으로 합니다.
실제 peak는 이보다 작을 수 있으며, 복잡한 쿼리에서는 이 수치를 넘어서 OOM이 발생할 수도 있습니다.
최대 총 사용량 ≒ work_mem × 병렬 프로세스 수 × 메모리 사용 Plan 노드 수
# 예제
work_mem = 64MB
병렬 worker 수 = 2 (Leader 포함 총 3개 프로세스)
Plan 노드 수 = Sort, Aggregate, Hash Join = 3개
→ 64MB × 3 × 3 = 576MB
| 구성 요소 | 설명 |
| work_mem | Plan 노드 1개 기준의 최대 메모리 |
| 병렬 프로세스 수 | leader + 병렬 worker 수 |
| Plan 노드 수 | Sort, Aggregate, Hash Join 등 |
관련 소스 코드
1. Sort 노드
- 파일: nodeSort.c
- 핵심 함수: ExecInitSort()
estate->es_sort_mem = work_mem;
tuplesort_begin_heap(...) // es_sort_mem 값 전달
- 정렬 알고리즘에서 메모리 제한은 work_mem을 기반으로 tuplesort_begin_heap() 함수에 전달되어 적용됩니다.
2. Hash Join
- 파일: nodeHash.c
- 핵심 함수: ExecHashTableCreate() → 상위 호출자는 ExecHashJoin()
hashtable->spaceAllowed = work_mem * 1024L;
- Hash Join은 조인 대상 테이블의 해시 테이블을 work_mem을 기반으로 메모리에 생성하며, 입력 row가 많거나 해시 키 분포가 고르지 않아 work_mem을 초과하게 되면, PostgreSQL은 batching (배치 분할 처리) 기법을 사용하여 디스크에 일부 데이터를 spill하고 여러 번에 나누어 조인을 수행합니다.
3. Hash Aggregate
- 파일: execGrouping.c
- 핵심 함수: build_hash_table()
- HashAggregate는 내부적으로 build_hash_table()에서 spaceAllowed = work_mem * 1024L을 설정하여 해시 테이블을 메모리에 생성합니다. 초과 시 배치 분할 처리로 전환됩니다. (Hash Join과 비슷하게 처리됩니다.)
4. 메모리 컨텍스트 할당 실패
- 파일: aset.c
- 함수: AllocSetAlloc()
- PostgreSQL의 모든 메모리 요청은 내부적으로 AllocSetAlloc()을 통해 처리되며, 요청 크기가 너무 크거나 spill 불가능한 컨텍스트(예: ExprContext)에서는 OOM이 발생합니다.
정리
| 항목 | 소스 파일 | 함수 | 설명 |
| Sort | nodeSort.c | ExecInitSort() | work_mem 기반 메모리 정렬 설정 |
| HashJoin | nodeHash.c | ExecHashJoin() 내부 | 해시 빌드 시 work_mem 사용 |
| HashAggregate | execGrouping.c | build_hash_table() | work_mem만큼 해시 메모리 제한 |
| 메모리 실패 | aset.c | AllocSetAlloc() | ExprContext 등 spill 불가 컨텍스트에서 OOM 발생 |
4. OOM이 발생하는 이유
PostgreSQL의 work_mem은 Sort, HashAggregate, HashJoin 등 일부 Plan 노드에서는 spill-to-disk 기능이 제공되지만, 모든 Plan 노드가 spill을 지원하는 것은 아닙니다.
특히 ExprContext, FunctionScan, 사용자 정의 함수(UDF) 또는 복잡한 표현식 평가 중에는 메모리를 직접 할당하며, 이 메모리는 spill되지 않습니다.
이 경우 메모리 사용량이 시스템 제한을 초과하면 PostgreSQL 프로세스는 OOM (out of memory) 에러를 발생시킵니다.
ERROR: out of memory
DETAIL: Failed on request of size 782400677 in memory context "ExprContext"
관련 소스 코드
1. aset.c – PostgreSQL의 기본 메모리 할당기
- 파일: aset.c
- 함수: AllocSetAlloc()
- 역할:
if (chunk == NULL)
elog(ERROR, "out of memory");
- PostgreSQL의 모든 메모리 요청(palloc, MemoryContextAlloc)은 이 함수에서 처리됩니다.
- 할당이 실패하면 여기서 "out of memory" 에러가 발생합니다.
2. execExpr.c – ExprContext가 사용되는 대표 위치
- 파일: execExpr.c
- 핵심 구조: ExprContext
- 사용 예: ExecInterpExpr()
- 설명:
- SQL에서 CASE, WHERE, json_agg(...), fn(...) 등 복잡한 표현식들은 실행 시 ExprContext에 메모리를 할당합니다.
- 이 컨텍스트는 ExprContext->ecxt_per_tuple_memory에 연결되며, 정렬처럼 spill이 불가능합니다.
5. 예제
테스트 환경
CPU: 4core
총 메모리: 8GB
work_mem = 20MB
Case 1: work_mem 내 처리
테스트 테이블 생성
DROP TABLE IF EXISTS test_table;
CREATE TABLE test_table (
id serial PRIMARY KEY,
category int,
description text
);
테스트 데이터 삽입
-- 1억 건 삽입 (중복 카테고리 포함)
INSERT INTO test_table (category, description)
SELECT
(random() * 100)::int,
repeat('Sample Text ', (random() * 10)::int)
FROM generate_series(1, 100000000);
수행 SQL
EXPLAIN (ANALYZE, BUFFERS)
SELECT category, COUNT(*)
FROM test_table
GROUP BY category
ORDER BY COUNT(*) DESC;
실행계획
--- QUERY PLAN ---
Sort (cost=1920124.47..1920124.73 rows=101 width=12) (actual time=66918.403..66961.120 rows=101 loops=1)
Sort Key: (count(*)) DESC
Sort Method: quicksort Memory: 28kB
Buffers: shared hit=14 read=1253284 dirtied=1253284 written=100841
-> Finalize GroupAggregate (cost=1920095.52..1920121.11 rows=101 width=12) (actual time=66918.280..66961.065 rows=101 loops=1)
Group Key: category
Buffers: shared hit=14 read=1253284 dirtied=1253284 written=100841
-> Gather Merge (cost=1920095.52..1920119.09 rows=202 width=12) (actual time=66918.260..66961.013 rows=303 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=14 read=1253284 dirtied=1253284 written=100841
-> Sort (cost=1919095.50..1919095.75 rows=101 width=12) (actual time=66908.551..66908.556 rows=101 loops=3)
Sort Key: category
Sort Method: quicksort Memory: 28kB
Buffers: shared hit=14 read=1253284 dirtied=1253284 written=100841
Worker 0: Sort Method: quicksort Memory: 28kB
Worker 1: Sort Method: quicksort Memory: 28kB
-> Partial HashAggregate (cost=1919091.13..1919092.14 rows=101 width=12) (actual time=66908.476..66908.486 rows=101 loops=3)
Group Key: category
Batches: 1 Memory Usage: 24kB
Buffers: shared read=1253284 dirtied=1253284 written=100841
Worker 0: Batches: 1 Memory Usage: 24kB
Worker 1: Batches: 1 Memory Usage: 24kB
-> Parallel Seq Scan on test_table (cost=0.00..1697155.42 rows=44387142 width=4) (actual time=0.664..62200.756 rows=33333333 loops=3)
Buffers: shared read=1253284 dirtied=1253284 written=100841
Planning:
Buffers: shared hit=1 read=1
Planning Time: 1.266 ms
Execution Time: 66961.226 ms
(29 rows)

실행계획 분석
| 항목 | 값 |
| 처리된 행 수 | 약 1억 건 (33,333,333 rows × 3 workers) |
| 실행 시간 | 약 66.9초 |
| 병렬 처리 | 사용 (Leader + 2 Workers) |
| 디스크 I/O | 매우 큼 (read: 1,253,284 blocks, write: 100,841 blocks) |
| 스필 여부 | Sort Method: quicksort, 메모리 내 처리 → 스필 없음 |
| 메모리 사용량 | Sort: 28kB, HashAggregate: 24kB → 여유 있음 |
1. Parallel Seq Scan
- 각 워커가 병렬로 테이블 전체 스캔
- 총 33,333,333 rows × 3 = 1억 rows 처리
- Buffers:
- read=1,253,284 blocks ≒ 약 10.0GB (1 block = 8KB)
- dirtied=1,253,284, written=100,841 → shared buffer write-back 존재
📌 I/O 부하 매우 큼, 전체 데이터를 full scan
2. Partial HashAggregate (병렬 집계)
- 각 워커에서 category 기준 partial 집계
- Memory Usage: 24kB, Batches: 1 → work_mem 내에서 hash table 처리 완료
- Spill 없이 한 번에 처리됨
3. Sort (Leader + 각 워커)
- Leader + 각 워커에서 정렬 (category, count(*))
- Sort Method: quicksort → 메모리 내 처리
- Memory: 28kB 사용 → work_mem 내 여유 있게 처리됨
4. Gather Merge + Finalize GroupAggregate
- Gather Merge는 워커 결과를 병합 정렬
- Finalize GroupAggregate는 Leader가 최종 집계 수행
📌 병렬 처리 구조가 잘 잡혀 있으나, 병렬 I/O 처리량이 너무 커서 실행 시간은 길어짐
work_mem 사용량 추정
| 요소 | 수량 | per-instance 예상 사용량 |
| Partial HashAggregate | 3 (각 worker) | 24kB |
| Sort (worker) | 3 | 28kB |
| Sort (Leader) | 1 | 28kB |
| Finalize Aggregate (Leader) | 1 | 작음 |
24kB × 3 (Hash) + 28kB × 4 (Sorts) ≒ 약 200kB
- 집계 대상은 많지만, 정렬 대상이 작아서 work_mem 설정값 대비 작은 메모리가 사용됨
Case 2: Disk Spill 유도
Truncate 후 테스트 데이터 삽입
TRUNCATE test_table;
-- 1억 건 삽입 (중복 카테고리 포함)
INSERT INTO test_table (category, description)
SELECT
(random() * 10000)::int,
repeat('Sample Text ', (random() * 100)::int)
FROM generate_series(1, 100000000);
수행 SQL
EXPLAIN (ANALYZE, BUFFERS)
WITH base AS (
SELECT
id,
category,
description,
row_number() OVER (PARTITION BY category ORDER BY length(description) DESC) AS rn
FROM test_table
WHERE description ILIKE '%sample%'
),
filtered AS (
SELECT *
FROM base
WHERE rn <= 1000
),
aggregated AS (
SELECT
category,
count(*) AS cnt,
array_agg(description ORDER BY id DESC) AS desc_arr,
json_agg(json_build_object(
'id', id,
'desc', description,
'len', length(description)
)) AS desc_json
FROM filtered
GROUP BY category
)
SELECT *
FROM aggregated
ORDER BY cnt DESC
LIMIT 50;
실행계획
--- QUERY PLAN ---
Limit (cost=44751051.24..44751051.36 rows=50 width=76) (actual time=900462.259..900815.590 rows=50 loops=1)
Buffers: shared hit=259225 read=7734973, temp read=18173848 written=21010388
-> Sort (cost=44751051.24..44751076.23 rows=9995 width=76) (actual time=900456.844..900810.138 rows=50 loops=1)
Sort Key: (count(*)) DESC
Sort Method: external merge Disk: 22811264kB
Buffers: shared hit=259225 read=7734973, temp read=18173848 written=21010388
-> GroupAggregate (cost=24402984.25..44750719.21 rows=9995 width=76) (actual time=248261.722..840631.845 rows=10001 loops=1)
Group Key: base.category
Buffers: shared hit=259225 read=7734973, temp read=15303252 written=15306486
-> Incremental Sort (cost=24402984.25..43474711.12 rows=85057211 width=610) (actual time=248252.242..780240.585 rows=10001000 loops=1)
Sort Key: base.category, base.id DESC
Presorted Key: base.category
Full-sort Groups: 10001 Sort Method: quicksort Average Memory: 102kB Peak Memory: 102kB
Pre-sorted Groups: 10001 Sort Method: quicksort Average Memory: 1151kB Peak Memory: 1152kB
Buffers: shared hit=259225 read=7734973, temp read=15303252 written=15306486
-> Subquery Scan on base (cost=24401182.33..36859212.43 rows=85057211 width=610) (actual time=248249.825..776738.334 rows=10001000 loops=1)
Buffers: shared hit=259225 read=7734973, temp read=15303252 written=15306486
-> WindowAgg (cost=24401182.33..36008640.32 rows=85057211 width=622) (actual time=248249.823..775774.739 rows=10001000 loops=1)
Run Condition: (row_number() OVER (?) <= 1000)
Buffers: shared hit=259225 read=7734973, temp read=15303252 written=15306486
-> Gather Merge (cost=24401182.20..34307496.10 rows=85057211 width=614) (actual time=248249.809..767864.359 rows=99499246 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=259225 read=7734973, temp read=15303252 written=15306486
-> Sort (cost=24400182.18..24488783.44 rows=35440505 width=614) (actual time=247924.084..425705.118 rows=33166415 loops=3)
Sort Key: test_table.category, (length(test_table.description)) DESC
Sort Method: external merge Disk: 20415248kB
Buffers: shared hit=259225 read=7734973, temp read=15303252 written=15306486
Worker 0: Sort Method: external merge Disk: 20415360kB
Worker 1: Sort Method: external merge Disk: 20382744kB
-> Parallel Seq Scan on test_table (cost=0.00..8604099.51 rows=35440505 width=614) (actual time=0.623..118444.806 rows=33166415 loops=3)
Filter: (description ~~* '%sample%'::text)
Rows Removed by Filter: 166918
Buffers: shared hit=259145 read=7734973
Planning:
Buffers: shared hit=43
Planning Time: 4.221 ms
Execution Time: 901432.269 ms
(38 rows)

실행계획 키워드
| 키워드 | 설명 |
| external merge | work_mem 초과 → 디스크 기반 정렬 |
| Disk: ...kB | 해당 Plan 노드의 임시 디스크 사용량 |
| WindowAgg | row_number() 등 순위 함수 처리 |
| Incremental Sort | 미리 정렬된 키를 기반으로 빠르게 정렬 |
| Gather Merge | 병렬로 정렬된 데이터를 Leader가 병합 |
실행계획 분석
| 항목 | 값 |
| 전체 소요 시간 | 약 15분 (901초) |
| 총 처리 row 수 | 약 1억 row (~100,010,000) |
| 병렬 처리 | Leader + 2 Workers → 3개 프로세스 |
| 정렬 방식 | external merge sort (디스크 스필 발생) |
| 디스크 스필량 | 임시 파일 총 약 22GB 이상 (22811264kB) |
| temp read/write | 1.8억 블록 read / 2.1억 블록 write |
| 주요 I/O 병목 지점 | 정렬(Sort), WindowAgg, Incremental Sort |
1. Parallel Seq Scan
Parallel Seq Scan on test_table ... rows=33,166,415 loops=3
- 병렬로 테이블을 읽음 (총 약 1억 row)
- description ILIKE '%sample%'는 인덱스를 타지 않으므로 Full Scan
- Buffers read = 7,735,000 blocks (약 59GB)
📌 I/O 병목 시작 지점으로 병목을 줄이려면, description에 GIN/GiST 인덱스 + pg_trgm 확장 고려 또는 사전 전처리된 description_tsvector 활용
2. Sort (WindowAgg 내부 정렬)
Sort Method: external merge Disk: 20GB+ per worker
Worker 0 / 1: external merge
- 이 정렬은 row_number() OVER (PARTITION BY category ORDER BY length(description) DESC) 실행을 위한 정렬
- 디스크 스필 발생: work_mem이 부족했기 때문
- 병렬 정렬 → 총 3개 정렬 노드 → 약 60GB 임시 디스크 사용
📌 row_number()의 정렬 기준으로 category, length(description)를 사용하므로 복잡한 sort key가 필요.
→ 정렬된 블록을 외부 정렬(merge)로 처리.
3. WindowAgg + Subquery Scan
WindowAgg → Subquery Scan base → Incremental Sort
- row_number() <= 1000 조건 때문에 매 그룹별 1000개만 추림
- 그 이전까지는 정렬된 전체 row가 메모리/디스크로 흐름
- Subquery Scan 이후에 필터가 적용되므로 불필요한 정렬까지 실행됨 ← 성능 저하 포인트
📌 개선 여지: row_number() <= N 조건을 CTE 필터링보다 더 안쪽으로 넣기 어려움.
📌 PostgreSQL의 WindowAgg는 sort → evaluate → filter 순서.
4. GroupAggregate + Final Sort
GroupAggregate ... rows=10001
Sort (LIMIT 50) ... external merge, Disk: 22GB
- 최종적으로 category별로 그룹화하여 집계
- 그 결과를 ORDER BY count(*) DESC로 정렬
- 이 정렬도 external merge 방식으로 디스크 스필 유발
📌 1만 개 그룹만 정렬하지만 입력 row가 너무 많음 (약 1억 row).
📌 각 그룹이 내부적으로 array_agg, json_agg 되었다면 더 큰 메모리 필요.
메모리 사용 및 work_mem 영향
| Plan 노드 | 설명 | work_mem 사용 | Disk Spill |
| Sort (WindowAgg용) | row_number()용 | ✅ | ✅ (20GB+ per worker) |
| Incremental Sort | group by category + id | ✅ | ✅ |
| Final Sort | count(*) DESC | ✅ | ✅ (22GB 이상) |
| HashAggregate or GroupAggregate | category별 집계 | ✅ | 대부분 spill 없이 처리됨 |
| json_agg / array_agg | 내부적으로 메모리 누적 | ✅ (ExprContext 기반) | ❌ (spill 불가, → OOM 위험 대상) |
work_mem 사용량 추정
| 요소 | 수량 | per-instance 예상 사용량 |
| Sort (WindowAgg 정렬) | 3 (각 worker) | 20GB 이상 디스크 스필 발생 |
| Incremental Sort (Leader) | 1 | 약 1MB (Peak Memory: 1152kB) |
| Final Sort (LIMIT) | 1 | 약 1MB (Disk: 22GB external sort 발생) |
| GroupAggregate | 1 | 수 MB 내외 (정렬 후 집계) |
| ExprContext (json_agg, array_agg) | 3+ (각 프로세스) | 수백 MB까지 커질 수 있음 (spill 불가) |
- 총 예상 work_mem Plan 노드 수: 5~6개
Incremental Sort + Final Sort + GroupAggregate + ExprContext + 병렬 워커 메모리 복제
| 구성 요소 | 사용량 추정 |
| 정렬 (5~6개 노드) | 20MB x 5~6 ≒ 120MB |
| ExprContext 누적 | 최대 수백 MB (spill 불가) |
| 병렬 워커 오버헤드 | 200~400MB 수준 |
| Executor/Plan 구조체 | 수 MB 수준 |
- 최대 1.5GB 이상 사용 가능성이 있습니다.
요약 정리
work_mem이 쿼리 실행 중 어떤 Plan 노드에서, 어떤 방식으로 누적 소비되고, 디스크 스필이나 OOM에 어떤 영향을 주는지를 파악하기 위해 두 가 케이스를 비교해봤습니다.
Case 1 vs Case 2 비교
| 항목 | Case 1 (work_mem 내) | Case 2 (Disk Spill 발생) |
| 쿼리 규모 | 1천만 row | 1억 row |
| 주요 연산 | GROUP BY + ORDER BY | row_number, json_agg 등 |
| 병렬성 | 3개 프로세스 | 3개 프로세스 |
| Sort 대상 | 적음 | 큼 (description, 윈도 함수 등) |
| Spill 여부 | ❌ 없음 | ✔️ 있음 (22GB+) |
| ExprContext 크기 | 작음 | 큼 |
| 총 메모리 사용량 | ≈ 200kB | 최대 ≈ 1.5GB 이상 |
- 같은 병렬 구조지만, 메모리 사용량이 전혀 다름.
- 병렬 Plan이 동일하게 3개의 프로세스를 사용했지만, Plan 구조(WindowAgg, json_agg, Incremental Sort)에 따라 메모리 압박이 달라짐.
- work_mem 사용은 Plan 노드 수와 row 수에 따라 기하급수적으로 증가.
- 단순 GroupAggregate는 28kB 내외로 끝났지만, 윈도 함수 + 다중 정렬 + 중첩 집계(json_agg)는 external merge sort 및 temp 디스크 사용 유발.
- spill이 발생하는 Plan 노드 vs 발생하지 않는 노드를 구분할 수 있음.
- Sort, HashAggregate: spill 가능.
- ExprContext, json_agg, FunctionScan: spill 불가 → OOM 위험.
+ 실행계획에서 반드시 봐야 할 메모리 사용 포인트
| 확인 포인트 | 의미 | 예시 |
| Sort Method | Spill 여부 (quicksort vs external) | external merge |
| Memory Usage | Plan 노드별 사용량 확인 | Memory Usage: 24kB |
| Disk: ...kB | 디스크 스필 크기 | Disk: 22811264kB |
| ExprContext OOM | Spill 불가 메모리 영역 | json_agg, ST_AsGeoJSON |
| Plan 노드 수 × 병렬성 | 누적 work_mem 예상량 계산 | Sort 3개 x 3 = 9 x work_mem |
| temp read/write | I/O 병목 여부 파악 | 144GB / 160GB 수준 |
++ 실시간 메모리 사용 확인 쿼리
SELECT * FROM pg_backend_memory_contexts WHERE name LIKE '%Sort%' OR name LIKE '%Expr%';
요약하자면,
- 단순 쿼리는 low work_mem으로도 충분히 처리 가능합니다.
- 복잡 쿼리는 Plan 단위로 메모리 사용을 분산 분석해야 합니다.
- work_mem은 단일 쿼리 설정이 아니라, Plan 노드와 병렬 구조에 따라 누적되어 사용됩니다.
- Spill이 발생하는 Plan은 Sort, Aggregate, Hash Join입니다.
- Spill이 발생하지 않는 Plan은 위험할 수도 있습니다. → OOM 발생 가능
PostgreSQL 쿼리의 메모리 사용은 단순히 work_mem만으로 결정되지 않으며, Plan 노드의 구조, 병렬성, 데이터량, 표현식의 복잡도까지 모두 반영해야 실질적인 메모리 사용량과 위험을 예측할 수 있습니다.
오늘은 여기까지~
'PostgreSQL' 카테고리의 다른 글
| PostgreSQL: EXPLAIN (0) | 2025.05.29 |
|---|---|
| PostgreSQL: postgres_fdw vs dblink (0) | 2025.05.28 |
| PostgreSQL: Disk Spill (0) | 2025.05.26 |
| PostgreSQL: ExprContext 기반 GeoJSON 조회 쿼리로 인한 OOM(Out of Memory) 발생 사례 분석 (0) | 2025.05.24 |
| PostgreSQL: WAL (0) | 2025.05.23 |