查询语言

查询语言

当引入关系模型时,关系模型包含了一种查询数据的新方法:SQL 是一种 声明式 查询语言,而 IMS 和 CODASYL 使用命令式代码来查询数据库。许多常用的编程语言是命令式的。例如,给定一个动物物种的列表,返回列表中的鲨鱼可以这样写:

function getSharks() {
  var sharks = [];
  for (var i = 0; i < animals.length; i++) {
    if (animals[i].family === "Sharks") {
      sharks.push(animals[i]);
    }
  }
  return sharks;
}

在关系代数中:$sharks = σ_{family = “sharks”}(animals)$,只返回符合条件的动物,family=“shark”。定义 SQL 时,它紧密地遵循关系代数的结构:

> SELECT * FROM animals WHERE family ='Sharks';

命令式语言告诉计算机以特定顺序执行某些操作。可以想象一下,逐行地遍历代码,评估条件,更新变量,并决定是否再循环一遍。在声明式查询语言(如 SQL 或关系代数)中,你只需指定所需数据的模式 - 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合)- 但不是如何实现这一目标。数据库系统的查询优化器决定使用哪些索引和哪些连接方法,以及以何种顺序执行查询的各个部分。

声明式查询语言是迷人的,因为它通常比命令式 API 更加简洁和容易。但更重要的是,它还隐藏了数据库引擎的实现细节,这使得数据库系统可以在无需对查询做任何更改的情况下进行性能提升。SQL 示例不确保任何特定的顺序,因此不在意顺序是否改变。但是如果查询用命令式的代码来写的话,那么数据库就永远不可能确定代码是否依赖于排序。SQL 相当有限的功能性为数据库提供了更多自动优化的空间。

最后,声明式语言往往适合并行执行。现在,CPU 的速度通过内核的增加变得更快,而不是以比以前更高的时钟速度运行。命令代码很难在多个内核和多个机器之间并行化,因为它指定了指令必须以特定顺序执行。声明式语言更具有并行执行的潜力,因为它们仅指定结果的模式,而不指定用于确定结果的算法。在适当情况下,数据库可以自由使用查询语言的并行实现。

MapReduce 查询

MapReduce 是一个由 Google 推广的编程模型,用于在多台机器上批量处理大规模的数据。一些 NoSQL 数据存储(包括 MongoDB 和 CouchDB)支持有限形式的 MapReduce,作为在多个文档中执行只读查询的机制。MapReduce 既不是一个声明式的查询语言,也不是一个完全命令式的查询 API,而是处于两者之间:查询的逻辑用代码片断来表示,这些代码片段会被处理框架重复性调用。它基于 map(也称为 collect)和 reduce(也称为 fold 或 inject)函数,两个函数存在于许多函数式编程语言中。

最好举例来解释 MapReduce 模型。假设你是一名海洋生物学家,每当你看到海洋中的动物时,你都会在数据库中添加一条观察记录。现在你想生成一个报告,说明你每月看到多少鲨鱼。在 PostgreSQL 中,你可以像这样表述这个查询:

SELECT
	date_trunc('month', observation_timestamp) AS observation_month,
	sum(num_animals)                           AS total_animals
FROM observations
WHERE family = 'Sharks'
GROUP BY observation_month;

date_trunc('month',timestamp) 函数用于确定包含 timestamp 的日历月份,并返回代表该月份开始的另一个时间戳。换句话说,它将时间戳舍入成最近的月份。这个查询首先过滤观察记录,以只显示鲨鱼家族的物种,然后根据它们发生的日历月份对观察记录果进行分组,最后将在该月的所有观察记录中看到的动物数目加起来。同样的查询用 MongoDB 的 MapReduce 功能可以按如下来表述:

db.observations.mapReduce(
  function map() {
    var year = this.observationTimestamp.getFullYear();
    var month = this.observationTimestamp.getMonth() + 1;
    emit(year + "-" + month, this.numAnimals);
  },
  function reduce(key, values) {
    return Array.sum(values);
  },
  {
    query: {
      family: "Sharks"
    },
    out: "monthlySharkReport"
  }
);
  • 可以声明式地指定只考虑鲨鱼种类的过滤器(这是一个针对 MapReduce 的特定于 MongoDB 的扩展)。
  • 每个匹配查询的文档都会调用一次 JavaScript 函数map,将this设置为文档对象。
  • map函数发出一个键(包括年份和月份的字符串,如"2013-12""2014-1")和一个值(该观察记录中的动物数量)。
  • map发出的键值对按键来分组。对于具有相同键(即,相同的月份和年份)的所有键值对,调用一次reduce函数。
  • reduce函数将特定月份内所有观测记录中的动物数量相加。
  • 将最终的输出写入到monthlySharkReport集合中。

例如,假设 observations 集合包含这两个文档:

[
  {
    observationTimestamp: Date.parse("Mon, 25 Dec 1995 12:34:56 GMT"),
    family: "Sharks",
    species: "Carcharodon carcharias",
    numAnimals: 3
  },
  {
    observationTimestamp: Date.parse("Tue, 12 Dec 1995 16:17:18 GMT"),
    family: "Sharks",
    species: "Carcharias taurus",
    numAnimals: 4
  }
];

对每个文档都会调用一次 map 函数,结果将是 emit("1995-12",3)emit("1995-12",4)。随后,以 reduce("1995-12",[3,4]) 调用 reduce 函数,将返回 7。map 和 reduce 函数在功能上有所限制:它们必须是纯函数,这意味着它们只使用传递给它们的数据作为输入,它们不能执行额外的数据库查询,也不能有任何副作用。这些限制允许数据库以任何顺序运行任何功能,并在失败时重新运行它们。然而,map 和 reduce 函数仍然是强大的:它们可以解析字符串,调用库函数,执行计算等等。

MapReduce 是一个相当底层的编程模型,用于计算机集群上的分布式执行。像 SQL 这样的更高级的查询语言可以用一系列的 MapReduce 操作来实现,但是也有很多不使用 MapReduce 的分布式 SQL 实现。请注意,SQL 中没有任何内容限制它在单个机器上运行,而 MapReduce 在分布式查询执行上没有垄断权。能够在查询中使用 JavaScript 代码是高级查询的一个重要特性,但这不限于 MapReduce,一些 SQL 数据库也可以用 JavaScript 函数进行扩展。

MapReduce 的一个可用性问题是,必须编写两个密切合作的 JavaScript 函数,这通常比编写单个查询更困难。此外,声明式查询语言为查询优化器提供了更多机会来提高查询的性能。基于这些原因,MongoDB 2.2 添加了一种叫做聚合管道的声明式查询语言的支持。用这种语言表述鲨鱼计数查询如下所示:

db.observations.aggregate([
  { $match: { family: "Sharks" } },
  {
    $group: {
      _id: {
        year: { $year: "$observationTimestamp" },
        month: { $month: "$observationTimestamp" }
      },
      totalAnimals: { $sum: "$numAnimals" }
    }
  }
]);

聚合管道语言与 SQL 的子集具有类似表现力,但是它使用基于 JSON 的语法而不是 SQL 的英语句子式语法。

下一页