【问题标题】:选择SQL中组的前N个独占值?
【发布时间】:2022-01-23 05:54:42
【问题描述】:

在 SQL(最好是 SQL Server)中有没有办法选择一个组中排在其他组之外的前 N ​​条记录?

例如:

DROP TABLE IF EXISTS #DISTANCE

CREATE TABLE #DISTANCE
(
    GNAME VARCHAR(3)
,   CNAME VARCHAR(3)
,   DIST NUMERIC(5,3)
)

INSERT INTO #DISTANCE
VALUES ('E1', 'C1', 1), ('E1','C2',2),
       ('E2', 'C1', 1.5), ('E2','C2',2.5)

如果我按距离 ASC 为每个 ENAME 寻找第一个专有 CNAME,我希望得到这样的输出:

Ename Cname Dist
E1 C1 1
E2 C2 2.5

请注意,省略 E1|C2 和 E2|C1,因为它们将是该组排名结果中的第二个值。

我想出了一些 SQL 方法来尝试正确地提取它,但是当我在 ENAME 上添加其他组并且如果我更改我的 Top N 值时,我的工作就会崩溃。

如果我增加复杂性:

TRUNCATE TABLE #DISTNACE

INSERT INTO #DISTANCE
VALUES ('E1', 'C1', 1), ('E1', 'C2', 2), 
       ('E1', 'C3', 3), ('E1', 'C4', 5),
       ('E2', 'C1', 2.5), ('E2', 'C2', 4), 
       ('E2', 'C3', 3.5), ('E2', 'C4', 6),
       ('E3', 'C4', 7), ('E3', 'C5', 6), 
       ('E3', 'C6', 4)

我试图得到的 SQL 输出如下所示:

GNAME CNAME DIST
E1 C1 1.000
E1 C2 2.000
E1 C3 3.000
E2 C4 6.000
E3 C6 4.000
E3 C5 6.000

我可以让它在这个特定的实例中工作,使用这个代码:

WITH X AS  
(
    SELECT * 
    --, RNK             = DENSE_RANK() OVER (ORDER BY DIST ASC) 
    , CNAME_RNK_BY_DIST = DENSE_RANK() OVER (PARTITION BY CNAME ORDER BY DIST ASC)
    , CNAME_RNK_BY_DIST = DENSE_RANK() OVER (PARTITION BY CNAME ORDER BY DIST ASC) 
    FROM #DISTANCE
    )
,MINDIST AS ( -- FIRST OCCURANCE OF CNAME VALUE
    SELECT 
        CNAME
    ,   MIN(DIST) MINDIST
    FROM X GROUP BY CNAME
)
            -- SELECT * , CALC = SUM(CNAME_RNK_BY_DIST / 4) OVER (PARTITION BY CNAME ORDER BY DIST ASC) FROM X order by CNAME, DIST
, X2 AS (

    SELECT *, CALC = SUM(FLOOR(CNAME_RNK_BY_DIST / 4)) OVER (PARTITION BY CNAME ORDER BY DIST ASC) FROM X
)
    --SELECT * FROM X2 order by CNAME, dist
, CALC AS (
    SELECT CNAME, MAXINC = MAX(CALC) FROM X2 GROUP BY CNAME
)
    --SELECT * FROM CALC
, FIRST_OCCURANCE_PAIRS AS (
    SELECT A.*
        ,OCCURANCE = RANK() OVER (PARTITION BY CNAME ORDER BY DIST)
    FROM X A
    JOIN MINDIST B ON A.CNAME = B.CNAME AND A.DIST = B.MINDIST
)
    --SELECT * FROM FIRST_OCCURANCE_PAIRS

,ISO AS 
(
    SELECT * fROM FIRST_OCCURANCE_PAIRS WHERE OCCURANCE > 3 
)
--select * from FIRST_OCCURANCE_PAIRS
--  SELECT * FROM ISO
, NEXT_OCCURANCE AS (
    SELECT A.*
    FROM  X AS A
    JOIN CALC ON A.CNAME = CALC.CNAME
    JOIN ISO B ON A.CNAME_RNK_BY_DIST  =  CALC.MAXINC               and A.CNAME = B.CNAME
)
    --select * from NEXT_OCCURANCE
