SQLite 使用指南:从入门到实践
引言
在移动互联网时代,本地数据存储是每个开发者都必须面对的问题。无论是手机应用、桌面软件,还是物联网设备,如何高效、可靠地存储和管理数据,始终是一个核心需求。
SQLite 给出了一个优雅的答案——一个零配置、无服务器、嵌入式的关系型数据库引擎。它不需要安装,不需要配置,不需要启动独立的服务进程,整个数据库就是一个普通的磁盘文件。正因如此,SQLite 成为了全球部署最广泛的数据库引擎,从 Android 到 iOS,从浏览器到桌面应用,处处都有它的身影。
本文将带你从零开始,全面掌握 SQLite 的使用——从命令行工具到编程语言集成,从基础 CRUD 到性能优化,最后通过一个完整的实战项目串联所有知识点。
一、SQLite 是什么?
1.1 核心特性
SQLite 的设计理念可以用四个字概括:轻量、可靠。
| 特性 | 说明 |
|---|---|
| 零配置 | 无需安装、无需配置、无需启动服务进程 |
| 无服务器 | 应用程序直接读写磁盘文件,无中间件开销 |
| 单文件存储 | 整个数据库(表、索引、视图)存储在一个文件中 |
| 跨平台 | 支持 Windows、Linux、macOS、iOS、Android 等全平台 |
| ACID 兼容 | 支持事务、原子性、一致性、隔离性、持久性 |
| 轻量级 | 核心库仅约 500KB,内存占用极低 |
| 公有领域 | 完全免费,可用于任何商业用途 |
1.2 适用场景
SQLite 的“轻量”并不等于“弱鸡”。它的适用场景非常广泛:
- 移动应用本地存储:Android 和 iOS 的首选嵌入式数据库
- 桌面软件配置管理:浏览器书签、IDE 配置、邮件客户端
- 物联网设备数据采集:传感器数据的本地缓存与同步
- 开发与测试环境:原型验证、单元测试的临时数据库
- 中小型网站后端:访问量不大的 Web 应用
当然,SQLite 也有它的边界——超高并发写入、客户端-服务器架构、细粒度用户权限管理等场景,更适合使用 PostgreSQL 或 MySQL。
1.3 命令行工具 vs SQLite 库
这里需要区分两个容易混淆的概念:
- SQLite 库(library):实现 SQL 数据库引擎的代码,是供其他程序调用的底层组件
- sqlite3 命令行工具(CLI):一个独立的应用程序,为用户提供手动输入和执行 SQL 的界面
当我们说“学习 SQLite”时,通常是指学习如何使用 SQLite 库;而 sqlite3 命令行工具是我们学习和调试的得力助手。
二、快速开始:命令行工具的使用
无论你最终用哪种编程语言操作 SQLite,掌握 sqlite3 命令行工具都是非常有帮助的——它可以让你快速验证 SQL 语句、查看数据库结构、进行数据导入导出。
2.1 安装
Windows:下载预编译的二进制包(sqlite-tools-win32-*.zip),解压后将 sqlite3.exe 所在目录添加到系统 PATH
Linux:
sudo apt install sqlite3
# 或
sudo yum install sqlite3
macOS:
brew install sqlite3
2.2 启动并创建数据库
启动 sqlite3 时,可以指定数据库文件名。如果文件不存在,SQLite 会自动创建:
$ sqlite3 test.db
SQLite version 3.43.0 2023-08-11 17:45:23
Enter ".help" for usage hints.
sqlite>
如果不指定数据库文件名,SQLite 会使用一个临时内存数据库,程序退出后数据即消失:
$ sqlite3
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite>
2.3 基本操作
-- 创建表
sqlite> CREATE TABLE users (
...> id INTEGER PRIMARY KEY AUTOINCREMENT,
...> name TEXT NOT NULL,
...> age INTEGER,
...> created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
...> );
-- 插入数据
sqlite> INSERT INTO users (name, age) VALUES ('Alice', 25);
sqlite> INSERT INTO users (name, age) VALUES ('Bob', 30);
sqlite> INSERT INTO users (name, age) VALUES ('Charlie', 28);
-- 查询数据
sqlite> SELECT * FROM users;
1|Alice|25|2025-01-15 10:30:00
2|Bob|30|2025-01-15 10:30:05
3|Charlie|28|2025-01-15 10:30:10
-- 设置输出格式(更易读)
sqlite> .mode column
sqlite> .headers on
sqlite> SELECT * FROM users;
id name age created_at
-- -------- --- -------------------
1 Alice 25 2025-01-15 10:30:00
2 Bob 30 2025-01-15 10:30:05
3 Charlie 28 2025-01-15 10:30:10
2.4 常用点命令
sqlite3 有一类以点(.)开头的特殊命令,称为点命令(dot-command),用于控制输出格式和执行管理操作:
| 命令 | 作用 |
|---|---|
.tables |
列出当前数据库中的所有表 |
.schema [表名] |
显示表的创建语句 |
.databases |
显示当前打开的数据库文件 |
.mode [模式] |
设置查询结果的输出格式(column、csv、json 等) |
.headers on/off |
是否显示列名 |
.output [文件名] |
将输出重定向到文件 |
.import [文件] [表名] |
从 CSV 文件导入数据 |
.dump |
将整个数据库导出为 SQL 语句 |
.quit |
退出命令行工具 |
查看所有可用命令:
sqlite> .help
2.5 退出命令行
输入系统 EOF 字符(通常是 Ctrl+D)或执行 .quit 命令:
sqlite> .quit
三、核心 SQL 操作
SQLite 支持标准的 SQL 语法,以下是最常用的操作。
3.1 创建表
-- 基础表结构
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL CHECK(price > 0),
stock INTEGER DEFAULT 0,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 添加索引(提升查询性能)
CREATE INDEX idx_products_name ON products(name);
CREATE INDEX idx_products_price ON products(price);
-- 复合索引
CREATE INDEX idx_products_name_price ON products(name, price);
字段类型说明:SQLite 是动态类型数据库,但推荐使用以下类型:
INTEGER:整数REAL:浮点数TEXT:文本字符串BLOB:二进制数据
3.2 插入数据
-- 单条插入
INSERT INTO products (name, price, stock) VALUES ('Laptop', 5999.00, 10);
-- 批量插入
INSERT INTO products (name, price, stock) VALUES
('Mouse', 29.90, 100),
('Keyboard', 89.90, 50),
('Monitor', 1299.00, 20);
-- 插入或替换(主键冲突时替换)
INSERT OR REPLACE INTO products (id, name, price) VALUES (1, 'Gaming Laptop', 7999.00);
3.3 查询数据
-- 基础查询
SELECT * FROM products WHERE price > 100;
-- 分页查询(推荐方式:基于 ID)
SELECT * FROM products WHERE id > last_id ORDER BY id LIMIT 10;
-- 传统分页(偏移量大时性能较差)
SELECT * FROM products LIMIT 10 OFFSET 1000;
-- 聚合查询
SELECT
COUNT(*) as total,
AVG(price) as avg_price,
MAX(price) as max_price,
SUM(stock) as total_stock
FROM products;
3.4 更新与删除
-- 更新数据
UPDATE products SET price = 6499.00 WHERE name = 'Laptop';
-- 删除数据
DELETE FROM products WHERE stock = 0;
-- 清空表(重置 AUTOINCREMENT)
DELETE FROM products;
-- 或
DROP TABLE products;
3.5 事务处理
事务可以保证一组操作要么全部成功,要么全部失败,常用于批量操作和数据一致性场景:
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 检查结果
SELECT * FROM accounts;
COMMIT; -- 提交事务
-- 如果出错,可以回滚
ROLLBACK;
四、编程语言集成:Go + SQLite 实战
理论讲完,我们来写点实际的代码。这里以 Go 语言为例,演示如何用 SQLite 构建一个完整的应用。
4.1 安装驱动
Go 中最常用的 SQLite 驱动是 mattn/go-sqlite3:
go get github.com/mattn/go-sqlite3
4.2 数据库连接与初始化
package main
import (
"database/sql"
"log"
_ "github.com/mattn/go-sqlite3" // 匿名导入,仅执行 init 函数
)
var db *sql.DB
func initDB() {
var err error
// 打开数据库文件(不存在则自动创建)
db, err = sql.Open("sqlite3", "./app.db")
if err != nil {
log.Fatal(err)
}
// 测试连接
if err = db.Ping(); err != nil {
log.Fatal(err)
}
// 创建表
createTableSQL := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT NOT NULL,
age INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Fatal(err)
}
log.Println("数据库初始化成功")
}
func main() {
initDB()
defer db.Close()
// 后续操作...
}
注意:defer db.Close() 确保程序退出时正确关闭数据库连接。
4.3 CRUD 操作
定义用户结构体:
type User struct {
ID int
Username string
Email string
Age int
CreatedAt string
}
插入数据:
func createUser(username, email string, age int) error {
query := `INSERT INTO users (username, email, age) VALUES (?, ?, ?)`
result, err := db.Exec(query, username, email, age)
if err != nil {
return err
}
id, _ := result.LastInsertId()
log.Printf("用户创建成功,ID: %d", id)
return nil
}
批量插入(使用预处理语句提升性能):
func batchCreateUsers(users []User) error {
// 开始事务
tx, err := db.Begin()
if err != nil {
return err
}
// 准备预处理语句
stmt, err := tx.Prepare("INSERT INTO users (username, email, age) VALUES (?, ?, ?)")
if err != nil {
return err
}
defer stmt.Close()
// 批量插入
for _, u := range users {
_, err = stmt.Exec(u.Username, u.Email, u.Age)
if err != nil {
tx.Rollback()
return err
}
}
// 提交事务
return tx.Commit()
}
查询单条数据:
func getUserByID(id int) (*User, error) {
query := `SELECT id, username, email, age, created_at FROM users WHERE id = ?`
row := db.QueryRow(query, id)
var user User
err := row.Scan(&user.ID, &user.Username, &user.Email, &user.Age, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil // 未找到
}
return nil, err
}
return &user, nil
}
查询多条数据:
func getAllUsers() ([]User, error) {
rows, err := db.Query("SELECT id, username, email, age, created_at FROM users ORDER BY id")
if err != nil {
return nil, err
}
defer rows.Close() // 重要:关闭结果集
var users []User
for rows.Next() {
var user User
err = rows.Scan(&user.ID, &user.Username, &user.Email, &user.Age, &user.CreatedAt)
if err != nil {
return nil, err
}
users = append(users, user)
}
// 检查遍历过程中的错误
if err = rows.Err(); err != nil {
return nil, err
}
return users, nil
}
更新数据:
func updateUserAge(id, age int) error {
query := `UPDATE users SET age = ? WHERE id = ?`
result, err := db.Exec(query, age, id)
if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return fmt.Errorf("用户 ID %d 不存在", id)
}
return nil
}
删除数据:
func deleteUser(id int) error {
query := `DELETE FROM users WHERE id = ?`
result, err := db.Exec(query, id)
if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return fmt.Errorf("用户 ID %d 不存在", id)
}
return nil
}
4.4 完整示例
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int
Name string
Age int
Email string
}
func main() {
// 1. 打开数据库
db, err := sql.Open("sqlite3", "./demo.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 2. 创建表
createSQL := `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
age INTEGER,
email TEXT UNIQUE
);`
db.Exec(createSQL)
// 3. 插入数据
insertSQL := `INSERT INTO users (name, age, email) VALUES (?, ?, ?)`
result, _ := db.Exec(insertSQL, "张三", 25, "zhangsan@example.com")
id, _ := result.LastInsertId()
fmt.Printf("插入成功,ID: %d\n", id)
// 4. 查询数据
rows, _ := db.Query("SELECT id, name, age, email FROM users")
defer rows.Close()
for rows.Next() {
var user User
rows.Scan(&user.ID, &user.Name, &user.Age, &user.Email)
fmt.Printf("%d: %s (%d岁) - %s\n", user.ID, user.Name, user.Age, user.Email)
}
}
五、高级特性
5.1 全文检索(FTS5)
SQLite 内置了全文检索扩展,可以高效地进行文本搜索:
-- 创建全文检索虚拟表
CREATE VIRTUAL TABLE docs USING fts5(title, content);
-- 插入数据
INSERT INTO docs VALUES ('SQLite 教程', 'SQLite 是一个轻量级的嵌入式数据库');
INSERT INTO docs VALUES ('Go 语言入门', 'Go 是 Google 开发的编程语言');
-- 全文搜索
SELECT * FROM docs WHERE docs MATCH '嵌入式';
-- 返回:SQLite 教程
5.2 JSON 支持(SQLite 3.38+)
从 SQLite 3.38 版本开始,原生支持 JSON 操作:
-- 创建带 JSON 字段的表
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
details JSON
);
-- 插入 JSON 数据
INSERT INTO orders (details) VALUES
('{"items": [{"name": "book", "qty": 2}], "total": 20}');
-- 查询 JSON 字段
SELECT json_extract(details, '$.total') FROM orders; -- 返回 20
SELECT json_extract(details, '$.items[0].name') FROM orders; -- 返回 'book'
5.3 内存数据库
在某些场景(如缓存、测试)下,你可能希望数据库完全运行在内存中:
// Go 语言中使用内存数据库
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatal(err)
}
// 所有操作都在内存中进行,程序退出后数据自动销毁
内存数据库的优势是极快的读写速度,适合存储会话级临时数据、测试用例等场景。
5.4 多数据库关联
SQLite 支持在同一个会话中 ATTACH 多个数据库文件:
-- 附加另一个数据库
ATTACH DATABASE 'archive.db' AS archive;
-- 跨数据库查询
SELECT * FROM main.users WHERE id IN (SELECT user_id FROM archive.old_users);
-- 数据迁移
INSERT INTO archive.old_users SELECT * FROM main.users WHERE created_at < '2024-01-01';
六、性能优化与最佳实践
6.1 PRAGMA 配置
PRAGMA 是 SQLite 的特殊命令,用于调整数据库的行为:
-- WAL 模式(Write-Ahead Logging):提升并发读写性能
PRAGMA journal_mode = WAL;
-- 同步模式(性能 vs 安全)
PRAGMA synchronous = NORMAL; -- OFF: 最快但可能丢数据,NORMAL: 平衡,FULL: 最安全
-- 缓存大小(单位:页,负数表示 KB)
PRAGMA cache_size = -2000; -- 2MB 缓存
-- 页大小(在新建数据库时设置)
PRAGMA page_size = 4096;
-- 查看当前设置
PRAGMA journal_mode;
PRAGMA synchronous;
WAL 模式说明:传统的回滚日志模式每次写入都要刷盘,WAL 模式将变更先写入单独的 WAL 文件,减少了磁盘 I/O,提升了并发性能。
6.2 事务批量写入
如果需要插入大量数据,使用事务可以大幅提升性能:
func batchInsert(users []User) error {
tx, _ := db.Begin()
stmt, _ := tx.Prepare("INSERT INTO users (name, age) VALUES (?, ?)")
defer stmt.Close()
for _, u := range users {
stmt.Exec(u.Name, u.Age)
}
return tx.Commit() // 一次性提交所有写入
}
原理:没有事务时,每条 INSERT 都会触发一次磁盘同步;使用事务后,所有写入在内存中完成,最后一次性写入磁盘。
6.3 索引优化
索引是提升查询性能的关键,但过多索引会拖慢写入速度:
-- 分析查询计划
EXPLAIN QUERY PLAN SELECT * FROM users WHERE age > 25;
-- 输出说明:SEARCH TABLE users USING INDEX idx_age(使用索引)或 SCAN TABLE users(全表扫描)
-- 创建索引
CREATE INDEX idx_users_age ON users(age);
CREATE INDEX idx_users_name_age ON users(name, age); -- 复合索引
-- 查看已创建的索引
.indices users
索引设计原则:
- 为 WHERE、ORDER BY、JOIN 中频繁使用的列创建索引
- 每个表的索引数量控制在 3-5 个
- 复合索引的顺序很重要:将最常作为等值查询的列放在前面
6.4 数据库维护
-- VACUUM:重建数据库文件,回收空间
VACUUM;
-- ANALYZE:更新统计信息,帮助查询优化器选择更好的执行计划
ANALYZE;
-- 检查完整性
PRAGMA integrity_check;
6.5 常见问题与解决方案
问题 1:数据库锁定(database is locked)
原因:一个连接正在写入时,另一个连接尝试写入(SQLite 在 WAL 模式外一次只允许一个写入者)。
解决方案:
- 启用 WAL 模式:
PRAGMA journal_mode = WAL; - 缩短事务长度
- 确保所有连接正确关闭
问题 2:大数据量查询慢
解决方案:
- 使用基于 ID 的分页代替 OFFSET
- 创建合适的索引
- 只 SELECT 需要的列,避免
SELECT *
问题 3:数据库文件过大
解决方案:
- 执行
VACUUM回收空间 - 考虑使用
AUTO_VACUUM模式 - 定期清理历史数据
七、实战项目:笔记管理应用
将以上知识综合起来,我们来构建一个完整的笔记管理 Web 应用,使用 Go + SQLite + HTML 模板。
7.1 项目结构
notes-app/
├── main.go # 主程序:路由和 HTTP 处理器
├── database.go # 数据库初始化和连接
├── note.go # Note 结构体和 CRUD 操作
├── index.html # 前端页面模板
└── notes.db # SQLite 数据库文件(运行时生成)
7.2 database.go
package main
import (
"database/sql"
"log"
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
func initDB() {
var err error
db, err = sql.Open("sqlite3", "./notes.db")
if err != nil {
log.Fatal(err)
}
createTableSQL := `
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
_, err = db.Exec(createTableSQL)
if err != nil {
log.Fatal(err)
}
log.Println("数据库初始化完成")
}
7.3 note.go
package main
import "log"
type Note struct {
ID int
Title string
Content string
}
func createNote(title, content string) error {
query := `INSERT INTO notes (title, content) VALUES (?, ?)`
_, err := db.Exec(query, title, content)
return err
}
func getAllNotes() ([]Note, error) {
rows, err := db.Query("SELECT id, title, content FROM notes ORDER BY id DESC")
if err != nil {
return nil, err
}
defer rows.Close()
var notes []Note
for rows.Next() {
var note Note
err = rows.Scan(¬e.ID, ¬e.Title, ¬e.Content)
if err != nil {
return nil, err
}
notes = append(notes, note)
}
return notes, nil
}
func deleteNote(id int) error {
query := `DELETE FROM notes WHERE id = ?`
result, err := db.Exec(query, id)
if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
log.Printf("笔记 ID %d 不存在", id)
}
return nil
}
7.4 main.go
package main
import (
"html/template"
"log"
"net/http"
"strconv"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
notes, err := getAllNotes()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles("index.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.Execute(w, notes)
}
func addHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
title := r.FormValue("title")
content := r.FormValue("content")
if title != "" {
createNote(title, content)
}
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func deleteHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.FormValue("id")
if id, err := strconv.Atoi(idStr); err == nil {
deleteNote(id)
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func main() {
initDB()
defer db.Close()
http.HandleFunc("/", indexHandler)
http.HandleFunc("/add", addHandler)
http.HandleFunc("/delete", deleteHandler)
log.Println("服务器启动于 http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
7.5 index.html
<!DOCTYPE html>
<html>
<head>
<title>我的笔记</title>
<style>
body { font-family: Arial; max-width: 800px; margin: 50px auto; padding: 20px; }
.note { border: 1px solid #ddd; margin: 10px 0; padding: 10px; border-radius: 5px; }
.note h3 { margin: 0 0 5px 0; }
.note p { margin: 0; color: #666; }
.delete-btn { color: red; cursor: pointer; float: right; }
form { margin: 20px 0; }
input, textarea { width: 100%; margin: 5px 0; padding: 8px; }
button { background: #007bff; color: white; border: none; padding: 10px 20px; cursor: pointer; }
</style>
</head>
<body>
<h1>笔记应用</h1>
<form method="POST" action="/add">
<input type="text" name="title" placeholder="标题" required>
<textarea name="content" rows="3" placeholder="内容"></textarea>
<button type="submit">添加笔记</button>
</form>
<h2>我的笔记</h2>
{{range .}}
<div class="note">
<form method="POST" action="/delete" style="display: inline; float: right;">
<input type="hidden" name="id" value="{{.ID}}">
<button type="submit" class="delete-btn" style="background: none; color: red; padding: 0;">删除</button>
</form>
<h3>{{.Title}}</h3>
<p>{{.Content}}</p>
</div>
{{else}}
<p>暂无笔记,添加第一条吧!</p>
{{end}}
</body>
</html>
7.6 运行应用
# 安装依赖
go get github.com/mattn/go-sqlite3
# 运行
go run *.go
# 访问 http://localhost:8080
这个完整的示例涵盖了:
- SQLite 数据库的初始化和连接
- CRUD 操作的完整实现
- 使用
?占位符防止 SQL 注入 defer rows.Close()确保资源释放- 与 Web 前端的集成
八、总结
SQLite 不是一个“玩具数据库”,而是一个经过二十多年打磨的工业级嵌入式数据库。它的设计哲学是:简单、可靠、快速。
通过本文,你应该已经掌握了:
- 命令行操作:
sqlite3的安装、点命令、SQL 执行 - 核心 SQL:建表、增删改查、事务、索引
- 编程集成:Go 语言中使用
go-sqlite3驱动进行 CRUD - 高级特性:全文检索、JSON 支持、内存数据库
- 性能优化:PRAGMA 配置、事务批量写入、索引设计
- 实战应用:完整的笔记管理 Web 应用
选型建议:如果你是以下场景,SQLite 是绝佳选择:
- 移动应用、桌面软件、物联网设备
- 中小型网站(日活 < 10 万)
- 数据分析和原型验证
- 任何需要嵌入式数据库的场景
进一步学习资源:
希望这篇指南能帮助你快速上手 SQLite,并在实际项目中灵活运用!