web-dev-qa-db-fra.com

SELECT avec plusieurs sous-requêtes à la même table

J'utilise le même modèle SQL encore et encore, et je sais qu'il doit y avoir un meilleur moyen, mais j'ai du mal à le reconstituer. Voici une version simple du modèle, où je retire les informations sur les étudiants et le dernier livre extrait, le cas échéant:

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

(Pour les besoins de cet exemple, veuillez ignorer le fait que TBookCheckouts devrait probablement être scindé en TCheckouts et TBooks)

Ce que j'essaie d'illustrer: j'ai tendance à avoir beaucoup de sous-requêtes pour les colonnes de la même table. J'ai aussi tendance à avoir besoin de trier ces tables sous-classées par une date pour obtenir le dernier enregistrement. Ce n'est donc pas aussi simple (du moins pour moi) que de faire un LEFT JOIN. Notez cependant que, sauf pour le champ renvoyé, je fais essentiellement la même sous-requête 3 fois. SQL Server est peut-être assez intelligent pour optimiser cela, mais je ne le pense pas (il faut absolument que je lise mieux les plans d'exécution ...).

Bien qu'il puisse y avoir des avantages à la structurer de cette façon (parfois cela finit par être plus lisible, si j'ai des tonnes de sous-requêtes et de sous-tables), il ne semble pas que cela soit particulièrement efficace.

J'ai envisagé de créer une jointure LEFT à partir d'une table dérivée, en incorporant éventuellement un ROW_NUMBER () et un PARTITION BY, mais je n'arrive pas à rassembler tous les éléments.

13
Jon Smock

Si vous utilisez SQL Server 2005 et versions ultérieures, vous pouvez utiliser une fonction de classement comme suit:

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
12
Thomas

À partir de 2005, OUTER APPLY est votre ami:

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
9
A-K

Utilisation:

   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

Si l'étudiant n'a pas acheté de livres, les variables bookname, bookauthor et datecheckedout seront NULL.

3
OMG Ponies
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    

Ajoutez simplement la jointure à TStudent et vous avez terminé. Il reste un problème: vous obtenez plusieurs BookCheckouts par étudiant s’il existe au moins deux Bookcheckouts pour un étudiant avec la même date de sortie maximale.

  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

Pour éviter les doublons:

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

J'ai utilisé "BookName" comme critère de second ordre. => Utilisez plutôt la clé primaire pour en faire un critère vraiment unique.

0
nang

Essayer

    ;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
0
Noel Abrahams

La réponse d'OMGPonies est bonne. Je l'écrirais avec des expressions de table communes pour plus de lisibilité:

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

Le c.rank = 1 peut être remplacé par c.rank IN(1, 2) pour les 2 derniers paiements, BETWEEN 1 AND 3 pour les 3 derniers, etc ...

0
thomaspaulb

J'espère que c'est ce que vous recherchez, un moyen simple que je connais pour ces cas

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]

C’est ce que j’ai résolu lorsque j’ai fait face à un problème comme celui-ci. Je pense que ce serait la solution à votre cas.

0
Dennis

Si vous souhaitez utiliser une expression de table commune, vous pouvez utiliser la requête suivante. Cela ne vous rapporte rien, dans ce cas, mais pour l'avenir:

;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 
0
p.campbell