表的结构:数据类型和限制
在描述表字段时,你需要为它们指定数据类型,但 SQLite 中的数据类型概念与 MQL5 大相径庭。
MQL5 是一种强类型语言:每个变量或结构体字段始终保持其声明时的数据类型。而 SQL 是一种弱类型语言:我们在表描述中指定的类型仅仅是一种建议。程序可以将任意类型的值写入任何“单元格”(成为记录中的一个字段),并且该“单元格”会改变其类型,这一点尤其可以被内置的 MQL 函数 DatabaseColumnType检测到。
当然,在实践中,大多数用户倾向于“遵循”列类型。
SQL 类型机制的第二个显著区别是存在大量描述类型的关键字,但所有这些字词最终都归结为五个存储类别。作为 SQL 的简化版本,SQLite 在大多数情况下不区分同一组的关键字(例如,在描述长度限制为 VARCHAR(80) 的字符串时,此限制不受控制,该描述等同于 TEXT 存储类),因此通过组名描述类型更为逻辑。保留某些特定类型只是为了与其他 DBMS 兼容(但这对于我们来说不重要)。
下表列出了 MQL5 类型及其对应的“近似类型”(指 SQL 类型的概括性特征)。
MQL5 类型 |
通用 SQL 类型 |
---|---|
NULL(在 MQL5 中不是一种类型) |
NULL(无值) |
bool、char、short、int、long、uchar、ushort、 |
INTEGER |
float、double |
REAL |
(MQL5 中无对应类型, |
NUMERIC |
string |
TEXT |
(任意“原始”数据, |
BLOB(二进制大对象),NONE |
当向 SQL 数据库写入值时,它会根据几条规则确定其类型:
- 没有引号、小数点或指数,则为 INTEGER
- 存在小数点和/或指数,则为 REAL
- 用单引号或双引号框起来,则表示 TEXT 类型
- 不带引号的 NULL 值对应于 NULL 类
- 带有二进制数据的字面量(常量)作为以 'x' 为前缀的十六进制字符串写入
使用特殊的 SQL 函数 typeof,你可以检查值的类型。例如,以下查询可以在 MetaEditor 中运行。
SELECT typeof(100), typeof(10.0), typeof('100'), typeof(x'1000'), typeof(NULL); |
它将在结果表中输出:
integer | real | text | blob | null |
不能通过比较 '=' 来检查 NULL 值(因为结果也会给出 NULL),而应该使用特殊的 NOT NULL 运算符。
SQLite 对存储数据施加了一些限制:其中一些限制很难达到(因此在本文中省略),但其他一些在设计程序时需要加以考虑。表中列的最大数量是 2000,而一行、BLOB 以及通常一条记录的大小不能超过一百万字节。SQL 查询长度限制也选择了相同的值。
就日期和时间而言,SQL 理论上可以以三种格式存储它们,但只有第一种与 MQL5 中的 datetime 匹配:
- INTEGER 自 1970.01.01 以来的秒数(也称为“Unix 纪元时间”)
- REAL 从公元前 4714 年 11 月 24 日起的天数(带小数部分)
- TEXT 格式为 "YYYY-MM-DD HH:mm:SS.sss"、精确到毫秒的日期和时间,可选时区,为此添加后缀 "[±]HH:mm" 表示与 UTC 的偏移量
真实的日期存储类型(也称为儒略日,有内置的 SQL 函数 Julianday)的有趣之处在于,它允许存储精确到毫秒的时间。理论上,这也可以通过 "YYYY-MM-DDTHH:mm:SS.sssZ" 格式的字符串来实现,但这种存储非常不经济。从我们熟悉的日期 1970.01.01 00:00:00 开始,将“天数”转换为带小数部分的秒数,是根据以下公式进行的:julianday('now') - 2440587.5) * 86400.0。此处的 'now' 表示当前 UTC 时间,但可以更改为 SQLite 文档中描述的其他值。常量 2440587.5 精确等于指定“零”日期(“Unix 纪元时间”起点)的“日历”天数。
除了类型之外,每个字段还可以有一个或多个约束,这些约束在类型之后用特殊的关键字写入。约束描述了字段可以取什么值,甚至允许根据字段的预定义用途自动完成。
我们考虑主要的约束。
... DEFAULT expression |
添加新记录时,如果未指定该字段的值,系统将自动输入此处指定的值(常量)或计算表达式(函数)。
... CHECK ( boolean_expression ) |
当添加新记录时,系统将检查表达式(可以包含字段名作为变量)是否为真。如果表达式为假,则不会插入记录,系统将返回错误。
... UNIQUE |
系统检查表中所有记录在此字段上具有不同的值。尝试添加具有已存在值的条目将导致错误,并且不会进行添加。
为了跟踪唯一性,系统会为指定字段隐式创建一个索引。
... PRIMARY KEY |
系统使用标记有此属性的字段来标识表中的记录,并从其他表链接到它们(关系型数据库(如 SQLite)的名称由此而来,即关系是如何形成的)。显然,此功能还包括唯一索引。
如果表没有带 PRIMARY KEY 属性的 INTEGER 类型字段,系统会自动隐式创建这样一个类别,且名为 rowid。如果表有一个声明为主键的整数字段,那么它也可以使用别名 rowid。
如果向表中添加一条省略了 rowid 或 rowid 为 NULL 的记录,SQLite 将自动为其分配下一个整数(64 位,对应 MQL5 中的 long),该整数比表中最大 rowid 大 1。初始值为 1。
通常计数器每次只递增 1,但如果曾经插入到一个表中的记录数量(然后可能被删除)超过 long 的最大值,计数器将跳回到开头,系统将尝试查找空闲的数字。但这不太可能发生。例如,如果以平均每毫秒 1 条分时报价的速度向表中写入数据,那么 2.92 亿年后才会溢出。
只能有一个主键,但它可以由多个列组成,这是通过直接在表描述中使用不同于约束的语法来完成的。
CREATE TABLE table_name (
|
我们回到约束。
... AUTOINCREMENT |
此约束只能作为 PRIMARY KEY 的补充指定,确保标识符始终递增。这意味着任何以前的 ID,即使是用在已删除条目上的 ID,也不会被重新选择。然而,与简单的 PRIMARY KEY 相比,SQLite 中此机制的实现在计算资源方面效率较低,因此不建议使用。
... NOT NULL |
此约束禁止向表中添加此字段未填充的记录。默认情况下,当没有约束时,任何非唯一字段都可以在添加的记录中省略,并将设置为 NULL。
... CURRENT_TIME
|
使用这些指令,你可以在插入记录时自动用时间(不含日期)、日期(不含时间)或完整的 UTC 时间填充字段(前提是 INSERT SQL 语句没有显式地向此字段写入任何内容,甚至 NULL 也不行)。SQLite 不知道如何以类似方式自动检测记录更改的时间,为此你将必须编写一个触发器(这不在本书的讨论范围)。
遗憾的是,CURRENT_TIMESTAMP 组限制在 SQLite 中的实现有一个疏漏:如果字段为 NULL,则不应用时间戳。这使得 SQLite 与其他 SQL 引擎以及 SQLite 本身处理主键字段中 NULL 的方式有所不同。事实证明,对于自动标记,不能将整个对象写入数据库,而需要显式指定除日期和时间字段之外的所有字段。为了解决这个问题,我们需要一个替代方案,即在编译的查询中为相应的列替换 SQL 函数 STRFTIME('%s')。