속성을 컬럼으로 승격하기#

레이블이 스키마 없는 것은 속성이 요소마다 하나씩 있는 jsonb 문서 안에 있기 때문입니다. 그 대가로 두 가지를 치릅니다.

m.year를 읽는 것은 쿼리가 닿는 모든 행에 대해 JSON 문서 안에서 year 키를 찾는 일입니다. 그리고 그 읽기가 내놓는 값은 jsonb 값이므로, 그것에 걸린 인덱스는 jsonb에 걸린 인덱스입니다. 결코 integer나 vector에 걸린 인덱스가 아닙니다.

승격은 고른 키 하나에 대해 그 두 비용을 없앱니다. ALTER VLABEL film ADD COLUMN year int GENERATED는 year 속성에 실제 integer 컬럼을 주고, 그 컬럼은 properties 옆에 저장되며 서버가 유지합니다. 또한 Cypher 컴파일러가 f.year를 그 컬럼에서 답하도록 가르칩니다.

쿼리는 바뀌지 않습니다. 속성에 관한 것도 아무것도 바뀌지 않습니다. 여전히 속성이고, 레이블은 다른 모든 키에 대해 여전히 스키마가 없으며, 승격은 되돌릴 수 있습니다.

이 페이지는 그 교환이 언제 할 만한지, 그리고 그것으로 무엇을 감당하게 되는지를 다룹니다. DDL 구문은 ALTER VLABEL과 CREATE VLABEL에 있습니다.

승격을 시험할 자기 레이블#

승격은 레이블의 모양을 바꾸는 일이므로, 매뉴얼이 공유하는 그래프가 아니라 사본에서 작업하십시오. 레이블 테이블에 대한 LOAD FROM은 그 properties를 그대로 옮겨 줍니다:

CREATE GRAPH promolab;
SET graph_path = promolab;
CREATE VLABEL film;
LOAD FROM moviekb.movie AS r CREATE (:film = r.properties);

이렇게 하면 예제 데이터셋의 영화 18편을 마음대로 모양을 바꿀 수 있는 그래프에 갖게 됩니다. 이 페이지 전체가 거기서 실행되고 끝에서 그것을 지웁니다.

승격이 하는 일#

승격 전에는 year에 대한 필터가 properties 안으로 들어갑니다. EXPLAIN (VERBOSE)가 속성 읽기를 properties.'key'로 보여 줍니다:

EXPLAIN (VERBOSE, COSTS OFF) MATCH (f:film) WHERE f.year > 2010 RETURN f.title;
                   QUERY PLAN                    
-------------------------------------------------
 Seq Scan on promolab.film f
   Output: f.properties.'title'
   Filter: (f.properties.'year' > '2010'::jsonb)

그것을 승격합니다:

ALTER VLABEL film ADD COLUMN year int GENERATED;
\d promolab.film
                                                            Table "promolab.film"
   Column   |  Type   | Collation | Nullable |                                            Default                                            
------------+---------+-----------+----------+-----------------------------------------------------------------------------------------------
 id         | graphid |           | not null | graphid(graph_labid('promolab.film'::cstring), nextval('promolab.film_id_seq'::regclass))
 properties | jsonb   |           | not null | jsonb_build_object()
 year       | integer |           |          | generated always as (ag_property_text(properties, 'year'::text, 'n'::"char")::integer) stored
Indexes:
    "film_pkey" PRIMARY KEY, btree (id)
Inherits: promolab.ag_vertex

이 컬럼은 GENERATED ALWAYS AS ... STORED입니다. properties에서 키 하나를 꺼내는 표현식을 가진 평범한 PostgreSQL 생성 컬럼입니다. ag_property_text(properties, 'year', 'n')은 값이 그 컬럼이 담을 수 있는 종류일 때만 그 키의 텍스트를 반환합니다. 'n'은 숫자, 's'는 문자열, 'b'는 불리언입니다.

나머지는 integer로의 형 변환이 합니다. 아무도 이 컬럼에 쓰지 않습니다. 서버가 삽입과 갱신마다 계산합니다.

