TypeDB示例

TypeDB示例

本文使用 TypeDB 3.12.1 / TypeQL 3,完整演示以下业务:

  • 一个客户可以有多张发票;
  • 一张发票可以有多张收款单;
  • 一张发票可以有多个付款计划;
  • 从任意对象出发查询关联对象;
  • 点查、关系遍历、反向遍历、聚合、否定匹配、可选匹配和嵌套结果;
  • 每段 TypeQL 对应的关系型数据库建模和 SQL。

1. 业务模型

text 复制代码
customer ──billing──> invoice ──settlement──────> receipt
                           └──invoice-schedule──> payment-plan

在 TypeDB 中,customerinvoicereceiptpayment-plan 是实体类型,三条连接是关系类型。关系中的端点不是"起点/终点",而是带名称的角色:

TypeDB 关系 角色 业务含义 简化后的 SQL 表示
billing customerinvoice 发票属于哪个客户 invoice.customer_id 外键
settlement invoicereceipt 收款单冲销哪张发票 receipt.invoice_no 外键
invoice-schedule invoiceplan 发票包含哪些付款计划 payment_plan.invoice_no 外键

TypeDB 的关系是可以独立扩展的实例。例如以后可以让 settlement 自己拥有"核销金额""核销日期"等属性。SQL 中如果关系本身需要属性,通常要把外键改成独立的中间表。

2. SQL 参照模型

为了方便比较,本文 SQL 使用最简单的一对多外键模型:

sql 复制代码
CREATE TABLE customer (
    customer_id   varchar(50) PRIMARY KEY,
    customer_name varchar(200) NOT NULL
);

CREATE TABLE invoice (
    invoice_no     varchar(50) PRIMARY KEY,
    customer_id    varchar(50) NOT NULL REFERENCES customer(customer_id),
    invoice_date   date NOT NULL,
    invoice_amount bigint NOT NULL
);

CREATE TABLE receipt (
    receipt_no     varchar(50) PRIMARY KEY,
    invoice_no     varchar(50) NOT NULL REFERENCES invoice(invoice_no),
    receipt_date   date NOT NULL,
    receipt_amount bigint NOT NULL
);

CREATE TABLE payment_plan (
    plan_id        varchar(50) PRIMARY KEY,
    invoice_no     varchar(50) NOT NULL REFERENCES invoice(invoice_no),
    due_date       date NOT NULL,
    planned_amount bigint NOT NULL,
    plan_status    varchar(30) NOT NULL
);

这套 SQL Schema 将三个 TypeDB 关系压缩成了三个外键。如果需要与 TypeDB 逐字对应,也可以建立 billing(customer_id, invoice_no)settlement(invoice_no, receipt_no)invoice_schedule(invoice_no, plan_id) 三张关系表。

3. 创建数据库

在 TypeDB Console 中执行:

text 复制代码
database create invoice_demo

用途:创建名为 invoice_demo 的数据库。它类似 SQL 的:

sql 复制代码
CREATE DATABASE invoice_demo;

如果数据库已经存在,不要重复执行创建命令。可以用下面的命令确认:

text 复制代码
database list

4. 定义 TypeDB Schema

先开启 Schema 事务:

text 复制代码
transaction schema invoice_demo

然后执行完整定义:

typeql 复制代码
define
attribute customer-id, value string;
attribute customer-name, value string;
attribute invoice-no, value string;
attribute invoice-date, value date;
attribute invoice-amount, value integer;
attribute receipt-no, value string;
attribute receipt-date, value date;
attribute receipt-amount, value integer;
attribute plan-id, value string;
attribute due-date, value date;
attribute planned-amount, value integer;
attribute plan-status, value string;

entity customer,
    owns customer-id @key,
    owns customer-name,
    plays billing:customer;

entity invoice,
    owns invoice-no @key,
    owns invoice-date,
    owns invoice-amount,
    plays billing:invoice,
    plays settlement:invoice,
    plays invoice-schedule:invoice;

entity receipt,
    owns receipt-no @key,
    owns receipt-date,
    owns receipt-amount,
    plays settlement:receipt;

entity payment-plan,
    owns plan-id @key,
    owns due-date,
    owns planned-amount,
    owns plan-status,
    plays invoice-schedule:plan;

relation billing,
    relates customer,
    relates invoice;

