SurrealDB 初探(一):从PostgreSQL迁移
最近把整个网站的后端从PostgreSQL迁移到了SurrealDB,感受很好,同时也顺带解决了我不会数据库的问题。
迁移原因
国庆前的最后一天在了解大模型的记忆功能,然后仔细读了一下mem0和其他几个记忆模块的文档,其中有提到的就是有知识图谱的构建和传统RAG的一起运用。所以当时顺带看了一下图数据库,最后就决定用这一个SurrealDB。
PostgreSQL很强大,但是配置很复杂,及时到现在我创建用户还需要先搜索一下命令,在主从复制上的配置就更复杂了。这是迁移的主因吧,另外就是图谱RAG的研究需要。
SurrealDB宣传称可以基于TiKV实现原生的高可用和横向拓展,大概就像ceph一样,3个数据块允许丢1块。但是具体的集群和实现现在还没有做,阿里云应该不会挂,往后推一推。
如何迁移
迁移部分很简单,可以说是无缝,因为nosql不需要配置结构,直接导入即可。首先就是下载一个Navicat,然后试用14天,连接数据库右键选择导出.json结构就可以了。
然后在官方的图形管理工具Surreallist里选择import database,选择和配置数据类型就可以了。


然后其他的一切都跟普通的数据库没区别。
迁移后的体验
对于Benchmark的部分,可以看一下这篇官方文章SurrealDB | Beginning our benchmarking journey,截取一个关系型的对比。反正对博客来说应该都无所谓。

Surreallist图形化管理界面
相较于PG的命令行管理,Surreallist确实简单不少,在这里可以选择本地或者远程连接,甚至网页也可以直接管理(但是无法连接本地)。对于日常的查看和插入绝对是足够了。






图关系这里我还没有添加,这里添加后可以看见每个表的关系

权限管理可以到Record级别,支持用户、JWT等方式

