找到小偷¶
扮演侦探的角色,在这个虚构的村庄里找到小偷。你将学习如何使用 QL 中的逻辑连接词、量词和聚合。
简介¶
在群山深处隐藏着一个村庄。村庄分为四个部分——北、南、东、西,村庄中心矗立着一座阴暗神秘的城堡……城堡里,最高塔里锁着一个宝贵的金皇冠。一天晚上,发生了一起可怕的罪行。一个小偷闯入塔里,偷走了皇冠!
你知道小偷一定住在村子里,因为只有村民知道皇冠。经过一番专家级侦探工作,你获得了村里所有人的姓名和部分个人资料。
姓名 | 年龄 | 头发颜色 | 身高 | 位置 |
---|---|---|---|---|
… | … | … | … | … |
不幸的是,你仍然不知道是谁偷走了皇冠,所以你四处走动寻找线索。村民们表现得非常可疑,你确信他们知道关于小偷的信息。他们拒绝直接告诉你,但他们勉强同意回答问题。他们仍然不太爱说话,而且**只用“是”或“否”来回答问题**。
你开始问一些有创意的问题,并记下答案,以便以后与你的信息进行比较。
问题 | 答案 | |
---|---|---|
小偷的身高超过 150 厘米吗? | 是 | |
小偷的头发是金色的吗? | 否 | |
小偷是秃头的吗? | 否 | |
小偷的年龄小于 30 岁吗? | 否 | |
小偷住在城堡的东边吗? | 是 | |
小偷的头发是黑色或棕色吗? | 是 | |
小偷的身高在 180 厘米到 190 厘米之间吗? | 否 | |
小偷是村里年龄最大的人吗? | 否 | |
小偷是村里个子最高的人吗? | 否 | |
小偷的身高低于村民的平均身高吗? | 是 | |
小偷是村庄东部年龄最大的人吗? | 是 |
信息太多,无法手动搜索,所以你决定使用你新获得的 QL 技能来帮助你调查……
注意
你可以在 GitHub Codespaces 中使用 CodeQL 模板(测试版)来试用这些教程中的 QL 概念和与编程语言无关的示例。该模板包含一个引导式介绍,介绍了如何使用 QL,并使你能够轻松上手。
当你准备好对实际代码库运行 CodeQL 查询时,需要在 Visual Studio Code 中安装 CodeQL 扩展。有关说明,请参阅 GitHub 文档中的 为 Visual Studio Code 安装 CodeQL。
QL 库¶
我们定义了一系列 QL 谓词,以帮助你从表格中提取数据。QL 谓词是一个小型查询,它表达了各种数据之间的关系,并描述了它们的一些属性。在这种情况下,谓词会提供有关一个人的信息,例如他们的身高或年龄。
谓词 | 描述 |
---|---|
getAge() |
返回一个人的年龄(以年为单位),数据类型为 int |
getHairColor() |
返回一个人的头发颜色,数据类型为 string |
getHeight() |
返回一个人的身高(以厘米为单位),数据类型为 float |
getLocation() |
返回一个人的住址(北、南、东、西),数据类型为 string |
我们将这些谓词存储在 QL 库 tutorial.qll
中。若要访问此库,请在查询控制台中键入 import tutorial
。
库便于存储常用谓词。这可以让你不必每次需要谓词时都重新定义。相反,你可以直接 import
库并直接使用谓词。导入库后,你可以通过追加谓词将任何谓词应用于表达式。
例如,t.getHeight()
将 getHeight()
应用于 t
并返回 t
的身高。
开始搜索¶
村民们对“小偷的身高超过 150 厘米吗?”这个问题回答了“是”。要使用此信息,你可以编写以下查询来列出所有身高超过 150 厘米的村民。这些都是可能的嫌疑人。
from Person t
where t.getHeight() > 150
select t
第一行 from Person t
声明 t
必须是 Person
。我们说 t
的 类型 是 Person
。
在你将其余答案用于 QL 搜索之前,这里有一些工具和示例,可以帮助你编写自己的 QL 查询。
逻辑连接词¶
使用 逻辑连接词,你可以编写更复杂的查询,这些查询可以组合不同的信息片段。
例如,如果你知道小偷的年龄超过 30 岁,而且头发是棕色的,你可以使用以下 where
子句来链接两个谓词。
where t.getAge() > 30 and t.getHairColor() = "brown"
注意
谓词
getHairColor()
返回一个string
,所以我们需要在结果"brown"
周围加上引号。
如果小偷没有住在城堡的北边,你可以使用
where not t.getLocation() = "north"
如果小偷的头发是棕色或黑色,你可以使用
where t.getHairColor() = "brown" or t.getHairColor() = "black"
你还可以将这些连接词组合成更长的语句
where t.getAge() > 30
and (t.getHairColor() = "brown" or t.getHairColor() = "black")
and not t.getLocation() = "north"
注意
我们在
or
子句周围加了括号,以确保查询按预期进行评估。如果没有括号,连接词and
优先于or
。
谓词并不总是返回一个值。例如,如果一个人 p
的头发是黑色的,并且正在变灰,p.getHairColor()
将返回两个值:黑色和灰色。
如果小偷是秃头的呢?在这种情况下,小偷没有头发,所以 getHairColor()
谓词根本不返回任何结果!
如果你知道小偷肯定不是秃头的,那么一定有一种颜色与小偷的头发颜色匹配。在 QL 中表达这一点的一种方法是引入一个新的变量 c
,类型为 string
,并选择那些 t
,其中 t.getHairColor()
与 c
的值匹配。
from Person t, string c
where t.getHairColor() = c
select t
请注意,我们只是暂时引入了变量 c
,并且在 select
子句中根本不需要它。在这种情况下,最好使用 exists
from Person t
where exists(string c | t.getHairColor() = c)
select t
exists
引入一个临时变量 c
,类型为 string
,并且只有在至少存在一个满足 t.getHairColor() = c
的 string c
时才成立。
注意
如果你熟悉逻辑,你可能会注意到 QL 中的
exists
对应于逻辑中的存在 量词。QL 也有一个全称量词forall(vars | formula 1 | formula 2)
,在逻辑上等价于not exists(vars | formula 1 | not formula 2)
。
真正的调查¶
你现在已经准备好追踪小偷了!使用上面的示例,编写一个查询,以查找满足前八个问题的答案的人。
问题 | 答案 | |
---|---|---|
1 | 小偷的身高超过 150 厘米吗? | 是 |
2 | 小偷的头发是金色的吗? | 否 |
3 | 小偷是秃头的吗? | 否 |
4 | 小偷的年龄小于 30 岁吗? | 否 |
5 | 小偷住在城堡的东边吗? | 是 |
6 | 小偷的头发是黑色或棕色吗? | 是 |
7 | 小偷的身高在 180 厘米到 190 厘米之间吗? | 否 |
8 | 小偷是村里年龄最大的人吗? | 否 |
提示¶
- 不要忘记
import tutorial
! - 分别将每个问题翻译成 QL。如果你卡住了,请查看上面的示例。
- 对于问题 3,请记住,秃头的人没有头发颜色。
- 对于问题 8,请注意,如果一个人不是年龄最大的,那么至少存在一个比他年龄更大的人。
- 使用逻辑连接词组合这些条件,以得到以下形式的查询
import tutorial
from Person t
where <condition 1> and
not <condition 2> and
...
select t
完成后,你将获得一个可能的嫌疑人列表。这些人中一定有一个是小偷!
➤ 查看你的答案
你越来越接近解开谜团了!不幸的是,你仍然有一个很长的嫌疑人列表……要找出你的嫌疑人中谁是小偷,你必须收集更多信息并在下一步细化你的查询。
更高级的查询¶
如果你想找到村里年龄最大、最小、个子最高或最矮的人怎么办?如前一主题所述,你可以使用 exists
来实现这一点。但是,在 QL 中还有一种更有效的方法可以做到这一点,使用 max
和 min
之类的函数。这些是 聚合 的示例。
一般来说,聚合函数是对多个数据进行操作并返回单个值作为输出的函数。常见的聚合函数包括 count
、max
、min
、avg
(平均值)和 sum
。使用聚合函数的一般方式是
<aggregate>(<variable declarations> | <logical formula> | <expression>)
例如,你可以使用 max
聚合函数来查找村庄中最年长的人的年龄
max(int i | exists(Person p | p.getAge() = i) | i)
该聚合函数会考虑所有整数 i
,将 i
限制为与村庄中的人的年龄匹配的值,然后返回最大的匹配整数。
但是,如何在实际查询中使用它呢?
如果小偷是村庄中最年长的人,那么你知道小偷的年龄等于村民的最大年龄
from Person t
where t.getAge() = max(int i | exists(Person p | p.getAge() = i) | i)
select t
这种通用的聚合语法相当冗长且不方便。在大多数情况下,你可以省略聚合函数的某些部分。一个特别有用的 QL 功能是有序聚合。这允许你使用 order by
对表达式进行排序。
例如,如果你使用有序聚合函数,选择最年长的村民就会变得简单很多。
select max(Person p | | p order by p.getAge())
有序聚合函数会考虑每个人 p
并选择年龄最大的那个人。在这种情况下,对要考虑的人员没有限制,因此 <logical formula>
子句为空。请注意,如果有多个人的年龄相同,则查询会列出所有这些人的信息。
以下是一些聚合函数的更多示例
示例 | 结果 |
---|---|
min(Person p | p.getLocation() = "east" | p order by p.getHeight()) |
村庄东部最矮的人 |
count(Person p | p.getLocation() = "south" | p) |
村庄南部的人数 |
avg(Person p | | p.getHeight()) |
村民的平均身高 |
sum(Person p | p.getHairColor() = "brown" | p.getAge()) |
所有棕色头发村民的年龄总和 |
抓捕罪犯¶
现在你可以将剩余的问题翻译成 QL
问题 | 答案 | |
---|---|---|
… | … | … |
9 | 小偷是村里个子最高的人吗? | 否 |
10 | 小偷的身高低于村民的平均身高吗? | 是 |
11 | 小偷是村庄东部年龄最大的人吗? | 是 |
你找到小偷了吗?
➤ 查看答案
答案¶
在这些答案中,我们使用 /*
和 */
来标记查询的不同部分。任何被 /*
和 */
包围的文本都不会作为 QL 代码的一部分进行评估,而是被视为注释。
练习 1¶
import tutorial
from Person t
where
/* 1 */ t.getHeight() > 150 and
/* 2 */ not t.getHairColor() = "blond" and
/* 3 */ exists (string c | t.getHairColor() = c) and
/* 4 */ not t.getAge() < 30 and
/* 5 */ t.getLocation() = "east" and
/* 6 */ (t.getHairColor() = "black" or t.getHairColor() = "brown") and
/* 7 */ not (t.getHeight() > 180 and t.getHeight() < 190) and
/* 8 */ exists(Person p | p.getAge() > t.getAge())
select t
练习 2¶
import tutorial
from Person t
where
/* 1 */ t.getHeight() > 150 and
/* 2 */ not t.getHairColor() = "blond" and
/* 3 */ exists (string c | t.getHairColor() = c) and
/* 4 */ not t.getAge() < 30 and
/* 5 */ t.getLocation() = "east" and
/* 6 */ (t.getHairColor() = "black" or t.getHairColor() = "brown") and
/* 7 */ not (t.getHeight() > 180 and t.getHeight() < 190) and
/* 8 */ exists(Person p | p.getAge() > t.getAge()) and
/* 9 */ not t = max(Person p | | p order by p.getHeight()) and
/* 10 */ t.getHeight() < avg(float i | exists(Person p | p.getHeight() = i) | i) and
/* 11 */ t = max(Person p | p.getLocation() = "east" | p order by p.getAge())
select "The thief is " + t + "!"