【问题标题】:Optimization of multiple aggregations in SELECTSELECT 中多个聚合的优化
【发布时间】:2011-12-21 14:15:33
【问题描述】:

我在Microsoft T-SQL Performance Tuning whitepaper 中读到,相关子查询在大型表上的性能方面可能代价高昂:

...将此与第一个进行比较 扫描整个表并为每个表执行相关子查询的解决方案 排。在一张小桌子上,性能上的差异可以忽略不计。但在一张大桌子上 可能需要数小时的处理时间...

是否有一种通用方法可以将具有基于不同标准的多个聚合的查询作为相关子查询转换为使用JOINs 而不是相关子查询的单个查询?

考虑一个例子:

准备架构:

CREATE TABLE Student (
    ID INT NOT NULL PRIMARY KEY IDENTITY(1,1),
    Name NVARCHAR(255) NOT NULL
);

CREATE TABLE Grade (
    ID INT NOT NULL PRIMARY KEY IDENTITY(1,1),
    StudentID INT NOT NULL FOREIGN KEY REFERENCES Student(ID),
    Score INT NOT NULL,
    CONSTRAINT CK_Grade_Score CHECK (Score >= 0 AND Score <= 100)
);

INSERT INTO Student (Name) VALUES ('Steven');
INSERT INTO Student (Name) VALUES ('Timmy');
INSERT INTO Student (Name) VALUES ('Maria');
 
INSERT INTO Grade (StudentID, Score) VALUES (1, 90);
INSERT INTO Grade (StudentID, Score) VALUES (1, 81);
INSERT INTO Grade (StudentID, Score) VALUES (1, 82);
INSERT INTO Grade (StudentID, Score) VALUES (1, 82);

INSERT INTO Grade (StudentID, Score) VALUES (2, 99);
INSERT INTO Grade (StudentID, Score) VALUES (2, 63);
INSERT INTO Grade (StudentID, Score) VALUES (2, 97);
INSERT INTO Grade (StudentID, Score) VALUES (2, 90);

INSERT INTO Grade (StudentID, Score) VALUES (3, 66);
INSERT INTO Grade (StudentID, Score) VALUES (3, 61);
INSERT INTO Grade (StudentID, Score) VALUES (3, 60);

有问题的查询:

SELECT Name,
    (SELECT AVG(Score) FROM Grade WHERE StudentID = Student.ID AND Score < 65) AS 'F',
    (SELECT AVG(Score) FROM Grade WHERE StudentID = Student.ID AND Score >= 65 AND Score < 70) AS 'D',
    (SELECT AVG(Score) FROM Grade WHERE StudentID = Student.ID AND Score >= 70 AND Score < 80) AS 'C',
    (SELECT AVG(Score) FROM Grade WHERE StudentID = Student.ID AND Score >= 80 AND Score < 90) AS 'B',
    (SELECT AVG(Score) FROM Grade WHERE StudentID = Student.ID AND Score >= 90 AND Score <= 100) AS 'A'
FROM Student

产生以下结果:

Name    F     D     C     B     A
-----------------------------------------
Steven  NULL  NULL  NULL  81    90
Timmy   63    NULL  NULL  NULL  95
Maria   60    66    NULL  NULL  NULL

我知道您可以在 COUNT() 中使用的技术,您可以在其中执行单个 SELECTJOIN,然后使用 CASE 语句在主键行时选择性地向计数器添加 1在您的加入和您的条件为真之间。我正在寻找一种类似的技术,可以应用于不同类型的聚合(而不仅仅是COUNT)。

有没有一种有效的方法可以将此示例查询转换为使用JOIN 而不是多个子查询?

