获取特定行两侧的最小总行数

时间:2016-12-17 18:22:29

标签: sql-server tsql

我有一个玩家表,每个玩家都有一个ID(索引主键),名称和分数。除索引外,表不排序。 e.g。

[dbo].[PlayerScores]
ID | Name | Score
=================
1  | Bob  | 17
2  | Carl | 24
3  | Ann  | 31
4  | Joan | 11
5  | Lou  | 17
6  | Dan  | 25
7  | Erin | 33
8  | Fred | 29

我已经定义了一个排行榜,以便所有玩家按照他们的分数排序并分配排名,所以我使用的是RANK()函数:

SELECT RANK() OVER (ORDER BY [Score] DESC) AS [Score_Rank],
    [Name],
    [Score]
FROM [dbo].[PlayerScores]

到目前为止一切顺利。对于上述数据,我会得到

Rank | Name | Score
=================
1    | Erin | 33
2    | Ann  | 31
3    | Fred | 29
4    | Dan  | 25
5    | Carl | 24
6    | Bob  | 17
6    | Lou  | 17
8    | Joan | 11

然而,当我向玩家展示这个排行榜时,我不需要或想要向他们展示一切 - 只有玩家紧接在他们上方和下方(不会有任何分页导航 - 玩家只能看到一个他们总体位置的快照。)

因此我试图检索(n)特定玩家周围的数据行,例如:

  1. 如果表中有(n)个或更少的行,则将返回所有行。
  2. 如果表格中至少有(n)行,则会返回(n)行数据。
  3. 指定播放器上方和下方应有(n / 2)行。
  4. 如果指定的播放器上方没有(n / 2)行,则返回上面的所有行,下面有足够的行来组成(n)行。
  5. 如果指定的播放器下方没有(n / 2)行,则返回下面的所有行,并且上面有足够的行来组成(n)行。
  6. 如何构建查询以便始终返回最小行数?例如。对于我的上述数据集和n = 5,Erin会看到

    Rank | Name | Score
    =================
    1    | Erin | 33
    2    | Ann  | 31
    3    | Fred | 29
    4    | Dan  | 25
    5    | Carl | 24
    

    虽然Dan会看到

    Rank | Name | Score
    =================
    2    | Ann  | 31
    3    | Fred | 29
    4    | Dan  | 25
    5    | Carl | 24
    6    | Bob  | 17
    

    Lou会看到

    Rank | Name | Score
    =================
    4    | Dan  | 25
    5    | Carl | 24
    6    | Bob  | 17
    6    | Lou  | 17
    8    | Joan | 11
    

    我在两个查询上找到了UNION的部分解决方案(一个在上面有n / 2行,另一个在指定玩家下面有n / 2行),但如果玩家在(或附近)表格的顶部或底部 - 剪切结果数据集,并且总是想要尽可能检索完整(n)行。

    我认为该解决方案可能与Window函数有关,使用了LAG和LEAD,但老实说我无法理解语法,我发现的大部分示例都不关心返回足够的行数。谢谢!

3 个答案:

答案 0 :(得分:1)

这将做你想要的。

WITH cte AS (
SELECT RANK() OVER (ORDER BY [Score] DESC) AS [Score_Rank],
    ROW_NUMBER() OVER (ORDER BY [Score] DESC) AS [RowNum],
    COUNT(ID) OVER (PARTITION BY (Select NULL)) AS MaxRow,
    [Name],
    [Score],
    [ID]
FROM @playScores
)
SELECT Score_Rank, Name, Score
FROM
    cte
    CROSS APPLY (SELECT RowNum AS AnchorRN FROM cte WHERE ID = @playerID) tmp
WHERE
    (
    RowNum <=
    CASE WHEN tmp.AnchorRN < ((@n)/2) THEN @n
        ELSE tmp.AnchorRN + ((@n)/2) END 
    )
    AND
    (
    RowNum >=
    CASE WHEN tmp.AnchorRN > (MaxRow - (@n)/2) THEN (MaxRow -@n + 1)
    ELSE tmp.AnchorRN - ((@n)/2) END
    );

SELECT *
    , ROW_NUMBER() OVER (ORDER BY Score) AS RowNum
FROM
    @playScores
ORDER BY
    RowNum;

这是完整的答案和测试代码。

DECLARE @playScores TABLE (
    ID INT
    , Name NVARCHAR(50)
    , Score INT
);

INSERT INTO @playScores (ID, Name, Score)
VALUES
(1  ,' Bob  ', 17),
(2  ,' Carl ', 24),
(3  ,' Ann  ', 31),
(4  ,' Joan ', 11),
(5  ,' Lou  ', 17),
(6  ,' Dan  ', 25),
(7  ,' Erin ', 33),
(8  ,' Fred ', 29);

DECLARE @n INT = 5;
DECLARE @playerID INT =5;

SELECT *
FROM
    @playScores
ORDER BY
    Score DESC;

