第十五章 ETS和DETS:大資料的儲存機制
ETS和DETS都提供"鍵-值"搜尋表, 只不過ETS駐留在記憶體而DETS駐留在磁碟, 因此ETS高效但資料儲存是臨時的, DETS資料儲存是持久的且節省記憶體但比較低效。
15.1 表的基本操作
- 建立和開啟表
ets:new或dets:open_file
- 插入表
insert(TableName, X)
- 查詢元組
lookup(TableName, Key)
- 釋放表
ets:delete(TableId)或dets:close(TableId)
15.2 表的型別
- set表
每個元組的鍵值不能相同 - ordered set表
排序的set表 - bag表
多個元組可以有相同的鍵值, 但不能有兩個完全相同的元組 - duplicate bag表
多個元組可以有相同的鍵值, 且同一個元組可以在表中出現多次
程式碼示例
-module(ets_test). -export([start/0]). %% 分別以四種模式建立表並插入資料, 然後輸出表中的資料檢視異同 start() -> lists:foreach(fun test_ets/1, [set, ordered_set, bag, duplicate_bag]). test_ets(Mode) -> TableId = ets:new(test, [Mode]), ets:insert(TableId, {a, 1}), ets:insert(TableId, {b, 2}), ets:insert(TableId, {a, 1}), ets:insert(TableId, {a, 3}), List = ets:tab2list(TableId), io:format("~-13w => ~p~n", [Mode, List]), ets:delete(TableId).
執行結果:
1> ets_test:start().
set => [{b,2},{a,3}]
ordered_set => [{a,3},{b,2}]
bag => [{b,2},{a,1},{a,3}]
duplicate_bag => [{b,2},{a,1},{a,1},{a,3}]
ok
15.3 ETS表的效率考慮
set表保證鍵值不相同, 因此相對耗費空間;ordered_set保證了排序, 因此相對耗費時間。
因為bag表插入資料時需要比較是否存在相同的鍵值, 因此如果大量元組存在相同鍵值, 使用bag表將會非常低效。
ETS表隸屬於建立它的程序, 隨程序消亡而消亡, 因此無需考慮垃圾回收。
儘可能的使用二進位制資料。
15.4 建立ETS表
建立ETS表的函式原型
@spec ets:new(Name, [Opt]) -> TableId
其中[Opt]的取值:
# 表型別
set | ordered_set | bag | duplicate_bag
# 許可權
private 只有所有者程序可以讀寫
public 所有可以獲取TableId的程序都可以讀寫
protected 所有可以獲取TableId的程序都可以讀, 但只有所有者程序可以寫
# 命名
named_table 是否可以使用Name來操作表
# 指定鍵的位置
{keypos, K} 通常是第一個位置, 當需要儲存記錄時, 第一個位置是記錄的名字, 因此需要指定鍵的位置
# 預設選項為[set, protected, {keypos, 1}]
15.5 ETS程式示例
15.5.1 三字索引迭代器
%% 從壓縮檔案中讀取英文單詞, 每個單詞都使用F函式處理
for_each_trigram_in_the_english_language(F, A0) ->
{ok, Bin0} = file:read_file("354984si.ngl.gz"),
Bin = zlib:gunzip(Bin0),
scan_word_list(binary_to_list(Bin), F, A0).
scan_word_list([], _, A) ->A;
scan_word_list(L, F, A) ->
%% 遍歷單詞, 在單詞前新增空格
{Word, L1} = get_next_word(L, []),
A1 = scan_trigrams([$\s|Word], F, A),
scan_word_list(L1, F, A1).
%% 單詞匹配, 遇到換行符則新增空格
get_next_word([$\r, $\n|T], L) ->{reverse([$\s|L]), T};
get_next_word([H|T], L) ->get_next_word(T, [H, L]);
get_next_word([], L) ->{reverse([$\s|L]), []}.
%% 建立三字索引
scan_trigrams([X, Y, Z], F, A) ->F([X, Y, Z], A);
scan_trigrams([X, Y, Z|T], F, A) ->
A1 = F([X, Y, Z], A),
scan_trigrams([Y, Z|T], F, A1);
scan_trigrams(_, _, A) ->A.
15.5.2 構造表
%% 分別建立兩者型別的ets表
make_ets_ordered_set() ->make_a_set(ordered_set, "trigramsOS.tab").
make_ets_set() ->make_a_set(set, "trigramsS.tab").
%% 根據指定型別建立ets表, 匯入資料後轉成檔案儲存
make_a_set(Type, FileName) ->
Tab = ets:new(table, [Type]),
%% 宣告遍歷英文單詞時所使用的函式
%% 這裡將每個單詞插入到ets表
F = fun(Str, _) ->ets:insert(Tab, {list_to_binary(Str)}) end,
for_each_trigram_in_the_english_language(F, 0),
%% 轉換為檔案, 獲取記錄數後在記憶體中刪除ets表
ets:tab2file(Tab, FileName),
Size = ets:info(Tab, size),
ets:delete(Tab),
Size.
make_mod_set() ->
%% 使用sets儲存單詞
D = sets:new(),
F = fun(Str, Set) ->sets:add_element(list_to_binary(Str), Set) end,
D1 = for_each_trigram_in_the_english_language(F, D),
file:write_file("trigrams.set", [term_to_binary(D1)]).
15.5.3 構造表有多快
%% 統計有多少個單詞
how_many_trigrams() ->
%% 遇到一個單詞就將累加器增加1
F = fun(_, N) ->1 + N end,
for_each_trigram_in_the_english_language(F, 0).
make_tables() ->
%% 統計有多少個三字索引
{Micro1, N} = timer:tc(?MODULE, how_many_trigrams, []),
io:format("Counting - No of trigrams=~p time/trigram=~p~n", [N, Micro1/N]),
%% 統計排序的ets表平均佔用儲存和時間
{Micro2, Ntri} = timer:tc(?MODULE, make_ets_ordered_set, []),
FileSize1 = filelib:file_size("trigramsOS.tab"),
io:format("Ets ordered Set size=~p time/trigram=~p~n", [FileSize1/Ntri, Micro2/N]),
%% 統計普通ets表平均佔用儲存和時間
{Micro3, _} = timer:tc(?MODULE, make_ets_set, []),
FileSize2 = filelib:file_size("trigramsS.tab"),
io:format("Ets set size=~p time/trigram=~p~n", [FileSize2/Ntri, Micro3/N]),
%% 統計使用set方式平均佔用儲存和時間
{Micro4, _} = timer:tc(?MODULE, make_mod_set, []),
FileSize3 = filelib:file_size("trigrams.set"),
io:format("Module sets size=~p time/trigram=~p~n", [FileSize3/Ntri, Micro4/N]).
執行結果:
1> c(lib_trigrams).
{ok,lib_trigrams}
2> lib_trigrams:make_tables().
Counting - No of trigrams=3357707 time/trigram=0.1504470163715893
Ets ordered Set size=19.029156153630503 time/trigram=0.6226862558287546
Ets set size=19.028408559947668 time/trigram=0.40671625010758833
Module sets size=9.433978132884777 time/trigram=1.728039700903027
ok
共有3 357 707個三字索引, 處理每個三字索引平均時間為0.15微秒;
ordered_set型別的ETS表平均每個三字索引佔用19位元組耗費0.62微秒;
普通型別的ETS表平均每個三字索引佔用19位元組耗費0.41微秒;
而使用Erlang的集合模組平均儲存佔用9位元組耗費1.73微秒
15.5.4 訪問表有多快
lookup_all_ets(Tab, L) ->
lists:foreach(fun({K}) ->ets:lookup(Tab, K) end, L).
time_lookup_module_sets() ->
%% 讀取檔案轉成二進位制資料
{ok, Bin} = file:read_file("trigrams.set"),
%% 二進位制資料轉成列表
Set = binary_to_term(Bin),
Keys = sets:to_list(Set),
Size = length(Keys),
{M, _} = timer:tc(?MODULE, lookup_all_set, [Set, Keys]),
io:format("Module set lookup=~p micro seconds~n", [M/Size]).
%% 遍歷集合
lookup_all_set(Set, L) ->
lists:foreach(fun(Key) ->sets:is_element(Key, Set) end, L).
執行結果:
1> lib_trigrams:timer_tests().
Ets ordered Set lookup=0.45313522100738246 micro seconds
Ets set lookup=0.1906363891225119 micro seconds
Module set lookup=0.31632557704887393 micro seconds
ok
15.5.5 勝出的是…
%% 查詢某個字串是否為單詞
%% 表中儲存時已經添加了空格, 因此這裡要補全
is_word(Tab, Str) ->is_word1(Tab, "\s" ++ Str ++ "\s").
%% 查詢符合[A, B, C]格式的字串是否為單詞
is_word1(Tab, [_, _, _] = X) ->is_this_a_trigram(Tab, X);
is_word1(Tab, [A, B, C|D]) ->
case is_this_a_trigram(Tab, [A, B, C]) of
true ->is_word1(Tab, [B, C|D]);
false ->false
end;
is_word1(_, _) ->false.
%% 查詢ETS表中是否存在此索引
is_this_a_trigram(Tab, X) ->
case ets:lookup(Tab, list_to_binary(X)) of
[] ->false;
_ ->true
end.
%% 找到與原始碼檔案同目錄下的資料檔案並轉換成ETS表
open() ->
{ok, I} = ets:file2tab(filename:dirname(code:which(?MODULE)) ++ "/trigramsS.tab"),
I.
close(Tab) ->ets:delete(Tab).
執行結果
1> Tab = lib_trigrams:open().
16402
2> lib_trigrams:is_word(Tab, "example").
true
15.6 DETS
如果在開啟DETS檔案後沒有關閉, 則Erlang將自動進行修復, 而修復可能會耗費大量的時間, 因此應在使用完後關閉DETS檔案。
開啟一個DETS表時, 要給出一個全域性性的名字, 以便於多個程序用相同的名字和屬性來共享這個表。
程式碼示例:
-module(lib_filenames_dets).
-export([open/1, close/0, test/0, filename2index/1, index2filename/1]).
open(File) ->
io:format("dets opened:~p~n", [File]),
Bool = filelib:is_file(File),
%% 使用?MODULE巨集獲取檔名作為TableName
case dets:open_file(?MODULE, [{file, File}]) of
{ok, ?MODULE} ->
case Bool of
true ->void;
%% 插入鍵為free, 值為1的記錄, 用於統計表中記錄的總數
false ->ok = dets:insert(?MODULE, {free, 1})
end,
true;
{error, _Reason} ->
io:format("cannot open dets table~n"),
exit(eDetsOpen)
end.
close() ->dets:close(?MODULE).
%% 將二進位制格式的檔名插入到dets表
filename2index(FileName) when is_binary(FileName) ->
%% 插入前先查詢檔名是否已經存在
case dets:lookup(?MODULE, FileName) of
[] ->
%% 不存在則先查詢最大索引, 然後將索引-檔名, 檔名-索引, free-最大索引插入到dets表
[{_, Free}] = dets:lookup(?MODULE, free),
ok = dets:insert(?MODULE, [{Free, FileName}, {FileName, Free}, {free, Free+1}]),
Free;
%% 存在則直接返回索引值
[{_, N}] ->N
end.
%% 根據索引值查詢檔名
index2filename(Index) when is_integer(Index) ->
case dets:lookup(?MODULE, Index) of
[] ->error;
[{_, Bin}] ->Bin
end.
test() ->
%% 每次測試都重建dets表
file:delete("./filenames.dets"),
open("./filenames.dets"),
%% 使用正則查詢當前目錄下的所有erl檔案新增到dets表中
F = lib_files_find:files(".", ".*[.](erl).*$", true),
lists:foreach(fun(I) ->filename2index(list_to_binary(I)) end, F).
執行結果:
1> c(lib_filenames_dets).
{ok,lib_filenames_dets}
2> lib_filenames_dets:test().
dets opened:"./filenames.dets"
ok
3> lib_filenames_dets:index2filename(1).
<<"./lib_trigrams.erl">>
4> lib_filenames_dets:index2filename(2).
<<"./lib_files_find.erl">>
5> lib_filenames_dets:index2filename(3).
<<"./lib_filenames_dets.erl">>
6> lib_filenames_dets:index2filename(4).
<<"./ets_test.erl">>
7> lib_filenames_dets:filename2index(list_to_binary("./ets_test.erl")).
4
15.7 我們沒有提及的部分
ETS和DETS的線上手冊:
http://www.erlang.org/doc/man/ets.html
http://www.erlang.org/doc/man/dets.html