relation settlement,
    relates invoice,
    relates receipt;

relation invoice-schedule,
    relates invoice,
    relates plan;

提交 Schema 事务:

text 复制代码
commit

4.1 每类语句的作用

TypeQL 作用 SQL 中最接近的概念
attribute invoice-no, value string 定义属性类型和值类型 定义一列及其数据类型
entity invoice 定义实体类型 CREATE TABLE invoice
owns invoice-no 声明实体可以拥有该属性 表中包含该列
@key 属性值唯一并标识拥有者 PRIMARY KEYUNIQUE NOT NULL
relation settlement 定义一类关系 外键或关系表
relates invoice 定义关系中的角色 关系表中的一个外键列
plays settlement:invoice 允许实体扮演该角色 允许该表被相应外键引用

注意:attribute due-date 同时包含"属性类型定义"和运行时的属性实例。每个具体日期值通过 has due-date ... 归属于某个实体,并不是脱离实体的一行普通业务数据。

5. 插入完整示例数据

开启写事务:

text 复制代码
transaction write invoice_demo

执行:

typeql 复制代码
insert
$c1 isa customer,
    has customer-id "C001",
    has customer-name "华星制造";
$c2 isa customer,
    has customer-id "C002",
    has customer-name "远航贸易";

$i1 isa invoice,
    has invoice-no "INV-001",
    has invoice-date 2026-01-10,
    has invoice-amount 10000;
$i2 isa invoice,
    has invoice-no "INV-002",
    has invoice-date 2026-02-15,
    has invoice-amount 6000;
$i3 isa invoice,
    has invoice-no "INV-003",
    has invoice-date 2026-03-01,
    has invoice-amount 8000;

$r1 isa receipt,
    has receipt-no "RCP-001",
    has receipt-date 2026-01-20,
    has receipt-amount 3000;
$r2 isa receipt,
    has receipt-no "RCP-002",
    has receipt-date 2026-02-05,
    has receipt-amount 7000;
$r3 isa receipt,
    has receipt-no "RCP-003",
    has receipt-date 2026-03-01,
    has receipt-amount 2000;

$p1 isa payment-plan,
    has plan-id "PLAN-001",
    has due-date 2026-02-10,
    has planned-amount 10000,
    has plan-status "paid";
$p2 isa payment-plan,
    has plan-id "PLAN-002",
    has due-date 2026-03-15,
    has planned-amount 3000,
    has plan-status "partial";
$p3 isa payment-plan,
    has plan-id "PLAN-003",
    has due-date 2026-04-15,
    has planned-amount 3000,
    has plan-status "pending";
$p4 isa payment-plan,
    has plan-id "PLAN-004",
    has due-date 2026-04-30,
    has planned-amount 8000,
    has plan-status "pending";

billing (customer: $c1, invoice: $i1);
billing (customer: $c1, invoice: $i2);
billing (customer: $c2, invoice: $i3);

settlement (invoice: $i1, receipt: $r1);
settlement (invoice: $i1, receipt: $r2);
settlement (invoice: $i2, receipt: $r3);

invoice-schedule (invoice: $i1, plan: $p1);
invoice-schedule (invoice: $i2, plan: $p2);
invoice-schedule (invoice: $i2, plan: $p3);
invoice-schedule (invoice: $i3, plan: $p4);

最后提交:

text 复制代码
commit

$c1$i1 等变量只在当前查询中有效。先将实体绑定到变量,后面的关系插入语句再引用变量,把实体连接起来。关系实例相当于显式维护的"边"。

对应 SQL 数据:

sql 复制代码
INSERT INTO customer VALUES
  ('C001', '华星制造'),
  ('C002', '远航贸易');

INSERT INTO invoice VALUES
  ('INV-001', 'C001', DATE '2026-01-10', 10000),
  ('INV-002', 'C001', DATE '2026-02-15',  6000),
  ('INV-003', 'C002', DATE '2026-03-01',  8000);

INSERT INTO receipt VALUES
  ('RCP-001', 'INV-001', DATE '2026-01-20', 3000),
  ('RCP-002', 'INV-001', DATE '2026-02-05', 7000),
  ('RCP-003', 'INV-002', DATE '2026-03-01', 2000);

