SQL查询在每列中返回相同的值

时间:2016-03-29 19:43:19

标签: sql sql-server left-join

我在查询中遇到SQL连接问题,该查询旨在查询Post表已加入评论,点击和投票表并返回有关每个帖子活动的统计信息。我的查询是我一直在使用的。

SELECT
    p.PostID, 
    p.Title, 
    CASE 
        WHEN COUNT(cm.CommentID) IS NULL THEN 0
        ELSE COUNT(cm.CommentID)
    END AS CommentCount, 
    CASE
        WHEN COUNT(cl.ClickID) IS NULL THEN 0 
        ELSE COUNT(cl.ClickID)
    END AS ClickCount, 
    CASE
        WHEN SUM(vt.Value) IS NULL THEN 0
        ELSE SUM(vt.Value)
    END AS VoteScore
FROM 
    Post p
    LEFT OUTER JOIN Comment cm ON p.PostID = cm.PostID
    LEFT OUTER JOIN Click cl ON p.PostID = cl.PostID
    LEFT OUTER JOIN Vote vt ON p.PostID = vt.PostID
GROUP BY
    p.PostID, 
    p.Title

产生以下结果

| PostID | CommentCount | ClickCount | VoteScore |
|--------|--------------|------------|-----------|
| 41     |            60|          60|         60|
| 50     |          1683|        1683|       1683|

这个,我知道不对。当注释掉除了一个连接之外的所有连接时:

SELECT
     p.PostID 
    ,p.Title 
    ,CASE 
        WHEN COUNT(cm.CommentID) IS NULL THEN 0
        ELSE COUNT(cm.CommentID)
     END AS CommentCount
/*
    ,CASE
        WHEN COUNT(cl.ClickID) IS NULL THEN 0 
        ELSE COUNT(cl.ClickID)
     END AS ClickCount
    ,CASE
        WHEN SUM(vt.Value) IS NULL THEN 0
        ELSE SUM(vt.Value)
     END AS VoteScore
 */
FROM 
    Post p
    LEFT OUTER JOIN Comment cm ON p.PostID = cm.PostID
/*
    LEFT OUTER JOIN Click cl ON p.PostID = cl.PostID
    LEFT OUTER JOIN Vote vt ON p.PostID = vt.PostID
 */
GROUP BY
    p.PostID, 
    p.Title

我得到了

| PostID | CommentCount |
|--------|--------------|
| 41     |             3|

哪个是对的。我做错了什么想法?

感谢。

5 个答案:

答案 0 :(得分:3)

正在返回的结果是预期的,因为查询正在生成笛卡尔(或半笛卡尔)积。该查询基本上告诉MySQL对从commentclickvote返回的行执行“交叉连接”操作。

comment(对于给定的postid)返回的每一行都匹配click的每一行(对于相同的postid)。然后,该结果中的每一行都与vote中的每一行匹配(对于相同的postid)。

因此,对于来自comment的两行,来自click的三行和来自vote的四行,这将返回总共24(= 2x3x4)行。

解决此问题的常用模式是避免交叉连接操作。

有几种方法可以做到这一点。

选择列表中的相关子查询

如果您只需要来自三个表中的每个表的单个聚合(例如COUNT或SUM),则可以删除连接,并在SELECT列表中使用相关子查询。编写一个获取单个postid计数的查询,例如

 SELECT COUNT(1)
   FROM comment cmt
  WHERE cmt.postid = ?

然后将该查询包装在parens中,并在另一个查询的SELECT列表中引用它,并将问号替换为外部查询中引用的表中对postid的引用。

SELECT p.postid
     , ( SELECT COUNT(1)
           FROM comment cmt
           WHERE cmt.postid = p.postid
       ) AS comment_count
  FROM post p

重复相同的模式,从clickvote获取“计数”。

这种方法的缺点是SELECT列表中的子查询将由外部查询返回的每个行执行。因此,如果外部查询返回大量行,这可能会变得昂贵。如果comment是一个大表,那么为了获得合理的性能,comment上有适当的索引是至关重要的。

在内联视图中进行预聚合

另一种方法是“预聚合”结果内联视图。编写一个返回postid注释计数的查询。例如

SELECT cmt.postid 
     , COUNT(1) 
  FROM comment cmt
 GROUP BY cmt.postid 

在parens中包装该查询并在另一个查询的FROM子句中引用它,指定别名。内联视图查询基本上取代了外部查询中的表。

SELECT p.postid
     , cm.postid
     , cm.comment_count
  FROM post p 
  LEFT
  JOIN ( SELECT cmt.postid 
              , COUNT(1) AS comment_count
           FROM comment cmt
          GROUP BY cmt.postid
       ) cm
    ON cm.postid = p.postid

并为clickvote重复相同的模式。这里的技巧是内联视图查询中的GROUP BY子句,它保证它不会返回任何重复的postid值。而且笛卡尔积(交叉连接)不会产生重复。

这种方法的缺点是派生表不会被索引。因此对于大量postid,在外部查询中执行连接可能是昂贵的。 (更新版本的MySQL通过自动创建适当的索引来部分解决这个缺点。)

