问题:SQLite 撑不住分析查询了
小李运营的电商网站每天产生约 50 万条订单数据,一直用 SQLite 存着。最近老板要看「按品类统计的季度销售趋势」,小李写好 SQL 一跑——等了 30 秒还没出结果。
SQLite 是一款优秀的嵌入式 OLTP 数据库,在单行插入、简单主键查询上表现出色。但当你开始对它跑 GROUP BY、窗口函数、多表聚合 这类分析查询时,情况就不同了。
DuckDB 恰好填补了这个空白——它同样是嵌入式数据库(无需服务器),但专门为 OLAP(在线分析处理)场景设计。
问题是:DuckDB 比 SQLite 快多少?什么时候该换用 DuckDB?
本文用 100 万行真实电商数据进行实测,给出可量化的答案。
测试环境与数据
硬件/软件
| 项目 | 规格 |
|---|---|
| CPU | AMD EPYC (4 vCPU) |
| 内存 | 8 GB |
| 存储 | NVMe SSD |
| OS | Ubuntu 22.04 |
| DuckDB | v1.5.2 |
| SQLite | 3.45.1 |
测试数据
使用一个包含 100 万行 的电商订单数据集,结构如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键 |
| category | VARCHAR | 商品品类(6 类) |
| product_name | VARCHAR | 商品名(10000 种) |
| price | DOUBLE | 价格 ($5–$505) |
| quantity | INTEGER | 数量 (1–10) |
| discount | DOUBLE | 折扣 ($1–$101) |
| order_date | DATE | 2025 年随机日期 |
| region | VARCHAR | 区域 (CN/US) |
| user_id | VARCHAR | 用户 (50000 人) |
生成数据(DuckDB 版)
-- 在 DuckDB 中生成 100 万行测试数据
COPY (
SELECT
range + 1 AS id,
CASE WHEN random() < 0.3 THEN 'electronics'
WHEN random() < 0.5 THEN 'clothing'
WHEN random() < 0.65 THEN 'home'
WHEN random() < 0.78 THEN 'books'
WHEN random() < 0.88 THEN 'sports'
ELSE 'food' END AS category,
'product_' || (range % 10000 + 1) AS product_name,
ROUND(random() * 500 + 5, 2) AS price,
(random() * 10 + 1)::INT AS quantity,
ROUND(random() * 100 + 1, 2) AS discount,
DATE '2025-01-01' + INTERVAL (random() * 364) DAY AS order_date,
CASE WHEN random() < 0.5 THEN 'CN' ELSE 'US' END AS region,
'user_' || (range % 50000 + 1) AS user_id
FROM range(1000000)
) TO 'ecommerce_1m.csv' (HEADER, DELIMITER ',');
10 个典型查询的对比
测试方法
- DuckDB:直接从 CSV 查询(
read_csv_auto),零 ETL - SQLite:先
.importCSV 到表,再查询 - 每个查询跑多次取有代表性的时间
- 两数据库查询完全相同(语法尽量兼容)
测试 SQL 代码
DuckDB 版:
-- DuckDB:直接从 CSV 加载(零 ETL)
CREATE TABLE sales AS
SELECT * FROM read_csv_auto('ecommerce_1m.csv');
-- Q1: 简单计数
SELECT COUNT(*) FROM sales;
-- Q2: 总收入计算
SELECT SUM(price * quantity) AS total_revenue FROM sales;
-- Q3: 按品类分组聚合
SELECT category, COUNT(*) AS orders,
SUM(price * quantity) AS revenue, AVG(price) AS avg_price
FROM sales GROUP BY category ORDER BY revenue DESC;
-- Q4: 日期范围过滤
SELECT COUNT(*), SUM(price * quantity)
FROM sales WHERE order_date BETWEEN '2025-06-01' AND '2025-08-31';
-- Q5: 多维度分组
SELECT region, category, COUNT(*) AS cnt, SUM(price * quantity) AS revenue
FROM sales GROUP BY region, category ORDER BY revenue DESC;
-- Q6: 窗口函数 - 月度累计
SELECT strftime(order_date, '%Y-%m') AS month,
SUM(price * quantity) AS monthly_revenue,
SUM(SUM(price * quantity)) OVER (ORDER BY strftime(order_date, '%Y-%m')) AS running_total
FROM sales GROUP BY month ORDER BY month;
-- Q7: Top 10 商品
SELECT product_name, SUM(price * quantity) AS revenue, COUNT(*) AS orders
FROM sales GROUP BY product_name ORDER BY revenue DESC LIMIT 10;
-- Q8: 按区域统计客单价
SELECT region, AVG(price * quantity) AS avg_order_value,
COUNT(*) AS orders, SUM(price * quantity) AS total_revenue
FROM sales GROUP BY region;
-- Q9: 高频用户(下单 > 5 次)
SELECT user_id, COUNT(*) AS order_count, SUM(price * quantity) AS total_spent
FROM sales GROUP BY user_id
HAVING COUNT(*) > 5 ORDER BY total_spent DESC LIMIT 20;
-- Q10: 条件聚合
SELECT
SUM(CASE WHEN price > 200 THEN 1 ELSE 0 END) AS expensive_orders,
SUM(CASE WHEN discount > 50 THEN 1 ELSE 0 END) AS high_discount_orders,
AVG(CASE WHEN region = 'CN' THEN price ELSE NULL END) AS cn_avg_price,
AVG(CASE WHEN region = 'US' THEN price ELSE NULL END) AS us_avg_price
FROM sales;
SQLite 版(语法兼容):
-- SQLite:需先导入 CSV
.mode csv
.import ecommerce_1m.csv sales
-- 查询与 DuckDB 相同,唯一区别:
-- DuckDB 用 strftime() 而 SQLite 的 strftime() 参数顺序略有不同,但功能等价
实测结果
| # | 查询类型 | DuckDB | SQLite | 加速比 |
|---|---|---|---|---|
| Q1 | COUNT(*) | 0.004s | 0.035s | 8.7x |
| Q2 | SUM 总收入 | 0.005s | 0.408s | 81.6x |
| Q3 | GROUP BY 品类 | 0.020s | 2.275s | 113.8x |
| Q4 | 日期过滤 | 0.006s | 0.413s | 68.8x |
| Q5 | 多维度 GROUP BY | 0.020s | 4.185s | 209.3x |
| Q6 | 窗口函数 | 0.094s | 1.555s | 16.5x |
| Q7 | Top 10 商品 | 0.041s | 1.473s | 35.9x |
| Q8 | 客单价统计 | 0.010s | 1.687s | 168.7x |
| Q9 | HAVING 子句 | 0.056s | 2.323s | 41.5x |
| Q10 | 条件聚合 | 0.043s | 1.303s | 30.3x |
关键发现:在涉及全表扫描+聚合的查询(SUM、GROUP BY)上,DuckDB 比 SQLite 快 80–200 倍。在简单 COUNT 上差距最小(8.7 倍),因为 SQLite 有 B-Tree 索引可以加速。
为什么 DuckDB 这么快?
1. 列式存储 vs 行式存储
| DuckDB(列式) | SQLite(行式) | |
|---|---|---|
| 读取方式 | 只读需要的列 | 读整行再丢弃不需要的列 |
| 压缩率 | 高(同列数据类型一致) | 低 |
| 缓存效率 | 列数据连续存储,CPU cache 友好 | 行数据分散 |
举例:Q2 只要求 price 和 quantity 两列,DuckDB 只读取这 2 列,而 SQLite 需要读取所有 9 列。磁盘 I/O 相差 4.5 倍。
2. 向量化执行
DuckDB 按 1024 行一批 批量处理数据,充分利用 CPU 的 SIMD 指令。SQLite 逐行处理,有大量函数调用开销。
3. 多线程并行
DuckDB 在所有查询中自动使用多核,而 SQLite 默认单线程(SQLite 的并行写入也不安全)。
4. 零拷贝读取
DuckDB 可以直接查询 CSV/Parquet 文件,不需要导入阶段——这在处理一次性分析时节省了大量时间。
什么时候该用 SQLite?什么时候该换 DuckDB?
✅ 继续用 SQLite 的场景
- Web 应用后端:需要低延迟的单行插入/更新
- 移动端/桌面 App:SQLite 是嵌入式的首选,安装包仅有几百 KB
- 事务密集型:多个并发写入,ACID 保证
- 数据量 < 10 万行:此时差距不明显
✅ 换 DuckDB 的场景
- 分析报表生成:GROUP BY、窗口函数、复杂的聚合查询
- 大数据量探索:100 万行以上数据,快速获取洞察
- ETL 管道:读取 CSV/Parquet/JSON,清洗、转换后输出
- 批量处理:不需要低延迟,但要高吞吐
黄金法则
OLTP 选 SQLite,OLAP 选 DuckDB。
如果数据既需要事务写入又需要分析查询——可以考虑在 SQLite 中写入,用 DuckDB 的 sqlite 扩展直接查询 SQLite 数据库:
-- DuckDB 直接查询 SQLite 数据库!
INSTALL sqlite;
LOAD sqlite;
SELECT category, SUM(price * quantity) AS revenue
FROM sqlite_scan('myapp.db', 'orders')
GROUP BY category ORDER BY revenue DESC;
实际迁移案例
小李最终这样解决问题:
- 保留 SQLite 作为写入数据库:电商系统继续写入 SQLite
- DuckDB 做分析层:每天凌晨用 DuckDB 读取 SQLite 数据生成报表
- 结果:原来跑 30 秒的月度销售报表,降到 0.1 秒
# Python 示例:用 DuckDB 分析 SQLite 数据
import duckdb
con = duckdb.connect()
con.execute("INSTALL sqlite; LOAD sqlite;")
# 直接分析 SQLite 数据,无需导出
result = con.execute("""
SELECT strftime(order_date, '%Y-%m') AS month,
category,
SUM(price * quantity) AS revenue
FROM sqlite_scan('ecommerce.db', 'orders')
GROUP BY month, category
ORDER BY month, revenue DESC
""").fetchdf()
print(result)
总结
| 对比维度 | DuckDB | SQLite |
|---|---|---|
| 设计目标 | OLAP 分析 | OLTP 事务 |
| 100 万行 GROUP BY | 0.02s | 2.28s |
| 多维度聚合 | 0.02s | 4.19s |
| 窗口函数 | 0.09s | 1.56s |
| 安装大小 | ~50MB | ~600KB |
| 并发写入 | 不支持 | ✅ 支持 |
| 直接查 CSV | ✅ 原生支持 | ❌ 需导入 |
一句话总结:如果你的数据超过 10 万行,且查询涉及聚合分析——DuckDB 是比 SQLite 好 10–200 倍的选择。两者不是替代关系,而是互补——SQLite 管写入,DuckDB 管分析,各取所长。
