LoginSignup
5
1

More than 3 years have passed since last update.

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

Posted at

まえがき

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

  • client_1.erl
  • client_perf.erl

単純クライアント

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

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の対話シェルを開いて下記のように実行できます。

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として実用性がない・・・)

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で実行すると下記のような時間になりました。

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

  機種名:    MacBook Pro
  機種ID: MacBookPro14,1
  プロセッサ名:   Dual-Core Intel Core i5
  プロセッサ速度:    2.3 GHz
  プロセッサの個数: 1
  コアの総数:  2
  二次キャッシュ(コア単位):  256 KB
  三次キャッシュ:    4 MB
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の本題とも言える部分の実装に入っていく予定です。

5
1
0

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
5
1