"值"是一个单独的数字、字符串、BLOB 或 NULL。有时使用限定名称"标量值"来强调只涉及单个数量。
"行值"是两个或多个标量值的排序列表。换句话说,"行值"是向量或元组。
行值的大小是行值包含的标量值的个数。行值的大小始终至少为 2。只有一个列的行值只是一个标量值。没有列的行值是语法错误。
SQLite 允许以两种方式表达行值
SQLite 可以将行值用于两种上下文
下面将通过示例说明行值的语法以及可以使用行值的场合。
通过从左到右查看组成标量值来比较两个行值。NULL 表示“未知”。如果可以通过替换构成 NULL 的备选值使结果为真或假,则比较的总体结果为 NULL。以下查询演示了一些行值比较
SELECT (1,2,3) = (1,2,3), -- 1 (1,2,3) = (1,NULL,3), -- NULL (1,2,3) = (1,NULL,4), -- 0 (1,2,3) < (2,3,4), -- 1 (1,2,3) < (1,2,4), -- 1 (1,2,3) < (1,3,NULL), -- 1 (1,2,3) < (1,2,NULL), -- NULL (1,3,5) < (1,2,NULL), -- 0 (1,2,NULL) IS (1,2,NULL); -- 1
"(1,2,3)=(1,NULL,3)" 的结果为 NULL,因为如果我们将 NULL→2 或 NULL→9,结果可能是真或假。"(1,2,3)=(1,NULL,4)" 的结果不为 NULL,因为没有构成 NULL 的替换会使表达式为真,因为第三列中的 3 永远不会等于 4。
前面示例中的任何行值都可以替换为返回三列的子查询,并且会得到相同的结果。例如
CREATE TABLE t1(a,b,c); INSERT INTO t1(a,b,c) VALUES(1,2,3); SELECT (1,2,3)=(SELECT * FROM t1); -- 1
对于行值 IN 运算符,左侧(以下简称“LHS”)可以是带括号的值列表,也可以是具有多个列的子查询。但右侧(以下简称“RHS”)必须是子查询表达式。
CREATE TABLE t2(x,y,z); INSERT INTO t2(x,y,z) VALUES(1,2,3),(2,3,4),(1,NULL,5); SELECT (1,2,3) IN (SELECT * FROM t2), -- 1 (7,8,9) IN (SELECT * FROM t2), -- 0 (1,3,5) IN (SELECT * FROM t2); -- NULL
行值也可以在 UPDATE 语句的 SET 子句中使用。LHS 必须是列名列表。RHS 可以是任何行值。例如
UPDATE tab3 SET (a,b,c) = (SELECT x,y,z FROM tab4 WHERE tab4.w=tab3.d) WHERE tab3.e BETWEEN 55 AND 66;
假设一个应用程序想要按姓氏、名字的字母顺序显示联系人列表,在一个只能显示 7 个联系人的滚动窗口中。将滚动窗口初始化为前 7 个条目很容易
SELECT * FROM contacts ORDER BY lastname, firstname LIMIT 7;
当用户向下滚动时,应用程序需要查找第二组 7 个条目。一种方法是使用 OFFSET 子句
SELECT * FROM contacts ORDER BY lastname, firstname LIMIT 7 OFFSET 7;
OFFSET 给出了正确的答案。但是,OFFSET 需要与偏移值成比例的时间。使用“LIMIT x OFFSET y”时,SQLite 实际上是将查询计算为“LIMIT x+y”,并丢弃前 y 个值,而不会将它们返回给应用程序。因此,当窗口向下滚动到长列表的底部时,并且 y 值变得越来越大,连续的偏移计算需要越来越多的时间。
一种更有效的方法是记住当前显示的最后一个条目,然后在 WHERE 子句中使用行值比较
SELECT * FROM contacts WHERE (lastname,firstname) > (?1,?2) ORDER BY lastname, firstname LIMIT 7;
如果前一个屏幕底部的姓氏和名字绑定到 ?1 和 ?2,则上面的查询计算接下来的 7 行。并且,假设存在适当的索引,它会非常有效地执行此操作——比 OFFSET 更有效。
在数据库表中存储日期的常用方法是作为单个字段,例如 Unix 时间戳、儒略日数或 ISO-8601 日期字符串。但有些应用程序将日期存储为三个单独的字段,分别表示年份、月份和日期。
CREATE TABLE info( year INT, -- 4 digit year month INT, -- 1 through 12 day INT, -- 1 through 31 other_stuff BLOB -- blah blah blah );
当日期以这种方式存储时,行值比较提供了一种方便的方式来比较日期
SELECT * FROM info WHERE (year,month,day) BETWEEN (2015,9,12) AND (2016,9,12);
假设我们想知道任何项目的订单号、产品号和数量,其中产品号和数量与订单号 365 中任何项目的数量相匹配
SELECT ordid, prodid, qty FROM item WHERE (prodid, qty) IN (SELECT prodid, qty FROM item WHERE ordid = 365);
上面的查询可以重写为连接,并且不使用行值
SELECT t1.ordid, t1.prodid, t1.qty FROM item AS t1, item AS t2 WHERE t1.prodid=t2.prodid AND t1.qty=t2.qty AND t2.ordid=365;
因为相同的查询可以在不使用行值的情况下编写,所以行值不提供新的功能。但是,许多开发人员表示行值格式更易于阅读、编写和调试。
即使在 JOIN 形式中,也可以通过使用行值使查询更清晰
SELECT t1.ordid, t1.prodid, t1.qty FROM item AS t1, item AS t2 WHERE (t1.prodid,t1.qty) = (t2.prodid,t2.qty) AND t2.ordid=365;
此后的查询生成与之前的标量公式完全相同的 字节码,但使用更简洁易读的语法。
行值表示法可用于根据单个查询的结果更新表的两个或多个列。一个示例是在 Fossil 版本控制系统 的全文搜索功能中。
在 Fossil 全文搜索系统中,参与全文搜索的文档(Wiki 页面、工单、签入、文档文件等)由名为“ftsdocs”(full text search documents)的表跟踪。将新文档添加到存储库后,不会立即对其建立索引。索引会被延迟,直到有搜索请求。ftsdocs 表包含一个“idxed”字段,如果文档已建立索引,则该字段为 true,否则为 false。
当发生搜索请求并且首次对挂起的文档建立索引时,必须通过将 idxed 列设置为 true 以及用与搜索相关的其他信息填充其他几个列来更新 ftsdocs 表。这些其他信息是从连接中获得的。查询如下
UPDATE ftsdocs SET idxed=1, name=NULL, (label,url,mtime) = (SELECT printf('Check-in [%%.16s] on %%s',blob.uuid, datetime(event.mtime)), printf('/timeline?y=ci&c=%%.20s',blob.uuid), event.mtime FROM event, blob WHERE event.objid=ftsdocs.rid AND blob.rid=ftsdocs.rid) WHERE ftsdocs.type='c' AND NOT ftsdocs.idxed
(有关更多详细信息,请参阅 源代码。其他示例 在此处 和 此处。)
ftsdocs 表中的九列中有五列已更新。两个修改后的列“idxed”和“name”可以独立于查询进行更新。但三列“label”、“url”和“mtime”都需要与“event”和“blob”表进行连接查询。如果没有行值,则等效的 UPDATE 将需要重复连接三次,每次更新一个要更新的列。
有时使用行值只会使 SQL 更易于阅读和编写。请考虑以下两个 UPDATE 语句
UPDATE tab1 SET (a,b)=(b,a); UPDATE tab1 SET a=b, b=a;
这两个 UPDATE 语句执行完全相同的事情。(它们生成相同的 字节码。)但是第一种形式,即行值形式,似乎更清楚地表明语句的意图是交换列 A 和 B 中的值。
或者考虑这些相同的查询
SELECT * FROM tab1 WHERE a=?1 AND b=?2; SELECT * FROM tab1 WHERE (a,b)=(?1,?2);
同样,SQL 语句生成相同的字节码,因此以完全相同的方式执行完全相同的工作。但是,通过将查询参数组合到单个行值中而不是分散到 WHERE 子句中,第二种形式更容易让人们阅读。
行值已添加到 SQLite 3.15.0 版(2016-10-14)。尝试在早期版本的 SQLite 中使用行值将生成语法错误。