같은 쿼리가 이제 컬럼을 대상으로 필터합니다:

EXPLAIN (VERBOSE, COSTS OFF) MATCH (f:film) WHERE f.year > 2010 RETURN f.title;
           QUERY PLAN           
--------------------------------
 Seq Scan on promolab.film f
   Output: f.properties.'title'
   Filter: (f.year > 2010)

year만 승격했으므로 f.title은 여전히 properties 읽기입니다. 답은 그대로입니다:

MATCH (f:film) WHERE f.year > 2010 RETURN f.title AS title, f.year AS year ORDER BY year;
           title            | year 
----------------------------+------
 "The Avengers"             | 2012
 "Elysium"                  | 2013
 "Interstellar"             | 2014
 "Blade Runner 2049"        | 2017
 "The Matrix Resurrections" | 2021

기존 행은 ALTER의 일부로 채워지므로, 컬럼은 생기는 순간 이미 완전합니다. 그리고 값은 양쪽에 있습니다. properties도 여전히 그것을 담고 있습니다:

SELECT properties->>'title' AS title, year, properties->>'year' AS in_the_bag
FROM promolab.film ORDER BY year LIMIT 3;
     title      | year | in_the_bag 
----------------+------+------------
 Blade Runner   | 1982 | 1982
 The Terminator | 1984 | 1984
 Total Recall   | 1990 | 1990

이 중복은 의도한 것입니다. 속성은 그대로 속성이므로, properties를 읽는 클라이언트는 늘 보던 것을 봅니다. 덤프는 데이터를 properties에 한 번 담고 컬럼은 다시 생성합니다. 컬럼을 지워도 잃는 것이 없습니다. 대가는 저장 공간입니다. 그 값을 두 번 치르는 것입니다.

속성 키에서 컬럼으로의 대응은 ag_label_property에 기록됩니다:

SELECT l.labname, p.propname, a.attname, format_type(a.atttypid, a.atttypmod) AS type
FROM pg_catalog.ag_label l
  JOIN pg_catalog.ag_label_property p ON p.laboid = l.oid
  JOIN pg_attribute a ON a.attrelid = l.relid AND a.attnum = p.attnum
WHERE l.relid = 'promolab.film'::regclass;
 labname | propname | attname |  type   
---------+----------+---------+---------
 film    | year     | year    | integer

이 행이 읽기를 컬럼으로 해석되게 만듭니다. 이것이 없으면 컬럼은 존재하지만 Cypher가 그것을 알지 못합니다. 긴 형태는 아무것도 등록하지 않는다를 참고하십시오.

승격된 속성 읽기는 컬럼의 타입을 갖는다#

Cypher의 RETURN은 속성 읽기를 jsonb로 돌려주므로 RETURN f.title은 "The Matrix"를 따옴표와 함께 출력합니다. 승격된 키를 읽는 것이 그 예외입니다. 컬럼에 선언된 타입으로 돌아옵니다. 아래에서 year는 승격됐고 title은 아닙니다.

SELECT pg_typeof(t) AS title_type, pg_typeof(y) AS year_type
FROM (MATCH (f:film) RETURN f.title AS t, f.year AS y LIMIT 1) q;
 title_type | year_type 
------------+-----------
 jsonb      | integer

값과 순서는 어느 쪽이든 같지만, 클라이언트가 받는 타입은 같지 않습니다. Cypher의 모든 컬럼이 JSON으로 올 것이라 기대하는 드라이버는 여기서 integer를 보게 됩니다.

보통 이것은 단순화입니다. 평범한 값으로 가는 가장 짧은 길이기 때문입니다. 그러나 결과의 모양이 바뀌는 것이므로, 그것을 받아 쓰는 쪽과 맞는지 확인할 만합니다.

승격된 컬럼에 인덱스 걸기#

