从零开发到工程查询:SQLite 嵌入式数据库与高级 SQL 实战

从零开发到工程查询:SQLite 嵌入式数据库与高级 SQL 实战

摘要:SQLite 是世界上最流行的嵌入式数据库,Chrome、微信、Android 都在用它。本文从 SQLite 的定位出发,拆解 Python 和 Node.js 中的使用差异,深入 SQL 查询的逻辑执行顺序与分组聚合技巧,帮你打通从"存数据"到"查数据"的完整链路。

📑 目录

  • SQLite 是什么?------ 一个文件就是一个数据库
  • SQLite 的核心定位与适用场景
  • Python 中使用 SQLite(sqlite3)
  • Node.js 中使用 SQLite(better-sqlite3)
  • 两种语言使用方式对比
  • SQL 查询的逻辑执行顺序
  • 聚合函数与 GROUP BY 的"两把锁"规则
  • WHERE vs HAVING:过滤行还是过滤组?
  • 工程级复杂查询的"积木式拆解"思维
  • 互动讨论

SQLite 是什么?------ 一个文件就是一个数据库

平时你写 Node.js 或 Python 脚本时,习惯用 fs.readFileSync('./data.json') 读配置文件,再用 JSON.parse() 翻数据。这种方式的问题是:数据量大了之后,每次都要读整个文件、解析整个 JSON,效率很低。

SQLite 本质上就是把查询语言换成了 SQL,解析器编译进应用里,直接读写一个 .db 文件------没有独立进程,不占端口,不需要安装任何数据库服务。

一句话定位 :SQLite 是一个嵌入式关系型数据库。它不是一个独立的服务器进程,而是一个 C 语言库,应用程序通过函数调用直接操作数据库文件。

生活中的类比

想象你有一个 Excel 文件:

  • 用 JSON 读数据就像打开整个 Excel,把所有 Sheet 全部加载到内存再查找
  • 用 SQLite 就像给 Excel 配了一个索引目录,问"姓张的有哪些人",目录里直接告诉你答案

SQLite 的核心定位与适用场景

SQLite 的特点

特点 说明
嵌入式 C 语言库,应用通过函数调用直接操作文件,无独立进程
零配置 无端口、无用户、无配置文件,sqlite3.connect('test.db') 即建库
单写者 同时只能一个写者,WAL 模式下读写可并发但写唯一
功能完整 支持 ACID、索引、窗口函数、CTE(公共表表达式)

常见误区纠正

误区一:"轻量"=功能弱

"轻量"指部署轻(一个文件),不是功能弱。Chrome 用它存百万级书签,Android 用它做系统数据库,都不是"简单场景"。

误区二:适合"数据库逻辑简单"的场景

SQLite 适合"单机 + 低写入并发"的场景,SQL 逻辑可以很复杂------窗口函数、递归 CTE 都支持。

适用场景

场景 说明
本地开发 替代 Docker 起的 PostgreSQL,启动快,零配置
单元测试 :memory: 模式,用完自动销毁
生产小工具 单文件备份,无运维负担
客户端应用 浏览器、手机 App 的本地数据存储
不适合 高并发写场景(如电商订单),写入锁会拖慢响应

