【问题标题】:SQL Cross Tab QuerySQL 交叉表查询
【发布时间】:2014-10-23 14:28:59
【问题描述】:

需要帮助确定如何在一个查询中生成交叉表报表。涉及到 3-4 个表,但用户表可能不需要包含在查询中,因为我们只需要计数。

我将表格架构和数据的屏幕截图放在一起作为示例,如下所示:

我需要它返回一个查询结果,如下所示:

所以我可以制作如下所示的报告:

我尝试做光标循环,因为这是我用我的基本知识可以做到的唯一方法,但它太慢了。

我正在尝试生成的一份特定报告包含 32 行和 64 列以及大约 70,000 个答案,因此这完全是关于将其归结为一个查询并尽可能快的性能。

我知道这可能取决于索引等,但如果有人可以帮助我弄清楚如何在 1 个查询中完成这项工作(使用多个连接?),那就太棒了!

谢谢!

【问题讨论】:

  • 你读过“SQL PIVOT”吗?
  • 您可以使用数据透视查询,或者如果您在所需的输出中有一组固定的列,您可以使用 case 语句。或者,更好的是,您可以使用报告工具来处理您的演示文稿。
  • 我研究了 SQL PIVOT,但不知道如何在我的场景中使用它。不幸的是,使用第 3 方报告工具不是一种选择。
  • 只有 2 个问题,所以您只需要查看 question2 的答案吗?
  • 是的,它只会是 2 个问题的交叉表。一个问题的选项是列,一个是行。这些问题永远不会相同,因此不能以任何方式硬编码。

标签: sql sql-server performance tsql sql-server-2012


【解决方案1】:

我想我看到了问题所在。我知道您无法修改架构,但您需要一个用于交叉表信息的概念表,例如哪个 questionID 是 rowHeader,哪个是 colHeader。您可以在外部数据源中创建它并与现有数据源连接,或者简单地将表值硬编码到 sql 中。

您需要有 2 个问题/选项/答案关系实例,每个 rowHeader 一个,每个交叉表一个 colHeader。这两个关系由 userID 连接。

这个版本有你的外连接: sqlFiddle

这个版本没有 crossTab 表,只有 row 和 col questionID 硬编码: sqlFiddleNoTbl

【讨论】:

  • 感谢您@Beth 的努力,Mike 的回答有点直接适合我的代码,但总体上几乎相同的答案。谢谢 ! :)
【解决方案2】:
SELECT MIN(ro.OptionText) RowOptionText, MIN(co.OptionText) RowOptionText, COUNT(ca.AnswerID) AnswerCount
FROM tblQuestions rq 
CROSS JOIN tblQuestions cq 
JOIN tblOptions ro ON rq.QuestionID = ro.QuestionID
JOIN tblOptions co ON cq.QuestionID = co.QuestionID
LEFT JOIN tblAnswers ra ON ra.OptionID = ro.OptionID
LEFT JOIN tblAnswers ca ON ca.OptionID = co.OptionID AND ca.UserID = ra.UserID
WHERE rq.questionText = 'Gender'
AND cq.questionText = 'How happy are you?'
GROUP BY ro.OptionID, co.OptionID
ORDER BY ro.OptionID, co.OptionID

这至少应该接近您的要求。将其转换为数据透视表需要动态 SQL,因为 SQL Server 要求您指定将转换为一列的实际值。

我们交叉连接问题并将每个问题引用的结果限制为分别针对行值和列值的单个问题。然后我们将选项值加入到相应的问题参考中。如果用户没有回答所有问题,我们使用 LEFT JOIN 作为答案。我们通过 UserID 加入答案,以便我们匹配每个用户的行问题和列问题。选项文本上的 MIN 是因为我们按 OptionID 进行分组和排序以匹配您显示的排序。

编辑:这是SQLFiddle

不管怎样,您的查询很复杂,因为您使用的是实体-属性-值设计模式。相当多的 SQL Server 专家认为该模式存在问题,应尽可能避免。例如见https://www.simple-talk.com/sql/t-sql-programming/avoiding-the-eav-of-destruction/。

编辑 2:既然您接受了我的回答,这里是动态 SQL 数据透视解决方案 :) SQLFiddle

DECLARE @SqlCmd NVARCHAR(MAX)

SELECT @SqlCmd = N'SELECT RowOptionText, ' + STUFF(
    (SELECT ', ' + QUOTENAME(o.OptionID) + ' AS ' + QUOTENAME(o.OptionText)
    FROM tblOptions o 
    WHERE o.QuestionID = cq.QuestionID
    FOR XML PATH ('')), 1, 2, '') + ', RowTotal AS [Row Total]
