[Erlang-0008][OTP] 高效指南 -- 表和数据库(ets mnesia)

原文链接:http://www.erlang.org/doc/efficiency_guide/tablesDatabases.html

错误之处欢迎指正

 

7 表和数据库

 

7.1 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。

 

注意: 也有例外的情况可以不检索整张表,例如当检索ordered_set表时一个关键字不足以精确查找到结果,或者是Mnesia有第二索引,用这个字段去select/match。如果关键字能够精确匹配出结果,当然select/match是没有意义的,除非你有一个bag表,并且只对检索结果的一个子集感兴趣。
(NOTE:There are exceptions when the complete table is not scanned, for instance if part of the key is bound when searching an ordered_set table, or if it is a Mnesia table and there is a secondary index on the field that is selected/matched. If the key is fully bound there will, of course, be no point in doing a select/match, unless you have a bag table and you are only interested in a sub-set of the elements with the specific key.)

 

当创建一个被用作select/match操作的记录时,想绝大部分字段的值是'_'。最简单快捷的方法是下面这样

#person{age = 42, _ = '_'}.

 

删除一个元素

删除操作被当做是成功的,如果一个元素不在表里。因此,删除之前,所有尝试去检测元素是否存在于ets/mnesia表的操作都是非必要的。这里有个ets表的操作。

DO

...
ets:delete(Tab, Key),
...

DO NOT

...
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等是接口函数,那完全是另一回事了,因为你不想让接口使用者知道内部数据结构。

DO

%%% 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]).

DO NOT

%%% 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 functionss
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表。即使Mnesia的dirty_write操作只比ets写操作代价高那么一点点。Mnesia还必须检测这个表是否有别的拷贝,或者是否有索引,所以每次dirty_write至少包含一次ets lookup操作。因此ets写永远比Mnesia写快。

 

tab2list

假设我们有一个ets表,用idno作为key,内容如下:

[#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的代价就太高了。如果我们只想要每个记录的一个字段,例如年龄,应该这样做:

DO

...
ets:select(Tab,[{ #person{idno='_', 
                          name='_', 
                          age='$1', 
                          occupation = '_'},
                [],
                ['$1']}]),
...

DO NOT

...
TabList = ets:tab2list(Tab),
lists:map(fun(X) -> X#person.age end, TabList),
...

 

如果我们只对名叫Bryan的人的年龄感兴趣,应该:

DO

...
ets:select(Tab,[{ #person{idno='_', 
                          name="Bryan", 
                          age='$1', 
                          occupation = '_'},
                [],
                ['$1']}]),
...

DO NOT

...
TabList = ets:tab2list(Tab),
lists:foldl(fun(X, Acc) -> case X#person.name of
                                "Bryan" ->
                                    [X#person.age|Acc];
                                 _ ->
                                     Acc
                           end
             end, [], TabList),
...

REALLY DO NOT

...
TabList = ets:tab2list(Tab),
BryanList = lists:filter(fun(X) -> X#person.name == "Bryan" end,
                         TabList),
lists:map(fun(X) -> X#person.age end, BryanList),
...

 

如果我们需要表中名叫Bryan的人的所有信息:

DO

...
ets:select(Tab, [{#person{idno='_', 
                          name="Bryan", 
                          age='_', 
                          occupation = '_'}, [], ['$_']}]),
...

DO NOT

...
TabList = ets:tab2list(Tab),
lists:filter(fun(X) -> X#person.name == "Bryan" end, TabList),
...

 

Ordered_set表

如果表中的数据要经常被访问,那么有序的KEYS是很有意义的。ordered_set类型的表可以用来代替大部分常见的set类型表。ordered_set表的key总是按照Erlang term顺序排序,所以select,match_object,foldl的返回值也是根据Key排序的。ordered_set的first和next操作也是按照key的排序返回的。

注意:ordered_set表保证每条记录都按照key的顺序处理。ets:select/2的结果也是按照这个顺序,即使结果中不包含key

 

7.2 ets特性

 

利用好ets的Key

ets表时一个单key的表(不论哈希表还是树结构)而且应该只用一个key。换句话说,任何可能的情况下都用key去lookup。一个lookup查询对set ets表来说代价是一个常数,对于ordered_set ets表来说是O(logN)。用Key去lookup永远好于需要整表遍历。上面的例子中,字段idno是表的key,所有用姓名字段来查询的,都需要遍历整表来得到匹配结果。

 

一个简单的解决办法就是用name字段代替idno字段作为Key,但是如果名字不唯一就会有问题。更常用的解决方法是创建第二张表,名字为key,idno为数据,把这个表索引到主表的name字段。第二张表必须要和原表保持一致。mnesia可以为你做这些,但是一个自制的索引表会比用mnesia高效的多。

 

前面例子的索引表必须是个bag表(因为有重复key),内容如下:

[#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的人的年龄,应该这样做:

...
MatchingIDs = ets:lookup(IndexTable,"Bryan"),
lists:map(fun(#index_entry{idno = ID}) ->
                 [#person{age = Age}] = ets:lookup(PersonTable, ID),
                 Age
          end,
          MatchingIDs),
...

 


注意上面的代码永远不要用match/2代替lookup/2。lists:map/2只用来遍历了名为Bryan的数据,所以主表的查询操作已经最少了。

使用索引表会产生一些开销,当向表中插入记录时,因此插入的记录越多越效率越低。但是记住能用key来查询元素,意义是很大的。

 

7.3 mnesia特性

 

第二索引

如果你经常以非Key字段来查询表操作,你将会因使用mnesia:select/match_object而损失性能,因为这些函数会遍历整表。。你可以创建一个第二索引来代替,用mnesia:index_read来快速访问,但是这也会消耗更多的内存,例如:

-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数据库保持一致,即使许多不同的进程并行更新。但是如果你对实时性要求很高,推荐使用脏操作代替事务。当使用脏操作时会损失一致性保证,通常的解决方法是让一个进程来更新表。别的进程都发送更新请求给这个进程。

...
% 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}),
...

 

 

你可能感兴趣的:(erlang)