使用 DuckDB 与图查询发现可疑资金流动

使用 DuckDB 与图查询发现可疑资金流动

DuckDB 可以处理图。本篇使用 DuckDB 与 DuckPGQ 社区扩展,借助 SQL:2023 标准中的 SQL/PGQ 图语法,分析金融数据中的欺诈模式。 追踪资金比表面上困难。手法复杂的犯罪者会利用漫长、复杂的交易链隐藏踪迹,掩盖非法资金来源。梳理这些网络是经典的图问题:需要在庞大的账户与交易网络中寻找可疑模式和隐藏路径。 过去,这类分析往往需要把数据导入专门的图数据库,增加复杂度与开销。能否直接在日常使用的数据库中完成这些图分析?

这正是 DuckDB 扩展能力发挥作用的地方。本文将深入金融数据集,使用 DuckDB 和图查询扩展,寻找可能指向洗钱活动或其他高风险账户的模式。 ## 从关系表到属性图

寻找可疑活动前,先了解数据。本文使用模拟金融网络的 LDBC Financial Benchmark 数据集。执行以下命令,附加包含数据集的数据库:

ATTACH 'https://blobs.duckdb.org/data/finbench.duckdb' AS finbench;
USE finbench;

若要跟随本文示例,建议使用 DuckDB v1.4.1。 本文使用数据集的一个子集,包括 Person、Account,以及连接账户的 AccountTransferAccount 表。

金融数据模式图 先了解网络规模:

SELECT
    (SELECT count(*) FROM Person) AS num_persons,
    (SELECT count(*) FROM Account) AS num_accounts,
    (SELECT count(*) FROM AccountTransferAccount) AS num_transfers;
┌─────────────┬──────────────┬───────────────┐
│ num_persons │ num_accounts │ num_transfers │
│    int64    │    int64     │     int64     │
├─────────────┼──────────────┼───────────────┤
│     785     │     2055     │     8132      │
└─────────────┴──────────────┴───────────────┘

这条查询概括实体与连接的数量。上面的模式图表明,账户与转账表已经构成一张图:实体是节点或顶点,实体之间的关系则由边表示。 为增强查询表达能力,使用属性图模型(Property Graph)。这意味着可以为节点和边添加描述性标签,例如 Account、Person,以及 accountId、nickname 等具体属性。 如果觉得这与关系模型很像,你是对的。Person 表是一组带有 Person 标签的节点,其列就是属性。这种自然映射使 DuckDB 这样的高性能关系数据库成为图分析的良好基础。 ### DuckDB 中的属性图 可以直接用熟悉的 SQL 编写图查询,不过借助 DuckDB 的扩展生态会更简单。本文使用 DuckPGQ 社区扩展:它为 DuckDB 解析器添加可视化图语法,即 SQL/Property Graph Queries(SQL/PGQ),属于正式的 SQL:2023 标准,部分设计受图查询语言 Cypher 启发。 > DuckPGQ 最初是研究原型,现在已作为社区扩展提供。

安装并加载扩展非常直接:

INSTALL duckpgq FROM community;
LOAD duckpgq;

使用 DuckPGQ 的第一步,是在已有表之上创建属性图。

CREATE PROPERTY GRAPH finbench
VERTEX TABLES (
    Person,
    Account
)
EDGE TABLES (
    AccountTransferAccount
        SOURCE KEY (fromId) REFERENCES Account (accountId)
        DESTINATION KEY (toId) REFERENCES Account (accountId)
        LABEL Transfer,
    PersonOwnAccount
        SOURCE KEY (personId) REFERENCES Person (personId)
        DESTINATION KEY (accountId) REFERENCES Account (accountId)
        LABEL PersonOwn
);

创建属性图时,要明确区分 VERTEX 表与 EDGE 表。对于顶点表,只需指定表名。对于边表,还必须分别为 SOURCE 与 DESTINATION 指定边表中充当相应键的列。 这与定义 FOREIGN KEY 约束的原理相同:把边表连接回它所关联的节点表。LABEL 子句为关系类型赋予清晰名称。虽然表名是 AccountTransferAccount,其中的边表示 Transfer 关系;后续图查询使用的就是这一名称。 属性图已创建,可以开始调查金融数据。 ## 图处理

数据库中的图处理通常包括:

  • 模式匹配:在数据中寻找某种模式。
  • 路径查找:寻找路径,路径长度可能不固定。

接下来用 DuckDB 与 DuckPGQ 完成这两类任务。 ### 寻找可疑活动