승격된 키에 대한 CREATE PROPERTY INDEX는 properties에 대한 표현식 인덱스가 아니라 그 컬럼에 대한 평범한 btree를 만듭니다. 플래너는 다른 컬럼 인덱스와 똑같이 그것을 씁니다. 그 인덱스가 어떤 조건을 처리할 수 있는지, 그리고 승격 뒤에 나빠지는 두 조건이 무엇인지는 인덱스와 제약이 측정해 놓았습니다. 짧게 적으면 이렇습니다:

  • 같음 비교, 범위, ORDER BY가 처리되고, ORDER BY는 인덱스 전용 스캔으로 나아집니다.

  • IS NULL이 인덱스로 처리 가능해집니다. properties에서는 그렇지 않았습니다.

  • IN과 STARTS WITH는 인덱스를 쓰지 않게 됩니다. jsonb 피연산자와 만나려고 컬럼이 다시 jsonb로 감싸이기 때문입니다.

여기서 규칙 하나가 따라오고, 놓치기 쉽습니다. 키를 승격하는 순간 properties에 걸린 인덱스는 죽습니다. 더 이상 아무것도 properties.'year'로 컴파일되지 않으므로, 그 표현식에 걸린 인덱스는 결코 맞지 않습니다. 그런데도 쓰기마다 계속 유지됩니다. 승격은 항상 두 단계입니다. 승격하고, 그다음 컬럼에 인덱스를 다시 만듭니다.

레이블과 함께 승격 컬럼 선언하기#

레이블을 만들 때는 컬럼을 레이블 정의 안에 씁니다:

CREATE VLABEL doc (words int GENERATED, tag text GENERATED (category));

words int GENERATED는 words 속성을 같은 이름의 컬럼으로 승격합니다. 괄호를 쓴 형태는 다른 키를 승격합니다. tag text GENERATED (category)는 “컬럼 tag를 속성 category에서 채운다”는 뜻입니다. 원하는 키가 컬럼 이름으로 쓸 수 없거나 적절하지 않을 때 쓰십시오.

카탈로그는 컬럼 이름이 아니라 키를 기록합니다:

SELECT l.labname, p.propname, a.attname, format_type(a.atttypid, a.atttypmod) AS type
FROM pg_catalog.ag_label l
  JOIN pg_catalog.ag_label_property p ON p.laboid = l.oid
  JOIN pg_attribute a ON a.attrelid = l.relid AND a.attnum = p.attnum
WHERE l.relid = 'promolab.doc'::regclass ORDER BY p.propname;
 labname | propname | attname |  type   
---------+----------+---------+---------
 doc     | category | tag     | text
 doc     | words    | words   | integer

그래서 그 컬럼으로 해석되는 Cypher 읽기는 키인 d.category입니다. d.tag는 아무도 설정한 적 없는 속성입니다:

CREATE (:doc {title: 'a note', words: 12, category: 'news'});
MATCH (d:doc) RETURN d.words AS words, d.category AS category, d.tag AS tag;
 words | category | tag 
-------+----------+-----
    12 | news     | 

ALTER ELABEL은 간선 속성을 같은 방식으로 승격합니다. 순회에 쓰는 가중치라면 해 둘 만합니다.

확장 타입#

컬럼의 타입은 확장에서 올 수도 있습니다. 승격이 미세 최적화 이상이 되는 경우가 바로 이것입니다. 속성으로 저장된 임베딩은 JSON 배열이므로, 모든 유사도 쿼리가 그것을 형 변환해야 합니다. 매번, 모든 행에 대해 p.emb::vector(4)를 해야 합니다. 승격하면 그 형 변환이 쓰기 시점에 한 번만 일어납니다:

CREATE EXTENSION IF NOT EXISTS vector;
CREATE VLABEL page (emb vector(4) GENERATED);
\d promolab.page
                                                           Table "promolab.page"
   Column   |   Type    | Collation | Nullable |                                          Default                                          
