SQLite3 文件格式

SQLite3 文件格式

本文来源:Zero’s Donkey Den
原文地址:7-0.cc/sqlite3_file_format
转载许可:未经授权,禁止转载

SQLite 文件格式

SQLite是目前世界上应用最多、最广泛的数据库,以其开源、无版权、轻量级等特性,使它涵盖了各种各样的应用场景。比如嵌入式电子设备、手机、电脑、汽车等各种领域。可以这么说:它没有出现在你的生活中,但却影响了你生活中的方方面面。

具体来说,你的手机可能会用它来存储联系人、短信、通话记录、浏览器历史、书签、QQ、微信等APP的聊天内容等信息。可想而知它是如此的重要,而因此也衍生出了诸如:数据恢复司法取证存储及查询优化等业务场景,而正确的理解SQLite的文件格式是其中必不可少的一个环节。

SQLite3主文件格式

我们知道SQLite3是以单文件来存储的数据,当数据库打开时,文件系统上可能还会存在两个额外文件:*-wal日志文件、*-shm共享内存文件。

在这里我们讨论主数据库文件,暂不涉及*-wal*-shm

而另外需要注意的是:在撰写本文时,当前SQLite3最高版本为:3.42.0 (2023-05-16),笔者测试的最低版本为:3.24.0 (2018-06-04)。如果你的版本在高于或低于这个区间,实际文件格式可能存在差异!

SQLite3文件由固定大小的页(Page)组成

SQLite3-DB.png

  • 页大小为: 512~32768, 必须是2整倍数, 默认1024字节。
  • 页编号为: 1~2147483648(2^31), 以4字节表示(记作Page n)。页编号为0时, 表示不存在。
  • 页类型为:
    • B-tree页
      • 表内部页(Table Interior)
      • 表叶子页(Table Leaf)
      • 索引内部页(Index Interior)
      • 索引叶子页(Index Leaf)
    • 空闲页(Freelist)
      • 主干页(Freelist Trunk)
      • 叶子页(freelist Leaf)
    • 溢出页(Overflow)
    • lock-byte
    • pointer-map

文件头

Page 1的页类型总是B-tree, 且只有该页的前100字节是数据库的文件头, 记录了数据库的相关信息, 定义在: btreeint.h:45(未合并的版本)

All of the integer values are big-endian (most significant byte first).

OffsetSizeDescription
016The header string: “SQLite format 3\000”
162The database page size in bytes. Must be a power of two between 512 and 32768 inclusive, or the value 1 representing a page size of 65536.
181File format write version. 1 for legacy; 2 for WAL.
191File format read version. 1 for legacy; 2 for WAL.
201Bytes of unused “reserved” space at the end of each page. Usually 0.
211Maximum embedded payload fraction. Must be 64.
221Minimum embedded payload fraction. Must be 32.
231Leaf payload fraction. Must be 32.
244File change counter.
284Size of the database file in pages. The “in-header database size”.
324Page number of the first freelist trunk page.
364Total number of freelist pages.
404The schema cookie.
444The schema format number. Supported schema formats are 1, 2, 3, and 4.
484Default page cache size.
524The page number of the largest root b-tree page when in auto-vacuum or incremental-vacuum modes, or zero otherwise.
564The database text encoding. A value of 1 means UTF-8. A value of 2 means UTF-16le. A value of 3 means UTF-16be.
604The “user version” as read and set by the user_version pragma.
644True (non-zero) for incremental-vacuum mode. False (zero) otherwise.
684The “Application ID” set by PRAGMA application_id.
7220Reserved for expansion. Must be zero.
924The version-valid-for number.
964SQLITE_VERSION_NUMBER

SQLite Documentation: PRAGMA page_size;

sqlite3-filehead.png

页头 Page Header

