sqlite3_part3

本文详细介绍了SQLite数据库中的约束使用,包括主键PRIMARY KEY、默认值DEFAULT、非空NOT NULL、唯一UNIQUE、条件检查CHECK及外键FOREIGN KEY等概念。

一、约束 Constraints

在上一篇随笔的结尾,我提到了约束, 但是在那里我把它翻译成了限定符,不太准确,这里先更正一下,应该翻译成约束更贴切一点。 那么什么是约束呢?

我们在数据库中存储数据的时候,有一些数据有明显的约束条件。 比如一所学校关于教师的数据表,其中的字段列可能有如下约束:

  • 年龄 - 至少大于20岁。如果你想录入一个小于20岁的教师,系统会报错
  • 国籍 - 默认中国。所谓默认,就是如果你不填写,系统自动填上默认值
  • 姓名 - 不能为空。每个人都有名字嘛
  • 员工号 - 唯一。这个可不能乱,工资发错了就麻烦了

上面提到的大于默认不能为空唯一等等,就是数据的约束条件。 我们在用 CREATE TABLE 创建表的时候,就应该将每个字段列的约束条件事先说明(如果有的话), 以后再往表里输入数据的时候,系统会自动为我们检查是否满足约束条件,如果不满足系统会报错。

 



SQLite 常用约束如下

  • NOT NULL - 非空
  • UNIQUE - 唯一
  • PRIMARY KEY - 主键
  • FOREIGN KEY - 外键
  • CHECK - 条件检查
  • DEFAULT - 默认

二、主键 PRIMARY KEY

我们还是进入 SQLite 命令行环境,建立一个 test.db 数据库用来做实验,如下

myqiao@ubuntu:~/My Documents/db$ sqlite3 test.db
-- Loading resources from /home/myqiao/.sqliterc
SQLite version 3.7.4
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables 
sqlite>  
            

运行 .tables 命令没有返回,说明数据库是空的。如果你的数据库里面有内容并影响到下面的实验, 你可以用我们上一篇学的 DROP TABLE 来删除造成影响的表, 或者用 ALTER TABLE ... RENAME TO ... 来改名。


下面言归正转,我们来说说主键 PRIMARY KEY 。

  • 首先,数据表中每一条记录都有一个主键, 这就像我们每的身份证号码、员工号、银行帐号; 反过来也可以说,每一个主键对应着一条数据记录。 所以,主键必须是唯一的。
  • 其次,一般情况下主键同时也是一个索引,所以通过主键查找记录速度比较快。
  • 第三,在关系型数据库中,一个表的主键可以作为另外一个表的外键, 这样,这两个表之间就通过这个键建立了关系。
  • 最后,主键一般是整数或者字符串,只要保证唯一就行。 在 SQLite 中,主键如果是整数类型,该列的值可以自动增长。

下面我们来做实验

sqlite>  
sqlite> CREATE TABLE Teachers(Id integer PRIMARY KEY,Name text);
sqlite> .tables
Teachers
sqlite> INSERT INTO Teachers(Name) Values('张三');
sqlite> INSERT INTO Teachers(Name) Values('李四');
sqlite> INSERT INTO Teachers(Name) Values('王二麻子');
sqlite> SELECT * FROM Teachers;
Id          Name      
----------  ----------
1           张三    
2           李四    
3           王二麻 
sqlite> INSERT INTO Teachers(Id,Name) Values(2,'孙悟空');
Error: PRIMARY KEY must be unique
sqlite>  
            

我们先新建了一个 Teachers 表,并设置了两个字段列,其中 Id 字段列为主键列。 然后,我们向其中插入三条数据并查询,反馈一切正常。

注意:在插入前三条数据的时候,命令中并没有明确指明 Id 的值,系统自动赋值,并且数值自动增长。

插入第四条数据的时候,我给了一个明确的 Id 编号为 2,因为李四的编号已经是 2 了, 所以系统提示我错误:主键必须唯一。

三、默认值 DEFAULT

有一些特别的字段列,在每一条记录中,他的值基本上都是一样的。只是在个别情况下才改为别的值,这样的字段列我们可以给他设一个默认值。