------------+-----------+-----------+----------+-------------------------------------------------------------------------------------------
 id         | graphid   |           | not null | graphid(graph_labid('promolab.page'::cstring), nextval('promolab.page_id_seq'::regclass))
 properties | jsonb     |           | not null | jsonb_build_object()
 emb        | vector(4) |           |          | generated always as (((properties ->> 'emb'::text))::vector(4)) stored
Indexes:
    "page_pkey" PRIMARY KEY, btree (id)
Inherits: promolab.ag_vertex

생성 표현식을 보십시오. JSON에 대응물이 없는 타입에 대해서는 서버가 ag_property_text 대신 평범한 ->>를 쓰고, 그 텍스트를 받아들일지 거부할지는 타입 자신의 입력 함수에 맡깁니다. vector(4)는 대괄호 안의 숫자 네 개가 아닌 것은 모두 거부합니다.

그러면 그 컬럼에 실제 벡터 인덱스를 걸 수 있습니다. 인덱스 정의에 형 변환 표현식이 필요하지 않습니다. 남아 있는 형 변환이 없기 때문입니다. 먼저 인덱스를 고를 만한 행 수를 적재합니다:

CREATE TABLE seed_page AS
SELECT 'page ' || g AS title, jsonb_build_array(random(), random(), random(), random()) AS emb
FROM generate_series(1, 5000) g;
LOAD FROM seed_page AS r CREATE (:page {title: r.title, emb: r.emb});
CREATE PROPERTY INDEX ON page USING hnsw (emb vector_cosine_ops);
ANALYZE promolab.page;

그리고 Cypher 유사도 검색이 그것을 사용합니다:

EXPLAIN (COSTS OFF)
MATCH (p:page) RETURN p.title AS title
ORDER BY p.emb <=> '[0.1,0.2,0.3,0.4]'::vector(4) LIMIT 3;
                         QUERY PLAN                         
------------------------------------------------------------
 Limit
   ->  Index Scan using page_emb_idx on page p
         Order By: (emb <=> '[0.1,0.2,0.3,0.4]'::vector(4))

같은 쿼리의 승격 전 형태는 모든 행에 대해 거리를 계산하고 그것을 정렬해야 합니다:

SET enable_property_promotion = off;
EXPLAIN (COSTS OFF)
MATCH (p:page) RETURN p.title AS title
ORDER BY p.emb::vector(4) <=> '[0.1,0.2,0.3,0.4]'::vector(4) LIMIT 3;
RESET enable_property_promotion;
                                        QUERY PLAN                                        
------------------------------------------------------------------------------------------
 Limit
   ->  Sort
         Sort Key: (((p.properties.'emb')::vector(4) <=> '[0.1,0.2,0.3,0.4]'::vector(4)))
         ->  Seq Scan on page p

승격된 컬럼에 ::vector(4)를 그대로 남겨 두어도 거부되지 않고 접혀 사라지므로, 기존 쿼리는 계속 동작하고 인덱스도 계속 씁니다.

((emb::vector(4)) vector_cosine_ops) 같은 형 변환 표현식에 속성 인덱스를 거는 것보다 승격을 택하십시오. 승격된 컬럼에 걸린 인덱스는 보통 컬럼 인덱스이고, 덤프·복원을 거쳐도 더 오래가는 형태입니다. 벡터 검색을 참고하십시오.

승격의 대가#

승격된 키는 더 이상 스키마에서 자유롭지 않다#

이것이 진짜 교환입니다. 레이블의 다른 모든 키는 여전히 어떤 JSON 값이든 받습니다. 승격된 키는 그 컬럼이 담을 수 있는 것만 받고, 맞지 않는 쓰기는 중단됩니다:

CREATE (:film {title: 'Bad Year', year: 'nineteen ninety nine'});
ERROR:  property "year" holds a string
DETAIL:  A column promoting this property holds numbers.
CONTEXT:  while computing the promoted typed columns of graph label "film"
CREATE (:film {title: 'Fractional', year: 1999.5});
ERROR:  invalid input syntax for type integer: "1999.5"
CONTEXT:  while computing the promoted typed columns of graph label "film"