INSERT INTO payment_plan VALUES
  ('PLAN-001', 'INV-001', DATE '2026-02-10', 10000, 'paid'),
  ('PLAN-002', 'INV-002', DATE '2026-03-15',  3000, 'partial'),
  ('PLAN-003', 'INV-002', DATE '2026-04-15',  3000, 'pending'),
  ('PLAN-004', 'INV-003', DATE '2026-04-30',  8000, 'pending');

6. 开始查询

开启只读事务:

text 复制代码
transaction read invoice_demo

交互模式下一次粘贴一条查询。脚本模式用 end; 分隔查询,最后执行 close 关闭事务。

6.1 查询 1:按照发票号点查

typeql 复制代码
match
  $invoice isa invoice,
    has invoice-no "INV-002",
    has invoice-date $date,
    has invoice-amount $amount;
select $date, $amount;

解释:

  • match 描述必须同时成立的模式;
  • $invoice isa invoice 限制对象类型;
  • has invoice-no "INV-002" 用固定属性值过滤;
  • $date$amount 接收匹配到的属性实例;
  • select 只输出指定变量。

对应 SQL:

sql 复制代码
SELECT invoice_date AS date, invoice_amount AS amount
FROM invoice
WHERE invoice_no = 'INV-002';

实际结果:

$date $amount
2026-02-15 6000

6.2 查询 2:从客户查询其全部发票

typeql 复制代码
match
  $customer isa customer, has customer-name "华星制造";
  billing (customer: $customer, invoice: $invoice);
  $invoice has invoice-no $invoice_no, has invoice-amount $amount;
select $invoice_no, $amount;
sort $invoice_no;

解释:先找到客户,再让该客户扮演 billing:customer,发票扮演 billing:invoicesort 按发票号排序。这是 TypeDB 中典型的一对多关系遍历。

对应 SQL:

sql 复制代码
SELECT i.invoice_no, i.invoice_amount AS amount
FROM customer c
JOIN invoice i ON i.customer_id = c.customer_id
WHERE c.customer_name = '华星制造'
ORDER BY i.invoice_no;

实际结果:

$invoice_no $amount
INV-001 10000
INV-002 6000

6.3 查询 3:从收款单反向查询发票和客户

typeql 复制代码
match
  $receipt isa receipt,
    has receipt-no "RCP-003",
    has receipt-amount $received;
  settlement (receipt: $receipt, invoice: $invoice);
  billing (invoice: $invoice, customer: $customer);
  $invoice has invoice-no $invoice_no;
  $customer has customer-name $customer_name;
select $customer_name, $invoice_no, $received;

解释:关系没有只能单向读取的限制。查询从收款单开始,经 settlement 找发票,再经 billing 找客户。角色的书写顺序不表示方向。

对应 SQL:

sql 复制代码
SELECT c.customer_name, i.invoice_no, r.receipt_amount AS received
FROM receipt r
JOIN invoice i  ON i.invoice_no = r.invoice_no
JOIN customer c ON c.customer_id = i.customer_id
WHERE r.receipt_no = 'RCP-003';

实际结果:

$customer_name $invoice_no $received
华星制造 INV-002 2000

6.4 查询 4:按发票聚合已收款金额

typeql 复制代码
match
  $invoice isa invoice, has invoice-no $invoice_no;
  settlement (invoice: $invoice, receipt: $receipt);
  $receipt has receipt-amount $received;
reduce $received_total = sum($received) groupby $invoice_no;
sort $invoice_no;

解释:reduce 对匹配流执行聚合,sum($received) 求和,groupby $invoice_no 按发票号分组。由于普通匹配要求 settlement 存在,没有收款单的 INV-003 不会出现。

对应 SQL:

sql 复制代码
SELECT i.invoice_no, SUM(r.receipt_amount) AS received_total
FROM invoice i
JOIN receipt r ON r.invoice_no = i.invoice_no
GROUP BY i.invoice_no
ORDER BY i.invoice_no;

实际结果:

$invoice_no $received_total
INV-001 10000
INV-002 2000

6.5 查询 5:只查询没有收款单的发票

typeql 复制代码
match
  $invoice isa invoice,
    has invoice-no $invoice_no,
    has invoice-amount $amount;
  not {
    settlement (invoice: $invoice, receipt: $receipt);
  };
select $invoice_no, $amount;