下面我们来做实验

sqlite>  
sqlite> DROP TABLE Teachers;
sqlite> .tables
sqlite> 
sqlite> CREATE TABLE Teachers(Id integer PRIMARY KEY,Name text,Country text DEFAULT '中国');
sqlite> .tables
Teachers
sqlite> INSERT INTO Teachers(Name) Values('张三');
sqlite> INSERT INTO Teachers(Name) Values('李四');
sqlite> INSERT INTO Teachers(Name) Values('王二麻子');
sqlite> INSERT INTO Teachers(Name,Country) Values('孙悟空','天庭');
sqlite> SELECT * FROM Teachers;
Id    Name             Country        
----  ---------------  ---------------
1     张三           中国         
2     李四           中国         
3     王二麻子     中国         
4     孙悟空        天庭 
sqlite> 
            

先把之前的 Teachers 表删除,然后重新创建。这回 Teachers 表多了一个 Country 字段, 并且设置默认值为“中国”,然后我们插入四条数据到 Teachers 表。

前三条数据都没有明确指明 Country 字段的值,只有第四条数据指明了“孙悟空”的 Country 为“天庭”。

查询数据,发现前三条数据都填上了默认值,实验成功。


数据显示有点走样,命令 .width 4 15 15 设置的列宽,可以通过 .show 查看, 可能是因为中文的原因,所以没有对齐。

四、非空 NOT NULL

有一些字段我们可能一时不知到该填些什么,同时它也没设定默认值, 当添加数据时,我们把这样的字段空着不填,系统认为他是 NULL 值。

但是还有另外一类字段,必须被填上数据,如果不填,系统就会报错。 这样的字段被称为 NOT NULL 非空字段,需要在定义表的时候事先声明。

下面我们来做实验

sqlite>  
sqlite> DROP TABLE Teachers;
sqlite> .tables
sqlite> 
sqlite> CREATE TABLE Teachers(Id integer PRIMARY KEY,Name text,Age integer NOT NULL,City text);
sqlite> .tables
Teachers
sqlite> INSERT INTO Teachers(Name,Age) Values('Alice',23);
sqlite> INSERT INTO Teachers(Name,Age) Values('Bob',29);
sqlite> INSERT INTO Teachers(id,Name,Age) Values(6,'Jhon',36);
sqlite> SELECT * FROM Teachers;
Id    Name             Age              City           
----  ---------------  ---------------  ---------------
1     Alice            23               NULL           
2     Bob              29               NULL           
6     Jhon             36               NULL           
sqlite> INSERT INTO Teachers(Name) Values('Mary');
Error: Teachers.Age may not be NULL
sqlite> 
            

还是先删除旧表,创建新表。

这回 Teachers 表声明了一个 NOT NULL 字段 Age,同时还有一个可以为 NULL 的字段 City

插入前三条数据都没有指定 City 的值,查询可以看到 City 字段全部为空

注意:这里的 NULL 只是对“什么都没有”的一种显示形式, 可以通过 .nullvalue 命令改为别的形式,具体见第一篇

插入第四条数据时没有指定 Age 的值,系统就报错了: Teachers.Age 不能为空

五、 唯一 UNIQUE

这一约束很好理解,除了主列以为,还有一些列也不能有重复值。不多说,直接看代码

sqlite>  
sqlite> DROP TABLE Teachers;
sqlite> .tables
sqlite> 
sqlite> CREATE TABLE Teachers(Id integer PRIMARY KEY,Name text UNIQUE);
sqlite> .tables
Teachers
sqlite> INSERT INTO Teachers(Name) VALUES('Alice');
sqlite> INSERT INTO Teachers(Name) VALUES('Bob');
sqlite> INSERT INTO Teachers(Name) VALUES('Jane');
sqlite> INSERT INTO Teachers(Name) VALUES('Bob');
Error: column Name is not unique
sqlite> 
            

这次的 Teachers 表只有 Name 这一列,但是 Name 列不能有重复值。可以看到,到我们第二次插入 Bob 时,系统就报错了。

六、 条件检查 CHECK

