LoginSignup
71
40

Rust製データベースSurrealDBの紹介

Last updated at Posted at 2023-01-21

はじめに

この資料は、2022年7月に公開されたSurrealDBについてまとめたものです。

SurrealDBの歴史

  • 公開されてからの期間は浅いが2016年から開発が始まっている
    • 2016年 Feb GoLangで開発開始
    • 2017年 Jul SaaS のバックエンドDBとして運用開始
    • 2021年 Oct Open Source として公開決定、Rustで再構築
    • 2022年 Jul Beta.1 リリース
    • 2022年 Aug Beta.5リリース
    • 2022年 Oct Beta.8 リリース
  • SurrealDB 社
    • 2021年 Nov SurrealDB Ltd. をロンドンに設立
    • 2023年 Jan DBaaS ために 600万ドル調達

SurrealDBが生まれた背景

SurrealDBの注目度合い

SurrealDBのライセンス

  • SurrealDBのソースコードはBusiness Software License 1.1
  • SDKやライブラリ/ドライバはMIT
  • SurrealDBのBSLは、商用DBaaSとして提供しない限り、ノード数に制限なくSurrealDBを使用することがでる
    • 製品に組み込むこともOK
  • SurrealDBのBSLは、4年間有効
  • 2026年1月1日この制限は失効、コードは現在のApache License 2.0によるオープンソースになる
    • どんな目的にも自由に使用することができる

SurrealDBの特徴

  • Rustで実装
    • Segmentation Faultが発生しにくい
    • クロスコンパイル
    • 他の言語と比べて相対的に速い
  • 軽量: バイナリサイズ Linux 24MB, macOS: 44MB
  • ひとつのバイナリでサーバーとREPLクライアントを兼用
  • 簡単なインストール
  • HTTP/Restful APIをサポート
  • WebSocketをサポート
  • バックエンドDB: EchoDB, RocksDB, TiKV, FoundationDB, IndexedDB

データベースとしての特徴

  • スキーマーレス:スキーマーを定義しても問題ない
  • 多様な保存形式:テーブル、ドキュメント、グラフなど
  • 複数行、複数テーブルのACIDトランザクション
  • レコードリンクと有向グラフ接続
    • JOINが不要、N+1問題をスマートに回避
  • 分析クエリの事前定義
    • データが書き込まれると、選択、集約、グループ化、順序づけ
  • 埋め込みJavaScriptでクエリを拡張できる
  • クエリに正規表現を記述できる (/regex/)
  • GeoJSONをサポート
  • CRUD操作を並列実行できる

SurrealDBの強み

  • 差別化できる機能を同時に備えている
    • ユーザーがfrontendから直接アクセスできる
    • データベース側で認証認可を適切に設定できる
    • リアルタイムでデータが同期できる
  • 対抗馬は少なく Google Firestore などがあるぐらい
    • オンプレミスで使用が条件となるとオンリーワンのDB
  • データベース側で認証認可ができると…

SurrealDBの弱み

  • 公開されてからまだ日が経っていない(2022年Jul)
    • 潜在的なセキュリティの脆弱性やバグが存在する可能性がある
      • PostgreSQLの初版は1997年、前身のPostgressは1989年)
      • MySQL の初版は1995年
      • MariaDB の初版は2009年
      • MongoDB の初版は2009年
  • 情報が少ない
    • 公式ドキュメントも製作中
    • 困ったらソースコードを読めということ
  • 機能をすべて実装できていない
  • SurrealDB社は今時点で未収益:DBaaSのサービスインは2023年

現在開発中の機能 (beta.9でも未完)

  • 分散モードでのマルチノード対応
  • レプリケーション
  • ヘルスチェック
  • GraphQL
  • FULLTEXT - フルテキストインデクス
  • LEARNフィールド
    • 指定されたフィールドの機械学習分析に基づいて自動設定
  • バージョン管理された一時テーブル
    • データ参照するときに「過去にさかのぼる」ことができる
  • IDEコードハイライト(Atom, VSCode, Vim…)
  • ユーザI/Fアプリのリリースは1.xとして計画にはある

SurrealDBの2-Tierアーキテクチャ概要

surrealdb_arch.png

SurrealDBの動作概要

surrealdb_dataflow.png

ソースコード

  • 数値はコメントを含む行数(SurrealDB 1.0.0-beta.8)

    ちなみに MariaDB 10.9 は 130万行超(クライアントだけで12,000行超)

|-- src            // API layer    4172
     |-- net                                    1981
     |-- cli                                    895
     |-- rpc                                    174
     ...
|-- lib/src       // BL   Layer     32261
     |-- sql                                    19030
     |-- fnc                                    3524
    ...
     |-- kvs                                    3286
           |-- indexdb                          220
           |-- rocksdb                          316
           |-- tikv                             269
      ...

SurrealDBで実装している機能は驚くほど少ない

  • クエリ解析のために nom を利用
    • 関数を連結してインクリメンタルにパーサーを構築できる
  • メモリ格納するために echodb を利用 (Tobie)
    • マルチバージョン並行処理制御が可能なin-Memory KVS DB
  • KVSへ格納するために storekey を利用(Tobie)
    • 辞書順序を保持してバイナリエンコーディングする
    • ソートされたKVSのキーを作成するときに便利
  • シリアライズ/ディシリアライズはMsgPack と serde を利用(rmp-serde)
  • GeoJSONの解析には geo を利用
  • ローカルファイルをデータストアにするときはRocksDBを利用
  • 分散DBとしての機能はTiKVやFoundationDB を利用

SurrealDBがKVSに保存する項目

  • メタデータ
    • テーブル、インデックス、スコープなどの構造
  • データ
    • SurealDBが保持するオブジェクトの値