여기서 서로 다른 검사 두 가지가 보입니다. 첫째는 종류 검사입니다. 문자열은 무엇이라고 적혀 있든 숫자가 아닙니다. 둘째는 컬럼 타입 자신의 검사입니다. 확장 타입에는 둘째만 적용됩니다:

CREATE (:page {title: 'wrong dims', emb: [1, 2, 3]});
ERROR:  expected 4 dimensions, not 3
CONTEXT:  while computing the promoted typed columns of graph label "page"

그 키를 아예 빼는 것은 언제나 괜찮습니다. 없는 속성은 null 컬럼을 만들며, 없는 속성이 null로 읽히는 것과 정확히 같습니다:

CREATE (:film {title: 'Undated'});
MATCH (f:film {title: 'Undated'}) RETURN f.year AS year;
 year 
------
     

그러므로 승격은 속도에 관한 결정이면서 그만큼 데이터 품질에 관한 결정입니다. 운영 중인 레이블의 키를 승격하기 전에, 기존 값이 모두 들어맞는지 확인하십시오. ALTER는 이미 있는 행에도 같은 검사를 적용하고, 맞지 않는 첫 행에서 실패합니다:

CREATE VLABEL city;
CREATE (:city {name: 'Seoul', pop: 9700000}), (:city {name: 'Nowhere', pop: 'many'});
ALTER VLABEL city ADD COLUMN pop bigint GENERATED;
ERROR:  property "pop" holds a string
DETAIL:  A column promoting this property holds numbers.

이 실패는 유용한 종류입니다. 아무것도 커밋되기 전에 그 레이블의 데이터가 생각과 다르다는 것을 알려 줍니다.

ALTER는 테이블을 다시 쓴다#

저장되는 생성 컬럼을 추가하면 레이블의 힙을 다시 쓰고, 그동안 ACCESS EXCLUSIVE 락을 잡습니다. 18행짜리 레이블에서는 즉시 끝납니다. 큰 레이블에서는 다른 ALTER TABLE 재작성과 마찬가지로 계획을 세우십시오. 실제로 그런지 보고 싶다면 관계의 파일 번호를 확인하십시오:

SELECT relfilenode FROM pg_class WHERE oid = 'promolab.film'::regclass;

ALTER 전의 번호와 그 뒤의 번호가 다릅니다.

승격된 컬럼은 자기 콜레이션을 가질 수 없다#

속성 맵은 문자열을 데이터베이스 기본 콜레이션으로 비교합니다. 다른 콜레이션이 붙은 컬럼은 같은 값을 다르게 정렬하고 다르게 비교합니다. 그러면 “이 읽기에 둘 중 어느 쪽이 답하는가”가 속도에 관한 결정이 아니라 결과에 관한 결정이 됩니다.

그래서 거부되며, 콜레이션이 직접 적혀 있지 않고 도메인을 통해 들어오는 경우도 마찬가지입니다:

CREATE DOMAIN sortkey AS text COLLATE "C";
CREATE VLABEL note;
ALTER VLABEL note ADD COLUMN k sortkey GENERATED;
ERROR:  a promoted property cannot have a collation of its own
DETAIL:  Column "k" is collated "C", while a property map compares strings with the database default.
HINT:  Leave the collation unspecified, or use a type that does not carry one.

Cypher 쿼리 위의 뷰는 컬럼을 붙잡아 둔다#

뷰는 한 번 컴파일되며, 어떤 속성을 컬럼으로 해석한 Cypher 쿼리 위의 뷰는 그 컬럼에 대한 의존성을 갖습니다:

CREATE VIEW recent_films AS
SELECT title #>> '{}' AS title, year
FROM (MATCH (f:film) WHERE f.year > 2010 RETURN f.title AS title, f.year AS year) q;
ALTER VLABEL film DROP COLUMN year;
ERROR:  cannot drop column year of table promolab.film because other objects depend on it
DETAIL:  view recent_films depends on column year of table promolab.film
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