OFFSETSIZENAMEDESCRIPTION
01FlagThe one-byte flag indicating the b-tree page type.
12FreeblockOffestbyte offset to the first freeblock
32CellTotalnumber of cells on this page
52CellOffestfirst byte of the cell content area
71FragmentedBytesnumber of fragmented free bytes
84Right child (the Ptr(N) value). Omitted on leaves.
  • Flag 定义了页的类型:
    • 0x2 表示索引内部页(the page is an interior index b-tree page)
    • 0x5 表示表内部页(the page is an interior table b-tree page)
    • 0xA 表示索引叶子页(the page is a leaf index b-tree page)
    • 0xD 表示表叶子页(the page is a leaf table b-tree page)
  • FreeblockOffest 表示第一个空闲块的偏移量
  • CellOffest 表示单元内容的偏移量
  • FragmentedBytes 表示此页中碎片空间的总字节数

注意: Flags字段的官方描述如下(btreeInt.h:136), 但与实际并不相符,以官方文档描述为准.

The flags define the format of this btree page. The leaf flag means that
this page has no children. The zerodata flag means that this page carries
only keys and no data. The intkey flag means that the key is an integer
which is stored in the key size entry of the cell header rather than in
the payload area.

B-tree页

  • B-tree用于存储表的数据与索引.

  • B-tree内部实际承载数据的媒介称为单元(Cell), 由页头中定义的Flag, 决定单元的格式.

  • B-tree必然存在, 而FreelistOverflow不一定存在.

  • B-tree至少占用一个完整的页, 每个页是B-tree的一个节点, 每个表或索引的第一页称为根页.

    • 所有表或索引的根页编号都存储在系统表sqlite_master中, 该表的根页编号为Page 1.
  • B-tree页结构

    |-----------------|
    | file header     |   100 字节, 仅存在于page 1.
    |-----------------|
    | page header     |   叶子页(leaf pages)8字节, 内部页(interior pages)12字节
    |-----------------|
    | cell pointer    |   |  存储单元指针的数组
    | array           |   |  每个元素2字节, 按记录顺序存储
    |                 |   v  向下增长
    |-----------------|
    | unallocated     |   未分配的空间
    | space           |
    |-----------------|   ^  向上增长
    | cell content    |   |  无序且夹杂着空闲块(freeblocks)以及自由空间碎片(Free space).
    | area            |   |  存储单元内容的区域
    |-----------------|
    | reserved region |   保留空间
    |                 |   用于扩展存储每个页的信息, 其大小由文件头的第20字节处定义, 通常为0.
    |-----------------|   
    

可变长整数

可变长整数, 为1~9字节, 仅使用低7位, 而第8位在第一个字节(整数的低地址方向)是0, 其余是1.

0x00                      becomes  0x00000000
0x7f                      becomes  0x0000007f
0x81 0x00                 becomes  0x00000080
0x82 0x00                 becomes  0x00000100
0x80 0x7f                 becomes  0x0000007f
0x8a 0x91 0xd1 0xac 0x78  becomes  0xa2345678 // 源码这里写错成了0x12345678, 根据算法结果应该是: 0xa2345678 
0x81 0x81 0x81 0x81 0x01  becomes  0x10204081 

// 参考:
// https://github.com/sqlite/sqlite/blob/58b5921ca460032c6d19bfe6f0f736763f33c3bc/src/btreeInt.h#L183 
// https://sqlite.org/forum/forumpost/11fc7308d5e607f2

0x81 0x00 解码过程,如下图所示:

SQLite3-Varable-Number.png

参考代码 Python:

def fromBytes(ibytes:bytes):
    '''
    解码可变长整数
    '''
    value = 0
    count = 0
    total = len(ibytes)
    if total > 9:
        total = 9
    while count < total:
        value = value << 7 | ibytes[count] & 0x7f
        if not ibytes[count] & 0x80:
            return SQLiteNumber(value, count + 1)
        elif count + 1 >= total: 
            return SQLiteNumber() # 解析失败
        count = count + 1
    return SQLiteNumber(value, count)