CDN加速
SurrealDB最让我眼前一亮的就是有http、ws的SDK。这意味着,你可以使用CDN为数据库连接加速,只需要在启动时添加 -b 0.0.0.0:12345 的参数,然后其余的一切就跟网站加速一样(POST请求不会被缓存)。因此我们可以把数据库放在一个性能强、高可用而线路差的机器上。
实时查询
SurrealDB也支持实时返回数据库条目的变动(基于websocket),并且,无需做任何的配置!SurrealDB一出生就拥有了非常强大的实时监控变动的功能。
use futures::StreamExt;
use serde::Deserialize;
use surrealdb::engine::remote::ws::Ws;
use surrealdb::opt::auth::Root;
use surrealdb::{Notification, Surreal};
use surrealdb::RecordId;
#[derive(Debug, Deserialize)]
struct Person {
id: RecordId,
}
// Handle the result of the live query notification
fn handle(result: Result<Notification<Person>, surrealdb::Error>) {
println!("Received notification: {:?}", result);
}
#[tokio::main]
async fn main() -> surrealdb::Result<()> {
let db = Surreal::new::<Ws>("127.0.0.1:8000").await?;
db.signin(Root {
username: "root",
password: "secret",
})
.await?;
db.use_ns("ns").use_db("db").await?;
// Select the "person" table and listen for live updates.
let mut stream = db.select("person").live().await?;
// Process updates as they come in.
while let Some(result) = stream.next().await {
// Do something with the notification
handle(result);
}
Ok(())
}
简单插入
SurrealDB的插入非常简单,只需要你有一个可以Serialize的struct就可以。例如下面的函数就是把传入的ContactFormData插入到"contact_form"这一个表。你也可以在struct指定id: RecordID。
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ContactFormData {
name: String,
contact: String,
service: String,
message: String,
}
pub async fn surrealdb_submit_form(data: ContactFormData) -> surrealdb::Result<()> {
let rows: Vec<ContactFormData> = SURREAL_DB.insert("contact_form").content(data).await?;
Ok(())
}
是不是非常简单?对于一组数据也是这样,data可以是Vec
简单查询
查询也很简单,如下。但是反序列化并不像sqlx一样会在编译时检查,这里不报错并不一定代表运行时能成功地反序列化,在发布之前需要在本地先测试一下能否成功。
pub async fn surrealdb_get_comments(
slug: String,
cursor: i64,
lang: String,
) -> surrealdb::Result<Vec<SurrealCommentDto>> {
let mut result = SURREAL_DB
.query(
r#"SELECT record::id(id) AS id, post_slug AS slug, nickname, parent_id, content, time::format(created_at, "%Y-%m-%d %H:%M") AS created_at, edited FROM comments WHERE post_slug = $slug ORDER BY created_at DESC
LIMIT 20 START $cursor;"#,
).bind(("slug",slug)).bind(("cursor",cursor))
.await?;
let comments: Vec<SurrealCommentDto> = result.take(0)?;
Ok(comments)
}
同时也要注意官方在安全最佳实践指南中提到的不要用format!拼接用户的输入,而要用.bind()
// Do this:
let name = "tobie"; // User-controlled input.
let mut result = db
.query("CREATE person CONTENT name = $name;")
.bind(("name", name))
.await?;
// Do NOT do this:
let name = "tobie"; // User-controlled input.
let mut result = db
.query(format!("CREATE person CONTENT name = {name};"))
.await?;
常用的SQL指令一览
以下的指令都已经检查过,不会报错。这里更多的是简单查询,但是更多的的Database Function需要在这里去按需查找 Database Functions | SurrealQL。
回退查询
SELECT
(title.zh OR title['zh-cn'] OR title['zh-CN'] OR title.en) AS title,
(description.zh OR description['zh-cn'] OR description['zh-CN'] OR description.en) AS description,
latest_version AS version,
last_updated_date AS lastmod,
*
FROM example;
筛选分类
SELECT
(title.zh OR title['zh-cn'] OR title['zh-CN'] OR title.en) AS title,
(description.zh OR description['zh-cn'] OR description['zh-CN'] OR description.en) AS description,
latest_version AS version,
last_updated_date AS lastmod,
*
FROM example
WHERE category CONTAINS 'reading';
去重筛选
SELECT
array::group(category) AS categories
FROM example
WHERE category IS NOT NULL
GROUP ALL;
自定义返回结构
BEGIN;
LET $pairs = (SELECT platforms, category FROM json_datetime WHERE category != NULL);
LET $all = array::group(SELECT VALUE category FROM $pairs);
LET $android = array::group(SELECT VALUE category FROM $pairs WHERE platforms CONTAINS 'android');
LET $linux = array::group(SELECT VALUE category FROM $pairs WHERE platforms CONTAINS 'linux');
LET $macos = array::group(SELECT VALUE category FROM $pairs WHERE platforms CONTAINS 'macos');
LET $windows = array::group(SELECT VALUE category FROM $pairs WHERE platforms CONTAINS 'windows');
RETURN {
all: $all,
platform_categories: [
{ platform: 'android', categories: $android },
{ platform: 'linux', categories: $linux },
{ platform: 'macos', categories: $macos },
{ platform: 'windows', categories: $windows }
]
};
COMMIT;
后记
迁移的最大收获可能就是学会了简单的SQL,昨天把微信公众号后端也迁移到SurrealDB后也算完全完成了迁移,当然还有一些小细节还要再继续修改。也遇到了一些小问题,比如插入单条数据和多条数据在指定id上的不同。还有就是SELECT VALUE和SELECT,一部分的嵌套结构和数组,就是{}和[],这俩 ,复杂的反序列化会遇见(如果是单个数据可以在select后面加一个[0])。
我现在也只是把它作为文档数据库在使用,但是它实际上是同时支持文档数据库、图数据库、时序数据库的多模态数据库。这几天会继续深入了解一下它的图数据库部分,先把简单的关系建立起来。
SurrealDB也支持http方式的调用,它可以POST到你在启动时配置的允许连接的http端点,实现webhook实时通知或者其他的回调。
如果不需要极致的性能、想要更简单的配置和CDN加速可以尝试一下,整体的资源占用大概是200M左右(本地 rocksdb,数据几千条左右),TiKV的部署和图数据库过几天我再写一篇文章。对了,如果决定要用一定要多多多多多读文档,因为AI写的大半是用不了的,这个时候需要你自己在官方文档里去找。
