表的结构:数据类型和限制

在描述表字段时,你需要为它们指定数据类型,但 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、
uint、ulong、datetime、color、enum

INTEGER

float、double

REAL

(MQL5 中无对应类型,
指固定精度实数)

NUMERIC

string

TEXT

(任意“原始”数据,
类似于 uchar[] 数组或其他)

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 (
  column_name type [ restrictions ]
  [, column_name type [ restrictions ] ...]
  , PRIMARY KEY ( column_name [, column_name ...] ) );

我们回到约束。

... AUTOINCREMENT

此约束只能作为 PRIMARY KEY 的补充指定,确保标识符始终递增。这意味着任何以前的 ID,即使是用在已删除条目上的 ID,也不会被重新选择。然而,与简单的 PRIMARY KEY 相比,SQLite 中此机制的实现在计算资源方面效率较低,因此不建议使用。

... NOT NULL

此约束禁止向表中添加此字段未填充的记录。默认情况下,当没有约束时,任何非唯一字段都可以在添加的记录中省略,并将设置为 NULL。

... CURRENT_TIME
... CURRENT_DATE
... CURRENT_TIMESTAMP

使用这些指令,你可以在插入记录时自动用时间(不含日期)、日期(不含时间)或完整的 UTC 时间填充字段(前提是 INSERT SQL 语句没有显式地向此字段写入任何内容,甚至 NULL 也不行)。SQLite 不知道如何以类似方式自动检测记录更改的时间,为此你将必须编写一个触发器(这不在本书的讨论范围)。

遗憾的是,CURRENT_TIMESTAMP 组限制在 SQLite 中的实现有一个疏漏:如果字段为 NULL,则不应用时间戳。这使得 SQLite 与其他 SQL 引擎以及 SQLite 本身处理主键字段中 NULL 的方式有所不同。事实证明,对于自动标记,不能将整个对象写入数据库,而需要显式指定除日期和时间字段之外的所有字段。为了解决这个问题,我们需要一个替代方案,即在编译的查询中为相应的列替换 SQL 函数 STRFTIME('%s')。