[Oracle Cloud] NoSQL Database Cloud Service の Table, Primary Key, Shard Key の関係性をひも解いてみた

# はじめに

Oracle Cloud Infrastructure(以下OCI)では、NoSQL Database Cloud Service が提供されています。フルマネージドな NoSQL サービスです。ユーザーはインスタンスを気にする必要なくデータストアを使うことが出来るのが特徴的です。

NoSQL データを扱っていくためには、Table, PrimaryKey, ShardKey の概念を理解するのが大事です。ドキュメントや動作検証を通して、自分が整理した内容を紹介します。(間違っているところがあれば指摘貰えると助かります :bow:)



# Table とは

Table は、NoSQL Database Cloud Service を使い始めるときに一番初めに作成するものです。いわゆる、RDBMS でよく使われる Table と同じような概念です。Table の中には、複数の Row を格納することが出来ます。Row の中には、複数の Field が存在します。図に表すとこんな感じです。RDBMS と似たような概念ですね。

![1591220933523.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104247/fcddcf94-75aa-4ec8-7668-27aa0c4b469a.png)



この Table は、サービス提供側で完全にマネージドされているのが特徴です。

- Table の裏側にあるマシンを管理する必要がない
- 自動的に高可用性となっている
- スケーリングで気するのは、Table に割り当てる 書き込み性能・読み込み性能・容量の3点のみ

ただ、デメリットもちろんあって、RDBMS のような複数の Table 間で複雑なトランザクション処理には向いていません。単一 Table 内で、シンプルなトランザクションを大規模に処理することに向いています。いわゆる適材適所です。



Table のスキーマは、事前に適切な設計をすることも出来ますし、スキーマレスで JSON データを格納することも出来ます。各Field で使えるデータ型は次の Document で公開されています。String, Integer などの基本的なものもありますし、Record, JSON のようなものもあります。JSON でスキーマレスにデータを書き込む方法・読み込む方法はまた別の記事で詳細を見ていきます。

https://docs.cloud.oracle.com/en-us/iaas/nosql-database/doc/table-design.html#CSNSD-GUID-833B2B2A-1A32-48AB-A19E-413EAFB964B8



# Primary Key と Shard Key

この Table を構成するのに大事な概念が、Primary Key と Shard Key です。Table を新規作成するときに、Primary Key と Shard Key を指定します。後から変更することは出来ないので、注意しましょう。

**Primary Keyとは**

Table 内で Row を一意に特定するものです。Table 内の Column から、Primary Key を指定して Table を作成します。Primary Key は 1 個でも良いですし、複数で構成もできます。例えば、次のように商品を扱うときは、商品名が一意であることがわかっているので、Primary Key として指定します。

```
CREATE TABLE if not exists myProducts 
(
  productName STRING,
  productType STRING,
  productLine STRING,
  PRIMARY KEY (productName)
)";
```

図で表すとこんな感じです。

![1591225651115.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104247/c51e2ce0-6892-230e-6495-5cfd72c7c54b.png)



Primary Key は、他にもデータの取得方法に影響があります。NoSQL Database Cloud Service では、大きくわけて2種類のデータ取得方法が提供されています。

- Primary Key を指定して取得
- SQL ライクなクエリー

Primary Key は、1個目の「Primary Key を指定して取得」の方法に影響があります。ただ、「SQL ライクなクエリー」を使うと、Primary Key に関係なく、柔軟にデータ取得は出来るため、あまり気にする必要はないのかなと思っています。(データ取得の効率性を厳密に検証していく場合は、考慮ポイントがあるかも)

具体的にソースコードと実行結果を出して説明します。Java SDK を使って、「Primary Key を指定して取得」する時のコードです。`put("productName", "cookie");` このあたりで、Primary Key を指定しています。

```java
    private static void readRows(NoSQLHandle handle) throws Exception {
        MapValue key = new MapValue().put("productName", "cookie");
        GetRequest getRequest = new GetRequest().setKey(key).setTableName("myProducts");
        GetResult getRes = handle.get(getRequest);
        System.out.println("Read " + getRes.getValue());
    }
```



実際の実行結果がこれです。Primary Key によって一意に決められたデータを取得できます。

```
Read {"productName":"cookie","productType":"sweets","productLine":"XX"}
```