解释:not { ... } 表示括号内的模式不得存在。这里不是"不返回收款单字段",而是只保留不存在任何 settlement 关系的发票。

对应 SQL:

sql 复制代码
SELECT i.invoice_no, i.invoice_amount AS amount
FROM invoice i
WHERE NOT EXISTS (
    SELECT 1
    FROM receipt r
    WHERE r.invoice_no = i.invoice_no
);

实际结果:

$invoice_no $amount
INV-003 8000

6.6 查询 6:用 try 保留没有收款单的发票

typeql 复制代码
match
  $invoice isa invoice, has invoice-no $invoice_no;
  try {
    settlement (invoice: $invoice, receipt: $receipt);
    $receipt has receipt-no $receipt_no, has receipt-amount $received;
  };
select $invoice_no, $receipt_no, $received;
sort $invoice_no;

解释:

  • 外层的发票模式是必须匹配的;
  • try { ... } 中的收款单模式是可选的;
  • 匹配到收款单时绑定 $receipt_no$received
  • 匹配不到时仍保留发票,两变量为空;
  • 一张发票匹配到两张收款单,会产生两行。

它最接近 SQL 的 LEFT JOIN

sql 复制代码
SELECT i.invoice_no, r.receipt_no, r.receipt_amount AS received
FROM invoice i
LEFT JOIN receipt r ON r.invoice_no = i.invoice_no
ORDER BY i.invoice_no;

实际结果:

$invoice_no $receipt_no $received
INV-001 RCP-001 3000
INV-001 RCP-002 7000
INV-002 RCP-003 2000
INV-003
去掉 try 会怎样
typeql 复制代码
match
  $invoice isa invoice, has invoice-no $invoice_no;
  settlement (invoice: $invoice, receipt: $receipt);
  $receipt has receipt-no $receipt_no, has receipt-amount $received;
select $invoice_no, $receipt_no, $received;
sort $invoice_no;

此时所有模式都必须成立,效果类似 INNER JOININV-003 没有收款关系,所以被过滤掉,只剩前三行。

6.7 查询 7:用 fetch 返回嵌套对象

typeql 复制代码
match
  $customer isa customer, has customer-id "C001";
  billing (customer: $customer, invoice: $invoice);
fetch {
  "customer": $customer.customer-name,
  "invoice": $invoice.invoice-no,
  "invoice-amount": $invoice.invoice-amount,
  "receipts": [
    match
      settlement (invoice: $invoice, receipt: $receipt);
    fetch {
      "receipt": $receipt.receipt-no,
      "received": $receipt.receipt-amount
    };
  ]
};

解释:select 返回变量行,fetch 返回文档结构。$customer.customer-name 直接读取实体拥有的属性;方括号中的子查询为每张发票构造收款单数组,适合 API 返回 JSON 风格的数据。

实际结果:

json 复制代码
{
  "customer": "华星制造",
  "invoice": "INV-001",
  "invoice-amount": 10000,
  "receipts": [
    { "receipt": "RCP-001", "received": 3000 },
    { "receipt": "RCP-002", "received": 7000 }
  ]
}
{
  "customer": "华星制造",
  "invoice": "INV-002",
  "invoice-amount": 6000,
  "receipts": [
    { "receipt": "RCP-003", "received": 2000 }
  ]
}

普通 SQL 返回的是平面行,不能直接一一对应。以 PostgreSQL 为例,可使用 JSON 聚合:

sql 复制代码
SELECT jsonb_build_object(
    'customer', c.customer_name,
    'invoice', i.invoice_no,
    'invoice-amount', i.invoice_amount,
    'receipts', COALESCE(
        jsonb_agg(
            jsonb_build_object(
                'receipt', r.receipt_no,
                'received', r.receipt_amount
            )
        ) FILTER (WHERE r.receipt_no IS NOT NULL),
        '[]'::jsonb
    )
) AS document
FROM customer c
JOIN invoice i ON i.customer_id = c.customer_id
LEFT JOIN receipt r ON r.invoice_no = i.invoice_no
WHERE c.customer_id = 'C001'
GROUP BY c.customer_name, i.invoice_no, i.invoice_amount;

6.8 查询 8:客户、发票、收款单、付款计划完整关系链

