檢視原始碼 表格與資料庫
Ets、Dets 和 Mnesia
每個使用 Ets 的範例在 Mnesia 中都有對應的範例。一般來說,所有 Ets 的範例也適用於 Dets 表格。
Select/Match 操作
在 Ets 和 Mnesia 表格上的 Select/match 操作可能會變成非常耗費資源的操作。它們通常需要掃描整個表格。嘗試組織資料以盡量減少對 select/match 操作的需求。然而,如果您確實需要 select/match 操作,它仍然比使用 tab2list
更有效率。以下章節提供了這些範例,以及如何避免 select/match 的方法。函式 ets:select/2
和 mnesia:select/3
優先於 ets:match/2
、ets:match_object/2
和 mnesia:match_object/3
。
在某些情況下,select/match 操作不需要掃描整個表格。例如,當搜尋 ordered_set
表格時,如果部分鍵已綁定,或者如果是 Mnesia 表格,且所選取/匹配的欄位上有次要索引。如果鍵已完全綁定,則沒有必要執行 select/match,除非您有一個 bag 表格,且僅對具有特定鍵的元素子集感興趣。
當建立要在 select/match 操作中使用的記錄時,您會希望大多數欄位的值為 _
。最簡單和最快的方法如下:
#person{age = 42, _ = '_'}.
刪除元素
如果該元素不存在於表格中,則 delete
操作會被視為成功。因此,所有在刪除之前檢查該元素是否存在於 Ets/Mnesia 表格中的嘗試都是不必要的。以下是 Ets 表格的範例:
應該做
ets:delete(Tab, Key),
不應該做
case ets:lookup(Tab, Key) of
[] ->
ok;
[_|_] ->
ets:delete(Tab, Key)
end,
擷取資料
不要擷取您已有的資料。
假設您有一個模組處理抽象資料類型 Person
。您匯出介面函式 print_person/1
,它使用內部函式 print_name/1
、print_age/1
和 print_occupation/1
。
注意
如果函式
print_name/1
等等是介面函式,情況就會有所不同,因為您不希望介面的使用者知道內部資料表示。
應該做
%%% Interface function
print_person(PersonId) ->
%% Look up the person in the named table person,
case ets:lookup(person, PersonId) of
[Person] ->
print_name(Person),
print_age(Person),
print_occupation(Person);
[] ->
io:format("No person with ID = ~p~n", [PersonID])
end.
%%% Internal functions
print_name(Person) ->
io:format("No person ~p~n", [Person#person.name]).
print_age(Person) ->
io:format("No person ~p~n", [Person#person.age]).
print_occupation(Person) ->
io:format("No person ~p~n", [Person#person.occupation]).
不應該做
%%% Interface function
print_person(PersonId) ->
%% Look up the person in the named table person,
case ets:lookup(person, PersonId) of
[Person] ->
print_name(PersonID),
print_age(PersonID),
print_occupation(PersonID);
[] ->
io:format("No person with ID = ~p~n", [PersonID])
end.
%%% Internal functions
print_name(PersonID) ->
[Person] = ets:lookup(person, PersonId),
io:format("No person ~p~n", [Person#person.name]).
print_age(PersonID) ->
[Person] = ets:lookup(person, PersonId),
io:format("No person ~p~n", [Person#person.age]).
print_occupation(PersonID) ->
[Person] = ets:lookup(person, PersonId),
io:format("No person ~p~n", [Person#person.occupation]).
非持久性資料庫儲存
對於非持久性資料庫儲存,請優先使用 Ets 表格而不是 Mnesia 的 local_content
表格。即使是 Mnesia 的 dirty_write
操作,與 Ets 寫入相比,也帶有固定的額外負擔。Mnesia 必須檢查表格是否已複寫或有索引,這表示每次 dirty_write
至少需要一次 Ets 查詢。因此,Ets 寫入總是比 Mnesia 寫入更快。
tab2list
假設一個 Ets 表格使用 idno
作為鍵,並包含以下內容:
[#person{idno = 1, name = "Adam", age = 31, occupation = "mailman"},
#person{idno = 2, name = "Bryan", age = 31, occupation = "cashier"},
#person{idno = 3, name = "Bryan", age = 35, occupation = "banker"},
#person{idno = 4, name = "Carl", age = 25, occupation = "mailman"}]
如果您必須傳回 Ets 表格中儲存的所有資料,您可以使用 ets:tab2list/1
。然而,通常您只對部分資訊感興趣,在這種情況下,ets:tab2list/1
效率較低。如果您只想從每筆記錄中提取一個欄位,例如,每個人的年齡,那麼:
應該做
ets:select(Tab, [{#person{idno='_',
name='_',
age='$1',
occupation = '_'},
[],
['$1']}]),
不應該做
TabList = ets:tab2list(Tab),
lists:map(fun(X) -> X#person.age end, TabList),
如果您只對所有名為「Bryan」的人的年齡感興趣,那麼:
應該做
ets:select(Tab, [{#person{idno='_',
name="Bryan",
age='$1',
occupation = '_'},
[],
['$1']}])
不應該做
TabList = ets:tab2list(Tab),
lists:foldl(fun(X, Acc) -> case X#person.name of
"Bryan" ->
[X#person.age|Acc];
_ ->
Acc
end
end, [], TabList)
如果您需要 Ets 表格中儲存的所有關於名為「Bryan」的人的資訊,那麼:
應該做
ets:select(Tab, [{#person{idno='_',
name="Bryan",
age='_',
occupation = '_'}, [], ['$_']}]),
不應該做
TabList = ets:tab2list(Tab),
lists:filter(fun(X) -> X#person.name == "Bryan" end, TabList),
ordered_set
表格
如果表格中的資料需要按照表格中鍵的順序存取,則可以使用 ordered_set
表格類型,而不是更常用的 set
表格類型。ordered_set
總是按照 Erlang 項的順序遍歷鍵欄位,因此來自 select
、match_object
和 foldl
等函式的傳回值會按照鍵值排序。使用 first
和 next
操作遍歷 ordered_set
也會傳回排序過的鍵。
注意
ordered_set
只保證物件會按照鍵順序處理。即使結果中沒有包含鍵,來自ets:select/2
等函式的結果也會按照鍵順序出現。
ETS
使用 Ets 表格的鍵
Ets 表格是單一鍵表格(可以是雜湊表或依鍵排序的樹狀結構),應該這樣使用。換句話說,盡可能使用鍵來查詢內容。set
Ets 表格中已知鍵的查詢是常數時間複雜度,而 ordered_set
Ets 表格則為 O(log N)。鍵查詢總是比必須掃描整個表格的呼叫更佳。在之前的範例中,欄位 idno
是表格的鍵,而所有只知道名稱的查詢都會導致掃描整個(可能很大的)表格以尋找匹配的結果。
一個簡單的解決方案是使用 name
欄位作為鍵,而不是 idno
欄位,但如果名稱不是唯一的,這會導致問題。更通用的解決方案是建立第二個表格,以 name
作為鍵,idno
作為資料,也就是說,對表格的 name
欄位建立索引(反轉)。顯然,第二個表格必須與主表格保持一致。Mnesia 可以為您做到這一點,但是與使用 Mnesia 所涉及的額外負擔相比,自行建立的索引表格可能非常有效率。
先前範例中表格的索引表格必須是一個 bag(因為鍵可能會出現多次),並且可以包含以下內容:
[#index_entry{name="Adam", idno=1},
#index_entry{name="Bryan", idno=2},
#index_entry{name="Bryan", idno=3},
#index_entry{name="Carl", idno=4}]
有了這個索引表格,可以按照以下方式查詢所有名為「Bryan」的人的 age
欄位:
MatchingIDs = ets:lookup(IndexTable,"Bryan"),
lists:map(fun(#index_entry{idno = ID}) ->
[#person{age = Age}] = ets:lookup(PersonTable, ID),
Age
end,
MatchingIDs),
請注意,此程式碼沒有使用 ets:match/2
,而是使用 ets:lookup/2
呼叫。lists:map/2
呼叫僅用於遍歷表格中與名稱「Bryan」匹配的 idno
;因此,將主表格中的查詢次數降至最低。
在表格中插入記錄時,維護索引表格會引入一些額外負擔。因此,必須比較從表格中獲得的操作次數與在表格中插入物件的操作次數。然而,請注意,當可以使用鍵來查詢元素時,獲益會很顯著。
Mnesia
次要索引
如果您經常對不是表格鍵的欄位執行查詢,使用 mnesia:select() 或 mnesia:match_object()
會導致效能損失,因為這些函式會遍歷整個表格。相反,您可以建立次要索引,並使用 mnesia:index_read/3
來更快地存取資料,但會增加記憶體使用量。
範例
-record(person, {idno, name, age, occupation}).
...
{atomic, ok} =
mnesia:create_table(person, [{index,[#person.age]},
{attributes,
record_info(fields, person)}]),
{atomic, ok} = mnesia:add_table_index(person, age),
...
PersonsAge42 =
mnesia:dirty_index_read(person, 42, #person.age),
交易
使用交易是一種保證分散式 Mnesia 資料庫保持一致性的方法,即使許多不同的程序同時更新它也是如此。然而,如果您有即時要求,建議使用 dirty 操作而不是交易。當使用 dirty 操作時,您會失去一致性保證;通常會透過只讓一個程序更新表格來解決此問題。其他程序必須將更新請求傳送到該程序。
範例
...
%% Using transaction
Fun = fun() ->
[mnesia:read({Table, Key}),
mnesia:read({Table2, Key2})]
end,
{atomic, [Result1, Result2]} = mnesia:transaction(Fun),
...
%% Same thing using dirty operations
...
Result1 = mnesia:dirty_read({Table, Key}),
Result2 = mnesia:dirty_read({Table2, Key2}),