MySQL:如何select和处理"n to n"相关数据以提高性能

MySQL: How to select and process "n to n" related data with a view to performance

分配:
我需要 select 具有多对多关系的数据并搜索具有良好性能的解决方案。我目前有两个可行的解决方案(见下文)。

示例/说明:
比赛是由组织推动的。一场比赛可以有none个组织作为发起人。我只需要每场比赛在结果中出现一次,并提供发起人的 ID 列表。

数据结构:
- Table "competition": (id, name)
- Table "organisation": (id, name)
- Table "competition_organisation": (competitionID, organisationID)

所需结果:
|id|名称|promoter_list|
|1|欧洲冠军联赛|1241|
|2|国际足联世界杯|1240|
|3|FIFA 世界杯预赛 - 非洲|1240, 1242|

开发平台: Cold Fusion

数据库: MySQL

基于给定答案的附加说明:
- 我的问题的主要目的是找到一种比我过去更好地处理这种关系的方法。比赛只是我需要的例子之一。
- 我试着让它更简单,也许我忽略了一个事实。在我的应用程序中,我还需要组织名称。为此,我加入了组织table。
- 比赛有比我在这个例子中描述的更多的相关信息。我的应用程序中的查询使用多个连接到其他 tables.


方案一:
- 查询到select比赛数据
- 循环结果
- 将每条记录存储在循环内的数组中
- 附加查询 select 结果/循环中每条记录的启动子
- 将带有另一个查询循环的启动子 ID 添加到数组

主查询:

SELECT competition.id, competition.name
FROM competition
WHERE ...

循环内附加启动子查询:

SELECT DISTINCT organisation.id 
FROM organisation 
INNER JOIN competition_organisation 
ON competition_organisation.organisationID = organisation.id 
WHERE competition_organisation.competitionID = competition.id[currentrow]#

方案二:
- 仅使用一个子查询 select
- 循环结果
- 将每条记录存储在循环内的数组中

SELECT competition.id, competition.name,
(
    SELECT CONVERT(GROUP_CONCAT(organisation.id SEPARATOR ', ') USING utf8)
    FROM organisation
    WHERE organisation.id in 
    (
        SELECT DISTINCT competition_organisation.organisationID 
        FROM competition_organisation
        WHERE competition_organisation.competitionID = competition.id
    )
) AS promoter_list
FROM competition
WHERE ...

方案三(Spencer7593提出):

SELECT c.id, 
       c.name,
       CONVERT(GROUP_CONCAT(DISTINCT o.id ORDER BY o.id) USING utf8) AS promoter_id_list,
       CONVERT(GROUP_CONCAT(DISTINCT o.name ORDER BY o.id) USING utf8) AS promoter_list
FROM competition c
LEFT JOIN competition_organisation c_o ON c_o.competitionID = c.id
LEFT JOIN organisation o ON o.id = c_o.organisationID
GROUP BY c.id, c.name

(我稍微修改了代码并添加了组织名称)


方案四(Thorsten Kettner 提出,Rick James 优化):

SELECT id, name,
       ( SELECT CONVERT(GROUP_CONCAT(organisationID SEPARATOR ', ') USING utf8)         
             FROM competition_organisation
             WHERE competitionID = c.id
       ) AS promoter_id_list,
       ( SELECT CONVERT(GROUP_CONCAT(organisation.name SEPARATOR ', ') USING utf8)          
             FROM competition
             left join competition_organisation on competition_organisation.competitionID = competition.id
             left join organisationen on organisationen.id = competition_organisation.organisationID
             WHERE competitionID = c.id
       ) AS promoter_list
    FROM competition AS c

(还添加了组织名称,希望以正确的方式)


性能比较:

解决方案 1 - 100 条记录:~30ms + (100 x ~1ms) = ~130ms
解决方案 1 - 1000 条记录:~70ms + (1000 x ~1ms) = ~1070ms

解决方案 2 - 100 条记录:~5500 毫秒
解决方案 2 - 1000 条记录:~48000ms

