Couchbase:N1QL JOIN 性能问题

Couchbase: N1QL JOIN performance issue

我正在熟悉 Couchbase(我正在开始使用 Server Community Edition),我的目标是迁移我们当前的 SQLite 数据库迁移到 Couchbase 以构建与移动设备的高效实时同步机制。

到目前为止,第一步是积极的,我们已经创建了存储桶(每个 SQLite 一个存储桶 table)并导入了所有数据(每个 SQLite 行一个 JSON 个文档)。 此外,为了允许复杂的查询和过滤,我们为所有存储桶创建了索引(主要和次要)

总而言之,我们有两个主要方面:

1) players,其中包含具有以下结构的文档

{
  "name": "xxx",
  "transferred": false,
  "value": n,
  "playmaker": false,
  "role": "y",
  "team": "zzz"
}

2) marks,具有以下结构(其中 "player" 字段是对玩家存储桶中文档 ID 的引用)

{
  "drawgoal": 0,
  "goal": 0,
  "owngoal": 0,
  "enter": 1,
  "mpenalty": 0,
  "gotgoal": 0,
  "ycard": 0,
  "assist": 0,
  "wingoal": 0,
  "mark": 6,
  "penalty": 0,
  "player": "xxx",
  "exit": 0,
  "fmark": 6,
  "team": "yyy",
  "rcard": 0,
  "source": "zzz",
  "day": 1,
  "spenalty": 0
}

到目前为止还不错,但是当我尝试 运行 需要 JOIN 的复杂 N1QL 查询时,性能是与 SQLite 相比相当糟糕。 例如,执行此查询大约需要 3 秒

select mark.*, player.`role` from players player join marks mark on key mark.player for player where mark.type = "xxx" and mark.day = n order by mark.team asc, player.`role` desc;

我们目前在播放器中有 600 个文档(已用磁盘 = 16MB,已用 RAM = 12MB)和标记中有 20K 个文档(已用磁盘 = 70MB,已用 RAM = 17MB),从我的角度来看,这应该不算多。

谢谢

我找到了答案:)

通过将查询更改为:

select marks.*, players.`role` from marks join players on keys marks.player where marks.day = n and marks.type = "xxx" order by marks.team asc, players.`role` desc;

执行时间减少到不到 300 毫秒。显然,反转 JOIN(从标记到玩家)可以显着提高性能。

这个查询比另一个查询快得多的原因是 Couchbase 按如下方式评估查询:

  • 首先检索符合过滤条件的所有marks文档
  • 然后尝试使用玩家文件加入他们

这样一来,要加入的文档数量就会少得多,因此执行时间也会缩短。

我认为你遗漏了一些细节,所以我将用我的猜测来填补空白。首先,JSON 文档不能有像 "value": n 这样的字段。它需要是像 "n" 这样的字符串或像 1 这样的数字。我假设你的意思是一个字面数字,所以我把 1 放在那里。

接下来,让我们看看您的查询:

select m.*, p.`role`
from players p
join marks m on key m.player for p
where m.type = "xxx"
and m.day = 1
order by m.team asc, p.`role` desc;

同样,你有 m.day = n,所以我输入 m.day = 1。此查询不会 运行 没有索引。我将假设您创建了一个主索引(它将扫描整个存储桶,并且不适合生产):

create primary index on players;
create primary index on marks;

查询仍然没有 运行,所以您必须在标记中的 'players' 字段上添加索引:

create index ix_marks_player on marks(player);

查询 运行s,但 returns 没有结果,因为您的示例文档缺少一个 "type": "xxx" 字段。所以我添加了那个字段,现在你的查询 运行s.

只需单击 "plan text" 即可查看计划文本(如果您使用的是企业版,您会看到计划图的可视化版本)。

计划文本显示查询正在玩家存储桶上使用 PrimaryScan。事实上,您的查询试图加入每个播放器文档。所以随着玩家桶的增长,查询会变慢。

在您对 SO 的回答中,您说获取相同数据的不同查询工作得更快:

select m.*, p.`role`
from marks m
join players p on keys m.player
where m.day = 1
and m.type = "xxx"
order by m.team asc, p.`role` desc;

您交换了联接,但查看计划文本,您仍在 运行宁 PrimaryScan。这次它扫描所有标记文档。我假设你有更少的人(总数更少,或者因为你在过滤的那一天你加入的人更少)。

所以我的回答基本上是:你总是需要加入所有的文件吗? 如果是这样,为什么?如果没有,我建议您修改查询以添加 LIMIT/OFFSET(可能用于分页)或其他一些过滤器,这样您就不会查询所有内容。

还有一点:看起来您正在使用存储桶进行组织。这并不是严格意义上的 错误 ,但它不会真正扩展。桶分布在整个集群中,因此您可以合理使用的桶数量受到限制(甚至可能硬性限制为 10 个桶)。 我不知道您的用例,但通常最好在文档中使用 "type"/"_type"/"docType"/etc 值进行组织,而不是依赖存储桶。

The first steps have been positive so far, we've created buckets (one bucket per SQLite table) and imported all data (one JSON document per SQLite row)

你这里有问题。您已尝试将 SQL 数据库模式映射到文档数据库模式,而不考虑 Couchbase 文档中的最佳实践甚至可怕的警告。

首先,您应该使用一个存储桶。与 table 相比,存储桶更像是一个数据库(尽管它比这更复杂)并且 Couchbase 建议每个集群使用一个存储桶,除非您有充分的理由不这样做。它有助于提高性能、扩展和资源利用率。您的每个文档都应该有一个指示数据类型的字段。这就是您的 "tables" 的区别所在。我使用名为“_type”的字段。例如。您将有 'player' 和 'mark' 文档类型。

其次,您应该重新考虑将数据导入为每个文档一行。文档数据库为您提供了不同的架构选项,其中一些对于提高性能非常有用。您当然可以保持这种方式,但它可能不是最佳选择。这是开发人员 运行 首次使用 NoSQL 数据库时常犯的一个错误。

一个很好的例子是一对多关系。您可以将标记作为数组嵌入到播放器文档中,而不是为单个播放器文档设置多个标记文档。文档可以存储对象数组!

例如

{
  "name": "xxx",
  "transferred": false,
  "value": n,
  "playmaker": false,
  "role": "y",
  "team": "zzz",
  "_type": "player",
  "marks": [
    "mark": {
      "drawgoal": 0,
      "goal": 0,
      "owngoal": 0,
      "enter": 1,
    },
    "mark": {
      "drawgoal": 0,
      "goal": 0,
      "owngoal": 0,
      "enter": 1,
    },
    "mark": {
      "drawgoal": 0,
      "goal": 0,
      "owngoal": 0,
      "enter": 1,
    }
  ]
}

您也可以为团队和角色执行此操作,但听起来这会使您可能尚未准备好处理的事情非规范化,这并不总是一个好主意。

Couchbase 可以在 JSON 内建立索引,因此您仍然可以使用 N1QL 查询所有玩家的标记。这还可以让您在单个 key:value 调用中提取播放器的文档和标记,这是最快的一种。