【问题标题】:SELECT with multiple subqueries to same tableSELECT 对同一个表的多个子查询
【发布时间】:2010-10-11 14:55:04
【问题描述】:

我一遍又一遍地使用相同的 SQL 模式,而且我知道必须有更好的方法,但我无法将它们拼凑在一起。这是该模式的一个简单版本,我在其中提取学生的信息和他们检查的最后一本书(如果存在):

SELECT TStudents.*,
       BookName = (SELECT TOP 1 BookName 
                     FROM TBookCheckouts 
                    WHERE StudentID = TStudents.ID 
                 ORDER BY DateCheckedOut DESC),
       BookAuthor = (SELECT TOP 1 BookAuthor 
                       FROM TBookCheckouts 
                      WHERE StudentID = TStudents.ID 
                   ORDER BY DateCheckedOut DESC),
       BookCheckout = (SELECT TOP 1 DateCheckedOut 
                         FROM TBookCheckouts 
                         WHERE StudentID = TStudents.ID 
                     ORDER BY DateCheckedOut DESC)
   FROM TStudents

(为了这个例子,请忽略 TBookCheckouts 可能应该分为 TCheckouts 和 TBooks 的事实)

我要说明的是:我倾向于对同一个表中的列有很多子查询。我还倾向于需要按日期对这些子查询表进行排序以获取最新记录,因此它不像执行 LEFT JOIN 那样简单(至少对我而言)。但是请注意,除了返回哪个字段之外,我基本上是在执行相同的子查询 3 次。 SQL Server 可能足够聪明,可以优化它,但我不这么认为(我肯定需要更好地阅读执行计划......)。

虽然以这种方式构建它可能有优势(有时这最终会更具可读性,如果我有大量的子查询和子表),但这似乎并不是特别有效。

我已经研究过从派生表中进行 LEFT JOIN,可能包含 ROW_NUMBER() 和 PARTITION BY,但我似乎无法将它们拼凑在一起。

【问题讨论】:

  • 如果多本书在同一天借阅怎么办?有决胜局的情况吗?
  • @LittleBobbyTables 好问题。幸运的是这不是一个真实的场景,所以我不必真正考虑这个哈哈。在我的真实场景中,我通常只是获取最新的资源,如果同一日期有两个,那要么是其他地方的错误,要么我不在乎。

标签: sql sql-server tsql subquery


【解决方案1】:

如果您使用的是 SQL Server 2005 及更高版本,则可以使用如下所示的排名函数:

With LastCheckout As
    (
    Select StudentId, BookName, BookAuthor, DateCheckedOut 
        , Row_Number() Over ( Partition By StudentId Order By DateCheckedOut Desc) As CheckoutRank
    From TBookCheckouts
    )
Select ..., LastCheckout.BookName, LastCheckout.BookAuthor, LastCheckout.DateCheckedOut
From TStudents
    Left Join LastCheckout 
        On LastCheckout.StudentId = TStudents.StudentId
                And LastCheckout.CheckoutRank = 1

【讨论】:

  • 我喜欢 CTE;我可能会使用一个,具体取决于查询。我希望我能给出两个被接受的答案。
  • 实际上我正在将接受的答案切换给您,因为我认为 CTE 解决方案效果更好。谢谢!
  • @Jon Smock:使用 CTE 并没有提高性能,但阅读起来更干净。
  • @OMG Ponies 这基本上就是我切换的原因。我实际上是从你的解决方案开始的,但是一旦我开始工作,我做的第一件事就是用 CTE 重写它来清理它。就像我说的,我真的希望我能给出两个被接受的答案,特别是因为你们都提交了彼此的会议记录。我确实给了你一个加分:-P
  • @Jon Smock,@OMG Ponies - 支持投票。正如 OMG Ponies 所说,没有性能差异。它只是更具可读性。
【解决方案2】:

在 2005 年及更高版本中,OUTER APPLY 是您的朋友:

SELECT TStudents.*,
       t.BookName ,
       t.BookAuthor ,
       t.BookCheckout
   FROM TStudents
  OUTER APPLY(SELECT TOP 1 s.* 
                     FROM TBookCheckouts AS s
                    WHERE s.StudentID = TStudents.ID 
                 ORDER BY s.DateCheckedOut DESC) AS t