SQL/PGQ 通过可视化图语法,更自然地描述图模式。下面寻找可能与洗钱有关的模式。

隐藏非法资金的一种常见方式称为 smurfing(拆分交易):将可能触发报告要求的一笔大额转账,拆分为一段时间内的多笔小额交易。 可寻找交易次数高、平均金额相对低的账户对。将平均金额阈值设为 50,000 美元,检查是否存在高频交易关系:

SELECT
    fromName,
    count(amount) AS number_of_transactions,
    round(avg(amount), 2) AS avg_amount,
    toName
FROM GRAPH_TABLE (finbench
    MATCH (a:Account)-[t:Transfer]->(a2:Account)
    COLUMNS (a.nickname AS fromName,
             t.amount,
             a2.nickname AS toName
            )
)
GROUP BY ALL
HAVING avg_amount < 50_000
ORDER BY number_of_transactions DESC, avg_amount ASC
LIMIT 5;

查询得到以下结果:

┌───────────────────┬────────────────────────┬────────────┬───────────────────┐
│     fromName      │ number_of_transactions │ avg_amount │      toName       │
│      varchar      │         int64          │   double   │      varchar      │
├───────────────────┼────────────────────────┼────────────┼───────────────────┤
│ Noe Trites        │                      1 │   49365.04 │ Dale Croucher     │
│ Madeleine Bussing │                      1 │   46663.56 │ Delphine Primiano │
│ Bonnie Centeno    │                      1 │   46663.56 │ Maile Boon        │
│ Darci Sheedy      │                      1 │   44856.02 │ Carmella Estelle  │
│ Marguerita Gurne  │                      1 │   44393.68 │ Delphine Primiano │
└───────────────────┴────────────────────────┴────────────┴───────────────────┘

查询正常工作,但交易次数始终为 1,结果未显示可疑活动的迹象。下面拆解查询,理解原因。

关键位于 FROM 子句。GRAPH_TABLE (finbench ...) 允许对刚创建的属性图运行图查询,并将结果作为普通表处理。 MATCH (a:Account)-[t:Transfer]->(a2:Account) 是模式的核心:它直观描述从一个账户 (a:Account) 到另一个账户 (a2:Account) 的转账。() 表示节点,[] 表示连接边,ASCII 箭头 -> 表示边方向。COLUMNS(...) 则像模式的 SELECT 列表,取出账户昵称与转账金额。 SQL/PGQ 的优点在于,图模式匹配的结果可以直接进入熟悉的标准 SQL。GROUP BY ALL 汇总同一对主体之间的所有转账,HAVING avg_amount < 50_000 按设定的拆分交易模式过滤。 查询本身正确,但数据集中没有这种简单的拆分交易模式,因此需要继续调查更复杂的模式。图查询可查找难以用传统 SQL JOIN 表达的结构,例如交易路径。 ### 在交易中寻找路径

另一种典型的潜在欺诈模式,是资金经过交易链后回到最初发送者的循环。传统 SQL 很难表达这种问题。阅读本节后可以自行尝试,下一节会给出答案。

交易路径查询模式 SQL/PGQ 使路径查询简单得多。上图展示目标模式:在同一个人 P 拥有的两个不同账户 A1、A2 之间,寻找由一笔或多笔转账组成的路径。一个人可以拥有多个账户。下面查找编号为 125 的人所持账户之间的循环:

FROM GRAPH_TABLE(finbench
    MATCH p = ANY SHORTEST
                  (p:Person)-[o1:PersonOwn]->(a1:Account)
                  -[t:Transfer]->+
                  (a2:Account)<-[o2:PersonOwn]-(p:Person)
WHERE
    p.personId = 125 AND a1.accountId <> a2.accountId
    COLUMNS (
        path_length(p) AS path_length,
        a1.accountId AS start_account,
        a2.accountId AS end_account
    )
)
ORDER BY path_length;

结果显示,这个人的多个账户之间存在长度不同的循环:

┌─────────────┬─────────────────────┬─────────────────────┐
│ path_length │    start_account    │     end_account     │
│    int64    │        int64        │        int64        │
├─────────────┼─────────────────────┼─────────────────────┤
│           8 │ 4753267931712848113 │ 4794926228266025204 │
│           8 │ 4769874955338776819 │ 4794926228266025204 │
│           8 │ 4796615078126289138 │ 4769874955338776819 │
│           9 │ 4753267931712848113 │ 4769874955338776819 │
│           9 │ 4769874955338776819 │ 4753267931712848113 │
│           9 │ 4794926228266025204 │ 4753267931712848113 │
│           9 │ 4796615078126289138 │ 4753267931712848113 │
│           9 │ 4796615078126289138 │ 4794926228266025204 │
│          12 │ 4794926228266025204 │ 4769874955338776819 │
└─────────────┴─────────────────────┴─────────────────────┘

