SELECT с несколькими подзапросами в одну таблицу

Я использую один и тот же шаблон 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, но я просто не могу собрать все это вместе.

Ответы

Ответ 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

Ответ 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

Если студент не проверяет какие-либо книги, bookname, bookauthor и datecheckedout будут иметь значение NULL.

Ответ 4

Try

    ;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

Ответ 5

Если вы хотите использовать выражение Common Table Expression, вы можете выполнить следующий запрос. Это не принесет вам ничего, в данном случае, но для будущего:

;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 

Ответ 6

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 проблема: вы получаете несколько экземпляров BookCheckouts для каждого учащегося, если есть 2 или более книжных чеков для ученика с той же максимальной датой проверки.

  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" в качестве второго критерия упорядочивания. = > Вместо этого используйте первичный ключ, чтобы сделать его реальным уникальным критерием.

Ответ 7

Ответ OMGPonies - хороший. Я бы написал его с помощью Common Table Expressions для чтения:

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) для последних двух проверок, BETWEEN 1 AND 3 для последних 3 и т.д.