, FRAME AS (

SELECT 
    CNAME
,   CNAME
,   DIST

FROM FIRST_OCCURANCE_PAIRS
--WHERE OCCURANCE <=3
UNION
SELECT
    CNAME
,   CNAME
,   DIST
FROM NEXT_OCCURANCE
)
--select * from FRAME
, FINAL AS (
    SELECT * ,FINALRNK = ROW_NUMBER() OVER (PARTITION BY CNAME ORDER BY DIST)
    FROM
    FRAME
)
    SELECT * FROM FINAL WHERE FINALRNK <4

但随着更多记录的添加,逻辑失败。有没有办法清理这个 SQL 并获得任意数量组合的结果?

【问题讨论】:

  • 您似乎想要每个cnamedist 最少的行。如果是这种情况,那么您的第一个示例是错误的。您希望在 C2 的结果中出现 ('E1','C2',2),而不是 ('E2','C2',2.5),因为 2

标签: sql sql-server tsql


【解决方案1】:

在查看了我的问题之后,我认为我没有为我的问题提供足够有用的信息。经过一些调整,我想我能够达到我想要的。我认为我必须运行批处理执行步骤来传递我的数据以提取我需要的东西,而不是能够在单个查询中执行此操作。也许我有办法减少/优化下面的代码?

DROP TABLE IF EXISTS #DISTANCE
CREATE TABLE #DISTANCE
(
    GNAME       VARCHAR(3)
,   CNAME       VARCHAR(3)
,   DIST        NUMERIC(5,3)
)

DROP TABLE IF EXISTS #RESULT
CREATE TABLE #RESULT
(
    GNAME       VARCHAR(3)
,   CNAME       VARCHAR(3)
,   DIST        NUMERIC(5,3)
--, FINRNK      INT
)
--INSERT INTO #DISTANCE
--VALUES 
--  ('E1', 'C1', 1  )       
--, ('E1', 'C2', 2  )
--, ('E1', 'C3', 3  )
--, ('E1', 'C4', 5  )
--, ('E2', 'C1', 2.5)   
--, ('E2', 'C2', 4  )
--, ('E2', 'C3', 3.5)
--, ('E2', 'C4', 6  )
--, ('E3', 'C4', 7  )       
--, ('E3', 'C5', 6  )
--, ('E3', 'C6', 4  )
---- CONCLUSIONG; SCENARIO WORKS, VALUES SHIFT.


--INSERT INTO #DISTANCE
--VALUES 
--  ('E1', 'C1', 1  )       
--, ('E1', 'C2', 2  )
--, ('E1', 'C3', 3  )
--, ('E1', 'C4', 5  )
--, ('E2', 'C7', 2.5)   
--, ('E2', 'C8', 4  )
--, ('E2', 'C9', 3.5)
--, ('E2', 'C4', 6  )
--, ('E3', 'C4', 7  )       
--, ('E3', 'C5', 6  )
--, ('E3', 'C6', 4  )
----,   ('E3', 'C3', 1  )
-- CONCLUSIONG; SCENARIO WORKS, VALUES SHIF

INSERT INTO #DISTANCE
VALUES 
    ('E1', 'C1', 1  )       
,   ('E1', 'C2', 2  )
,   ('E1', 'C3', 3  )
,   ('E1', 'C4', 4  ) -- switch with E2 dist to switch groups in results
,   ('E2', 'C1', 2.5)   
,   ('E2', 'C2', 4  )
,   ('E2', 'C3', 3.5)
,   ('E2', 'C4', 6  ) -- switch dist with e1 to move groups in results
,   ('E3', 'C4', 7  )       
,   ('E3', 'C5', 6  )
,   ('E3', 'C6', 4  )
,   ('E3', 'C3', 1  )

