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. 基本数据类型
类型 | 解释 |
---|---|
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 没有原生的日期和时间类型,但可以用其他类型间接表示:
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
|
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'; |
3.2. 操作符
1 |
SELECT a FROM foo WHERE b == 'c' AND c == 'd'; |
3.3. 通配符
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
选择表的某个(些)列或表达式 必须使用 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
)
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 |
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++ 设计一个更好的版本,希望我能做到。