如何从数据库中获取用于 Graphql 分页的游标?

How to get a cursor for pagination in Graphql from a database?

我在获取真正的游标来解决 GraphQL 中的数据库分页结果时遇到了严重的问题。不管我用的是什么数据库(SQL e.g. mysql or NoSQL document e.g. mongodb),没办法,我好像能搞定光标或类似光标的对象。

可能我遗漏了一些基本概念,但在搜索我的...之后,我开始严重怀疑官方 GraphQL 分页文档是否如此

https://graphql.org/learn/pagination/

完全基于任何真实的现场体验。

这是我的问题:我怎样才能从这样的 SQL 查询中获得任何类似于游标的东西?

SELECT authors.id, authors.last_name, authors.created_at FROM authors
ORDER BY authors.last_name, author.created_at
LIMIT 10
OFFSET 20

我知道,不应该使用基于偏移的分页,而基于光标的导航被认为是一种补救措施。而且我绝对想治愈我的应用程序的抵消疾病。但为了做到这一点,我需要能够从 某个地方 .

检索光标

我也明白(忘了是在哪里读到的)主键也不应该用于分页。

所以,我被困在这里了。

我认为您因为提出了一个好问题而被否决了。 first/last/before/after 概念很难在 SQL 中实现。

我一直在为同样的问题伤脑筋。分页文档没有说明在您应用自定义 ORDER 语句时如何定义游标。

而且我也没有真正在网上找到全面的解决方案。我发现一些 posts 的人正在解决这个问题,但答案只是部分正确或部分完整(只是 base64 编码 ID 字段以使光标似乎是首选答案,但这说明很少关于查询实际上必须做什么来计算游标)。此外,任何涉及 row_number 的解决方案都非常丑陋,不适用于不同的 SQL 方言。因此,让我们尝试不同的方法。

快速免责声明,这将是一个相当全面的 post,但是如果您的后端使用一个不错的查询构建器,您可以在技术上编写一个方法来实现 first/last/before/after Relay GraphQL 要求的分页到 ANY 预先存在的查询。唯一的要求是你排序的 tables 都有一个唯一代表记录默认顺序的列(通常如果你的主键是一个整数并且使用自动生成的 ID,你可以使用那个一,即使在技术上按其主键排序 table 并不总是会产生与 return 未排序的 table 相同的结果)

暂时忘掉 base64,假设 ID 是代表 table.

默认顺序的有效游标字段

你在网上找到的关于使用游标的答案通常是这样的。

SELECT * FROM TABLE T
WHERE T.id > $cursorId;

好吧,只要您不对查询应用任何其他排序,这对于获取光标后的所有条目非常有效。一旦您使用示例中的自定义排序,此建议就会失效。

然而,其中的核心逻辑可以重新应用于排序查询,但解决方案需要扩展。让我们试着想出完整的算法。


算法 c 后的第一个 n (光标后的前 n 个节点)

节点或边与 SQL 术语中的行相同。 (如果 1 行代表单个实体,例如 1 位作者)

虽然光标是我们将在其后开始 return 兄弟行的行,无论是向前还是向后。

给定C是光标

A 是与 C.

进行比较的任何其他行

T 是 table 其中 AC 都是行。

