查看源代码 表格和数据库
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)
如果你需要 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 相关的开销相比,自制的索引表格可能非常高效。
前面示例中表格的索引表格必须是一个包(因为键会出现多次),并且可以包含以下内容
[#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 数据库保持一致性的方法,即使许多不同的进程并行更新它也是如此。但是,如果你有实时要求,建议使用脏操作而不是事务。使用脏操作时,你会失去一致性保证;这通常通过仅允许一个进程更新表格来解决。其他进程必须向该进程发送更新请求。
示例
...
%% 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}),