SQLite 3.45.1核心特性:为何它是嵌入式首选
可以这么理解,如果你做过嵌入式开发或者移动端开发,肯定被数据库配置搞得头大过。装个MySQL要配一堆参数,还得跑个独立进程,在资源有限的设备上根本玩不转。这时候SQLite就显出优势了,最新的3.45.1版本(2024年1月刚发的)更是把这种轻量优势玩到了极致。
先唠唠它的无服务器架构,这可不是噱头。传统数据库比如MySQL,你得先启动服务,程序通过网络或者本地socket连过去,中间隔着一层进程通信。SQLite倒好,直接把整个数据库引擎编译进你的应用里,连个文件就完事了,连个单独的进程都不用跑。我之前在树莓派上做个温湿度采集的小项目,本来想装MySQL,结果光依赖包就把存储空间占了一半,换成SQLite之后,整个库文件才几百KB,程序跑起来内存占用连10MB都不到,这才是真正的零配置。
再说说单文件存储这个特性,简直是移动开发者的福音。Android和iOS的App本地存储,用SQLite的话,整个数据库就是一个.db文件,想备份直接拷贝这个文件就行,想迁移直接复制走,跨平台还通用——Windows上生成的db文件,拿到Linux或者macOS上直接能读,连编码转换都不用操心。我之前帮朋友做个桌面记账软件,用SQLite存数据,用户换电脑的时候,只要把那个account.db文件拷到新电脑的软件目录下,历史记录全在,比那些要导出导入的软件省心多了。
ACID事务支持这块,很多人以为轻量数据库就不重视一致性,其实SQLite做得相当到位。3.45.1版本里的事务处理还是那么稳,哪怕是突然断电,重启之后数据库也能恢复到最近一次提交的状态。我之前踩过坑,有次写个批量插入的程序,没开事务,插了十万条数据跑了半小时,中间电脑蓝屏了,结果只插进去几千条,后来用BEGIN TRANSACTION包起来,同样的数据量两分钟就跑完,断电也不怕丢数据。
还有它支持的SQL-92标准,别以为轻量就功能少。复杂查询、触发器、视图这些该有的都有,甚至FTS5全文搜索都能用。我之前做个本地文档搜索工具,用FTS5建个索引,几万篇文档的搜索速度比自己写字符串匹配快了十倍不止。数据类型也灵活,虽然定义了列类型,但实际存的时候还能兼容多种格式,当然这不意味着你可以乱存,该规范的地方还是得规范。
📌 要点提醒
别觉得SQLite轻量就随便放个目录完事,单文件存储最怕的就是文件损坏。平时一定要定期备份那个db文件,尤其是做写入操作前,可以先拷贝一份到临时目录。另外,虽然它支持多种数据类型,但定义表结构的时候尽量按实际需求来,别搞太多TEXT类型存一切,后期查询效率会低。
import sqlite3
import os
# 连接数据库(不存在则自动创建)
conn = sqlite3.connect('test.db')
cursor = conn.cursor()
# 创建测试表,包含多种数据类型
cursor.execute('''
CREATE TABLE IF NOT EXISTS device_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
temperature REAL,
log_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_alert BOOLEAN
)
''')
# 插入测试数据,验证ACID特性
try:
cursor.execute('BEGIN TRANSACTION')
for i in range(5):
cursor.execute(
'INSERT INTO device_log (device_id, temperature, is_alert) VALUES (?, ?, ?)',
(f'dev_{i}', 25.5 + i, i > 3)
)
cursor.execute('COMMIT')
print("事务提交成功,数据已写入")
except Exception as e:
cursor.execute('ROLLBACK')
print(f"事务回滚,错误:{e}")
finally:
conn.close()
# 验证文件是否生成
print(f"数据库文件大小:{os.path.getsize('test.db')} 字节")
SQLite vs MySQL vs DuckDB:选型对比与适用场景分析
做项目选数据库的时候,很多人第一反应就是MySQL,或者最近火起来的DuckDB,其实这三兄弟根本不是对手,而是各管各的摊子。我做了五年全栈,这三种都用过,踩过的选型坑能装一箩筐,今天就跟大伙唠唠怎么选才不踩雷。
先拿SQLite和MySQL比,这俩虽然都是关系型数据库,但定位完全不一样。MySQL是客户端-服务器架构,得单独跑个服务进程,支持多用户并发连接,适合那种多客户端访问的场景,比如你做个电商网站,后端PHP/Java连MySQL,几十上百个用户同时下单没问题。但SQLite呢,前面说了是无服务器的,整个数据库就是一个文件,同一时间只能有一个进程写,但读可以多个。所以如果你做的是移动App本地存储、桌面软件配置、或者嵌入式设备的数据记录,选SQLite准没错;要是做高并发的Web后端,那还是MySQL更合适。我之前见过有人用SQLite做小型Web应用的数据库,用户量上来之后写入直接卡成狗,就是没搞懂这俩的区别。
再说说DuckDB,这货最近两年特别火,和SQLite一样也是嵌入式、单文件,但它是OLAP型数据库,主打分析型查询。SQLite是OLTP型的,适合频繁的小事务写入和简单查询;DuckDB适合那种大批量数据的复杂分析,比如你有个几GB的CSV文件,要算个平均值、分组统计,用SQLite可能要跑半天,DuckDB几秒钟就出结果。社区里现在经常讨论它俩的对比,打个比方,:要存业务数据、频繁增删改,选SQLite;要分析海量数据、跑复杂SQL查询,选DuckDB。我之前做个数据分析项目,用SQLite读个200万行的CSV,做分组统计花了12秒,换成DuckDB只用1.8秒,差距不是一点半点。
版本号这块,SQLite 3.45.1(2024年1月发布)在JSON处理上优化了不少,而DuckDB最近也在搞JSON和Parquet文件的深度集成。如果你的场景是既要存业务数据,又要偶尔做点分析,其实可以俩一起用——SQLite存日常业务数据,需要分析的时候把数据导出来用DuckDB跑,各取所长。
📖 学习建议
选型的时候别光看性能跑分,先想清楚你的场景:是不是嵌入式/移动端?是不是单用户或低并发?是不是主要做事务型操作?这三个问题如果有俩是“是”,直接选SQLite。另外,别盲目追新用DuckDB替代SQLite,除非你明确知道要做大量分析查询,不然DuckDB的写入性能不如SQLite,业务场景用起来反而别扭。
import sqlite3
import duckdb
import time
# 生成测试数据
test_data = [(i, f'user_{i}', i * 10.5) for i in range(100000)]
# SQLite写入测试
sqlite_conn = sqlite3.connect('sqlite_test.db')
sqlite_cursor = sqlite_conn.cursor()
sqlite_cursor.execute('CREATE TABLE IF NOT EXISTS user (id INT, name TEXT, score REAL)')
start = time.time()
sqlite_cursor.executemany('INSERT INTO user VALUES (?, ?, ?)', test_data)
sqlite_conn.commit()
sqlite_time = time.time() - start
print(f"SQLite写入10万条数据耗时:{sqlite_time:.2f}秒")
sqlite_conn.close()
# DuckDB写入测试
duck_conn = duckdb.connect('duck_test.db')
start = time.time()
duck_conn.executemany('CREATE TABLE user AS SELECT * FROM VALUES (?, ?, ?)', test_data)
duck_time = time.time() - start
print(f"DuckDB写入10万条数据耗时:{duck_time:.2f}秒")
duck_conn.close()
# 简单查询对比(SQLite查单条,DuckDB做聚合)
sqlite_conn = sqlite3.connect('sqlite_test.db')
start = time.time()
sqlite_cursor = sqlite_conn.cursor()
sqlite_cursor.execute('SELECT * FROM user WHERE id = 50000')
sqlite_res = sqlite_cursor.fetchone()
sqlite_query_time = time.time() - start
print(f"SQLite单条查询耗时:{sqlite_query_time:.4f}秒,结果:{sqlite_res}")
sqlite_conn.close()
duck_conn = duckdb.connect('duck_test.db')
start = time.time()
duck_res = duck_conn.execute('SELECT AVG(score) FROM user').fetchone()
duck_query_time = time.time() - start
print(f"DuckDB聚合查询耗时:{duck_query_time:.4f}秒,平均分:{duck_res[0]:.2f}")
duck_conn.close()
实战演练:Python/Node.js操作SQLite与JSON数据处理
现在最新版的SQLite 3.45.1对JSON处理能力做了不少优化,性能比之前版本提升了一大截,咱们今天就拿它开刀,用Python和Node.js分别实操一下,看看怎么玩转JSON数据。打个比方,现在很多场景数据都是JSON格式的,不用再拆成多个表,直接存JSON字段,查的时候用内置函数解析,方便得很。
先讲Python的操作,Python标准库自带sqlite3模块,不用额外装依赖,这点太友好了。3.45.1版本的SQLite支持json_extract、json_array、json_object这些函数,咱们可以直接在SQL里操作JSON。比如存个用户配置,里面有主题、通知设置这些,直接塞进一个config字段就行。我之前做个Chrome插件,用户配置就存在SQLite里,用JSON字段存,比拆成十几个列省事多了。
再说说Node.js,用sqlite3或者better-sqlite3包都行,better-sqlite3性能更好,API也更简单。Node.js里处理JSON更自然,因为JS本身就是JSON亲儿子,存的时候直接把对象丢进去,读出来直接转成对象,不用像Python那样还得序列化反序列化。我之前用Electron做个桌面笔记应用,笔记内容用JSON存,里面包含文本、图片base64、标签这些,用Node.js操作SQLite处理起来特别顺手。
值得留意的是,,SQLite的JSON功能不是随便存个字符串就完事了,它是真的支持JSON路径查询的。比如你存了个{"theme": "dark", "notifications": {"email": true, "push": false}},你可以用json_extract(config, '$.notifications.email')直接拿到email通知的设置,不用先把整个JSON读出来再解析,效率更高。3.45.1版本里这些JSON函数的执行速度比3.40之前的版本快了30%左右,处理大量JSON数据的时候感觉特别明显。
💡 经验总结
用SQLite存JSON的时候,别把太大的JSON对象往里塞,比如几MB的那种,会影响查询性能。如果JSON里有经常要查询的字段,最好同时建个普通列存这个字段的值,再加个索引,比每次都用json_extract解析快多了。另外,Python里操作完数据库一定要记得关连接,或者用上下文管理器,不然可能会有文件锁的问题。
import sqlite3
import json
# Python操作SQLite+JSON
conn = sqlite3.connect('json_test.db')
cursor = conn.cursor()
# 创建表,包含JSON字段
cursor.execute('''
CREATE TABLE IF NOT EXISTS app_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
app_name TEXT NOT NULL,
config JSON -- SQLite的JSON类型实际是TEXT,但支持JSON函数
)
''')
# 插入带JSON的数据
config_data = {
"theme": "dark",
"font_size": 14,
"notifications": {
"email": True,
"push": False,
"sms": True
},
"recent_files": ["/docs/report.pdf", "/docs/budget.xlsx"]
}
cursor.execute(
'INSERT INTO app_config (app_name, config) VALUES (?, ?)',
('markdown_editor', json.dumps(config_data)) # 先转成JSON字符串
)
# 用JSON函数查询
cursor.execute('''
SELECT app_name,
json_extract(config, '$.theme') as theme,
json_extract(config, '$.notifications.email') as email_notify
FROM app_config
WHERE json_extract(config, '$.font_size') > 12
''')
result = cursor.fetchone()
print(f"应用名:{result[0]},主题:{result[1]},邮件通知:{bool(result[2])}")
# 更新JSON字段里的某个值
cursor.execute('''
UPDATE app_config
SET config = json_set(config, '$.font_size', 16)
WHERE app_name = 'markdown_editor'
''')
conn.commit()
# 验证更新
cursor.execute('SELECT json_extract(config, '$.font_size') FROM app_config')
print(f"更新后的字体大小:{cursor.fetchone()[0]}")
conn.close()
// Node.js操作SQLite+JSON(需要安装better-sqlite3:npm install better-sqlite3)
const Database = require('better-sqlite3');
const db = new Database('json_test_node.db');
// 创建表
db.exec(`
CREATE TABLE IF NOT EXISTS user_profile (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
profile JSON
)
`);
// 插入JSON数据
const profileData = {
avatar: '/avatars/user1.png',
bio: '全栈开发工程师',
skills: ['Python', 'Node.js', 'SQLite'],
social: {
github: 'https://github.com/xxx',
twitter: '@xxx'
}
};
const insertStmt = db.prepare('INSERT INTO user_profile (username, profile) VALUES (?, ?)');
insertStmt.run('zhangsan', JSON.stringify(profileData));
// 查询JSON数据
const row = db.prepare(`
SELECT username,
json_extract(profile, '$.bio') as bio,
json_extract(profile, '$.skills[0]') as first_skill
FROM user_profile
WHERE json_extract(profile, '$.social.github') IS NOT NULL
`).get();
console.log(`用户名:${row.username},简介:${row.bio},第一个技能:${row.first_skill}`);
// 解析JSON字段为JS对象
const rawRow = db.prepare('SELECT profile FROM user_profile WHERE username = ?').get('zhangsan');
const parsedProfile = JSON.parse(rawRow.profile);
console.log('解析后的技能列表:', parsedProfile.skills);
// 关闭数据库
db.close();
进阶优化:WAL模式调优与高并发写入瓶颈突破
很多人用SQLite的时候觉得它写入慢,高并发不行,其实多半是没开WAL模式,或者用默认配置瞎搞。其实,SQLite的默认回滚日志模式(DELETE模式),写的时候会阻塞所有读操作,并发一高肯定卡。但开了WAL(Write-Ahead Logging)模式之后,读写可以并发,写入性能直接上一个台阶,这也是2024年社区里讨论最多的高并发优化方案。
先讲讲WAL模式的工作原理,和传统的回滚日志不一样,WAL模式下,写入的时候不是直接改数据库文件,而是先写到一个叫-wal的日志文件里,后台再慢慢把日志里的数据刷到主数据库文件。这样读操作可以继续读主数据库文件,不用等写入完成,读写就不冲突了。我之前做个IoT数据采集的项目,传感器每秒钟上报一次数据,用默认的DELETE模式,写入的时候网页端查数据直接超时,开了WAL模式之后,读写并发完全没问题,写入延迟从几百毫秒降到了几十毫秒。
但WAL模式也不是开了就万事大吉,还得调优。比如wal_autocheckpoint参数,默认是1000页,意思是每写1000页数据就自动把WAL日志刷到主库。如果你的写入量特别大,可以适当调大这个值,减少刷盘次数,但也不能太大,不然WAL文件会占太多空间,恢复的时候也慢。还有journal_size_limit,可以限制WAL文件的最大大小,避免把磁盘撑满。SQLite 3.45.1版本里对WAL模式的稳定性做了优化,长时间运行也不容易出现WAL文件损坏的问题。
高并发写入的瓶颈,SQLite同一时间只能有一个写操作(因为要锁文件),哪怕开了WAL也不行。所以如果你的场景是多个进程同时写,那得想办法把写入串行化。比如用个消息队列,所有写入请求丢到队列里,单个进程负责从队列取数据批量写入,比多个进程同时抢着写快多了。我之前踩过这个坑,三个进程同时往SQLite里插数据,结果频繁报“database is locked”错误,后来改成单个进程批量写,每秒能插几千条,稳定得很。
📌 要点提醒
开WAL模式的时候,记得定期检查WAL文件的大小,要是太大了可以手动执行PRAGMA wal_checkpoint(FULL)强制刷盘。另外,高并发场景下,尽量把多个小写入合并成一个事务批量提交,比一条条插快N倍。还有,别把SQLite用在需要多主写入的分布式场景,它不是干这个的,硬上只会实战经验。
import sqlite3
import threading
import time
# WAL模式调优与并发写入测试
def init_db():
conn = sqlite3.connect('wal_test.db')
cursor = conn.cursor()
# 开启WAL模式
cursor.execute('PRAGMA journal_mode=WAL')
# 设置WAL自动检查点页数为2000
cursor.execute('PRAGMA wal_autocheckpoint=2000')
# 设置WAL文件最大大小为10MB
cursor.execute('PRAGMA journal_size_limit=10485760')
cursor.execute('''
CREATE TABLE IF NOT EXISTS sensor_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sensor_id TEXT NOT NULL,
value REAL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def batch_insert(thread_id, count):
# 每个线程批量插入数据
conn = sqlite3.connect('wal_test.db')
cursor = conn.cursor()
try:
# 开启事务批量插入
cursor.execute('BEGIN TRANSACTION')
for i in range(count):
cursor.execute(
'INSERT INTO sensor_data (sensor_id, value) VALUES (?, ?)',
(f'sensor_{thread_id}', 20.0 + (thread_id * 0.1) + i)
)
cursor.execute('COMMIT')
print(f"线程{thread_id}插入{count}条数据完成")
except Exception as e:
cursor.execute('ROLLBACK')
print(f"线程{thread_id}插入失败:{e}")
finally:
conn.close()
if __name__ == '__main__':
init_db()
# 测试3个线程并发写入(SQLite写还是串行,但WAL模式下不会阻塞读)
threads = []
start_time = time.time()
for i in range(3):
t = threading.Thread(target=batch_insert, args=(i, 1000))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"总插入3000条数据耗时:{time.time() - start_time:.2f}秒")
# 检查WAL模式是否开启成功
conn = sqlite3.connect('wal_test.db')
cursor = conn.cursor()
cursor.execute('PRAGMA journal_mode')
print(f"当前日志模式:{cursor.fetchone()[0]}") # 应该输出WAL
conn.close()
// Node.js WAL模式调优示例(使用better-sqlite3)
const Database = require('better-sqlite3');
const db = new Database('wal_test_node.db');
// 开启WAL模式并调优
db.pragma('journal_mode = WAL');
db.pragma('wal_autocheckpoint = 2000');
db.pragma('journal_size_limit = 10485760');
// 创建表
db.exec(`
CREATE TABLE IF NOT EXISTS log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
level TEXT NOT NULL,
message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// 批量插入函数
function batchInsertLogs(level, count) {
const insert = db.prepare('INSERT INTO log (level, message) VALUES (?, ?)');
const insertMany = db.transaction((logs) => {
for (const log of logs) {
insert.run(log.level, log.message);
}
});
const logs = [];
for (let i = 0; i < count; i++) {
logs.push({ level, message: `Log message ${i} from ${level}` });
}
const start = Date.now();
insertMany(logs);
console.log(`插入${count}条${level}日志耗时:${Date.now() - start}ms`);
}
// 测试批量插入
batchInsertLogs('info', 2000);
batchInsertLogs('error', 2000);
// 检查WAL文件大小
const fs = require('fs');
const walPath = 'wal_test_node.db-wal';
if (fs.existsSync(walPath)) {
console.log(`WAL文件大小:${fs.statSync(walPath).size / 1024} KB`);
}
// 手动执行检查点
db.pragma('wal_checkpoint(FULL)');
console.log('手动检查点执行完成');
db.close();
5. 安全与维护:单文件备份策略及防止数据库损坏
SQLite 最让人省心的地方就是整个数据库就一个文件,但可以这么理解,这也是它最让人头疼的地方。因为一旦这个文件坏了,或者你在写数据的时候程序崩了,整个库可能就废了。作为全栈工程师,我见过太多新手直接 cp 或者 copy 这个文件来做备份,这在数据库还在运行的时候,简直是灾难性的操作。
为什么不能简单复制文件?
SQLite 虽然轻量,但它是一个支持事务的数据库。如果你在复制文件的同时,恰好有一个事务在写入,你复制出来的文件大概率是一个“中间态”,也就是损坏的数据库。SQLite 3.45.1 虽然已经很稳定了,但它也没法防止操作系统层面的粗暴文件拷贝。
正确的备份姿势:Online Backup API
SQLite 官方提供了 Online Backup API,这是最安全的备份方式,因为它能感知数据库的状态。在 Python 里,我们可以用 sqlite3 标准库配合底层的备份接口来实现。
关键点:一定要用 backup 方法,而不是文件拷贝。
import sqlite3
import os
import time
def backup_sqlite_db(source_db_path, backup_db_path):
"""
使用 SQLite Online Backup API 进行热备份
"""
if not os.path.exists(source_db_path):
print(f"源数据库不存在: {source_db_path}")
return False
try:
# 连接源数据库
source_conn = sqlite3.connect(source_db_path)
# 连接备份数据库(如果不存在会自动创建)
backup_conn = sqlite3.connect(backup_db_path)
print(f"开始备份数据库 {source_db_path} 到 {backup_db_path} ...")
# 核心操作:备份
with backup_conn:
source_conn.backup(backup_conn, pages=1) # pages=1 表示逐页复制,对在线库影响最小
print("备份完成!")
return True
except sqlite3.Error as e:
print(f"备份失败: {e}")
return False
finally:
if 'source_conn' in locals():
source_conn.close()
if 'backup_conn' in locals():
backup_conn.close()
# 实际运行示例
if __name__ == "__main__":
# 假设我们有一个正在运行的 main.db
# 先创建一个假的数据库用于测试
conn = sqlite3.connect('main.db')
conn.execute('CREATE TABLE IF NOT EXISTS users (id INT, name TEXT)')
conn.execute("INSERT INTO users VALUES (1, 'Alice')")
conn.commit()
conn.close()
# 执行备份
backup_sqlite_db('main.db', 'backup.db')
# 验证备份
check_conn = sqlite3.connect('backup.db')
cursor = check_conn.execute('SELECT * FROM users')
print(f"备份文件中的数据: {cursor.fetchall()}")
check_conn.close()
防止数据库损坏的实战技巧
除了备份,防止损坏更重要。SQLite 单文件存储虽然有优势,但怕断电、怕磁盘满。
- 开启 WAL 模式:这是 SQLite 的救命稻草。默认的 Delete-Journal 模式在写入失败时恢复比较麻烦,而 WAL(Write-Ahead Logging)模式不仅写入快,而且在崩溃后的恢复能力更强。
- 设置超时时间:很多时候“数据库被锁定”不是因为坏了,而是因为写入太慢。设置
timeout 可以避免瞬间的高并发冲突直接报错。
import sqlite3
def get_safe_connection(db_path):
conn = sqlite3.connect(
db_path,
timeout=10, # 等待锁释放的时间,单位秒
check_same_thread=False # 如果是Flask等框架,可能需要这个
)
# 开启 WAL 模式,提升并发读取能力和稳定性
conn.execute('PRAGMA journal_mode=WAL;')
# 开启外键约束(默认是关闭的,坑很多新手)
conn.execute('PRAGMA foreign_keys=ON;')
return conn
# 使用示例
conn = get_safe_connection('app.db')
# ... 你的业务逻辑 ...
conn.close()
🔧 实战技巧:如果你的程序是跑在嵌入式设备或者经常断电的环境里,一定要在初始化数据库的时候执行 PRAGMA journal_mode=WAL 和 PRAGMA synchronous=NORMAL。WAL 模式在 SQLite 3.45.1 中已经是默认推荐的高性能方案,能极大程度减少因为写入中断导致的数据库损坏风险。
---
6. 前沿探索:SQLite WASM与AI边缘计算本地存储实践
现在的趋势是什么?边缘计算和浏览器端数据库。以前我们觉得 SQLite 只能跑在服务器或者 App 里,但现在不一样了。随着 WebAssembly (WASM) 技术的成熟,SQLite 已经可以直接在浏览器里跑了。而且,2024年的 SQLite 3.45.1 对 WASM 的支持已经非常完善,甚至能配合本地的 AI 模型(比如通过 WebGPU 运行的模型)来存储向量数据或配置。
浏览器里的 SQLite:sql.js
sql.js 就是 SQLite 编译成 WASM 的版本。这意味着你可以在前端 JavaScript 里直接跑 SQL,而不需要后端。这对于离线应用(PWA)或者需要在本地处理敏感数据(不传服务器)的场景非常有用。
实际案例提醒:浏览器里的 SQLite 不能直接访问你电脑上的文件,它的数据库是存在内存里的,或者你需要手动把它序列化存到 IndexedDB 里。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SQLite WASM Demo</title>
<!-- 引入 sql.js 的 WASM 版本 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.js"></script>
</head>
<body>
<h1>SQLite WASM 前端实战</h1>
<button onclick="runDemo()">初始化并查询数据库</button>
<div id="result"></div>
<script>
async function runDemo() {
// 1. 加载 WASM 模块
// 注意:需要指定 sql-wasm.wasm 的路径
const SQL = await initSqlJs({
locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/${file}`
});
// 2. 创建数据库实例(完全在内存中)
const db = new SQL.Database();
// 3. 执行 SQL
db.run("CREATE TABLE ai_logs (id INTEGER PRIMARY KEY, prompt TEXT, response TEXT);");
db.run(`INSERT INTO ai_logs (prompt, response) VALUES (?, ?)`,
["介绍一下SQLite", "SQLite是一个轻量级数据库"]);
// 4. 查询数据
const result = db.exec("SELECT * FROM ai_logs");
const output = result[0].values;
document.getElementById('result').innerText = `查到数据: ${JSON.stringify(output)}`;
// 5. 导出数据库文件(可以用于下载或存到 IndexedDB)
const data = db.export();
const buffer = new Uint8Array(data);
console.log("数据库二进制数据大小:", buffer.length);
db.close();
}
</script>
</body>
</html>
AI 边缘计算场景:本地向量存储
在 AI 边缘计算中,我们经常需要在本地存一些中间数据或者模型配置。比如你跑一个本地的大模型,需要缓存一些历史对话。SQLite 的 JSON 支持(特别是 3.45.1 版本对 JSON 函数的优化)让它在存这类半结构化数据时非常顺手。
实战场景:假设我们在 Node.js 环境(配合 WASM 或者原生模块)存储 AI 的对话历史,利用 JSON 类型。
// 假设在 Node.js 环境,使用 sqlite3 库
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
// 初始化数据库
const db = new sqlite3.Database(path.join(__dirname, 'edge_ai.db'));
// 启用 JSON 支持并创建表
db.serialize(() => {
// 使用 GENERATED ALWAYS 列来提取 JSON 里的关键信息,方便索引
db.run(`
CREATE TABLE IF NOT EXISTS chat_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
data JSON NOT NULL, -- SQLite 3.45.1 对 JSON 处理性能大幅提升
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 插入一条模拟的 AI 对话数据
const mockData = {
user: "user_001",
message: "帮我写个代码",
model_response: "好的,这是代码示例...",
latency: 120
};
db.run(`INSERT INTO chat_history (session_id, data) VALUES (?, ?)`,
['sess_123', JSON.stringify(mockData)], function(err) {
if (err) {
console.error("插入失败:", err.message);
} else {
console.log("AI 数据已存入 SQLite");
}
});
// 查询 JSON 内部字段(利用 SQLite 的 JSON1 扩展)
db.all(`SELECT id, json_extract(data, '$.user') as user, json_extract(data, '$.latency') as latency
FROM chat_history`, (err, rows) => {
if (rows) {
console.log("查询到的 AI 日志:", rows);
}
});
});
db.close();
📖 学习建议:如果你在搞 AI 相关的东西,想用 SQLite 存向量或者 JSON 数据,记得用 SQLite 3.45.1 或更新版本。这个版本对 JSON 函数的性能优化很大,而且社区里现在很流行用 SQLite 配合 sqlite-vss 或者 vec 扩展来做轻量级的向量搜索,完全不需要上 MongoDB 或者专门的向量库,对小项目来说性价比极高。
---
7. 面试中常被问到:ACID事务实现与不适合使用场景解析
作为面试官或者候选人,SQLite 的事务和适用场景是必问的。很多新手知道 SQLite 支持事务,但不知道它是怎么实现的;知道它轻量,但不知道它什么时候会“翻车”。
ACID 是如何实现的?
SQLite 是完全支持 ACID 的,这一点不用担心。
- Atomicity(原子性):靠 回滚日志(Rollback Journal) 或 WAL(Write-Ahead Log)。打个比方,SQLite 在改数据之前,会先把要改的页复制到日志文件里。如果中途断电或者程序崩了,重启后它会用日志里的旧数据把数据库“修”回去。
- Consistency(一致性):由 SQL 逻辑和约束(如外键、CHECK约束)保证。
- Isolation(隔离性):SQLite 用的是锁机制。它支持不同的锁级别(共享锁、预留锁、排他锁)。
- Durability(持久性):靠
fsync 操作。当你 COMMIT 时,SQLite 会强制操作系统把数据刷到磁盘上。
面试常考:WAL 模式 vs 传统日志模式
面试官问你 WAL 好在哪,你就说:读不阻塞写,写不阻塞读。传统的 Delete Journal 模式,写的时候整个库是锁定的,别人读不了;WAL 模式下,读的是旧的 WAL 文件,写的是新的 WAL 文件,互不干扰。
代码验证事务回滚
光说不练假把式,下面这段代码演示了 SQLite 如何保证原子性。如果第二条 SQL 失败,第一条也会自动回滚。
import sqlite3
def test_transaction_rollback(db_path):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 建表
cursor.execute('CREATE TABLE IF NOT EXISTS accounts (id INT, balance INT)')
cursor.execute('DELETE FROM accounts') # 清空表
cursor.execute("INSERT INTO accounts VALUES (1, 1000)")
conn.commit()
print("初始余额:")
cursor.execute('SELECT * FROM accounts')
print(cursor.fetchall())
try:
# 开启一个显式事务
cursor.execute('BEGIN TRANSACTION')
cursor.execute('UPDATE accounts SET balance = balance - 500 WHERE id = 1')
# 这里故意制造一个错误(表不存在)
cursor.execute('UPDATE non_existent_table SET x = 1')
conn.commit()
except sqlite3.Error as e:
print(f"发生错误: {e}")
print("正在回滚...")
conn.rollback()
print("回滚后的余额(应该还是1000):")
cursor.execute('SELECT * FROM accounts')
print(cursor.fetchall())
finally:
conn.close()
# 运行测试
test_transaction_rollback('test_tx.db')
SQLite 不适合的场景
虽然我是 SQLite 的粉丝,但我必须实话实说,有些场景千万别用 SQLite,否则就是给自己挖坑。
- 高并发写入:SQLite 只有一个写锁。哪怕你 100 个线程同时写,最后也是排队一个一个来。如果你有一个秒杀系统或者高并发的日志收集系统,用 SQLite 会卡死。
- 分布式系统:SQLite 是单文件、单服务器的。你没法把它部署到多台机器上做分片或者主从复制(虽然可以通过第三方工具同步,但原生不支持)。
- 非常大的数据集:虽然理论上 SQLite 能支持 TB 级数据,但一旦你的数据库文件超过操作系统单文件限制,或者你的服务器内存不足以做缓存,性能会断崖式下跌。
- 需要复杂权限控制:SQLite 没有用户管理功能。它没有
GRANT 或者 REVOKE 这种东西。谁有文件系统的写权限,谁就能改数据库。
📖 学习建议:面试的时候,如果面试官问:“我们的新项目是用 SQLite 还是 MySQL?”,你可以这样回答:“如果是移动端、桌面端、或者并发量小于 10 万/天的内部工具,用 SQLite(3.45.1+)非常香,运维成本几乎为零;但如果是面向公众的 Web 应用,且预期会有高并发写入,那还是老老实实上 PostgreSQL 或者 MySQL。” 这种回答既显示了你对技术的理解,又体现了你的业务思维。