이것이 옳습니다. 그렇지 않으면 뷰가 조용히 properties를 읽기 시작할 것입니다. 다만 그 말은 뷰가 하나의 약속이라는 뜻입니다. 뷰를 지우고, 컬럼을 지우고, 뷰를 다시 만드십시오. 이 의존성은 pg_get_viewdef에서 보이고, 거기 나오는 컴파일된 SQL이 f.year를 이름으로 부릅니다. Cypher 쿼리 위의 뷰는 어떻게 보이는가를 참고하십시오.

DROP VIEW recent_films;

표현식 속성 인덱스는 properties에 걸린다#

맨 키와, 표현식 안에 든 같은 키는 CREATE PROPERTY INDEX에서 다르게 해석됩니다. 맨 키는 승격된 컬럼이 됩니다. 그 키에 대한 표현식은 jsonb properties에 대한 표현식이 됩니다:

ALTER VLABEL film ADD COLUMN title text GENERATED;
CREATE PROPERTY INDEX film_title_idx ON film (title);
CREATE PROPERTY INDEX film_lower_idx ON film ((lower(title)));
SELECT c.relname, pg_get_indexdef(c.oid) AS real_definition
FROM pg_class c JOIN pg_index i ON i.indexrelid = c.oid
WHERE i.indrelid = 'promolab.film'::regclass AND c.relname LIKE 'film_%idx'
ORDER BY 1;
    relname     |                                   real_definition
----------------+--------------------------------------------------------------------------------------
 film_lower_idx | CREATE INDEX film_lower_idx ON promolab.film USING btree (lower(properties.'title'))
 film_title_idx | CREATE INDEX film_title_idx ON promolab.film USING btree (title)

두 경우 모두 쿼리는 컬럼을 읽으므로, 쿼리가 쓸 수 있는 인덱스를 주는 것은 맨 키 형태입니다.

\dGi는 둘 다 Cypher 형태로 보고하며, 둘 다 title로 표기합니다:

\dGi promolab.*
                                                       List of property indexes
  Graph   | LabelName |   IndexName    | Unique | Owner |                                  Indexdef
----------+-----------+----------------+--------+-------+-----------------------------------------------------------------------------
 promolab | film      | film_lower_idx | f      | agens | CREATE PROPERTY INDEX film_lower_idx ON film USING btree ((lower((title))))
 promolab | film      | film_title_idx | f      | agens | CREATE PROPERTY INDEX film_title_idx ON film USING btree (title)

그러므로 어떤 레이블의 인덱스가 둘 중 무엇에 걸려 있는지 알고 싶으면 pg_get_indexdef를 읽으십시오.

승격된 키에 인덱스를 거는 방법은 두 가지입니다. 맨 키로 걸면 쿼리가 그 컬럼을 직접 씁니다. 또는 표현식 자체에 인덱스를 걸어야 한다면, 컬럼에 대한 SQL로 쓰십시오. 그것이 쿼리가 읽는 것과 맞습니다:

CREATE INDEX film_lower_col_idx ON promolab.film (lower(title));

어느 쪽이든, 어떤 키를 승격하기 전에 그 키를 읽는 표현식 인덱스를 지우고, 위 두 형태 중 하나로 다시 만드십시오.

긴 형태는 키 하나가 아니면 아무것도 등록하지 않는다#

긴 방식으로 쓴 생성 컬럼은 평범한 생성 컬럼입니다. 그 표현식이 properties에서 키 하나를 꺼내는 것과 정확히 같을 때만 승격으로 인식됩니다. (properties ->> 'key')::type, 또는 덤프가 내보내는 ag_property_text 형태입니다. 키 하나보다 많은 것에서 끌어냈거나 계산한 것은 어떤 대응도 등록하지 않습니다:

CREATE VLABEL post;
ALTER VLABEL post ADD COLUMN body_len int GENERATED ALWAYS AS (length(properties->>'body')) STORED;
CREATE (:post {body: 'hello there'});

컬럼은 있고 SQL은 그것을 읽을 수 있습니다:

