8 表格和数据库
8.1 Ets、Dets 和 Mnesia
每个使用 Ets 的示例在 Mnesia 中都有相应的示例。通常,所有 Ets 示例也适用于 Dets 表格。
选择/匹配操作
对 Ets 和 Mnesia 表格进行选择/匹配操作可能会变得非常昂贵。它们通常需要扫描整个表格。尝试构建数据以尽量减少选择/匹配操作的需要。但是,如果您需要选择/匹配操作,它仍然比使用 tab2list 更有效率。以下部分提供了这方面的示例以及如何避免选择/匹配的示例。函数 ets:select/2 和 mnesia:select/3 比 ets:match/2、ets:match_object/2 和 mnesia:match_object/3 更值得推荐。
在某些情况下,选择/匹配操作不需要扫描整个表格。例如,如果在搜索 ordered_set 表格时键的一部分是绑定的,或者如果它是 Mnesia 表格,并且在被选择/匹配的字段上有一个二级索引。如果键完全绑定,则没有必要进行选择/匹配,除非您有一个包表格,并且只对具有特定键的元素的子集感兴趣。
在创建要用于选择/匹配操作的记录时,您希望大多数字段的值为 "_”。最简单、最快的实现方法如下所示
#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), ...
绝对不要
... TabList = ets:tab2list(Tab), BryanList = lists:filter(fun(X) -> X#person.name == "Bryan" end, TabList), lists:map(fun(X) -> X#person.age end, BryanList), ...
如果您需要存储在 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 等函数的结果以**键**的顺序出现,即使键未包含在结果中。
8.2 Ets 特定
使用 Ets 表格的键
Ets 表格是单键表格(哈希表格或按键排序的树),应该按此方式使用。换句话说,尽可能使用键来查找内容。在 set Ets 表格中查找已知键是常数时间操作,而在 ordered_set Ets 表格中是 O(logN) 操作。键查找总是比需要扫描整个表格的调用更可取。在前面的示例中,字段 idno 是表格的键,所有仅知道姓名的查找都会导致对(可能很大)表格进行完整扫描,以查找匹配的结果。
一个简单的解决方案是使用 name 字段作为键,而不是 idno 字段,但这会导致如果姓名不唯一就会出现问题。更通用的解决方案是创建一个第二个表格,使用 name 作为键,使用 idno 作为数据,也就是说,针对 name 字段对表格进行索引(反转)。显然,第二个表格必须与主表格保持一致。Mnesia 可以为您完成此操作,但自制索引表格与使用 Mnesia 所涉及的开销相比可能非常高效。
前面示例中表格的索引表格必须是一个包(因为键会多次出现),并且可以包含以下内容
[#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;因此,在主表格中查找的数量最小化了。
维护索引表格会在将记录插入表格时引入一些开销。因此,必须将表格带来的操作增益与在表格中插入对象的操作次数进行比较。但是,请注意,当可以使用键来查找元素时,增益非常显著。
8.3 Mnesia 特定
二级索引
如果您经常对表格的键以外的字段进行查找,那么使用 "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 数据库保持一致性的一种方式,即使许多不同的进程并行更新它也是如此。但是,如果您有实时要求,建议使用 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}), ...