GraphDB 8종 벤치마크 (1/2) — RCTE 290x 격차의 원인 분해
- 1. GraphDB 8종 벤치마크 (1/2) — RCTE 290x 격차의 원인 분해
- 2. GraphDB 8종 벤치마크 (2/2) — 워크로드 매트릭스와 최종 선택 기준
GraphRAG·LightRAG처럼 이웃노드탐색이 지배적인 지식그래프 검색 워크로드를 8개 그래프 엔진에서 동일 데이터(1.14M edges) · 동일 인프라(GCP n2-standard-8)로 측정한 결과, plain PostgreSQL의 Recursive CTE가 Apache AGE 대비 290배 빠른 처리량을 기록했습니다. 격차의 원인은 cypher() wrapper의 호출당 13ms 오버헤드와 PostgreSQL plan generation 비용 누적입니다. 이 글에서는 측정 셋업, 워크로드 분포, RCTE와 AGE의 내부 동작 차이, 그리고 격차가 발생하는 정확한 지점을 정리합니다. 8개 엔진 전체 비교와 워크로드별 권장은 후속 글에서 다룹니다.
측정 셋업
측정 목적은 단일 질문에 답하는 것이었습니다. "이웃노드탐색이 지배적인 지식그래프 검색 워크로드에서, 그래프 전용 엔진이 plain PostgreSQL보다 실제로 빠른가." 이 질문에 답하려면 엔진마다 다른 인프라나 데이터를 쓰면 안 됩니다.
데이터. Wikipedia를 rule-based 파싱(LLM 미사용)으로 처리해 50K docs, 108K chunks, 587K entities, 1.14M edges를 생성했습니다. LightRAG 스키마를 프록시로 삼은 지식그래프 구조이며, 모든 엔진이 동일한 데이터를 적재했습니다.
인프라. GCP asia-northeast3-a 존에 n2-standard-8(8 vCPU, 32GB) VM 8대를 배치했습니다. 엔진마다 전용 VM을 할당해 간섭을 없앴고, 부하 생성기는 별도 VM에서 내부 VPC를 통해 접근해 외부 네트워크 지연을 제거했습니다.
측정 엔진. 8개 엔진을 비교했습니다.
| 엔진 | 분류 | 버전 |
|---|---|---|
| PostgreSQL RCTE | PG 평면 SQL + Recursive CTE | PG 17 |
| Apache AGE | PG extension + cypher() wrapper | AGE 1.6.0 / PG 17 |
| Neo4j | 네이티브 GraphDB, Bolt 프로토콜 | 5.20 Community |
| MemGraph | in-RAM Cypher 엔진 | Community |
| FalkorDB | Redis 모듈 + GraphBLAS | (RedisGraph 계보) |
| AgensGraph | PG 16 fork + native Cypher executor | v2.16 |
| pgRouting | PG extension, 경로 탐색 전용 | — |
| PGQ | PG 19-dev SQL/PGQ | dev branch |
PGQ는 개발 브랜치라 production 비교에서는 제외하고 참고 수치로만 언급합니다.
부하 시나리오. Phase 1~5는 JMeter로, Phase 6부터는 Go native binary harness로 교체해 JMeter 자체 스레드 오버헤드를 제거했습니다. 동시 사용자 u=1 / 10 / 50을 각각 180초 측정했습니다. 시드 풀은 500개 랜덤 chunk_id·entity_id로 구성해 동일 쿼리 반복에 의한 플랜 캐시 왜곡을 줄였습니다.
벤치마크 방법
부하 생성기는 Phase 6부터 Go로 직접 작성한 정적 바이너리를 사용했습니다. JMeter는 JSR223 Groovy 스레드 관리 오버헤드가 실측에서 수십 ms씩 더해지는 것을 Phase 5에서 확인해 교체했습니다.
워크로드 mix 정의. 쿼리 종류와 비율은 엔진 인터페이스 파일에 선언했습니다.
// bench/native/engine/engine.go
var QueryNames = []string{
"Q1_POINT_LOOKUP", // Chunk 단건 조회
"Q2_CHUNK_TO_ENTITY", // Chunk → Entity 1-hop
"Q3_ENTITY_TO_CHUNK", // Entity → Chunk 역방향
"Q4_CHUNK_ENTITY_CHUNK",// 2-hop (Chunk–Entity–Chunk)
"Q5_DOC_EXPAND", // Document → Chunk 전체
"Q6_CO_ENTITY", // co-entity 집계
"Q7_HYBRID_EXPAND", // multi-seed expand
}
// Phase 5 JMX의 30/30/20/10/3/4/3 비율을 그대로 이식
var QueryWeights = []int{30, 30, 20, 10, 3, 4, 3}부하 생성 루프. 각 worker goroutine은 seed pool에서 랜덤 파라미터를 뽑아 가중치 분포대로 쿼리를 선택하고 타이밍을 기록합니다.
// bench/native/runner.go (핵심 발췌)
for w := 0; w < cfg.users; w++ {
go func(workerID int) {
rng := rand.New(rand.NewSource(int64(workerID)*1_000_003 + time.Now().UnixNano()))
for time.Now().Before(deadline) {
qIdx := pickQuery(cfg.cumWeights, cfg.totalWeight, rng)
qName := cfg.queryNames[qIdx]
params := paramsForQuery(qName, cfg.seeds, rng) // seed pool에서 랜덤 선택
start := time.Now()
_, err := eng.Execute(qName, params)
elapsed := time.Since(start)
cfg.jtl.write(sample{
label: qName,
elapsedMs: elapsed.Milliseconds(),
success: err == nil,
allThreads: cfg.users,
})
}
}(w)
}측정 실행. 동시 사용자 u=1 / 10 / 50을 각 180초씩 순차 측정합니다. 엔진별로 VM을 분리해 두었으므로 루프 안에서 endpoint만 바꿉니다.
# bench/native/run_phase6.sh (핵심 발췌)
ENGINES="neo4j memgraph age rcte pgq falkordb"
USERS="1 10 50"
DURATION="180s"
RAMP="10s"
for eng in $ENGINES; do
for u in $USERS; do
./bench-native \
--engine="$eng" \
--users="$u" \
--duration="$DURATION" \
--ramp="$RAMP" \
--csv="seeds_random.csv" \ # 500개 랜덤 seed
--jtl="results/${eng}_u${u}.jtl" \
${ENGINE_ARGS[$eng]}
done
done메트릭 집계. 결과는 JTL(JMeter CSV 호환 포맷)으로 저장하고 Python 스크립트로 백분위를 계산합니다.
# bench/jmeter/analyze_jtl.py (핵심 발췌)
def analyze(jtl: Path) -> dict:
elapsed: dict[str, list[int]] = defaultdict(list)
with jtl.open() as f:
for row in csv.DictReader(f):
elapsed[row["label"]].append(int(row["elapsed"]))
duration_s = (t_end - t_start) / 1000 # ms 타임스탬프 차이
for label, vals in elapsed.items():
s = sorted(vals)
n = len(s)
stats[label] = {
"p50": s[int(n * 0.50)],
"p95": s[int(n * 0.95)],
"p99": s[min(int(n * 0.99), n - 1)],
"rps": round(n / duration_s, 1),
}seed pool은 500개 랜덤 chunk_id · entity_id로 구성했습니다. 동일한 파라미터 반복에 의한 plan cache 왜곡을 줄이는 동시에, 고빈도 entity(degree 수백)와 희소 entity(degree 1~2)가 섞여 실제 분포를 모사합니다.
워크로드 분포
측정 기준을 정하기 전에 실제 RAG retrieval이 어떤 쿼리를 던지는지를 먼저 파악했습니다. LightRAG의 BaseGraphStorage 호출 분포를 분석하면 다음과 같습니다.
| 연산 | 비율 | 성격 |
|---|---|---|
get_node |
35% | 단일 노드 point lookup |
get_node_edges |
25% | 노드의 인접 엣지 전체 조회 |
upsert_node |
15% | 노드 생성 또는 갱신 |
node_degree |
15% | 노드의 차수 계산 |
has_node |
10% | 노드 존재 여부 확인 |
모두 단일 노드를 기준으로 하는 1-hop OLTP 패턴입니다.
이 분포가 엔진 선택에 결정적입니다. 그래프 DB가 강점을 보이는 패턴 — variable-depth path traversal, shortestPath, cycle detection — 은 retrieval critical path에 없습니다. get_knowledge_graph는 BFS 탐색을 포함하지만 시각화 보조 경로이고, depth 2, max_nodes 50으로 상한이 고정되어 있습니다. RCTE의 알려진 약점인 deep BFS frontier 폭발이 이 워크로드에서는 설계 차원에서 회피됩니다.
더 일반적인 GraphRAG OLTP 혼합 시나리오(Q1~Q7)에서는 비중을 조정했습니다. Q1 point lookup 30% / Q2 1-hop 순방향 30% / Q3 1-hop 역방향 20% / Q4 2-hop 10% / Q5 doc expand 3% / Q6 co-entity 집계 4% / Q7 hybrid expand 3%. 이 혼합에서도 1-hop lookup이 80% 이상을 차지합니다.
측정 지표. 주요 측정 지표는 RPS(초당 처리 요청 수)와 p50 / p95 / p99 latency입니다. RPS는 동시 처리 능력을 나타내고, tail latency는 실사용에서 사용자가 경험하는 최악 응답 시간을 나타냅니다. u=50(동시 사용자 50)이 production-scale 기준값입니다.
핵심 결과 — RCTE 22,581 RPS, AGE 78 RPS
Phase 6 OLTP 측정(Go harness, u=50, 180초)의 핵심 결과입니다.
처리량 비교 (u=50, RPS)
──────────────────────────────────────────────
PostgreSQL RCTE ████████████████████████ 22,581
MemGraph (in-RAM) ████████████████████████ 22,462 ¹
Neo4j ████████████████ 14,541
PGQ (dev) ████ 3,157 ²
AgensGraph █ 4,795 ³
Apache AGE ▏ 78
──────────────────────────────────────────────
¹ 1GB 컨테이너에서 OOM-kill 9회
² PG 19-dev, production 참고용
³ Q1 단독 수치 — mix 워크로드에서는 사실상 0| 엔진 | RPS (u=50) | 에러율 | 비고 |
|---|---|---|---|
| RCTE (PG 17 flat SQL) | 22,581 | 0% | 1순위 |
| MemGraph (in-RAM) | 22,462 | 0% | 동률이나 1GB OOM-kill 9회 |
| Neo4j (Bolt v5) | 14,541 | 0% | 안정적 올라운더 |
| PGQ (PG 19-dev) | 3,157 | 0% | dev branch, 참고 수치 |
| AGE (cypher() wrapper) | 78 | 6.9% | RCTE 대비 290배 느림 |
| FalkorDB | ~18 | 99.3% | ConnectionPool 고갈, 사실상 실패 |
| AgensGraph | catastrophic | mixed | Q7 단일 쿼리 분 단위 폭발 |
에러율 6.9%까지 포함하면 AGE의 실효 처리량은 더 낮습니다.
메모리 제약 조건(1GB 컨테이너)에서의 결과도 주목할 만합니다.
| 엔진 | RPS (1GB, u=50) | 정상 대비 변화 |
|---|---|---|
| RCTE | 22,370 | -0.4% |
| Neo4j | FAIL | JVM 512M heap 최소치 미달, 기동 불가 |
| MemGraph | 46,519 (93.5% err) | OOM-kill 재시작 루프 |
RCTE는 32GB에서 1GB로 메모리를 줄여도 성능 저하가 -0.4%입니다. PostgreSQL의 shared_buffers가 128MB로 축소되면 나머지 요청은 OS page cache와 디스크 접근으로 처리되는데, 이 graceful degradation 덕분에 성능 곡선이 평탄합니다. 반면 Neo4j는 JVM heap이 512MB 미만이면 기동 자체가 불가능하고, MemGraph는 RAM 한계를 넘는 순간 OOM-kill로 프로세스가 종료됩니다.
RCTE가 왜 이만큼 빠른가
RCTE의 그래프 모델링은 두 개의 평면 테이블로 시작합니다.
CREATE TABLE nodes (
id text PRIMARY KEY,
label text,
properties jsonb
);
CREATE TABLE edges (
src text REFERENCES nodes(id),
dst text REFERENCES nodes(id),
edge_type text,
properties jsonb,
PRIMARY KEY (src, dst, edge_type)
);
CREATE INDEX ON edges(src); -- 1-hop forward traversal
CREATE INDEX ON edges(dst); -- 역방향 traversal이 테이블 위에서 LightRAG의 dominant 패턴인 get_node_edges 구현은 다음과 같습니다.
-- get_node_edges(entity_id)
SELECT e.dst, n.label, n.properties
FROM edges e
JOIN nodes n ON n.id = e.dst
WHERE e.src = $1;같은 의미의 AGE 쪽 쿼리는 다음과 같습니다.
-- AGE get_node_edges 등가 쿼리 (cypher() wrapper 경유)
SELECT * FROM cypher('graphdb', $$
MATCH (c:Chunk {id: 'abc123'})-[:MENTIONS]->(e:Entity)
RETURN e.id, e.canonical_title
LIMIT 50
$$) AS (id agtype, canonical_title agtype);RCTE 쪽은 plain SQL, AGE 쪽은 문자열 Cypher를 cypher() 함수 인자로 넘깁니다. 실행 경로가 다릅니다. RCTE는 WHERE chunk_id = $1 조건 하나로 B-tree index를 타고 곧바로 결과를 가져옵니다. AGE는 문자열 파싱 → AST 생성 → SQL 변환 → planner 전달 경로를 매 호출마다 반복합니다.
B-tree index hit + nested loop join. PostgreSQL planner가 인덱스를 직접 활용해 ms 미만으로 응답합니다. 그래프 전용 엔진의 traversal 레이어가 개입하지 않습니다.
이유는 세 가지 구조적 특성이 겹친 결과입니다. 1-hop lookup은 평면 SQL + B-tree 인덱스가 가장 효율적으로 처리하는 패턴입니다. WHERE src = $1 조건 하나로 인덱스를 hit하고 결과를 바로 반환합니다. 그래프 전용 엔진이 제공하는 pointer chain 구조나 custom traversal cursor는 이 패턴에서 추가 레이어가 될 뿐입니다. RCTE 쿼리는 표준 SQL이라 PostgreSQL의 prepared statement 캐시를 그대로 활용하고, 동일한 쿼리 구조가 반복되면 parse와 plan generation 비용이 한 번으로 수렴합니다. 메모리 측면에서는 shared_buffers를 넘어서는 요청이 OS page cache로 흘러가고, 그것도 넘어서면 디스크에서 읽습니다. 어느 단계에서도 프로세스가 종료되지 않고, per-query working memory(work_mem 기본 4MB) 안에서 1-hop lookup의 중간 결과 집합은 작아 spill도 거의 발생하지 않습니다.
시각화 용도의 BFS가 필요한 경우에는 WITH RECURSIVE 구문이 등장합니다.
-- get_knowledge_graph(node_id, depth=2, max_nodes=50)
WITH RECURSIVE graph_bfs AS (
SELECT id, properties, 0 AS depth
FROM nodes
WHERE id = $1
UNION ALL
SELECT n.id, n.properties, b.depth + 1
FROM graph_bfs b
JOIN edges e
ON (e.src = b.id OR e.tgt = b.id)
JOIN nodes n
ON n.id = CASE
WHEN e.src = b.id THEN e.tgt
ELSE e.src
END
WHERE b.depth < 2
)
SELECT DISTINCT id, properties FROM graph_bfs
LIMIT 50;depth 2, max_nodes 50 제약 안에서 이 쿼리는 단일 SQL 문장으로 BFS 전체를 처리합니다. 별도 그래프 엔진 없이 PostgreSQL 표준 SQL로 표현되는 구조입니다.
AGE는 왜 이만큼 느린가
Apache AGE는 PostgreSQL extension입니다. Bitnine의 AgensGraph(PG fork)에서 시작해, AGE는 PG 본체를 건드리지 않고 cypher() SQL 함수 형태로 Cypher를 노출하는 설계를 선택했습니다. 이 선택이 성능에 직접적인 영향을 미칩니다.
cypher() 함수는 다음과 같이 호출됩니다.
SELECT * FROM cypher('graph_name', $$
MATCH (e:Entity {id: $entity_id})-[:MENTIONS]->(c:Chunk)
RETURN c.id, c.properties
$$) AS (id agtype, properties agtype);문자열 형태의 Cypher를 인자로 받아 실행하는 구조입니다. 이 호출 경로에서 매 요청마다 다음 작업이 발생합니다.
cypher() 진입
→ Cypher 문자열 파싱 (cypher_gram.y → AST 생성)
→ AST → SQL JOIN 시퀀스 변환 (cypher_clause.c)
→ PostgreSQL planner에 SQL 전달
→ 결과를 agtype으로 wrap
→ 역직렬화 후 반환아래는 벤치마크에서 실제로 실행된 Q2(Chunk → Entity 1-hop) 쌍입니다.
-- RCTE Q2: chunk → entity 1-hop (rcte_queries.py)
SELECT entity_id
FROM rcte_mentions
WHERE chunk_id = 'c_00042'
LIMIT 50;-- AGE Q2: 동일 의미, cypher() wrapper 경유 (age_queries.py)
SELECT * FROM cypher('graphdb', $$
MATCH (c:Chunk {id: 'c_00042'})-[:MENTIONS]->(e:Entity)
RETURN e.id, e.canonical_title
LIMIT 50
$$) AS (id agtype, canonical_title agtype);두 쿼리의 결과는 동일합니다. 실행 비용이 다릅니다.
Phase 8에서 AgensGraph(PG fork로 Cypher executor가 PG 내부에 직접 통합됨)와 AGE를 동일 워크로드로 비교해 wrapper 비용을 분리 측정했습니다. 결과는 다음과 같습니다.
- AGE cypher() wrapper 비용: 호출당 약 13ms
- Q1 단순 point lookup 기준, RCTE p50이 0.1ms 미만인 반면 AGE는 12~15ms
- wrapper 비용이 실제 쿼리 실행 비용을 압도
AgensGraph는 wrapper 비용 없이 Q1 단독으로 4,795 RPS를 기록했습니다. AGE의 같은 조건 RPS보다 100배 가까이 높습니다. 이 차이가 cypher() wrapper가 부과하는 오버헤드의 규모를 보여줍니다.
wrapper 비용 외에 구조적 한계가 하나 더 있습니다. AGE가 Cypher를 SQL로 변환하면, PostgreSQL planner는 그 결과물을 일반 관계형 JOIN으로 최적화하려 합니다.
Phase 2 Q4(2-hop traversal)의 EXPLAIN ANALYZE 결과에서 다음 패턴이 관찰됩니다.
Hash Join (rows=8436)
→ Nested Loop
Rows Removed by Join Filter: 19,450,994MATCH (c)-[:MENTIONS]->(e)<-[:MENTIONS]-(c2) 패턴이 FROM MENTIONS m1 JOIN Entity e ON e.id = m1.end_id JOIN MENTIONS m2 ON m2.end_id = e.id 형태의 N-way JOIN으로 변환됩니다. PostgreSQL planner는 이 패턴이 그래프 traversal이라는 사실을 알 수 없어 selectivity 추정에 실패하고, 1,940만 행을 검사한 뒤 최종 결과를 추립니다. Neo4j가 같은 쿼리를 pointer chain으로 O(degree) 처리하는 것과 근본적으로 다른 실행 경로입니다.
동시성 확장 측면에서도 차이가 드러납니다. Phase 2에서 u=1 → u=50으로 동시성을 50배 늘렸을 때, Neo4j는 처리량이 9.6배 늘어난 반면 AGE는 2.6배에 그쳤습니다. 처리량이 늘지 않는 상태에서 동시 요청이 쌓이면 queuing delay가 누적됩니다. u=50에서 AGE Q6(co-entity 집계) p50이 4,486ms에 달하는 이유입니다.
lightrag_pg_not_age 후속 검증 프로젝트는 동일한 결론을 LightRAG 실제 출력 데이터로 재확인했습니다. 소규모(8,050 nodes / 25,844 edges) 환경에서도 RCTE가 AGE 대비 58~73배 높은 throughput을 보였고, AGE p95 latency는 200ms를 넘었습니다. 데이터 규모가 작아지면 격차가 다소 줄어들지만 방향은 동일합니다.
격차의 본질 — 워크로드와 추상화의 매칭
RCTE의 290배 우위는 우연이 아닙니다. 워크로드 구조와 자료구조가 매칭된 결과입니다.
이웃노드탐색-heavy 지식그래프 검색 워크로드의 핵심 특성은 두 가지입니다. 요청의 60% 이상이 1-hop lookup이고, BFS가 포함되더라도 depth 2, max_nodes 50으로 상한이 고정됩니다. 이 두 특성이 결합되면 B-tree 인덱스를 활용한 평면 SQL이 가장 효율적인 표현이 됩니다.
Cypher wrapper(AGE)와 PG fork(AgensGraph)는 이 워크로드에서 PostgreSQL plan generation 비용을 추가하기만 합니다. AGE는 cypher() 호출마다 13ms를 부과하고, AgensGraph는 wrapper를 제거했지만 PG planner의 그래프 패턴 한계는 그대로입니다. 두 접근 모두 Cypher 질의 레이어가 제공하는 기능을 실제로 활용하지 않는 워크로드에서 그 레이어의 비용만 지불합니다.
"그래프 워크로드에는 그래프 DB가 필요하다"는 가정이 널리 받아들여지지만, 그래프 DB가 강점을 발휘하는 패턴은 구체적입니다. variable-depth path traversal([*1..N]), shortestPath, cycle detection, community detection — 이 패턴들이 retrieval critical path에 있을 때 그래프 DB의 투자 이유가 생깁니다.
측정에서 확인된 수치를 보면 격차가 얼마나 선명한지 드러납니다.
| 패턴 | Neo4j | RCTE | 비율 |
|---|---|---|---|
| Q11 shortestPath (u=10) | 5,739 RPS | ~0.02 RPS | 약 287,000배 |
| Q8 variable-depth (u=10) | 2,820 RPS | 0.2 RPS | 14,000배 |
| Q1 point lookup (u=50) | 14,541 RPS | 22,581 RPS | RCTE 1.55배 우위 |
shortestPath와 variable-depth traversal에서 Neo4j가 RCTE보다 수만 배 빠른 것은 pointer chain 구조(fixed-size 16~34B record, doubly-linked list 체인)가 O(degree) traversal을 가능하게 하기 때문입니다. RCTE는 같은 패턴에서 모든 경로를 enumerate해야 하고 1.14M edges 전체를 탐색하는 상황이 발생합니다.
그러나 LightRAG의 get_knowledge_graph는 depth 2, max_nodes 50으로 고정되어 있습니다. RCTE의 frontier 폭발 약점이 이 워크로드에서 의도적으로 회피됩니다. LightRAG가 설계한 방식이 RCTE의 sweet spot 안에 정확히 들어와 있는 구조입니다.
1-hop lookup 위주의 워크로드에서 Cypher 추상화 레이어는 그래프 연산 기능을 제공하는 대신 그 비용을 매 호출에 부과합니다. RCTE는 그 레이어 없이 PostgreSQL planner가 직접 인덱스를 활용합니다.
추가로, RCTE는 SQL 표준(ANSI SQL:1999 WITH RECURSIVE)이라 PostgreSQL 9.1 이후 모든 버전에서 동작하고, MySQL, SQL Server, Oracle도 동일 구문을 지원합니다. AGE나 AgensGraph가 각자 지원하는 Cypher subset과 달리 호환성 격차가 없습니다. Phase 7에서 확인된 Cypher 호환성 현황을 보면, AGE는 shortestPath() 파서 에러와 variable-length plan 폭발이 있고, FalkorDB는 shortestPath()와 EXISTS { } 미구현 상태입니다. "Cypher 호환성"이 마케팅 차원에서 주장되지만 엔진마다 실제 지원 범위가 다릅니다.
측정이 커버하지 않는 영역
290배 격차와 RCTE 우위는 이웃노드탐색-heavy 지식그래프 검색 워크로드에 한정됩니다. 측정 범위 바깥의 패턴에서는 결론이 달라집니다.
shortestPath가 retrieval critical path에 있다면 RCTE는 실용 불가입니다. 단발 쿼리에 50초가 걸리는 수치는 선택지가 아닙니다. community detection(Leiden/Louvain)이 검색 흐름의 핵심인 Microsoft GraphRAG 스타일 워크로드도 RCTE로 직접 처리하기 어렵습니다. 이 영역은 Neo4j GDS 또는 외부 batch 처리(NetworkX/igraph)가 담당합니다.
메모리가 충분하고 workload가 안정적인 환경에서 MemGraph의 u=50 처리량(22,462 RPS)은 RCTE와 동률입니다. 단 1GB 컨테이너에서 OOM-kill 9회, 93.5% 에러율을 보인 운영 위험은 별도로 감수해야 합니다.
후속 글 예고
Part 2에서는 Neo4j 안전한 올라운더 분석, MemGraph OOM 위험과 운영 복잡도, FalkorDB catastrophic 실패와 analytics-only 용도, AgensGraph Q1/Q7 비대칭, pgRouting 한계, Microsoft GraphRAG / Leiden community 의존 시스템, 그리고 워크로드별 권장 매트릭스 전체를 다룹니다.