问题 如何从每组中选择TOP 5 PERCENT?


我有一个这样的样本表:

CREATE TABLE #TEMP(Category VARCHAR(100), Name VARCHAR(100))

INSERT INTO #TEMP VALUES('A', 'John')
INSERT INTO #TEMP VALUES('A', 'John')
INSERT INTO #TEMP VALUES('A', 'John')
INSERT INTO #TEMP VALUES('A', 'John')
INSERT INTO #TEMP VALUES('A', 'John')
INSERT INTO #TEMP VALUES('A', 'John')
INSERT INTO #TEMP VALUES('A', 'Adam')
INSERT INTO #TEMP VALUES('A', 'Adam')
INSERT INTO #TEMP VALUES('A', 'Adam')
INSERT INTO #TEMP VALUES('A', 'Adam')
INSERT INTO #TEMP VALUES('A', 'Lisa')
INSERT INTO #TEMP VALUES('A', 'Lisa')
INSERT INTO #TEMP VALUES('A', 'Bucky')
INSERT INTO #TEMP VALUES('B', 'Lily')
INSERT INTO #TEMP VALUES('B', 'Lily')
INSERT INTO #TEMP VALUES('B', 'Lily')
INSERT INTO #TEMP VALUES('B', 'Lily')
INSERT INTO #TEMP VALUES('B', 'Lily')
INSERT INTO #TEMP VALUES('B', 'Tom')
INSERT INTO #TEMP VALUES('B', 'Tom')
INSERT INTO #TEMP VALUES('B', 'Tom')
INSERT INTO #TEMP VALUES('B', 'Tom')
INSERT INTO #TEMP VALUES('B', 'Ross')
INSERT INTO #TEMP VALUES('B', 'Ross')
INSERT INTO #TEMP VALUES('B', 'Ross')

SELECT Category, Name, COUNT(Name) Total
FROM #TEMP
GROUP BY Category, Name
ORDER BY Category, Total DESC

DROP TABLE #TEMP

给我以下内容:

A   John    6
A   Adam    4
A   Lisa    2
A   Bucky   1
B   Lily    5
B   Tom     4
B   Ross    3

现在,我该如何选择 TOP 5 PERCENT 每个类别的记录 假设每个类别有超过100条记录(这里未显示样本表)?例如,在我的实际表中,它应该删除 John 来自 A 和 Lily 来自 B 如果合适(再次,我没有在这里显示完整的表)得到:

A   Adam    4
A   Lisa    2
A   Bucky   1
B   Tom     4
B   Ross    3

我一直在尝试使用 CTEs和 PARTITION BY 条款但似乎无法实现我想要的。它从总体结果中删除了TOP 5 PERCENT,但不从每个类别中删除。有什么建议么?


3223
2017-09-28 07:50


起源

可能会有一点帮助 - 如果你有一个组的计数,请记住5%将是“row_num <=(5 * count)/ 100” - Kieren Johnstone
@KierenJohnstone:+1谢谢。我知道我可能不得不使用CROSS APPLY或类似的东西,但仍然有一些麻烦。如果我搞清楚会更新。 - Legend
那么期望的输出是什么?与6的计数相比,删除前5%的百分比非常少。一行(A,John)为16%。 - gbn
看到这个类似的问题: stackoverflow.com/questions/4373451/... - ypercubeᵀᴹ
@Legend - 仍然不清楚你想要什么。请给出预期的结果并解释它们是如何到达的。不确定你想要什么 TOP 5% 应用。 - Martin Smith


答案:


您可以使用与CTE配对的CTE(公用表表达式) NTILE 窗口函数 - 这会将您的数据切割成您需要的多个切片,例如在你的情况下,分为20片(每片5%)。

;WITH SlicedData AS
(
   SELECT Category, Name, COUNT(Name) Total,
            NTILE(20) OVER(PARTITION BY Category ORDER BY COUNT(Name) DESC) AS  'NTile'
   FROM #TEMP
   GROUP BY Category, Name
)
SELECT *
FROM SlicedData
WHERE NTile > 1

