数据库设计注意事项
逻辑设计 vs. 物理设计
逻辑设计考虑
逻辑设计涉及如何从概念上组织数据。具体来说,您需要决定:
- 定义哪些对象及其相互关系;
- 如何实现这些关系(例如通过oid和ref、autoid和autoid_t、索引或反规范化使用向量);
- 需要哪些访问方法;
- 应用程序的性能要求;
- 需要哪些索引以及它们的类型;
- 是否需要对象标识符(oid),其结构应如何设计;
- 哪些类将具有oid。
物理设计考虑
物理设计则关注具体的实现细节,包括:
- 页面大小;
- 初始数据库大小;
- 增量扩展;
- 某些字段是否可以是可选的;
- 类是否可以进行精简。
SmartEDB的限制
尽管SmartEDB施加的限制较少,但仍有一些重要事项需要注意:
类的数量:每个数据库最多支持64K(65,536)个类。
字段数量:每个类最多可以有64K个字段,每个对象的大小限制为2^32(4,294,967,296)字节。
索引字段:一个索引最多可以包含64K个字段。
索引数量:一个数据库最多可以有64K个索引。
对象数量:每个类可以存储232个对象(在64位平台上可以存储264个对象)。
向量元素:向量最多包含64K个元素,由无符号2字节整数索引。
页面大小:页面大小可以在40字节到64K之间调整,但这在实际应用中通常不是限制因素。
字符串大小:字符串的最大长度为64K。如果需要存储更大的字符串,则需要使用blob字段。
我们希望这些信息能帮助您更好地理解和设计您的SmartEDB数据库。如果您有任何疑问或需要进一步的帮助,请随时联系我们的技术支持团队。
页面大小
SmartEDB运行时提供了优化的内存管理功能,包括一个低级页管理器和一个堆管理器。当创建数据库实例时,内存空间会在堆上分配或在静态内存中声明。这些内存由运行时格式化,并在整个数据库生命周期内使用。其中一小部分内存用于数据库运行时控制结构,其余部分则由页面管理器控制,包括任何扩展。内存被划分为多个页,每个页具有相同的大小(page_size)。页面管理器是一种非常快速且高效的机制,允许任何使用者(例如树索引)释放的页面立即被其他使用者(例如对象布局管理器)重新使用。page_size参数在创建数据库实例时传递给运行时,并影响所有派生的内存管理器。
选择合适的页面大小
根据经验,建议将page_size设置在60到512字节之间;100字节的页面在大多数情况下表现良好。page_size应该是4的倍数,如果不是,运行时会自动进行调整。几乎所有的内存都用于存储对象或索引数据。当page_size大于60字节时,索引内存管理器施加的开销实际上不会受到太大影响,因为索引控制数据的固定部分通常每页只需4到12字节。因此,页面大小主要影响对象布局管理器施加的开销。
动态与固定大小对象的内存使用
具有动态数据字段(如字符串或向量)的对象总是占用整个页面。多个固定大小的对象可以共享一个页面。例如,如果页面大小为100字节,而某个具有动态字段的对象占用440字节(包括所有控制数据),那么会有60字节(= 5*100 - 440)被浪费。提前确定最佳页面大小并不容易,因为它取决于运行时的对象大小分布、动态数据的实际大小、操作的顺序以及最频繁存储的对象类型等因素。
为了帮助您确定运行时内存需求,我们提供了一个计算器工具,可以根据实际的应用程序数据库字典和指定的对象数量进行内存计算。通过生成的统计数据,您可以轻松调整页面大小参数,以优化内存使用。
初始数据库大小
SmartEDB允许应用程序在运行时定义多个内存设备。如果您已知最大内存量,可以在数据库打开参数中指定该值,以便提前声明所需的内存。运行时会创建一个使用连续地址空间进行对象存储的页管理器。一旦声明,这些物理内存将无法在不破坏数据库的情况下返回给操作系统。因此,如果事先不确定最大数据库大小,您可以先申请一个合理大小的内存块,然后根据需要在运行时扩展这个内存块。
扩展数据库大小
实际上,扩展数据库内存不会带来性能损失,应用程序也不需要了解页面管理器的具体工作原理。唯一需要注意的是,在不破坏数据库内容的情况下,已经分配给数据库的内存无法返回给操作系统。(有关内存管理的详细讨论,请参阅数据库控制页)。
使用OID
估计OID表项数
在数据库模式中,您可以为C、C++和Python应用程序定义自定义OID。(请注意,Java和C# API不支持OID。)DDL规范要求您估计将要创建的OID总数。这个数字用于运行时构建OID哈希表。(有关如何指定静态和动态哈希表以及它们如何由SmartEDB运行时管理的详细信息,请参阅“哈希和自动ID索引”部分。)
OID设计注意事项
如果您的应用程序数据模型中有OID,则类应该使用OID。换句话说,如果模式中描述的对象具有一些本地标识信息,并且这些信息在不同类型的对象之间是通用的,那么OID是表示该模型的自然方式。如果应用程序的对象根据其类型有不同的标识,则不应使用OID。OID可以包含多个字段,但这些字段的大小必须是固定的。
结构的使用
SmartEDB中的结构与C语言中的用法非常相似。结构声明定义了一个类型,并指定了该结构中可以具有不同类型的元素列表。结构和简单类型是构造对象定义的基本组件。结构可以用作其他结构的字段。然而,与C/C++不同的是,SmartEDB结构不能在没有类的情况下实例化,它们本身也不占用任何内存;它们只能作为对象的字段存在。SmartEDB结构提供了一种紧凑的方式来处理跨多个类使用的数据。此外,应用程序经常将结构用作向量字段。
SmartEDB使用结构有几种非常规(不像C语言)的方式:OID的声明需要一个结构(即使OID是单个字段),并且可以使用可选字段;只有结构字段可以声明为可选的。
请注意,即使将结构用作向量字段,SmartEDB也允许按结构字段进行索引。
类声明的紧凑修饰符
紧凑类限定符的作用
紧凑类限定符将类元素的大小限制为64K。这是因为在每个对象的布局中使用了2字节的偏移量,而不是4字节的偏移量来寻址。这样做可以减少内存占用,但也意味着SmartEDB在支持某些数据布局时会增加一些开销。这种开销主要来自于对动态数据类型的支持,例如向量、字符串和可选字段。
例如,每个字符串字段被实现为指向实际数据的偏移量。对于紧凑类,这个偏移量是2字节;而对于非紧凑类,则是4字节。另一个例子是可选字段。在应用程序中,有时在创建特定对象时某些数据可能未知。这时可以将这些字段声明为可选,而不是为每个对象预留空间。SmartEDB会在数据布局中放置一个指向实际数据的偏移量。如果数据不存在或已被删除,这个偏移量为空。只有在需要存储数据时才会分配该结构的空间。所有这些偏移量在紧凑模型中都是2字节。
注意事项
请注意,紧凑对象大小的总64K限制不包括为类定义的blob。因此,即使使用紧凑类,仍然可以拥有大于64K的大型blob。blob中的寻址不受紧凑声明的影响。
使用紧凑类
您可以使用mcocomp模式编译器的-c或-compact选项,使数据库中的所有类都采用紧凑模式。
示例
考虑一个包含两个字符串字段和一个可选结构的类。对于该类的1000个对象,使用紧凑声明可以节省至少3 * 2 * 1000 = 6000字节的开销(3个字段,每个字段少2字节的开销,乘以1000个对象等于6000字节)。
紧凑类的限制
紧凑类的唯一限制是对象的总大小为64K。如果确定类的对象总是需要少于64K的空间,那么使用紧凑限定符是有益的。
Char<n> vs. String
固定长度与可变长度字段的比较
char<n>
声明定义了一个固定长度的n字节字节数组(其中n <= 64K)。而字符串声明则定义了一个最大长度为64K的可变长度字节数组。对于char<n>
,每个对象(类的实例)将为该字段消耗固定的n个字节。当在每个实例中正好使用n个字节时,最好使用char<n>
,例如在作为必选条目的社会保险号字段中。对于字符串元素,SmartEDB对每个实例施加2或4字节的开销(取决于是否使用紧凑限定符,详见上文)。
Blob
具有K个已分配blob的对象在对象布局中会分配(4 + 8 * K)个字节。每个blob在存储时会在第一个blob页中写入一个32字节的头,并在第2到第N个页中添加8字节。
向量
与字符串和blob类似,每个向量也会为每个对象带来至少2字节或4字节的开销。如果向量包含结构体或字符串,则开销为2 * (N+1)(紧凑模式)或4 * (N+1)(普通模式),其中N是向量中的元素数。如果向量是简单类型,则开销仅为2(紧凑模式)或4(普通模式)字节。
向量具有结构化的特点,其元素可以通过偏移量高效定位。相比之下,blob访问方法类似于顺序文件访问——blob是一系列字节,就像文件一样。因此,当数据本质上是规则的并且需要通过元素编号访问时,使用向量总是更好的选择。
可变长度与固定长度元素
此讨论适用于char / string(包括Unicode变体)以及固定大小数组与向量的选择。
SmartEDB将类的所有固定大小元素存储在单个页面上,并将可变长度元素存储在单独的页面上(以允许它们增长)。具有固定大小元素的页面包含每个可变长度字段的2或4字节偏移量。
因此,使用可变长度字段可能会比定义固定长度元素更不高效地利用数据库空间,即使知道固定长度元素的一部分可能不会被使用。
示例
假设页面大小为100字节,字符字段可能包含8到50个字符,平均长度为18。定义为char<50>
的字段将平均留下32个字节未使用。而字符串字段将额外使用2(或4)个字节,并且在用于字符串字段的页面上至少留下50个未使用的字节。在这种情况下,固定长度的字符字段会更好。相反,如果必须允许最多256字节的字符字段,则最好定义为字符串字段。
同样的基本原则也适用于选择固定长度数组或可变长度向量。
可选结构字段
在SmartEDB中,只有结构可以声明为可选。因此,如果您希望使单个字段成为可选的,需要将其包含在一个仅包含该字段的结构中。如果将结构声明为可选,则SmartEDB会使用4字节(普通模式)或2字节(紧凑模式)作为偏移量。这是与普通结构相比,可选结构的额外开销。显然,如果一个结构总是存在,那么将其声明为可选并不会带来任何好处。
请注意,可选结构元素不能用于索引中。
自愿索引
类上索引的优点与挑战
类上的索引具有快速搜索和运行时维护顺序的优势(除散列索引外的所有索引)。然而,创建或修改索引确实需要额外的时间,尤其是在插入、更新和删除操作时。此外,还需要额外的内存空间来保持索引控制结构。通常情况下,索引是在数据库实例初始化时创建的,并在数据库的生命周期内进行维护。
但是,有些索引仅对特定目的或任务有用,在任务完成后就不再需要。例如,应用程序可能在引导阶段需要快速获取数据,或者在某个高事务速率的时间段内需要快速处理,但此时不执行复杂的搜索。而在其他时间,虽然事务速率较低,但搜索操作必须复杂且快速。在这种情况下,可以使用自愿索引(除非需要散列索引——散列索引不能是自愿的),并将索引创建延迟到关键性能周期之后。
自愿索引的应用场景
您可以创建自愿索引,用于一系列事务,然后销毁。由于索引创建是一项相对“繁重”的操作,如果只需要在执行期间的某个特定时间执行一些搜索,那么总是创建索引是没有意义的。在这种情况下,可以将索引声明为自愿的,并在搜索操作之前根据需要构建索引。
自愿索引的特点
自愿索引与常规索引使用相同的算法并占用相同的空间;它们的区别在于动态创建和销毁的能力,以及在创建数据库实例时不会自动创建自愿索引。
哈希和树索引
SmartEDB提供了多种类型的散列索引算法和树索引算法,这些算法经过优化,适用于临时类在内存中的高效操作。b树索引算法是最通用的,它可以用于所有类型的搜索和有序获取。b-tree索引可以是唯一的,也可以不是唯一的,并且可以通过范围和部分键值进行搜索。除了b树之外,SmartEDB还提供了专门的树型索引,包括“Patricia tree”、“R-Tree”和“Kd-Tree”,这些索引在“索引”和“游标”页面中有详细描述。散列索引仅适用于按相等搜索或无序顺序扫描,也可以是唯一的或非唯一的。与树索引相比,对于插入和查找操作,哈希索引可以表现出更好的平均性能,但这取决于初始哈希表的大小和键分布。
散列表的优化
在理想情况下,哈希表的大小将被调整为包含一个条目(有时称为“桶”),用于索引每个数据库对象,这意味着单个哈希表查找(简单的哈希函数计算)将足以访问每个数据库对象。但实际上,无论表大小如何,散列函数都不能保证一个唯一的值,因此多个索引值可以映射到同一个桶(称为“冲突”),并且会产生一个对象指针链表(称为“冲突链”)。
因此,如果事先知道数据库对象的数量,则可以在索引的模式声明中指定该数量,从而获得最佳性能。但是,如果不能以合理的置信度知道数据库对象的数量,则SmartEDB的动态散列特性将根据需要自动重新分配散列表。这将避免(a)如果估计的对象数量太少,则会出现长碰撞链,或者(b)在过于谨慎的大估计的情况下浪费内存。(请参阅C API索引和游标页了解更多关于动态哈希的细节。)
内存消耗
瞬态类的树索引和散列索引的内存消耗相当。Hash和Autoid索引页提供了一种估算索引内存消耗的方法。
列表索引
每个列表声明都会创建一个额外的动态结构,这将消耗与树索引相似的资源。列表声明在以下情况下非常有用:
类的对象需要按顺序访问;
应用程序不需要特定的访问顺序;
类没有合适的索引。
请注意,列表声明是一个已弃用的功能,仅为了向后兼容而保留。从SmartEDB 4.0版本开始,可以在哈希索引的条目上实例化游标,从而消除了对列表游标的需求。
如果您有任何疑问或需要进一步的帮助,请随时联系我们的技术支持团队。
结构的向量与类的对象
对象的一个重要特征是,当它被删除时,其所有依赖部分(如标量字段、结构体、数组、向量、字符串和blob)也会一并删除。为此,这些依赖部分通过对象布局管理器进行存储。为了表达对象各部分之间的一对多关系,使用向量可能非常高效。例如,一个字符串向量本身只需要2或4个字节,加上每个字符串2或4个字节的开销,而如果为每个字符串创建一个单独的对象,则每个字符串至少需要一个页面,因此开销会更大。
当应用程序中的对象模型已经包含动态结构化的项时,向量特别有用。例如,假设一个应用程序需要收集各种“目标”的雷达测量值,每个测量值都是一组结构。在这种情况下,数据库可以定义如下:
struct Target
{
uint2 x;
uint2 y;
uint2 dx;
uint2 dy;
uint2 type;
};
class Measurement
{
uint4 timestamp;
uint4 radar_number;
vector< Target > targets;
}
或者,以“标准化”的形式,它可以被定义为:
class Target2
{
uint4 m_unique_id; // ref to measurement
uint2 x;
uint2 y;
uint2 dx;
uint2 dy;
uint2 type;
}
class Measurement2
{
uint4 m_unique_id;
uint4 timestamp;
uint4 radar_number;
}
练习:构建高效的数据处理应用程序
作为一项练习,您可以构建一个应用程序,用于存储密集的测量数据流,并对这些数据进行一些简单的查找操作。例如:
删除所有超过30分钟的测量记录;
定期请求已检测到特定类型目标的雷达信标(由字段radar_number
指示)。
通过这个练习,您将能够直观地感受到SmartEDB方法的优势。与传统方法相比,SmartEDB不仅执行速度更快,而且占用的空间更少。这是因为SmartEDB需要维护的对象更少,执行的操作也更为简化。