{#performance-tuning-a-query} # 쿼리 튜닝 Cypher 쿼리는 PostgreSQL의 플래너가 계획합니다. 자기 규칙을 가진 별도의 그래프 엔진이 없으므로, `EXPLAIN`이 동작하고, 통계가 중요하며, 이미 알고 있는 튜닝이 그대로 적용됩니다. AgensGraph에 특유한 부분은 순회를 위한 몇 개의 실행 계획 노드, 새 그래프마다 반드시 걸리는 통계 문제 하나, 그리고 실행 계획의 모양을 바꾸는 설정 다섯 개입니다. 이 페이지는 실행 계획의 모양만 다룹니다. 자원 쪽 튜닝은 `shared_buffers`, `work_mem`, `effective_cache_size`, 병렬성, autovacuum을 다루며, 그중 그래프에 특유한 것은 없고 [구성](../operations/configuration.md)에 있습니다. {#performance-a-graph-to-measure-on} ## 측정할 그래프 [예제 데이터셋](../get_started/example-dataset.md)은 요소가 46개여서, 플래너가 흥미로운 결정을 내리기에는 부족합니다. 차수 분포가 있는 것을 만들고 끝에서 지웁니다: ```sql CREATE TABLE seed_account AS SELECT g AS n, 'user' || g AS handle FROM generate_series(1, 20000) g; CREATE TABLE seed_follows AS SELECT (1 + (g % 20000)) AS src, (1 + ((g * 7919) % 20000)) AS dst FROM generate_series(1, 120000) g; ``` ```cypher CREATE GRAPH perflab; SET graph_path = perflab; CREATE VLABEL account; CREATE ELABEL follows; LOAD FROM seed_account AS r CREATE (:account {handle: r.handle, n: r.n}); CREATE PROPERTY INDEX ON account (handle); ``` ```cypher LOAD FROM seed_follows AS r MATCH (a:account {n: r.src}), (b:account {n: r.dst}) CREATE (a)-[:follows]->(b); ``` ```sql ANALYZE perflab.account; ``` 계정 20,000개와 `follows` 간선 120,000개입니다. 정점 레이블만 분석했습니다. 그 누락이 [뒤에 나오는 절의 요점](#performance-statistics-on-edge-labels)입니다. {#performance-reading-a-cypher-explain} ## Cypher의 EXPLAIN 읽기 ```cypher EXPLAIN (COSTS OFF) MATCH (a:account {handle: 'user42'})-[r1:follows]->(b:account)-[r2:follows]->(c:account) RETURN c.handle; ``` ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Hash Join Hash Cond: (r2."end" = c.id) -> Nested Loop -> Nested Loop -> Index Scan using account_handle_idx on account a Index Cond: (properties.'handle' = '"user42"'::jsonb) -> Bitmap Heap Scan on follows r1 Recheck Cond: ((a.id = start) AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) -> Bitmap Index Scan on follows_start_idx Index Cond: ((start = a.id) AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) -> Index Scan using follows_start_idx on follows r2 Index Cond: ((start = r1."end") AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) Filter: (r1.id <> id) -> Hash -> Seq Scan on account c ``` 위에서 AgensGraph에 특유한 것은 네 가지이고, 나머지는 전에 읽어 본 적 있는 실행 계획입니다. **속성 읽기는 `properties.'key'`로 출력됩니다.** 그것이 Cypher 파서가 속성 접근을 표기하는 방식이며, 직접 입력할 수 있는 SQL이 아닙니다. `Index Cond`에 있으면 표현식 인덱스와 맞춰지고 있는 것입니다. `Filter`에 있으면 행마다 평가되고 있는 것입니다. 둘 중 어느 쪽을 받는지가 [인덱스 페이지의 주제](indexes-and-constraints.md)입니다. **`graphid` 범위 검사는 레이블 검사입니다.** `start >= '3.0' AND start <= '3.281474976710655'`은 "간선의 이쪽 끝에 있는 정점이 `account`다"라는 뜻입니다. 레이블 id는 `graphid`의 상위 비트에 있으므로 한 레이블의 id는 연속된 구간을 이루고, 그 검사는 간선 자신의 컬럼에서 답할 수 있습니다. [식별자: graphid](#graph-on-postgresql-identity-the-graphid)를 참고하십시오. 두 번째 간선에 붙은 **`Filter: (r1.id <> id)`** 는 고유성 규칙입니다. 어떤 경로도 한 간선을 두 번 쓸 수 없습니다. **이름을 붙이지 않은 패턴 요소에는 합성 별칭이 주어지고**, `"<0000000002>"` 처럼 출력됩니다. 그 뒤의 카운터는 문장마다 올라가므로, 그것이 들어 있는 실행 계획은 실행할 때마다 달라집니다. 기준선과 비교하려는 계획이라면 패턴의 모든 요소에 이름을 붙이십시오. `EXPLAIN (ANALYZE)`도 동작하며, 추정치를 실제와 견주어 볼 수 있는 유일한 방법입니다: ```cypher EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) MATCH (a:account {handle: 'user42'})-[r1:follows]->(b:account) RETURN count(b); ``` ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Aggregate (actual rows=1.00 loops=1) -> Nested Loop (actual rows=6.00 loops=1) -> Nested Loop (actual rows=6.00 loops=1) -> Index Scan using account_handle_idx on account a (actual rows=1.00 loops=1) Index Cond: (properties.'handle' = '"user42"'::jsonb) Index Searches: 1 -> Bitmap Heap Scan on follows r1 (actual rows=6.00 loops=1) Recheck Cond: ((a.id = start) AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) Heap Blocks: exact=1 -> Bitmap Index Scan on follows_start_idx (actual rows=6.00 loops=1) Index Cond: ((start = a.id) AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) Index Searches: 6 -> Index Only Scan using account_pkey on account b (actual rows=1.00 loops=6) Index Cond: (id = r1."end") Heap Fetches: 6 Index Searches: 6 ``` ```{note} 가변 길이 계획과 최단 경로 계획은 기본 텍스트 형식으로 읽으십시오. 셋 중 `EXPLAIN (FORMAT JSON)`으로도 계획이 제대로 읽히는 것은 `Dijkstra`입니다. ``` {#performance-plan-nodes-for-a-traversal} ## 순회를 위한 실행 계획 노드 길이가 고정된 패턴에는 특별한 노드가 필요하지 않습니다. 위에서 본 것처럼 스캔과 조인입니다. 아래 노드들은 SQL에 대응 연산자가 없는 것들을 위해 나타납니다. `Graph VLE`는 가변 길이 간선이며, 간선 레이블과 범위를 이름으로 보여 줍니다: ```cypher SET graph_path = moviekb; EXPLAIN (COSTS OFF) MATCH (a:movie {title: 'The Matrix'})-[r:related_to*1..3]->(b:movie) RETURN b.title; ``` ``` QUERY PLAN -------------------------------------------------------------- Nested Loop -> Seq Scan on movie a Filter: (properties.'title' = '"The Matrix"'::jsonb) -> Hash Join Hash Cond: (r."end" = b.id) -> Subquery Scan on r -> Graph VLE on related_to [1..3] -> Result -> Hash -> Seq Scan on movie b ``` 읽어야 할 줄은 `on related_to`입니다. 타입 없는 가변 길이 간선은 `Graph VLE on ag_edge`가 되고, 그것은 매 홉마다 그래프의 모든 간선 레이블을 훑습니다. 경로 쿼리에서 피할 수 있는 가장 큰 비용이 그것입니다. 비교는 [경로](#paths-what-a-path-costs)에 있습니다. `Shortestpath VLE [min..max]`는 `shortestpath` 또는 `allshortestpaths`이며, `Hash2Side` 노드 두 개 위에 놓입니다. 그 두 노드는 양쪽 끝에서 각각 자라나는 양방향 탐색의 두 전선입니다. `Dijkstra`는 가중 탐색입니다. 그 자식이 확장에 쓰는 스캔이므로, 그것이 훑는 것은 간선 레이블의 `start` 컬럼에 걸린 인덱스입니다. `Graph Create`, `Graph Set`, `Graph Delete`가 쓰기 노드입니다: ```cypher SET graph_path = perflab; EXPLAIN (COSTS OFF) MATCH (a:account {handle: 'user42'}) SET a.seen = 1; ``` ``` QUERY PLAN --------------------------------------------------------------- Graph Set -> Index Scan using account_handle_idx on account a Index Cond: (properties.'handle' = '"user42"'::jsonb) ``` **`eager`** 로 표시된 쓰기 노드는 그 위의 어떤 것이 실행되기 전에 끝나야 합니다. 그 위의 무언가가 그 쓰기가 바꾼 것을 읽기 때문입니다: ```cypher EXPLAIN (COSTS OFF) MATCH (a:account {handle: 'user42'}) SET a.seen = 1 WITH a MATCH (a)-[r:follows]->(b:account) RETURN count(b); ``` ``` QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------- Aggregate -> Nested Loop -> Nested Loop -> Subquery Scan on _agens_default_s -> Graph Set eager -> Index Scan using account_handle_idx on account a Index Cond: (properties.'handle' = '"user42"'::jsonb) -> Index Only Scan using follows_start_idx on follows r Index Cond: ((start = (_agens_default_s.a).id) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) -> Index Only Scan using account_pkey on account b Index Cond: (id = r."end") ``` [같은 문장 안의 뒤따르는 읽기에 쓰기가 보이게](#writing-reading-what-you-just-wrote) 만드는 것이 이 eagerness입니다. 대가는 쓰기 출력의 구체화이고, 그 크기는 `eager_mem`으로 제한됩니다. `MERGE`는 항상 eager이며, 문장의 마지막 절이 아닌 `SET`·`REMOVE`·`DELETE`도 그렇습니다. `_agens_default_s`로 시작하는 이름은 파이프라인 단계에 자동으로 붙은 별칭이고, 대략 `WITH` 하나에 하나씩입니다. 한 실행 계획 안에서는 안정적이며 튜닝할 대상이 아닙니다. {#performance-statistics-on-edge-labels} ## 간선 레이블의 통계 새 그래프에서 가장 먼저 확인할 것이 이것이고, 어떤 인덱스보다 값어치가 큽니다. 순회의 비용은 차수의 곱입니다. 출발한 정점에서 나가는 간선이 몇 개인지, 도달한 정점에서 나가는 간선이 몇 개인지, 그런 식으로 이어집니다. 간선 레이블의 `start`와 `end` 컬럼에 대한 통계가 그 차수를 설명합니다. AgensGraph는 레이블을 만들 때 두 컬럼의 통계 목표를 최댓값인 1,000으로 설정합니다: ```sql SELECT attname, attstattarget FROM pg_attribute WHERE attrelid = 'perflab.follows'::regclass AND attnum > 0 ORDER BY attnum; ``` ``` attname | attstattarget ------------+--------------- id | start | 1000 end | 1000 properties | ``` 목표는 표본이 아닙니다. 무언가가 그 레이블을 분석하기 전까지는 아무것도 없습니다: ```sql SELECT count(*) AS stats_rows FROM pg_stats WHERE schemaname = 'perflab' AND tablename = 'follows'; ``` ``` stats_rows ------------ 0 ``` 그리고 추정치는 짐작입니다. 갓 적재한 그래프에서 두 홉을 세어 보면 이렇습니다: ```cypher EXPLAIN MATCH (a:account {handle: 'user42'})-[r1:follows]->(b:account)-[r2:follows]->(c:account) RETURN count(c); ``` ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Aggregate (cost=10088.00..10088.01 rows=1 width=32) -> Hash Join (cost=677.81..9622.10 rows=186358 width=8) Hash Cond: (r2."end" = c.id) -> Nested Loop (cost=20.81..8475.81 rows=186358 width=24) -> Nested Loop (cost=20.39..779.08 rows=433 width=16) -> Index Scan using account_handle_idx on account a (cost=0.29..8.30 rows=1 width=8) Index Cond: (properties.'handle' = '"user42"'::jsonb) -> Bitmap Heap Scan on follows r1 (cost=20.10..766.44 rows=433 width=24) Recheck Cond: ((a.id = start) AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) -> Bitmap Index Scan on follows_start_idx (cost=0.00..20.00 rows=433 width=0) Index Cond: ((start = a.id) AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) -> Index Scan using follows_start_idx on follows r2 (cost=0.42..13.48 rows=430 width=24) Index Cond: ((start = r1."end") AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) Filter: (r1.id <> id) -> Hash (cost=407.00..407.00 rows=20000 width=8) -> Seq Scan on account c (cost=0.00..407.00 rows=20000 width=8) ``` 계정 하나에서 나가는 간선 433개, 두 홉 쌍 186,358개, 그리고 먼 쪽 끝점을 해결하기 위해 계정 20,000개 전체에 대한 해시입니다. 이제 간선 레이블을 분석합니다: ```sql ANALYZE perflab.follows; ``` ```cypher EXPLAIN MATCH (a:account {handle: 'user42'})-[r1:follows]->(b:account)-[r2:follows]->(c:account) RETURN count(c); ``` ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Aggregate (cost=51.17..51.18 rows=1 width=32) -> Nested Loop (cost=5.52..51.08 rows=36 width=8) -> Nested Loop (cost=5.23..39.78 rows=36 width=24) -> Nested Loop (cost=4.81..35.54 rows=6 width=16) -> Index Scan using account_handle_idx on account a (cost=0.29..8.30 rows=1 width=8) Index Cond: (properties.'handle' = '"user42"'::jsonb) -> Bitmap Heap Scan on follows r1 (cost=4.52..27.18 rows=6 width=24) Recheck Cond: ((a.id = start) AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) -> Bitmap Index Scan on follows_start_idx (cost=0.00..4.52 rows=6 width=0) Index Cond: ((start = a.id) AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) -> Index Scan using follows_start_idx on follows r2 (cost=0.42..0.65 rows=6 width=24) Index Cond: ((start = r1."end") AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) Filter: (r1.id <> id) -> Index Only Scan using account_pkey on account c (cost=0.29..0.31 rows=1 width=8) Index Cond: (id = r2."end") ``` 간선 6개, 쌍 36개, 먼 쪽 끝점에는 인덱스 전용 스캔, 그리고 추정 비용은 10,088 대신 51입니다. 그리고 36은 정확히 맞습니다: ```cypher MATCH (a:account {handle: 'user42'})-[r1:follows]->(b:account)-[r2:follows]->(c:account) RETURN count(c) AS pairs; ``` ``` pairs ------- 36 ``` 여기서 따라오는 규칙은 이것입니다. **대량 적재 뒤에는 정점 레이블만이 아니라 간선 레이블에도 `ANALYZE`를 돌리십시오.** autovacuum도 언젠가는 하지만, 그 "언젠가"는 첫 사용자들이 이미 그 쿼리를 실행한 뒤입니다. 허브 정점이 들어와 차수의 모양이 바뀐 그래프라면, 행 수 임계치를 기다리지 말고 명시적으로 다시 분석할 만합니다. {#performance-the-settings-that-change-plan-shape} ## 실행 계획의 모양을 바꾸는 설정 그래프 쿼리가 어떻게 계획되는지에 영향을 주는 설정이 다섯 개 있습니다. 아래는 `pg_settings`에서 읽은 실제 값입니다: | 설정 | 기본값 | 범위 | 하는 일 | | --- | --- | --- | --- | | `enable_eager` | `on` | 세션 | 끄면, eager 계획이 필요한 문장은 계획되지 않고 거부됩니다. | | `eager_mem` | 4 MB | 세션 | eager 노드가 디스크로 넘기기 전에 쓸 수 있는 메모리입니다. | | `enable_graph_endpoint_elision` | `on` | 세션 | 쿼리가 값을 전혀 읽지 않는 패턴 끝점의 스캔을 없앱니다. | | `enable_property_promotion` | `on` | 세션 | 승격된 타입 컬럼이 있으면 Cypher의 속성 읽기를 그 컬럼으로 해석합니다. | | `auto_gather_graphmeta` | `off` | 슈퍼유저 | `ag_graphmeta`의 간선 연결 정보를 유지하며, 그것으로 플래너가 도달할 수 없는 레이블을 스캔에서 잘라 냅니다. | ```{note} `postgresql.conf.sample`은 `enable_eager`와 `enable_multiple_update`의 기본값을 틀리게 적어 놓았습니다. 둘 다 켜져 있습니다. 샘플 파일이 아니라 `pg_settings`를 읽으십시오. ``` 이 중 어느 것도 운영에서 돌릴 손잡이가 아닙니다. 네 개는 이미 켜져 있고, 다섯째는 카탈로그를 유지할지에 대한 결정입니다. 진단을 위해 알아 두십시오. 하나를 끄면 그것이 예상하지 못한 실행 계획의 원인인지 알 수 있습니다. {#performance-endpoint-elision} ### 끝점 생략 쿼리가 값을 전혀 읽지 않는 끝점은 아예 스캔되지 않습니다. 여기서 `b`는 바인딩되고 레이블도 붙었지만 투영되지 않습니다: ```cypher EXPLAIN (COSTS OFF) MATCH (a:account)-[r:follows]->(b:account) RETURN a.handle; ``` ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------- Hash Join Hash Cond: (r.start = a.id) -> Seq Scan on follows r Filter: (("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid) AND (start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid)) -> Hash -> Seq Scan on account a ``` 조인 하나이고, `(b:account)`는 간선의 `"end"` 컬럼에 대한 범위 검사로만 남습니다. 이 생략을 끄면 두 번째 스캔과 조인이 돌아옵니다: ```cypher SET enable_graph_endpoint_elision = off; EXPLAIN (COSTS OFF) MATCH (a:account)-[r:follows]->(b:account) RETURN a.handle; RESET enable_graph_endpoint_elision; ``` ``` QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Hash Join Hash Cond: (r."end" = b.id) -> Hash Join Hash Cond: (r.start = a.id) -> Seq Scan on follows r Filter: ((start >= '3.0'::graphid) AND (start <= '3.281474976710655'::graphid) AND ("end" >= '3.0'::graphid) AND ("end" <= '3.281474976710655'::graphid)) -> Hash -> Seq Scan on account a -> Hash -> Seq Scan on account b ``` 교훈은 그 설정에 관한 것이 아닙니다. 필요하지 않은 끝점을 투영하면 매번 스캔 하나와 조인 하나의 비용이 듭니다. 그러므로 `RETURN`은 호출하는 쪽이 읽는 것으로 한정하십시오. {#performance-scan-pruning-from-ag_graphmeta} ### ag_graphmeta로 스캔 잘라 내기 `ag_graphmeta`는 그래프에 실제로 나타나는 (시작 레이블, 간선 레이블, 끝 레이블) 세 쌍마다 한 행을 담고, 그것을 이루는 간선이 몇 개인지도 함께 담습니다. 그것이 있으면 레이블 없는 패턴 끝점을 그 간선이 도달한다고 알려진 레이블로 좁힐 수 있습니다. 이 정보는 `auto_gather_graphmeta`가 켜져 있을 때만 유지되므로, 기본적으로 레이블 없는 끝점은 `ag_vertex` 계열 전체를 스캔합니다: ```cypher EXPLAIN (COSTS OFF) MATCH (n)-[r:follows]->(m) RETURN n, m; ``` ``` QUERY PLAN --------------------------------------------------- Hash Join Hash Cond: (m.id = r."end") -> Append -> Seq Scan on ag_vertex m_1 -> Seq Scan on account m_2 -> Hash -> Hash Join Hash Cond: (n.id = r.start) -> Append -> Seq Scan on ag_vertex n_1 -> Seq Scan on account n_2 -> Hash -> Seq Scan on follows r ``` 수집을 켜면 `Append` 둘이 접혀 사라집니다. 수집은 데이터베이스의 모든 그래프에 대해 기준선을 만들므로, 의도를 갖고 하십시오: ```cypher SET auto_gather_graphmeta = on; EXPLAIN (COSTS OFF) MATCH (n)-[r:follows]->(m) RETURN n, m; RESET auto_gather_graphmeta; ``` ``` QUERY PLAN ----------------------------------------- Hash Join Hash Cond: (r."end" = m.id) -> Hash Join Hash Cond: (r.start = n.id) -> Seq Scan on follows r -> Hash -> Seq Scan on account n -> Hash -> Seq Scan on account m ``` `follows`는 언제나 `account` → `account`로만 이어지므로, 레이블 없는 두 끝점은 계정일 수밖에 없습니다. 레이블이 많은 그래프에서는 이것이 끝점마다 스캔 하나와 레이블마다 스캔 하나의 차이입니다. 대가는 간선 쓰기마다 드는 유지 비용, 그리고 기준선이 무효화될 때마다 드는 재스캔입니다. 이 예제에는 레이블이 둘뿐이므로 여기서의 절약은 작습니다. 요점은 그 모양입니다. `pg_catalog.ag_graphmeta`가 그 개수를 담고 `ag_graph.graphmeta_valid`가 그 사용을 통제합니다. [그래프 메타데이터](../system_catalogs/graph-metadata.md)를 참고하십시오. 가장 단순한 대안은 패턴에 레이블을 이름으로 쓰는 것입니다. `MATCH (n:account)-[r:follows]->(m:account)`에는 카탈로그가 전혀 필요하지 않습니다. {#performance-forbidding-an-eager-plan} ### eager 계획 금지하기 `enable_eager = off`는 다른 실행 계획을 만들지 않습니다. 계획을 만들기를 거부합니다: ```cypher SET enable_eager = off; EXPLAIN (COSTS OFF) MATCH (a:account {handle: 'user42'}) SET a.seen = 1 WITH a MATCH (a)-[r:follows]->(b:account) RETURN count(b); RESET enable_eager; ``` ``` ERROR: eagerness plan is not allowed. ``` 작업 부하 안에서 어느 문장이 eagerness에 의존하는지 알아내는 데 쓰십시오. 이것을 끈 채로 운영하지는 마십시오. 그런 문장에서 eagerness는 비용의 선택이 아니라 정확성의 요구 조건이므로, 되돌아갈 더 값싼 계획이 없고 서버는 대신 오류를 냅니다. {#performance-resolution-to-a-promoted-column} ### 승격된 컬럼으로의 해석 `enable_property_promotion = off`은 타입 컬럼이 있는 곳에서도 속성 읽기를 `jsonb` 컬럼인 `properties`로 되돌려 보냅니다. 실행 계획은 바뀌고 답은 바뀌지 않습니다. 값이 양쪽에 다 있기 때문입니다. 컬럼을 지우지 않고도 예상하지 못한 실행 계획의 배후가 승격인지 확인하는 방법입니다. 전과 후는 [해석 기능 끄기](#typed-columns-turning-resolution-off)에 있습니다. {#performance-what-to-do-in-order} ## 무엇을 어떤 순서로 할까 1. **쿼리가 닿는 모든 레이블에 `ANALYZE`를 돌리십시오. 간선 레이블도 포함합니다.** 이것을 하기 전까지는 이 목록의 다른 어떤 것도 의미가 없습니다. 2. **고정점을 확인하십시오.** 순회에는 선택도가 높고 인덱스가 걸린 출발점 하나가 필요합니다. 그 출발점을 골라 내는 속성에 대해 `EXPLAIN`이 `Filter`가 아니라 `Index Cond`를 보여 줘야 합니다 — 어떤 조건이 그럴 수 있는지는 [인덱스와 제약](indexes-and-constraints.md)이 말해 줍니다. 3. **모든 가변 길이 패턴에 간선 레이블을 이름으로 쓰십시오.** 실행 계획에 나온 `Graph VLE on ag_edge`는 플래너가 아니라 쿼리를 가리킵니다. 4. 가변 길이 패턴의 **길이에 상한을 두십시오.** 5. **호출하는 쪽이 읽는 것만 투영하십시오.** 그러면 쓰지 않는 끝점이 생략됩니다. 6. 늘 필터·조인·정렬에 쓰는 **속성의 승격을 검토하십시오** — [속성을 컬럼으로 승격하기](#typed-columns-when-to-promote). 7. **그러고 나서 자원을 보십시오.** [구성](../operations/configuration.md). {#performance-clean-up} ## 정리 ```cypher DROP GRAPH perflab CASCADE; ``` ```sql DROP TABLE seed_account; DROP TABLE seed_follows; ``` {#performance-where-to-go-next} ## 다음에 갈 곳 * [인덱스와 제약](indexes-and-constraints.md) — 어떤 조건에 인덱스가 쓰일 수 있는지입니다. * [경로](#paths-what-a-path-costs) — 이 페이지의 순회 특화판입니다. * [속성을 컬럼으로 승격하기](typed-columns.md) — 읽기 자체가 비용일 때입니다. * [구성](../operations/configuration.md) — 메모리, I/O, autovacuum입니다.