SELECT id, body_len, properties FROM promolab.post;
 id  | body_len |       properties        
-----+----------+-------------------------
 8.1 |       11 | {"body": "hello there"}

Cypher는 읽을 수 없습니다:

MATCH (p:post) RETURN p.body_len AS body_len;
 body_len 
----------
 

p.body_len은 body_len이라는 이름의 속성을 읽는데 그런 속성을 가진 글은 없고, 그 컬럼이 값이 될 수 있는 키도 없습니다. 그래서 승격되지 않았는데도 승격된 것처럼 보입니다. 승격에는 GENERATED 축약형을 쓰고, SQL에서만 읽으려는 파생 컬럼에는 긴 형태를 쓰십시오.

해석 기능 끄기#

enable_property_promotion은 Cypher의 속성 읽기가 컬럼으로 해석되는지를 제어합니다. 기본적으로 켜져 있습니다. 끄면 같은 쿼리가 properties로 돌아갑니다:

SET enable_property_promotion = off;
EXPLAIN (COSTS OFF) MATCH (f:film) WHERE f.year > 2010 RETURN f.title;
RESET enable_property_promotion;
                  QUERY PLAN                   
-----------------------------------------------
 Seq Scan on film f
   Filter: (properties.'year' > '2010'::jsonb)

그리고 같은 답을 냅니다. properties가 여전히 그 값을 담고 있기 때문입니다. 이 설정의 목적이 그것입니다. 컬럼을 지우지 않고도, 예상하지 못한 실행 계획의 원인이 승격인지 확인하는 것입니다. 이 설정이 컬럼의 유지를 멈추지는 않습니다.

승격 되돌리기#

ALTER VLABEL film DROP COLUMN year;

컬럼과 그 카탈로그 행이 사라지고, 그것에 걸린 인덱스도 함께 사라지며, 읽기는 같은 결과를 내면서 properties로 되돌아갑니다:

MATCH (f:film) WHERE f.year > 2010 RETURN count(f) AS films;
 films 
-------
 5

잃는 것은 없습니다. 컬럼에만 저장된 것은 처음부터 없었기 때문입니다. 먼저 정리해야 할 것은 그 컬럼을 붙잡아 둔 뷰뿐입니다.

언제 승격할까#

네 가지가 모두 참일 때 키를 승격하십시오:

  • 쿼리가 그 키로 필터하거나 조인하거나 정렬한다. 단순히 투영하는 것이 아니어야 합니다. 반환만 하는 키는 얻는 것이 없습니다. properties 읽기가 이미 그것을 얻는 가장 값싼 방법입니다.

  • 값이 균일하다. 어떤 행은 숫자를, 어떤 행은 문자열을 담고 있다면 ALTER가 거부하며, 그것들을 한 타입으로 밀어 넣는 것은 튜닝 변경이 아니라 데이터 변경입니다.

  • 값 자신의 타입에 인덱스를 걸고 싶다 — 실제 integer에 대한 범위, 인덱스 전용 ORDER BY, vector에 대한 HNSW 인덱스 같은 것입니다.

  • 레이블이 읽기 비용이 문제가 될 만큼 크다. 18행에서는 여기 있는 어느 것도 측정되지 않습니다.

모든 정점에 나타난다는 이유로 키를 승격하지는 마십시오. 균일성은 전제 조건이지 이유가 아닙니다. 그리고 나빠지는 두 조건 IN과 STARTS WITH를 애플리케이션이 실제로 보내는 것과 맞춰 확인하십시오.

정리#

DROP GRAPH promolab CASCADE;
DROP TABLE seed_page;
DROP DOMAIN sortkey;

다음에 갈 곳#

  • 인덱스와 제약 — 승격 전과 후의 인덱스 처리 가능성 표입니다.

  • 쿼리 튜닝 — 실행 계획을 좌우하는 다른 선택들 사이에서 승격이 놓이는 자리입니다.

  • 벡터 검색 — 승격된 컬럼이 있을 때와 없을 때의 임베딩입니다.