SQLite 二周目
很久以前写过一个很屎的 SQLite C++ binding ,实在是太屎了,最近想用的时候发现我自己已经看不懂了,研究了一会之后我发现我设计的非常失败, API 嗯抄 Python 的 sqlite3 模块 ,但由于实力不足,抄成了一坨,而且还非要用 PIMPL ,导致源文件非常臃肿,而且因为 sqlite3*
和 sqlite3_stmt*
指针的处理不当,在某些情况会 segfault ,还有些情况会内存泄漏。所以打算重新设计一个 binding ,趁此机会捡一捡 SQL 的基础。
新的 binding 已经基本实现好了
(非常的新鲜,非常的美味),放在了我发起的「ExDinner - 超级低能」组织里了,项目名为 sqlitemm
参考内容:
- MySQL Crash Course / MySQL 必知必会
- SQLite 文档
1. SQLite 没有实现的一些 SQL feature
1.1. ALTER TABLE
实现不完整
只支持 RENAME TABLE
, ADD COLUMN
, RENAME COLUMN
和 DROP COLUMN
。
1.2. trigger 实现不完整
FOR EACH ROW
trigger 是支持的, FOR EACH STATEMENT
trigger 是不支持的。
1.3. VIEW
是只读的
不能对 VIEW
执行 DELETE
, INSERT
或 UPDATE
语句。但写一个 trigger 对 VIEW
进行修改是可以的。
1.4. 没有 GRANT
和 REVOKE
一般 CS 架构的 DBMS 才会用这种东西, SQLite 中这种语句没有意义。
2. 基本数据类型
SQLite 只有 5 种数据:
类型 | 解释 |
---|---|
NULL | 顾名思义 |
INTEGER | 根据实际值,占用 1 2 4 6 8 字节的有符号整数 |
REAL | 8 字节 IEEE 浮点 |
TEXT | 貌似没有长度限制的字符串 |
BLOB | 貌似没有长度限制的二进制数据 |
2.1. 布尔值
SQLite 使用 INTEGER
的 0
和 1
表示 TRUE
和 FALSE
,相反 TRUE
和 FALSE
在 SQLite 中仅为 0
和 1
的别名。
2.2. 日期和时间
SQLite 没有原生的日期和时间类型,但可以用其他类型间接表示:
INTEGER
表示 Unix 时间TEXT
保存 ISO8601 时间(YYYY-MM-DD HH:MM:SS.SSS
)
3. SQL 基础
关键词 | 解释 |
---|---|
database | 顾名思义,但 SQLite 和 MariaDB 之类称为 DBMS |
table | 顾名思义 |
schema | 模式,数据库和表的布局及特征信息,比如 SQLite 每个数据库都有一个 sqlite_schema 表 |
column | 顾名思义 |
datatype | 顾名思义 |
row | 顾名思义 |
primary key | 主键,顾名思义 |
clause | 子句,比如 SELECT , FROM 和 ORDER BY 都算子句 |
operator | 顾名思义,一般用在 WHERE 子句,也叫 logical operator ,比如 AND , OR |
SQL 读作
S-Q-L
或sequel
,而不是circle
3.1. 检索
1 | SELECT * FROM foo; |
3.1.1. DISTINCT
1 | SELECT DISTINCT foo FROM bar; |
这个关键词作用到所有列,第二个语句的输出可能包含 foo 列相同但 bar 列不同的两行数据。
3.1.2. LIMIT
1 | SELECT foo FROM bar LIMIT 5; |
第二个语句表示输出 ${rows:5:5}
(shell) ,第三个语句为第二个语句的上位替代,因为它的语义更明确
3.1.3. ORDER BY
1 | SELECT a, b FROM foo ORDER BY c, d; -- 默认为升序 (ASC) |
3.1.4. WHERE
1 | SELECT a FROM foo WHERE b == 'c'; |
这里需要注意的是 SQLite 对于字符串比较是大小写敏感的, i.e.
"Foo" != "foo"
3.2. 操作符
1 | SELECT a FROM foo WHERE b == 'c' AND c == 'd'; |
3.3. 通配符
LIKE
子句可以使用通配符 (wildcard) ,比如 %
表示任何字符出现任何次数、 _
匹配单个字符; 114_1419%
这种叫做搜索模式 (search pattern) 。注意 '%'
也不能匹配 NULL
这里需要注意的是 SQLite 的
LIKE
子句貌似不是大小写敏感的, i.e."Foo" == "foo"
1 | SELECT a FROM foo WHERE b LIKE '114_1419%'; |
3.4. 正则表达式
1 | SELECT a FROM foo WHERE b REGEXP '114\d+19810'; |
在我测试下, SQLite 的正则表达式支持了几乎我能想到的所有关键字
3.5. 内置函数
节选了一些内置函数,见完整列表
1 | -- 字符串 |
3.6. 数据分组
1 | SELECT a, count(*) AS a_count FROM foo GROUP BY a; |
SELECT
子句顺序
子句 说明 使用情况 SELECT
选择表的某个(些)列或表达式 必须使用 FROM
选择某个(些)表 需要选择表时使用 WHERE
行级过滤 对每行进行限定时使用 GROUP BY
进行分组 对行进行分组时使用 HAVING
组级过滤 对组进行限定时使用 ORDER BY
输出排序 顾名思义 LIMIT
限制输出行数 顾名思义
4. 高端操作
1 | sqlite> SELECT unhex('e9ab98e7abafe6938de4bd9c') AS 队友呢队友呢救一下啊; |
4.1. 子查询
1 | SELECT a FROM foo WHERE b IN ( |
4.2. 联结 (JOIN
)
显式使用 WHERE
的联结:
1 | SELECT a, b FROM foo, bar WHERE foo.c == bar.c; |
4.2.1. 内部联结 (INNER JOIN
)
1 | SELECT a, b FROM foo INNER JOIN bar ON foo.c == bar.c; |
和上面使用 WHERE
的那句输出是一致的,不清楚有什么区别, Gemini 跟我说这种 INNER JOIN
也会隐式使用 WHERE
,所以两种是等价的
4.2.2. 自联结
这两句是等价的,但第二种更好:
1 | SELECT id, name FROM products WHERE id == ( |
4.2.3. 自然联结
指联结多个表时输出没有重复的列的联结,目前所有联结都算作自然联结
4.2.4. 外部联结 (OUTER JOIN
)
上面内部联结的
1 | SELECT a, b FROM foo INNER JOIN bar ON foo.c == bar.c; |
会导致输出只包含 foo 和 bar 两个表中都含有 c 列数据且一致的行,有时需要以其中某个表为准:
1 | SELECT foo.a, bar.b FROM foo LEFT OUTER JOIN bar ON foo.c == bar.c; |
这句会输出 foo 表的所有行的 a 列,以及 bar 表中对应 c 列相同的行的 b 列,如果没有对应的 b 列则会填 NULL
,在 SQLite CLI 中显示为空
4.3. 组合查询 (UNION
)
1 | SELECT a FROM foo WHERE b < 5 |
会输出 foo 表中 b 值小于 5 以及 c 值在 114 和 514 之间的所有行
4.3.1. 去重
默认 UNION
会去掉重复的行,如果不需要去重可以加上 ALL
关键字:
1 | SELECT a FROM foo WHERE b < 5 |
4.3.2. 排序
可以在最后的最后加上 ORDER BY
表示对所有结果进行排序,不可以对某个 SELECT
加:
1 | SELECT a FROM foo WHERE b < 5 |
5. 数据操作
1 | -- 狠狠地向纳西妲注入脱氧核糖核酸 |
5.1. UPDATE
1 | UPDATE foo |
把 foo 表中 b 列值为 114514 的行的 a 列值改为 NULL
5.2. DELETE
1 | DELETE FROM foo |
删除 foo 表中所有 b 列值为 114514 的行
6. 数据库管理
6.1. 表格
6.1.1. 创建表格
详细语法见 CREATE TABLE —— SQLite 文档
1 | CREATE TABLE foo ( |
6.1.2. 修改表格
详细语法见 ALTER TABLE —— SQLite 文档
1 | ALTER TABLE foo ADD COLUMN type TEXT; |
6.1.3. 删除表格
1 | DROP TABLE foo; |
6.2. 视图 (VIEW
)
1 | CREATE VIEW foobar AS |
foobar
就是下面那句 SELECT
的别名了,在此基础上可以进行筛选:
1 | SELECT a, b FROM foobar WHERE c == '114514'; |
6.3. 触发器 (TRIGGER
)
有点像回调,详细语法见 CREATE TRIGGER —— SQLite 文档
1 | CREATE TRIGGER newfoo AFTER INSERT ON foo FOR EACH ROW BEGIN |
每次往 foo 表中插入数据时执行 SELECT 'Row added';
,但亲测 SQLite CLI 不会输出任何内容。不想要了可以删除:
1 | DROP TRIGGER newfoo; |
SQLite 支持 BEFORE
, AFTER
, INSTEAD OF
于 DELETE
, INSERT
, UPDATE
操作的触发器
6.4. 事务 (TRANSACTION
)
说白了就是保证某一系列操作是完整的,如果其中出现了错误则恢复到执行操作之前的状态。详细语法见 Transaction —— SQLite 文档
TRANSACTION
: 一组 SQL 语句ROLLBACK
: 顾名思义COMMIT
: 类似 Git ,把暂存的结果写进数据库SAVEPOINT
: 顾名思义,学习水龙的品德
1 | SELECT * FROM foo; -- foo 表本来有数据 |
事务中的语句产生的结果不会自动保存到数据库里,需要手动 COMMIT;
:
1 | BEGIN TRANSACTION; |
这样就保证了 foo 表被删干净了,因为如果事务期间发生了错误,数据库会自动撤销。 START TRANSACTION
对应 COMMIT
或 ROLLBACK
表示事务结束。需要更精细的 ROLLBACK
可以使用 SAVEPOINT
:
1 | BEGIN TRANSACTION; |
SAVEPOINT
在事务结束后会自动释放,也可以手动释放,详细语法见 Savepoints —— SQLite 文档:
1 | RELEASE SAVEPOINT foo; |
SQLite 与 MariaDB 有一些不同,它默认是 autocommit 模式的,但貌似没有一个语句显式关掉 autocommit ,只能 BEGIN TRANSACTION;
后开启,到 COMMIT;
或 ROLLBACK;
结束。
读后感
其实最近写 sqlitemm 的时候有一些地方文档写的不清楚,比如 SQLite C API 有两种错误信息的获取方式: sqlite3_errmsg
和 sqlite3_errstr
,文档没有写什么情况下应该用哪个。
所以我下载了源码,跟着文档尝试构建(为了用 bear
生成 compile_commands.json 喂给 clangd 使我看源码能不像一个原始人),发现 SQLite 构建前必须把所有源码都粘到一个文件里,官方说法是能让编译器实现更好的优化,这个生成的文件一共 25 万行, clangd 要花十几秒甚至几十秒才能开始正常工作。但不得不说 SQLite 的源码是真的精致,测试用例更是细致到家,有点像艺术品。
说回 SQL ,最近在写一个基于哈希和体积的文件查重工具,想要用 SQLite 实现一个缓存,之前用 Python 实现过一个类似的,但现在某些文件越来越多,所以想好好用 C++ 设计一个更好的版本,希望我能做到。