じゃあ、Primary Key ではない通常の Column を指定してみましょう。`productType` を指定してみます。

```java
    private static void readRows(NoSQLHandle handle) throws Exception {
        MapValue key = new MapValue().put("productType", "sweets");
        GetRequest getRequest = new GetRequest().setKey(key).setTableName("myProducts");
        GetResult getRes = handle.get(getRequest);
        System.out.println("Read " + getRes.getValue());
    }
```



実行結果を見ると、例外が発生しています。Primary Key の `productName` が指定していない例外ですね。

```
java.lang.IllegalArgumentException: GET: Illegal Argument: Missing primary key field: productName
```



では、「SQL ライクなクエリー」の方法を見てみましょう。これは、Primary Key や、今回は説明していない Index は関係なくデータを取得できます。

```java
    private static void queryRows(NoSQLHandle handle) throws Exception {
        String query = "SELECT * FROM myProducts WHERE productType = 'sweets'";

        List<MapValue> results = runQuery(handle, query);

        System.out.println("Number of query results for " + query + " \nRows:" + results.size());
        for (MapValue qval : results) {
            System.out.println("\t" + qval.toString());
        }
    }
```



実行結果はこんな感じです。

```
Number of query results for SELECT * FROM myProducts WHERE productType = 'sweets' 
Rows:2
        {"productName":"chocolate","productType":"sweets","productLine":"YY"}
        {"productName":"cookie","productType":"sweets","productLine":"XX"}
```



他の NoSQL サービスでは、Primary Key や Index の指定が、データ取得の有無に密接に関係しているものもあります。NoSQL Database Cloud Service では、SQL ライクなクエリーを使うことで、柔軟性をもってデータを取得できるのが魅力的ですね。



**Shard Key とは**

NoSQL Database Cloud Service クラスターに、効率的にデータを分散するための Key です。クラスター内のどこにデータを配置するか、コントロール出来ます。同じ Shard Key を持つ Row は、同一の物理的な場所に格納されます。図に表すとこんな感じです。

![1591226437803.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104247/32958db8-4c0c-de3c-5a05-13af5e3269d4.png)


Shard Key を適切に分散することで、データが格納される物理的な場所を分散が出来ます。

また、Shard Key が影響が及ぼすものは、他にもトランザクション機能があります。NoSQL Database Cloud Service では、ACID 特性を備えた操作を提供していますが、同一の Shard Key のデータのみトランザクション機能が利用できます。なので、Shard Key を設定する時には、事前にトランザクション機能の必要性を慎重に考えましょう。後から変更は出来ないので、更に慎重になる必要があります。

トランザクションは、具体的には、WriteMultipleRequestあたりの Class が関係しているはずです。この機能は、また別途詳細を見ていきます。
https://docs.oracle.com/en/cloud/paas/nosql-cloud/csnjv/oracle/nosql/driver/ops/WriteMultipleRequest.html

# 付録 : Javaソースコード

実際につかった Java コードをのせておきます

