TypeDB示例
本文使用 TypeDB 3.12.1 / TypeQL 3,完整演示以下业务:
- 一个客户可以有多张发票;
- 一张发票可以有多张收款单;
- 一张发票可以有多个付款计划;
- 从任意对象出发查询关联对象;
- 点查、关系遍历、反向遍历、聚合、否定匹配、可选匹配和嵌套结果;
- 每段 TypeQL 对应的关系型数据库建模和 SQL。
1. 业务模型
text
customer ──billing──> invoice ──settlement──────> receipt
└──invoice-schedule──> payment-plan
在 TypeDB 中,customer、invoice、receipt 和 payment-plan 是实体类型,三条连接是关系类型。关系中的端点不是"起点/终点",而是带名称的角色:
| TypeDB 关系 | 角色 | 业务含义 | 简化后的 SQL 表示 |
|---|---|---|---|
billing |
customer、invoice |
发票属于哪个客户 | invoice.customer_id 外键 |
settlement |
invoice、receipt |
收款单冲销哪张发票 | receipt.invoice_no 外键 |
invoice-schedule |
invoice、plan |
发票包含哪些付款计划 | 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 KEY 或 UNIQUE 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:invoice。sort 按发票号排序。这是 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 JOIN。INV-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_invoice是RCP-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 并不会根据两个属性值相等自动认定业务关系。要表达客户与发票、发票与收款单之间的业务关联,应插入相应关系实例;也可以在查询时临时用属性值条件做匹配,但那只是动态等值匹配,不是数据库中持久化的关系。