{#typed-columns-promoting-a-property-to-a-column} # 속성을 컬럼으로 승격하기 레이블이 스키마 없는 것은 속성이 요소마다 하나씩 있는 `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`](../reference/ddl/alter-label.md)과 [`CREATE VLABEL`](../reference/ddl/create-vlabel.md)에 있습니다. {#typed-columns-a-label-of-your-own-to-promote-on} ## 승격을 시험할 자기 레이블 승격은 레이블의 모양을 바꾸는 일이므로, 매뉴얼이 공유하는 그래프가 아니라 사본에서 작업하십시오. 레이블 테이블에 대한 `LOAD FROM`은 그 `properties`를 그대로 옮겨 줍니다: ```cypher CREATE GRAPH promolab; SET graph_path = promolab; CREATE VLABEL film; LOAD FROM moviekb.movie AS r CREATE (:film = r.properties); ``` 이렇게 하면 [예제 데이터셋](../get_started/example-dataset.md)의 영화 18편을 마음대로 모양을 바꿀 수 있는 그래프에 갖게 됩니다. 이 페이지 전체가 거기서 실행되고 끝에서 그것을 지웁니다. {#typed-columns-what-promotion-does} ## 승격이 하는 일 승격 전에는 `year`에 대한 필터가 `properties` 안으로 들어갑니다. `EXPLAIN (VERBOSE)`가 속성 읽기를 `properties.'key'`로 보여 줍니다: ```cypher 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) ``` 그것을 승격합니다: ```cypher ALTER VLABEL film ADD COLUMN year int GENERATED; ``` ```psql \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`로의 형 변환이 합니다. 아무도 이 컬럼에 쓰지 않습니다. 서버가 삽입과 갱신마다 계산합니다. 같은 쿼리가 이제 컬럼을 대상으로 필터합니다: ```cypher 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` 읽기입니다. 답은 그대로입니다: ```cypher 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`도 여전히 그것을 담고 있습니다: ```sql 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`에 기록됩니다: ```sql 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가 그것을 알지 못합니다. [긴 형태는 아무것도 등록하지 않는다](#typed-columns-the-long-form-registers-nothing-unless-it-is-a-single-key)를 참고하십시오. {#typed-columns-a-promoted-read-has-the-columns-type} ### 승격된 속성 읽기는 컬럼의 타입을 갖는다 Cypher의 `RETURN`은 속성 읽기를 jsonb로 돌려주므로 `RETURN f.title`은 `"The Matrix"`를 따옴표와 함께 출력합니다. 승격된 키를 읽는 것이 그 예외입니다. 컬럼에 선언된 타입으로 돌아옵니다. 아래에서 `year`는 승격됐고 `title`은 아닙니다. ```cypher 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`를 보게 됩니다. 보통 이것은 단순화입니다. [평범한 값](#combining-sql-and-cypher-getting-a-plain-value-out)으로 가는 가장 짧은 길이기 때문입니다. 그러나 결과의 모양이 바뀌는 것이므로, 그것을 받아 쓰는 쪽과 맞는지 확인할 만합니다. {#typed-columns-indexing-a-promoted-column} ## 승격된 컬럼에 인덱스 걸기 승격된 키에 대한 `CREATE PROPERTY INDEX`는 `properties`에 대한 표현식 인덱스가 아니라 그 컬럼에 대한 평범한 btree를 만듭니다. 플래너는 다른 컬럼 인덱스와 똑같이 그것을 씁니다. 그 인덱스가 어떤 조건을 처리할 수 있는지, 그리고 승격 뒤에 *나빠지는* 두 조건이 무엇인지는 [인덱스와 제약](#indexes-and-constraints-what-promotion-changes)이 측정해 놓았습니다. 짧게 적으면 이렇습니다: * 같음 비교, 범위, `ORDER BY`가 처리되고, `ORDER BY`는 인덱스 전용 스캔으로 나아집니다. * `IS NULL`이 인덱스로 처리 가능해집니다. `properties`에서는 그렇지 않았습니다. * `IN`과 `STARTS WITH`는 인덱스를 쓰지 않게 됩니다. jsonb 피연산자와 만나려고 컬럼이 다시 jsonb로 감싸이기 때문입니다. 여기서 규칙 하나가 따라오고, 놓치기 쉽습니다. **키를 승격하는 순간 `properties`에 걸린 인덱스는 죽습니다.** 더 이상 아무것도 `properties.'year'`로 컴파일되지 않으므로, 그 표현식에 걸린 인덱스는 결코 맞지 않습니다. 그런데도 쓰기마다 계속 유지됩니다. 승격은 항상 두 단계입니다. 승격하고, 그다음 컬럼에 인덱스를 다시 만듭니다. {#typed-columns-declaring-promoted-columns-with-the-label} ## 레이블과 함께 승격 컬럼 선언하기 레이블을 만들 때는 컬럼을 레이블 정의 안에 씁니다: ```cypher CREATE VLABEL doc (words int GENERATED, tag text GENERATED (category)); ``` `words int GENERATED`는 `words` 속성을 같은 이름의 컬럼으로 승격합니다. 괄호를 쓴 형태는 *다른* 키를 승격합니다. `tag text GENERATED (category)`는 "컬럼 `tag`를 속성 `category`에서 채운다"는 뜻입니다. 원하는 키가 컬럼 이름으로 쓸 수 없거나 적절하지 않을 때 쓰십시오. 카탈로그는 컬럼 이름이 아니라 키를 기록합니다: ```sql 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`는 아무도 설정한 적 없는 속성입니다: ```cypher 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`은 간선 속성을 같은 방식으로 승격합니다. 순회에 쓰는 가중치라면 해 둘 만합니다. {#typed-columns-extension-types} ## 확장 타입 컬럼의 타입은 확장에서 올 수도 있습니다. 승격이 미세 최적화 이상이 되는 경우가 바로 이것입니다. 속성으로 저장된 임베딩은 JSON 배열이므로, 모든 유사도 쿼리가 그것을 형 변환해야 합니다. 매번, 모든 행에 대해 `p.emb::vector(4)`를 해야 합니다. 승격하면 그 형 변환이 쓰기 시점에 한 번만 일어납니다: ```sql CREATE EXTENSION IF NOT EXISTS vector; ``` ```cypher CREATE VLABEL page (emb vector(4) GENERATED); ``` ```psql \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)`는 대괄호 안의 숫자 네 개가 아닌 것은 모두 거부합니다. 그러면 그 컬럼에 실제 벡터 인덱스를 걸 수 있습니다. 인덱스 정의에 형 변환 표현식이 필요하지 않습니다. 남아 있는 형 변환이 없기 때문입니다. 먼저 인덱스를 고를 만한 행 수를 적재합니다: ```sql 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; ``` ```cypher 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); ``` ```sql ANALYZE promolab.page; ``` 그리고 Cypher 유사도 검색이 그것을 사용합니다: ```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)) ``` 같은 쿼리의 승격 전 형태는 모든 행에 대해 거리를 계산하고 그것을 정렬해야 합니다: ```cypher 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)` 같은 *형 변환 표현식*에 속성 인덱스를 거는 것보다 승격을 택하십시오. 승격된 컬럼에 걸린 인덱스는 보통 컬럼 인덱스이고, 덤프·복원을 거쳐도 더 오래가는 형태입니다. [벡터 검색](../guides/search/vector.md)을 참고하십시오. {#typed-columns-what-promotion-costs-you} ## 승격의 대가 {#typed-columns-a-promoted-key-is-no-longer-schemaless} ### 승격된 키는 더 이상 스키마에서 자유롭지 않다 이것이 진짜 교환입니다. 레이블의 다른 모든 키는 여전히 어떤 JSON 값이든 받습니다. 승격된 키는 그 컬럼이 담을 수 있는 것만 받고, 맞지 않는 쓰기는 중단됩니다: ```cypher 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" ``` ```cypher 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" ``` 여기서 서로 다른 검사 두 가지가 보입니다. 첫째는 종류 검사입니다. 문자열은 무엇이라고 적혀 있든 숫자가 아닙니다. 둘째는 컬럼 타입 자신의 검사입니다. 확장 타입에는 둘째만 적용됩니다: ```cypher 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로 읽히는 것과 정확히 같습니다: ```cypher CREATE (:film {title: 'Undated'}); MATCH (f:film {title: 'Undated'}) RETURN f.year AS year; ``` ``` year ------ ``` 그러므로 승격은 속도에 관한 결정이면서 그만큼 데이터 품질에 관한 결정입니다. 운영 중인 레이블의 키를 승격하기 전에, 기존 값이 모두 들어맞는지 확인하십시오. `ALTER`는 이미 있는 행에도 같은 검사를 적용하고, 맞지 않는 첫 행에서 실패합니다: ```cypher CREATE VLABEL city; CREATE (:city {name: 'Seoul', pop: 9700000}), (:city {name: 'Nowhere', pop: 'many'}); ``` ```cypher ALTER VLABEL city ADD COLUMN pop bigint GENERATED; ``` ``` ERROR: property "pop" holds a string DETAIL: A column promoting this property holds numbers. ``` 이 실패는 유용한 종류입니다. 아무것도 커밋되기 전에 그 레이블의 데이터가 생각과 다르다는 것을 알려 줍니다. {#typed-columns-the-alter-rewrites-the-table} ### ALTER는 테이블을 다시 쓴다 저장되는 생성 컬럼을 추가하면 레이블의 힙을 다시 쓰고, 그동안 `ACCESS EXCLUSIVE` 락을 잡습니다. 18행짜리 레이블에서는 즉시 끝납니다. 큰 레이블에서는 다른 `ALTER TABLE` 재작성과 마찬가지로 계획을 세우십시오. 실제로 그런지 보고 싶다면 관계의 파일 번호를 확인하십시오: ```sql SELECT relfilenode FROM pg_class WHERE oid = 'promolab.film'::regclass; ``` `ALTER` 전의 번호와 그 뒤의 번호가 다릅니다. {#typed-columns-a-promoted-column-cannot-have-a-collation-of-its-own} ### 승격된 컬럼은 자기 콜레이션을 가질 수 없다 속성 맵은 문자열을 데이터베이스 기본 콜레이션으로 비교합니다. 다른 콜레이션이 붙은 컬럼은 같은 값을 다르게 정렬하고 다르게 비교합니다. 그러면 "이 읽기에 둘 중 어느 쪽이 답하는가"가 속도에 관한 결정이 아니라 결과에 관한 결정이 됩니다. 그래서 거부되며, 콜레이션이 직접 적혀 있지 않고 도메인을 통해 들어오는 경우도 마찬가지입니다: ```sql CREATE DOMAIN sortkey AS text COLLATE "C"; ``` ```cypher 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. ``` {#typed-columns-a-view-over-a-cypher-query-pins-the-column} ### Cypher 쿼리 위의 뷰는 컬럼을 붙잡아 둔다 뷰는 한 번 컴파일되며, 어떤 속성을 컬럼으로 해석한 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; ``` ```cypher 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 쿼리 위의 뷰는 어떻게 보이는가](#combining-sql-and-cypher-a-view-over-a-cypher-query)를 참고하십시오. ```sql DROP VIEW recent_films; ``` {#typed-columns-an-expression-property-index-is-built-over-the-bag} ### 표현식 속성 인덱스는 `properties`에 걸린다 맨 키와, 표현식 안에 든 같은 키는 `CREATE PROPERTY INDEX`에서 다르게 해석됩니다. 맨 키는 승격된 컬럼이 됩니다. 그 키에 대한 표현식은 jsonb `properties`에 대한 표현식이 됩니다: ```cypher 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))); ``` ```sql 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`로 표기합니다: ```psql \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로 쓰십시오. 그것이 쿼리가 읽는 것과 맞습니다: ```sql CREATE INDEX film_lower_col_idx ON promolab.film (lower(title)); ``` 어느 쪽이든, 어떤 키를 승격하기 전에 그 키를 읽는 표현식 인덱스를 지우고, 위 두 형태 중 하나로 다시 만드십시오. {#typed-columns-the-long-form-registers-nothing-unless-it-is-a-single-key} ### 긴 형태는 키 하나가 아니면 아무것도 등록하지 않는다 긴 방식으로 쓴 생성 컬럼은 평범한 생성 컬럼입니다. 그 표현식이 `properties`에서 키 하나를 꺼내는 것과 정확히 같을 때만 승격으로 인식됩니다. `(properties ->> 'key')::type`, 또는 덤프가 내보내는 `ag_property_text` 형태입니다. 키 하나보다 많은 것에서 끌어냈거나 계산한 것은 어떤 대응도 등록하지 않습니다: ```cypher CREATE VLABEL post; ALTER VLABEL post ADD COLUMN body_len int GENERATED ALWAYS AS (length(properties->>'body')) STORED; CREATE (:post {body: 'hello there'}); ``` 컬럼은 있고 SQL은 그것을 읽을 수 있습니다: ```sql SELECT id, body_len, properties FROM promolab.post; ``` ``` id | body_len | properties -----+----------+------------------------- 8.1 | 11 | {"body": "hello there"} ``` Cypher는 읽을 수 없습니다: ```cypher MATCH (p:post) RETURN p.body_len AS body_len; ``` ``` body_len ---------- ``` `p.body_len`은 `body_len`이라는 이름의 속성을 읽는데 그런 속성을 가진 글은 없고, 그 컬럼이 값이 될 수 있는 키도 없습니다. 그래서 승격되지 않았는데도 승격된 것처럼 보입니다. 승격에는 `GENERATED` 축약형을 쓰고, SQL에서만 읽으려는 파생 컬럼에는 긴 형태를 쓰십시오. {#typed-columns-turning-resolution-off} ## 해석 기능 끄기 `enable_property_promotion`은 Cypher의 속성 읽기가 컬럼으로 해석되는지를 제어합니다. 기본적으로 켜져 있습니다. 끄면 같은 쿼리가 `properties`로 돌아갑니다: ```cypher 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`가 여전히 그 값을 담고 있기 때문입니다. 이 설정의 목적이 그것입니다. 컬럼을 지우지 않고도, 예상하지 못한 실행 계획의 원인이 승격인지 확인하는 것입니다. 이 설정이 컬럼의 유지를 멈추지는 않습니다. {#typed-columns-undoing-a-promotion} ## 승격 되돌리기 ```cypher ALTER VLABEL film DROP COLUMN year; ``` 컬럼과 그 카탈로그 행이 사라지고, 그것에 걸린 인덱스도 함께 사라지며, 읽기는 같은 결과를 내면서 `properties`로 되돌아갑니다: ```cypher MATCH (f:film) WHERE f.year > 2010 RETURN count(f) AS films; ``` ``` films ------- 5 ``` 잃는 것은 없습니다. 컬럼에만 저장된 것은 처음부터 없었기 때문입니다. 먼저 정리해야 할 것은 그 컬럼을 붙잡아 둔 뷰뿐입니다. {#typed-columns-when-to-promote} ## 언제 승격할까 네 가지가 모두 참일 때 키를 승격하십시오: * **쿼리가 그 키로 필터하거나 조인하거나 정렬한다.** 단순히 투영하는 것이 아니어야 합니다. 반환만 하는 키는 얻는 것이 없습니다. `properties` 읽기가 이미 그것을 얻는 가장 값싼 방법입니다. * **값이 균일하다.** 어떤 행은 숫자를, 어떤 행은 문자열을 담고 있다면 `ALTER`가 거부하며, 그것들을 한 타입으로 밀어 넣는 것은 튜닝 변경이 아니라 데이터 변경입니다. * **값 자신의 타입에 인덱스를 걸고 싶다** — 실제 `integer`에 대한 범위, 인덱스 전용 `ORDER BY`, `vector`에 대한 HNSW 인덱스 같은 것입니다. * **레이블이 읽기 비용이 문제가 될 만큼 크다.** 18행에서는 여기 있는 어느 것도 측정되지 않습니다. 모든 정점에 나타난다는 이유로 키를 승격하지는 마십시오. 균일성은 전제 조건이지 이유가 아닙니다. 그리고 나빠지는 두 조건 `IN`과 `STARTS WITH`를 애플리케이션이 실제로 보내는 것과 맞춰 확인하십시오. {#typed-columns-clean-up} ## 정리 ```cypher DROP GRAPH promolab CASCADE; ``` ```sql DROP TABLE seed_page; DROP DOMAIN sortkey; ``` {#typed-columns-where-to-go-next} ## 다음에 갈 곳 * [인덱스와 제약](indexes-and-constraints.md) — 승격 전과 후의 인덱스 처리 가능성 표입니다. * [쿼리 튜닝](performance.md) — 실행 계획을 좌우하는 다른 선택들 사이에서 승격이 놓이는 자리입니다. * [벡터 검색](../guides/search/vector.md) — 승격된 컬럼이 있을 때와 없을 때의 임베딩입니다.