(我们可以通过创建具有适当索引的临时功能来解决此限制。但是这种方法需要额外的SQL语句,并不完全适用于特殊的单一语句。但对于大型集的批处理,额外的复杂性可以值得一些显着的性能提升。

按DISTINCT值折叠笛卡尔积

作为一种完全不同的方法,使用交叉连接操作保留您的查询,并允许MySQL生成笛卡尔积。然后SELECT列表中的聚合可以过滤掉重复项。这要求您为comment创建一个列(或表达式),该列对于给定postid的注释中的每一行都是唯一的。

SELECT p.postid
     , COUNT(DISTINCT c.id) AS comment_count
  FROM post p
  LEFT
  JOIN comment c
    ON c.postid = p.postid
 GROUP BY p.postid

这种方法的一大缺点是它有可能产生巨大的中间结果,然后通过“使用文件排序”操作“折叠”(以满足GROUP BY)。对于大型套装而言,这可能相当昂贵。

这不是列出所有可能的查询模式的详尽列表,以实现您希望返回的结果。只是一个代表性的抽样。

答案 1 :(得分:2)

你可能想要这样的东西:

SELECT
    p.PostID, 
    p.Title,
   (SELECT COUNT(*) FROM Comment cm
    WHERE cm.PostID = p.PostID) AS CommentCount,
   (SELECT COUNT(*) FROM Click cl 
    WHERE  p.PostID = cl.PostID) AS ClickCount ,
   (SELECT SUM(vt.Value) FROM Vote vt 
    WHERE p.PostID = vt.PostID) AS VoteScore 
FROM 
    Post p

您的查询存在问题,即第二个和第三个LEFT JOIN操作重复记录:在应用第一个LEFT JOIN后,您有PostID = 41的帖子记录,例如3个。第二个LEFT JOIN现在加入到这3条记录中,因此PostID = 41在第二个LEFT JOIN中使用 3次

如果(PostComment),(PostClick)和({}之间存在1:多关系直接 {1}},Post),那么上面的查询很可能会给你你想要的东西。

答案 2 :(得分:1)

您的查询未执行您认为正在执行的操作。当您加入并计算这样的行时,您将创建一个包含x行的新数据集,然后只计算该数据集中的行三次。因此,你得到三次相同的计数。

您要做的只是计算评论行,并点击左连接在这两个表中找到数据的位置,例如:

SELECT
   p.PostID 
   ,p.Title 
   ,COUNT(CASE 
      WHEN cm.PostID IS NULL THEN 0
      ELSE 1
    END) AS CommentCount

  ,COUNT(CASE
     WHEN cl.PostID IS NULL THEN 0 
     ELSE 1
   END) AS ClickCount
  ,SUM(CASE
     WHEN vt.PostID IS NULL THEN 0
     ELSE ISNULL(vt.Value,0)
   END) AS VoteScore

FROM 
  Post p
  LEFT OUTER JOIN Comment cm ON p.PostID = cm.PostID
  LEFT OUTER JOIN Click cl ON p.PostID = cl.PostID
  LEFT OUTER JOIN Vote vt ON p.PostID = vt.PostID
GROUP BY
  p.PostID, 
  p.Title

答案 3 :(得分:1)

已经解释了你的查询有什么问题:对于postid 41,说3条评论,5次点击和4票(每次投票值为1),你得到3x5x4 = 60次计数第一次和第二次计数表达式和3x5x4x1 = 60表示总和。

当与聚合一起处理多个外部联接时,您不能先连接表并稍后聚合,而是首先聚合并加入聚合:

select
  p.postid, 
  p.title, 
  coalesce(cm.cnt, 0) as commentcount, 
  coalesce(cl.cnt, 0) as clickcount, 
  coalesce(vt.total, 0) as votescore 
from post p
left outer join (select postid, count(*) as cnt from comment group by postid) cm
  on cm.postid = p.postid
left outer join (select postid, count(*) as cnt from click group by postid) cl
  on cl.postid = p.postid
left outer join (select postid, sum(value) as total from vote group by postid) vt
  on vt.postid = p.postid;

答案 4 :(得分:1)

COUNT计算非空值。但是将空值设置为0,它们就算了。将您的计数更改为SUM并将其移出案例,我认为它将解决问题。

EG

SELECT
    p.PostID, 
    p.Title, 
    SUM(CASE 
        WHEN cm.CommentID IS NULL THEN 0
        ELSE cm.CommentID
    END) AS CommentCount, 
    SUM(CASE
        WHEN cl.ClickID IS NULL THEN 0 
        ELSE cl.ClickID
    END) AS ClickCount, 
    SUM(CASE
        WHEN SUM(vt.Value IS NULL THEN 0
        ELSE SUM(vt.Value
    END) AS VoteScore
FROM 
    Post p
    LEFT OUTER JOIN Comment cm ON p.PostID = cm.PostID
    LEFT OUTER JOIN Click cl ON p.PostID = cl.PostID
    LEFT OUTER JOIN Vote vt ON p.PostID = vt.PostID
GROUP BY
    p.PostID, 
    p.Title