【问题讨论】:

    标签: sql optimization aggregation correlated-subquery


    【解决方案1】:

    也许我遗漏了一些东西,但使用 CASE 的解决方案也适用于聚合:

    SELECT st.name, 
           avg(CASE WHEN g.score < 65 THEN g.score ELSE NULL END) as F,
           avg(CASE WHEN g.score >= 65 AND g.score < 70 THEN g.score ELSE NULL END) as D,
           avg(CASE WHEN g.score >= 70 AND g.score < 80 THEN g.score ELSE NULL END) as C,
           avg(CASE WHEN g.score >= 80 AND g.score < 90 THEN g.score ELSE NULL END) as B,
           avg(CASE WHEN g.score >= 90 AND g.score <= 100 THEN g.score ELSE NULL END) as A
    FROM Grade g
      JOIN Student st ON g.studentid = st.ID
    GROUP BY st.name
    

    【讨论】:

    • 效果很好,但是当学生包含多个条目时,计算可能会有所不同。在这个问题上,案例是最好的。
    • 为什么?这将是 OP 想要的每个学生的平均值 - 他也是每个 StudentID 的平均值。
    • Avarage 会有所不同,当查询包含其他联接的产品而不是 Student 表时,并且该产品包含多个相同的 touple - 在这种情况下,多子查询可能会有所帮助,因为有纠正结果的地方。例如使用 DISTINCT。这只是假设 - 不在问题的范围内。
    • @Max 会影响sum,count 但不会影响avg,因为如果使用Student 完成连接,则成绩的倍数相同。使用Grades 时不是这样
    • 我必须打勾,因为我认为它提供了最佳的可读性,同时提供了来自JOIN 的减少结果集(从而提高了大型表的性能)。我什至没有想过使用CASE 来返回NULL 以允许选择性平均。
    【解决方案2】:

    我使用 CTE 尝试了类似以下的操作,但结果与您得到的有点不同,因为它计算的是所有成绩的平均值:

    ;WITH
    Scores(ID,Score) AS(
        SELECT S.ID,AVG(Score)
        FROM Student S
        JOIN Grade G
            ON S.ID = G.StudentID
        GROUP BY S.ID)
    
    SELECT ST.Name
        ,CASE WHEN S.Score  < 65 THEN S.Score ELSE NULL END AS 'F'
        ,CASE WHEN S.Score  BETWEEN 65 AND 70 THEN S.Score ELSE NULL END AS 'D'
        ,CASE WHEN S.Score  BETWEEN 70 AND 80 THEN S.Score ELSE NULL END AS 'C'
        ,CASE WHEN S.Score  BETWEEN 80 AND 90 THEN S.Score ELSE NULL END AS 'B'
        ,CASE WHEN S.Score  BETWEEN 90 AND 100 THEN S.Score ELSE NULL END AS 'A'
    FROM Scores S
    JOIN Student ST
        ON S.ID = ST.ID
    

    【讨论】:

    • 我认为这个查询的结果会与 OP 的不同。
    【解决方案3】:

    试试这个:

    SELECT s.Name
        ,SUM(CASE Score_g WHEN 'F' THEN Score_avg END) as 'F'
        ,SUM(CASE Score_g WHEN 'D' THEN Score_avg END) as 'D'
        ,SUM(CASE Score_g WHEN 'C' THEN Score_avg END) as 'C'
        ,SUM(CASE Score_g WHEN 'B' THEN Score_avg END) as 'B'
        ,SUM(CASE Score_g WHEN 'A' THEN Score_avg END) as 'A'
    FROM Student s,
         (
          SELECT StudentId, score_g, avg(score) as score_avg
          FROM  (
                SELECT StudentID, Score
                CASE
                  WHEN Score < 65                   THEN 'F'
                  WHEN Score >= 65 AND Score < 70   THEN 'D'
                  WHEN Score >= 70 AND Score < 80   THEN 'C'
                  WHEN Score >= 80 AND Score < 90     THEN 'B'
                  WHEN Score >= 90 AND Score <= 100 THEN 'A'
                  ELSE 'X'
                END AS Score_g
                FROM Grade
            ) g
           GROUP BY StudentId, score_g
        ) t
    WHERE s.ID = t.StudentID
    GROUP BY s.Name
    

    如果你真的讨厌子查询,你可以使用:

    SELECT s.name
        ,AVG(CASE WHEN Score < 65                   THEN SCORE END) AS 'F'
        ,AVG(CASE WHEN Score >= 65 AND Score < 70   THEN SCORE  END) AS 'D'
        ,AVG(CASE WHEN Score >= 70 AND Score < 80   THEN SCORE  END) AS 'C'
        ,AVG(CASE WHEN Score >= 80 AND Score < 90     THEN SCORE END) AS 'B'
        ,AVG(CASE WHEN Score >= 90 AND Score <= 100 THEN SCORE  END) AS 'A'
    FROM Grade g, Student s
    WHERE g.StudentID = s.ID
    GROUP BY s.name
    

    但在这种情况下,学生表必须包含一个学生实体的唯一条目。

    【讨论】:

    • 它有点复杂,但做同样的事情。而不是使用相关查询,而是使用三遍计算。对于大数据更快。在您提供的数据中,可读性较差。
    【解决方案4】:

    如果你的 DBMS 支持PIVOT,你也可以试试这样的:

    ;WITH marked AS (
      SELECT
        StudentID,
        Score,
        Mark = CASE
          WHEN Score < 65 THEN 'F'
          WHEN Score < 70 THEN 'D'
          WHEN Score < 80 THEN 'C'
          WHEN Score < 90 THEN 'B'
          ELSE 'A'
        END
      FROM Grade
    ),
    pivoted AS (
      SELECT
        StudentID,
        F, D, C, B, A
      FROM marked m
      PIVOT (
        AVG(Score) FOR Mark IN (F, D, C, B, A)
      ) p
    )
    SELECT
      s.Name,
      p.F,
      p.D,
      p.C,
      p.B,
      p.A
    FROM Student s
      INNER JOIN pivoted p ON s.ID = p.StudentID
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-23
      • 2018-02-18
      • 1970-01-01
      • 2021-12-17
      • 2010-12-18
      • 2017-11-03
      • 2014-12-14
      • 1970-01-01
      相关资源
      最近更新 更多