まえがき
前回実装した、シンプルデータベースサーバを利用する側となるクライアントソースを公開します。紹介するソースは下記の2つです。一つは全ての操作を実行するための簡単なクライアントです。もう一つは、100万件のレコードを挿入、更新、削除するちょっとした性能テストクライアントです。
- client_1.erl
- client_perf.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として実用性がない・・・)
-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の本題とも言える部分の実装に入っていく予定です。