【问题标题】:SELECT TOP 1 in a many to many query在多对多查询中选择 TOP 1
【发布时间】:2010-12-18 09:15:56
【问题描述】:

我有一个失败的简单多对多模式,如下所示: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 列,我打算将其用于排序。

是的,我确实想要尚未参加任何比赛的球员,左加入是故意的。

【问题讨论】:

  • 没有 MatchDate 字段吗?这将使它变得非常容易......
  • 另外,我是否正确假设查询中的 LEFT JOIN 意味着您希望为尚未参加任何比赛的玩家返回行?您可能应该将此信息添加到您的问题中。
  • tblAppearance.AppearanceId 是否与 tblMatch.MatchDateTime 的顺序相同?或者是否存在 ID 和日期顺序不同的情况(例如提前安排的比赛、提前结束的比赛等)?请将此信息添加到您的问题中。谢谢!

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


【解决方案1】:

对于这些类型的问题(在一组中排在首位),我使用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 win 是值得的。如果玩家人数较少,请忽略前一段。 :-)

【讨论】:

    【解决方案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 来确定最近的匹配,你可以使用它作为最大值。

    【讨论】:

    • 啊,你打败了我 :) 我也打算使用派生表连接
    • 这假定UserMatchID 是严格单调的。可能不是这样。
    • 我要求 OP 澄清 UserMatchID(OP 架构中的 AppearanceID)是否以与 MatchDateTime 相同的顺序给出。 FWIW,这个假设可能不成立的原因有很多。例如,如果匹配是可变长度的并且仅在匹配结束时添加记录,则开始较晚的匹配可能会比开始较晚的匹配更早结束。类似地,如果在比赛之前安排比赛,则该假设也可能不成立。有趣的是,在我的回答中,我假设了保守的假设——你假设了表现最好的假设。我更喜欢你的态度! :-)
    • @astander:另外,如果我正确理解了 OP 的查询,他的 LEFT JOIN 意味着对于还没有参加过比赛的玩家也应该返回行。所以(如果我的理解是正确的)您需要一个 UNION 和一个连接才能使您的查询准确,因为您将丢失没有匹配项的用户的所有行。我也要求澄清这一点。
    • ...不要介意UNION,您也可以在这里使用OUTER JOIN,例如开(um.UserID = u.UserID 或 um.UserID 为 NULL)
    【解决方案3】:

    不要在连接中使用 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 ...
    

    【讨论】:

    • FWIW,我对 APPLY 查询的体验在性能方面并不是很好。 SQL 似乎经常为使用 ROW_NUMBER() 或子查询的 APPLY 解决方案与等效解决方案(参见我的答案)选择更差的查询计划。所以我通常避免 APPLY,除非表很小或者性能不是什么大问题。也就是说,每个人的里程可能会有所不同——但下次你想使用 APPLY 时,使用 ROW_NUMBER() 尝试同样的事情,看看哪一个完成得更快。 :-)
    • @Justin:APPLY 根据定义是一个嵌套循环,普通连接可以使用哈希或合并运算符。 APPLY 也没有并行性潜力。所以是的,在某些情况下它可能会跑赢。还有一些场景是 APPLY 是 only 解决方案(XML 节点、表值函数)。在这些特定情况下(一组中的第一行)进行有趣的比较,但不确定哪一个会在正面交锋中获胜 APPLY (TOP 1 ORDER BY...)JOIN (ROW_NUMBER() OVER (PARTITION BY...)
    • 我同意 100%,APPLY 非常有用,因为在很多情况下没有其他实用选项。但是,如果有另一种选择,我还没有找到一个案例(至少到目前为止)APPLY 是最快的查询计划。我记得连接类型的限制,但我也完全忘记了并行性的损失。我曾经在一个 8-proc 怪物服务器上尝试应用 APPLY 十亿行查询......结果等待我的答案很长时间。 :-)
    猜你喜欢
    • 1970-01-01
    • 2017-11-17
    • 1970-01-01
    • 2016-07-08
    • 2015-05-05
    • 2018-05-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多