쿼리 튜닝#
Cypher 쿼리는 PostgreSQL의 플래너가 계획합니다. 자기 규칙을 가진 별도의 그래프 엔진이 없으므로,
EXPLAIN이 동작하고, 통계가 중요하며, 이미 알고 있는 튜닝이 그대로 적용됩니다.
AgensGraph에 특유한 부분은 순회를 위한 몇 개의 실행 계획 노드, 새 그래프마다 반드시 걸리는 통계 문제 하나, 그리고 실행 계획의 모양을 바꾸는 설정 다섯 개입니다.
이 페이지는 실행 계획의 모양만 다룹니다. 자원 쪽 튜닝은 shared_buffers, work_mem,
effective_cache_size, 병렬성, autovacuum을 다루며, 그중 그래프에 특유한 것은 없고
구성에 있습니다.
측정할 그래프#
예제 데이터셋은 요소가 46개여서, 플래너가 흥미로운 결정을 내리기에는 부족합니다. 차수 분포가 있는 것을 만들고 끝에서 지웁니다:
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;
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);
LOAD FROM seed_follows AS r MATCH (a:account {n: r.src}), (b:account {n: r.dst}) CREATE (a)-[:follows]->(b);
ANALYZE perflab.account;
계정 20,000개와 follows 간선 120,000개입니다. 정점 레이블만 분석했습니다. 그 누락이
뒤에 나오는 절의 요점입니다.
Cypher의 EXPLAIN 읽기#
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에 있으면 행마다 평가되고 있는 것입니다. 둘 중 어느 쪽을 받는지가
인덱스 페이지의 주제입니다.
graphid 범위 검사는 레이블 검사입니다. start >= '3.0' AND start <= '3.281474976710655'은
“간선의 이쪽 끝에 있는 정점이 account다”라는 뜻입니다. 레이블 id는 graphid의 상위 비트에 있으므로
한 레이블의 id는 연속된 구간을 이루고, 그 검사는 간선 자신의 컬럼에서 답할 수 있습니다.
식별자: graphid를 참고하십시오.
두 번째 간선에 붙은 Filter: (r1.id <> id) 는 고유성 규칙입니다. 어떤 경로도 한 간선을 두 번
쓸 수 없습니다.
이름을 붙이지 않은 패턴 요소에는 합성 별칭이 주어지고, "<0000000002>" 처럼 출력됩니다. 그
뒤의 카운터는 문장마다 올라가므로, 그것이 들어 있는 실행 계획은 실행할 때마다 달라집니다. 기준선과
비교하려는 계획이라면 패턴의 모든 요소에 이름을 붙이십시오.
EXPLAIN (ANALYZE)도 동작하며, 추정치를 실제와 견주어 볼 수 있는 유일한 방법입니다:
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
참고
가변 길이 계획과 최단 경로 계획은 기본 텍스트 형식으로 읽으십시오. 셋 중
EXPLAIN (FORMAT JSON)으로도 계획이 제대로 읽히는 것은 Dijkstra입니다.
순회를 위한 실행 계획 노드#
길이가 고정된 패턴에는 특별한 노드가 필요하지 않습니다. 위에서 본 것처럼 스캔과 조인입니다. 아래 노드들은 SQL에 대응 연산자가 없는 것들을 위해 나타납니다.
Graph VLE는 가변 길이 간선이며, 간선 레이블과 범위를 이름으로 보여 줍니다:
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가 되고,
그것은 매 홉마다 그래프의 모든 간선 레이블을 훑습니다. 경로 쿼리에서 피할 수 있는 가장 큰 비용이
그것입니다. 비교는 경로에 있습니다.
Shortestpath VLE [min..max]는 shortestpath 또는 allshortestpaths이며, Hash2Side 노드 두 개
위에 놓입니다. 그 두 노드는 양쪽 끝에서 각각 자라나는 양방향 탐색의 두 전선입니다.
Dijkstra는 가중 탐색입니다. 그 자식이 확장에 쓰는 스캔이므로, 그것이 훑는 것은 간선 레이블의
start 컬럼에 걸린 인덱스입니다.
Graph Create, Graph Set, Graph Delete가 쓰기 노드입니다:
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 로 표시된 쓰기 노드는 그 위의 어떤 것이 실행되기 전에 끝나야 합니다. 그 위의 무언가가
그 쓰기가 바꾼 것을 읽기 때문입니다:
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")
같은 문장 안의 뒤따르는 읽기에 쓰기가 보이게 만드는 것이
이 eagerness입니다. 대가는 쓰기 출력의 구체화이고, 그 크기는 eager_mem으로 제한됩니다. MERGE는
항상 eager이며, 문장의 마지막 절이 아닌 SET·REMOVE·DELETE도 그렇습니다.
_agens_default_s로 시작하는 이름은 파이프라인 단계에 자동으로 붙은 별칭이고, 대략 WITH 하나에
하나씩입니다. 한 실행 계획 안에서는 안정적이며 튜닝할 대상이 아닙니다.
간선 레이블의 통계#
새 그래프에서 가장 먼저 확인할 것이 이것이고, 어떤 인덱스보다 값어치가 큽니다.
순회의 비용은 차수의 곱입니다. 출발한 정점에서 나가는 간선이 몇 개인지, 도달한 정점에서 나가는
간선이 몇 개인지, 그런 식으로 이어집니다. 간선 레이블의 start와 end 컬럼에 대한 통계가 그
차수를 설명합니다. AgensGraph는 레이블을 만들 때 두 컬럼의 통계 목표를 최댓값인 1,000으로
설정합니다:
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 |
목표는 표본이 아닙니다. 무언가가 그 레이블을 분석하기 전까지는 아무것도 없습니다:
SELECT count(*) AS stats_rows FROM pg_stats WHERE schemaname = 'perflab' AND tablename = 'follows';
stats_rows
------------
0
그리고 추정치는 짐작입니다. 갓 적재한 그래프에서 두 홉을 세어 보면 이렇습니다:
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개 전체에 대한 해시입니다. 이제 간선 레이블을 분석합니다:
ANALYZE perflab.follows;
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은 정확히 맞습니다:
MATCH (a:account {handle: 'user42'})-[r1:follows]->(b:account)-[r2:follows]->(c:account) RETURN count(c) AS pairs;
pairs
-------
36
여기서 따라오는 규칙은 이것입니다. 대량 적재 뒤에는 정점 레이블만이 아니라 간선 레이블에도
ANALYZE를 돌리십시오. autovacuum도 언젠가는 하지만, 그 “언젠가”는 첫 사용자들이 이미 그 쿼리를
실행한 뒤입니다. 허브 정점이 들어와 차수의 모양이 바뀐 그래프라면, 행 수 임계치를 기다리지 말고
명시적으로 다시 분석할 만합니다.
실행 계획의 모양을 바꾸는 설정#
그래프 쿼리가 어떻게 계획되는지에 영향을 주는 설정이 다섯 개 있습니다. 아래는 pg_settings에서
읽은 실제 값입니다:
설정 |
기본값 |
범위 |
하는 일 |
|---|---|---|---|
|
|
세션 |
끄면, eager 계획이 필요한 문장은 계획되지 않고 거부됩니다. |
|
4 MB |
세션 |
eager 노드가 디스크로 넘기기 전에 쓸 수 있는 메모리입니다. |
|
|
세션 |
쿼리가 값을 전혀 읽지 않는 패턴 끝점의 스캔을 없앱니다. |
|
|
세션 |
승격된 타입 컬럼이 있으면 Cypher의 속성 읽기를 그 컬럼으로 해석합니다. |
|
|
슈퍼유저 |
|
참고
postgresql.conf.sample은 enable_eager와 enable_multiple_update의 기본값을 틀리게 적어
놓았습니다. 둘 다 켜져 있습니다. 샘플 파일이 아니라 pg_settings를 읽으십시오.
이 중 어느 것도 운영에서 돌릴 손잡이가 아닙니다. 네 개는 이미 켜져 있고, 다섯째는 카탈로그를 유지할지에 대한 결정입니다. 진단을 위해 알아 두십시오. 하나를 끄면 그것이 예상하지 못한 실행 계획의 원인인지 알 수 있습니다.
끝점 생략#
쿼리가 값을 전혀 읽지 않는 끝점은 아예 스캔되지 않습니다. 여기서 b는 바인딩되고 레이블도 붙었지만
투영되지 않습니다:
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" 컬럼에 대한 범위 검사로만 남습니다. 이 생략을 끄면
두 번째 스캔과 조인이 돌아옵니다:
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은 호출하는 쪽이 읽는 것으로 한정하십시오.
ag_graphmeta로 스캔 잘라 내기#
ag_graphmeta는 그래프에 실제로 나타나는 (시작 레이블, 간선 레이블, 끝 레이블) 세 쌍마다 한 행을
담고, 그것을 이루는 간선이 몇 개인지도 함께 담습니다. 그것이 있으면 레이블 없는 패턴 끝점을 그
간선이 도달한다고 알려진 레이블로 좁힐 수 있습니다. 이 정보는 auto_gather_graphmeta가 켜져 있을
때만 유지되므로, 기본적으로 레이블 없는 끝점은 ag_vertex 계열 전체를 스캔합니다:
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 둘이 접혀 사라집니다. 수집은 데이터베이스의 모든 그래프에 대해 기준선을
만들므로, 의도를 갖고 하십시오:
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가 그 사용을 통제합니다.
그래프 메타데이터를 참고하십시오.
가장 단순한 대안은 패턴에 레이블을 이름으로 쓰는 것입니다.
MATCH (n:account)-[r:follows]->(m:account)에는 카탈로그가 전혀 필요하지 않습니다.
eager 계획 금지하기#
enable_eager = off는 다른 실행 계획을 만들지 않습니다. 계획을 만들기를 거부합니다:
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는 비용의 선택이 아니라 정확성의 요구 조건이므로, 되돌아갈 더 값싼 계획이 없고 서버는 대신 오류를 냅니다.
승격된 컬럼으로의 해석#
enable_property_promotion = off은 타입 컬럼이 있는 곳에서도 속성 읽기를 jsonb 컬럼인 properties로 되돌려
보냅니다. 실행 계획은 바뀌고 답은 바뀌지 않습니다. 값이 양쪽에 다 있기 때문입니다. 컬럼을 지우지
않고도 예상하지 못한 실행 계획의 배후가 승격인지 확인하는 방법입니다. 전과 후는
해석 기능 끄기에 있습니다.
무엇을 어떤 순서로 할까#
쿼리가 닿는 모든 레이블에
ANALYZE를 돌리십시오. 간선 레이블도 포함합니다. 이것을 하기 전까지는 이 목록의 다른 어떤 것도 의미가 없습니다.고정점을 확인하십시오. 순회에는 선택도가 높고 인덱스가 걸린 출발점 하나가 필요합니다. 그 출발점을 골라 내는 속성에 대해
EXPLAIN이Filter가 아니라Index Cond를 보여 줘야 합니다 — 어떤 조건이 그럴 수 있는지는 인덱스와 제약이 말해 줍니다.모든 가변 길이 패턴에 간선 레이블을 이름으로 쓰십시오. 실행 계획에 나온
Graph VLE on ag_edge는 플래너가 아니라 쿼리를 가리킵니다.가변 길이 패턴의 길이에 상한을 두십시오.
호출하는 쪽이 읽는 것만 투영하십시오. 그러면 쓰지 않는 끝점이 생략됩니다.
늘 필터·조인·정렬에 쓰는 속성의 승격을 검토하십시오 — 속성을 컬럼으로 승격하기.
그러고 나서 자원을 보십시오. 구성.
정리#
DROP GRAPH perflab CASCADE;
DROP TABLE seed_account;
DROP TABLE seed_follows;
다음에 갈 곳#
인덱스와 제약 — 어떤 조건에 인덱스가 쓰일 수 있는지입니다.
경로 — 이 페이지의 순회 특화판입니다.
속성을 컬럼으로 승격하기 — 읽기 자체가 비용일 때입니다.
구성 — 메모리, I/O, autovacuum입니다.