굵게 키워드 · 기울임 직접 채우는 자리 · [ ] 생략 가능 · ... 반복 가능. 구문 표기 규약을 참고하십시오.
{#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)