typeql 复制代码
match
  $customer isa customer, has customer-name $customer_name;
  billing (customer: $customer, invoice: $invoice);
  $invoice has invoice-no $invoice_no,
           has invoice-amount $invoice_amount;
  settlement (invoice: $invoice, receipt: $receipt);
  $receipt has receipt-no $receipt_no,
           has receipt-amount $received;
  invoice-schedule (invoice: $invoice, plan: $plan);
  $plan has plan-id $plan_id,
        has planned-amount $planned,
        has plan-status $status;
select $customer_name, $invoice_no, $invoice_amount,
       $receipt_no, $received, $plan_id, $planned, $status;
sort $invoice_no, $receipt_no, $plan_id;

对应 SQL:

sql 复制代码
SELECT c.customer_name,
       i.invoice_no,
       i.invoice_amount,
       r.receipt_no,
       r.receipt_amount AS received,
       p.plan_id,
       p.planned_amount AS planned,
       p.plan_status AS status
FROM customer c
JOIN invoice i      ON i.customer_id = c.customer_id
JOIN receipt r      ON r.invoice_no = i.invoice_no
JOIN payment_plan p ON p.invoice_no = i.invoice_no
ORDER BY i.invoice_no, r.receipt_no, p.plan_id;

实际结果:

客户 发票 发票金额 收款单 收款金额 计划 计划金额 状态
华星制造 INV-001 10000 RCP-001 3000 PLAN-001 10000 paid
华星制造 INV-001 10000 RCP-002 7000 PLAN-001 10000 paid
华星制造 INV-002 6000 RCP-003 2000 PLAN-002 3000 partial
华星制造 INV-002 6000 RCP-003 2000 PLAN-003 3000 pending

这里要注意:收款单和付款计划是两组独立的一对多关系。当同一发票有 2 张收款单和 3 个付款计划时,平面结果会有 2 × 3 = 6 行。TypeQL 与 SQL JOIN 都会出现这种组合膨胀。只想获得层次结构时应使用嵌套 fetch,分别返回收款单数组和付款计划数组。

6.9 查询 9:查询客户尚未完成的付款计划

typeql 复制代码
match
  $customer isa customer, has customer-name "华星制造";
  billing (customer: $customer, invoice: $invoice);
  $invoice has invoice-no $invoice_no;
  invoice-schedule (invoice: $invoice, plan: $plan);
  $plan has plan-id $plan_id,
        has due-date $due,
        has plan-status $status;
  not { $status == "paid"; };
select $invoice_no, $plan_id, $due, $status;
sort $invoice_no, $due;

这里的 not { $status == "paid"; } 否定的是值比较,即保留状态不为 paid 的计划。

对应 SQL:

sql 复制代码
SELECT i.invoice_no, p.plan_id, p.due_date, p.plan_status
FROM customer c
JOIN invoice i      ON i.customer_id = c.customer_id
JOIN payment_plan p ON p.invoice_no = i.invoice_no
WHERE c.customer_name = '华星制造'
  AND p.plan_status <> 'paid'
ORDER BY i.invoice_no, p.due_date;

实际结果:

发票 计划 到期日 状态
INV-002 PLAN-002 2026-03-15 partial
INV-002 PLAN-003 2026-04-15 pending

6.10 查询 10:从收款单反查客户,再横向展开该客户的全部付款计划

这条查询不是沿一条链一直向前,而是先反向追溯,再以客户为中心展开另一条分支:

text 复制代码
RCP-003
  → 原发票 INV-002
  → 客户华星制造
  → 该客户的所有发票
  → 每张发票的付款计划
typeql 复制代码
match
  $source_receipt isa receipt, has receipt-no "RCP-003";
  settlement (receipt: $source_receipt, invoice: $source_invoice);
  billing (invoice: $source_invoice, customer: $customer);
  $source_invoice has invoice-no $source_invoice_no;
  $customer has customer-name $customer_name;

  billing (customer: $customer, invoice: $related_invoice);
  $related_invoice has invoice-no $related_invoice_no;
  invoice-schedule (invoice: $related_invoice, plan: $plan);
  $plan has plan-id $plan_id, has plan-status $status;

select $customer_name, $source_invoice_no,
       $related_invoice_no, $plan_id, $status;
sort $related_invoice_no, $plan_id;