v w x y z是tableT[=160=上的5列,自然都是AC 有这些列。

算法必须根据游标对象、给定 n 和提供的这 5 列的顺序来决定 A 是否包含在 return 查询中。

让我们从一个订单开始。

假设有 1 个顺序 (v):(如果我们假设我们的 table 是按其主要顺序排序的,那么至少应该始终有一个顺序默认键) 要显示 前 n 条记录,我们需要应用 n 限制,这很简单。困难的部分是c之后。

对于仅按 1 个字段排序的 table,可归结为:

 SELECT A FROM T
 WHERE A.v > C.v
 ORDER BY T.v ASC
 LIMIT n

这应该显示 v 大于 C 的所有行,并删除 v 小于 C 的所有行,这意味着在 C 之前不会留下任何行。如果我们假设主键正确表示自然规律,我们 可以删除 ORDER BY 语句。那么该查询的可读性稍强的版本将变为:

 SELECT A FROM T
 WHERE A.id > $cursorIdGivenByClient
 LIMIT n

至此,我们得出了为 'unsorted' table 提供游标的最简单解决方案。这与处理游标的普遍接受的答案相同,但不完整。

现在让我们看一下按两列(vw)排序的查询:

 SELECT A FROM T
 WHERE A.v > C.v
 OR (A.v = C.v AND A.w > C.w)
 ORDER BY T.v ASC, T.w ASC
 LIMIT n

我们从相同的 WHERE A.v > C.v 开始,从中删除值 v (A.v) 小于第一次排序 (C.v) 的 C 值的任何行输出结果。但是,如果一阶 v 的列对于 A 和 C 具有相同的值,A.v = C.v 我们需要查看二阶列以查看是否仍允许在查询结果中显示 A。如果 A.w > C.w

就是这种情况

让我们继续进行具有 3 种排序的查询:

 SELECT A FROM T
 WHERE A.v > C.v
 OR (A.v = C.v AND A.w > C.w)
 OR (A.v = C.v AND A.w = C.w AND A.x > C.x)
 ORDER BY T.v ASC, T.w ASC, T.x ASC
 LIMIT n

这与 2 种排序的逻辑相同,但更有效一些。如果第一列相同,我们需要查看第二列,看看谁最大。如果第二列也一样,我们需要查看第三列。重要的是要认识到主键始终是 ORDER BY 语句中的最后一个排序列,也是要比较的最后一个条件。在这种情况下 A.x > C.x(或 A.id > $cursorId)

无论如何,模式应该开始出现了。要对 4 列进行排序,查询将如下所示:

 SELECT A FROM T
 WHERE A.v > C.v
 OR (A.v = C.v AND A.w > C.w)
 OR (A.v = C.v AND A.w = C.w AND A.x > C.x)
 OR (A.v = C.v AND A.w = C.w AND A.x = C.x AND A.y > C.y)
 ORDER BY T.v ASC, T.w ASC, T.x ASC, T.y ASC
 LIMIT n

最后对 5 列进行排序。

 SELECT A FROM T
 WHERE A.v > C.v
 OR (A.v = C.v AND A.w > C.w)
 OR (A.v = C.v AND A.w = C.w AND A.x > C.x)
 OR (A.v = C.v AND A.w = C.w AND A.x = C.x AND A.y > C.y)
 OR (A.v = C.v AND A.w = C.w AND A.x = C.x AND A.y = C.y AND A.z > C.z)
 ORDER BY T.v ASC, T.w ASC, T.x ASC, T.y ASC, T.z ASC
 LIMIT n

比较的数量多得吓人。对于添加的每一个订单,计算 first n after c 所需的比较次数会随着在每一行上执行的 Triangular Number 而增加。幸运的是,我们可以应用一些布尔代数来压缩和优化这个查询。

 SELECT A FROM T
 WHERE (A.v > C.v OR
           (A.v = C.v AND 
              (A.w > C.w OR
                   (A.w = C.w AND
                       (A.x > C.x OR
                           (A.x = C.x AND
                               (A.y > C.y OR
                                    (A.y = C.y AND
                                        (A.z > C.z)))))))))
 ORDER BY T.v ASC, T.w ASC, T.x ASC, T.y ASC, T.z ASC
 LIMIT n

即使凝聚之后,纹路也很清晰。每个条件行在 OR 和 AND 之间改变,每个条件行在 > 和 = 之间改变,最后每 2 个条件行我们比较下一个顺序列。

而且这种比较的性能也令人惊讶。在第一个 A.v > C.v 检查后,平均有一半的行符合条件并停在那里。而另一半通过的,大多数将在第二个 A.v = C.v 检查时失败并停在那里。因此,虽然它可能会产生大量查询,但我不会太担心性能。

但让我们具体一点,并使用它来回答您如何在所讨论的示例中使用游标:

 SELECT authors.id, authors.last_name, authors.created_at FROM authors
 ORDER BY authors.last_name, author.created_at

您的基本查询是否已排序,但尚未分页。

您的服务器收到一个请求,要求显示“带光标的作者之后的前 20 位作者” 游标解码后,我们发现它代表的是id为15的作者

首先我们可以 运行 一个小的先行查询来获取我们需要的必要信息:

 $authorLastName, $authorCreatedAt =
      SELECT authors.last_name, authors.created_at from author where id = 15;

然后我们应用算法并替换字段:

  SELECT a.id, a.last_name, a.created_at FROM authors a
  WHERE (a.last_name > $authorLastName OR
            (a.last_name = $authorLastName AND 
               (a.created_at > $authorCreatedAt OR
                    (a.created_at = $authorCreatedAt AND
                        (a.id > 15)))))
 ORDER BY a.last_name, a.created_at, a.id
 LIMIT 20;

此查询将根据查询的排序正确return ID 15 作者之后的前 20 位作者。

如果您不喜欢使用变量或辅助查询,您也可以使用子查询:

  SELECT a.id, a.last_name, a.created_at FROM authors a
  WHERE (a.last_name > (select last_name from authors where id 15) OR
            (a.last_name = (select last_name from authors where id 15) AND 
               (a.created_at > (select created_at from authors where id 15)  OR
                    (a.created_at = (select created_at from authors where id 15) AND
                        (a.id > 15)))))
 ORDER BY a.last_name, a.created_at, a.id
 LIMIT 20;

同样,这并不像看起来那么糟糕,子查询不相关并且结果将缓存在行循环中,因此它不会对性能造成特别糟糕的影响。但是查询确实变得混乱,尤其是当您开始使用 JOINS 时,它也需要在子查询中应用。

您不需要在 a.id 上显式调用 ORDER,但我这样做是为了与算法保持一致。如果您使用 DESC 而不是 ASC,它确实变得非常重要。

那么,如果您使用 DESC 列而不是 ASC 会怎样?算法会崩溃吗?好吧,如果你应用一个小的额外规则。对于使用 DESC 而不是 ASC 的列,您将“>”符号替换为“<”,该算法现在将适用于双向排序。

JOINS 对这个算法没有影响(谢天谢地),除了来自连接 tables 的 20 行不一定代表 20 个实体(在这种情况下是 20 个作者),但这是一个与整个 first/after 问题无关的问题,使用 OFFSET 也会遇到该问题。

处理已经存在 WHERE 条件的查询也不是特别困难。您只需获取所有预先存在的条件,将它们括在括号中,然后将它们与 AND 语句组合到算法生成的条件。

在那里,我们实现了一种算法,可以处理任何输入查询并使用 first/after 对其进行正确分页。 (如果有我遗漏的边缘情况,请告诉我)

你可以到此为止,但不幸的是

你还需要处理first n, last n, before c, c之后,c之前的最后n个,c之后的最后n个和c之前的第n个c 如果你想符合 GraphQL Relay 规范并完全摆脱偏移 :).