解决方案 3 - 100 条记录:~120 毫秒
解决方案 3 - 1000 条记录:~210ms

解决方案 4 - 100 条记录:~110 毫秒
解决方案 4 - 1000 条记录:~200ms


如您所见,解决方案2的性能很糟糕。


结论:

我决定采用 Spencer 的解决方案 3。 3和4的性能几乎相同。但是 3 的代码更简单并且与我现有的查询完美匹配,尤其是与他们的左连接。

我对结果很满意。性能大大提高,以后我需要的代码/文件更少。

非常感谢您的帮助!

单个查询几乎总是比过程更快。如果不是,这可能表示查询中存在缺陷。

DISTINCT 不属于 IN 子查询。应该由 DBMS 如何 来查找数据。 IN 子句也应该是不相关的。如果您想要或需要相关的东西,请改用 EXISTS。那么,你为什么要加入tableorganisation呢? table.

你不需要任何东西
select c.id, c.name, co.promoter_list
from competition c
left join
(
  select 
    competitionid, 
    group_concat(organisationid separator ', ') as promoter_list
  from competition_organisation
  group by competitionid
) co on co.competitionid = c.id;

解决方案 3:

利用外连接操作和 MySQL 特定的 GROUP_CONCAT 聚合函数到 return 逗号分隔的 organizationid 值列表。

-- SHOW VARIABLES LIKE 'group_concat_max_len';
-- SET group_concat_max_len = 1048576;

SELECT c.id      AS id
     , c.name    AS name
     , GROUP_CONCAT(DISTINCT p.organisationid ORDER BY p.organisationid) AS promoter_list
  FROM competition c
  LEFT
  JOIN competition_organisation p
    ON p.competitionid = c.id
 GROUP
    BY c.id
     , c.name
 ORDER
    BY c.id
     , c.name

请注意,如果 GROUP_CONCAT 生成的字符串长度超过 group_concat_max_len,该字符串将自动截断为允许的长度。 (没有错误,没有警告)。

将returned字符串的字节长度与系统变量的值进行比较,检测字符串是否被截断。


organisation table 也可以包含在查询中,如果有必要或有充分的理由这样做的话。

SELECT c.id      AS id
     , c.name    AS name
     , GROUP_CONCAT(DISTINCT o.id ORDER BY o.id) AS promoter_list
  FROM competition c
  LEFT
  JOIN competition_organisation p
    ON p.competitionid = c.id
  LEFT
  JOIN organisation o
    ON o.id = p.organisationid
 GROUP
    BY c.id
     , c.name
 ORDER
    BY c.id
     , c.name

(注意:此答案包含两种改进 Thorsten 答案的方法,但我尚未验证它是否是 OP 问题的有效解决方案。)

方案A:

LEFT JOIN ( SELECT ... )很可能表现不佳。

SELECT id,
       name,
       ( SELECT GROUP_CONCAT(organisationid SEPARATOR ', ')
             FROM competition_organization
             WHERE competitionid = c.id
       ) AS promoter_list
    FROM competition AS c;

LEFT变成关联子查询,当什么都没有时returnNULL(或''?)

B计划:

如果不需要 LEFT——也就是说,如果总是有一个组织列表,那么这个重写可能会更好:

SELECT c.id, c.name, co.promoter_list
    FROM ( SELECT competitionid,
                  GROUP_CONCAT(organisationid SEPARATOR ', ') AS promoter_list
             FROM competition_organization
             GROUP BY competitionid
         ) AS co
    JOIN competition AS c  ON c.id = co.competitionid;

这样做的好处是可以完全构建 GROUP_CONCATsthen 之后的 idname

假设 cPRIMARY KEY(id)

两种变体都假定 coINDEX(competitionid, organizationid) 的顺序。

参见 http://mysql.rjweb.org/doc.php/index_cookbook_mysql#many_to_many_mapping_table 构建最优的多对多 table。