原文链接: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}), ...