解释:

  • $source_invoiceRCP-003 原本关联的发票;
  • 通过第一条 billing 找到该发票的客户;
  • 第二次使用 billing,从同一个客户匹配 $related_invoice
  • $source_invoice$related_invoice 是两个不同用途的变量,但它们可以绑定同一个发票,因此结果中也包含 INV-002
  • 最后通过 invoice-schedule 展开每张相关发票的付款计划。

对应 SQL 需要为 invoice 使用两个别名:

sql 复制代码
SELECT c.customer_name,
       source_i.invoice_no AS source_invoice_no,
       related_i.invoice_no AS related_invoice_no,
       p.plan_id,
       p.plan_status AS status
FROM receipt source_r
JOIN invoice source_i
  ON source_i.invoice_no = source_r.invoice_no
JOIN customer c
  ON c.customer_id = source_i.customer_id
JOIN invoice related_i
  ON related_i.customer_id = c.customer_id
JOIN payment_plan p
  ON p.invoice_no = related_i.invoice_no
WHERE source_r.receipt_no = 'RCP-003'
ORDER BY related_i.invoice_no, p.plan_id;

实际结果:

客户 来源发票 相关发票 付款计划 状态
华星制造 INV-002 INV-001 PLAN-001 paid
华星制造 INV-002 INV-002 PLAN-002 partial
华星制造 INV-002 INV-002 PLAN-003 pending

这是一个多跳查询,但不是递归查询,因为跳数和关系类型在查询中已经固定。

6.11 查询 11:有待付款计划,但完全没有收款单的发票

typeql 复制代码
match
  $customer isa customer, has customer-name $customer_name;
  billing (customer: $customer, invoice: $invoice);
  $invoice has invoice-no $invoice_no;
  invoice-schedule (invoice: $invoice, plan: $plan);
  $plan has plan-id $plan_id, has plan-status "pending";
  not {
    settlement (invoice: $invoice, receipt: $any_receipt);
  };
select $customer_name, $invoice_no, $plan_id;
sort $invoice_no, $plan_id;

解释:

  • 正向关系链负责找到"客户 → 发票 → pending 付款计划";
  • not 是关联否定模式,针对当前 $invoice 检查是否不存在任何收款单;
  • 只有两个条件同时满足的发票才会返回。

对应 SQL:

sql 复制代码
SELECT c.customer_name, i.invoice_no, p.plan_id
FROM customer c
JOIN invoice i
  ON i.customer_id = c.customer_id
JOIN payment_plan p
  ON p.invoice_no = i.invoice_no
WHERE p.plan_status = 'pending'
  AND NOT EXISTS (
      SELECT 1
      FROM receipt r
      WHERE r.invoice_no = i.invoice_no
  )
ORDER BY i.invoice_no, p.plan_id;

实际结果:

客户 发票 付款计划
远航贸易 INV-003 PLAN-004

6.12 查询 12:跨客户、发票和收款单,按客户聚合

typeql 复制代码
match
  $customer isa customer, has customer-name $customer_name;
  billing (customer: $customer, invoice: $invoice);
  settlement (invoice: $invoice, receipt: $receipt);
  $receipt has receipt-amount $received;
reduce
  $received_total = sum($received),
  $receipt_count = count
groupby $customer_name;
sort $customer_name;

解释:

  • match 先生成"客户---发票---收款单"匹配流;
  • groupby $customer_name 把属于同一个客户的所有匹配行归为一组;
  • sum($received) 汇总收款金额;
  • count 统计每组匹配到的收款单数量;
  • 没有收款单的客户无法满足普通 settlement 匹配,因此不会产生分组。

对应 SQL:

sql 复制代码
SELECT c.customer_name,
       SUM(r.receipt_amount) AS received_total,
       COUNT(*) AS receipt_count
FROM customer c
JOIN invoice i
  ON i.customer_id = c.customer_id
JOIN receipt r
  ON r.invoice_no = i.invoice_no
GROUP BY c.customer_name
ORDER BY c.customer_name;

实际结果:

客户 收款总额 收款次数
华星制造 12000 3

这里的 12000 来自 INV-001 的 3000、7000,以及 INV-002 的 2000。

6.13 查询 13:多层 fetch 同时返回收款单数组和付款计划数组

typeql 复制代码
match
  $customer isa customer, has customer-id "C001";
