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,截取一个关系型的对比。反正对博客来说应该都无所谓。

image

Surreallist图形化管理界面

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

数据库连接

查询页面

表格预览和筛选

数据库切换

定义Field

索引定义

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

表预览(可以看见一些定义了的field和图关系)

权限管理可以到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,这个ContactFormData在后续也可以随意的添加或者修改字段。

简单查询

查询也很简单,如下。但是反序列化并不像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写的大半是用不了的,这个时候需要你自己在官方文档里去找。

重庆长久办公服务

专业从事办公设备采购、出租、维护和售后服务,致力于为企业提供一站式办公设备解决方案。

地址:重庆市渝中区大坪街道上肖家湾17号
电话:156 8329 5935
邮箱:contact@mail.changjiu365.cn

主要服务

  • 办公设备采购
  • 设备租赁服务
  • 设备维护保养
  • 技术支持服务
  • 售后保障服务

联系方式

📱微信咨询
💻在线咨询
🚚上门服务

© 2025 渝中区长久办公设备经营部. 保留所有权利.

渝ICP备2025059305号-1