您可以使用我刚刚提供的给定 AFTER 算法完成一半。但对于另一半,您将需要使用 BEFORE 算法。它与 AFTER 算法非常相似:

 SELECT A FROM T
 WHERE (A.v < C.v OR
           (A.v = C.v AND 
              (A.w < C.w OR
                   (A.w = C.w AND
                       (A.x < C.x OR
                           (A.x = C.x AND
                               (A.y < C.y OR
                                    (A.y = C.y AND
                                        (A.z < C.z)))))))))
 ORDER BY T.v ASC, T.w ASC, T.x ASC, T.y ASC, T.z ASC
 LIMIT n

要获得 BEFORE 算法,您可以采用 AFTER 算法并将所有“<”运算符切换为“>”运算符,反之亦然。 (所以本质上前后是相同的算法 BEFORE/AFTER + ASC/DESC 决定运算符必须指向哪个方向。)

对于 'first n',除了将 'LIMIT n' 应用于查询外,您无需执行任何操作。

对于 'last n' 您需要应用 'LIMIT n' 并反转所有给定的 ORDERS ,将 ASC 与 DESC 切换,将 DESC 与 ASC 切换。 'last n' 有一个警告,虽然它会正确地 return 最后 n 条记录,但它会以相反的顺序这样做,因此您需要再次手动反转 returned 集,无论是在您的数据库中还是在您的代码中。

使用这些规则,您可以成功地将来自 Relay GraphQL 规范的任何分页请求集成到任何 SQL 查询中,使用唯一的 sortable 列(通常是主键)作为代表table.

默认排序的真实来源

这非常令人生畏,但我设法使用这些算法为 Doctrine DQL 构建器编写了一个插件,以使用 MySQL 数据库实现 first/last/before/after 分页方法。所以这绝对是可行的。