--INSERT INTO #DISTANCE
--VALUES 
--  ('E1', 'C1', 1  )       
--, ('E1', 'C2', 2  )
--, ('E1', 'C3', 3  )
--, ('E1', 'C4', 5  )
--, ('E2', 'C7', 2.5)   -- CHANGE CVAL TO UNIQUE TO PULL THROUGH    
--, ('E2', 'C8', 4  )   -- CHANGE CVAL TO UNIQUE TO PULL THROUGH
--, ('E2', 'C11', 3.5)  -- CHANGE CVAL TO UNIQUE TO PULL THROUGH
--, ('E2', 'C4', 6  )
--, ('E3', 'C9', 3  ) -- NEW
--, ('E3', 'C4', 7  )       
--, ('E3', 'C5', 6  )
--, ('E3', 'C6', 4  )
-- SCENARIO CORRECT. C4 SHOULD NOT BE PULLED IN.
GO

BEGIN
    ;
    WITH 
     NOROOM AS (
        SELECT GNAME FROM #RESULT GROUP BY GNAME HAVING COUNT(1) >= 3 -- HAS TO BE NOROOM BECAUSE 'HASROOM' WOULD not make sense in first pass.
    )
    ,X AS (
        SELECT 
            A.CNAME
        ,   A.DIST
        ,   A.GNAME
        ,   RNK = RANK()        OVER (PARTITION BY A.GNAME ORDER BY A.DIST ASC)
        ,   RNK2 = ROW_NUMBER() OVER (ORDER BY A.DIST ASC)
        ,   #RESULT.CNAME AS RSLT
        FROM #DISTANCE A
        LEFT JOIN #RESULT ON A.CNAME = #RESULT.CNAME
        LEFT JOIN NOROOM ON A.GNAME = NOROOM.GNAME
        WHERE NOROOM.GNAME IS NULL AND #RESULT.CNAME IS NULL --ORDER BY RNK
        )
        --SELECT * FROM X
    , Y AS (
        SELECT X.CNAME, MINRNK2 = MIN(X.RNK2) 
        FROM X 
        --LEFT JOIN #RESULT B ON X.CNAME = B.CNAME
        GROUP BY  X.CNAME
    )
    --  select * from y
    --  SELECT * FROM x JOIN Y ON X.CNAME = Y.CNAME AND X.RNK2 = Y.MINRNK2
    INSERT INTO #RESULT
    SELECT X.GNAME, X.CNAME, X.DIST FROM X 
     JOIN Y AS SQ ON X.CNAME = SQ.CNAME AND X.RNK2 = SQ.MINRNK2
    WHERE X.RNK <=3 --AND   #RESULT.GNAME IS NULL

     PRINT 'LOOP'
END
GO 10
     SELECT * FROM #RESULT ORDER BY GNAME

【讨论】:

    【解决方案2】:

    您似乎想要每个cnamedist 最少的行。很简单:

    select gname, cname, dist
    from
    (
      select
        gname, cname, dist, rank() over (partition by cname order by dist) as rnk
      from mytable
    )as r
    where rnk = 1;
    

    我在这里使用RANK 来处理关系。因此,当cname 有两行具有相同的最小值dist 时,您将获得这两行的cname。如果您希望每个cname 只允许一行,则必须使用ROW_NUMBER 而不是RANK,但您还必须决定要显示哪一个gname

    【讨论】:

    • 他还想确保 cname 不会为其他组重复
    • @Venkataraman R:我不明白这应该是一个附加规则。如果我们选择每个cname 中具有最少dist 的行,我们就会对每个cname 进行一次——好吧,至少只要没有平局。我使用RANK 来处理关系。如果 OP 在这种情况下只想选择一行,他们可以使用 ROW_NUMBER 代替。你是这个意思吗?但是,那么准确选择一行的规则是什么?你提到的“其他群体”是什么?
    • 从OP,我是这样理解的。例如,(E1, C1) 是一个组合,那么 C1 不应该出现在结果集中。它将是(E2,C2)。
    • 文卡塔拉曼,你的理解是正确的。在示例中,C1 不应在其他 G 组中重复。
    • @mdiprima:C1 在我的查询中不会出现两次,除非您打成平手。我提到了这一点。如果 C1 有两行 ('E1', 'C1', 1)('E2', 'C1', 1),则两者都会显示。如果你不想要这个,那么你想显示哪个 gname?您可以使用ROW_NUMBER 而不是我也提到过的RANK,但是您必须决定在出现平局时选择 gname 的规则。
    猜你喜欢
    • 2013-01-25
    • 2015-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多