SQL を用いたスキーマレスなグラフ

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410176/6ac71aba-2267-c330-b1be-46290a3e7781.png)

以前に [この記事](https://qiita.com/ryotayamanaka/items/1dbbe3269e5101f3cd33) で紹介した通り、SQL の現在の仕様（SQL:2023）には、表データをベースにしたグラフの定義とそれに対するパターンマッチングの記法が含まれています。

私の周りでは、これを機にグラフ・データベースからリレーショナル・データベースに移行を検討するケースが見受けられます。その理由として多く聞かれるのは、ネイティブのグラフ・データベースの拡張性や管理性に関する懸念で、このようなユーザーは既にグラフ・データベースをプロダクションで使っていると考えられます。

上のような懸念がリレーショナル・データベースのプロパティ・グラフ機能（以下では SQL グラフと呼ぶことにします）で解決できるのか気になるところですが、今後、事例が出てくるだろうと思います。

## グラフ・データベースはスキーマレス？

そんな動きの中で、私が度々受ける質問のひとつは「SQL グラフはグラフ・データベースのスキーマレスな性質を持っていないのではないか」というものです。これを理由に「SQL グラフはグラフ・データベースとして認めない」と考えることもありますし、より現実的な視点で移行に問題がないことを検証されている方もいます。

スキーマレスな性質には以下のような期待があるようです。

- 新たなタイプのノードやエッジを追加する際に、スキーマ変更が必要ない
- ノードやエッジに新たなプロパティを追加する際に、スキーマ変更が必要ない

これらの性質は SQL グラフでは通常想定されておらず、代わりに以下で説明するようなスキーマ変更が必要になります。

## SQL グラフではスキーマ変更が必要

SQL グラフは表データをベースとしたビューであるため、新たなタイプのノード／エッジや新たなプロパティを追加の際にはスキーマ変更が必要になります。まずはこの使い方を見ておきましょう。

次の公式ドキュメントに掲載されているグラフの例を簡略化したものを graph0 として作成することにします。まず初めに、人物の友人関係を表すために、person 表と friend_of 表を用意して、それらの表をベースにしたグラフを作成します。

* [Examples for SQL Graph Queries](https://docs.oracle.com/en/database/oracle/property-graph/24.3/spgdg/examples-sql-property-graph-queries.html#GUID-9FD9FAA6-EC7F-48AD-82B7-D9E4553895F3)

```sql:ベースとなる表の作成
CREATE TABLE person (
    id NUMBER,
    name VARCHAR2(10),
    birth_date DATE
);

INSERT INTO person VALUES (1, 'John', to_date('13/06/1963', 'DD/MM/YYYY'));
INSERT INTO person VALUES (2, 'Mary', to_date('25/09/1982', 'DD/MM/YYYY'));
INSERT INTO person VALUES (3, 'Bob', to_date('11/03/1966', 'DD/MM/YYYY'));
INSERT INTO person VALUES (4, 'Alice', to_date('01/02/1987', 'DD/MM/YYYY'));

CREATE TABLE friend_of (
    id NUMBER GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1),
    person_a NUMBER,
    person_b NUMBER,
    meeting_date DATE
);

INSERT INTO friend_of (person_a, person_b, meeting_date) VALUES (1, 3, to_date('01/09/2000', 'DD/MM/YYYY'));
INSERT INTO friend_of (person_a, person_b, meeting_date) VALUES (2, 4, to_date('19/09/2000', 'DD/MM/YYYY'));
INSERT INTO friend_of (person_a, person_b, meeting_date) VALUES (2, 1, to_date('19/09/2000', 'DD/MM/YYYY'));
INSERT INTO friend_of (person_a, person_b, meeting_date) VALUES (3, 2, to_date('10/07/2001', 'DD/MM/YYYY'));
```
```:SQL グラフの作成
CREATE PROPERTY GRAPH graph0
  VERTEX TABLES (
    person
      KEY (id)
      LABEL person
      PROPERTIES (id, name, birth_date)
  )
  EDGE TABLES (
    friend_of
      KEY (id)
      SOURCE KEY (person_a) REFERENCES person (id)
      DESTINATION KEY (person_b) REFERENCES person (id)
      LABEL friend_of
      PROPERTIES (meeting_date)
  );
```

### 新しいタイプのノードとエッジの追加

ここでは、人物の所属大学の情報を加えるため、大学ノードと学生であるというエッジを追加します。表の作成とグラフの更新といったスキーマの変更が必要になることがわかります。

```sql:ノードとエッジの追加
CREATE TABLE university (
    id NUMBER,
    name VARCHAR2(10)
);

INSERT INTO university VALUES (1, 'ABC');
INSERT INTO university VALUES (2, 'XYZ');

CREATE TABLE student_of (
    id NUMBER GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1),
    university_id NUMBER,
    person_id NUMBER,
    subject VARCHAR2(10)
);

INSERT INTO student_of (university_id, person_id, subject) VALUES (1, 1, 'Arts');
INSERT INTO student_of (university_id, person_id, subject) VALUES (1, 3, 'Music');
INSERT INTO student_of (university_id, person_id, subject) VALUES (2, 2, 'Math');
INSERT INTO student_of (university_id, person_id, subject) VALUES (2, 4, 'Science');
```
```:SQL グラフの更新
CREATE OR REPLACE PROPERTY GRAPH graph0
  VERTEX TABLES (
    person
      KEY (id)
      LABEL person
      PROPERTIES (name, birth_date),
    university
      KEY (id)
      LABEL university
      PROPERTIES (name)
  )
  EDGE TABLES (
    friend_of
      KEY (id)
      SOURCE KEY (person_a) REFERENCES person (id)
      DESTINATION KEY (person_b) REFERENCES person (id)
      LABEL friend_of
      PROPERTIES (meeting_date),
    student_of
      KEY (id)
      SOURCE KEY (person_id) REFERENCES person (id)
      DESTINATION KEY (university_id) REFERENCES university (id)
      LABEL student_of
      PROPERTIES (subject)
  );
```

### 新しいプロパティの追加

さらに、各人物に身長の情報を加えます。この際も、表への列の追加とそれに伴うグラフ定義の更新といった、スキーマ変更が必要になります。

```sql:プロパティの追加
ALTER TABLE person ADD (height FLOAT);

UPDATE person SET height = 1.80 WHERE id = 1;
UPDATE person SET height = 1.65 WHERE id = 2;
UPDATE person SET height = 1.75 WHERE id = 3;
UPDATE person SET height = 1.70 WHERE id = 4;
```
```:SQL グラフの更新
CREATE OR REPLACE PROPERTY GRAPH graph0
  VERTEX TABLES (
    person
      KEY (id)
      LABEL person
      PROPERTIES (name, birth_date, height),
    university
      KEY (id)
      LABEL university
      PROPERTIES (name)
  )
  EDGE TABLES (
    friend_of
      KEY (id)
      SOURCE KEY (person_a) REFERENCES person (id)
      DESTINATION KEY (person_b) REFERENCES person (id)
      LABEL friend_of
      PROPERTIES (meeting_date),
    student_of
      KEY (id)
      SOURCE KEY (person_id) REFERENCES person (id)
      DESTINATION KEY (university_id) REFERENCES university (id)
      LABEL student_of
      PROPERTIES (subject)
  );
```



## スキーマレスな使い方

上記の通り、SQL グラフはリレーショナル・データベースのスキーマの上に定義されるものです。その一方で、スキーマレスなグラフを使いたいというケースも多く見受けられ、例えば、他のグラフ・データベースから移行する場合や、文書データから知識グラフを構築する場合などがあります。そこで、ここでは、SQL グラフを用いてスキーマレスなグラフを扱う方法を紹介します。

まず、全てのノードを格納するノード表と全てのエッジを格納するエッジ表を作成します。柔軟にプロパティを格納するために、props という JSON 列を持たせています。

```sql:ノード表
CREATE TABLE graph1node (
  id VARCHAR2(255)
, label VARCHAR2(255)
, props JSON
, CONSTRAINT graph1node_pk PRIMARY KEY (id)
);
```
```sql:エッジ表
CREATE TABLE graph1edge (
  id VARCHAR2(255)
, src VARCHAR2(255)
, dst VARCHAR2(255)
, label VARCHAR2(255)
, props JSON
, CONSTRAINT graph1edge_pk PRIMARY KEY (id)
, CONSTRAINT graph1edge_fk_src FOREIGN KEY (src) REFERENCES graph1node(id)
, CONSTRAINT graph1edge_fk_dst FOREIGN KEY (dst) REFERENCES graph1node(id)
);
```

これらの表の上に SQL グラフを作成します。ここで、全てのノードとエッジのラベルは node と edge になるので、本来ラベルとして格納したい情報（例えば person や friend_of）は label プロパティとして保持されます。JSON 列も props プロパティとなります。

```
CREATE PROPERTY GRAPH graph1
  VERTEX TABLES (
    graph1node
      KEY (id)
      LABEL node
      PROPERTIES (id, label, props)
  )
  EDGE TABLES (
    graph1edge
      KEY (id)
      SOURCE KEY(src) REFERENCES graph1node(id)
      DESTINATION KEY(dst) REFERENCES graph1node(id)
      LABEL edge
      PROPERTIES (id, label, src, dst, props)
  );
```

ノード表とエッジ表に対して通常の INSERT 文でノードとエッジを追加することができます。ここでは上の例と同様に person ノードと friend_of エッジを追加しています。

```sql
INSERT INTO graph1node VALUES ('p1', 'person', '{"name":"John", "birth_date":"1963-06-13"}');
INSERT INTO graph1node VALUES ('p2', 'person', '{"name":"Mary", "birth_date":"1982-09-25"}');
INSERT INTO graph1node VALUES ('p3', 'person', '{"name":"Bob", "birth_date":"1966-03-11"}');
INSERT INTO graph1node VALUES ('p4', 'person', '{"name":"Alice", "birth_date":"1987-02-01"}');

INSERT INTO graph1edge VALUES (sys_guid(), 'p1', 'p3', 'friend_of', '{"meeting_date":"2000-09-01"}');
INSERT INTO graph1edge VALUES (sys_guid(), 'p2', 'p4', 'friend_of', '{"meeting_date":"2000-09-19"}');
INSERT INTO graph1edge VALUES (sys_guid(), 'p2', 'p1', 'friend_of', '{"meeting_date":"2000-09-19"}');
INSERT INTO graph1edge VALUES (sys_guid(), 'p3', 'p2', 'friend_of', '{"meeting_date":"2001-07-10"}');
```

### 新しいタイプのノードとエッジの追加

この場合、university ノードや student_of エッジを作成する場合に、表やグラフの定義を変更する必要はありません。

```sql
INSERT INTO graph1node VALUES ('u1', 'university', '{"name":"ABC University"}');
INSERT INTO graph1node VALUES ('u2', 'university', '{"name":"XYZ University"}');

INSERT INTO graph1edge VALUES (sys_guid(), 'p1', 'u1', 'student_of', '{"meeting_date":"2000-09-01"}');
INSERT INTO graph1edge VALUES (sys_guid(), 'p3', 'u1', 'student_of', '{"meeting_date":"2000-09-19"}');
INSERT INTO graph1edge VALUES (sys_guid(), 'p2', 'u2', 'student_of', '{"meeting_date":"2000-09-19"}');
INSERT INTO graph1edge VALUES (sys_guid(), 'p4', 'u2', 'student_of', '{"meeting_date":"2001-07-10"}');
```

プロパティを追加する場合にも、JSON に新しい要素を追加するだけなので、スキーマの変更は必要ありません。

```sql
UPDATE graph1node SET props = JSON_TRANSFORM(props, INSERT '$.height' = 1.80) WHERE id = 'p1';
UPDATE graph1node SET props = JSON_TRANSFORM(props, INSERT '$.height' = 1.65) WHERE id = 'p2';
UPDATE graph1node SET props = JSON_TRANSFORM(props, INSERT '$.height' = 1.75) WHERE id = 'p3';
UPDATE graph1node SET props = JSON_TRANSFORM(props, INSERT '$.height' = 1.70) WHERE id = 'p4';
```

## 検索クエリ

スキーマの変更なしにグラフを格納できましたが、検索クエリはどのように書くことができるでしょうか。次のクエリは元の SQL グラフ（graph0）を対象として、GRAPH_TABLE を用いて Mary の友人を辿るものです。

```sql
SELECT *
FROM GRAPH_TABLE (graph0
  MATCH (a IS person) -[e IS friend_of]-> (b IS person)
  WHERE a.name = 'Mary'
  COLUMNS (a.name AS person_a, b.name AS person_b)
);
```
```
PERSON_A   PERSON_B  
---------- ----------
Mary       Alice     
Mary       John      
```

同じ検索をスキーマレスなグラフ（graph1）に対して実行する場合には以下のようなクエリになります。元のラベルは label プロパティ、元のプロパティは props プロパティの JSON 要素、にそれぞれ格納されているため書き換えが必要です。

```sql
SELECT *
FROM GRAPH_TABLE (graph1
  MATCH (a) -[e]-> (b)
  WHERE JSON_VALUE(a.props, '$.name') = 'Mary'
    AND a.label = 'person'
    AND e.label = 'friend_of'
    AND b.label = 'person'
  COLUMNS (
    JSON_VALUE(a.props, '$.name') AS person_a
  , JSON_VALUE(b.props, '$.name') AS person_b
  )
);
```
```
PERSON_A   PERSON_B  
---------- ----------
Mary       Alice     
Mary       John      
```

元のクエリの方が簡潔ではありますが、どちらも同じグラフ・パターンで検索できることがわかります。ちなみに JSON_VALUE 関数については SQL:2023 の [JSON ドット表記法](https://docs.oracle.com/cd/F32587_01/adjsn/json-path-expressions.html#GUID-2DC05D71-3D62-4A14-855F-76E054032494) を使って以下のように書くこともできます。

```sql
SELECT *
FROM GRAPH_TABLE (graph1
  MATCH (a) -[e]-> (b)
  WHERE a.props.name.string() = 'Mary'
    AND a.label = 'person'
    AND e.label = 'friend_of'
    AND b.label = 'person'
  COLUMNS (
    a.props.name.string() AS person_a
  , b.props.name.string() AS person_b
  )
);
```

## 索引

大きなデータを SQL グラフで扱うためにはベースとなる表に適切な索引が必要です。上記で作成したスキーマレスな SQL グラフの場合には、以下のような索引を作成することで、大きなグラフに対して高いクエリ性能を維持することができます。

### エッジの始点と終点

ノード表とエッジ表の結合に使用される B ツリー索引です。ノードを起点にエッジを辿る際、この索引がないとエッジ表の全表スキャンが実行されてしまうので、これらの索引は必須です。

```sql
CREATE INDEX IF NOT EXISTS graph1edge_src_idx ON graph1edge (src);
CREATE INDEX IF NOT EXISTS graph1edge_dst_idx ON graph1edge (dst);
```

### プロパティの全文索引

JSON で格納されたプロパティに全文索引を作成しておくと、JSON 要素を指定して全文検索を実行することができます。ノードをそのプロパティの値（例えば名前の部分一致）で探すことは場合などに便利です。

```sql
CREATE SEARCH INDEX graph1node_search_idx ON graph1node (props);
```
```sql
SELECT *
FROM graph1node
WHERE JSON_TEXTCONTAINS(props, '$.name', 'Mary');
```

JSON 列に対する全文検索の使い方と性能についてはこちらをご参照ください！
* [SQL を用いた JSON 全文検索](https://qiita.com/ryotayamanaka/items/02a2efdcb74cac280b3e)

### プロパティのファクション索引

JSON で格納された特定のプロパティの値を検索したい場合、JSON_VALUE を用いたファンクション索引を利用することができます。次の例では、birth_date でソートするクエリを高速化するために、ファンクション索引を作成しています。

```sql
CREATE INDEX IF NOT EXISTS graph1node_updated_idx
    ON graph1node (JSON_VALUE(props, '$.birth_date' RETURNING DATE));
```
```sql
SELECT *
FROM graph1node
ORDER BY JSON_VALUE(props, '$.birth_date' RETURNING DATE);
```

## まとめ

この記事では、リレーショナル・データベースの最新のプロパティ・グラフ機能を使って、スキーマレスなグラフ・データベースと同様のデータを格納し、グラフを辿るクエリを実行する方法を紹介しました。ここでは紹介していませんが、ノードに複数の label を付与できるようにしたり、エッジに有向／無向の区別を持たせたり、といったことも可能です。

多くのグラフ・データベースは、そのデータモデルとクエリ文法に特徴があるだけでなく、グラフを辿る処理に対して最適化された実装を持っています。今後、Oracle Database をはじめとしたマルチモデル・データベースがグラフのための実装を備えることで、SQL の高い表現力を用いながら、柔軟なデータモデルに対応し、安定した拡張性を実現することが期待できます。