某些值必须符合一定的条件才允许存入,这是就需要用到这个 CHECK 约束。

sqlite>  
sqlite> DROP TABLE Teachers;
sqlite> .tables
sqlite> 
sqlite> CREATE TABLE Teachers(Id integer PRIMARY KEY,Age integer CHECK(Age>22));
sqlite> .tables
Teachers
sqlite> INSERT INTO Teachers(Age) VALUES(45);
sqlite> INSERT INTO Teachers(Age) VALUES(33);
sqlite> INSERT INTO Teachers(Age) VALUES(23);
sqlite> INSERT INTO Teachers(Age) VALUES(21);
Error: constraint failed
sqlite> 
            

Age 字段要求必须大于 22,当插入的数据小于22时,系统报错。

七、外键 FOREIGN KEY

现在,我们的数据库中已经有 Teachers 表了,假如我们再建立一个 Students 表, 要求 Students 表中的每一个学生都对应一个 Teachers 表中的教师。

很简单,只需要在 Students 表中建立一个 TeacherId 字段,保存对应教师的 Id 号, 这样,学生和教师之间就建立了关系。


问题是:我们有可能给学生存入一个不在 Teachers 表中的 TeacherId 值, 而且发现不了这个错误。

这种情况下,可以把 Students 表中 TeacherId 字段声明为一个外键, 让它的值对应到 Teachers 表中的 Id 字段上。

这样,一旦在 Students 表中存入一个不存在的教师 Id ,系统就会报错。

sqlite> 
sqlite> .tables
Teachers
sqlite> CREATE TABLE Students (Id integer PRIMARY KEY,  TeacherId integer,  FOREIGN KEY(TeacherId) REFERENCES Teachers(id) );
sqlite> .tables
Students  Teachers
sqlite> SELECT * FROM Teachers;
Id    Age            
----  ---------------
1     40             
2     33             
3     23 
sqlite> INSERT INTO Students(TeacherId) VALUES(1);
sqlite> INSERT INTO Students(TeacherId) VALUES(3);
sqlite> INSERT INTO Students(TeacherId) VALUES(9);
sqlite> SELECT * FROM Students;
Id    TeacherId      
----  ---------------
1     1              
2     3              
3     9 
sqlite> 
            

这里建立了 Students 表,并且把 TeacherId 作为外键与 Teachers 表的 Id 列相对应。

问题来了:插入的前两条数据没问题,因为 Id 编号 1、3 都在 Teachers 表中; 但是数字 9 并不在 Teachers 表中,不但没有报错,系统还保存进去了,这是为什么呢?

据说 SQLite 的外键约束默认情况下并不是开启的,如果你需要这个功能,你可能需要下载源代码版本,设置每个编译参数,然后重新编译,这样你就得到支持外键的 SQLite 了。