fetch {
  "customer": $customer.customer-name,
  "invoices": [
    match
      billing (customer: $customer, invoice: $invoice);
    fetch {
      "invoice": $invoice.invoice-no,
      "amount": $invoice.invoice-amount,
      "receipts": [
        match
          settlement (invoice: $invoice, receipt: $receipt);
        fetch {
          "receipt": $receipt.receipt-no,
          "received": $receipt.receipt-amount
        };
      ],
      "plans": [
        match
          invoice-schedule (invoice: $invoice, plan: $plan);
        fetch {
          "plan": $plan.plan-id,
          "due": $plan.due-date,
          "planned": $plan.planned-amount,
          "status": $plan.plan-status
        };
      ]
    };
  ]
};

解释:

  • 第一层以客户为根对象;
  • 第二层相关子查询使用外层 $customer,生成发票数组;
  • 每个发票对象内部又有两个相关子查询,分别生成收款单数组和付款计划数组;
  • 两个数组分别计算,不会产生"收款单数量 × 付款计划数量"的平面 JOIN 膨胀;
  • 这仍然不是递归,而是预先写明的固定三层结构。

实际结果:

json 复制代码
{
  "customer": "华星制造",
  "invoices": [
    {
      "invoice": "INV-001",
      "amount": 10000,
      "receipts": [
        { "receipt": "RCP-001", "received": 3000 },
        { "receipt": "RCP-002", "received": 7000 }
      ],
      "plans": [
        {
          "plan": "PLAN-001",
          "due": "2026-02-10",
          "planned": 10000,
          "status": "paid"
        }
      ]
    },
    {
      "invoice": "INV-002",
      "amount": 6000,
      "receipts": [
        { "receipt": "RCP-003", "received": 2000 }
      ],
      "plans": [
        {
          "plan": "PLAN-002",
          "due": "2026-03-15",
          "planned": 3000,
          "status": "partial"
        },
        {
          "plan": "PLAN-003",
          "due": "2026-04-15",
          "planned": 3000,
          "status": "pending"
        }
      ]
    }
  ]
}

以 PostgreSQL 为例,对应思路是为每个层级使用相关子查询和 jsonb_agg

sql 复制代码
SELECT jsonb_build_object(
    'customer', c.customer_name,
    'invoices', (
        SELECT COALESCE(jsonb_agg(
            jsonb_build_object(
                'invoice', i.invoice_no,
                'amount', i.invoice_amount,
                'receipts', (
                    SELECT COALESCE(jsonb_agg(
                        jsonb_build_object(
                            'receipt', r.receipt_no,
                            'received', r.receipt_amount
                        )
                    ), '[]'::jsonb)
                    FROM receipt r
                    WHERE r.invoice_no = i.invoice_no
                ),
                'plans', (
                    SELECT COALESCE(jsonb_agg(
                        jsonb_build_object(
                            'plan', p.plan_id,
                            'due', p.due_date,
                            'planned', p.planned_amount,
                            'status', p.plan_status
                        )
                    ), '[]'::jsonb)
                    FROM payment_plan p
                    WHERE p.invoice_no = i.invoice_no
                )
            )
        ), '[]'::jsonb)
        FROM invoice i
        WHERE i.customer_id = c.customer_id
    )
) AS document
FROM customer c
WHERE c.customer_id = 'C001';

7. TypeQL 和 SQL 的思维差异

需求 SQL TypeQL
点查 WHERE primary_key = ... has key-attribute ...
多表关联 指定表及 JOIN ... ON ... 声明实体共同参与某种关系
一对多 多方表保存外键 插入一条关系实例连接双方
反向查询 沿外键反向 JOIN 使用同一关系模式,角色顺序不限
内连接 INNER JOIN 普通 match 中的关系模式
左连接 LEFT JOIN try { ... } 可选模式
不存在关联 NOT EXISTS not { ... }
聚合 GROUP BY + SUM reduce ... groupby ...
JSON/嵌套结果 数据库特定 JSON 函数 fetch 和嵌套子查询

TypeDB 并不会根据两个属性值相等自动认定业务关系。要表达客户与发票、发票与收款单之间的业务关联,应插入相应关系实例;也可以在查询时临时用属性值条件做匹配,但那只是动态等值匹配,不是数据库中持久化的关系。