关键仍在 FROM 子句:MATCH 沿给定模式寻找 ANY SHORTEST 路径。模式第一部分 (p:Person)-[o1:PersonOwn]->(a1:Account) 找到 Person 125 的全部账户。第二部分执行路径查找。特别注意 (a1:Account)-[t:Transfer]->+(a2:Account) 中的 +:两个账户不必由一条边直接连接。 + 表示两个账户之间存在一条或多条 Transfer 边,没有设置上限。最后,检查目标账户是否也属于同一个人 p。 查询发现,Person 125 的账户之间存在多条不直观的路径,长度从 8 到 12 不等。 每一行代表连接其两个账户的隐藏交易链。还可以看到清晰的循环模式:例如,账户 4753267931712848113 到 4769874955338776819 存在一条 9 步路径,反方向也存在另一条 9 步路径。原文认为,这提示有复杂而有意的账户间资金转移,值得进一步调查。 ## 使用传统方式实现

前面请你思考如何用传统 SQL 寻找这类所有权循环。下面给出答案。

与 SQL/PGQ 版本比较时,需要注意两点: 1. 性能保护:必须手动限制路径长度(ps.depth < 11),以避免无限递归及稠密图中可能出现的平方级运行时间。SQL/PGQ 的 ->+ 语法不需要这个手动限制。 2. 路径长度差异:此查询结果的 path_length 比 DuckPGQ 结果少两跳,因为它只计数 Transfer 边,而 DuckPGQ 查询还把两条 PersonOwn 边计入路径。 下面的传统递归 CTE 查找同一个人拥有的任意两个账户之间的最短路径:

WITH RECURSIVE
    owned_accounts AS (
        SELECT accountId
        FROM PersonOwnAccount
        WHERE personId = 125
    ),
    path_search(start_node, end_node, path, depth) AS (
        -- Base case: a direct transfer from one of the person's accounts
        SELECT
            fromId,
            toId,
            [fromId, toId],
            1
        FROM
            accounttransferaccount
        WHERE
            fromId IN (SELECT accountId FROM owned_accounts)
        UNION ALL
        -- Recursive step: find the next transfer in the path
        SELECT
            ps.start_node,
            t.toId,
            list_append(ps.path, t.toId),
            ps.depth + 1
        FROM path_search ps
        JOIN accounttransferaccount t ON ps.end_node = t.fromId
        WHERE
            t.toId NOT IN (SELECT unnest(ps.path)) AND ps.depth < 11
    )
SELECT distinct start_node, end_node, min(depth) AS path_length
FROM path_search
WHERE end_node IN (SELECT accountId FROM owned_accounts)
  AND start_node <> end_node
GROUP BY ALL
ORDER BY path_length;

可以看到,这套逻辑需要 WITH RECURSIVE、列表形式的手动路径跟踪,以及显式循环检测。SQL/PGQ 的可视化语法正是为了避免这样冗长而复杂的查询。 ## 结语

本文从一个简单目标出发:能否利用 DuckDB 寻找图分析中的复杂模式与隐藏路径?通过对 Financial Benchmark 数据集的探索,答案是可以。 最明显的收益是易用性。DuckPGQ 提供的 SQL/PGQ 可视化语法,将复杂的“所有权循环”查询从庞大的递归 CTE 简化为几行易读代码,这正是实际分析任务需要的表达能力。完整文档见 duckpgq.org。 整个调查过程都在 DuckDB 内直接完成。通过扩展生态获得图查询能力,无需导出数据,也无需管理独立的专用系统。所有计算都在数据所在位置,利用 DuckDB 的高性能向量化引擎运行。

来源与许可

原文:Uncovering Financial Crime with DuckDB and Graph Queries,作者 Daniël ten Wolde,发布于 2025-10-22。Copyright 2018–2025 Stichting DuckDB Foundation,MIT。2026-10-03 中文翻译与静态排版转换,保留 DuckDB v1.4.1 版本范围及全部查询、结果。SQL 未执行;样本模式本身不构成对真实个人违法的认定。

完整许可证原文
Copyright 2018-2025 Stichting DuckDB Foundation

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容