单元(Cell)

  • 单元长度是可变的, 并可能会跨越多个页(溢出).

  • 单元存储在单元内容区域(Cell Content Area), 位于页尾并向页头的方向逆向生长.

  • 单元存储是无序且不连续的.

  • 单元的索引或称指针存储在单元指针数组(Cell Pointer Array), 该数组的存储是有序的.

  • 单元的内部使用可变长度的整数.

  • 单元结构依赖于b-tree页面类型, 可以分为4种不同格式的单元:

    • 0x5 表内部页(Table Interior)
      存储的是用于组织和导航数据的信息,不存储实际数据(简单的说就是:维护了一些信息,用来快速定位数据所在的表叶子页)。
    • 0xD 表叶子页(Table Leaf)
      存储实际的数据库记录,包括所有的表结构表数据等等。
    • 0x2 索引内部页(Index Interior)、0xA 索引叶子页(Index Leaf)
      存储的是索引信息,会存储部分表字段(如:主键、索引列、唯一约束等字段)的内容副本。
  • 不同格式的单元结构对比如下:

    DatatypeAppears in...Description
    Table Leaf (0x0d)Table Interior (0x05)Index Leaf (0x0a)Index Interior (0x02)
    4-byte integer  左子(left child)页号
    varint 负载数据字节数
    varint  Rowid
    byte array 负载数据
    4-byte integer? ??第一个溢出页的页号, 如果没有溢出则不存在

Page中的单元及负载:

SQLite-Page-Cell.png

单元负载(Cell.Payload)
  • 单元负载的结构称为"Record Format", 由两部分组成:

    • 标头(Header)
    • 主体(Body)
  • Header与Body存在多个字段, 且两个区域的字段是一一对应的, 前者包含字段类型与数据长度, 后者是字段的数据.

  • 结构如下:

    | ------------------- HEADER ----------------- | ------------ BODY ------------ |
    | header size | type 1 | type 2 | ... | type N | data 1 | data 2 | ... | data N |
    
    • Header中的所有字段都是可变长整数.
    • Header size 表示标头区域的字节数.
    • type 1, type 2 分别对应 data 1, data 2 …以此类推, 直到 type N对应data N.
    • type 1, type 2 等类型称为"Serial Type", 其存储的值, 表示字段的类型与长度(字节数):
      Serial TypeContent SizeMeaning
      00Value is a NULL.
      1~4NValue is a big-endian (N * 8)-bit twos-complement integer.
      56Value is a big-endian 48-bit twos-complement integer.
      68Value is a big-endian 64-bit twos-complement integer.
      78Value is a big-endian IEEE 754-2008 64-bit floating point number.
      80Value is the integer 0. (Only available for schema format 4 and higher.)
      也用于Float类型的0值.
      90Value is the integer 1. (Only available for schema format 4 and higher.)
      10,11可变仅内部使用
      N≥12 & 偶数(N-12)/2Value is a BLOB that is (N-12)/2 bytes in length.
      N≥13 & 奇数(N-13)/2Value is a string in the text encoding and (N-13)/2 bytes in length. The nul terminator is not stored.

上图中的负载数据解析:

 H  06  00 01 31 [82 63]
 E      |  |  |  |  
 A  rowid  |  |  |  
 D         |  |  |  
           |  |  |  
 B         16 |  |                                                   = '22'
 O            E8 |B5 B5 E5 8D 81 E4 B9 9D E4 B8 83 E5 8D 81 E4 B8 80 = UTF-8 STR '赵十九七十一' 
 D               4D 61 65 63 65 6E 61 73 20 74 72   ...  8C E3 80 82 = UTF-8 STR 'Maecenas tristique orci ac sem。汉字内容在这里。 Duis ultricies pharetra magna。汉字内容在这里。 Donec accumsan malesuada orci。汉字内容在这里。'
 Y    

B-tree内部页(Table Interior 0x05)的导航信息

表内部页仅存储导航信息, 所有数据都存储在叶子页中, 且所有叶子页都在同一层上并以key排序.

导航信息看起来如下(与实际存储有一定差异):

----------------------------------------------------------------
|  Ptr(0) | Key(0) | Ptr(1) | Key(1) | ... | Key(N-1) | Ptr(N) |
----------------------------------------------------------------
  • Ptr(i) 存储的是页号
  • Key(i) 存储的是唯一key(默认是rowid)
  • Ptr(N) 是最右边的页号, 存储在页头种的最后一个字段
  • 从左到右的排序逻辑为(以Key升序排列):
    • Ptr(i)页内部所有记录的key <= key(i)
    • Ptr(i)页内部所有记录的key > Key(i-1)