SurrealDBのKVSへの保存方法

  • Rust構造体とMsgPackをserdeバインディングでマップ
    • ストレージサイズの節約
    • シリアライズ/ディシリアライズの効率化
  • KVSのデータアクセスの方法は2つ
    • キーベース:特定のキーを指定して、値を取得 (速い)
    • スキャン:キーの範囲を指定して、すべての値を取得(遅い)
  • 階層構造をキーに保持
    • Namespace -> Database -> Table -> ID
  • SurrealDBはキーを階層的に構築することでスキャンする範囲に変換

インストール(簡単&速い)

  • Linux
$ curl -sSf https://install.surrealdb.com | sh
  • macOS
$ brew install surrealdb/tap/surreal
  • Windows
PS C:\> iwr https://windows.surrealdb.com -useb | iex
  • Docker/Podmandocker
$ docker run --rm -p 8000:8000 surrealdb/surrealdb:latest start
$ podman run --rm -p 8000:8000 surrealdb/surrealdb:latest start

SurrealDBの起動

  • start サブコマンドの第1引数がデータの書き出し先

    • デフォルトは memory
    $ surreal start --user root --pass root   memory
    
  • その他の書き出し先

    file:///path/to/data.db                ファイルシステム(RocksDB)
    rocksdb:///path/to/data.db             RocksDB
    tikv://endpoiint TiKV                  TiKV
    fdb:[///path/to/clusterfile]           FoundationDB (リビルドが必要)
    
  • STRICTモード

    • --strict オプションを与えて起動
    • NAMESPACE,、DATABASEを定義しないとエラーになる
    • TABLE定義をしないとエラーになる

ダンプ/リストア

  • export サブコマンドでファイルにダンプ
  • import サブコマンドでファイルからリストア
$ surreal export --conn http://dev00:8000 --ns test --db test  dump.db
$ surreal import --conn http://dev00:8000 --ns test --db test  dump.db

CLIクライアントから接続

  • sqlサブコマンドを実行
$ surreal sql --conn http://dev00:8000 --ns test --db test --pretty
  • --user--pass ROOT認証のユーザ/パスワード
  • --pretty でJSON出力ば整形して表示
  • --ns NAMESPACE --db DATABASE を指示する
  • SurrealDBがSTRICTモードで起動しているときは、—ns と —db オプションは無視される

HTTP RESTful API

PATH TYPE 説明
/key/:table GET データベースからテーブル内の全レコードを取得
/key/:table/:id GET データベースから特定のレコードを取得
/key/:table POST データベース内のテーブルにレコードを作成
/key/:table/:id POST データベース内のテーブルに特定のレコードを作成
/key/:table DELETE データベースからテーブルの全レコードを削除
/key/:table/:id PUT データベース内の指定されたレコードを更新
/key/:table/:id PATCH データベース内の指定されたレコードを変更
/key/:table/:id DELETE データベース内の指定されたレコードを削除

HTTP RESTful API (cont.)

PATH TYPE 説明
/version GET SurrealDBのバージョンを返す
/signup POST SCOPE認証の登録
/signin POST SCOPE認証でログイン
/rpc POST WebSocketへJON-RPCでリクエスト
/sql POST SurQLクエリを許可
/export GET データベースの内容をダンプ
/import POST クエリの内容をデータベースに適用(リストア)

HTTP RESTful API (未完のもの)

PATH TYPE 説明
/sync GET レプリケーション
/health GET データベースのヘルスチェック
/status GET ステータスを返す

一般的なSQLでのテーブル定義

CREATE TABLE human (
      id int,
      nickname text,
      age int,
      PRIMARY KEY(id)
   );

SurealDBはスキーマーレス

  • テーブルやフィールドを定義する必要がない
  • フィールドを追加するときも変更する必要がない
  • サーバーがSTRICTモードで起動されていると、先にテーブル定義が必要
CREATE human:freddie SET nickname="freddie", age=99 ;
CREATE human:brian SET nickname="brian", age=75, sex=true ;

ID == TableName:UniqID  IDにテーブル名が含まれていること注目

スキーマレスでフィールド型を指定

CREATE human:freddie SET
       nickname = <string> "freddie",
       age = <int> 99 ;

型とキャスト

  • bool, int, float, string, number, decimal, datetime, duration
  • 日時文字列はISO8601に変換される: <datetime> でのキャストと同じ
  • 日時文字列をそのまま文字列として扱いたいときは <string>でキャスト
SELECT * FROM "2023-01-01";
SELECT * FROM <datetime> "2023-01-01";
SELECT * FROM <string> "2023-01-01T02:03:00Z" + "-test";

スキーマーの定義

DEFINE TABLE human SCHEMAFULL;
DEFINE FIELD nickname ON human TYPE string;
DEFINE FIELD age ON human TYPE int;
  • テーブルをSCHMALESSとして[再]定義
DEFINE TABLE human SCHEMALESS ;
  • テーブルをSCHMAFULLとして[再]定義
DEFINE TABLE human SCHEMAFULL ;

SCHEMAFULL

  • 定義されたフィールドで許可されたデータのみが格納されrる
  • 特定のデータ型に制限することができる
  • DEFINE FIELD でデータが入力されない場合のデフォルト値を設定できる
  • 設定する値は $value にセットされる
DEFINE TABLE person SCHEMAFULL;
DEFINE FIELD name ON person TYPE string VALUE $value OR 'guest';

データ追加

  • id フィールドの設定省略すると、IDは自動設定される
INSERT INTO human (nickname, age)
       VALUES ('brian', 75);

INSERT INTO human (id, nickname, age)
       VALUES ('human:freddie', 'freddie', -1);

CREATE human:robert SET nickname=robert, age=30;

CREATE human SET
       id = human:jack, nickname=jack, age=30;

CREATE human CONTENT
       { id: 'human:john', nickname: 'john', age: 99 };

INSERT

  • IDが重複する時 UPDATE することができる
INSERT INTO test (id, test, something)
    VALUES (tester, true, other ) ;
INSERT INTO test (id, test, something)
    VALLUES (tester, true, other )
    ON DUPLICATE KEY UPDATE something = else ;

フィールドのネスト

  • テーブルのフィールドはネストさせることができる
  • ネストされたフィールドはドット表記で参照できる
UPDATE person:test CONTENT {
              settings: {
                  nested: {
                      object: {
                          thing: 'test'
                      }
                  }
              }
          };
SELECT settings.nested.object FROM person ;

NONEとNULL

  • フィールドの値にはNONENULLを持つことができる
    • NONE:値が設定されていない
    • NULL:空の値が設定されている
CREATE person:test1 SET email = 'info@example.com';
CREATE person:test2 SET email = NONE;
CREATE person:test3 SET email = NULL;

USE

  • 使用するNAMESPACE、DATABASEを指定
    • ROOT認証でアクセスしているときに有効
USE NAMESPACE test ;
USE NAMESPACE test DATABASE db1 ;
USE NS test DB db1 ;

一般的なSQLでのリレーション

CREATE TABLE armor (
    id int,
    name text,
    resistance text,
    PRIMARY KEY(id)
 );

INSERT INTO armor VALUES
    (0, "leather", 3);
    (1, "platemail", 30),
    (2, "chainmail", 20),

CREATE TABLE player (
   name text,
    strength int,
    armor_id int,
    PRIMARY KEY((name),
    CONSTRAIN fk_armor
      FOREIGN KEY(armor_id)
      REFERENCECS armor(id)
);

SurrealQL(SurQL)でのリレーション

CREATE armor:leather SET registance = 3;
CREATE armor:chainmail SET registance = 20;
CREATE armor:platemail SET registance = 30;
CREATE player:jack SET strength = 22, armor = armor:platemail;
CREATE player:brian SET strength = 20, armor = armor:leather;

ID にテーブル名が含まれていること利用

SurQL: スキーマーを定義してのリレーション

DEFINE TABLE armor SCHEMAFULL;
DEFINE FIELD resistance ON armor TYPE int;

CREATE armor:leather SET resistance = 3;
CREATE armor:chainmail SET resistance = 20;
CREATE armor:platemail SET resistance = 30;

DEFINE TABLE player SCHEMAFULL;
DEFINE FIELD strength ON player TYPE int;
DEFINE FIELD armor ON player TYPE record(armor);

CREATE player:jack SET strength = 22, armor = armor:platemail;
CREATE player:brian SET strength = 20, armor = armor:leather;

一般的なSQLでのリレーション:JOIN

SELECT
    player.name,
    player.strength,
    armor.name AS armor_name,
    armor.resistance AS armor_resistance
 FROM player
 JOIN armor
 ON armor.id = player.armor_id

SurQLでのリレーション:JOINが不要

  • FETCHで指定したフィールドが展開される
SELECT * FROM player FETCH armor;

レコードリンク

CREATE armor:leather SET registance = 3;
CREATE armor:chainmail SET registance = 20;
CREATE armor:platemail SET registance = 30;

CREATE player:jack SET strength = 22, armor = armor:platemail;
CREATE player:brian SET strength = 20, armor = armor:leather;

Foreign Key == Record Link

リレーション: ONE-TO-ONE

CREATE human:freddie SET nickname="freddie", age=99 ;
CREATE human:brian SET nickname="brian", age=75
UPDATE human:brian SET bff = human:freddie;

SELECT bff.nickname, bff.age FROM human:brian

外部テーブルのフィールドをドットで繋いで指示

リレーション: ONE-TO-MANY

CREATE car:tesla  SET model='Model S', ev=True, price=99000;
CREATE car:mustang SET model='Mustang Cobra', ev=False, price=60000;

UPDATE human:brian SET cars=["car:tesla"];
UPDATE human:freddie SET cars=["car:mustang"];
UPDATE car:tesla SET owner = human:brian;
UPDATE car:mustang SET owner = human:freddie;

CREATE parts:tire SET brand='Michelin', size=5;
CREATE parts:gastank SET brand='Tanksy', size=10;
CREATE parts:battery SET brand='Xi Ping', size=20;

UPDATE car:mustang SET parts = ['parts:tire', 'parts:gastank'];
UPDATE car:tesla SET parts = ['parts:tire', 'parts:battery'];

リレーション: ONE-TO-MANY

SELECT parts FROM car:mustang
SELECT cars.parts.brand FROM human:brian ;

外部テーブルのフィールドをドットで繋いで指示

グラフコネクション

RELATE player:jack -> wants_to_buy -> armor:dragon;
RELATE player:jack -> wants_to_buy -> armor:platemail;

SELECT * FROM wants_to_buy;
SELECT id, -> wants_to_buy -> armor AS wtb FROM player;
SELECT id, <- wants_to_buy <- player AS players FROM armor:dragon

外部テーブルのフィールドを ”->” や “<-” で繋いで指示

LIMIT

  • SELECTで返す結果の数を指定する
  • 今時点ではFETCHを指示するとうまく動作しない
CREATE tag:rs SET name = 'Rust';
CREATE tag:go SET name = 'Golang';
CREATE tag:js SET name = 'JavaScript';

CREATE person:tobie SET tags = [tag:rs, tag:go, tag:js];
CREATE person:jaime SET tags = [tag:js];

SELECT * FROM person LIMIT 1;
SELECT * FROM (SELECT * FROM person FETCH tags) LIMIT 1;
// SELECT * FROM person LIMIT 1 FETCH tags;

START

  • SELECTで返す結果の始めの位置を指定する(ゼロはじまり)
  • FETCHを指示してもうまく動作する
SELECT * FROM person START AT 1;
SELECT * FROM (SELECT * FROM person FETCH tags) START 1;
SELECT * FROM person START 1 FETCH tags;

SurQL: WHERE, ORDER BY, GROUP BY

SELECT * FROM armor ;

SELECT * armor WHERE resistance >= 30 ;
SELECT math::sum(strength) FROM player GROUP BY ALL ;

SELECT * FROM armor ORDER BY RAND();
SELECT * FROM armor ORDER RAND();

SELECT * FROM armor ORDER resistance NUMERIC ASC ;
SELECT * FROM armor ORDER resistance NUMERIC DESC ;

SurQL: BEFORE, AFTER, DIFF

  • CREATE、UPDATE、DELETE でクエリの前後や差分を返すことができる
UPDATE human:freddie SET email = 'freddie@example.com';
UPDATE human:freddie SET email = 'freddie@dummy.com' RETURN DIFF ;

分析クエリの事前定義

  • データが書き込まれると、選択、集約、グループ化、順序づけなどを実行する
DEFINE TABLE person SCHEMALESS;
DEFINE TABLE person_by_age AS
    SELECT
          count(),
          age,
          math::sum(age) AS total,
          math::mean(age) AS average
          FROM person
          GROUP BY age ;

EVENT

  • ON TABLE で指定したテーブルの内容: $before, $after
  • イベントが発生したID:$this
  • 発生したイベント: $event
UPDATE human:freddie SET email = 'freddie@example.com';
UPDATE human:brian SET email = 'brian@example.com';

DEFINE EVENT changelog ON TABLE human
       WHEN $before.email != $after.email
       THEN ( CREATE changelog SET
              time = time::now(),
              email = $after.email );

RANGE コロンで指定した回数だけ繰り返す

> CREATE |test:10| SET time = time::now();
[
  {
    "result": [
      {
        "id": "test:g9hq0dowz77us5yvxnst",
        "time": "2022-12-20T04:10:19.282031670Z"
      },
      {
        "id": "test:nk45tn46dy2bn1hd6zj9",
        "time": "2022-12-20T04:10:19.282450969Z"
      },

RANGE 初期値と終了値を指示して繰り返す

> CREATE |test:1..10| SET time = time::now();
[
  {
    "result": [
      {
        "id": "test:1",
        "time": "2022-12-20T04:12:21.477667592Z"
      },
      {
        "id": "test:2",
        "time": "2022-12-20T04:12:21.478289880Z"
      },

正規表現

> SELECT * FROM test WHERE id = /.*[24].*/
[
  {
    "result": [
      {
        "id": "test:2",
        "time": "2022-12-20T04:12:21.478289880Z"
      },
      {
        "id": "test:4",
        "time": "2022-12-20T04:12:21.478332436Z"
      }
    ],
    "status": "OK",

IF THEN ELSE

UPDATE person SET classtype =
    IF age <= 10 THEN
      'junior'
    ELSE IF age <= 21 THEN
      'student'
    ELSE IF age >= 65 THEN
      'senior'
    ELSE
      NULL
    END ;

MERGE

  • テーブルのフィールドをマージ:追加、削除
UPDATE person:test SET
      name.initials = 'TMH',
      name.first = 'Tobie',
      name.last = 'Morgan Hitchcock';
UPDATE person:test MERGE {
      name: {
          title: 'Mr',
          initials: NONE,
          suffix: ['BSc', 'MSc'],
          }
     };

ASSERT テーブル制約

  • 定義された各フィールドは、ASSERTでデータに対する制約を定義できる
DEFINE FIELD countrycode ON user TYPE string
    // Ensure country code is ISO-3166
    ASSERT $value != NONE AND $value = /[A-Z]{3}/
    // Set a default value if empty
    VALUE $value OR 'GBR'
  ;

FUTURE関数

  • • テーブルのフィールドをあとで設定される値によって定義する
UPDATE person:test SET
        can_drive = <future> {
             birthday && time::now() > birthday + 18y };

UPDATE person:test SET birthday = <datetime> '2007-06-22';
UPDATE person:test SET birthday = <datetime> '2001-06-22';

PERMISSIONS

  • TABLE、FIELDのCRUD操作を制限する
DEFINE TABLE user SCHEMALESS
    PERMISSIONS
    FOR select, create, update
        WHERE id = $auth.id
    FOR delete
        WHERE id = $auth.id OR $auth.admin = true ;

ACIDトランザクション

BEGIN TRANSACTION;

UPDATE coin:one SET balance += -23.00 ;
UPDATE coin:two SET balance -= 23.00 ;

COMMIT TRANSACTION;
  • あるいは
CANCEL TRANSACTION;
  • DROPが設定されているテーブルで新規トランザクションが指定されると、終了して
    いないトランザクションは破棄される
  • デフォルトは、新規トランザクションはリードオンリー(可能であれば)になる

WebSocket

  • ws://dev00:8000/rpc のようにURLを指定 (ws://、wss://)
  • 次のようなJSONをメッセージボディに設定してPOST送信
{
  "id": <識別するためのID>,
  "method": <コマンド>,
  "params": <コマンドが要求するパラメタの配列>
}

SurrealDBの認証

  • ROOT認証
    • サーバー起動時に指定したユーザ/パスワード
    • -pass オプションを省略するとROOT認証は無効になる
  • ユーザ認証
    • DEFINE LOGIN で作成したユーザ/パスワード
  • トークン認証
    • JSON Web Token (JWT)による認証 (RFC7519RFC8725)
    • 3rd パーティーのOAuth認証
  • SCOPE認証
    • SIGNUPSIGNINを事前に定義
    • アクセス期間を限定することができる

LOGIN

  • NAMESPACE や DATABASE に対してアクセス制限ができる
  • NAMESPACEに権限がないユーザはDB作成/削除ができない
DEFINE LOGIN admin ON NAMESPACE PASSWORD admin.admin;
DEFINE LOGIN guest ON DATABASE PASSWORD guest.guest;

LOGINのイメージ

surrealdb_login.png

TOKEN

  • 特定のトークンをヘッダに持つリクエストだけアクセスを許可
  • NAMESPACE、DATABASE、SCOPEに対して設定できる
DEFINE TOKEN my_token ON DATABASE
    TYPE HS512 VALUE '1234567890';

SCOPE

  • JSON-RPC の全てのフィールドが変数にセットされる
  • SCOPEはデータベースにアクセスする能力を与える
  • テーブルやフィールドへのアクセスはPERMISSIONSに従う
  • TOKEN付きSCOPEは、テーブルの作成/変更/削除、情報の表示ができない
DEFINE FIELD email ON TABLE user TYPE string ASSERT is::email($value);
DEFINE INDEX email ON TABLE user COLUMNS email UNIQUE;

DEFINE SCOPE account SESSION 24h
          SIGNUP ( CREATE user SET
                email = $email,
                pass = crypto::argon2::generate($pass) )
        SIGNIN ( SELECT * FROM user
                WHERE email = $email
                AND crypto::argon2::compare(pass, $pass) );

SCOPEのイメージ

surrealdb_scope.png

SCOPE認証にSIGNUP

let jwt = fetch('https://api.surrealdb.com/signup', {
        method: 'POST',
        headers: {
                'Accept': 'application/json',
                'NS': 'google', // Specify the namespace
                'DB': 'gmail', // Specify the database
    },
        body: JSON.stringify({
                'NS': 'google',
                'DB': 'gmail',
                'SC': 'account',
                email: 'tobie@surrealdb.com',
                pass: 'a85b19*1@jnta0$b&!'
        }),
});

クエリからトークンや認証データを参照

  • $session$scope$token および $auth には、クライアントに関連する特別な情報がセットされる
  • NAMESPACEやDATABASE、 TOKENを使用している間は、 $session および $token がセッ>トされる
  • $toekn にはJWT トークンのすべてのフィールドがセットされる
  • $scope には SCOPE認証でのSCOPE名がセットされる
  • $auth は、SCOPE認証でJWTがidフィールドを持ち、idで指定されたデータがテーブルに存在するときにセットされる
SELECT * FROM $session;
SELECT * FROM $token;
SELECT * FROM $scope;
SELECT * FROM $auth;

LIVEクエリ

  • WebScoket 経由でアクセスしたときに有効
  • データ変更は、クライアント、アプリケーション、エンドユーザーデバイス、サーバー
    サイドライブラリーへリアルタイムにプッシュされる
  • すべてのクライアントデバイスは同期させたまま維持される
  • LIVEクエリを終了するためには KILLで指定する
LIVE SELECT * FROM user WHERE age > 18 ;

PARALLEL

  • CREATE、DELETE、UPDATE、SELECT で PARALLEL を付加
  • クエリ実行が並列処理される
SELECT * FROM test PARALLEL ;

TIMEOUT

  • CREATE、DELETE、UPDATE、SELECT で TIMEOUTを付加
  • クエリ実行で指定した時間だけ待つ
SELECT * FROM
    http::get('https://ipinfo.io')
    TIMEOUT 10s;

GeoJSON

  • Pont, Line, Polygon, MultiPoint, MultiLine, MultiPolygon, Collection
UPDATE university:oxford SET area = {
  type: 'MultiPolygon',
  coordinates: [
    [[ [10.0, 11.2],[10.5, 11.9],[10.8, 12.0],[10.0, 11.2] ]],
    [[ [9.0, 11.2], [10.5, 11.9],[10.3, 13.0], [9.0, 11.2] ]]
    ]
};

SELECT * FROM university:oxford;

SurQL の組み込み関数

  • array::xxxx()
    • combine, complement, concat, difference, disinc, intersect, len, sort::asc, sort::desc, sort, union, all, any, add, append, insert, prepend, remove, reverse, group, push, pop
  • count()
  • crypto::xxxx()
    • argon2::compare, argon2::generate, bcrypt::compare, bcrypt::generate, md5, pdkdf2::compare, pdkdf2::generate, scrypt::compare, scrypt::generate, sha1, sha25, sha512

SurQL の組み込み関数 (cont.)

  • duration::xxxx()
    • days, hours, mins, secs, weeks, years
  • geo::xxxx()
    • area, bearing, centroid, distance, hash::decode, hash::encode
  • http::xxxx()
    • head, get, put, post, patch, delete
  • is::xxxx()
    • alphanum, alpha, domain, email, hexadecimal, latitude, longitude, numeric, semver, url, uuid, url, datetime

SurQL の組み込み関数 (cont.)

  • math::xxxx()
    • abs, bottom, ceil, fixed, floor, interquartile, max, mean, midhinge, min, mode, nearestrank, percentile, round, spread, sqrt, stddev, sum, top, trimean, variance, pow
  • not()
  • parse::email()
    • host, user
  • parse::url::xxxx)
    • domain, fragment, host, port, path, query, scheme

SurQL の組み込み関数 (cont.)

  • rand()
  • rand::xxxx()
    • bool, enum, float, guid, int, string, time, uuuid:v4, uuid:v7, uuid
  • session::xxxx()
    • db, id, ip, ns, origin, sc, sd, token,

SurQL の組み込み関数 (cont.)

  • string::xxxx()
    • concat, endsWith, join, length, lowercase, repeat, replace, reverse, slice, slug, split, startsWith, trim, uppercase, words
  • time::xxxx()
    • day, floor, format, group, hour, minute, month, nano, now, round, second, unix, wday, yday, year, timezone
  • type::xxxx()
    • bool, datetime, decimal, duration, float, int, number, point, regex, table, thin

SurQL の組み込み常数

  • math::xxxx
    • E, FRAC_1_PI, FRAC_1_SQORT_2, FRAC_2_PI, FRAC_2_SQRT_PI, FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, FRAC_PI_8, LN_10, LN_2, LOGO10_2, LOG10_E, LOG2_10, LOG2_E, PI, SQRT_2, TAU

LET パラメタ設定

  • 数値、文字列などオブジェクトを変数に設定できる
  • クエリから $変数 として参照できる
LET $test = { some: 'thing', other: true };
SELECT * FROM $test WHERE some = 'thing';

JavaScriptでSurQLを拡張

  • SurrealDB からの全ての値は、JavaScript の型に自動変換
  • JavaScript 関数からの戻り値は、SurrealDB の値に自動変換
  • ブール値、整数、フロート、文字列、配列、オブジェクト、および日付オブジェクトは
    、すべて自動的に SurrealDB の値に変換または、SurrealDB の値から変換される
CREATE user:test SET created_at = function() {
        return new Date();
};

JavaScript拡張のサンプル 1

CREATE platform:test SET version = function() {
            const { platform } = await import('os');
            return platform();
        };

JavaScript拡張のサンプル 2

LET $value = 'SurrealDB';
LET $words = ['awesome', 'advanced', 'cool'];
CREATE article:test SET summary = function($value, $words) {
    return `${arguments[0]} is ${arguments[1].join(', ')}`;
};

JavaScript拡張のサンプル 3

CREATE film:test SET
    ratings = [
        { rating: 6.3 },
        { rating: 8.7 },
    ],
    display = function() {
        return this.ratings.filter(r => {
            return r.rating >= 7;
            }).map(r => {
                return { ...r,
                    rating: Math.round(r.rating * 10) };
            });
    };

PythonでのRETful APIの簡単な実装例

from urllib.request import Request, urlopen
import base64
import json
from pprint import pprint as pp

_BASE_URL = "http://dev00:8000"
_NAMESPACE = "test"
_DATABASE = "test"
_USER = "root"
_PASS = "root"

auth_str = f"{_USER}:{_PASS}".encode("utf-8")
credential = base64.b64encode(auth_str)
auth = "Basic " + credential.decode("utf-8")

headers = {
    "Accept": "application/json",
    "Authorization": auth,
    "NS": _NAMESPACE,
    "DB": _DATABASE,
}


url = _BASE_URL + "/key/human"
request = Request(url, headers=headers)

with urlopen(request) as res:
    data = res.read()
    pp(json.loads(data)[0]['result'])

PythonでのRETful API経由でクエリを行う例

import requests
from requests.auth import HTTPBasicAuth
from pprint import pprint as pp

_URL = "http://dev00:8000/sql"
_NAMESPACE = "test"
_DATABASE = "test"
_USER = "root"
_PASS = "root"

_headers = {
  'Content-Type': 'application/json',
  'Accept':'application/json',
  'ns': _NAMESPACE,
  'db': _DATABASE
}
_auth = HTTPBasicAuth(_USER, _PASS)

def db(query):
    res = requests.post( _URL,
          headers=_headers,
         auth = _auth,
         data=query )
  if "code" in res.json():
      raise Exception(res.json())
  return res.json()

if __name__ == '__main__':
    while True:
      sql = input('SQL> ')
      if sql.upper() == 'Q':
          break
      val = db(sql)
      pp(val)

PythonでのWebSocket経由でクエリを行う例

import asyncio
from surrealdb import WebsocketClient
from pprint import pprint as pp

_URL = "ws://dev00:8000/rpc"
_NAMESPACE = "test"
_DATABASE = "test"
_USER = "root"
_PASS = "root"

async def main():
    async with WebsocketClient( url=_URL,
        namespace=_NAMESPACE, database=_DATABASE,
        username=_USER, password=_PASS,
    ) as session:
        while True:
            sql = input('SQL> ')
            if sql.upper() == 'Q': break
            res = await session.query(sql)
            pp(res)

PyPIのものはWebSocketでうまく接続できない

その他の実装例

留意するべき項目

  • surrealdb-1.0.0-beat8 で使用できるバックエンドDB
    • RocksDB, TiKV
    • FoundationDB、IndexedDB はリビルドが必要
    • IndexedDB は組み込み用途での利用が本来かも
  • DATABASEにアクセスするためにはNAMESPACEが必要
    • NAMESPACEが異なると同じ名前のDATABASEも別物になる
  • 1つのNAMESPACEにはいくつでもDATABASEを作成できる
    • ただしNAMESPACEへのアクセス権限が必要/

これはバグでしょう!

  • 一部のSurQLはCLIではうまく動作しない
    • sqlサブコマンドはリターンキー押下でHTTP RestfulAPIアクセス
    • セミコロンの入力までを1つのクエリと認識しない
    • import サブコマンドであたえるファイル中ではOK
    • Copy&Pasteでリターン押下ならOK
  • コメントは受け付けつけるけど、コメントだけだとエラー
    • 空文字(’’)を実行したことになるため
  • LETで定義した変数はリターンキー押下で消失する
  • トランザクションは途中でリターンキー押下するとNG
  • USE もリターンキー押下で宣言が消失

まとめ

  • インストールと設定が驚くほど簡単
  • Webアプリとの親和性がとてもとても高い
  • JavaScriptで拡張可能な強力なクエリ
  • ビジネス ロジックとユーザー認証をデータベース内で直接処理が可能
  • バックエンド技術スタックの簡素化→開発期間を短縮→コスト削減
  • まだ未完成ではあるものの、将来性は非常に高い

参考資料

オススメのRESTクライアント

付録

Server のオプションと環境変数

  • DB_PATH: データの格納先 (memory)
  • USER ROOT認証のユーザ名 (root)、--user/-u
  • PASS ROOT認証のユーザに対してのパスワード、--pass/-p
  • ADDR ROOT認証を許可するサブネット (127.0.0.1/32)、--addr
  • BIND コネクションを待ち受けるホスト名/IPアドレス(0.0.0.0:8000)、--bind/-b
  • KEY ON-DISK暗号化のための秘密鍵、--key/-k
  • KVS_CA KVS接続のためのCAファイル、--kvs-ca
  • KVS_CRT KVS接続のためのCERTファイル、--kvs-crt
  • KVS_KEY KVS接続のための秘密鍵、--kvs-key
  • WEB_CRT SSL接続で待ち受けるためのCERTファイル, --web-crt
  • WEB_KEY SSL接続で待ち受けるための秘密鍵、--web-key
  • STRICT 設定されていればSTRICTモードで起動、--strict
  • LOG ログレベル "warn", ["info"], "debug", "trace", "full"、--log

ACID:トランザクションを定義する4つの特性

  • Atomicity(原子性)
    • ランザクションの各ステートメントは1 つの単位として扱われる
  • Consistency(一貫性)
    • トランザクションがテーブルに、事前定義された予測可能な方法でのみ変更を加え
      ることを保証
  • Isolation(独立性)
    • 複数のユーザーが同じテーブルで読み書きを同時に実行しても、各要求は単独で発
      生しているように扱われる
  • Durability(永続性
    • システム障害が発生した場合でも、正常に実行されたトランザクションによるデー
      タの変更が保存されることを保証

N+1問題

  • データベースアクセスでクエリが合計 N+1 回実行されてしまう問題
    • SELECT を 1 回実行し、N レコードを取得
    • Nレコードに関連するデータを取得するSELECT を N 回実行
  • ORMを使っているときに裏側で発生しやすい
  • アプリケーションの動作が重く(遅く)なる原因になりやすい

RocksDB

  • 人気の高い高性能な組み込み型KVSデータベース
  • Meta(Facebook)社が開発したLevelDBのフォーク
  • Facebook、Yahoo!、LinkedInなど様々なWebサービスのプロダクションで使用されている
  • データの永続化の実現と同時に、性能と安全性を高めている
  • CPUの数が多ければ、性能が線形に増加する
  • RocksDBの性能はプラットフォームのチューニングに強く影響する
    • 設定可能なパラメータが多く複雑なため簡単ではない

TiKV

  • TiDBのバックエンドで動作するKVSデータベース
  • データの永続化(with RocksDB)
  • 分散型データベースのデータ整合性の保証
  • MVCC(Multi-Version Concurrency Control)
  • 分散トランザクションの実現
  • Google Parcorator / 2PC(2 Phase Commit)
  • Coprocessor

TiKVのアーキテクチャ

tikv_stack.png

IndexedDB

  • ブラウザベースの組み込み型KVSデータベース
    • ユーザーのブラウザー内にデータを永続的に保存する
    • ネットワークの状態にかかわらず高度なクエリ機能を持つWebアプリを作成できる
  • スキーマーレス
  • ACIDトランザクションをサポート
  • 非同期処理
  • マルチバージョン並行処理
  • Cookie消去する程度の気軽さでユーザがデータを削除できてしまう
  • ブラウザを選ぶ
  • 一定容量を超えると、IndexedDBにデータ登録できなくなる
  • IndexedDBに登録できるデータ量は、環境によって変化する

FoundationDB

  • ACIDトランザクションをサポートするNoSQL
  • SQLで操作可能
  • キーはソートされる
  • SSDを使う場合で、1コアで20,000書き込み/秒のスループット
  • 500コアまでリニアにスケール
  • 読み込みは1ms、書き込みは5ms
  • クラスタ構成で分散/冗長化ができるようになる
    • 最低1ノードで構成可能(冗長化はない/あとからノード追加可能)

バイナリリリースはFoundationDBが無効

  • FoundationDBのバージョンに依存するため features で指定されている
$ cargo feature surreal
   Avaliable features for `surreal`
default = ["storage-rocksdb", "scripting", "http"]
http = ["surrealdb/http"]
scripting = ["surrealdb/scripting"]
storage-fdb = ["surrealdb/kv-fdb-6_3"]
storage-rocksdb = ["surrealdb/kv-rocksdb"]
storage-tikv = ["surrealdb/kv-tikv"]

$ grep kv-fdb- lib/Cargo.toml
kv-fdb-5_1 = ["foundationdb/fdb-5_1", "kv-fdb"]
kv-fdb-5_2 = ["foundationdb/fdb-5_2", "kv-fdb"]
kv-fdb-6_0 = ["foundationdb/fdb-6_0", "kv-fdb"]
kv-fdb-6_1 = ["foundationdb/fdb-6_1", "kv-fdb"]
kv-fdb-6_2 = ["foundationdb/fdb-6_2", "kv-fdb"]
kv-fdb-6_3 = ["foundationdb/fdb-6_3", "kv-fdb"]
kv-fdb-7_0 = ["foundationdb/fdb-7_0", "kv-fdb"]
kv-fdb-7_1 = ["foundationdb/fdb-7_1", "kv-fdb"]

Avaliable の表記は cargo-featureのバグ。プルリク発行しときました。

SurrealDBのリビルド

  • rust の開発環境を整える (必要があれば)
$ curl -sSf https://sh.rustup.rs | sh
$ source $HOME/.cargo/env
$ rustup install stable
  • SurrealDBのリポジトリをクローン
$ git clone https://github.com/surrealdb/surrealdb.git
$ cd surrealdb
  • リビルド
$ carrgo build –release –all-features        # 2GBメモリだと失敗する
# もしくは
$ cargo build –release –features storage-fdb # TiKVは無効になる

TiKV vs FoundationDB

  • FDBのモニタリング機能は弱い

     $ fdbcli --exec 'status json'
    
  • TiKVにはPrometheusでデータ参照、Grafanaでステータスモニタリング

  • FDBはバージョン・センシティブ

  • FDBはデフォルトではメモリに格納するため設定変更が必要

  • FDBはC++、TiKVはRustで実装

  • FDBは最低1ノードでサービスできるが、TiKVは最低3ノードが必要

RocksDBをレプリケーション

  • rocksplicator を使うとリアルタイムレプリケーションができる
  • ただし、Rocksplicatorは、Pinterestによって積極的に保守・サポートされていない、アーカイブされたプロジェクトであることに注意

バイナリファイルを格納することについて

SurQL構文

INFO

INFO FOR [
        KV
        | NS | NAMESPACE
        | DB | DATABASE
        | SCOPE @scope
        | TABLE @table
];

DEFINE  NAMESPACE | DATABASE …

DEFINE [
        NAMESPACE @name
        | DATABASE @name
        | LOGIN @name ON [ NAMESPACE | DATABASE ]
            [ PASSWORD @pass | PASSHASH @hash ]
        | TOKEN @name ON [ NAMESPACE | DATABASE | SCOPE ]
            TYPE @algorithm VALUE @value
        | SCOPE @name [ SESSION @duration ]
            [ SIGNUP @expression ] [ SIGNIN @expression ]
        | EVENT @name ON [ TABLE ] @table
            WHEN @expression THEN @expression

@algorithm
EDDSA, ES256, ES384, ES512, HS256, HS384, HS512,
PS256, PS384, PS512, RS256, RS384, RS51

DEFINE  TABLE

DEFINE [
        | TABLE @name
                [ DROP ]
                [ SCHEMAFULL | SCHEMALESS ]
                [ AS SELECT @projections
                        FROM @tables
                        [ WHERE @condition ]
                        [ GROUP [ BY ] @groups ]
                ]
                [ PERMISSIONS [ NONE | FULL
                        | FOR select @expression
                        | FOR create @expression
                        | FOR update @expression
                        | FOR delete @expression
                ] ]
;

DEFINE  FIELD | INDEX

DEFINE [
        | FIELD @name ON [ TABLE ] @table
                [ TYPE @type ]
                [ VALUE @expression ]
                [ ASSERT @expression ]
                [ PERMISSIONS [ NONE | FULL
                        | FOR select @expression
                        | FOR create @expression
                        | FOR update @expression
                        | FOR delete @expression
                ] ]
        | INDEX @name ON [ TABLE ] @table [ FIELDS | COLUMNS ] @fields [ UNIQUE ]
] ;

CREATE

CREATE @targets
        [ CONTENT @value
          | SET @field = @value ...
        ]
        [ RETURN [ NONE | BEFORE | AFTER | DIFF | @projections ... ]
        [ TIMEOUT @duration ]
        [ PARALLEL ]
;

REMOVE

REMOVE [
        NAMESPACE @name
        | DATABASE @name
        | LOGIN @name ON [ NAMESPACE | DATABASE ]
        | TOKEN @name ON [ NAMESPACE | DATABASE ]
        | SCOPE @name
        | TABLE @name
        | EVENT @name ON [ TABLE ] @table
        | FIELD @name ON [ TABLE ] @table
        | INDEX @name ON [ TABLE ] @table
] ;

INSERT

INSERT [ IGNORE ] INTO @what
        [ @value
          | (@fields) VALUES (@values)
                [ ON DUPLICATE KEY UPDATE @field = @value ... ]
        ]
;

UPDATE

UPDATE @targets
        [ CONTENT @value
          | MERGE @value
          | PATCH @value
          | SET @field = @value ...
        ]
        [ WHERE @condition ]
        [ RETURN [ NONE | BEFORE | AFTER | DIFF | @projections ... ]
        [ TIMEOUT @duration ]
        [ PARALLEL ]
;

DELETE

DELETE @targets
        [ WHERE @condition ]
        [ RETURN [ NONE | BEFORE | AFTER | DIFF | @projections ... ]
        [ TIMEOUT @duration ]
        [ PARALLEL ]
;

SELECT

SELECT @projections
        FROM @targets
        [ WHERE @condition ]
        [ SPLIT [ AT ] @field ... ]
        [ GROUP [ BY ] @field ... ]
        [ ORDER [ BY ]
                @field [RAND()| COLLATE| NUMERIC ] [ ASC | DESC ] ...
        ]
        [ LIMIT [ BY ] @limit ]
        [ START [ AT ] @start ]
        [ FETCH @field ... ]
        [ TIMEOUT @duration ]
        [ PARALLEL ]
;

USE

USE
  [ NAMESPACE | NS ] @namespace
  [[ DATABASE | DB ] @database ] ;
71
40
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
71
40