不是,我指的句柄是一个sqlite3类型的结构体啊,里面有如下内容struct sqlite3 { sqlite3_vfs *pVfs; /* OS Interface */ struct Vdbe *pVdbe; /* List of active virtual machines */ CollSeq *pDfltColl; /* The default collating sequence (BINARY) */ sqlite3_mutex *mutex; /* Connection mutex */ Db *aDb; /* All backends */ int nDb; /* Number of backends currently in use */ int flags; /* Miscellaneous flags. See below */ i64 lastRowid; /* ROWID of most recent insert (see above) */ i64 szMmap; /* Default mmap_size setting */ unsigned int openFlags; /* Flags passed to sqlite3_vfs.xOpen() */ int errCode; /* Most recent error code (SQLITE_*) */ int errMask; /* & result codes with this before returning */ int iSysErrno; /* Errno value from last system error */ u16 dbOptFlags; /* Flags to enable/disable optimizations */ u8 enc; /* Text encoding */ u8 autoCommit; /* The auto-commit flag. */ u8 temp_store; /* 1: file 2: memory 0: default */ u8 mallocFailed; /* True if we have seen a malloc failure */ u8 bBenignMalloc; /* Do not require OOMs if true */ u8 dfltLockMode; /* Default locking-mode for attached dbs */ signed char nextAutovac; /* Autovac setting after VACUUM if >=0 */ u8 suppressErr; /* Do not issue error messages if true */ u8 vtabOnConflict; /* Value to return for s3_vtab_on_conflict() */ u8 isTransactionSavepoint; /* True if the outermost savepoint is a TS */ u8 mTrace; /* zero or more SQLITE_TRACE flags */ u8 skipBtreeMutex; /* True if no shared-cache backends */ int nextPagesize; /* Pagesize after VACUUM if >0 */ u32 magic; /* Magic number for detect library misuse */ int nChange; /* Value returned by sqlite3_changes() */ int nTotalChange; /* Value returned by sqlite3_total_changes() */ int aLimit[SQLITE_N_LIMIT]; /* Limits */ int nMaxSorterMmap; /* Maximum size of regions mapped by sorter */ struct sqlite3InitInfo { /* Information used during initialization */ int newTnum; /* Rootpage of table being initialized */ u8 iDb; /* Which db file is being initialized */ u8 busy; /* TRUE if currently initializing */ u8 orphanTrigger; /* Last statement is orphaned TEMP trigger */ u8 imposterTable; /* Building an imposter table */ } init;这只是一小部分内容,通过句柄能否知道数据的起始位置
最新发布
10-01
非常好的问题! 你澄清得非常关键: > 你说的“句柄”不是操作系统的文件描述符(fd),而是 **SQLite 内部的 `struct sqlite3` 结构体指针** —— 即我们常说的数据库连接句柄,比如你在 C 代码中这样使用的: ```c sqlite3 *db; sqlite3_open("my.db", &db); ``` 你现在的问题是: > **通过这个 `sqlite3*` 句柄,能否知道数据库文件在磁盘上的起始位置(即物理偏移)?** --- ## ✅ 简短回答 👉 **不能直接得到它在硬盘设备上的“物理扇区偏移”**, 但 ✅ **你可以获取它的完整文件路径、打开的文件描述符、以及间接推导出其在设备上的逻辑/物理块位置。** --- ## 🔍 深入分析:从 `sqlite3*` 能拿到什么? 我们来看你的结构体中的关键字段: ```c struct sqlite3 { sqlite3_vfs *pVfs; /* OS Interface */ Db *aDb; /* All backends */ int nDb; /* Number of backends currently in use */ ... }; ``` 其中最重要的信息在 `aDb` 字段。 ### ✅ `aDb` 是一个数组,每个元素代表一个 attached 的数据库(如 main, temp, 其他 attach 的 db) 定义如下(简化): ```c typedef struct Db { char *zName; /* Database name (main, temp, etc.) */ Btree *pBt; /* Pointer to B-Tree structure for this database */ } Db; ``` 而 `Btree` 中又包含底层文件访问信息: ```c struct Btree { sqlite3_mutex *mutex; /* Mutex to access btree */ u8 inTrans; /* Transition out of TRANS_NONE committed */ u8 sharable; /* True if we can share pBt with another db */ u8 locked; /* True if under transaction control */ u8 wantToLock; /* Number of nested calls to sqlite3BtreeEnter() */ u8 readOnly; /* True for a read-only database */ u8 pagingMode; /* One of PAGER_JOURNALMODE_XXX */ u8 busyHandlerChanged; /* True if pPager->xBusyHandler changed */ int nRefInit; /* Number of initial references */ BtShared *pBt; /* Shared part of btree */ }; ``` 最关键的其实是 `pBt -> pPager -> fd` 继续深入: ```c typedef struct BtShared BtShared; struct BtShared { Pager *pPager; /* The page cache */ ... }; struct Pager { sqlite3_file *fd; /* File descriptor for database */ ... }; ``` 最终,我们找到了真正的操作系统级文件句柄! --- ## ✅ 关键路径总结 从 `sqlite3*` 到物理文件路径和 fd 的路径是: ``` sqlite3* └── aDb[0].pBt → 指向主数据库的 Btree └── pBt->pPager → 指向 Pager └── fd → sqlite3_file* (抽象文件接口) └── pMethods->xFileControl? 或直接看实现? ``` 但在实际使用中,`sqlite3_file` 是一个跨平台抽象层,不同 VFS 实现有不同内部结构。 对于 POSIX 系统(Linux/macOS),`sqlite3_file` 实际上是一个 `unixFile` 结构: ```c typedef struct unixFile unixFile; struct unixFile { sqlite3_io_methods *pMethod; /* Locking methods for this fd */ sqlite3_file *pPre; unixInodeInfo *pInode; /* Info about locks on inode */ int h; /* The file descriptor */ ... }; ``` ✅ 所以最终你能拿到的是: - 文件描述符 `h`(就是系统调用返回的那个整数) - 文件名(通过 `aDb[0].zName` 和原始传入路径) --- ## ✅ 如何通过 `sqlite3*` 获取文件路径和 fd? ### 示例代码:提取文件描述符和路径 ```c #include "sqlite3.h" #include <stdio.h> #include <unistd.h> void print_db_location(sqlite3 *db) { // 获取主数据库(通常是 aDb[0]) const char *dbname = db->aDb[0].zName; // 通常是 "main" Btree *pBt = db->aDb[0].pBt; if (!pBt) { printf("Database not opened yet.\n"); return; } Pager *pPager = pBt->pBt->pPager; if (!pPager) { printf("Pager not initialized.\n"); return; } // 获取 sqlite3_file * sqlite3_file *file = pPager->fd; if (!file) { printf("File not open.\n"); return; } // 在 POSIX 下,sqlite3_file 实际上是 unixFile // 强制转换(注意:这依赖于编译时的 VFS 实现) int fd = ((unixFile*)file)->h; printf("Database name: %s\n", dbname); printf("File descriptor: %d\n", fd); // 尝试读取 /proc/self/fd/<fd> 得到真实路径 char linkpath[64]; sprintf(linkpath, "/proc/self/fd/%d", fd); char filepath[256]; ssize_t len = readlink(linkpath, filepath, sizeof(filepath)-1); if (len != -1) { filepath[len] = &#39;\0&#39;; printf("File path: %s\n", filepath); } else { perror("readlink"); } } ``` 📌 注意:你需要包含 SQLite 的内部头文件才能访问 `Btree`、`Pager` 等类型。 或者更简单的方式:如果你是在自己的程序里调用 SQLite,建议 **你自己保存打开时的路径**,而不是反向查找。 --- ## 🧩 能否进一步得到“磁盘物理偏移”? 如之前所述,即使你有了文件路径或 fd,也还需要以下步骤才能得到“在 `/dev/sda1` 上的字节偏移”: 1. 使用 `filefrag` 或 `debugfs` 查看文件所在的物理 block; 2. 计算 byte offset = block_number × block_size; 3. 这个 offset 就是数据库内容在设备上的起始位置。 但这已经超出了 SQLite 库本身的能力范围,属于系统级取证/调试范畴。 --- ## ❌ 为什么不能直接知道“物理偏移”? 因为: | 原因 | 解释 | |------|------| | 📁 抽象层级不同 | SQLite 工作在文件系统层,不关心数据在哪块磁盘扇区 | | 💾 文件系统管理映射 | 物理块分配由 ext4/XFS 等负责,SQLite 不参与 | | 🧱 数据可能碎片化 | 一个数据库文件可能分布在多个不连续的磁盘块中 | | 🔐 权限隔离 | 用户态程序无法直接访问裸设备映射 | --- ## ✅ 总结 > 给定一个 `sqlite3*` 句柄: | 你能做的事 | 说明 | |-----------|------| | ✅ 获取数据库文件路径 | 通过 `/proc/self/fd/<fd>` + `readlink` | | ✅ 获取文件描述符 `fd` | 通过强制转换 `(unixFile*)pPager->fd` | | ✅ 验证是否为有效数据库 | 读前 16 字节是否匹配 `"SQLite format 3\0"` | | ⚠️ 间接获取物理偏移 | 需结合 `filefrag` / `debugfs` / `dd` 等工具 | | ❌ 直接获取物理偏移 | 不可能,SQLite 不提供此功能 | --- ###
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值