FROM (
    SELECT ro.OptionID RowOptionID, ro.OptionText RowOptionText, co.OptionID ColOptionID,
       ca.UserID, COUNT(ca.UserID) OVER (PARTITION BY ra.OptionID) AS RowTotal
    FROM tblOptions ro
    JOIN tblOptions co ON ro.QuestionID = ' + CAST(rq.QuestionID AS VARCHAR(10)) + 
    ' AND co.QuestionID = ' + CAST(cq.QuestionID AS VARCHAR(10)) + '
    LEFT JOIN tblAnswers ra ON ra.OptionID = ro.OptionID
    LEFT JOIN tblAnswers ca ON ca.OptionID = co.OptionID AND ca.UserID = ra.UserID
    UNION ALL 
    SELECT 999999, ''Column Total'' RowOptionText, co.OptionID ColOptionID,
       ca.UserID, COUNT(ca.UserID) OVER () AS RowTotal
    FROM tblOptions ro
    JOIN tblOptions co ON ro.QuestionID = ' + CAST(rq.QuestionID AS VARCHAR(10)) + 
    ' AND co.QuestionID = ' + CAST(cq.QuestionID AS VARCHAR(10)) + '
    LEFT JOIN tblAnswers ra ON ra.OptionID = ro.OptionID
    LEFT JOIN tblAnswers ca ON ca.OptionID = co.OptionID AND ca.UserID = ra.UserID
) t
PIVOT (COUNT(UserID) FOR ColOptionID IN (' + STUFF(
    (SELECT ', ' + QUOTENAME(o.OptionID) 
    FROM tblOptions o 
    WHERE o.QuestionID = cq.QuestionID
    FOR XML PATH ('')), 1, 2, '') + ')) p
ORDER BY RowOptionID'
FROM tblQuestions rq 
CROSS JOIN tblQuestions cq 
WHERE rq.questionText = 'Gender' 
AND cq.questionText = 'How happy are you?'

EXEC sp_executesql @SqlCmd

【讨论】:

  • 我将研究数据库设计,看看我能用它做什么。该数据库已有 10 多年的历史,拥有 1 亿条记录,因此更改设计有点棘手。无论如何,您的回答正是我所需要的。将其放入实时数据库会在 3 秒内返回 30 行 x 60 列报告的结果。 :) 谢谢!
  • sqlFiddle 不包含列值?您将 colOption 的别名与 rowOption 相同,因此它只返回第一列
  • 很好,@Beth,你是绝对正确的。立即修复。
  • 感谢@MikeStankavich,动态 SQL 效果很好!
【解决方案3】:

下面的一团乱麻在没有硬编码值的情况下工作,但无法显示计数为 0 的行。 但是,这可能仍然适用于您的报告。

;with stepone as(

SELECT
    RANK() OVER(PARTITION BY a.UserId ORDER BY o.QuestionID) AS [temprank]
,   o.QuestionID AS [QID1]
,   o.OptionID AS [OID1]
,   same.QuestionID
,   same.OptionID
,   a.UserId AS [IDUser]
,   same.UserId
FROM
    tblAnswers a
    INNER JOIN
    tblOptions o
        ON a.OptionID = o.OptionID
    INNER JOIN
    tblQuestions q
        ON o.QuestionID = q.QuestionID
    INNER JOIN
    (
    SELECT
        a.AnswerID
    ,   a.OptionID
    ,   a.UserId
    ,   o.QuestionID    
    FROM
        tblAnswers a
        INNER JOIN
        tblOptions o
            ON a.OptionID = o.OptionID
    ) same
        ON a.UserId = same.UserId AND a.AnswerID <> same.AnswerID

)

, stepthree AS(
SELECT
    t.QID1, t.OID1, t.QuestionID, t.OptionID
,   COUNT(UserId) AS myCount
FROM 
    stepone t
WHERE t.temprank = 1
GROUP BY
    t.QID1, t.OID1, t.QuestionID, t.OptionID
)

SELECT
    o1.OptionText AS [RowTest]
,   o2.OptionText AS [ColumnText]
,   t.myCount AS [Count]

FROM
    stepthree t
    INNER JOIN tblOptions o1
        ON t.OID1 = o1.OptionID
    INNER JOIN tblOptions o2
        ON t.OptionID = o2.OptionID
ORDER BY t.OID1

希望它有所帮助,我喜欢尝试这样做。

【讨论】:

  • 我将在实时代码中尝试一下,看看它与其他代码相比如何。谢谢!
  • @MikeStankavich 版本的外观更适合您的需求。 (虽然将问题文本放在 sql 中,但绝对是作弊);)
猜你喜欢
  • 1970-01-01
  • 2015-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-07
  • 2014-04-01
  • 2012-06-22
  • 1970-01-01
相关资源
最近更新 更多