MySQL 数据不迁移,SPARQL 为什么还能查?深入 Ontop 的 R2RML Mapping 与 SPARQL→SQL
前面介绍 Ontop 时,我们说过一句很有意思的话:
不用把 MySQL 数据搬到图数据库,也可以把它“变成”知识图谱。
第一次看到这里,很多人会有疑问:
数据明明还在 MySQL
↓
为什么可以使用 SPARQL 查询?
↓
RDF 三元组到底存在哪里?
↓
SPARQL 最后是谁执行的?
答案就是 Ontop 最核心的一项技术:

Mapping。
更具体一点:
R2RML Mapping
它负责把:
关系数据库
中的:
表
字段
主键
外键
映射成知识图谱里的:
实体
IRI
Class
Property
Triple
最终 Ontop 再把:
SPARQL
改写成:
SQL
发送给真正的 MySQL 执行。
整个过程:
SPARQL
↓
Ontology / Mapping
↓
Ontop Query Rewriting
↓
SQL
↓
MySQL
↓
ResultSet
↓
RDF / SPARQL Result
这篇我们不泛讲 Ontop。
就深入拆一个东西:
R2RML 到底是怎么把 MySQL 表“变成”知识图谱的?
项目:
https://github.com/ontop/ontop
一、先准备一个普通得不能再普通的 MySQL
假设我们现在有一个公司后台。
三张表:
employee
department
skill
外加一张员工技能关系表:
employee_skill
数据库:
CREATE DATABASE company;
USE company;
部门表:
CREATE TABLE department (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
员工表:
CREATE TABLE employee (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT,
department_id BIGINT,
CONSTRAINT fk_employee_department
FOREIGN KEY (department_id)
REFERENCES department(id)
);
技能表:
CREATE TABLE skill (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
员工技能关系:
CREATE TABLE employee_skill (
employee_id BIGINT NOT NULL,
skill_id BIGINT NOT NULL,
PRIMARY KEY (
employee_id,
skill_id
),
FOREIGN KEY (employee_id)
REFERENCES employee(id),
FOREIGN KEY (skill_id)
REFERENCES skill(id)
);
加入一些数据:
INSERT INTO department
(id, name)
VALUES
(1, '研发部'),
(2, '产品部');
员工:
INSERT INTO employee
(id, name, age, department_id)
VALUES
(1001, '张三', 28, 1),
(1002, '李四', 26, 1),
(1003, '王五', 30, 2);
技能:
INSERT INTO skill
(id, name)
VALUES
(1, 'Go'),
(2, 'Java'),
(3, 'Redis'),
(4, 'Docker');
关系:
INSERT INTO employee_skill
(employee_id, skill_id)
VALUES
(1001, 1),
(1001, 3),
(1001, 4),
(1002, 2),
(1002, 3);
这就是一个非常普通的业务数据库。
二、如果不用知识图谱,我们怎么查询?
查询:
张三属于哪个部门?
SQL:
SELECT
e.name,
d.name AS department_name
FROM employee e
JOIN department d
ON e.department_id = d.id
WHERE e.name = '张三';
查询:
哪些员工会 Redis?
SELECT
e.name
FROM employee e
JOIN employee_skill es
ON e.id = es.employee_id
JOIN skill s
ON es.skill_id = s.id
WHERE s.name = 'Redis';
查询:
哪些研发部员工同时会 Go?
SELECT
e.name
FROM employee e
JOIN department d
ON e.department_id = d.id
JOIN employee_skill es
ON e.id = es.employee_id
JOIN skill s
ON es.skill_id = s.id
WHERE
d.name = '研发部'
AND s.name = 'Go';
完全没问题。
但现在如果我们希望对外暴露一个统一的:
Knowledge Graph
让用户不关心:
employee
department
employee_skill
skill
这些表。
而只看到:
Employee
Department
Skill
Employee belongsToDepartment Department
Employee hasSkill Skill
怎么办?
这就是 Ontop。
三、第一步不是搬数据,而是定义“虚拟世界”
我们希望数据库里的:
employee.id = 1001
在知识图谱里变成:
http://example.com/employee/1001
希望:
employee.name
变成:
ex:name
希望:
department
变成:
ex:Department
希望:
employee.department_id
代表:
ex:belongsToDepartment
所以最终我们想看到的是:
employee 表
↓
Mapping
↓
<employee/1001>
a ex:Employee ;
ex:name "张三" ;
ex:age 28 ;
ex:belongsToDepartment
<department/1> .
注意:
这些 RDF 不一定真的被写入另一个数据库。
这就是“Virtual Knowledge Graph”的关键。
它们可以只是:
通过 Mapping 规则虚拟出来的 RDF 视图。
四、R2RML 最核心的概念:TriplesMap
R2RML 中非常重要的一个东西叫:
rr:TriplesMap
它干什么?
一句话:
规定一张关系表中的每一行,应该怎么生成 RDF Triple。
你可以把它理解成一个:
Row → RDF
转换器。
最典型结构:
TriplesMap
│
├── logicalTable
│
├── subjectMap
│
└── predicateObjectMap
翻译成人话:
从哪张表取数据?
↓
这一行数据对应哪个 RDF 实体?
↓
这一行有哪些字段要变成 RDF 属性?
五、先映射 employee 表
创建:
mapping.ttl
先定义 Prefix:
@prefix rr:
<http://www.w3.org/ns/r2rml#> .
@prefix rdf:
<http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xsd:
<http://www.w3.org/2001/XMLSchema#> .
@prefix ex:
<http://example.com/> .
然后:
<#EmployeeMapping>
a rr:TriplesMap ;
rr:logicalTable [
rr:tableName "employee"
] ;
这里最重要:
rr:logicalTable
表示:
这个 Mapping 从哪里取数据?
现在答案是:
employee
也就是:
SELECT *
FROM employee;
可以把它理解成 Mapping 的:
数据源。
六、subjectMap:数据库的一行到底是谁?
现在有一行:
id = 1001
name = 张三
age = 28
department_id = 1
知识图谱不能只叫:
1001
最好给它一个全局 IRI:
http://example.com/employee/1001
所以:
rr:subjectMap [
rr:template
"http://example.com/employee/{id}" ;
rr:class ex:Employee
] ;
这里:
rr:template
特别重要。
它相当于:
"http://example.com/employee/"
+
id
如果:
id = 1001
生成:
http://example.com/employee/1001
如果:
id = 1002
生成:
http://example.com/employee/1002
七、rr:class 做了什么?
这里:
rr:class ex:Employee
表示这个 Subject:
是 Employee 类型。
于是:
employee.id = 1001
会虚拟生成:
<http://example.com/employee/1001>
rdf:type ex:Employee .
换句话说:
数据库行
开始变成:
知识图谱实体
了。
八、把 name 字段映射成 ex:name
继续:
rr:predicateObjectMap [
rr:predicate ex:name ;
rr:objectMap [
rr:column "name"
]
] ;
什么意思?
非常简单。
Predicate
=
ex:name
Object 来自:
employee.name
所以数据库:
id name
1001 张三
变成:
<employee/1001>
ex:name
"张三"
九、把 age 也映射出来
rr:predicateObjectMap [
rr:predicate ex:age ;
rr:objectMap [
rr:column "age" ;
rr:datatype xsd:integer
]
] ;
生成:
<http://example.com/employee/1001>
ex:age
28 .
现在一个数据库 Row:
1001
张三
28
1
已经可以虚拟成:
<employee/1001>
a ex:Employee ;
ex:name
"张三" ;
ex:age
28 .
十、完整 Employee Mapping
组合起来:
@prefix rr:
<http://www.w3.org/ns/r2rml#> .
@prefix ex:
<http://example.com/> .
@prefix xsd:
<http://www.w3.org/2001/XMLSchema#> .
<#EmployeeMapping>
a rr:TriplesMap ;
rr:logicalTable [
rr:tableName "employee"
] ;
rr:subjectMap [
rr:template
"http://example.com/employee/{id}" ;
rr:class
ex:Employee
] ;
rr:predicateObjectMap [
rr:predicate
ex:name ;
rr:objectMap [
rr:column "name"
]
] ;
rr:predicateObjectMap [
rr:predicate
ex:age ;
rr:objectMap [
rr:column "age" ;
rr:datatype
xsd:integer
]
] .
到这里先记住三个核心:
rr:logicalTable
=
从哪取
rr:subjectMap
=
这一行是谁
rr:predicateObjectMap
=
这一行有哪些属性
R2RML 最基础的结构其实就这么简单。
十一、再映射 Department
数据库:
department
id
name
Mapping:
<#DepartmentMapping>
a rr:TriplesMap ;
rr:logicalTable [
rr:tableName "department"
] ;
rr:subjectMap [
rr:template
"http://example.com/department/{id}" ;
rr:class
ex:Department
] ;
rr:predicateObjectMap [
rr:predicate
ex:name ;
rr:objectMap [
rr:column "name"
]
] .
于是:
department
1
研发部
虚拟为:
<http://example.com/department/1>
a ex:Department ;
ex:name
"研发部" .
十二、最关键的问题来了:两个表怎么连接?
现在我们已经有:
Employee
以及:
Department
但是:
张三
和:
研发部
还没有关系。
数据库是通过:
employee.department_id
连接:
department.id
也就是:
employee.department_id
=
department.id
在 RDF 里希望得到:
张三
↓
belongsToDepartment
↓
研发部
怎么映射?
十三、最简单的写法:直接使用 URI Template
因为:
employee.department_id
已经有 Department ID。
所以可以直接:
rr:predicateObjectMap [
rr:predicate
ex:belongsToDepartment ;
rr:objectMap [
rr:template
"http://example.com/department/{department_id}" ;
rr:termType
rr:IRI
]
] ;
最终:
department_id = 1
生成:
http://example.com/department/1
于是:
<http://example.com/employee/1001>
ex:belongsToDepartment
<http://example.com/department/1> .
图就连接起来了。
十四、这一步非常重要
数据库里原来是:
employee.department_id = 1
只是一个:
数字外键
Mapping 以后变成:
Employee
↓
belongsToDepartment
↓
Department
也就是说:
Mapping 真正完成的是“关系数据库语义化”。
不是简单把:
Column A
改个名字叫:
Property A
而是把:
ID
外键
关系表
转化成了:
实体
关系
语义
十五、R2RML 还有一种正式的跨表关联方式
除了直接使用:
URI Template
还可以:
rr:parentTriplesMap
配合:
rr:joinCondition
例如:
rr:predicateObjectMap [
rr:predicate
ex:belongsToDepartment ;
rr:objectMap [
rr:parentTriplesMap
<#DepartmentMapping> ;
rr:joinCondition [
rr:child
"department_id" ;
rr:parent
"id"
]
]
] ;
翻译一下。
当前:
EmployeeMapping
是 Child。
Department:
DepartmentMapping
是 Parent。
Join:
employee.department_id
=
department.id
也就是:
JOIN department d
ON employee.department_id = d.id
十六、为什么 parentTriplesMap 很重要?
因为有时候对象的 URI 生成规则很复杂。
你不希望在每个地方复制:
http://example.com/department/{id}
而是直接告诉 Ontop:
Object 请使用
DepartmentMapping的 Subject。
于是:
DepartmentMapping
负责定义:
Department 的 IRI 怎么生成
而:
EmployeeMapping
只负责:
怎么关联过去
这种结构更加规范。
十七、再映射 Skill
技能表:
skill
id
name
Mapping:
<#SkillMapping>
a rr:TriplesMap ;
rr:logicalTable [
rr:tableName "skill"
] ;
rr:subjectMap [
rr:template
"http://example.com/skill/{id}" ;
rr:class
ex:Skill
] ;
rr:predicateObjectMap [
rr:predicate
ex:name ;
rr:objectMap [
rr:column "name"
]
] .
现在:
skill.id = 1
name = Go
虚拟生成:
<http://example.com/skill/1>
a ex:Skill ;
ex:name
"Go" .
十八、多对多关系怎么做?
这个更有意思。
数据库:
employee_skill
里面:
employee_id
skill_id
例如:
1001 1
表示:
张三会 Go
那这个关系表如何变成:
Employee
↓
hasSkill
↓
Skill
?
我们创建:
<#EmployeeSkillMapping>
a rr:TriplesMap ;
rr:logicalTable [
rr:tableName
"employee_skill"
] ;
Subject:
rr:subjectMap [
rr:template
"http://example.com/employee/{employee_id}"
] ;
然后 Object:
rr:predicateObjectMap [
rr:predicate
ex:hasSkill ;
rr:objectMap [
rr:template
"http://example.com/skill/{skill_id}" ;
rr:termType
rr:IRI
]
] .
就这么简单。
十九、关系表甚至可以“消失”
注意。
数据库里存在:
employee_skill
表。
但是知识图谱里我们不一定需要:
EmployeeSkill
这个实体。
它可以直接转化成:
Employee
|
| hasSkill
↓
Skill
比如:
1001 1
转:
<employee/1001>
ex:hasSkill
<skill/1> .
这就是一个很值得理解的点:
数据库 Schema 和 Knowledge Graph Schema 不需要长得一样。
你不是在机械复制数据库。
而是在做:
Semantic Mapping。
二十、现在数据库已经“变成”了一张图
原来的:
employee
department
skill
employee_skill
变成:
Go
↑
│
hasSkill
│
研发部 ← 张三 ─────→ Redis
↑ │
│ └────→ Docker
│
belongsToDepartment
但是再次强调:
RDF 数据并没有真的搬走。
原始数据依然是:
MySQL
Ontop 只是在上面构建了一个:
Virtual RDF Graph
二十一、现在开始用 SPARQL 查
比如查询:
所有 Employee。
PREFIX ex:
<http://example.com/>
SELECT ?employee
WHERE {
?employee
a
ex:Employee .
}
用户看到:
SPARQL
但 Ontop 并不会去某个:
RDF 数据库
查。
它会看 Mapping。
二十二、Ontop 会发现什么?
SPARQL:
?employee
a
ex:Employee .
Mapping 告诉 Ontop:
ex:Employee
来自:
EmployeeMapping
而 EmployeeMapping 又告诉它:
logicalTable
=
employee
于是它可以推导:
要回答这个问题
↓
查询 employee 表即可
所以最终 SQL 大致相当于:
SELECT id
FROM employee;
然后:
id = 1001
再根据:
rr:template
"http://example.com/employee/{id}"
组装:
http://example.com/employee/1001
返回 SPARQL 客户端。
二十三、这个过程就是 SPARQL → SQL
整个链路:
SPARQL
?employee a ex:Employee
↓
查 Mapping
ex:Employee
来自 employee 表
↓
生成 SQL
SELECT id
FROM employee
↓
MySQL
1001
1002
1003
↓
套 URI Template
employee/1001
employee/1002
employee/1003
↓
SPARQL Result
这就是 Ontop 的核心魔法。
其实没有魔法。
就是:
Query Rewriting。
二十四、查询姓名会发生什么?
SPARQL:
PREFIX ex:
<http://example.com/>
SELECT ?employee ?name
WHERE {
?employee
a ex:Employee ;
ex:name
?name .
}
Ontop 从 Mapping 得知:
Employee
→ employee 表
ex:name
→ name 字段
大致对应 SQL:
SELECT
id,
name
FROM employee;
然后转换成:
employee/1001
张三
employee/1002
李四
employee/1003
王五
二十五、FILTER 怎么处理?
SPARQL:
PREFIX ex:
<http://example.com/>
SELECT ?name ?age
WHERE {
?employee
a ex:Employee ;
ex:name
?name ;
ex:age
?age .
FILTER(?age >= 28)
}
如果 Ontop 能够下推这个条件,那么最终 SQL 大致就是:
SELECT
name,
age
FROM employee
WHERE age >= 28;
这非常重要。
不是:
SELECT 全表
↓
Java 内存
↓
再判断 age
而是尽量把工作交给:
数据库。
这也是 Virtual Knowledge Graph 能用于真实数据库的重要基础。
二十六、跨表 SPARQL 怎么办?
现在查询:
每个员工属于哪个部门?
PREFIX ex:
<http://example.com/>
SELECT
?employeeName
?departmentName
WHERE {
?employee
a ex:Employee ;
ex:name
?employeeName ;
ex:belongsToDepartment
?department .
?department
ex:name
?departmentName .
}
SPARQL 看不到:
department_id
更看不到:
JOIN
但是 Mapping 知道。
于是 Ontop 可以把图模式:
Employee
↓
belongsToDepartment
↓
Department
还原成数据库关系:
employee.department_id
=
department.id
大致生成:
SELECT
e.name,
d.name
FROM employee e
JOIN department d
ON e.department_id = d.id;
这就是非常关键的一层:
SPARQL 查询的是语义,Mapping 负责寻找数据库实现。
二十七、多对多查询更明显
查询:
哪些员工会 Redis?
SPARQL:
PREFIX ex:
<http://example.com/>
SELECT ?employeeName
WHERE {
?employee
a ex:Employee ;
ex:name
?employeeName ;
ex:hasSkill
?skill .
?skill
ex:name
"Redis" .
}
用户完全不用知道:
employee_skill
表存在。
但是 Ontop Mapping 知道:
Employee
↓
employee_skill
↓
Skill
所以实际 SQL 大致:
SELECT
e.name
FROM employee e
JOIN employee_skill es
ON e.id = es.employee_id
JOIN skill s
ON es.skill_id = s.id
WHERE s.name = 'Redis';
这就是 VKG 一个很大的价值:
把数据库物理结构藏起来,让上层只面对领域语义。
二十八、换数据库 Schema,上层 SPARQL甚至可以不改
这个才是真正有意思的地方。
假设旧系统:
employee
department
新系统表改成:
sys_user
sys_dept
字段也变了:
employee.id
变成:
sys_user.user_code
如果直接依赖 SQL:
所有查询都可能需要改。
但如果上层始终查询:
?employee
ex:belongsToDepartment
?department .
理论上我们只需要调整:
Mapping。
上层 SPARQL 的领域模型可以继续保持。
这就是:
数据库 Schema
与:
Knowledge Model
之间增加了一层隔离。
二十九、logicalTable 不一定是一张表
这是 R2RML 非常实用的一点。
我们前面写:
rr:logicalTable [
rr:tableName "employee"
] ;
但:
logicalTable
也可以来自:
SQL Query。
例如数据库数据比较复杂。
我们先写:
SELECT
e.id,
e.name,
e.age,
d.name AS department_name
FROM employee e
JOIN department d
ON e.department_id = d.id;
可以直接放进 R2RML:
rr:logicalTable [
rr:sqlQuery """
SELECT
e.id,
e.name,
e.age,
d.name AS department_name
FROM employee e
JOIN department d
ON e.department_id = d.id
"""
] ;
这叫:
R2RML View
三十、为什么 SQL Query Mapping 很有用?
比如数据库里:
first_name
last_name
但 Ontology 希望:
fullName
你可以直接:
SELECT
id,
CONCAT(
first_name,
' ',
last_name
) AS full_name
FROM employee;
然后 Mapping:
rr:predicateObjectMap [
rr:predicate
ex:fullName ;
rr:objectMap [
rr:column
"full_name"
]
] ;
所以 Mapping 不只是:
字段改名器
它还可以使用 SQL:
JOIN
CASE WHEN
CONCAT
COUNT
GROUP BY
构造更适合知识模型的数据。
三十一、比如计算“部门人数”
数据库里根本没有:
staffCount
字段。
我们可以:
SELECT
d.id,
d.name,
COUNT(e.id) AS staff_count
FROM department d
LEFT JOIN employee e
ON e.department_id = d.id
GROUP BY
d.id,
d.name;
R2RML:
<#DepartmentStatsMapping>
a rr:TriplesMap ;
rr:logicalTable [
rr:sqlQuery """
SELECT
d.id,
d.name,
COUNT(e.id)
AS staff_count
FROM department d
LEFT JOIN employee e
ON e.department_id = d.id
GROUP BY
d.id,
d.name
"""
] ;
rr:subjectMap [
rr:template
"http://example.com/department/{id}" ;
] ;
rr:predicateObjectMap [
rr:predicate
ex:staffCount ;
rr:objectMap [
rr:column
"staff_count" ;
rr:datatype
xsd:integer
]
] .
于是知识图谱里可以出现:
研发部
↓
staffCount
↓
2
虽然数据库原表里根本没有这个字段。
三十二、所以“虚拟”不是“简单”
很多人听到:
Virtual Knowledge Graph
会觉得:
是不是只是给 SQL 包一层 SPARQL?
其实 Mapping 可以做很多事情:
表
↓
Class
主键
↓
IRI
字段
↓
Literal Property
外键
↓
Object Property
关系表
↓
Graph Edge
SQL View
↓
虚拟属性 / 虚拟实体
这已经是一层:
Semantic Abstraction。
三十三、Ontop 还支持自己的 .obda 格式
除了标准:
R2RML .ttl
Ontop 还有一个更简洁的:
.obda
格式。
同样的员工映射可以写成类似:
[PrefixDeclaration]
ex:
http://example.com/
[MappingDeclaration] @collection [[
mappingId
EmployeeMapping
target
<http://example.com/employee/{id}>
a ex:Employee ;
ex:name {name} ;
ex:age {age} .
source
SELECT
id,
name,
age
FROM employee
]]
这个格式很好理解。
核心就三个东西:
mappingId
target
source
三十四、source 和 target 是什么?
source:
SELECT
id,
name,
age
FROM employee
表示:
数据从哪里来。
target:
<employee/{id}>
a Employee ;
name {name} ;
age {age}
表示:
这些 SQL 结果在 RDF 世界里长什么样。
所以整个 Mapping 可以直接理解:
SQL Result
↓
Mapping
↓
RDF Template
三十五、R2RML 和 OBDA 怎么选?
如果你希望:
标准化
跨工具
兼容 W3C
优先:
R2RML
如果:
主要就是使用 Ontop
希望文件容易阅读和手写
.obda 通常更加直观。
Ontop CLI 也提供:
R2RML
↔
OBDA
转换能力。
所以两个都可以学。
但如果要真正理解:
关系数据库是怎么映射成 RDF 的
我还是建议先理解:
R2RML
三十六、实际跑起来:MySQL 配置
例如:
company.properties
写:
jdbc.url=jdbc:mysql://localhost:3306/company?useCursorFetch=true
jdbc.user=root
jdbc.password=123456
jdbc.driver=com.mysql.cj.jdbc.Driver
然后需要准备对应:
MySQL JDBC Driver
给 Ontop 使用。
对于 MySQL,大数据查询时尤其值得注意:
useCursorFetch=true
否则 JDBC 可能先把整个结果集取回客户端,再继续处理。
三十七、启动 Ontop Endpoint
假设目录:
input/
├── mapping.ttl
└── company.properties
使用 Ontop CLI:
ontop endpoint \
--mapping=input/mapping.ttl \
--properties=input/company.properties
如果你还有:
ontology.ttl
可以再增加:
ontop endpoint \
--ontology=input/ontology.ttl \
--mapping=input/mapping.ttl \
--properties=input/company.properties
默认:
8080
启动。
SPARQL Endpoint:
/sparql
三十八、查询接口已经和 MySQL 解耦了
现在其他程序只需要:
SPARQL Endpoint
不需要:
MySQL Username
MySQL Password
表结构
JOIN 关系
例如 Python:
import requests
query = """
PREFIX ex:
<http://example.com/>
SELECT ?name
WHERE {
?employee
a ex:Employee ;
ex:name ?name .
}
"""
response = requests.get(
"http://localhost:8080/sparql",
params = {
"query": query
},
headers = {
"Accept":
"application/sparql-results+json"
}
)
print(
response.json()
)
三十九、再封装一个 Python 查询函数
import requests
ENDPOINT = (
"http://localhost:8080/sparql"
)
def query_sparql(
sparql: str
):
response = requests.get(
ENDPOINT,
params = {
"query": sparql
},
headers = {
"Accept":
"application/sparql-results+json"
},
timeout = 30
)
response.raise_for_status()
return response.json()
查询会 Go 的员工:
sparql = """
PREFIX ex:
<http://example.com/>
SELECT ?name
WHERE {
?employee
ex:name ?name ;
ex:hasSkill
?skill .
?skill
ex:name
"Go" .
}
"""
result = query_sparql(
sparql
)
for row in (
result["results"]["bindings"]
):
print(
row["name"]["value"]
)
结果:
张三
但 Python 从头到尾:
没写一行 SQL。
四十、怎么知道 Ontop 到底生成了什么 SQL?
这个问题特别适合学习 Ontop。
Ontop Endpoint 有开发模式。
启动时可以启用:
Development Mode
在这个模式下,它提供:
/ontop/reformulate
用于查看:
SPARQL 被改写成了什么 SQL。
例如请求:
SPARQL:
SELECT ?name
WHERE {
?e ex:name ?name .
}
你可以直接看到 Ontop reformulate 后的 SQL。
这对于学习:
SPARQL
↓
Mapping
↓
SQL
整个过程特别有价值。
四十一、这也是排查性能问题最重要的入口之一
如果你写了一条:
看起来很简单的 SPARQL
结果执行:
5 秒
不要第一时间怪:
SPARQL 太慢。
应该看:
Ontop 最终生成了什么 SQL?
比如:
有没有多余 JOIN?
有没有全表扫描?
WHERE 有没有下推?
索引有没有使用?
JOIN 字段有没有索引?
Mapping 的 URI Template 是否合理?
所以真正优化 Ontop,最后很多时候还是回到了:
SQL 优化。
四十二、主键为什么对 Ontop 很重要?
假设:
employee.id
是:
PRIMARY KEY
那么 Ontop 可以明确知道:
每个 id
只对应一名 Employee
这类数据库约束能够帮助查询优化。
同样:
FOREIGN KEY
可以提供:
表之间关系信息。
如果数据库约束完整,Ontop 在一些查询改写和优化场景里可以利用这些信息。
所以不要以为做了知识图谱之后:
数据库设计不重要了。
恰恰相反。
四十三、IRI Template 也非常重要
例如:
Employee
使用:
http://example.com/employee/{id}
Department:
http://example.com/department/{id}
Skill:
http://example.com/skill/{id}
这种模式非常清楚。
千万不要不同 Mapping 到处:
一会 employee/{id}
一会 users/{id}
一会 person/{id}
表示同一个东西。
否则会产生:
IRI 不一致
的问题。
在知识图谱里:
IRI 就是实体身份。
这有点像数据库里的:
Primary Key
但它是:
全局语义标识
四十四、NULL 怎么处理?
数据库很容易出现:
age = NULL
如果:
rr:objectMap
对应字段是 NULL,
通常不会生成对应 RDF Triple。
比如:
张三
name 张三
age NULL
知识图谱可能只有:
<employee/1001>
ex:name
"张三" .
没有:
ex:age
这和数据库:
age = NULL
的表达方式并不完全一样。
知识图谱更倾向:
没有这个事实
四十五、这也引出了一个很重要的问题
很多 Java/MySQL 程序员进入 RDF 世界以后,会下意识问:
这个属性为什么不能为空?
但 RDF 本身遵循:
Open World Assumption
缺少:
age
更多表示:
目前不知道年龄
而不一定:
年龄不存在
如果你需要严格校验:
Employee 必须有 age
那就会进入我们之前写过的:
SHACL。
所以整条技术链突然连起来了:
MySQL
↓
Ontop
↓
R2RML
↓
Virtual RDF
↓
Ontology
↓
SHACL
↓
SPARQL
四十六、这几个项目其实已经能串成一套体系
之前我们写:
Protégé
负责:
设计 Ontology
Owlready2:
Python 操作 Ontology
Apache Jena:
Java 操作 RDF / SPARQL
pySHACL:
校验 RDF 数据
而 Ontop:
把已有关系数据库接进这个语义世界。
整个架构:
Protégé
↓
Ontology
↓
MySQL ── R2RML ── Ontop ── SPARQL
│
↓
Knowledge View
│
┌─────────┴─────────┐
↓ ↓
Apache Jena Python
↓ ↓
Java App AI Agent
四十七、Ontop 最适合什么场景?
我认为最适合的一类就是:
企业已经有大量关系数据库,但又想做知识图谱。
比如公司已经有:
MySQL
PostgreSQL
Oracle
SQL Server
跑了十年。
里面:
100 张表
500 张表
1000 张表
你不可能说:
我们开始做知识图谱了,把所有数据迁移到图数据库。
风险和成本都非常高。
Ontop 的思路则是:
数据库继续跑
在上面加:
Mapping
+
Ontology
形成:
Virtual Knowledge Graph
业务系统基本不用大改。
四十八、它对 GraphRAG 也很有意思
传统企业 RAG:
PDF
Word
Markdown
↓
Chunk
↓
Embedding
↓
Vector DB
但是企业真正有价值的数据很多其实在:
MySQL
ERP
CRM
订单库
客户库
资产库
研发系统
如果通过 Ontop:
MySQL
↓
R2RML
↓
Virtual Knowledge Graph
↓
SPARQL
↓
实体关系
然后再给:
Agent / GraphRAG
使用。
Agent 就可以问:
张三属于哪个部门?
他掌握哪些技术?
哪些项目需要 Go?
谁最适合加入这个项目?
而底层数据:
仍然来自正在运行的业务数据库。
四十九、最后把整个 R2RML 思路记成一张图
数据库:
employee
id
name
age
department_id
↓
rr:logicalTable
决定:
数据从哪里来
↓
rr:subjectMap
决定:
这行数据是谁
↓
rr:template
生成:
http://example.com/employee/1001
↓
rr:predicateObjectMap
决定:
name → ex:name
age → ex:age
department_id
→ ex:belongsToDepartment
↓
最终虚拟为:
Employee
│
├── name → 张三
├── age → 28
└── belongsToDepartment
↓
研发部
五十、最后总结
Ontop 最值得理解的,并不是:
它支持 SPARQL。
真正核心的是:
它在关系数据库和语义知识模型之间,建立了一层 Mapping。
原来的数据库:
Table
Column
Primary Key
Foreign Key
JOIN
经过:
R2RML
被重新解释成:
Class
Property
IRI
Relation
Triple
而用户发出的:
SPARQL
又通过这套 Mapping:
SPARQL
↓
语义匹配
↓
Mapping
↓
Query Rewriting
↓
SQL
↓
Database
最终交给 MySQL 真正执行。
所以:
Ontop 并不是把 MySQL 变成图数据库。
更加准确的说法是:
Ontop 在关系数据库上构建了一层虚拟的 RDF / Knowledge Graph 视图。
数据继续待在:
MySQL
查询继续主要由:
数据库引擎
执行。
但上层看到的已经不再是:
employee.department_id
而是:
Employee
belongsToDepartment
Department
这就是从:
数据结构
走向:
数据语义
最关键的一步。
项目:
https://github.com/ontop/ontop

浙公网安备 33010602011771号