使用多个左连接查询 - 点列值不正确

时间:2014-12-11 12:30:42

标签: mysql database join left-join

我有以下数据库结构,我正在尝试运行单个查询,该查询将显示教室,教室中有多少学生,教室分配了多少奖励,以及分配了多少积分到一个教室(基于classroom_id专栏)。

使用最底层的查询我正在尝试收集教室分配的'totalPoints' - 基于在classroom_redeemed_codes表中计算points列并将其作为单个整数返回。

由于某些原因,totalPoints的值不正确 - 我做错了但不确定是什么......

- 更新 - 这是sqlfiddle: - http://sqlfiddle.com/#!2/a9f45

我的结构:

CREATE TABLE `organisation_classrooms` (
  `classroom_id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `active` tinyint(1) NOT NULL,
  `organisation_id` int(11) NOT NULL,
  `period` int(1) DEFAULT '0',
  `classroom_bg` int(2) DEFAULT '3',
  `sortby` varchar(6) NOT NULL DEFAULT 'points',
  `sound` int(1) DEFAULT '0',
  PRIMARY KEY (`classroom_id`)
);

CREATE TABLE organisation_classrooms_myusers (
  `classroom_id` int(11) NOT NULL,
  `user_id` bigint(11) unsigned NOT NULL,
);

CREATE TABLE `classroom_redeemed_codes` (
  `redeemed_code_id` int(11) NOT NULL AUTO_INCREMENT,
  `myuser_id` bigint(11) unsigned NOT NULL DEFAULT '0',
  `ssuser_id` bigint(11) NOT NULL DEFAULT '0',
  `classroom_id` int(11) NOT NULL,
  `order_product_id` int(11) NOT NULL DEFAULT '0',
  `order_product_images_id` int(11) NOT NULL DEFAULT '0',
  `date_redeemed` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `points` int(11) NOT NULL,
  `type` int(1) NOT NULL DEFAULT '0',
  `notified` int(1) NOT NULL DEFAULT '0',
  `inactive` tinyint(3) NOT NULL,
   PRIMARY KEY (`redeemed_code_id`),
);

SELECT
  t.classroom_id,
  title,
  COALESCE (
    COUNT(DISTINCT r.redeemed_code_id),
      0
   ) AS totalRewards,
  COALESCE (
    COUNT(DISTINCT ocm.user_id),
    0
   ) AS totalStudents,
  COALESCE (sum(r.points), 0) AS totalPoints
  FROM
  `organisation_classrooms` `t`
   LEFT OUTER JOIN classroom_redeemed_codes r ON (
   r.classroom_id = t.classroom_id
   AND r.inactive = 0
   AND (
    r.date_redeemed >= 1393286400
    OR r.date_redeemed = 0
   )
   )
   LEFT OUTER JOIN organisation_classrooms_myusers ocm ON (
   ocm.classroom_id = t.classroom_id
   )
   WHERE
    t.organisation_id =37383
   GROUP BY title
   ORDER BY t.classroom_id ASC
   LIMIT 10

- 编辑 -

OOPS!我有时讨厌SQL ......我犯了一个大错误,我试图计算 classroom_redeemed_codes 中的学生人数而不是organisation_classrooms_myuser表。我真的很抱歉我应该早点把它拿走?!

classroom_id | totalUniqueStudents
     16             1
     17             2
     46             1
     51             1
     52             1

classroom_redeemed_codes表中有7行,但由于teacher_id 46有两行,但myuser_id相同(这是学生ID),这应该显示为一个独特的学生。

这有意义吗?基本上试图根据myuser_id列获取classroom_redeemed_codes表中唯一学生的数量。

例如,一个教室id 46可以在classroom_redeemed_codes表中有100行,但如果它们的每个myuser_id相同,则应该显示totalUniqueStudents计为1而不是100。

如果不清楚,请告诉我。

- 更新 - 我有以下查询,似乎工作借用了一个似乎工作的用户...(我的头痛)我会再次接受答案。很抱歉这个混乱 - 我想我只是在想这个

select crc.classroom_id,
    COUNT(DISTINCT crc.myuser_id) AS users,
    COUNT( DISTINCT crc.redeemed_code_id ) AS classRewards,
    SUM( crc.points ) as classPoints, t.title
  from classroom_redeemed_codes crc
       JOIN organisation_classrooms t
         ON crc.classroom_id = t.classroom_id 
        AND t.organisation_id = 37383
        where crc.inactive = 0
        AND ( crc.date_redeemed >= 1393286400
        OR crc.date_redeemed = 0 )
        group by crc.classroom_id

3 个答案:

答案 0 :(得分:7)

我首先运行每个特定类的点数的预查询聚合,然后使用左连接到它。我在结果集中获得的行数多于您预期的样本,但是没有MySQL可以直接测试/确认。但是here is a SQLFiddle of your query通过使用点总和进行查询,并在应用users表时得到笛卡尔结果,这可能是复制点的基础。通过预先查询兑换代码本身,您只需获取该值,然后加入用户。

SELECT
      t.classroom_id,
      title,
      COALESCE ( r.classRewards, 0 ) AS totalRewards,
      COALESCE ( r.classPoints, 0) AS totalPoints,
      COALESCE ( r.uniqStudents, 0 ) as totalUniqRedeemStudents,
      COALESCE ( COUNT(DISTINCT ocm.user_id), 0 ) AS totalStudents
   FROM
      organisation_classrooms t
         LEFT JOIN ( select crc.classroom_id,
                            COUNT( DISTINCT crc.redeemed_code_id ) AS classRewards,
                            COUNT( DISTINCT crc.myuser_id ) as uniqStudents,
                            SUM( crc.points ) as classPoints
                        from classroom_redeemed_codes crc
                           JOIN organisation_classrooms t
                              ON crc.classroom_id = t.classroom_id 
                              AND t.organisation_id = 37383
                        where crc.inactive = 0
                          AND ( crc.date_redeemed >= 1393286400
                           OR crc.date_redeemed = 0 )
                        group by crc.classroom_id ) r
            ON t.classroom_id = r.classroom_id

         LEFT OUTER JOIN organisation_classrooms_myusers ocm 
            ON t.classroom_id = ocm.classroom_id
   WHERE
      t.organisation_id = 37383
   GROUP BY 
      title
   ORDER BY 
      t.classroom_id ASC
   LIMIT 10

答案 1 :(得分:5)

你需要sum(r.points)和左外连接中的子查询,见下面的

 SELECT
 t.classroom_id,
 title,
 COALESCE (
   COUNT(DISTINCT r.redeemed_code_id),
     0
  ) AS totalRewards,
 COALESCE(sum(r.points),0) AS totalPoints
,COALESCE(sum(T1.cnt),0) as totalStudents
 FROM
  `organisation_classrooms` `t`
left outer join (select classroom_id, count(user_id) cnt
                  from organisation_classrooms_myusers 
 group by classroom_id) T1 on (T1.classroom_id=t.classroom_id)
  LEFT OUTER JOIN classroom_redeemed_codes r ON (
  r.classroom_id = t.classroom_id
  AND r.inactive = 0
  AND (
   r.date_redeemed >= 1393286400
   OR r.date_redeemed = 0
  )
  )
  WHERE
   t.organisation_id =37383
  GROUP BY title
  ORDER BY t.classroom_id ASC
  LIMIT 10

答案 2 :(得分:4)

我简化了您的查询;没有必要将COALLESCECOUNT()一起使用,因为COUNT()永远不会返回NULL。对于SUM(),我更喜欢使用IFNULL(),因为它更短,更易读。下面显示的结果仅包含classroom_id#16,#17和#46的数据,以便于与问题中提供的示例进行比较。实际结果集更大,包含表中存在的所有classroom_id。但是,不需要他们的存在来理解它的工作原理和原因。

SELECT
  t.classroom_id,
  t.title,
  COUNT(DISTINCT r.redeemed_code_id) AS totalRewards,
  COUNT(DISTINCT ocm.user_id) AS totalStudents,
  IFNULL(SUM(r.points), 0) AS totalPoints
FROM `organisation_classrooms` t
  LEFT JOIN `classroom_redeemed_codes` r
    ON r.classroom_id = t.classroom_id
    AND r.inactive = 0
    AND (r.date_redeemed >= 1393286400 OR r.date_redeemed = 0)
  LEFT JOIN `organisation_classrooms_myusers` ocm
    ON ocm.classroom_id = t.classroom_id
WHERE t.organisation_id = 37383
GROUP BY t.classroom_id
ORDER BY t.classroom_id ASC

让我们尝试将它分成几块,然后将它们放在一起。首先,让我们看看用户的选择:

查询#1

SELECT
  t.classroom_id,
  t.title,
  ocm.user_id
FROM `organisation_classrooms` t
  LEFT JOIN `organisation_classrooms_myusers` ocm
    ON ocm.classroom_id = t.classroom_id
WHERE t.organisation_id = 37383
ORDER BY t.classroom_id ASC

我删除了classroom_redeemed_codes表及其字段,删除了GROUP BY并将汇总函数COUNT(ocm.user_id)替换为ocm.user_id,以查看用户的选择情况。

结果显示我们这部分查询是正确的:

classroom_id | title | user_id
-------------+-------+--------
16           | BLUE  | 2
16           | BLUE  | 1
17           | GREEN | 508835
17           | GREEN | 508826
46           | PINK  | NULL

在教室#16中有2个用户,在#7中有2个用户,在#46类中没有用户。 放回GROUP BY子句将使其在totalStudents列中返回正确的值(2,2,0)。

现在让我们检查与表classroom_redeemed_codes的关系:

查询#2

SELECT
  t.classroom_id,
  t.title,
  r.redeemed_code_id, r.points
FROM `organisation_classrooms` t
  LEFT JOIN `classroom_redeemed_codes` r
    ON r.classroom_id = t.classroom_id
    AND r.inactive = 0
    AND (r.date_redeemed >= 1393286400 OR r.date_redeemed = 0)
WHERE t.organisation_id = 37383
ORDER BY t.classroom_id ASC

结果是:

classroom_id | title | redeemed_code_id | points
-------------+-------+------------------+-------
16           | BLUE  | 7                | 50
17           | GREEN | 8                | 25
17           | GREEN | 9                | 75
46           | PINK  | 5                | 250
46           | PINK  | 6                | 100

同样,按classroom_id分组会在totalRewards列中生成(1,2,2),在totalPoints列中生成(50,100,350),这是正确的。

如果要将这些内容组合到单个查询中,则会出现问题。无论您使用何种类型的连接,对于提供的输入,您将获得classroom_id的(2 * 1,2 * 2,1 * 2)行,其值为16,17和46(按此顺序)。我在括号中乘以的值是第一个和上面的查询结果集中每个classroom_id的行数。

联合

让'在分组之前尝试选择行的查询:

SELECT
  t.classroom_id,
  t.title,
  r.redeemed_code_id, ocm.user_id, r.points
FROM `organisation_classrooms` t
  LEFT JOIN `classroom_redeemed_codes` r
    ON r.classroom_id = t.classroom_id
    AND r.inactive = 0
    AND (r.date_redeemed >= 1393286400 OR r.date_redeemed = 0)
  LEFT JOIN `organisation_classrooms_myusers` ocm
    ON ocm.classroom_id = t.classroom_id
WHERE t.organisation_id = 37383
ORDER BY t.classroom_id ASC

返回此结果集:

classroom_id | title | redeemed_code_id | user_id | points
-------------+-------+------------------+---------+-------
16           | BLUE  | 7                | 2       | 50          
16           | BLUE  | 7                | 1       | 50      <- *
-------------+-------+------------------+---------+-------
17           | GREEN | 8                | 508835  | 25
17           | GREEN | 8                | 508826  | 25      <- *
17           | GREEN | 9                | 508835  | 75
17           | GREEN | 9                | 508826  | 75      <- *
-------------+-------+------------------+---------+-------
46           | PINK  | 5                | NULL    | 250
46           | PINK  | 6                | NULL    | 100

当我们添加GROUP BY子句时,我添加了水平规则来分隔属于同一组的行。这基本上是执行带有SQL的{​​{1}}查询的方式,无论实现它的实际软件的名称如何。

正如您所看到的,对于每个教室,它将与教室相关的所有已兑换代码与与教室相关的所有用户组合在一起。如果您为表格中的教室#16,#17和#46添加更多用户和兑换代码,您将获得更大的结果集。

执行GROUP BY查询的下一步是从上面看到的每个组生成一行。列GROUP BYclassroom_id没有问题,它们在每个组中都包含一个值。对于列titleredeemed_code_id,您的查询会计算不同的值,并且也可以正常工作。问题在于添加了user_id。 如果您只是points,则会为该组中的每个user_id添加已兑换的代码。如果您使用SUM(),那么它也是错误的,因为它会忽略重复项,即使它们是表SUM(DISTINCT points)中的不同条目。

您想要为classroom_redeemed_codes添加points。我在上面的结果集上标记了你不想要的行。

使用此查询无法做到这一点,因为在计算聚合值时,每列都独立于另一列。我们需要一个查询,在对它们进行分组之前选择所需的行。

一个想法

我们可以尝试将缺少的列(包含DISTINCT redeemed_code_id值)添加到上面的两个简单查询NULL,然后从中选择UNION ALL

首先,让我们确定它选择了我们需要的东西:

GROUP BY

注意! SELECT t.classroom_id, t.title, NULL AS redeemed_code_id, ocm.user_id, NULL AS points FROM `organisation_classrooms` t LEFT JOIN `organisation_classrooms_myusers` ocm ON ocm.classroom_id = t.classroom_id WHERE t.organisation_id = 37383 UNION ALL SELECT t.classroom_id, t.title, r.redeemed_code_id, NULL AS user_id, r.points FROM `organisation_classrooms` t LEFT JOIN `classroom_redeemed_codes` r ON r.classroom_id = t.classroom_id AND r.inactive = 0 AND (r.date_redeemed >= 1393286400 OR r.date_redeemed = 0) WHERE t.organisation_id = 37383 ORDER BY classroom_id 子句适用于ORDER BY ed结果集。如果您想订购每个UNION的行(它没有帮助,因为SELECT没有保留订单),您需要将该查询括在括号中并放入{{那里有条款。

结果集看起来很棒:

UNION

现在我们可以在上面的查询周围添加一些括号(strip ORDER BY)并在另一个查询中使用它,按classroom_id | title | redeemed_code_id | user_id | points -------------+-------+------------------+---------+------- 16 | BLUE | NULL | 1 | NULL 16 | BLUE | NULL | 2 | NULL 16 | BLUE | 7 | NULL | 50 -------------+-------+------------------+---------+------- 17 | GREEN | 8 | NULL | 25 17 | GREEN | 9 | NULL | 75 17 | GREEN | NULL | 508826 | NULL 17 | GREEN | NULL | 508835 | NULL -------------+-------+------------------+---------+------- 46 | PINK | 5 | NULL | 250 46 | PINK | 6 | NULL | 100 46 | PINK | NULL | NULL | NULL 对数据进行分组,计算用户和兑换的代码并对其点进行求和。

您将获得一个看起来很糟糕的查询,并且在您当前的数据库模式中,当您的表有几百行时会进行爬网。 这就是我不会在这里写的

<强>注意! 通过向表中添加缺少的索引,在查询的ORDER BYclassroom_idONWHERE子句中显示的字段,可以提高其性能。 / p>

它会带来显着的改善,但我不会非常依赖它。对于非常大的表(数十万行),它仍然会爬行。

另一个想法

我们还可以先在查询#1 查询#2 上添加ORDER BY,然后再添加GROUP BY

GROUP BY

这会产生一个很好的结果集:

UNION ALL

此查询可以嵌入到另一个查询中,该查询按上面的总列数SELECT t.classroom_id, t.title, NULL AS totalRewards, COUNT(DISTINCT ocm.user_id) AS totalStudents, NULL AS totalPoints FROM `organisation_classrooms` t LEFT JOIN `organisation_classrooms_myusers` ocm ON ocm.classroom_id = t.classroom_id WHERE t.organisation_id = 37383 GROUP BY t.classroom_id UNION ALL SELECT t.classroom_id, t.title, COUNT(DISTINCT redeemed_code_id) AS totalRewards, NULL AS totalStudents, SUM(points) AS totalPoints FROM `organisation_classrooms` t LEFT JOIN `classroom_redeemed_codes` r ON r.classroom_id = t.classroom_id AND r.inactive = 0 AND (r.date_redeemed >= 1393286400 OR r.date_redeemed = 0) WHERE t.organisation_id = 37383 GROUP BY t.classroom_id ORDER BY classroom_id, totalRewards classroom_id | title | totalRewards | totalStudents | totalPoints -------------+-------+--------------+---------------+------------- 16 | BLUE | NULL | 2 | NULL 16 | BLUE | 1 | NULL | 50 17 | GREEN | NULL | 2 | NULL 17 | GREEN | 2 | NULL | 100 46 | PINK | NULL | 0 | NULL 46 | PINK | 2 | NULL | 350 进行分组,以获得最终结果。但同样,最后的查询是大而丑陋的 对于大型表来说,运行速度不是很快。再一次,这就是我在这里写不出来的原因

结论

它可以在一个查询中完成,但它看起来并不好,而且在大型表上也不能很好地工作。

关于效果,将classroom_id放在查询前面,然后检查结果列SUM()EXPLAINtype中的值。有关这些列的可能值的说明,尝试实现的内容以及应避免的内容,请参阅documentation

我在两个创意上创建的两个查询都会生成keyExtra类型的联接以及range列中的ALL(所有这些都很慢)。相反,在较大的查询中将它们用作子查询不会改善它们的执行方式。

我建议您将上一个代码示例中的各个Using filesort个查询作为两个单独的查询运行;它们将从上面的结果集中返回奇数行和偶数行。然后将其结果合并到客户端代码中。它会以这种方式运行得更快。