在多对多查询中选择TOP 1

时间:2009-11-19 16:24:38

标签: sql sql-server-2005 join group-by

我有一个非常简单的多对多架构,如下所示:alt text http://img406.imageshack.us/img406/8207/partialschema.png

我要做的是选择所有具有一些仲裁条件的球员,我还想选择他们最近的比赛,如果他们在一场比赛中进行了比赛。

我设法做的是:

SELECT tblPlayer.PlayerId, tblPlayer.Surname, tblPlayer.Forename,
(SELECT TOP 1 tblMatch.HomeClub + ' v ' + tblMatch.OpponentClub + ' ' + tblMatch.AgeGroup + ' (' + CONVERT(VARCHAR, tblMatch.MatchDateTime, 103) + ')'
    FROM tblAppearance
    INNER JOIN tblMatch ON tblAppearance.MatchID = tblMatch.MatchID
    WHERE tblAppearance.PlayerID = tblPlayer.PlayerID 
    ORDER BY MatchDateTime DESC) AS Match
FROM tblPlayer
LEFT JOIN tblAppearance ON tblAppearance.PlayerId = tblPlayer.PlayerId
LEFT JOIN tblMatch ON tblMatch.MatchId = tblAppearance.MatchId
WHERE tblPlayer.Forename LIKE '%rob%' AND tblPlayer.Surname LIKE '%white%'
ORDER BY tblPlayer.Surname, tblPlayer.Forename, tblPlayer.DOB, tblMatch.MatchDateTime DESC

问题是,这会选择玩家所在的所有比赛,而不仅仅是他们最近的比赛。我知道这应该很简单,但我似乎无法为它获得正确的语法。

另外,我宁愿将匹配表中的单独列作为单独的列返回,而不是作为格式化的块。

要求提供更多信息的答案:

是的,有一个MatchDateTime列,我打算用它进行排序。

是的我确实希望那些尚未参加比赛的球员,左联盟是故意的。

3 个答案:

答案 0 :(得分:3)

对于这些类型的问题(获得组中的第一行),我在性能和可维护性方面取得了最大的成功,使用ROW_NUMBER()CTE。模式很简单:CTE选择您想要的列,并为每个组中的ROW_NUMBER()添加一个额外的列(当然,按您所需的顺序排序)。然后,查询的后CTE部分将结果限制为ROW_NUMBER()为1的那些

像这样:

WITH cte AS
(
    SELECT tblPlayer.PlayerId, Surname, Forename, HomeClub, OpponentClub, AgeGroup, MatchDateTime, DOB,
        ROW_NUMBER () OVER (PARTITION BY tblPlayer.PlayerId ORDER BY MatchDateTime DESC) AS RowNum
    FROM tblPlayer
        LEFT JOIN tblAppearance ON tblAppearance.PlayerId = tblPlayer.PlayerId
        LEFT JOIN tblMatch ON tblMatch.MatchId = tblAppearance.MatchId
    WHERE Forename LIKE '%rob%' AND Surname LIKE '%white%'
)
SELECT PlayerId, Surname, Forename, HomeClub, OpponentClub, AgeGroup, MatchDateTime, DOB
FROM cte
WHERE RowNum = 1
ORDER BY Surname, Forename, DOB, MatchDateTime

请注意,我并未假设任何ID的排序方式与MatchDateTime相同 - 有很多原因(例如提前调度)为什么这个假设可能不成立。但是,如果外观ID的排序与日期相同,则上述查询可以更加高效,因为您不必进行任何连接即可找到您正在寻找的MatchID。

请注意,如果您有非常多的播放器(100,000+)且经常运行此查询,那么您需要在此处进行优化,因为每次运行此查询时都会进行表扫描播放器表格,以支持您的LIKE过滤器。如果是这种情况,您可能希望在Surname,Forename上创建覆盖索引,并让SQL分阶段运行您的查询:首先使用覆盖索引过滤Player记录,然后进行连接,最后拉出其他Player聚集索引中的列。通常很难让SQL执行这样的计划(你可能需要一个临时表来获得中间结果),但对于非常大的表,perf的胜利是值得的。如果你有少量玩家,请忽略前面的段落。 : - )

答案 1 :(得分:2)

如果我理解正确,你可以试试这个

DECLARE @User TABLE(
        UserID INT
)

DECLARE @Matches TABLE(
        MatchID INT
)

DECLARE @UserMatches TABLE(
        UserMatchID INT,
        UserID INT,
        MatchID INT
)

INSERT INTO @User SELECt 1
INSERT INTO @User SELECt 2

INSERT INTO @Matches SELECt 1
INSERT INTO @Matches SELECt 2

INSERT INTO @UserMatches SELECt 1, 1, 1
INSERT INTO @UserMatches SELECt 2, 1, 2
INSERT INTO @UserMatches SELECt 3, 2, 2

SELECT  u.*,
        m.*
FROm    @UserMatches um INNER JOIN
        (
            SELECT  UserID,
                    MAX(UserMatchID) MaxID
            FROM    @UserMatches um
            GROUP BY UserID
        ) MaxIdsPerUser ON um.UserID = MaxIdsPerUser.UserID
                        AND um.UserMatchID = MaxID INNER JOIN 
        @User u ON um.UserID = u.UserID INNER JOIN
        @Matches m ON um.MatchID = m.MatchID

如果你有一个DateTime来确定最近的匹配,你可以使用它来获得最大值。

答案 2 :(得分:1)

不使用连接中的tblAppearance,而是使用派生表,使用APPLY选择播放器的最后一个外观,并在此派生表上连接:

SELECT ...
FROM tblPlayer
OUTER APPLY (
  SELECT TOP (1) *
  FROM tblAppearance
  WHERE tblPlayer.PlayerId = tblAppearance.PlayerId
  ORDER BY MatchDateTime DESC) AS lastAppearance
LEFT JOIN tblMatch ON tblMatch.MatchId = lastAppearance.MatchId
WHERE ...