【问题标题】:Couchbase: N1QL JOIN performance issueCouchbase:N1QL JOIN 性能问题
【发布时间】:2018-07-17 15:00:08
【问题描述】:

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

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

总而言之,我们有两个主要的桶:

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

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

2) 标记,具有以下结构(其中“玩家”字段是对玩家存储桶中文档 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),应该不多从我的角度来看。

  • 是否可以调整任何设置来提高 JOIN 性能? 我可以创建任何特定索引吗?

  • 与 SQLite 相比,这种性能下降是为了获得更多灵活性和更多功能而付出的代价吗?

  • 我应该尽可能避免在 Couchbase 中使用 JOIN,而是在需要的地方复制数据?

谢谢

【问题讨论】:

    标签: json sqlite join couchbase n1ql


    【解决方案1】:

    我找到了答案:)

    通过将查询更改为:

    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 对查询的评估如下:

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

    通过这样做,要加入的文档数量要少得多,因此执行时间会减少。

    【讨论】:

      【解决方案2】:

      我认为你遗漏了一些细节,所以我将用我的猜测来填补空白。首先,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;
      

      查询仍然没有运行,因此您必须在标记中的“玩家”字段上添加了索引:

      create index ix_marks_player on marks(player);
      

      查询运行,但未返回任何结果,因为您的示例文档缺少"type": "xxx" 字段。所以我添加了那个字段,现在你的查询运行了。

      只需单击“计划文本”即可查看计划文本(如果您使用的是 Enterprise,您会看到计划图的可视版本)。

      计划文本显示查询正在使用玩家存储桶上的 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 值进行组织,而不是依赖存储桶。

      【讨论】:

      • 嗨,马修,是的,n 代表一个通用数字,但我认为它是 1、2 或 3 没有任何区别 :) 就是说,你猜对了(抱歉,我跳过了一些细节为简洁起见),我在各个字段上创建了主索引和二级索引(我越来越熟悉 Couchbase)。我不需要扫描整个存储桶,这就是为什么我在 WHERE 子句中添加了两个过滤器(m.day = 1 和 m.type = "xxx")。如果这不起作用,我怎样才能让 Couchbase 仅扫描与此过滤器匹配的那些文档?在这种情况下,分页不是一种选择。谢谢
      • 我是说这些过滤器是您的 SO 答案更快的原因。这不是因为您要反转联接,而是因为您执行的联接较少。
      • 嗨,通过查看计划文本,我实际上可以理解“反转联接”时发生了什么。谢谢你的解释;)
      【解决方案3】:

      到目前为止,第一步是积极的,我们创建了存储桶(每个 SQLite 表一个存储桶)并导入了所有数据(每个 SQLite 行一个 JSON 文档)

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

      首先,您应该使用一个存储桶。存储桶更像是数据库而不是表(尽管它比表更复杂),Couchbase 建议每个集群使用单个存储桶,除非您有充分的理由不这样做。它有助于提高性能、扩展性和资源利用率。您的每个文档都应该有一个指示数据类型的字段。这就是将您的“表格”分开的原因。我使用了一个名为“_type”的字段。例如。您将拥有“播放器”和“标记”文档类型。

      其次,您应该重新考虑将数据导入为每个文档一行。文档数据库为您提供不同的架构选项,其中一些对于提高性能非常有用。你当然可以保持这种方式,但它可能不是最佳的。这是开发人员在首次使用 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 查询所有玩家的标记。这也让您可以在单个键:值调用中提取玩家的文档和标记,这是最快的一种。

      【讨论】:

      • 嗨@Dave,谢谢你的回答。你是对的,我可能应该重新考虑我的数据结构,以利用 NoSQL 的好处并避免缺点。我担心迁移到 NoSQL 会比预期的更艰难,这只是一个确认。这将是一段很长的路:)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-10
      • 2011-11-13
      • 2011-06-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多