3. 基本查询模式:SELECT、WHERE、三元组模式、前缀声明与命名空间
讲查询优化之前,得先把“怎么问问题”这件事搞利索。SPARQL 的查询说到底就这几个构件,但很多人一上来就写满天飞的 URI,结果自己都看晕了。我习惯先把骨架搭好,再往里填肉。
3.1 前缀声明:给 URI 起个短名字
在 RDF 里,资源都用长长的 URI 表示。比如 http://example.org/people/Alice。这种东西到处都是,写全称不仅手累,眼睛也累。
前缀(PREFIX)就是给命名空间起个别名。
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX ex: <http://example.org/>
之后在查询里,你直接用 foaf:name 就代表 http://xmlns.com/foaf/0.1/name。省多少事?想想看。
我个人习惯把常用前缀整理成一个模板文件,每次写查询直接复制开头几行。碰到不熟悉的领域,先看一眼文档里默认的命名空间是啥。
PREFIX 缩写,但别指望服务端能自动猜测你没声明的前缀。少写一行就会报错,这玩意儿很死板。
3.2 SELECT 子句:你究竟想看什么?
SELECT 决定了查询结果返回哪些变量。变量用问号开头,比如 ?name、?email。
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?email
WHERE {
?person foaf:name ?name .
?person foaf:mbox ?email .
}
上面这个查询返回两列:名称和邮箱。你想想看,如果企业知识图谱里一个节点关联了几十个属性,你全 SELECT * 回来,光序列化时间就能多出好几倍。
说个我在项目里的经验:有一次做数据分析,查出来的结果集有十五列,但分析只需要三列。我直接复制了完整的查询没改 SELECT,结果网络传输负载高了四倍。优化一下 SELECT,效果立竿见影。
注意几个常用的 SELECT 变体:
| 写法 | 含义 |
|---|---|
SELECT ?x ?y |
返回指定变量 |
SELECT * |
返回所有变量 |
SELECT DISTINCT ?x |
去重,只返回不重复的值 |
SELECT COUNT(?x) |
返回计数,而不是具体值 |
SELECT DISTINCT 时,查询引擎需要额外做去重运算。数据量大的时候,这步可能成为瓶颈。我之前就碰过一回:一个跨多图的查询,用了 DISTINCT 硬是跑了三分钟,去掉之后只需二十秒——当然,前提是你的业务逻辑允许重复。
3.3 WHERE 与三元组模式:最核心的匹配逻辑
WHERE 后面跟的是图模式(Graph Pattern)。最基本的单位是三元组模式:主语、谓语、宾语。你可以把变量放在任何一个位置。
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?person ?name
WHERE {
?person foaf:name ?name .
?person foaf:age ?age .
FILTER(?age > 18)
}
上面那个模式,说白了就是:找所有人,取他的名字和年龄,然后只留下成年人。引擎会在后台匹配所有满足 foaf:name 和 foaf:age 的三元组,并把结果组合起来。
每个三元组模式中间的谓语(也就是属性)如果写成固定 URI,引擎能直接命中索引,效率最高。主语或宾语用变量会走全表扫描?不一定,得看数据分布。嗯,这里要注意:引擎通常会统计每个属性的出现次数,如果某个属性很稀疏,用变量做查询反而可能更快。
我在做知识图谱合并时遇到过这样一个场景:两个数据集使用不同的谓语描述同一个概念,一个用 dbo:birthDate,另一个用 schema:birthDate。后来我用 UNION 把它们包在一起,模式就灵活了。这部分等后面讲高级查询再细聊。
3.4 组合在一起:一个完整的例子
把前面的知识点串起来,写一个正经的查询:
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX dbo: <http://dbpedia.org/ontology/>
PREFIX dbp: <http://dbpedia.org/property/>
SELECT ?city ?population ?country
WHERE {
?city rdf:type dbo:City .
?city dbo:populationTotal ?population .
?city dbo:country ?country .
FILTER(?population > 1000000)
}
ORDER BY DESC(?population)
LIMIT 10
这个查询干了啥?它找到所有城市类型(rdf:type dbo:City)的资源,拿到人口和国家属性,只保留人口超过一百万的城市,按人口降序排,最后只返回前十名。
你仔细品一品:每一行三元组模式都像一把钥匙。第一个模式过滤出城市节点,第二个和第三个取数据。引擎执行的顺序可能不是你写的顺序——它有内置的优化器,会重新排列模式执行顺序。但,我们作为写查询的人,提前把选择性高的模式放前面,还是能帮引擎少走弯路。
3.5 避坑清单
这些都是我曾经踩过的坑:
- 忘记声明前缀 —— 引擎直接报错,不给你任何商量余地。
- 三元组没有用英文句号结尾 —— SPARQL 要求每个模式后面配一个点(
.)。漏写会导致解析失败。 - SELECT 和 WHERE 之间多写了逗号 —— 很多人从 SQL 带过来的习惯,SPARQL 变量之间不需要逗号。
- 谓语写成了字符串(加了引号) —— 比如
"foaf:name",这样会被当成字面量,不是谓词。去掉引号才对。 - 变量名冲突 —— 同一个变量在不同三元组中表示同一个资源,如果不小心写了两个不同的变量名(比如
?person和?p),引擎会认为它们是不同的东西,结果就全乱了。
SELECT * 探一探数据里都有啥,确认模式写对了,再换成需要的特定变量。省得改很多次。
下面这张图把基本查询模式的结构画清楚了。我尽量让箭头和节点少绕弯,一目了然。
图里左侧是前缀,中间是 SELECT,右边是 WHERE。三元组模式从上往下流动,经过可选的修饰符,最后汇聚成结果集。你仔细看箭头方向,它其实就是一个管道:从声明变量 → 绑定模式 → 过滤筛选 → 产出结果。理解了这个管道,后面的优化思路就全都是围绕怎么让这段管道跑得更快。
行了,这一节就到这儿。把 PREFIX、SELECT 和三元组模式练熟了,后面讲复合图模式和高级过滤才会顺手。说白了,这就是 SPARQL 的字母表,会拼了才能写文章。