Python 中使用 SQLite(sqlite3

Python 标准库自带 sqlite3,无需额外安装。

核心流程

text

sql 复制代码
连接 → 游标 → 执行 SQL → 提交(DML)→ 关闭

完整示例代码

python

python 复制代码
import sqlite3

# 1. 连接(自动建库文件)
conn = sqlite3.connect('test.db')
cursor = conn.cursor()

# 2. 建表(DDL 自动提交,但加 commit 更安全)
cursor.execute('''
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        age INTEGER
    )
''')
conn.commit()

# 3. 插入(DML 必须 commit)
cursor.execute("INSERT INTO users (name, age) VALUES (?, ?)", ("张三", 25))
conn.commit()
print(f"新增行ID: {cursor.lastrowid}")

# 4. 查询
cursor.execute("SELECT * FROM users WHERE age > ?", (20,))
rows = cursor.fetchall()  # 返回元组列表 [(1, '张三', 25)]
for row in rows:
    print(f"ID: {row[0]}, Name: {row[1]}, Age: {row[2]}")

# 5. 更新
cursor.execute("UPDATE users SET age = ? WHERE name = ?", (26, "张三"))
conn.commit()
print(f"影响行数: {cursor.rowcount}")

# 6. 删除
cursor.execute("DELETE FROM users WHERE name = ?", ("张三",))
conn.commit()
print(f"删除行数: {cursor.rowcount}")

# 7. 关闭
cursor.close()
conn.close()

关键 API 速查

API 作用
sqlite3.connect('test.db') 连接数据库,文件不存在则自动创建
conn.cursor() 创建游标对象
cursor.execute(sql, params) 执行单条 SQL,params 为元组
cursor.executemany(sql, list_of_tuples) 批量插入同模板的多组数据
conn.executescript(sql_string) 执行多条不同 SQL(用分号分隔)
conn.commit() 提交事务(DML 必须调用)
cursor.fetchall() 获取所有查询结果
cursor.lastrowid 获取最后插入行的 ID
cursor.rowcount 获取受影响的行数

重要提醒

DML(增删改)必须 conn.commit() 才落盘。 DDL(建表等)自动提交,但加 commit() 更安全。连接关闭时未提交的 DML 会自动回滚。

Node.js 中使用 SQLite(better-sqlite3

Node.js 生态中最流行的 SQLite 库是 better-sqlite3------同步 API、性能好、手感自然。

安装

bash

复制代码
npm install better-sqlite3

完整示例代码

typescript

javascript 复制代码
import Database from 'better-sqlite3';

const db = new Database('test.db');

// 重要:外键默认关闭,每次连接手动打开
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');

// 建表(exec 跑静态 SQL,不支持占位符)
db.exec(`
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        age INTEGER
    )
`);

// 插入(prepare + run)
const insertStmt = db.prepare('INSERT INTO users (name, age) VALUES (?, ?)');
const info = insertStmt.run('张三', 25);
console.log(`新增行ID: ${info.lastInsertRowid}`);

// 查询(prepare + all)
const selectStmt = db.prepare('SELECT * FROM users WHERE age > ?');
const rows = selectStmt.all(20);  // 直接返回对象数组
console.log(rows);

// 更新
const updateStmt = db.prepare('UPDATE users SET age = ? WHERE name = ?');
const updateInfo = updateStmt.run(26, '张三');
console.log(`影响行数: ${updateInfo.changes}`);

// 删除
const deleteStmt = db.prepare('DELETE FROM users WHERE name = ?');
const deleteInfo = deleteStmt.run('张三');
console.log(`删除行数: ${deleteInfo.changes}`);

db.close();

关键 API 速查

API 作用
new Database('test.db') 连接数据库,无游标,直接操作 db
db.exec(sql_string) 执行静态 SQL(建表等),不支持占位符
db.prepare(sql).run(params) 执行增删改,返回 { lastInsertRowid, changes }
db.prepare(sql).all(params) 查询所有结果,返回对象数组
db.prepare(sql).get(params) 查询单条结果
db.close() 关闭数据库

重要提醒

exec 不支持占位符,只接纯字符串,有注入风险。 所有带外部输入的操作必须走 prepare

两种语言使用方式对比

对比维度 Python (sqlite3) Node.js (better-sqlite3)
连接 sqlite3.connect() new Database()
游标 需要 cursor 不需要,直接操作 db
事务 DML 必须手写 commit() 每条语句自动提交
建表 cursor.execute(DDL) db.exec(DDL),不支持占位符
带参数操作 cursor.execute(sql, params) db.prepare(sql).run/all(params)
结果集 元组列表 [(1, '张三')] 对象数组 [{id, name}]
查询单条 cursor.fetchone() stmt.get(params)
批量插入 executemany(sql, params_list) Transaction + 循环 run

占位符写法对比

语言 位置占位符 命名占位符
Python ?,传元组 :name%(name)s,传字典
Node.js ?,传数组或展开 @name$name,传对象

两者都支持两种风格,选一种即可。

SQL 查询的逻辑执行顺序

书写顺序 ≠ 执行顺序。

你写 SQL 的物理顺序是:

sql

vbnet 复制代码
SELECT → FROM → JOIN → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT

但数据库逻辑上执行的顺序是:

执行顺序 关键词 作用
FROM / JOIN 确定数据来源,做连接生成虚拟表
WHERE 按条件过滤行(此时没有分组,也没有聚合结果)
GROUP BY 将过滤后的行按字段分组
HAVING 对分组后的结果进行过滤(聚合函数已计算完毕)
SELECT 计算表达式、取别名、去重,生成最终列
ORDER BY 对结果集排序
LIMIT / OFFSET 截取部分行

常见错误

错误一:在 WHERE 里使用聚合函数

sql

sql 复制代码
-- ❌ 错误
SELECT user_id, COUNT(*) FROM orders 
WHERE COUNT(*) > 3 GROUP BY user_id;

-- ✅ 正确:用 HAVING
SELECT user_id, COUNT(*) FROM orders 
GROUP BY user_id 
HAVING COUNT(*) > 3;

因为 WHERE 在第 2 步执行,那时还没分组,数据库不知道 COUNT(*) 是多少。

错误二:在 WHERE 里使用 SELECT 中定义的别名

sql

sql 复制代码
-- ❌ 错误
SELECT SUM(price) AS total FROM orders WHERE total > 100;

-- ✅ 正确:用 HAVING(如果是聚合字段)
SELECT SUM(price) AS total FROM orders HAVING total > 100;

SELECT 在第 5 步才执行,别名对第 2 步的 WHERE 来说不存在。

聚合函数与 GROUP BY 的"两把锁"规则

聚合函数速查

函数 作用 注意
COUNT(*) 统计总行数 包含 NULL 值行
COUNT(column) 统计该列非 NULL 的行数 自动忽略 NULL
COUNT(DISTINCT column) 统计去重后的非 NULL 值个数 常用在"有多少种"场景
SUM(column) 求和 NULL 被忽略(当作 0)
AVG(column) 求平均 NULL 不参与分子分母计算
MAX / MIN 最大/最小值 字符串也可比较(按字典序)

GROUP BY 的"两把锁"规则

执行 GROUP BY 之后,SELECT 后面只能写两种东西:

  1. 分组字段本身 (即 GROUP BY 后面写的列)
  2. 聚合函数 (如 SUM(amount)COUNT(*)

为什么? 分组后,每个组里可能有多行原始数据。如果写一个既不是分组字段也不是聚合函数的列,数据库不知道该取组里哪一行的值。

sql

sql 复制代码
-- ❌ 错误:user_id 不是分组字段也不是聚合函数
SELECT user_id, category, SUM(amount) 
FROM orders 
GROUP BY user_id;

-- ✅ 正确:加 category 到 GROUP BY,或用聚合函数包裹
SELECT user_id, MAX(category) AS category, SUM(amount) 
FROM orders 
GROUP BY user_id;

WHERE vs HAVING:过滤行还是过滤组?

一句话区分:WHERE 过滤"行",HAVING 过滤"组"。

维度 WHERE HAVING
执行时机 分组前(第2步) 分组后(第4步)
能否用聚合函数 ❌ 不能 ✅ 能
能否用别名 ❌ 不能 ❌ 不能(取决于数据库)
能否走索引 ✅ 能(快) ❌ 基本不能(慢)

工程原则 :能用 WHERE 提前过滤掉的,绝不留到 HAVING 里。

比如"只统计已完成的订单"------一定写在 WHERE status = 'completed',而不是写在 HAVING 里。因为 WHERE 在第 2 步就把无效数据筛掉了,后续 GROUP BYHAVING 处理的数据量更小,性能更好。

工程级复杂查询的"积木式拆解"思维

遇到复杂业务需求,按顺序一步步搭:

需求示例:统计 2026 年每个月、每个产品类别的总销售额,且只展示月销售额 > 1 万的,按月份倒序、销售额倒序排列。

积木式搭建过程

sql

sql 复制代码
SELECT 
    DATE_FORMAT(order_date, '%Y-%m') AS month,
    category,
    SUM(amount) AS total_sales,
    COUNT(DISTINCT user_id) AS unique_buyers
FROM orders
WHERE order_date BETWEEN '2026-01-01' AND '2026-12-31'
  AND status = 'completed'          -- ① WHERE 提前过滤无效单,减少分组负担
GROUP BY month, category            -- ② 按月、类别分组
HAVING SUM(amount) > 10000          -- ③ 用聚合结果过滤组
ORDER BY month DESC, total_sales DESC;  -- ④ 排序

积木式步骤

  1. 先写 FROMJOIN:确认需要哪几张表,怎么关联
  2. 再写 WHERE:把业务上"哪些行不需要"的过滤条件全加上
  3. GROUP BY:按什么维度统计(时间?类别?用户?)
  4. SELECT 聚合表达式:要算什么指标(总和?去重人数?平均值?)
  5. HAVING:对统计结果有什么门槛要求
  6. 最后 ORDER BY + LIMIT:排序和分页

互动讨论

💬 SQLite 能用在生产环境吗?

可以。Chrome、Firefox、Android、iOS 都在生产环境使用 SQLite。单机应用、桌面软件、移动 App 的本地存储,SQLite 是首选。不适用的是"高并发写"场景(如电商订单系统)。

💬 Python 和 Node.js 的 SQLite 库,哪个更好?

取决于项目。如果项目本身是 Python 后端,直接用标准库 sqlite3 无需额外依赖。如果是 Node.js 项目,better-sqlite3 性能更好、手感更自然,同步 API 在 SQLite 这种低延迟场景下完全够用。

💬 execprepare 有什么区别?

exec 执行静态 SQL,不支持占位符,只能用于 DDL(建表)或不含变量的 SQL。prepare 支持占位符,所有带外部输入的操作必须走 prepare,防止 SQL 注入。

💬 COUNT(*)COUNT(1) 性能有区别吗?

没有。现代数据库优化器已完全等价,按习惯写 COUNT(*) 即可。

💬 WHEREHAVING 的工程原则是什么?

能用 WHERE 不用 HAVING 因为 WHERE 能在分组前过滤掉大量数据,减少分组和聚合的计算量。HAVING 是分组后的过滤,数据已经汇总完了,此时过滤成本更高。

相关推荐
ShineWinsu1 小时前
对于MySQL:数据库的操作的解析
linux·数据库·c++·mysql·面试·笔试·库的操作
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
MySQL 数据类型
数据库·mysql
这个DBA有点耶2 小时前
索引合并不是万能药:MySQL同时用两个索引,为什么比不用还慢?
数据库·mysql·dba
Aime_Perfect3 小时前
sqlserver AlwaysOn(一)
数据库·sqlserver
HAYDENR3 小时前
毛利率波动大可能是什么原因?毛利率变动怎么判断是否合理?
数据库
大牧师3 小时前
MySQL 学习教程
数据库·sql·mysql·docker·node·全栈·后端数据
Elastic 中国社区官方博客3 小时前
如何通过一条 ES|QL 查询为 Elasticsearch 中的每个指标构建指标图表
大数据·运维·数据库·elasticsearch·搜索引擎·全文检索·kibana
数据库小学妹4 小时前
MySQL redo刷盘实测:innodb_flush_log_at_trx_commit与故障矩阵
运维·数据库·mysql
ByteRock4 小时前
ClickHouse 表的生老“并”死:表实例、表元数据与并发 DDL
数据库