在中小型项目中,我们经常遇到一种尴尬的局面:业务数据用关系表来组织最自然,但一旦涉及到用户关系链、商品推荐、权限继承这类图结构问题,用SQL写多层自连接又极其痛苦。SQLite与ArangoDB的组合恰好能解决这个问题。SQLite承担本地结构化数据的存储与查询,ArangoDB负责文档存储和图遍历计算,两者配合可以用很小的成本搭建一个多模型数据架构。

为什么选择SQLite与ArangoDB的组合
先说选型逻辑。SQLite是一个嵌入式数据库,整个引擎只是一个几百KB的动态库,不需要独立的服务进程,数据全部存在单个文件里。这意味着它的部署成本几乎为零,特别适合桌面应用、移动端、边缘设备,以及作为服务端的本地缓存层。它对标准SQL的支持非常完整,事务、索引、触发器、窗口函数一应俱全。
ArangoDB则是另一类思路。它是原生的多模型数据库,一套查询语言AQL(ArangoDB Query Language)可以同时操作文档集合、图遍历和键值查询。当你需要分析"某个用户的朋友的朋友中,有谁购买过某类商品"这种问题时,ArangoDB的图遍历语法比SQL的自连接直观得多。
两者组合的典型分工是:SQLite作为主数据的落地存储,保证事务一致性和离线可用;ArangoDB作为关系计算引擎,把SQLite中的外键关系转换成边集合,专门做图相关的分析任务。这样既不用把所有数据强行塞进一个数据库,也不必为图计算引入复杂的自建索引结构。
项目数据结构设计与建表
我们用一个简化版的社交电商项目作为例子。核心实体有三个:用户、商品、订单。在SQLite中,这三个实体用常规的关系表来建,表结构如下:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
category TEXT,
price REAL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
amount REAL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
注意这里的orders表实际上承载了两类关系:用户到订单的归属关系,订单到商品的包含关系。在关系模型里这是通过外键表达的,而转换到图模型时,这两个外键就会变成两种不同类型的边。
索引方面,给外键列加上索引能明显提升同步时的全量扫描速度:
CREATE INDEX idx_orders_user ON orders(user_id); CREATE INDEX idx_orders_product ON orders(product_id); CREATE INDEX idx_users_city ON users(city);
有一个容易被忽视的细节:SQLite默认不会为外键列自动建索引,而且外键约束默认是关闭的,需要每次连接后执行PRAGMA foreign_keys = ON才能启用。这个坑在数据同步阶段尤其致命,如果源数据本身就有脏数据,同步到ArangoDB后图结构会错乱,排查起来非常耗时。
数据同步:从SQLite导入ArangoDB
同步方案有两种思路:全量重建和增量同步。对于数据量在百万级以内的项目,全量重建反而更简单可靠,可以在每晚定时任务中执行;增量同步则适合数据量大、实时性要求高的场景。
在ArangoDB一侧,先创建对应的集合。用户和商品导入为文档集合,关系导入为边集合:
const db = require('@arangodb').db;
// 创建文档集合
if (!db._collection('users')) db._createCollection('users');
if (!db._collection('products')) db._createCollection('products');
// 创建边集合,_from 和 _to 是边的固定属性
if (!db._collection('owns')) db._createEdgeCollection('owns');
if (!db._collection('contains')) db._createEdgeCollection('contains');
if (!db._collection('friend_of')) db._createEdgeCollection('friend_of');
边集合的命名建议用动词或动名词,比如owns表示用户拥有订单,contains表示订单包含商品。这样在写图查询时语义更清晰:users/123 -owns-> orders/456 -contains-> products/789。
从SQLite读取数据并写入ArangoDB,用Python实现一个简单的同步脚本:
import sqlite3
from arango import ArangoClient
# 连接SQLite本地文件
sqlite_conn = sqlite3.connect('app.db')
cursor = sqlite_conn.cursor()
# 连接ArangoDB
client = ArangoClient(hosts='http://127.0.0.1:8529')
db = client.db('social_shop', username='root', password='your_password')
# 同步用户数据
for row in cursor.execute('SELECT id, name, city FROM users'):
doc = {'_key': str(row[0]), 'name': row[1], 'city': row[2]}
db.collection('users').insert(doc, overwrite=True)
# 同步订单关系:用户 -owns-> 订单,订单 -contains-> 商品
cursor.execute('SELECT id, user_id, product_id FROM orders')
for order_id, user_id, product_id in cursor.fetchall():
db.collection('owns').insert({
'_from': 'users/' + str(user_id),
'_to': 'orders/' + str(order_id)
}, overwrite=True)
db.collection('contains').insert({
'_from': 'orders/' + str(order_id),
'_to': 'products/' + str(product_id)
}, overwrite=True)
这里有个关键技巧:把SQLite的主键映射为ArangoDB文档的_key。这样同步天然是幂等的,重复执行不会产生重复数据,配合overwrite=True参数就能实现"存在则更新、不存在则插入"的效果,省去了自己判断Upsert的麻烦。
增量同步的思路是给SQLite表加一个updated_at字段,配合触发器在更新时自动刷新时间戳,同步脚本只捞取上次同步时间之后的记录。另外要注意边集合的删除问题:如果订单被删除了,对应的边不会自动消失,需要在同步时对比两侧的键集合,把多余的边删掉,否则图查询结果会包含幽灵节点。
用AQL实现图查询
数据进入ArangoDB后,就可以发挥图查询的优势了。最经典的需求是"给用户推荐他朋友买过的商品",翻译成图语言就是:从某用户出发,经过一跳好友关系,再经过订单归属和商品包含,最终到达商品节点。
LET userId = 'users/1001'
FOR user, edge, path IN 1..1 ANY userId GRAPH 'social_graph'
FILTER IS_SAME_COLLECTION('friend_of', edge)
FOR friendOrder, oe IN 1..1 ANY user GRAPH 'social_graph'
FILTER IS_SAME_COLLECTION('owns', oe)
FOR product, ce IN 1..1 ANY friendOrder GRAPH 'social_graph'
FILTER IS_SAME_COLLECTION('contains', ce)
RETURN DISTINCT KEEP(product, '_key', 'title', 'price')
如果觉得多层嵌套太繁琐,也可以不建命名图,直接用匿名图语法,从边的方向性入手。因为owns边是从用户指向订单的,contains边是从订单指向商品的,所以查询路径可以简化为一组方向一致的遍历:
LET userId = 'users/1001'
// 朋友的朋友,排除自己,做二度人脉分析
FOR v, e, p IN 2..2 ANY userId friend_of
FILTER v._key != PARSE_IDENTIFIER(userId).key
FILTER LENGTH(FOR f IN p.edges FILTER f._from == userId) == 0
RETURN DISTINCT v
AQL的遍历语法核心是IN minDepth..maxDepth,配合ANY(双向)、OUTBOUND(顺着边方向)、INBOUND(逆着边方向)三个方向修饰符。做推荐用OUTBOUND沿关系链走,做影响力分析用ANY找环路,灵活度远高于SQL自连接。
架构落地与性能优化建议
整个架构跑起来之后,有几条经验值得记录。第一,ArangoDB只放"关系密集型"的数据,不要贪心地把所有字段都同步过去。商品详情、订单金额这类纯属性数据留在SQLite,ArangoDB里只保留图计算需要的最小字段集,同步速度快、内存占用也小。
第二,注意同步频率与查询时效的平衡。图分析类需求通常对实时性要求不高,每天凌晨全量重建一次往往就够了;如果确实需要小时级更新,建议用ArangoDB的批量接口import_bulk,比循环单条插入快一到两个数量级。
第三,SQLite侧要做读优化。给同步脚本用的连接设置PRAGMA journal_mode = WAL,可以让同步读取不阻塞业务写入;对大型查询加上PRAGMA cache_size调大页缓存,扫描全表的耗时会有明显改善。
第四,做好降级预案。ArangoDB挂掉时,业务不能跟着瘫痪。核心交易链路只依赖SQLite,图查询作为增强功能独立部署,挂了就返回空结果或走缓存。这种"主从分明"的架构设计,才能让多模型组合真正稳定地服务在线业务。
总的来说,SQLite与ArangoDB的组合思路是"各取所长":一个管结构化数据的可靠存储,一个管复杂关系的快速计算。理解了数据如何在两种模型之间映射,这套方案可以很自然地推广到权限系统、知识图谱、风控网络等更多场景。