# TsDB2 完整操作教程 本教程涵盖 TsDB2 时序数据库的所有核心操作,包括建表、增删改查、条件查询、索引管理、事务控制等。 --- ## 📋 目录 1. [基础概念](#1-基础概念) 2. [建表与索引](#2-建表与索引) 3. [插入数据 (Create)](#3-插入数据-create) 4. [查询数据 (Read)](#4-查询数据-read) 5. [更新数据 (Update)](#5-更新数据-update) 6. [删除数据 (Delete)](#6-删除数据-delete) 7. [条件查询详解](#7-条件查询详解) 8. [排序与分页](#8-排序与分页) 9. [聚合查询](#9-聚合查询) 10. [多表关联](#10-多表关联) 11. [事务与提交](#11-事务与提交) 12. [高级特性](#12-高级特性) - 12.5b [Blob 存储(链式,≤~4GB)](#125b-blob-存储链式-4gb) 13. [表基类与「必须内嵌基类」约束](#13-表基类与必须内嵌基类约束) --- ## 1. 基础概念 ### 1.1 数据模型定义 TsDB2 使用 Go struct 定义数据模型,通过 `vrtx` tag 声明字符串长度与索引: ```go package main import ( "git.ycl.pub/yl32940/TsDB2/DB" "fmt" ) // 员工表模型 type Employee struct { DB.BaseModel // 内嵌 → _id 主键 (uint64, 自增/可显式指定) Name string `vrtx:"maxlen:32"` // 字符串字段,最大 32 字节 Age int64 // 数值类型无需 tag,按 Go 类型自动推断 (有符号 int64 → Int64) Score int64 // 有符号 int64 → Int64 (保序编码存盘) Dept string `vrtx:"maxlen:16"` // 字符串字段,最大 16 字节 } // 自定义主键: 内嵌基类 (表必须有基类), 再用 vrtx:"pk" 覆盖默认的 _id 列 type Product struct { DB.BaseModel // 表基类: 提供 Insert/Update/Delete/Save/Load/... 头部方法 SKU int64 `vrtx:"pk"` // 主键必须是 64 位整数 (int/uint/int64/uint64), 列名即字段名 Name string `vrtx:"maxlen:32"` Price int64 } // 订单表模型 type Order struct { DB.BaseModel EmployeeID int64 // 有符号 int64 → Int64 Amount int64 // 有符号 int64 → Int64 Status int64 // 0=pending, 1=done, 2=cancel } ``` **字段声明规则:** - **基类(必须)**:每个表结构体都要内嵌 `DB.BaseModel` / `DB.PrimaryModel`(主键表,`_id`)或 `DB.TimeSeriesModel`(时序表,`_ts`)。基类既是"这是一张本框架的表"的标记,也提供对象头部的增删改查(`Insert / Update / Delete / Save / Load / Reload / PK`)。没有内嵌基类的结构体不是本框架的表:`ParseModel` / `UseTable` 直接报 `DB.ErrNotModel`(校验在建表之前,库里不会留下半成品表)。 - **数值类型**:无需 tag,按 Go 类型自动映射。有符号整数单独占一档(`Int8`/`Int16`/`Int32`/`Int64`,盘上用保序编码),无符号整数走无符号档(`Uint8`/`Uint16`/`Uint32`/`Uint64`,v9 起的语义名):`int8` → `Int8`、`int16` → `Int16`、`int32` → `Int32`、`int64`/`int` → `Int64`;`uint8` → `Uint8`、`uint16` → `Uint16`、`uint32` → `Uint32`、`uint64` → `Uint64`。两档盘上字节不同、不可混用,否则范围查询 / 排序会出错。 - **字符串**:用 `vrtx:"maxlen:N"` 指定最大字节数,映射到最接近的定长档位(16~2048 共 24 档,约 1.25 倍递增就近向上取整,如 20→24B、200→224B;>2048 转动态列)。`N` 的上界是 **16360 字节**:超过 2048 字节的列转动态列(内容整体存进一个堆页),声明超过这个数的列在 `ParseModel` 阶段直接报错 —— 那种容量兑不了现,与其等写入时才失败,不如建模时就拒。 - **索引**:在 tag 中追加 `index:名称`(默认 B+ 树)或 `index:名称,type:hash`(Hash 桶式索引)。 - **主键**:`vrtx:"pk"` 把基类默认的键列(`_id` / `_ts`)**覆盖**成自己的字段(`_id` / `_ts` 不再建列),字段类型必须是 `int` / `uint` / `int64` / `uint64`;字符串、浮点、`int32` 等窄整数会在 `ParseModel` 时报 `DB.ErrInvalidPrimaryKey`。注意 `pk` 标签不能替代基类 —— 只写 `pk` 而不内嵌基类同样报 `DB.ErrNotModel`。 - 一个 tag 内多个声明用逗号分隔,例如 `vrtx:"maxlen:32,index:idx_name"`。 - `tsdb` 为旧标签,仍向后兼容,但新代码请统一使用 `vrtx`。 ### 1.2 打开数据库 ```go // 打开或创建数据库文件 db, err := DB.Open("mydb.db") if err != nil { panic(err) } defer db.Close() // 程序退出前关闭 // 设置自动提交延迟(防抖) db.SetAutoCommitDelay(500 * time.Millisecond) // 500ms 后自动 Commit ``` --- ## 2. 建表与索引 ### 2.1 创建表 ```go // ✅ 推荐方式:自动推导表名(从类型名 Employee -> "employee") empTbl, err := DB.UseTable[Employee](db) if err != nil { panic(err) } // 可选:手动指定表名 orderTbl, err := DB.UseTable[Order](db, "orders") // 自定义表名为 "orders" if err != nil { panic(err) } ``` **工作原理:** - `UseTable[Employee](db)` → 自动推导表名为 `"employee"`(首字母小写) - `UseTable[OrderItem](db)` → 自动推导表名为 `"orderItem"` - `UseTable[T](db, "custom_name")` → 使用自定义表名 - **`T` 必须内嵌表基类**(`DB.BaseModel` / `DB.PrimaryModel` / `DB.TimeSeriesModel`),否则 `UseTable` 报 `DB.ErrNotModel`。这道校验发生在建表之前,所以被拒绝的类型不会留下一张半成品表。 - 拿到 `Table[T]` 后两种写法都可用:`empTbl.Insert(&Employee{...})`(把对象交给表),或对象头部 `emp.Insert()`(见 [12.7](#127-对象头部增删改查))。 ### 2.2 创建索引 **推荐方式:在模型上用 `vrtx` tag 声明**,`UseTable` 会自动创建主键索引与所有二级索引: ```go type Employee struct { DB.BaseModel // 内嵌 → 自动生成主键索引 pk_employee (_id) Name string `vrtx:"maxlen:32"` // 无 index → 不建二级索引 Age int64 `vrtx:"index:idx_age,type:hash"` // Hash 二级索引 (等值 O(1)) Score int64 `vrtx:"index:idx_score"` // B+ 二级索引 (默认,支持范围/有序) Dept string `vrtx:"maxlen:16,index:idx_dept"` // B+ 二级索引 } ``` **底层方式**:也可通过 Core 对象手动建索引(字段序号从 1 开始,按结构体字段顺序): ```go import "git.ycl.pub/yl32940/TsDB2/DB/core/page" empCore, _ := db.CoreDB().FindTable("employee") empCore.CreateIndex("idx_age", page.IndexTypeBTree, []byte{2}) // Age 字段索引 empCore.CreateIndex("idx_dept", page.IndexTypeBTree, []byte{4}) // Dept 字段索引 ``` **索引类型说明:** - `IndexTypePrimary` (2): 主键索引,叶子节点存 `[Key → DataPage]`(行数据,聚簇) - `IndexTypeBTree` (5): 二级 B+ 树索引,叶子节点存 `[Key → PKID]`,支持等值/范围/有序 - `IndexTypeHash` (6): Hash 二级索引,等值 O(1),范围查询需全桶扫描 - `IndexTypeTime` (1): 时序索引,优化时间范围查询 - `IndexTypeCompose` (4): 复合索引。处理器底层即 SBT 二级 B+ 树(与 `IndexTypeBTree` 同结构/同性能):最左前缀等值/范围 O(logN)、全局有序、MIN/MAX 端点快路径可用;各分量按真实宽度定长拼接,**分量不限 8 字节**(Decimal/Year/定长字符串列均可)。ORM 侧用默认的多字段同名 `index:NAME` 即为 BTree 复合键;写 `type:compose` 会报「未知字段类型」(ORM 只认 hash/btree),core API 可直接用 `IndexTypeCompose` --- ## 3. 插入数据 (Create) ### 3.1 单行插入 ```go err := empTbl.Insert(&Employee{ Name: "张三", Age: 25, Score: 95, Dept: "工程部", }) if err != nil { fmt.Printf("插入失败: %v\n", err) } ``` ### 3.2 批量插入 ```go // 插入 10000 条员工记录 for i := 1; i <= 10000; i++ { err := empTbl.Insert(&Employee{ Name: fmt.Sprintf("Emp%07d", i), Age: int64(20 + (i % 50)), // 年龄 20~69 Score: int64(i % 1000), // 分数 0~999 Dept: []string{"工程部", "销售部", "人事部", "财务部", "运维部"}[i%5], }) if err != nil { panic(fmt.Sprintf("插入第 %d 行失败: %v", i, err)) } // 每 1000 行打印进度 if i%1000 == 0 { fmt.Printf("已插入 %d 行\n", i) } } ``` ### 3.3 性能优化:批量 Commit ```go // 禁用自动提交,手动控制 Commit 时机 db.SetAutoCommitDelay(0) // 禁用防抖 // 批量插入 10 万行 for i := 1; i <= 100000; i++ { empTbl.Insert(&Employee{...}) } // 一次性提交所有数据 fmt.Println("开始 Commit...") t1 := time.Now() if err := db.Commit(); err != nil { panic(err) } fmt.Printf("Commit 完成,耗时: %.2fs\n", time.Since(t1).Seconds()) ``` **性能对比:** - 每行自动 Commit: 10万行约 100秒 - 批量 Commit: 10万行约 4秒(**快 25 倍**) --- ## 4. 查询数据 (Read) ### 4.1 主键精确查询 ```go // 根据 ID 查询 emp, err := empTbl.Find(1) if err != nil { fmt.Printf("未找到 ID=1 的记录\n") } else { fmt.Printf("ID=%d, Name=%s, Age=%d, Score=%d, Dept=%s\n", emp.ID, emp.Name, emp.Age, emp.Score, emp.Dept) } ``` ### 4.2 条件查询 ```go // WHERE Age = 25 rows, err := empTbl.Where("Age", "=", int64(25)).Find() if err != nil { panic(err) } fmt.Printf("找到 %d 名 25 岁的员工\n", len(rows)) for _, emp := range rows { fmt.Printf(" - %s (Score: %d)\n", emp.Name, emp.Score) } ``` ### 4.3 遍历所有记录 ```go // 全表扫描 allEmps, err := empTbl.FindAll() // 无条件,返回所有记录 if err != nil { panic(err) } fmt.Printf("总共有 %d 名员工\n", len(allEmps)) // 循环处理 for i, emp := range allEmps { if emp.Score >= 90 { fmt.Printf("[%d] 优秀员工: %s, Score=%d\n", i+1, emp.Name, emp.Score) } } ``` ### 4.4 只取部分列(投影下推) ```go // 只解码 Name / Age 两列;未选中的字段在结果对象里保持零值 rows, _ := empTbl.Where("Dept", "=", "工程部").Select("Name", "Age").Find() for _, emp := range rows { fmt.Printf(" %s: %d岁\n", emp.Name, emp.Age) // emp.Score / emp.Dept 等是零值 } ``` - `Select(cols ...string)` 接受 **Go 字段名**,投影下推到引擎:未选中的列不做反序列化与堆页解引用。 - `ORDER BY` 列会被自动并入解码集,排序行为不受影响。 > **反序列化失败会中断查询**:行数据损坏(blob 链断裂、读取长度与引用不符等)时, > `Find` / `Scan` / `Select` 返回 error 并停止扫描,不再静默跳过坏行。 --- ## 5. 更新数据 (Update) ### 5.1 根据主键更新 ```go // 更新 ID=1 的员工信息 err := empTbl.Update(&Employee{ BaseModel: DB.BaseModel{ID: 1}, // 必须指定 ID Name: "李四", Age: 30, Score: 98, Dept: "技术部", }) if err != nil { fmt.Printf("更新失败: %v\n", err) } // 验证更新 emp, _ := empTbl.Find(1) fmt.Printf("更新后: Name=%s, Age=%d\n", emp.Name, emp.Age) // 输出: 更新后: Name=李四, Age=30 ``` ### 5.2 批量更新(循环) ```go // 给所有工程部员工加 10 分 emps, _ := empTbl.Where("Dept", "=", "工程部").Find() for _, emp := range emps { emp.Score += 10 empTbl.Update(emp) } fmt.Printf("已为 %d 名工程部员工加分\n", len(emps)) ``` --- ## 6. 删除数据 (Delete) ### 6.1 根据主键删除 ```go // 删除 ID=2 的员工 err := empTbl.Delete(2) if err != nil { fmt.Printf("删除失败: %v\n", err) } // 验证删除 _, err = empTbl.Find(2) if err != nil { fmt.Println("✅ ID=2 已成功删除") } ``` ### 6.2 批量删除(循环) ```go // 删除所有分数低于 60 的员工 lowScores, _ := empTbl.Where("Score", "<", int64(60)).Find() fmt.Printf("找到 %d 名低分员工\n", len(lowScores)) for _, emp := range lowScores { empTbl.Delete(emp.ID) } fmt.Println("✅ 已删除所有低分员工") // 验证 remaining, _ := empTbl.Count() fmt.Printf("剩余员工数: %d\n", remaining) ``` ### 6.3 级联删除 ```go // 删除部门时,同时删除该部门所有员工 deptToDelete := "临时工" emps, _ := empTbl.Where("Dept", "=", deptToDelete).Find() for _, emp := range emps { empTbl.Delete(emp.ID) } fmt.Printf("已删除 %d 名 %s 员工\n", len(emps), deptToDelete) ``` --- ## 7. 条件查询详解 ### 7.1 比较运算符 ```go // 等于 (=) rows, _ := empTbl.Where("Age", "=", int64(25)).Find() // 不等于 (!=) rows, _ = empTbl.Where("Status", "!=", int64(0)).Find() // 大于 (>) rows, _ = empTbl.Where("Score", ">", int64(90)).Find() // 大于等于 (>=) rows, _ = empTbl.Where("Age", ">=", int64(30)).Find() // 小于 (<) rows, _ = empTbl.Where("Score", "<", int64(60)).Find() // 小于等于 (<=) rows, _ = empTbl.Where("Age", "<=", int64(25)).Find() ``` ### 7.2 范围查询 ```go // WHERE Age >= 20 AND Age <= 30 rows, _ := empTbl. Where("Age", ">=", int64(20)). Where("Age", "<=", int64(30)). Find() fmt.Printf("20~30岁员工: %d 人\n", len(rows)) ``` ### 7.3 IN 查询 ```go // WHERE Dept IN ("工程部", "技术部") rows, _ := empTbl.Where("Dept", "in", []string{"工程部", "技术部"}).Find() fmt.Printf("工程/技术部员工: %d 人\n", len(rows)) // 整数集合 (分量按列类型自动归一); 空集合恒假 (0 行, 不是全表) rows, _ = empTbl.Where("Age", "in", []int64{20, 22, 24}).Find() rows, _ = empTbl.Where("Age", "in", []int64{}).Find() // → 0 行 ``` **IN 与索引**:IN 列上带索引(B+ / Hash / 复合索引的等值前缀)时,每个值展开一次索引定位, 不再退化为全表扫描;重复值自动去重。IN 与同列范围条件并存时,范围条件走索引区间、 IN 由行级过滤兜底(正确性不受影响)。 ### 7.4 LIKE 模糊查询 ```go // WHERE Name LIKE "张%" rows, _ := empTbl.Where("Name", "like", "张%").Find() fmt.Printf("姓张的员工: %d 人\n", len(rows)) // WHERE Name LIKE "%三" rows, _ = empTbl.Where("Name", "like", "%三").Find() fmt.Printf("名字以'三'结尾的员工: %d 人\n", len(rows)) ``` ### 7.5 多条件组合 ```go // WHERE Age > 25 AND Score >= 80 AND Dept = "工程部" rows, _ := empTbl. Where("Age", ">", int64(25)). Where("Score", ">=", int64(80)). Where("Dept", "=", "工程部"). Find() fmt.Printf("符合条件的员工: %d 人\n", len(rows)) ``` ### 7.6 OR 条件 ORM 层的 `Where` 链只表达 AND;**OR / NOT / 嵌套组合要到 core 层**用条件构造器(`core.Or` / `core.And` / `core.Not`),把组合条件交给 core `Query`: ```go import ( "git.ycl.pub/yl32940/TsDB2/DB/core" "git.ycl.pub/yl32940/TsDB2/DB/core/types" ) // WHERE Dept = "工程部" OR Dept = "技术部" —— 等价于 ORM 的 Where("Dept","in",...) empCore, _ := db.CoreDB().FindTable("employee") cond := core.Or( core.Eq("Dept", "工程部", types.FieldType(0)), core.Eq("Dept", "技术部", types.FieldType(0)), ) n, err := empCore.Query().Where(cond).Count() ``` > `core.Eq` 的第三个参数是列类型提示,`types.FieldType(0)` 表示自动推断;条件构造器还有 `Gt/Ge/Lt/Le/Like/ILike/In`,可继续用 `core.And` / `core.Or` / `core.Not` 任意嵌套。 --- ## 8. 排序与分页 ### 8.1 升序排序 ```go // ORDER BY Age ASC rows, _ := empTbl.OrderBy("Age").Find() fmt.Println("按年龄升序:") for _, emp := range rows[:5] { // 只看前5条 fmt.Printf(" %s: %d岁\n", emp.Name, emp.Age) } ``` ### 8.2 降序排序 ```go // ORDER BY Score DESC rows, _ := empTbl.OrderBy("Score", true).Find() // true = 降序 fmt.Println("按分数降序(前5名):") for i, emp := range rows[:5] { fmt.Printf(" [%d] %s: %d分\n", i+1, emp.Name, emp.Score) } ``` ### 8.3 LIMIT 限制数量 ```go // ORDER BY Age ASC LIMIT 10 rows, _ := empTbl.OrderBy("Age").Limit(10).Find() fmt.Printf("最年轻的 10 名员工:\n") for _, emp := range rows { fmt.Printf(" %s: %d岁\n", emp.Name, emp.Age) } ``` > **ORDER BY + LIMIT 是流式 top-k**:内存只保留 `limit+offset` 行的小顶堆,免全量排序; > 相等键的输出顺序与全量排序一致(稳定)。 ### 8.4 OFFSET + LIMIT 分页 ```go // 每页 20 条 pageSize := 20 // 第1页 page1, _ := empTbl.OrderBy("Age").Offset(0).Limit(pageSize).Find() fmt.Printf("第1页: %d 条\n", len(page1)) // 第2页 page2, _ := empTbl.OrderBy("Age").Offset(pageSize).Limit(pageSize).Find() fmt.Printf("第2页: %d 条\n", len(page2)) // 第3页 page3, _ := empTbl.OrderBy("Age").Offset(pageSize*2).Limit(pageSize).Find() fmt.Printf("第3页: %d 条\n", len(page3)) ``` ### 8.5 完整分页示例 ```go func printPage(tbl *DB.Table[Employee], pageNum, pageSize int) { offset := (pageNum - 1) * pageSize rows, _ := tbl.OrderBy("Age").Offset(offset).Limit(pageSize).Find() fmt.Printf("\n=== 第 %d 页 (共 %d 条) ===\n", pageNum, len(rows)) for _, emp := range rows { fmt.Printf(" %-15s %2d岁 %3d分 %s\n", emp.Name, emp.Age, emp.Score, emp.Dept) } } // 打印前3页 for page := 1; page <= 3; page++ { printPage(empTbl, page, 20) } ``` --- ## 9. 聚合查询 ### 9.1 COUNT 计数 ```go // SELECT COUNT(*) FROM employee total, err := empTbl.Count() if err != nil { panic(err) } fmt.Printf("员工总数: %d\n", total) // 带条件的 COUNT youngCount, _ := empTbl.Where("Age", "<", int64(25)).Count() fmt.Printf("25岁以下员工: %d 人\n", youngCount) ``` ### 9.2 MIN/MAX(通过排序实现) ```go // 最小年龄 minAgeRows, _ := empTbl.OrderBy("Age").Limit(1).Find() if len(minAgeRows) > 0 { fmt.Printf("最小年龄: %d岁 (%s)\n", minAgeRows[0].Age, minAgeRows[0].Name) } // 最大分数 maxScoreRows, _ := empTbl.OrderBy("Score", true).Limit(1).Find() if len(maxScoreRows) > 0 { fmt.Printf("最高分数: %d分 (%s)\n", maxScoreRows[0].Score, maxScoreRows[0].Name) } ``` ### 9.3 SUM/AVG(循环计算) ```go // 计算平均分数 allEmps, _ := empTbl.Find() var totalScore int64 for _, emp := range allEmps { totalScore += emp.Score } avgScore := float64(totalScore) / float64(len(allEmps)) fmt.Printf("平均分数: %.2f\n", avgScore) // 各部门平均分 deptMap := make(map[string][]int64) for _, emp := range allEmps { deptMap[emp.Dept] = append(deptMap[emp.Dept], emp.Score) } for dept, scores := range deptMap { var sum int64 for _, s := range scores { sum += s } avg := float64(sum) / float64(len(scores)) fmt.Printf(" %s: 平均 %.2f 分 (%d 人)\n", dept, avg, len(scores)) } ``` --- ## 10. 多表关联 ### 10.1 简单关联查询 ```go // 查询某员工的所有订单 employeeID := int64(1) orders, _ := orderTbl.Where("EmployeeID", "=", employeeID).Find() fmt.Printf("员工 %d 的订单数: %d\n", employeeID, len(orders)) for _, order := range orders { fmt.Printf(" 订单ID=%d, 金额=%d, 状态=%d\n", order.ID, order.Amount, order.Status) } ``` ### 10.2 嵌套循环关联 ```go // 查询每个员工的订单总数 emps, _ := empTbl.Find() for _, emp := range emps[:10] { // 只看前10名员工 orders, _ := orderTbl.Where("EmployeeID", "=", int64(emp.ID)).Find() fmt.Printf("%-15s: %d 个订单\n", emp.Name, len(orders)) } ``` ### 10.3 复杂关联 + 条件 ```go // 查询工程部员工的状态为"已完成"的订单 deptEmps, _ := empTbl.Where("Dept", "=", "工程部").Find() var totalAmount int64 var orderCount int for _, emp := range deptEmps { orders, _ := orderTbl. Where("EmployeeID", "=", int64(emp.ID)). Where("Status", "=", int64(1)). // 1 = done Find() for _, order := range orders { totalAmount += order.Amount orderCount++ } } fmt.Printf("工程部已完成订单: %d 笔, 总金额: %d\n", orderCount, totalAmount) ``` --- ## 11. 事务与提交 ### 11.1 手动 Commit ```go // 禁用自动提交 db.SetAutoCommitDelay(0) // 执行多个操作 empTbl.Insert(&Employee{Name: "A", Age: 25, Score: 90, Dept: "IT"}) empTbl.Insert(&Employee{Name: "B", Age: 30, Score: 85, Dept: "HR"}) empTbl.Delete(5) // 一次性提交 if err := db.Commit(); err != nil { fmt.Printf("提交失败: %v\n", err) // 可以回滚或重试 } else { fmt.Println("✅ 事务提交成功") } ``` ### 11.2 防抖自动提交 ```go // 设置 500ms 防抖 db.SetAutoCommitDelay(500 * time.Millisecond) // 连续插入,最后一次操作后 500ms 自动 Commit for i := 1; i <= 100; i++ { empTbl.Insert(&Employee{...}) } // 等待自动提交 time.Sleep(1 * time.Second) fmt.Println("数据已自动提交") ``` ### 11.3 脏页阈值自动提交 ```go // 当脏页超过 500 页时自动触发 Commit db.SetDirtyThreshold(500) // 大量插入时会自动分批 Commit for i := 1; i <= 100000; i++ { empTbl.Insert(&Employee{...}) } // 无需手动 Commit,系统会根据脏页阈值自动提交 ``` ### 11.4 提交策略预设(推荐) TsDB2 提供三种预设配置,可根据业务场景选择: ```go // 方式1: 使用预设(最简单) db.SetCommitPreset("batch") // 离线批量导入 db.SetCommitPreset("online") // 在线业务默认 db.SetCommitPreset("safe") // 金融/医疗关键数据 // 方式2: 手动配置 db.SetDirtyThreshold(500) // 脏页阈值(页数) db.SetAutoCommitDelay(time.Second) // 防抖超时 ``` #### 预设对比 | 预设 | 脏页阈值 | 内存占用 | 防抖超时 | 断电风险 | 适用场景 | |------|---------|---------|---------|---------|----------| | **batch** | 500页 | 8MB | 1s | ~10,000行 | 离线导入、日志归档 | | **online** | 100页 | 1.6MB | 500ms | ~2,000行 | Web应用、API服务 | | **safe** | 20页 | 320KB | 100ms | ~400行 | 金融交易、医疗记录 | #### 选择建议 **离线批量导入(batch)** ```go db, _ := DB.Open("import.db") db.SetCommitPreset("batch") // 高吞吐 tbl, _ := DB.UseTable[Record](db) for i := 1; i <= 1_000_000; i++ { tbl.Insert(&Record{Name: fmt.Sprintf("Emp%07d", i)}) } // 性能: ~28s 插入 100万行 // 风险: 断电最多丢失 1万行(可重跑脚本) ``` **在线业务(online)** ```go db, _ := DB.Open("app.db") db.SetCommitPreset("online") // 平衡 tbl, _ := DB.UseTable[Order](db) http.HandleFunc("/api/order", func(w http.ResponseWriter, r *http.Request) { order := parseOrder(r) tbl.Insert(order) // 每次请求插入 // 500ms 后自动落盘,断电最多丢失 2000行 }) ``` **关键数据(safe)** ```go db, _ := DB.Open("finance.db") db.SetCommitPreset("safe") // 最高安全 tbl, _ := DB.UseTable[Transaction](db) func transfer(from, to string, amount float64) { tbl.Insert(&Transaction{From: from, To: to, Amount: amount}) // 100ms 后自动落盘,断电最多丢失 400行 // 适合银行转账、医疗记录等关键业务 } ``` ### 11.5 Close 兜底提交 ```go // 即使忘记 Commit,Close 时也会自动提交 db, _ := DB.Open("test.db") tbl, _ := DB.UseTable[Employee](db, "test") for i := 1; i <= 1000; i++ { tbl.Insert(&Employee{...}) } // 没有手动 Commit db.Close() // ✅ Close 内部会调用 Commit,确保数据持久化 // 重新打开验证 db2, _ := DB.Open("test.db") tbl2, _ := DB.UseTable[Employee](db2, "test") count, _ := tbl2.Count() fmt.Printf("Close 后数据保留: %d 条\n", count) // 输出: 1000 条 ``` --- ## 12. 高级特性 ### 12.1 Schema 演进(添加字段) ```go // V1: 初始版本 type EmployeeV1 struct { DB.BaseModel Name string `vrtx:"maxlen:32"` Age int64 } // V2: 添加新字段 type EmployeeV2 struct { DB.BaseModel Name string `vrtx:"maxlen:32"` Age int64 Email string `vrtx:"maxlen:64"` // 新增字段 } // TsDB2 自动处理 schema 版本迁移 tbl, _ := DB.UseTable[EmployeeV2](db, "employee") ``` ### 12.2 删除字段(软删除) ```go // 标记字段为 dropped(不物理删除,保持兼容性) empCore.DropField("Score") // Score 字段标记为已删除 // 查询时不再返回该字段 emp, _ := tbl.Find(1) // emp.Score 将为默认值 0 ``` ### 12.3 删除表 ```go // 删除整个表(回收所有页) err := db.DropTable("order") if err != nil { fmt.Printf("删除表失败: %v\n", err) } // 验证删除 _, exists := db.CoreDB().FindTable("order") if !exists { fmt.Println("✅ order 表已删除") } ``` ### 12.4 索引管理 ```go // 查看所有索引 table, _ := db.CoreDB().FindTable("employee") for _, idx := range table.GetIndexes() { fmt.Printf("索引: %s, 类型: %d, 根页: %d\n", idx.Name, idx.Type, idx.EntryPage) } // 删除索引 table.DropIndex("idx_age") ``` ### 12.5 性能监控 ```go // 统计信息 fi, _ := os.Stat("mydb.db") fmt.Printf("数据库大小: %.2f MB\n", float64(fi.Size())/1024/1024) // 表统计 count, _ := empTbl.Count() fmt.Printf("员工表行数: %d\n", count) // BufferPool 统计 bp := db.CoreDB().GetBufferPool() fmt.Printf("缓存页数: %d\n", bp.Len()) fmt.Printf("脏页数: %d\n", bp.DirtyCount()) ``` BufferPool 是真正的 LRU + 读缓存:读/写命中都会刷新该页的热度,容量满时淘汰最久未使用的 干净页(脏页绝不淘汰,宁可临时超容量,靠脏页阈值触发的 Commit 回落);读未命中会把磁盘内容 作为干净页载入缓存,重复扫描/点查直接走内存。 ### 12.5b Blob 存储(链式,≤~4GB) 二进制大对象**只有一条存储路径**:`FieldType_Blob` 列的行内是 8 字节引用 (`[TotalLen(4B) + FirstPageID(4B)]`),内容落在**链页**上(`PageType=BlobIndex`, `PageHdr.NextPage` 串接),单值上限 `core.BlobChainMax` = 256K 页 ≈ 4GB。 与 `Dynamicbyte` 的分界是"内容能不能跨页":后者行内 6B 堆引用、内容整块落一个堆页, 单值上限 `core.MaxBlobSize` ≈ 16KB;blob 列可以跨任意多页。 | 入口 | 你写什么 | 何时用 | |------|----------|--------| | ORM `[]byte` + `vrtx:"type:blob"` | 内容本身(ORM 自动落链,读回即内容) | 要与行一起走增删改查的小对象 | | ORM `types.Blob`(不写 tag 自动推断 blob 列) | **流式载体**:`Source io.Reader`(文件/网络流直接进 struct),写侧 io.Copy 落链(内存峰值 ~128KB 与内容大小无关);读侧引擎只填回 8B 引用,`row.Data.Open(tbl.BlobReader())` 按需拿 `io.ReadCloser` 流式消费 | 大对象(图片/视频/日志文件),不想把内容整块拉进内存 | | core `Table.InsertRow` / `UpdateRow` | `[]byte` / `string` / `io.Reader` / `types.Blob`(不需要显式事务) | 迁移、手写 core 写入 | | core `tx.BlobCommit` → `BlobRef`(流式) | `io.Reader`(可选 Snappy) | 超大对象、想自己保管引用 | **写进行内的引用由引擎随行回收**(行删除 / 更新 / 表 drop 都会 `BlobFree` 旧链); 只有**行外自管**的引用(`BlobCommit` 的返回值没写进任何行)才需要你自己还链。 ```go // ===== 独立引用 + 流式提交(core 层,文件/视频/备份等大对象)===== tx := db.BeginTransaction() // 提交: io.Reader 流式写入; io.Copy 内部 32KB buf + snappy 内部 ~64KB, // 峰值内存 ≈ 128KB, 与文件大小无关 src, _ := os.Open("video.mp4") defer src.Close() ref, err := tx.BlobCommit(src, types.CompressSnappy) // types.CompressNone 或 CompressSnappy // ref.TotalLen 原始字节数(解压后) // ref.FirstPageID 链首页; (0,0) 表示空 blob, 不分配任何页 if err != nil { tx.Rollback(); return err } if err := tx.Commit(); err != nil { return err } // 读回: io.ReadAll → []byte (解压后), 长度 == ref.TotalLen data, err := db.CoreDB().FindTable("files").BlobRead(ref, types.CompressSnappy) if err != nil || uint32(len(data)) != ref.TotalLen { return ErrBlobCorrupted } // 用完释放 (空 blob 是 no-op; 写进行内的引用不用你管) db.CoreDB().FindTable("files").BlobFree(ref) ``` 调用约定: 1. **事务可选,但推荐**:`tx := db.BeginTransaction()` + `tx.Commit()` 时中途失败可 `tx.Rollback()` 整链回收(新页都在 `tx.allocated` 里);不在事务里也走得通(`db.BlobCommit`,或直接往 blob 列写内容),代价是失败时已分配的页成为孤儿。 2. **`compress` 由调用方选择并保存**:`BlobRef` 不携带压缩标志;`BlobCommit` 与 `BlobRead` 的 `compress` 必须一致(不一致 → `core.ErrBlobCorrupted`)。 3. **中断安全**:流中途 `io.Reader.Read` 返错 → 函数立即返错;已分配链页仍归本事务所有,`tx.Rollback()` 走 `tx.allocated` 路径整链回收;`tx.Commit()` 之后再丢,则需 `tbl.BlobFree(ref)` 显式还链。 4. **容量上限**:`BlobChainMax = 256 * 1024` 页 ≈ 4GB;超限返 `core.ErrBlobTooLarge`。 5. **行外引用要显式释放**(行内引用不用): ```go row, _ := tbl.Find(id) blobRef, _ := blobFromRow(row) // 你自己存到某列里的 BlobRef tbl.Delete(id) // 行内 blob 列的链由引擎随行回收 tbl.BlobFree(blobRef) // 行外的独立句柄: 引擎不知道它还在 ``` ```go // ===== blob 列(ORM,与行一起走增删改查)===== type File struct { DB.BaseModel Name string `vrtx:"maxlen:128"` Cover []byte `vrtx:"type:blob"` // 载体一: []byte —— 一次性, 读回即内容本身 Data types.Blob // 载体二: 流式 —— 不写 tag 也自动推断为 blob 列 } // []byte 载体: 增/改/查 与普通列无异; 索引不允许 (ErrIndexBlobField) _ = tbl.Insert(&File{Name: "avatar.jpg", Cover: jpegBytes}) // 读回来就是内容本身: obj.Cover 是 []byte, 不是引用 // 流式载体: 写侧 Source 是任意 io.Reader (文件/网络流直接进 struct) f, _ := os.Open("video.mp4") _ = tbl.Insert(&File{Name: "video.mp4", Data: types.Blob{Source: f}}) // 读侧引擎只填回 8B 引用, 内容仍在链页; 按需开流消费, 不整块进内存 got, _ := tbl.Find(id) rc, _ := got.Data.Open(tbl.BlobReader()) // io.ReadCloser io.Copy(dst, rc) // Insert/Update 成功后引用自动回填进对象; 之后只改普通字段的 Update 不会抹掉 blob 链 // 单值超 ~4GB: 写入报错 ErrBlobTooLarge, 含具体上限与写下的字节数 ``` 完整演示:`go run ./demo/blob_stream`(core 层 5 场景:空流 / 8KB 不压缩 / 1MB Snappy / 网络中断回滚 / 100MB 随机流)、`go run ./demo/blob_orm`(ORM 层 6 场景:32MB 流式写 / 流式读 CRC / []byte 对照 / 改名 Update 保链 / 重开持久化 / 删行归还链页)。 ### 12.6 并发安全 ```go // TsDB2 不是线程安全的,需要在应用层加锁 var mu sync.Mutex go func() { mu.Lock() defer mu.Unlock() empTbl.Insert(&Employee{...}) }() go func() { mu.Lock() defer mu.Unlock() empTbl.Find(1) }() ``` ### 12.7 对象头部增删改查 内嵌表基类之后,每个记录自己就带一套增删改查(父类定义方法、子类决定主键样式), 适合"读出来 → 改几个字段 → 写回去"的写法: ```go emp := empTbl.New() // New: 创建 + 绑定(知道自己属于哪张表) emp.Name, emp.Age = "alice", 25 emp.Insert() // 增:主键为 0 时引擎发号并回写 emp.ID emp.Age = 26 emp.Update() // 改:按 emp.ID 整行覆盖(未赋值字段会清零) emp.Reload() // 查:按 emp.ID 重新读进自己 // 想要一个类型安全的新对象请用 empTbl.Find(id) —— 头部方法拿不到类型参数 T emp.Delete() // 删:按 emp.ID ``` - 绑定是**自动**的:`Insert` / `Find` / `FindAll` / `Select` / `Where(...).Find()` 都会顺带绑定。 手工 `&Employee{...}` 构造且第一次动作就是 `Update` / `Delete` / `Reload` 时,先 `empTbl.Bind(obj)`。 - 未绑定的对象调用头部方法报 `DB.ErrModelNotBound`(不会崩、也不会写错表)。 - **不要浅拷贝已绑定的对象**:绑定信息存在对象里,`dup := *emp` 之后 `dup.Update()` 更新的是 `emp`。 需要独立对象用 `New` + `Load`。 - 主键列可以用 `vrtx:"pk"` 换成表自己的字段(见 1.1),头部方法随之按那一列定位: `u.No = 7; u.Insert()` 之后 `u.Update()` / `u.Reload()` / `u.Delete()` 都按 `No` 生效。 --- ## 13. 表基类与"必须内嵌基类"约束 | 基类 | 一级(行存储)索引 | 默认键列 | 基类字段 | |---|---|---|---| | `DB.BaseModel` / `DB.PrimaryModel`(等价) | `Primary`(B+ 树,聚簇) | `_id` | `ID uint64` | | `DB.TimeSeriesModel` | `Time`(分段) | `_ts` | `TS uint64` | - 一个结构体只能内嵌**一个**表基类;同时内嵌 `BaseModel` 与 `TimeSeriesModel` 报 `DB.ErrInvalidPrimaryKey`。 - **表必须内嵌基类**:没有内嵌基类的结构体不是本框架的表,`ParseModel` / `UseTable` 报 `DB.ErrNotModel`。 单写 `vrtx:"pk"` 不能替代基类 —— 它的语义是"覆盖基类默认的键列"。 - 按 SQL 元数据动态建表的 `migrate` 包(见 [`MIGRATION_GUIDE.md`](MIGRATION_GUIDE.md))走 core 层 `db.CreateTable`,不受这条约束:它建表时根本没有 Go 结构体。 --- ## 🎯 完整示例:员工管理系统 ```go package main import ( "git.ycl.pub/yl32940/TsDB2/DB" "fmt" "time" ) type Employee struct { DB.BaseModel Name string `vrtx:"maxlen:32"` Age int64 `vrtx:"index:idx_age"` // B+ 树二级索引 Score int64 `vrtx:"index:idx_score"` // B+ 树二级索引 Dept string `vrtx:"maxlen:16,index:idx_dept"` } func main() { // 1. 打开数据库 db, _ := DB.Open("company.db") defer db.Close() db.SetAutoCommitDelay(0) // 手动控制 // 2. 建表 + 索引(UseTable 会按 vrtx tag 自动创建全部索引) tbl, _ := DB.UseTable[Employee](db) // ✅ 无需传递 "employee" // 3. 批量插入 fmt.Println("插入数据...") depts := []string{"工程部", "销售部", "人事部", "财务部", "运维部"} for i := 1; i <= 1000; i++ { tbl.Insert(&Employee{ Name: fmt.Sprintf("Emp%04d", i), Age: int64(20 + (i % 50)), Score: int64(50 + (i % 51)), Dept: depts[i%5], }) } db.Commit() // 4. 查询 fmt.Println("\n=== 查询示例 ===") // 4.1 主键查询 emp, _ := tbl.Find(1) fmt.Printf("ID=1: %s, %d岁\n", emp.Name, emp.Age) // 4.2 条件查询 young, _ := tbl.Where("Age", "<", int64(25)).Find() fmt.Printf("25岁以下: %d 人\n", len(young)) // 4.3 排序 + 分页 top10, _ := tbl.OrderBy("Score", true).Limit(10).Find() fmt.Println("分数Top 10:") for i, e := range top10 { fmt.Printf(" [%d] %s: %d分\n", i+1, e.Name, e.Score) } // 4.4 聚合 total, _ := tbl.Count() fmt.Printf("总人数: %d\n", total) // 5. 更新(Update 为整行覆盖:未赋值字段会被清零,因此先取出整行再改) fmt.Println("\n=== 更新示例 ===") row, _ := tbl.Find(1) row.Score = 100 tbl.Update(row) emp, _ = tbl.Find(1) fmt.Printf("ID=1 更新后分数: %d\n", emp.Score) // 6. 删除 fmt.Println("\n=== 删除示例 ===") tbl.Delete(2) _, err := tbl.Find(2) if err != nil { fmt.Println("ID=2 已删除") } // 7. 复杂查询 fmt.Println("\n=== 复杂查询 ===") itHighScore, _ := tbl. Where("Dept", "=", "工程部"). Where("Score", ">", int64(80)). Find() fmt.Printf("工程部高分员工: %d 人\n", len(itHighScore)) fmt.Println("\n✅ 所有操作完成!") } ``` --- ## 📚 参考资料 - **Analyzer 工具**: 可视化查看数据库内部结构 - **verify_full**: 完整功能测试用例 - **verify_no_commit**: 事务与自动提交流程测试 --- **祝使用愉快!** 🎉