{#create-property-index-create-property-index} # CREATE PROPERTY INDEX 레이블의 속성 하나 이상에 인덱스를 만듭니다. {#create-property-index-synopsis} ## 개요
CREATE [ UNIQUE ] PROPERTY INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] index_name ]
    ON [ ONLY ] label_name [ USING method ]
    ( { property | ( expression ) } [ COLLATE collation ] [ opclass ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ... ] )
    [ NULLS [ NOT ] DISTINCT ]
    [ WITH ( storage_parameter [= value] [, ... ] ) ]
    [ TABLESPACE tablespace_name ]
    [ WHERE condition ]

굵게 키워드 · 기울임 직접 채우는 자리 · [ ] 생략 가능 · ... 반복 가능. 구문 표기 규약을 참고하십시오.

{#create-property-index-description} ## 설명 이 문장 뒤에 별도의 인덱스 종류가 있는 것은 아닙니다. 속성 인덱스는 레이블의 `jsonb` 컬럼인 `properties`에서 그 속성을 읽는 *표현식*에 걸린 평범한 PostgreSQL 인덱스입니다. `CREATE PROPERTY INDEX ON movie (title)`은 키가 `(properties.'title')`인 btree를 만듭니다. `\d`가 그렇게 보고하며, 플래너가 그 인덱스를 쓰려면 조건을 그 형태로 볼 수 있어야 합니다. 이 문장의 제한은 그 표현식이 전부 설명합니다: - **인덱싱되는 값의 타입이 `jsonb`이므로,** 접근 방식에 `jsonb`용 기본 연산자 클래스가 있어야 합니다. 기본값인 `btree`, 그리고 `hash`와 `gin`에는 있습니다. `gist`·`spgist`·`brin`에는 없고, 그중 하나를 요구하면 `data type jsonb has no default operator class for access method "gist"`로 실패합니다. - **콜레이션을 적용할 수 없습니다.** `jsonb`에는 콜레이션이 없으므로, `properties` 읽기에 `COLLATE`를 쓰면 `collations are not supported by type jsonb`로 실패합니다. - **그 값은 결코 포함 컬럼이 될 수 없습니다.** `INCLUDE`는 `expressions are not supported in included columns`로 거부되고, 속성 읽기는 언제나 표현식이므로 `INCLUDE`가 그것에 적용되지 않습니다. **승격된** 속성에 걸린 인덱스는 다릅니다. 그때는 값이 표현식이 아니라 컬럼이기 때문입니다. `\d`는 `btree ((properties.'year'))`가 아니라 `btree (year)`로 보고합니다. 접근 방식, 연산자 클래스, 콜레이션은 그 컬럼 자신의 타입에만 제한되므로, `int` 컬럼에 `brin (year)`도, `text` 컬럼에 `btree (name COLLATE "C")`도 둘 다 됩니다. 애초에 언제 승격해야 하는지는 [속성을 컬럼으로 승격하기](../../querying/typed-columns.md)가 다룹니다. 규칙이 세 가지 더 있습니다: - 레이블은 `graph_path`가 지정한 그래프에서 찾습니다. 그 이름은 수식할 수 없습니다. - 부모 레이블에 걸린 인덱스는 그 레이블 자신의 행만 덮습니다. `ONLY`를 쓰든 쓰지 않든 자식 레이블은 인덱싱되지 않습니다. [레이블 상속](#label-inheritance-what-it-costs)을 참고하십시오. - `IF NOT EXISTS`는 검사할 이름이 필요하므로, 이름 없는 인덱스와 함께 쓸 수 없습니다. {#create-property-index-parameters} ## 매개변수 UNIQUE : 인덱싱되는 값이 이미 있는 값과 중복되는 행을 거부하며, 인덱스를 만들 때 이미 있는 행도 검사합니다. 고유성 규칙을 Cypher 자신의 어법으로 표현하는 방법은 [`CREATE CONSTRAINT`](create-constraint.md)를 참고하십시오. CONCURRENTLY : 그 레이블에 대한 쓰기를 잠그지 않고 인덱스를 만듭니다. 대가는 더 긴 생성 시간과 테이블 스캔 두 번입니다. IF NOT EXISTS index_name : 그 이름의 관계가 이미 있을 때 오류가 아니라 알림을 냅니다. 아무것도 만들어지지 않고, 이미 있는 인덱스가 요구한 그 인덱스인지 확인하는 것도 없습니다. index_name : 인덱스의 이름입니다. 생략하면 레이블과 인덱싱한 속성에서 이름이 유도됩니다. `movie`와 `year`는 `movie_year_idx`가 됩니다. ONLY : 받아들여지지만 평범한 레이블에는 효과가 없습니다. 파티션으로 재귀하는 것을 막는 것인데, 레이블은 파티션된 테이블이 아니라 상속 부모입니다. label_name : 행을 인덱싱할 레이블이며, `graph_path`가 지정한 그래프 안에서 찾습니다. USING method : 인덱스 접근 방식입니다. `properties`에서 읽는 속성에 대해서는 기본값인 `btree`, `hash`, `gin` 중에서 고릅니다. 승격된 컬럼에 대해서는 그 컬럼 타입에 기본 연산자 클래스가 있는 방식이면 무엇이든 됩니다. property : 그 레이블의 속성이며, Cypher가 쓰는 방식으로 적습니다. `title`이거나, 중첩된 맵 안의 키라면 `name.first`처럼 적습니다. ( expression ) : 그 레이블의 속성에 대한, 괄호로 감싼 Cypher 표현식입니다. 예를 들어 `(year / 10)`입니다. COLLATE collation : 인덱싱되는 값의 콜레이션입니다. 그 값이 콜레이션을 가질 수 있는 타입의 승격된 컬럼일 때만 쓸 수 있습니다. opclass : 인덱싱되는 값의 연산자 클래스입니다. 예를 들어 `gin` 인덱스에는 `jsonb_path_ops`입니다. 연산자 클래스 *매개변수*는 받아들여지지 않습니다. ASC | DESC | NULLS FIRST | NULLS LAST : 인덱스에 저장되는 정렬 순서입니다. 기본값은 `ASC`와 `NULLS LAST`이고, `DESC` 아래에서는 `NULLS FIRST`가 기본값입니다. NULLS [ NOT ] DISTINCT : 고유 인덱스에서 null 둘을 서로 다른 값으로 셀지 여부입니다. 기본값인 `NULLS DISTINCT`는 인덱스가 null을 여러 개 담을 수 있게 합니다. WITH ( storage_parameter [= value] [, ...] ) : 인덱스의 저장 매개변수입니다. btree의 `fillfactor`나 gin의 `fastupdate` 같은 것입니다. TABLESPACE tablespace_name : 인덱스의 테이블스페이스입니다. 이것이 없으면 `default_tablespace`가 결정합니다. WHERE condition : 부분 인덱스를 만듭니다. 그 조건이 받아들이는 행만 덮습니다. {#create-property-index-examples} ## 예제 예제는 [예제 데이터셋](../../get_started/example-dataset.md)을 쓰지 않고 자체 그래프인 `indexdemo`를 만들고, 끝에서 그것을 지웁니다. ```cypher CREATE GRAPH indexdemo; SET graph_path = indexdemo; CREATE VLABEL movie; CREATE VLABEL person; CREATE (:movie {title: 'Arrival', year: 2016, genre: 'Sci-Fi'}), (:movie {title: 'Dune', year: 2021, genre: 'Sci-Fi'}), (:movie {title: 'Gattaca', year: 1997, genre: 'Sci-Fi'}), (:movie {title: 'Sicario', year: 2015, genre: 'Thriller'}), (:movie {title: 'Prisoners', year: 2013, genre: 'Thriller'}), (:movie {title: 'Enemy', year: 2013, genre: 'Thriller'}); CREATE (:person {name: 'Denis Villeneuve'}), (:person {name: 'Roger Deakins'}); ``` {#create-property-index-index-a-property} ### 속성에 인덱스 걸기 ```cypher CREATE PROPERTY INDEX movie_title_idx ON movie (title); ``` ``` CREATE PROPERTY INDEX ``` 이것이 표현식에 걸린 btree를 만들었습니다: ```psql \d indexdemo.movie ``` ``` Table "indexdemo.movie" Column | Type | Collation | Nullable | Default ------------+---------+-----------+----------+----------------------------------------------------------------------------------------------- id | graphid | | not null | graphid(graph_labid('indexdemo.movie'::cstring), nextval('indexdemo.movie_id_seq'::regclass)) properties | jsonb | | not null | jsonb_build_object() Indexes: "movie_pkey" PRIMARY KEY, btree (id) "movie_title_idx" btree ((properties.'title')) Inherits: indexdemo.ag_vertex ``` `\dGi`는 같은 인덱스를 그것을 다시 만들 문장의 형태로 나열합니다: ```psql \dGi indexdemo.* ``` ``` List of property indexes Graph | LabelName | IndexName | Unique | Owner | Indexdef -----------+-----------+-----------------+--------+-------+-------------------------------------------------------------------- indexdemo | movie | movie_title_idx | f | agens | CREATE PROPERTY INDEX movie_title_idx ON movie USING btree (title) (1 row) ``` 플래너는 `title`에 대한 같음 비교를 그 표현식과 맞춥니다. 정점 여섯 개는 플래너가 인덱스 스캔을 선호하기에는 턱없이 적으므로, 아래 설정으로 그 계획을 보이게 합니다: ```cypher SET enable_seqscan = off; EXPLAIN (COSTS OFF) MATCH (m:movie {title: 'Dune'}) RETURN m.year; ``` ``` QUERY PLAN ------------------------------------------------------ Index Scan using movie_title_idx on movie m Index Cond: (properties.'title' = '"Dune"'::jsonb) (2 rows) ``` ```cypher RESET enable_seqscan; ``` {#create-property-index-let-the-index-name-itself} ### 인덱스 이름을 자동으로 정하기 ```cypher CREATE PROPERTY INDEX ON movie (year); ``` ```psql \dGi indexdemo.movie_year_idx ``` ``` List of property indexes Graph | LabelName | IndexName | Unique | Owner | Indexdef -----------+-----------+----------------+--------+-------+------------------------------------------------------------------ indexdemo | movie | movie_year_idx | f | agens | CREATE PROPERTY INDEX movie_year_idx ON movie USING btree (year) (1 row) ``` {#create-property-index-choose-an-access-method} ### 접근 방식 고르기 속성 읽기에 대해 btree의 대안은 `gin`과 `hash`입니다: ```cypher CREATE PROPERTY INDEX movie_genre_gin ON movie USING gin (genre); CREATE PROPERTY INDEX movie_genre_hash ON movie USING hash (genre); ``` ``` CREATE PROPERTY INDEX CREATE PROPERTY INDEX ``` 나머지 세 접근 방식은 AgensGraph와 무관한 이유로 안 됩니다. `jsonb`용 기본 연산자 클래스가 없기 때문입니다: ```cypher CREATE PROPERTY INDEX movie_genre_gist ON movie USING gist (genre); ``` ``` ERROR: data type jsonb has no default operator class for access method "gist" HINT: You must specify an operator class for the index or define a default operator class for the data type. ``` `spgist`와 `brin`도 같은 방식으로 실패합니다. {#create-property-index-reject-duplicate-values} ### 중복 값 거부하기 ```cypher CREATE UNIQUE PROPERTY INDEX movie_title_uniq ON movie (title); CREATE (:movie {title: 'Dune', year: 1984}); ``` ``` ERROR: duplicate key value violates unique constraint "movie_title_uniq" DETAIL: Key ((properties.'title'))=("Dune") already exists. ``` 그 메시지의 `Key`는 인덱스 표현식이며, 그것으로 고유 속성 인덱스를 컬럼에 걸린 고유성 제약과 구별합니다. {#create-property-index-index-part-of-a-label} ### 레이블의 일부에 인덱스 걸기 ```cypher CREATE PROPERTY INDEX movie_recent_idx ON movie (title) WHERE (year > 2010); ``` ``` CREATE PROPERTY INDEX ``` {#create-property-index-index-several-properties-and-an-expression} ### 속성 여러 개와 표현식에 인덱스 걸기 ```cypher CREATE PROPERTY INDEX movie_genre_year_idx ON movie (genre, year DESC NULLS LAST); CREATE PROPERTY INDEX movie_decade_idx ON movie ((year / 10)); CREATE PROPERTY INDEX movie_title_ops ON movie USING gin (title jsonb_path_ops); ``` 그 하나하나가 표현식 인덱스이며, `\d`가 무엇이 만들어졌는지 정확히 보여 줍니다. 산술 인덱스가 지니고 있는 Cypher 연산자까지 보여 줍니다: ```psql \d indexdemo.movie ``` ``` Table "indexdemo.movie" Column | Type | Collation | Nullable | Default ------------+---------+-----------+----------+----------------------------------------------------------------------------------------------- id | graphid | | not null | graphid(graph_labid('indexdemo.movie'::cstring), nextval('indexdemo.movie_id_seq'::regclass)) properties | jsonb | | not null | jsonb_build_object() Indexes: "movie_pkey" PRIMARY KEY, btree (id) "movie_decade_idx" btree (((properties.'year') `/` cypher_to_jsonb(10))) "movie_genre_gin" gin ((properties.'genre')) "movie_genre_hash" hash ((properties.'genre')) "movie_genre_year_idx" btree ((properties.'genre'), (properties.'year') DESC NULLS LAST) "movie_recent_idx" btree ((properties.'title')) WHERE (properties.'year') > cypher_to_jsonb(2010) "movie_title_idx" btree ((properties.'title')) "movie_title_ops" gin ((properties.'title') jsonb_path_ops) "movie_title_uniq" UNIQUE, btree ((properties.'title')) "movie_year_idx" btree ((properties.'year')) Inherits: indexdemo.ag_vertex ``` {#create-property-index-index-a-promoted-property} ### 승격된 속성에 인덱스 걸기 속성이 컬럼으로 승격된 레이블은 그 대신 컬럼에 인덱스를 겁니다: ```cypher CREATE VLABEL title (year int GENERATED, name text GENERATED); CREATE (:title {name: 'Arrival', year: 2016}), (:title {name: 'Dune', year: 2021}), (:title {name: 'Gattaca', year: 1997}); CREATE PROPERTY INDEX title_year_idx ON title (year); ``` ```psql \d indexdemo.title ``` ``` Table "indexdemo.title" Column | Type | Collation | Nullable | Default ------------+---------+-----------+----------+----------------------------------------------------------------------------------------------- id | graphid | | not null | graphid(graph_labid('indexdemo.title'::cstring), nextval('indexdemo.title_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 name | text | | | generated always as (ag_property_text(properties, 'name'::text, 's'::"char")) stored Indexes: "title_pkey" PRIMARY KEY, btree (id) "title_year_idx" btree (year) Inherits: indexdemo.ag_vertex ``` `btree ((properties.'year'))`가 아니라 `btree (year)`입니다. 실행 계획도 같게 읽히며, `jsonb`가 아니라 정수를 비교합니다: ```cypher SET enable_seqscan = off; EXPLAIN (COSTS OFF) MATCH (t:title) WHERE t.year > 2010 RETURN t.name; ``` ``` QUERY PLAN -------------------------------------------- Index Scan using title_year_idx on title t Index Cond: (year > 2010) (2 rows) ``` ```cypher RESET enable_seqscan; ``` 인덱싱되는 값이 컬럼이므로, 콜레이션과 연산자 클래스, 그리고 그 컬럼 타입이 지원하는 접근 방식이 모두 쓸 수 있게 됩니다: ```cypher CREATE PROPERTY INDEX title_name_coll ON title (name COLLATE "C"); CREATE PROPERTY INDEX title_name_pat ON title (name text_pattern_ops); CREATE PROPERTY INDEX title_year_brin ON title USING brin (year); ``` ```psql \d indexdemo.title ``` ``` Table "indexdemo.title" Column | Type | Collation | Nullable | Default ------------+---------+-----------+----------+----------------------------------------------------------------------------------------------- id | graphid | | not null | graphid(graph_labid('indexdemo.title'::cstring), nextval('indexdemo.title_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 name | text | | | generated always as (ag_property_text(properties, 'name'::text, 's'::"char")) stored Indexes: "title_pkey" PRIMARY KEY, btree (id) "title_name_coll" btree (name COLLATE "C") "title_name_pat" btree (name text_pattern_ops) "title_year_brin" brin (year) "title_year_idx" btree (year) Inherits: indexdemo.ag_vertex ``` 그 셋 중 어느 것도 `properties`에서 읽는 속성에 대해서는 받아들여지지 않습니다. {#create-property-index-refusals-worth-recognizing} ### 알아 둘 만한 거부 사례 `INCLUDE`는 승격 컬럼이든 아니든 아예 쓸 수 없습니다. 속성은 언제나 표현식으로 적히기 때문입니다: ```cypher CREATE PROPERTY INDEX title_name_incl ON title (name) INCLUDE (year); ``` ``` ERROR: expressions are not supported in included columns ``` 연산자 클래스 매개변수는 이 문장의 일부가 아닙니다: ```cypher CREATE PROPERTY INDEX movie_title_op ON movie USING gin (title jsonb_ops (fastupdate = off)); ``` ``` ERROR: syntax error at or near "(" LINE 1: ...ovie_title_op ON movie USING gin (title jsonb_ops (fastupdat... ^ ``` 중첩 경로는 그 속성이 맵을 담고 있을 때만 됩니다. 여기서 `person.name`은 문자열이므로, 그 안으로 들어가려 하면 인덱스를 만드는 중에 실패합니다: ```cypher CREATE PROPERTY INDEX person_first_idx ON person (name.first); ``` ``` ERROR: map or list is expected but scalar value ``` 그리고 이미 쓰이고 있는 이름은 교체되는 것이 아니라 보고됩니다: ```cypher CREATE PROPERTY INDEX IF NOT EXISTS movie_title_idx ON movie (title); ``` ``` NOTICE: relation "movie_title_idx" already exists, skipping CREATE PROPERTY INDEX ``` 마지막으로 예제가 만든 그래프를 지웁니다: ```cypher DROP GRAPH indexdemo CASCADE; ``` {#create-property-index-notes} ## 참고 `CONCURRENTLY`는 받아들여지고 `CREATE INDEX`에서와 같은 일을 합니다. 쓰기를 잠그지 않고 만듭니다. `ONLY`도 받아들여지지만 평범한 레이블에서는 아무것도 바꾸지 않습니다. 부모에 걸린 인덱스는 어느 쪽이든 부모의 행만 덮으므로, 계층에서는 스캔될 레이블마다 인덱스가 하나씩 필요합니다. {#create-property-index-how-a-promoted-key-resolves-here} ### 승격된 키가 여기서 어떻게 해석되는가 [승격된 속성](../../querying/typed-columns.md)이 있는 레이블에서는 `( ... )`의 두 형태가 다르게 해석됩니다: | 적는 것 | 인덱싱되는 것 | | --- | --- | | 맨 키를 쓴 `( year )` | 승격된 컬럼 `year` | | 표현식을 쓴 `( (lower(title)) )` | jsonb `properties`에 대한 `lower(properties.'title')` | 승격된 키에 대한 쿼리는 두 경우 모두 컬럼을 읽으므로, 쿼리가 쓸 수 있는 인덱스를 주는 것은 맨 키 형태입니다. 승격된 키에 대한 표현식에 인덱스를 걸려면 그 컬럼에 대한 SQL로 쓰십시오 — `CREATE INDEX ON g.film (lower(title))` — 그것도 `\dGi`가 여전히 나열해 줍니다. `\dGi`는 자기 `Indexdef` 컬럼을 Cypher 형태로 렌더링하므로 둘 다 키를 같게 표기합니다. 어떤 인덱스가 둘 중 무엇에 걸려 있는지 보려면 `pg_get_indexdef`를 읽거나 레이블 테이블에 `\d`를 쓰십시오. 거기서 맨 승격 키는 `btree (year)`로, 속성 형태는 `btree ((properties.'year'))`로 읽힙니다. {#create-property-index-compatibility} ## 호환성 {#create-property-index-isoiec-39075-gql} ### ISO/IEC 39075 (GQL) GQL에는 인덱스 DDL이 없습니다. 표준의 카탈로그 변경 문장은 스키마·그래프·그래프 타입(§12)에 한정됩니다. 거기에는 인덱스의 표준 표기가 없으므로, `CREATE PROPERTY INDEX`는 AgensGraph 자신의 것입니다. {#create-property-index-postgresql} ### PostgreSQL `CREATE PROPERTY INDEX`는 표현식에 대한 `CREATE INDEX`에 대응하고, `UNIQUE`, `CONCURRENTLY`, `USING`, `COLLATE`, 연산자 클래스, 정렬 순서, `NULLS [NOT] DISTINCT`, `WITH`, `TABLESPACE`, `WHERE`는 모두 거기서와 같은 뜻입니다. 다른 점이 세 가지 중요합니다. 인덱스 대상을 컬럼이나 괄호로 감싼 SQL 표현식이 아니라 Cypher 속성으로 적으므로, 파서가 `properties` 읽기를 대신 만들어 줍니다. `CREATE INDEX`가 받는 `INCLUDE`와 연산자 클래스 매개변수는 쓸 수 없습니다. 그리고 [`DROP PROPERTY INDEX`](drop-property-index.md)는 속성 인덱스만 받아들이는 반면, `DROP INDEX`는 레이블의 어떤 인덱스든, 속성 인덱스를 포함해 받아들입니다. {#create-property-index-see-also} ## 관련 항목 - [`DROP PROPERTY INDEX`](drop-property-index.md) - [`REINDEX VLABEL`·`REINDEX ELABEL`](reindex.md) - [`CREATE CONSTRAINT`](create-constraint.md) - [속성을 컬럼으로 승격하기](../../querying/typed-columns.md) - [그래프가 어떻게 저장되는가](#graph-on-postgresql-what-a-property-index-and-a-constraint-become)