MySQL是否有一种方法可以“聚集”作为聚合函数?

时间:2020-04-13 02:27:15

标签: mysql

我正在尝试采用现有的应用程序并重新架构该架构,以支持新的客户请求并解决几个未解决的问题(大多数情况是围绕我们当前的架构进行了高度非规范化)。这样做时,我遇到了一个有趣的问题,乍看之下似乎有一个简单的解决方案,但似乎找不到我要寻找的功能。

该应用程序是一种媒体组织工具。

我们的旧架构:

我们的旧架构具有针对“组”,“子组”和“视频”的单独模型。一个小组可以有多个子组(一对多),一个小组可以有许多视频(一对多)。

“组”,“子组”和“视频”之间共享某些字段。例如,将视频嵌入页面时要使用的Google Analytics(分析)ID。每当我们显示嵌入页面时,我们都会首先查看是否在Video上设置了该值。如果没有,我们检查了其子组。如果没有,我们检查了它的组。该查询看起来大致像这样(我希望这是真正的查询,但是不幸的是,我们的应用程序是由许多初级开发人员编写的,所以事实要痛苦得多):

SELECT
    v.id,
    COALESCE(v.google_analytics_id, sg.google_analytics_id, g.google_analytics_id) as google_analytics_id
FROM
    Videos v
    LEFT JOIN Subgroups sg ON sg.id = v.subgroup_id
    LEFT JOIN Groups g ON g.id = sg.group_id

相当简单。现在,我们遇到的问题是客户希望能够任意深度嵌套组,而我们的架构显然只允许2个级别(实际上, 需要 >两个级别-即使您只想要一个级别)

新架构(首次通过):

作为第一步,我知道我们想要一个用于Groups的基本树结构,所以我想到了:

CREATE TABLE Groups (
    id INT PRIMARY KEY,
    name VARCHAR(255),
    parent_id INT,
    ga_id VARCHAR(20)
)

然后我们可以轻松地嵌套多达N个级别的N个连接,如下所示:

SELECT
    v.id,
    COALESCE(v.ga_id, g1.ga_id, g2.ga_id, g3.ga_id, ...) as ga_id
FROM
    Videos v
    LEFT JOIN Groups g1 ON g1.id = v.group_id
    LEFT JOIN Groups g2 ON g2.id = g1.parent_id
    LEFT JOIN Groups g3 ON g3.id = g2.parent_id
    ...

这种方法存在明显的缺陷:我们不知道会有多少父母,所以我们不知道应该加入多少次,迫使我们实施“最大深度”。然后,即使深度最大,如果一个人只有一个级别的组,我们仍然会执行多个JOIN,因为我们的查询无法知道他们需要走多深。 MySQL提供了递归查询,但是在研究这是否是正确的选项时,我发现了产生相同结果的更智能的模式

新模式(第2步):

寻找更好的方法来处理树结构,我了解了邻接表(我先前的解决方案),嵌套集,物化路径和闭包表。除了邻接表(依靠JOIN来获取整个树结构并因此在树上的每个节点上生成一行包含多列的单行)之外,其他三种解决方案都为树上的每个节点返回多行

我最终采用了这样的闭包表解决方案:

CREATE TABLE Groups (
    id INT PRIMARY KEY,
    name VARCHAR(255),
    ga_id VARCHAR(20)
)
CREATE TABLE Group_Closure (
    ancestor_id INT,
    descendant_id INT,
    PRIMARY KEY (ancestor_id, descendant_id)
)

现在有了视频,我可以像这样获得所有的父母

SELECT
    v.id,
    v.ga_id,
    g.id,
    g.ga_id
FROM
    Videos v
    JOIN Group_Closure gc ON v.group_id = gc.descendant
    JOIN Groups g ON g.id = gc.ancestor;

这会将层次结构中的每个组作为单独的行返回:

+------+---------+------+---------+
| v.id | v.ga_id | g.id | g.ga_id |
+------+---------+------+---------+
|   1  |  abc123 |   2  | new_val |
|   1  |  abc123 |   1  | default |
|   2  |   NULL  |   4  |  xyz987 |
|   2  |   NULL  |   3  |   NULL  |
|   2  |   NULL  |   1  | default |
|   3  |   NULL  |   3  |   NULL  |
|   3  |   NULL  |   1  | default |
+------+---------+------+---------+

我现在想要做的是以某种方式获得与在多个自联接组表上使用COALESCE所期望的相同结果:ga_id的单个值基于“最低”节点在树上

由于每个视频有多行,我怀疑可以使用GROUP BY和某种聚合函数来完成此操作:

SELECT
    v.id,
    COALESCE(v.ga_id, FIRST_NON_NULL(g.ga_id))
FROM
    Videos v
    JOIN Group_Closure gc ON v.group_id = gc.descendant
    JOIN Groups g ON g.id = gc.ancestor
GROUP BY v.id, v.ga_id;

请注意,因为(ancestor, descendant)是我的主键,所以我相信可以保证组关闭表的顺序总是相同的-这意味着如果我将最低的节点放在第一位,它将是第一行在结果查询中...如果我对此理解不正确,请告诉我。

1 个答案:

答案 0 :(得分:1)

如果要坚持使用邻接表,则可以使用递归CTE。这个从每个video id值开始遍历,直到找到一个非NULL ga_id

WITH RECURSIVE CTE AS (
  SELECT id, ga_id, group_id
  FROM videos
  UNION ALL
  SELECT CTE.id, COALESCE(CTE.ga_id, g.ga_id), g.parent_id
  FROM `groups` g
  JOIN CTE ON g.id = CTE.group_id AND CTE.ga_id IS NULL
)
SELECT id, ga_id
FROM CTE
WHERE ga_id IS NOT NULL

对于我尝试从您的问题中重建数据的尝试,结果如下:

id  ga_id
1   abc123
2   xyz987
3   default

Demo on dbfiddle