```java
package com.example;

import java.io.File;

import java.util.ArrayList;
import java.util.List;

import oracle.nosql.driver.NoSQLHandle;
import oracle.nosql.driver.NoSQLHandleConfig;
import oracle.nosql.driver.NoSQLHandleFactory;
import oracle.nosql.driver.Region;
import oracle.nosql.driver.iam.SignatureProvider;
import oracle.nosql.driver.ops.GetRequest;
import oracle.nosql.driver.ops.GetResult;
// import oracle.nosql.driver.ops.PutRequest;
// import oracle.nosql.driver.ops.PutResult;
// import oracle.nosql.driver.ops.TableLimits;
// import oracle.nosql.driver.ops.TableRequest;
// import oracle.nosql.driver.ops.TableResult;
import oracle.nosql.driver.values.MapValue;
import oracle.nosql.driver.ops.PrepareRequest;
import oracle.nosql.driver.ops.PrepareResult;
import oracle.nosql.driver.ops.QueryRequest;
import oracle.nosql.driver.ops.QueryResult;
import oracle.nosql.driver.ReadThrottlingException;

/**
 * Hello world!
 *
 */
public class App {
    /* Name of your table */
    // final static String tableName = "member";

    public static void main(String[] args) throws Exception {
        System.out.println("Start!");

        NoSQLHandle handle = generateNoSQLHandle();

        try {
            readRows(handle);
            queryRows(handle);
        } catch (Exception e) {
            System.err.println(e);
        } finally {
            handle.close();
        }

        System.out.println("End!");
    }

    /* Create a NoSQL handle to access the cloud service */
    private static NoSQLHandle generateNoSQLHandle() throws Exception {

        SignatureProvider ap = new SignatureProvider(
                "ocid1.tenancy.oc1..secret",
                "ocid1.user.oc1..secret",
                "06:3b:a5:d2:52:c0:9b:04:ed:fe:9d:9e:5c:b4:c5:a5", new File("/home/opc/.oci/oci_api_key.pem"),
                "".toCharArray());

        /* Create a NoSQL handle to access the cloud service */
        NoSQLHandleConfig config = new NoSQLHandleConfig(Region.AP_TOKYO_1, ap);
        NoSQLHandle handle = NoSQLHandleFactory.createNoSQLHandle(config);
        return handle;
    }

    /**
     * Make a row in the table and write it
     */
    // private static void writeRows(NoSQLHandle handle) throws Exception {
    // MapValue value = new MapValue().put("employeeid", 1).put("name", "Tracy");
    // PutRequest putRequest = new
    // PutRequest().setValue(value).setTableName(tableName);
    // PutResult putResult = handle.put(putRequest);
    // if (putResult.getVersion() != null) {
    // System.out.println("Wrote " + value);
    // } else {
    // System.out.println("Put failed");
    // }
    // }

    /**
     * Make a key and read the row from the table
     */
    private static void readRows(NoSQLHandle handle) throws Exception {
        MapValue key = new MapValue().put("productType", "sweets");
        GetRequest getRequest = new GetRequest().setKey(key).setTableName("myProducts");
        GetResult getRes = handle.get(getRequest);
        System.out.println("Read " + getRes.getValue());
    }

    /*
     * QUERY the table. The table name is inferred from the query statement.
     */
    private static void queryRows(NoSQLHandle handle) throws Exception {
        String query = "SELECT * FROM myProducts WHERE productType = 'sweets'";

        List<MapValue> results = runQuery(handle, query);

        System.out.println("Number of query results for " + query + " \nRows:" + results.size());
        for (MapValue qval : results) {
            System.out.println("\t" + qval.toString());
        }
    }

    /**
     * Runs a query in a loop to be sure that all results have been returned. This
     * method returns a single list of results, which is not recommended for queries
     * that return a large number of results. This method mostly demonstrates the
     * need to loop if a query result includes a continuation key.
     *
     * This is a common occurrence because each query request will only read a
     * limited amount of data before returning a response. It is valid, and expected
     * that a query result can have a continuation key but not contain any actual
     * results. This occurs if the read limit is reached before any results match a
     * predicate in the query. It is also expected for count(*) types of queries.
     *
     * NOTE: for better performance and less throughput consumption prepared queries
     * should be used
     */
    public static List<MapValue> runQuery(NoSQLHandle handle, String query) {

        /* A List to contain the results */
        List<MapValue> results = new ArrayList<MapValue>();

        /*
         * A prepared statement is used as it is the most efficient way to handle
         * queries that are run multiple times.
         */
        PrepareRequest pReq = new PrepareRequest().setStatement(query);
        PrepareResult pRes = handle.prepare(pReq);
        QueryRequest qr = new QueryRequest().setPreparedStatement(pRes.getPreparedStatement());

        try {
            do {
                QueryResult res = handle.query(qr);
                int num = res.getResults().size();
                if (num > 0) {
                    results.addAll(res.getResults());
                }
                /*
                 * Maybe add a delay or other synchronization here for rate-limiting.
                 */
            } while (!qr.isDone());
        } catch (ReadThrottlingException rte) {
            /*
             * Applications need to be able to handle throttling exceptions. The default
             * retry handler may retry these internally. This can result in slow
             * performance. It is best if an application rate-limits itself to stay under a
             * table's read and write limits.
             */
        }
        return results;
    }
}
```





# 参考URL

Cloud Concepts
https://docs.cloud.oracle.com/en-us/iaas/nosql-database/doc/cloud-concepts.html

Table Design
https://docs.cloud.oracle.com/en-us/iaas/nosql-database/doc/table-design.html