空闲块

  • 单元内容区域内未使用的空间被收集为空闲块链表(Freeblock List), 按照地址升序排列(即下一块地址总是大于前一块的地址)
  • 每个空闲块最小4字节, 因为需要4字节来存储空闲块信息, 小于4字节的空间称为碎片(Fragment).
  • 空闲块信息结构如下:
    SIZENAMEDESCRIPTION
    2NextOffestByte offset of the next freeblock
    2BlockSzieBytes in this freeblock

溢出页(overflow page)

  • 多个溢出页组成一个链表, 除了最后一页外, 每个页面都完全填充了数据(页面大小- 4字节), 最后一页可以只有1字节的数据。

  • 溢出页结构如下:

    SIZEDESCRIPTION
    4Page number of next overflow page
    *Data
  • 溢出到溢出页的负载大小也取决于页类型,算法如下:

    • U 为数据库页面的可用大小, 即: 总页大小减去每页末尾的保留空间(也就是文件头第20字节表示的大小)。

    • P 为负载大小

    • X 最大值: 表示可以直接存储在当前页(B-tree)上而不会溢出到溢出页上的的最大负载数

    • M 最小值: 表示在允许溢出之前必须存储在当前页上的最小有效负载数

    • K 理想值: 表示在当前页上存储多少数据后, 剩下的溢出页刚好容纳所有数据而不造成溢出页上出现剩余空间

    • X 取值算法(根据页面类型决定):

      • X = U-35 0xd(Table Leaf Page)
      • X = ((U-12)*64/255)-23 0xa, 0x2(Index Page)
    • M = ((U-12)*32/255)-23

    • K = M+((P-M)%(U-4))

    • 如果 P <= X:
      则整个负载存储在此页面上, 不会产生溢出。

    • 如果 P > X && K <= X:
      则存储在此页上的字节数为K

    • 如果 P > X && K > X:
      则存储在此页上的字节数为M, 页上存储的字节数永远不少于M

    • 常数说明:

      • (U-4) 溢出页除去页头之后的可用空间
      • (U-12) B-tree页除去页头之后的可用空间
      • (32/255) SQLite开发者预设的一个比值,具体考量不做探究。
      • (64/255) SQLite开发者预设的一个比值,具体考量不做探究。
      • (23) SQLite开发者预设的一个常数, 大概是为了预留一个缓冲空间。
      • (35) B-tree页页头 + (23)
    • 参考代码 C语言

        // https://sqlite.org/src/file/ext/misc/dbdata.c
        // dbdataNext()
        /* Figure out how much data to read from the local page */
        U = pCsr->nPage;
        if( bHasRowid ){
          X = U-35;
        }else{
          X = ((U-12)*64/255)-23;
        }
        if( nPayload<=X ){
          nLocal = nPayload;
        }else{
          int M, K;
          M = ((U-12)*32/255)-23;
          K = M+((nPayload-M)%(U-4));
          if( K<=X ){
            nLocal = K;
          }else{
            nLocal = M;
          }
        }
      

空闲页(freelist page)

  • 空闲页有两种子类型:
    • 主干页(Trunk Pages)
    • 叶子页(Leaf Pages)
  • 文件头指向了第一个trunk页链表, 每个trunk页指向了多个leaf页.
  • 叶子页的结构是未指定的
  • 主干页结构如下:
    SIZEDESCRIPTION
    4Page number of next trunk page
    4Number of leaf pointers on this page
    *zero or more pages numbers of leaves