WITH cte AS (
SELECT RANK() OVER (ORDER BY [Score] DESC) AS [Score_Rank],
    ROW_NUMBER() OVER (ORDER BY [Score] DESC) AS [RowNum],
    COUNT(ID) OVER (PARTITION BY (Select NULL)) AS MaxRow,
    [Name],
    [Score],
    [ID]
FROM @playScores
)
SELECT Score_Rank, Name, Score
FROM
    cte
    CROSS APPLY (SELECT RowNum AS AnchorRN FROM cte WHERE ID = @playerID) tmp
WHERE
    (
    RowNum <=
    CASE WHEN tmp.AnchorRN < ((@n)/2) THEN @n
        ELSE tmp.AnchorRN + ((@n)/2) END 
    )
    AND
    (
    RowNum >=
    CASE WHEN tmp.AnchorRN > (MaxRow - (@n)/2) THEN (MaxRow -@n + 1)
    ELSE tmp.AnchorRN - ((@n)/2) END
    );

SELECT *
    , ROW_NUMBER() OVER (ORDER BY Score) AS RowNum
FROM
    @playScores
ORDER BY
    RowNum;

SELECT *
    , ROW_NUMBER() OVER (ORDER BY Score) AS RowNum
FROM
    @playScores
ORDER BY
    RowNum;

答案 1 :(得分:1)

sql rank vs row number

同一程序的两个版本,一个按顺序输出结果集,第二个不按顺序输出。

rextester链接试用:http://rextester.com/JLQU48329

create table dbo.PlayerScores (Id int, Name nvarchar(64), Score int)
  insert into dbo.PlayerScores (Id, Name, Score) values
   (1,'Bob',17) ,(2,'Carl',24) ,(3,'Ann',31) ,(4,'Joan',11)
  ,(5,'Lou',17) ,(6,'Dan',25) ,(7,'Erin',33) ,(8,'Fred',29);
go
/* ordered resultset */
create procedure dbo.PlayerScores_getMiddle_byId (@PlayerId int, @Results int = 5) as 
begin;
  with cte as (
    select
        Score_Order = row_number() over (order by Score desc)
      , Score_Rank  = rank() over (order by Score desc)
      , Id
      , Name
      , Score
      from dbo.PlayerScores
  )
  select c.Score_Rank, c.Name, c.Score
    from (
      select top (@Results) i.* 
      from cte i 
        cross apply (select Score_Order from cte where Id = @PlayerId) as  x
      order by abs(i.Score_Order-x.Score_Order)
      ) as  c
    order by Score_Rank;
end
go
exec dbo.PlayerScores_getMiddle_byId 7,5; -- Erin
exec dbo.PlayerScores_getMiddle_byId 6,5; --Dan
exec dbo.PlayerScores_getMiddle_byId 5,5; --Lou

go
/* unordered result set */
/* 
create procedure dbo.PlayerScores_getMiddle_byId (@PlayerId int,@Results int = 5) as 
begin;
with cte as (
  select
      Score_Order = row_number() over (order by Score desc)
    , Score_Rank  = rank() over (order by Score desc)
    , Id
    , Name
    , Score
    from dbo.PlayerScores
    )

select top (@Results) c.Score_Rank, c.Name, c.Score
  from cte as c
    cross apply (select 
        Score_Order 
    from cte 
    where Id = @PlayerId) as x
    order by abs(c.Score_Order-x.Score_Order) 

end
--go
exec dbo.PlayerScores_getMiddle_byId 7,5; -- Erin
exec dbo.PlayerScores_getMiddle_byId 6,5; --Dan
exec dbo.PlayerScores_getMiddle_byId 5,5; --Lou
--*/

答案 2 :(得分:0)

或使用标准SQL:

with pRank(id, name, rank)
as (Select p.Id, p.Name nam,
      (Select count(*) from players
       where score <= p.score) rnk    
   from players p)
Select p.id, p.nam, p.score,
       n.id, n.nam, n.score
from pRank p join pRank n 
        on n.Rnk between 
               case when p.Rnk < @n/2 then 0 
                    else p.Rnk - @n / 2 end
           and case when p.Rnk < @n/2 then @n 
                    else p.Rnk + @n / 2 end   
order by p.rnk, p.Id, n.rnk    

测试:

declare @t table 
(id integer primary key not null,  
  nam varchar(30) not null, score int not null)   
insert @t(id, nam, score)
values
  (1, 'Bob ',17), 
  (2, 'Carl',24),
  (3, 'Ann ',31),
  (4, 'Joan',11),
  (5, 'Lou ',17),
  (6, 'Dan ',25),
  (7, 'Erin',33),
  (8, 'Fred',29)

declare @n int = 4;
with pRank(id, nam, rnk)
as (Select p.Id, p.Nam,
     (Select count(*) from @t
      where score <= p.score) rank    
from @t p)
Select p.id, p.Nam, p.rnk,
     n.id, n.nam, n.rnk
from pRank p join pRank n 
    on n.rnk between 
           case when p.rnk < @n/2 then 0 
                else p.rnk - @n / 2 end
       and case when p.rnk < @n/2 then @n 
                else p.rnk + @n / 2 end
 order by p.rnk, p.id, n.rnk        .