SPARQL 到底是怎么查知识图谱的?用 Apache Jena 写透 SELECT、FILTER、OPTIONAL 与多跳查询
前面我们已经用 Apache Jena 创建过这样的知识图谱:
张三
├── 属于 → 研发部
├── 掌握 → Go
├── 掌握 → Redis
└── 参与 → 支付系统
支付系统
├── 使用数据库 → PostgreSQL
└── 部署在 → Kubernetes
李四
└── 维护 → PostgreSQL

数据有了以后,下一个问题自然就是:
到底怎么查?
如果是 MySQL,我们会写:
SELECT *
FROM employee
WHERE name = '张三';
知识图谱则通常使用:
SPARQL
很多 Java 程序员第一次看到 SPARQL,会觉得:
这不就是换了一套语法的 SQL 吗?
其实两者有很大区别。
SQL 的核心思维通常是:
表
↓
JOIN
↓
字段
↓
WHERE
而 SPARQL 的核心是:
图模式匹配。
你不是先问:
我要查哪张表?
而是在描述:
我要找一种什么样的关系结构?
比如:
某个人
↓
掌握
↓
Go
SPARQL 就负责在整个 RDF Graph 中寻找:
谁满足这个图结构?
这篇我们就用 Apache Jena,从最简单的 SELECT 开始,一步一步写到:
FILTER
OPTIONAL
DISTINCT
ORDER BY
GROUP BY
聚合
多条件查询
多跳关系
Property Path
ASK
CONSTRUCT
最后再实现一个真正的:
员工技能关系查询系统。
项目地址:
https://github.com/apache/jena
一、先准备一张知识图谱
为了让后面的 SPARQL 有东西可查,我们先创建一些数据。
先定义命名空间:
private static final String NS =
"https://example.com/company/";
private static final String PROP =
NS + "property/";
private static final String EMP =
NS + "employee/";
private static final String SKILL =
NS + "skill/";
private static final String DEPT =
NS + "department/";
private static final String PROJECT =
NS + "project/";
创建 Model:
Model model =
ModelFactory.createDefaultModel();
设置 Prefix:
model.setNsPrefix(
"ex",
NS
);
model.setNsPrefix(
"prop",
PROP
);
二、先创建几个 Property
Property name =
model.createProperty(
PROP + "name"
);
Property age =
model.createProperty(
PROP + "age"
);
Property hasSkill =
model.createProperty(
PROP + "hasSkill"
);
Property belongsTo =
model.createProperty(
PROP + "belongsTo"
);
Property worksOn =
model.createProperty(
PROP + "worksOn"
);
Property usesDatabase =
model.createProperty(
PROP + "usesDatabase"
);
Property maintains =
model.createProperty(
PROP + "maintains"
);
三、创建部门
研发部:
Resource rd =
model.createResource(
DEPT + "rd"
);
rd.addProperty(
name,
"研发部"
);
产品部:
Resource product =
model.createResource(
DEPT + "product"
);
product.addProperty(
name,
"产品部"
);
四、创建技能
Resource go =
model.createResource(
SKILL + "go"
);
go.addProperty(
name,
"Go"
);
Java:
Resource java =
model.createResource(
SKILL + "java"
);
java.addProperty(
name,
"Java"
);
Redis:
Resource redis =
model.createResource(
SKILL + "redis"
);
redis.addProperty(
name,
"Redis"
);
Docker:
Resource docker =
model.createResource(
SKILL + "docker"
);
docker.addProperty(
name,
"Docker"
);
五、创建 PostgreSQL
Resource postgres =
model.createResource(
NS + "database/postgresql"
);
postgres.addProperty(
name,
"PostgreSQL"
);
六、创建一个支付系统项目
Resource payment =
model.createResource(
PROJECT + "payment"
);
payment.addProperty(
name,
"支付系统"
);
payment.addProperty(
usesDatabase,
postgres
);
这样:
支付系统
↓
usesDatabase
↓
PostgreSQL
已经形成。
七、创建张三
Resource zhangsan =
model.createResource(
EMP + "zhangsan"
);
zhangsan
.addProperty(
name,
"张三"
)
.addLiteral(
age,
28
)
.addProperty(
belongsTo,
rd
)
.addProperty(
hasSkill,
go
)
.addProperty(
hasSkill,
redis
)
.addProperty(
worksOn,
payment
);
八、创建李四
Resource lisi =
model.createResource(
EMP + "lisi"
);
lisi
.addProperty(
name,
"李四"
)
.addLiteral(
age,
26
)
.addProperty(
belongsTo,
rd
)
.addProperty(
hasSkill,
java
)
.addProperty(
hasSkill,
redis
)
.addProperty(
maintains,
postgres
);
九、创建王五
Resource wangwu =
model.createResource(
EMP + "wangwu"
);
wangwu
.addProperty(
name,
"王五"
)
.addLiteral(
age,
31
)
.addProperty(
belongsTo,
product
)
.addProperty(
hasSkill,
java
);
现在整张图大概是:
Go
↑
│ hasSkill
│
研发部 ← 张三 ─────────→ Redis
↑ │
│ │ worksOn
│ ↓
│ 支付系统
│ │
│ │ usesDatabase
│ ↓
│ PostgreSQL
│ ↑
│ │ maintains
│ │
└────────── 李四
│
├── Java
└── Redis
产品部 ← 王五
│
└── Java
接下来正式开始 SPARQL。
十、最简单的 SELECT
我们先做一个最简单的问题:
查询所有有姓名的实体。
SPARQL:
PREFIX prop:
<https://example.com/company/property/>
SELECT ?entity ?name
WHERE {
?entity
prop:name
?name .
}
这里:
?entity
?name
都是:
变量。
意思就是:
帮我找出所有满足
某个东西 → name → 某个名字的组合。
十一、Java 里怎么执行 SPARQL?
基本套路非常固定:
String sparql = """
PREFIX prop:
<https://example.com/company/property/>
SELECT ?entity ?name
WHERE {
?entity
prop:name
?name .
}
""";
创建 Query:
Query query =
QueryFactory.create(
sparql
);
执行:
try (
QueryExecution qexec =
QueryExecutionFactory.create(
query,
model
)
) {
ResultSet rs =
qexec.execSelect();
ResultSetFormatter.out(
rs
);
}
这就是 Jena 查询最基本的流程:
SPARQL String
↓
Query
↓
QueryExecution
↓
ResultSet
十二、手工读取 ResultSet
真实项目中通常不会直接:
ResultSetFormatter.out()
而是自己处理。
例如:
try (
QueryExecution qexec =
QueryExecutionFactory.create(
query,
model
)
) {
ResultSet rs =
qexec.execSelect();
while (
rs.hasNext()
) {
QuerySolution row =
rs.next();
Resource entity =
row.getResource(
"entity"
);
Literal entityName =
row.getLiteral(
"name"
);
System.out.println(
entity.getURI()
+ " => "
+ entityName.getString()
);
}
}
结果类似:
employee/zhangsan => 张三
employee/lisi => 李四
employee/wangwu => 王五
skill/go => Go
skill/java => Java
...
十三、为什么这条查询会把技能也查出来?
因为:
?entity
prop:name
?name .
没有限定:
entity 必须是 Employee。
所以只要有:
name
属性:
Employee
Skill
Department
Project
Database
都可能被匹配。
这就是:
Graph Pattern。
SPARQL 只关心:
谁满足这个模式?
十四、那怎么限定“员工”?
最规范的方法是给资源增加:
rdf:type
比如创建员工 Class:
Resource employeeClass =
model.createResource(
NS + "Employee"
);
给张三:
zhangsan.addProperty(
RDF.type,
employeeClass
);
李四:
lisi.addProperty(
RDF.type,
employeeClass
);
王五:
wangwu.addProperty(
RDF.type,
employeeClass
);
现在:
张三
↓
rdf:type
↓
Employee
十五、查询真正的 Employee
SPARQL:
PREFIX rdf:
<http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX ex:
<https://example.com/company/>
PREFIX prop:
<https://example.com/company/property/>
SELECT ?employee ?name
WHERE {
?employee
rdf:type
ex:Employee ;
prop:name
?name .
}
这里出现了一个写法:
?employee
rdf:type ex:Employee ;
prop:name ?name .
等价于:
?employee
rdf:type
ex:Employee .
?employee
prop:name
?name .
分号:
;
表示:
Subject 相同,继续写另一个 Predicate。
十六、这就是 SPARQL 最核心的思想
查询:
?employee
rdf:type
ex:Employee ;
prop:name
?name .
其实是在画一个图:
Employee
↑
│ rdf:type
│
?employee
│
│ name
↓
?name
SPARQL 会去整张 RDF Graph 里:
找满足这个结构的所有节点。
所以:
SPARQL 本质上是在做子图匹配。
理解这一句话,后面很多语法就通了。
十七、查询会 Go 的员工
目标图:
Employee
│
│ hasSkill
↓
Go
SPARQL:
PREFIX ex:
<https://example.com/company/>
PREFIX prop:
<https://example.com/company/property/>
PREFIX skill:
<https://example.com/company/skill/>
SELECT ?employee ?name
WHERE {
?employee
a
ex:Employee ;
prop:name
?name ;
prop:hasSkill
skill:go .
}
这里:
a
其实就是:
rdf:type
的简写。
所以:
?employee a ex:Employee .
表示:
employee 是 Employee 类型。
十八、查询同时会 Go 和 Redis 的人
目标图:
Go
↑
│
employee
│
↓
Redis
SPARQL:
PREFIX ex:
<https://example.com/company/>
PREFIX prop:
<https://example.com/company/property/>
PREFIX skill:
<https://example.com/company/skill/>
SELECT ?name
WHERE {
?employee
a
ex:Employee ;
prop:name
?name ;
prop:hasSkill
skill:go ;
prop:hasSkill
skill:redis .
}
结果:
张三
十九、这里其实发生了 JOIN
如果换成 SQL,可能需要:
employee
JOIN employee_skill
JOIN skill
JOIN employee_skill
JOIN skill
但 SPARQL 中没有显式写:
JOIN
为什么?
因为:
?employee
变量在多个 Triple Pattern 中重复出现。
例如:
?employee prop:name ?name .
?employee prop:hasSkill skill:go .
?employee prop:hasSkill skill:redis .
SPARQL 引擎会自动寻找:
同时满足这些条件的 ?employee
这其实就是:
隐式 JOIN。
二十、FILTER:给匹配结果加条件
现在问:
年龄大于 27 的员工。
SPARQL:
PREFIX ex:
<https://example.com/company/>
PREFIX prop:
<https://example.com/company/property/>
SELECT ?name ?age
WHERE {
?employee
a ex:Employee ;
prop:name
?name ;
prop:age
?age .
FILTER(
?age > 27
)
}
结果:
张三 28
王五 31
二十一、多个 FILTER
比如:
年龄在 27~30 岁之间。
FILTER(
?age >= 27
&&
?age <= 30
)
完整:
SELECT ?name ?age
WHERE {
?employee
prop:name ?name ;
prop:age ?age .
FILTER(
?age >= 27
&&
?age <= 30
)
}
结果:
张三 28
二十二、字符串过滤
查询:
姓名中包含“张”。
FILTER(
CONTAINS(
STR(?name),
"张"
)
)
完整:
PREFIX prop:
<https://example.com/company/property/>
SELECT ?name
WHERE {
?employee
prop:name
?name .
FILTER(
CONTAINS(
STR(?name),
"张"
)
)
}
二十三、正则表达式
也可以:
FILTER(
REGEX(
STR(?name),
"^张"
)
)
表示:
以“张”开头。
二十四、真正容易踩坑的 OPTIONAL
假设我们再增加一个属性:
Property email =
model.createProperty(
PROP + "email"
);
张三有邮箱:
zhangsan.addProperty(
email,
"zhangsan@example.com"
);
李四没有。
王五也没有。
现在我们问:
查询所有员工,如果有邮箱就显示邮箱。
很多 SQL 开发者第一反应可能写:
SELECT ?name ?email
WHERE {
?employee
prop:name
?name ;
prop:email
?email .
}
但是结果只有:
张三
为什么?
因为整个 Pattern 要同时满足:
有 name
+
有 email
李四没有 email:
整个匹配失败。
二十五、这时候要用 OPTIONAL
PREFIX ex:
<https://example.com/company/>
PREFIX prop:
<https://example.com/company/property/>
SELECT ?name ?email
WHERE {
?employee
a ex:Employee ;
prop:name
?name .
OPTIONAL {
?employee
prop:email
?email .
}
}
结果:
张三 zhangsan@example.com
李四 -
王五 -
二十六、OPTIONAL 很像 SQL 的 LEFT JOIN
可以这么理解。
SQL:
SELECT
e.name,
p.email
FROM employee e
LEFT JOIN profile p
ON e.id = p.employee_id;
SPARQL:
?employee
prop:name
?name .
OPTIONAL {
?employee
prop:email
?email .
}
所以对于 SQL 程序员:
OPTIONAL ≈ LEFT JOIN。
这个类比特别好理解。
二十七、Java 中怎么判断 OPTIONAL 是否存在?
while (
rs.hasNext()
) {
QuerySolution row =
rs.next();
String name =
row.getLiteral(
"name"
).getString();
String emailValue =
null;
if (
row.contains(
"email"
)
) {
emailValue =
row.getLiteral(
"email"
).getString();
}
System.out.println(
name
+ " | "
+ emailValue
);
}
输出:
张三 | zhangsan@example.com
李四 | null
王五 | null
二十八、FILTER + OPTIONAL 要特别小心
比如:
OPTIONAL {
?employee
prop:email
?email .
FILTER(
CONTAINS(
STR(?email),
"@example.com"
)
)
}
这里 FILTER 只作用在:
OPTIONAL
内部。
和把 FILTER 写在外面:
OPTIONAL {
?employee
prop:email
?email .
}
FILTER(...)
语义可能完全不同。
这是 SPARQL 新手非常容易踩的坑。
简单记:
FILTER 放在哪个 Graph Pattern 中,就对哪个匹配结果产生影响。
二十九、DISTINCT:去重
比如:
SELECT ?employee
WHERE {
?employee
prop:hasSkill
?skill .
}
张三有三个 Skill:
Go
Redis
Docker
所以:
张三
会出现三行。
如果只想知道:
哪些员工至少有一个技能?
写:
SELECT DISTINCT ?employee
WHERE {
?employee
prop:hasSkill
?skill .
}
三十、ORDER BY
按年龄排序:
SELECT ?name ?age
WHERE {
?employee
prop:name
?name ;
prop:age
?age .
}
ORDER BY ?age
从小到大。
倒序:
ORDER BY DESC(?age)
结果:
王五 31
张三 28
李四 26
三十一、LIMIT
只取两个:
SELECT ?name ?age
WHERE {
?employee
prop:name ?name ;
prop:age ?age .
}
ORDER BY DESC(?age)
LIMIT 2
结果:
王五
张三
三十二、OFFSET:分页
第一页:
LIMIT 10
OFFSET 0
第二页:
LIMIT 10
OFFSET 10
第三页:
LIMIT 10
OFFSET 20
是不是很像 SQL?
三十三、COUNT:统计员工数量
PREFIX ex:
<https://example.com/company/>
SELECT (
COUNT(?employee)
AS ?count
)
WHERE {
?employee
a ex:Employee .
}
Java:
QuerySolution row =
rs.next();
long count =
row
.getLiteral(
"count"
)
.getLong();
System.out.println(
"员工人数:"
+ count
);
三十四、GROUP BY:统计每个技能有多少员工
这是一个很实用的例子。
PREFIX prop:
<https://example.com/company/property/>
SELECT
?skill
(
COUNT(?employee)
AS ?employeeCount
)
WHERE {
?employee
prop:hasSkill
?skill .
}
GROUP BY ?skill
ORDER BY DESC(?employeeCount)
结果可能:
Redis 2
Java 2
Go 1
Docker 1
三十五、继续 JOIN 技能名称
现在结果是:
https://example.com/company/skill/redis
不够友好。
可以继续匹配:
?skill
prop:name
?skillName .
完整:
PREFIX prop:
<https://example.com/company/property/>
SELECT
?skillName
(
COUNT(?employee)
AS ?employeeCount
)
WHERE {
?employee
prop:hasSkill
?skill .
?skill
prop:name
?skillName .
}
GROUP BY ?skillName
ORDER BY DESC(?employeeCount)
结果:
Redis 2
Java 2
Go 1
Docker 1
三十六、开始进入重点:多跳查询
现在问:
张三参与的项目使用什么数据库?
图结构:
张三
↓ worksOn
支付系统
↓ usesDatabase
PostgreSQL
这就是:
两跳。
SPARQL:
PREFIX prop:
<https://example.com/company/property/>
PREFIX emp:
<https://example.com/company/employee/>
SELECT ?project ?database
WHERE {
emp:zhangsan
prop:worksOn
?project .
?project
prop:usesDatabase
?database .
}
这就是最基础的:
Multi-Hop Query。
三十七、多跳本质还是多个 Triple Pattern JOIN
第一段:
emp:zhangsan
prop:worksOn
?project .
得到:
?project = 支付系统
第二段:
?project
prop:usesDatabase
?database .
再得到:
?database = PostgreSQL
共同变量:
?project
把两个 Pattern 连接起来。
所以:
Graph Traversal
在 SPARQL 中很多时候就是:
共享变量不断 JOIN。
三十八、三跳查询
现在问:
张三负责的项目使用的数据库是谁维护的?
已有:
张三
↓
支付系统
↓
PostgreSQL
↑
李四
SPARQL:
PREFIX prop:
<https://example.com/company/property/>
PREFIX emp:
<https://example.com/company/employee/>
SELECT
?project
?database
?maintainer
WHERE {
emp:zhangsan
prop:worksOn
?project .
?project
prop:usesDatabase
?database .
?maintainer
prop:maintains
?database .
}
这里非常关键。
最后一个关系:
李四
→ maintains
→ PostgreSQL
所以 Triple Pattern 写:
?maintainer
prop:maintains
?database .
而不是反过来。
三十九、再把人名和系统名一起查出来
PREFIX prop:
<https://example.com/company/property/>
PREFIX emp:
<https://example.com/company/employee/>
SELECT
?projectName
?databaseName
?maintainerName
WHERE {
emp:zhangsan
prop:worksOn
?project .
?project
prop:name
?projectName ;
prop:usesDatabase
?database .
?database
prop:name
?databaseName .
?maintainer
prop:maintains
?database ;
prop:name
?maintainerName .
}
结果:
支付系统
PostgreSQL
李四
这就是知识图谱特别舒服的地方。
你描述的是:
关系路径
而不是:
表怎么 JOIN。
四十、Property Path:多跳查询还能写得更短
SPARQL 1.1 提供了:
Property Path
例如:
张三
→ worksOn
→ 项目
→ usesDatabase
→ Database
可以直接:
emp:zhangsan
prop:worksOn/prop:usesDatabase
?database .
完整:
PREFIX prop:
<https://example.com/company/property/>
PREFIX emp:
<https://example.com/company/employee/>
SELECT ?database
WHERE {
emp:zhangsan
prop:worksOn/
prop:usesDatabase
?database .
}
这里:
/
表示:
沿两个关系依次向前走。
四十一、Property Path 特别像“图上的路径”
比如:
prop:worksOn/prop:usesDatabase
就是:
worksOn
↓
usesDatabase
两条边。
所以:
A p1/p2 B
可以理解成:
A
↓ p1
某个节点
↓ p2
B
四十二、反向关系 ^
假设:
李四
→ maintains
→ PostgreSQL
现在从 PostgreSQL 出发:
谁维护它?
可以写:
?database
^prop:maintains
?maintainer .
^:
反向走 Property。
所以:
?database
^prop:maintains
?maintainer .
等价于:
?maintainer
prop:maintains
?database .
四十三、组合以后就更有意思了
从张三:
张三
→ 支付系统
→ PostgreSQL
← 李四
可以:
emp:zhangsan
prop:worksOn/
prop:usesDatabase/
^prop:maintains
?maintainer .
完整:
PREFIX prop:
<https://example.com/company/property/>
PREFIX emp:
<https://example.com/company/employee/>
SELECT ?maintainer
WHERE {
emp:zhangsan
prop:worksOn/
prop:usesDatabase/
^prop:maintains
?maintainer .
}
这就是:
图路径查询。
四十四、* 和 + 更强
假设组织关系:
后端组
→ partOf
→ 研发部
研发部
→ partOf
→ 技术中心
技术中心
→ partOf
→ 公司
如果不知道到底有几层怎么办?
可以:
prop:partOf+
+:
至少走一次。
例如:
<backend-team>
prop:partOf+
?parent .
结果:
研发部
技术中心
公司
四十五、* 和 + 的区别
+
表示:
1 次或多次
而:
*
表示:
0 次或多次
所以:
prop:partOf*
结果甚至可能包含:
backend-team 自己
因为:
走 0 次
也是合法路径。
四十六、| 表示“或者”
例如我们认为:
hasSkill
或者:
interestedIn
都算与技术有关。
可以:
(prop:hasSkill|prop:interestedIn)
例如:
?person
(prop:hasSkill|prop:interestedIn)
skill:go .
表示:
会 Go
或者:
对 Go 感兴趣
都匹配。
四十七、ASK:我只想知道“有没有”
有时候根本不需要:
ResultSet
只想问:
张三会不会 Go?
SPARQL:
PREFIX prop:
<https://example.com/company/property/>
PREFIX emp:
<https://example.com/company/employee/>
PREFIX skill:
<https://example.com/company/skill/>
ASK {
emp:zhangsan
prop:hasSkill
skill:go .
}
Java:
String askSparql = """
PREFIX prop:
<https://example.com/company/property/>
PREFIX emp:
<https://example.com/company/employee/>
PREFIX skill:
<https://example.com/company/skill/>
ASK {
emp:zhangsan
prop:hasSkill
skill:go .
}
""";
执行:
Query query =
QueryFactory.create(
askSparql
);
try (
QueryExecution qexec =
QueryExecutionFactory.create(
query,
model
)
) {
boolean result =
qexec.execAsk();
System.out.println(
result
);
}
输出:
true
四十八、ASK 非常适合权限和规则判断
比如:
用户是否属于某部门?
服务是否依赖 Redis?
某员工是否参与某项目?
一个节点是否存在某条关系?
都可以:
ASK {
...
}
不需要返回一大堆数据。
四十九、CONSTRUCT:查询完直接生成新图
这也是 SPARQL 和 SQL 差异很有意思的地方。
比如:
创建一张只包含会 Redis 的员工及其技能的新图。
PREFIX prop:
<https://example.com/company/property/>
PREFIX skill:
<https://example.com/company/skill/>
CONSTRUCT {
?employee
prop:hasSkill
skill:redis .
}
WHERE {
?employee
prop:hasSkill
skill:redis .
}
Java:
try (
QueryExecution qexec =
QueryExecutionFactory.create(
query,
model
)
) {
Model result =
qexec.execConstruct();
RDFDataMgr.write(
System.out,
result,
RDFFormat.TURTLE_PRETTY
);
}
返回的不是:
Table
而是:
一个新的 RDF Graph。
五十、CONSTRUCT 很适合知识图谱裁剪
比如原知识图谱有:
1000 万 Triple
某个 Agent 只需要:
支付系统相关知识
就可以:
CONSTRUCT {
...
}
WHERE {
...
}
生成一个:
小型 Context Graph
再交给 Agent。
这个思路其实和:
GraphRAG Subgraph
非常接近。
五十一、BIND:创建一个新变量
比如年龄:
28
希望计算:
10 年后的年龄
可以:
BIND(
?age + 10
AS ?ageAfter10Years
)
完整:
PREFIX prop:
<https://example.com/company/property/>
SELECT
?name
?age
?ageAfter10Years
WHERE {
?employee
prop:name
?name ;
prop:age
?age .
BIND(
?age + 10
AS ?ageAfter10Years
)
}
五十二、COALESCE:处理 OPTIONAL 空值
比如:
OPTIONAL {
?employee
prop:email
?email .
}
没有 Email 的时候变量:
?email
未绑定。
可以:
BIND(
COALESCE(
?email,
"未填写"
)
AS ?displayEmail
)
这样结果:
张三 zhangsan@example.com
李四 未填写
王五 未填写
五十三、一个真实一点的综合查询
现在需求:
查询研发部所有员工,显示姓名、年龄、邮箱、技能数量;邮箱没有就显示“未填写”,年龄从大到小。
SPARQL:
PREFIX prop:
<https://example.com/company/property/>
PREFIX dept:
<https://example.com/company/department/>
SELECT
?name
?age
?displayEmail
(
COUNT(?skill)
AS ?skillCount
)
WHERE {
?employee
prop:name
?name ;
prop:age
?age ;
prop:belongsTo
dept:rd .
OPTIONAL {
?employee
prop:email
?email .
}
OPTIONAL {
?employee
prop:hasSkill
?skill .
}
BIND(
COALESCE(
?email,
"未填写"
)
AS ?displayEmail
)
}
GROUP BY
?employee
?name
?age
?displayEmail
ORDER BY DESC(?age)
这个查询已经很接近真实系统了。
五十四、封装一个通用查询方法
Java 项目里,不要每个地方重复:
QueryFactory
QueryExecution
ResultSet
可以封装。
例如:
public class SparqlExecutor {
private final Model model;
public SparqlExecutor(
Model model
) {
this.model =
model;
}
public void select(
String sparql
) {
Query query =
QueryFactory.create(
sparql
);
try (
QueryExecution qexec =
QueryExecutionFactory.create(
query,
model
)
) {
ResultSet rs =
qexec.execSelect();
ResultSetFormatter.out(
rs
);
}
}
}
调用:
SparqlExecutor executor =
new SparqlExecutor(
model
);
executor.select(
"""
PREFIX prop:
<https://example.com/company/property/>
SELECT ?name
WHERE {
?employee
prop:name
?name .
}
"""
);
五十五、真实项目不要直接字符串拼用户输入
比如:
String query =
"SELECT * WHERE { ?s prop:name \""
+ username
+ "\" }";
这种写法和:
拼 SQL
一样,不够漂亮,也可能造成查询注入类问题。
Jena 提供:
ParameterizedSparqlString
可以参数化。
例如:
ParameterizedSparqlString pss =
new ParameterizedSparqlString(
"""
PREFIX prop:
<https://example.com/company/property/>
SELECT ?employee
WHERE {
?employee
prop:name
?name .
}
"""
);
然后:
pss.setLiteral(
"name",
"张三"
);
不过这里需要把查询设计成合适的参数形式。
更典型的是 IRI 参数。
例如:
ParameterizedSparqlString pss =
new ParameterizedSparqlString(
"""
PREFIX prop:
<https://example.com/company/property/>
SELECT ?name
WHERE {
?employee
prop:hasSkill
?targetSkill ;
prop:name
?name .
}
"""
);
设置:
pss.setIri(
"targetSkill",
SKILL + "redis"
);
再:
Query query =
pss.asQuery();
这种方式比:
字符串拼接
更适合真实系统。
五十六、为什么 SPARQL 特别适合知识图谱?
因为数据库里的问题经常是:
我要查哪张表?
而知识图谱真正关心:
我要找什么关系?
比如:
找出会 Go、属于研发部、参与使用 PostgreSQL 项目的员工。
SPARQL 可以直接写成:
SELECT ?employee
WHERE {
?employee
prop:hasSkill
skill:go ;
prop:belongsTo
dept:rd ;
prop:worksOn
?project .
?project
prop:usesDatabase
db:postgresql .
}
你会发现:
查询本身就在描述业务语义。
五十七、这和 GraphRAG 又连上了
我们前面讲 TrustGraph 时说:
GraphRAG
核心之一是:
沿 Entity Relationship 找上下文。
SPARQL 本身其实就是非常成熟的:
Graph Pattern Query Language。
比如用户:
张三负责项目的数据库维护人是谁?
可以转换成:
SELECT ?maintainer
WHERE {
emp:zhangsan
prop:worksOn
?project .
?project
prop:usesDatabase
?database .
?maintainer
prop:maintains
?database .
}
这实际上已经完成:
Entity
↓
Relationship
↓
Entity
↓
Relationship
↓
Entity
多跳检索。
五十八、未来 Agent 甚至可以自动生成 SPARQL
架构:
用户:
“哪些会 Redis 的研发部员工参与了支付系统?”
↓
LLM
↓
SPARQL
↓
Apache Jena / Fuseki
↓
Knowledge Graph
↓
ResultSet
↓
LLM
↓
自然语言答案
LLM 生成:
PREFIX prop:
<https://example.com/company/property/>
PREFIX skill:
<https://example.com/company/skill/>
PREFIX dept:
<https://example.com/company/department/>
PREFIX project:
<https://example.com/company/project/>
SELECT ?name
WHERE {
?employee
prop:name
?name ;
prop:hasSkill
skill:redis ;
prop:belongsTo
dept:rd ;
prop:worksOn
project:payment .
}
然后 Jena 真正执行。
这就是:
Natural Language → SPARQL → Knowledge Graph
非常典型的 AI + Ontology 使用方式。
五十九、最后总结 SPARQL 最值得记住的语法
SELECT
查询变量:
SELECT ?employee ?name
Triple Pattern
描述图结构:
?employee
prop:name
?name .
FILTER
过滤:
FILTER(
?age > 25
)
OPTIONAL
属性可能不存在:
OPTIONAL {
?employee
prop:email
?email .
}
可以类比:
LEFT JOIN
DISTINCT
去重:
SELECT DISTINCT ?employee
ORDER BY
排序:
ORDER BY DESC(?age)
LIMIT / OFFSET
分页:
LIMIT 20
OFFSET 40
GROUP BY
聚合:
GROUP BY ?skill
COUNT
统计:
COUNT(?employee)
ASK
判断事实是否存在:
ASK {
emp:zhangsan
prop:hasSkill
skill:go .
}
CONSTRUCT
生成新的 RDF Graph:
CONSTRUCT {
?s ?p ?o
}
WHERE {
?s ?p ?o
}
Property Path
沿关系走多跳:
prop:worksOn/
prop:usesDatabase
反向关系
^prop:maintains
任意层级
prop:partOf+
六十、真正理解 SPARQL,只需要记住一句话
很多教程会让你背:
SELECT
WHERE
FILTER
OPTIONAL
UNION
GROUP BY
但如果只背语法,非常容易忘。
真正应该记住的是:
SPARQL 是在描述“我想匹配什么样的图”。
例如:
?person
prop:worksOn
?project .
?project
prop:usesDatabase
?database .
你脑子里应该直接看到:
Person
↓
Project
↓
Database
而不是:
第一张表 JOIN 第二张表 JOIN 第三张表。
这就是从:
关系数据库思维
切换到:
图思维
最关键的一步。
Apache Jena 则负责把这些:
SPARQL
真正执行在:
Model
Dataset
TDB2
Fuseki
之上。
最终技术链:
Java
↓
Apache Jena
↓
RDF
↓
Knowledge Graph
↓
SPARQL
↓
Graph Pattern Matching
↓
Multi-Hop Query
↓
Agent / GraphRAG
如果你已经会 SQL,再学习 SPARQL,不需要把自己当成完全从零开始。
你只需要改变一个问题:
以前写 SQL 时你问:
数据在哪张表?
写 SPARQL 时,你开始问:
我要找的实体之间,到底是什么关系?
当这个思维转过来以后,SELECT、FILTER、OPTIONAL、多跳查询 基本都会顺起来。
项目地址:
https://github.com/apache/jena

浙公网安备 33010602011771号