MySQL是否有一种方法可以;联合;作为聚合函数?

MySQL是否有一种方法可以;联合;作为聚合函数?,mysql,Mysql,我正在尝试使用一个现有的应用程序,重新构建模式,以支持新的客户请求,并修复几个悬而未决的问题(主要是围绕我们当前的模式被严重非规范化)。在这样做的过程中,我遇到了一个有趣的问题,乍一看似乎有一个简单的解决方案,但我似乎找不到我正在寻找的函数 该应用程序是一个媒体组织工具 我们的旧模式: 我们的旧模式对“组”、“子组”和“视频”有单独的模型。一个组可以有多个子组(一对多),一个子组可以有多个视频(一对多) 在组、子组和视频中共享某些字段。例如,将视频嵌入页面时使用的Google Analytics

我正在尝试使用一个现有的应用程序,重新构建模式,以支持新的客户请求,并修复几个悬而未决的问题(主要是围绕我们当前的模式被严重非规范化)。在这样做的过程中,我遇到了一个有趣的问题,乍一看似乎有一个简单的解决方案,但我似乎找不到我正在寻找的函数

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

我们的旧模式: 我们的旧模式对“组”、“子组”和“视频”有单独的模型。一个组可以有多个子组(一对多),一个子组可以有多个视频(一对多)

在组、子组和视频中共享某些字段。例如,将视频嵌入页面时使用的Google Analytics ID。无论何时显示嵌入页面,我们都会首先查看视频中是否设置了该值。如果没有,我们检查它的子组。如果没有,我们检查了它的组。查询看起来大致如此(我希望这是真正的查询,但不幸的是,我们的应用程序是由许多初级开发人员多年编写的,因此事实上要痛苦得多):

非常直截了当。现在我们遇到的问题是,客户希望能够将组嵌套到任意深度,而我们的模式显然只允许两个级别(事实上,需要两个级别,即使您只需要一个级别)

新模式(首次通过): 作为第一步,我知道我们需要一个组的基本树结构,所以我提出了以下建议:

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
    ...
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;
这种方法有明显的缺陷:我们不知道会有多少家长,所以我们不知道应该加入多少次,这迫使我们实施“最大深度”。然后,即使有一个最大深度,如果一个人只有一个级别的组,我们仍然执行多个连接,因为我们的查询不知道他们需要到达多深。MySQL提供递归查询,但在研究这是否是正确的选择时,我发现了一个更智能的模式,可以产生相同的结果

新模式(以2为例): 为了更好地处理树结构,我学习了邻接列表(我以前的解决方案)、嵌套集、物化路径和闭包表。除了邻接列表(它依赖于连接来获取整个树结构,从而在树上的每个节点上生成具有多个列的单行),其他三种解决方案都为树上的每个节点返回多行

我最终使用了一个闭包表解决方案,如下所示:

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,
    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
    ...
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
指定一个值

因为我每个视频有多行,我怀疑这可以通过使用
groupby
和某种聚合函数来实现:

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;

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

如果您坚持使用邻接列表,您可以使用递归CTE。这一个从每个
视频
id值向上遍历,直到找到一个非
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

您使用的是哪一版本的MySQL?@Nick MySQL 8.0.19这似乎是可行的,尽管我对递归查询还不太熟悉,还不清楚它到底是如何工作的或为什么工作的。理想情况下,我希望答案不依赖MySQL 8的可移植性功能(以防我们切换到Postgres或其他东西),但至少在出现更好的解决方案之前(假设有更好的解决方案)。出于好奇,您知道递归查询对性能的影响吗?使用闭包表只需要两个连接操作和一个表扫描,所以速度非常快。@stevendesu几乎所有的DBMS都支持递归CTE(MySQL正在赶上版本8)。我不太熟悉闭包表(在锁定中可以阅读!)来确定您是否可以依赖所需的订购。就性能而言,这可能取决于您搜索有效的
ga_id
的深度。这可能只需要一个连接,就相当于整个树的深度,尽管每次连接都会变小。我今天早上刚刚测试了backtick操作符,缺少了backtick操作符,这在Postgres中引起了一些麻烦(我没有查找正确的语法,而是将
重命名为
视频组
,所以我不需要它们)递归CTE似乎可以在Postgres、Sqlite和MySQL 8中工作。所以这可能是我最好的选择。谢谢啊,是的,在PostgreSQL中需要双引号。正如您所说,将其重命名为非保留字更容易。我很高兴它能为您工作。我将大量虚拟数据转储到数据库中,开始尝试递归查询,但遇到了两个问题。首先,我注意到您的
连接中有
,并且CTE.ga_id为NULL
。如果我有多个不同的设置(不仅仅是
ga_id
),那么它们可以在递归查询的不同级别上进行设置。所以我不知道这个解决方案是否适用于多个列。第二,一旦我有了500000个视频(接近我们的制作数据集),递归查询就慢了。跑起来花了12秒!不幸的是,这是一个需要经常运行的查询。