Ptrmap页(Pointer Map or Ptrmap Pages)

  • SQLite FileHead 第52字节为零, 则没有Ptrmap页, 否则必须存在.
  • 数据库如果包含Ptrmap页, 则第一个Ptrmap页是Page 2.
  • Ptrmap页由元素长度为5字节的数组构成, 设 数组长度为J, 页可用空间为U, 则J=U/5.
  • 第一个Ptrmap页将包含从第3页到第J+2页的反向指针信息.
  • 第二个Ptrmap页将在页J+3上,该页将提供从页J+4到页2*J+3的反向指针信息。
  • 数据库如果包含Ptrmap页, 由上面计算确定的页码上的所有页都必须是Ptrmap页(换句话说这个坑只能蹲Ptrmap页)。但是,如果byte-lock页恰好在此页码上,对于这种情况,Ptrmap将移动到下一个页面(byte-lock页优先级更高)。
  • 5字节的数组元素格式如下:
    SIZEDESCRIPTION
    1page type
    4page number(big-endian)
  • 页类型如下:
    • 0x1 A b-tree root page. The page number should be zero.
    • 0x2 A freelist page. The page number should be zero.
    • 0x3 The first page of a cell payload overflow chain. The page number is the b-tree page that contains the cell whose content has overflowed.
    • 0x4 A page in an overflow chain other than the first page. The page number is the prior page of the overflow chain.
    • 0x5 A non-root b-tree page. The page number is the parent b-tree page.

SQLite FileHead 第52字节偏移量

The page number of the largest root b-tree page when in auto-vacuum or incremental-vacuum modes, or zero otherwise.

Lock-Byte页

  • 所有现代操作系统都支持查询文件锁定,因此实际上不再需要锁字节页,只是为了向后兼容而保留。
  • lock-byte是一个单独的页,其位于数据库文件的1073741824 ~ 1073742335,即1G的位置,占512个字节;小于1G的数据库文件则没有此页。
  • lock-byte被预留出来,供特定于操作系统的VFS实现在实现数据库文件锁定原语时使用。SQLite不使用锁字节页。
  • SQLite中内置的unix和win32 VFS实现不会写入锁字节页,但用于其他操作系统的第三方VFS实现可能会。

制作样本

$sqlite3.exe person.db

执行下面的SQL语句:

CREATE TABLE person(
  id   INTEGER PRIMARY KEY,
  age  INTEGER,
  name TEXT,
  note TEXT);

INSERT INTO person VALUES(1, 28, '赵四', Null);
INSERT INTO person VALUES(2, 96, '余老爷', Null);
INSERT INTO person VALUES(7, 27, 'This is deleted record 删除的记录 end', Null);
DELETE FROM person WHERE id = 7;

制作好的样本:
https://github.com/ZeroKwok/SQLite3DB-Samples.git

注意:
sqlite3.exe 最好是使用自己编译的版本, 否则其他的发行版可能出现删除数据后写0的情况.

编译SQLite3.exe

  • 52
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Qt 是一个跨平台的 C++ 应用程序开发框架,支持各种操作系统平台上的GUI应用程序开发。SQLite3 是一种轻型的嵌入式关系型数据库,它可以在各种操作系统上运行。在 Qt 中使用 SQLite3 进行数据存储和管理是非常常见的。而文件加密可以保证数据的安全性。 在 Qt 中实现 SQLite3 数据库文件加密可以通过以下几个步骤来完成: 1. 使用 Qt 内置的加密模块对密码进行加密。 2. 在打开 SQLite3 数据库文件时,通过输入正确的密码进行解密。 3. 在 SQLite3 数据库操作时,使用 SQLite3 提供的加密扩展函数对数据进行加密和解密。 下面是一个简单的示例代码: ``` // 使用 Qt 内置的加密模块对密码进行加密 QString password = "123456"; QByteArray encryptedPassword = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Md5); // 打开 SQLite3 数据库文件时输入正确的密码进行解密 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("mydatabase.db"); db.setPassword(encryptedPassword); // 使用 SQLite3 提供的加密扩展函数对数据进行加密和解密 QSqlQuery query; query.exec("PRAGMA key='" + password + "'"); query.exec("CREATE TABLE mytable (id INTEGER PRIMARY KEY, name TEXT)"); query.exec("INSERT INTO mytable (name) VALUES ('hello')"); query.exec("SELECT * FROM mytable"); while (query.next()) { int id = query.value(0).toInt(); QString name = query.value(1).toString(); qDebug() << "id:" << id << ", name:" << name; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值