檢視原始碼 表格與資料庫

Ets、Dets 和 Mnesia

每個使用 Ets 的範例在 Mnesia 中都有對應的範例。一般來說,所有 Ets 的範例也適用於 Dets 表格。

Select/Match 操作

在 Ets 和 Mnesia 表格上的 Select/match 操作可能會變成非常耗費資源的操作。它們通常需要掃描整個表格。嘗試組織資料以盡量減少對 select/match 操作的需求。然而,如果您確實需要 select/match 操作,它仍然比使用 tab2list 更有效率。以下章節提供了這些範例,以及如何避免 select/match 的方法。函式 ets:select/2mnesia:select/3 優先於 ets:match/2ets:match_object/2mnesia: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/1print_age/1print_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 項的順序遍歷鍵欄位,因此來自 selectmatch_objectfoldl 等函式的傳回值會按照鍵值排序。使用 firstnext 操作遍歷 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}),