这基本上将您的数据分组 Category,Name,通过其他东西订购(不确定是否 COUNT(Name) 这真的是你想要的东西),然后把它分成20块,每块代表5%的数据分区。切片用 NTile = 1 是最高的5%切片 - 从CTE中选择时忽略它。

看到:

了解更多信息


14
2017-09-28 08:04



这符合我的目的。太感谢了。我修改了你的帖子,查询了一些缺失的部分,使其开箱即用。 - Legend
@Legend我以为你想删除记录,而不仅仅是选择它们? - Tim Rogers
@TimRogers:好的。我刚刚使用此查询为我想要删除的名称创建了一个排除列表。我会尽力解决我的问题。 - Legend


编辑:我添加了第二个解决方案

SELECT   b.Id
        ,b.Category
        ,b.Name
        ,b.CategoryNameCount
FROM
(
        SELECT   a.Id   
                ,a.Category
                ,a.Name
                ,COUNT(*)OVER(PARTITION BY a.Category, a.Name) CategoryNameCount
                ,COUNT(*)OVER(PARTITION BY a.Category) CategoryCount
        FROM    #TEMP a
) b
WHERE   b.CategoryCount*5.0/100 > b.CategoryCount*b.CategoryNameCount*1.0/100
ORDER BY b.Category, b.CategoryNameCount DESC, b.Name

结果:

Id          Category Name       CategoryNameCount
----------- -------- ---------- -----------------
7           A        Adam       4
8           A        Adam       4
9           A        Adam       4
10          A        Adam       4
11          A        Lisa       2
12          A        Lisa       2
13          A        Bucky      1
19          B        Tom        4
20          B        Tom        4
21          B        Tom        4
22          B        Tom        4
23          B        Ross       3
24          B        Ross       3
25          B        Ross       3

要么

SELECT   b.Category, b.Name, b.CategoryNameCount
FROM
(
        SELECT  
                 a.Category
                ,a.Name
                ,COUNT(*)OVER(PARTITION BY a.Category, a.Name) CategoryNameCount
                ,COUNT(*)OVER(PARTITION BY a.Category) CategoryCount
        FROM    #TEMP a
) b
WHERE   b.CategoryCount*5.0/100 > b.CategoryCount*b.CategoryNameCount*1.0/100
GROUP BY b.Category, b.Name, b.CategoryNameCount
ORDER BY b.Category, b.CategoryNameCount DESC, b.Name

结果:

Category Name       CategoryNameCount
-------- ---------- -----------------
A        Adam       4
A        Lisa       2
A        Bucky      1
B        Tom        4
B        Ross       3

1
2017-09-28 08:12



+1谢谢你的时间。我没有 Id 专栏,但我猜我可以轻松添加。 - Legend
我希望这种新的解决方案更好。 - Bogdan Sahlean
甜!非常感谢您的时间:)如果可能的话,我会在我的数据库上回复性能比较。 - Legend


select Category,name,CountTotal,RankSeq,(50*CountTotal)/100 from (
select Category,name,COUNT(*)
over (partition by Category,name ) as CountTotal,
ROW_NUMBER()
over (partition by Category,name order by Category) RankSeq from #TEMP
--group by Category,Name 
) temp
where RankSeq <= ((50*CountTotal)/100)
order by Category,Name,RankSeq

输出:

Category    name     CountTotal RankSeq     50*CountTotal)/100
A           Adam     4          1           2
A           Adam     4          2           2
A           John     6          1           3
A           John     6          2           3
A           John     6          3           3
A           Lisa     2          1           1
B           Lily     5          1           2
B           Lily     5          2           2
B           Ross     3          1           1
B           Tom      4          1           2
B           Tom      4          2           2

我希望这有帮助 :)


1
2017-08-14 23:01





;WITH SlicedData AS
(
   SELECT Category, Name, COUNT(Name) Total,
            **PERCENT_RANK() OVER(PARTITION BY Category ORDER BY COUNT(Name) DESC) * 100** AS  'Percent'
   FROM #TEMP
   GROUP BY Category, Name
)
SELECT *
FROM SlicedData
WHERE Percent < 5

如果记录数小于您的图块编号,则NTile将不起作用。


0
2017-11-29 10:05