【讨论】:

    【解决方案3】:

    用途:

       SELECT s.*,
              x.bookname,
              x.bookauthor,
              x.datecheckedout
         FROM TSTUDENTS s
    LEFT JOIN (SELECT bc.studentid,
                      bc.bookname,
                      bc.bookauthor,
                      bc.datecheckedout,
                      ROW_NUMBER() OVER(PARTITION BY bc.studentid
                                            ORDER BY bc.datecheckedout DESC) AS rank
                 FROM TSBOOKCHECKOUTS bc) x ON x.studentid = s.id
                                           AND x.rank = 1
    

    如果学生没有借阅任何书籍,booknamebookauthordatecheckedout 将为 NULL。

    【讨论】:

      【解决方案4】:

      如果您想使用公用表表达式,可以使用以下查询。在这种情况下,它不会为您带来任何好处,但对于未来:

      ;with LatestBookOut as 
      (
          SELECT  C.StudentID, BookID, Title, Author, DateCheckedOut AS BookCheckout 
          FROM    CheckedOut AS C
          INNER JOIN ( SELECT StudentID, 
                              MAX(DateCheckedOut) AS DD 
                      FROM Checkedout 
                      GROUP BY StudentID) StuMAX                 
          ON StuMAX.StudentID = C.StudentID 
          AND StuMAX.DD = C.DateCheckedOut  
      )
      
      SELECT    B.BookCheckout,
              BookId, 
              Title,    
              Author, 
              S.*
      
      FROM    LatestBookOut AS B
      INNER JOIN Student  AS S ON S.ID = B.StudentID 
      

      【讨论】:

      • 这不会只返回一条记录总数吗?我正在为每位学生寻找 1 条记录。
      • @Jon:这里有更新。我从我的测试数据库中保留了相同的名称,并且没有使用相同的名称,因为看起来你在你的问题中被混淆/嘲笑了。希望这会有所帮助!
      【解决方案5】:
      create table BookCheckout(StudentID int, CheckoutDate date, BookName varchar(10))
      
      insert into BookCheckout values (1, '1.1.2010', 'a');
      insert into BookCheckout values (1, '2.1.2010', 'b');
      insert into BookCheckout values (1, '3.1.2010', 'c');
      insert into BookCheckout values (2, '1.1.2010', 'd');
      insert into BookCheckout values (2, '2.1.2010', 'e');
      
      select *
      from BookCheckout bc1
      where CheckoutDate = (
          Select MAX(CheckoutDate) 
          from BookCheckout bc2
          where bc2.StudentID= bc1.StudentID)
      
      StudentID    CheckoutDate    BookName
      2    2010-01-02    e
      1    2010-01-03    c    
      

      只需将连接添加到 TStudent 即可。 剩下 1 个问题:如果有 2 个或更多 Bookcheckout 的学生具有相同的最大结帐日期,您将获得每个学生的多个 BookCheckouts。

        select s.*, LastBookCheckout.*
        from TStudent s, 
          (select *
          from BookCheckout bc1
          where CheckoutDate = (
              Select MAX(CheckoutDate) 
              from BookCheckout bc2
              where bc2.StudentID= bc1.StudentID)) LastBookCheckout
        where s.ID = LastBookCheckout.StudentID
      

      为避免重复:

      select * 
      from (
        select *, RANK() over (partition by StudentID order by CheckoutDate desc,BookName) rnk
          from BookCheckout bc1) x
      where rnk=1
      

      我使用“BookName”作为第二个排序标准。 => 改用主键使其成为真正的唯一标准。

      【讨论】:

      • 这真的不能解决他的问题,正如你所说的You get multiple BookCheckouts per student if there are 2 or more Bookcheckouts for a Student with the same, max checkout date.
      • @LittleBobbyTables:添加了另一个没有重复的解决方案
      • 但缺少从未借过书的学生
      • 我考虑过用 TStudent 添加左连接。
      【解决方案6】:

      OMGPonis 的回答很好。为了便于阅读,我会用通用表表达式来编写它:

      WITH CheckoutsPerStudentRankedByDate AS (
          SELECT bookname, bookauthor, datecheckedout, studentid,
              ROW_NUMBER(PARTITION BY studentid ORDER BY datecheckedout DESC) AS rank
          FROM TSBOOKCHECKOUTS
      )
      SELECT 
          s.*, c.bookname, c.bookauthor, c.datecheckedout
      FROM TSTUDENTS AS s
      LEFT JOIN CheckoutsPerStudentRankedByDate AS c
          ON s.studentid = c.studentid
          AND c.rank = 1
      

      c.rank = 1 可以替换为 c.rank IN(1, 2) 用于最后 2 个结帐,BETWEEN 1 AND 3 用于最后 3 个等等...

      【讨论】:

        【解决方案7】:

        试试

            ;WITH LatestCheckouts
            AS
            (
                SELECT  DISTINCT
                        A.StudentID
                    ,   A.BookName   
                    ,   A.BookAuthor
                    ,   A.DateCheckedOut
                FROM    TBookCheckouts A
                    INNER JOIN
                (   
                    SELECT  StudentID
                    ,   DateCheckedOut =  MAX(DateCheckedOut)
                     FROM TBookCheckouts
                    GROUP  BY
                        StudentID
                ) B
        
                ON A.StudentID = B.StudentID
                AND A.DateCheckedOut =  B.DateCheckedOut
            )       
            SELECT students.*
                ,  BookName     = checkouts.BookName
                ,  BookAuthor   = checkouts.BookAuthor
                ,  BookCheckout = checkouts.DateCheckedOut
        
            FROM    TStudents students
                LEFT JOIN
                 LatestCheckouts checkouts
            ON  students.ID = checkouts.StudentID
        

        【讨论】:

        • 关闭,但这不会给我 TOP 1 部分。我需要每个学生一份记录。
        • @LittleBobbyTables,修复了编译错误。我相信现在应该可以了。
        • 如果学生在同一天签出多本书,则查询会多次返回该学生。
        • 是的,它需要我添加的 DISTINCT。但是其他一些解决方案更紧凑,所以真的只是为了记录。
        【解决方案8】:

        希望这就是你要找的,我知道的一种简单的方法

        SELECT (SELECT TOP 1 BookName 
                         FROM TBookCheckouts 
                        WHERE StudentID = TStudents.ID 
                     ORDER BY DateCheckedOut DESC)[BOOK_NAME],
           (SELECT TOP 1 BookAuthor 
                           FROM TBookCheckouts 
                          WHERE StudentID = TStudents.ID 
                       ORDER BY DateCheckedOut DESC)[BOOK_AUTHOR],
           (SELECT TOP 1 DateCheckedOut 
                             FROM TBookCheckouts 
                             WHERE StudentID = TStudents.ID 
                         ORDER BY DateCheckedOut DESC)[DATE_CHECKEDOUT]
        

        这就是我遇到此类问题时的解决方法,我认为这将是您的情况的解决方案。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-03-19
          • 1970-01-01
          • 1970-01-01
          • 2018-08-20
          • 1970-01-01
          • 2021-08-25
          • 1970-01-01
          相关资源
          最近更新 更多