【7日目】シンプルデータベース実装⑤

# まえがき
前回実装した、シンプルデータベースサーバを利用する側となるクライアントソースを公開します。紹介するソースは下記の2つです。一つは全ての操作を実行するための簡単なクライアントです。もう一つは、100万件のレコードを挿入、更新、削除するちょっとした性能テストクライアントです。

* client_1.erl
* client_perf.erl

## 単純クライアント
テーブルを作成し、データを挿入、更新、取得、削除した後、テーブルを削除するクライアント。

```Erlang:client_1.erl
-module(client_1).
-compile(export_all).

exec() ->
    {ok, Pid} = simple_db_server:start_link(),
    simple_db_server:create_table(Pid, fruit, [name, price]),
    simple_db_server:insert_data(Pid, fruit, [apple, 100]),
    simple_db_server:insert_data(Pid, fruit, [banana, 150]),
    simple_db_server:insert_data(Pid, fruit, [orange, 150]),
    io:format("Step1:~p~n", [simple_db_server:select_data(Pid, fruit, name, apple)]),
    io:format("Step1:~p~n", [simple_db_server:select_data(Pid, fruit, name, banana)]),
    io:format("Step1:~p~n", [simple_db_server:select_data(Pid, fruit, name, orange)]),
    simple_db_server:update_data(Pid, fruit, [{price, 120}], name, apple),
    simple_db_server:update_data(Pid, fruit, [{price, 160}], price, 150),
    io:format("Step2:~p~n", [simple_db_server:select_data(Pid, fruit, name, apple)]),
    io:format("Step2:~p~n", [simple_db_server:select_data(Pid, fruit, name, banana)]),
    io:format("Step2:~p~n", [simple_db_server:select_data(Pid, fruit, name, orange)]),
    simple_db_server:delete_data(Pid, fruit, name, banana),
    io:format("Step3:~p~n", [simple_db_server:select_data(Pid, fruit, name, apple)]),
    io:format("Step3:~p~n", [simple_db_server:select_data(Pid, fruit, name, banana)]),
    io:format("Step3:~p~n", [simple_db_server:select_data(Pid, fruit, name, orange)]),
    simple_db_server:drop_table(Pid, fruit),
    %% ↓を実行するとets:lookupでエラー落ちする
    % io:format("Step4:~p~n", [simple_db_server:select_data(Pid, fruit, name, apple)]),
    % io:format("Step4:~p~n", [simple_db_server:select_data(Pid, fruit, name, banana)]),
    simple_db_server:stop(Pid),
    ok.
```
erlコマンドでErlangの対話シェルを開いて下記のように実行できます。

```Erlang
1> c(simple_db_server).  
simple_db_server.erl:2: Warning: export_all flag enabled - all functions will be exported
{ok,simple_db_server}
2> c(client_1).
client_1.erl:2: Warning: export_all flag enabled - all functions will be exported
{ok,client_1}
3> client_1:exec().
Step1:[[apple,100]]
Step1:[[banana,150]]
Step1:[[orange,150]]
Step2:[[apple,120]]
Step2:[[banana,160]]
Step2:[[orange,160]]
Step3:[[apple,120]]
Step3:not_found
Step3:[[orange,160]]
Server teminated.
ok
```

## 性能テストクライアント
性能テストとして100万件の挿入、更新、削除をしてみます。
カラムを一つだけ持つテーブルを用意し、ランダムな値を挿入していきます。生成されるランダムな値は1からCardinalityの値の間の整数値です。PKや一意制約などないので、ただランダムな値が一つだけ入ったレコードを大量に持つテーブルができあがります。

明らかな性能問題として、カーディナリティが小さいカラムがある場合（例えば性別）、カラムインデックスに対する操作がめちゃくちゃ遅くなります。それは、例えば100万件のレコードを挿入した場合、性別カラムインデックスには"男性"というキーに対して、50万件のオブジェクトIDのリストがバリューとして保持され、"女性"というキーに対しても同じように50万件のオブジェクトIDのリストを保持するため、そのリストを走査するのに時間がかかってしまうからです。
そのため、カーディナリティが小さくなりすぎないよう注意する必要があります。(DBとして実用性がない・・・)

```Erlang:client_perf.erl
-module(client_perf).
-compile(export_all).

exec() ->
    %% カーディナリティが小さい場合、カラムインデックスが肥大化しやすいため性能劣化が激しい
    %% Cardinality/Count > 10% ぐらいは必要
    Cardinality = 100000,
    Count = 1000000,
    {ok, Pid} = simple_db_server:start_link(),
    simple_db_server:create_table(Pid, random, [random_val]),
    RandomValList = [rand:uniform(Cardinality) || _ <- lists:seq(1, Count)],
    UsortedRandomValList = lists:usort(RandomValList),
    UpdatedRandomValList = lists:map(fun(X) -> X - 1 end, UsortedRandomValList),
    % io:format("~p~n~p~n", [RandomValList, UsortedRandomValList]),
    io:format("start insert~n"),
    StartInsert = erlang:system_time(microsecond),
    lists:map(fun(Val) -> simple_db_server:insert_data(Pid, random, [Val]) end, RandomValList),
    EndInsert = erlang:system_time(microsecond),
    io:format("~p[microsec]~n", [EndInsert - StartInsert]),
    % io:format("~p~n", [ets:select(random, [{{'$1','$2'}, [{is_integer, '$1'}], ['$$']}])]),
    io:format("start update~n"),
    StartUpdate = erlang:system_time(microsecond),
    lists:map(fun(Val) -> simple_db_server:update_data(Pid, random, [{random_val, Val-1}], random_val, Val) end,
    UsortedRandomValList),
    EndUpdate = erlang:system_time(microsecond),
    % io:format("~p~n", [ets:select(random, [{{'$1','$2'}, [{is_integer, '$1'}], ['$$']}])]),
    io:format("~p[microsec]~n", [EndUpdate - StartUpdate]),
    io:format("start delete~n"),
    StartDelete = erlang:system_time(microsecond),
    lists:map(fun(Val) -> simple_db_server:delete_data(Pid, random, random_val, Val) end,
    UpdatedRandomValList),
    EndDelete = erlang:system_time(microsecond),
    io:format("~p[microsec]~n", [EndDelete - StartDelete]),
    simple_db_server:drop_table(Pid, random),
    simple_db_server:stop(Pid),
    ok.
```

私のMacBookProで実行すると下記のような時間になりました。

```YAML:スペック情報
ハードウェアの概要:

  機種名:	MacBook Pro
  機種ID:	MacBookPro14,1
  プロセッサ名:	Dual-Core Intel Core i5
  プロセッサ速度:	2.3 GHz
  プロセッサの個数:	1
  コアの総数:	2
  二次キャッシュ（コア単位）:	256 KB
  三次キャッシュ:	4 MB
```

```Erlang
4> c(client_perf).
client_perf.erl:2: Warning: export_all flag enabled - all functions will be exported
{ok,client_perf}
5> client_perf:exec().
start insert
4602570[microsec]
start update
3836056[microsec]
start delete
2956546[microsec]
Server teminated.
ok
```
カーディナリティが大きいため、致命的な性能問題にはなっていませんね。
ただカーディナリティを小さくすると途端に動かなくなってしまいます><

# まとめ
とりあえず、自分が実装したsimple_db_serverのクライアントソースを書いてみました。
クライアント1：サーバ1ならそこまで大きな問題は見られませんでしたね。
ここからはいよいよRDBMSの本題とも言える部分の実装に入っていく予定です。