SQL contando distinto sobre partición

10

Tengo una tabla con dos columnas, quiero contar los valores distintos en Col_B sobre (condicionado por) Col_A.

Mi mesa

Col_A | Col_B 
A     | 1
A     | 1
A     | 2
A     | 2
A     | 2
A     | 3
b     | 4
b     | 4
b     | 5

Resultado Esperado

Col_A   | Col_B | Result
A       | 1     | 3
A       | 1     | 3
A       | 2     | 3
A       | 2     | 3
A       | 2     | 3
A       | 3     | 3
b       | 4     | 2
b       | 4     | 2
b       | 5     | 2

Probé el siguiente código

select *, 
count (distinct col_B) over (partition by col_A) as 'Result'
from MyTable

cuenta (distinto col_B) no funciona. ¿Cómo puedo reescribir la función de conteo para contar valores distintos?

sara92
fuente

Respuestas:

18

Así es como lo haría:

SELECT      *
FROM        #MyTable AS mt
CROSS APPLY (   SELECT COUNT(DISTINCT mt2.Col_B) AS dc
                FROM   #MyTable AS mt2
                WHERE  mt2.Col_A = mt.Col_A
                -- GROUP BY mt2.Col_A 
            ) AS ca;

La GROUP BYcláusula es redundante dados los datos proporcionados en la pregunta, pero puede darle un mejor plan de ejecución. Ver el seguimiento de preguntas y respuestas CROSS APPLY produce una unión externa .

Considere votar por la solicitud de mejora de la cláusula OVER: cláusula DISTINCT para funciones agregadas en el sitio de comentarios si desea que esa característica se agregue a SQL Server.

Erik Darling
fuente
6

Puede emularlo usando dense_rank, y luego elegir el rango máximo para cada partición:

select col_a, col_b, max(rnk) over (partition by col_a)
from (
    select col_a, col_b
        , dense_rank() over (partition by col_A order by col_b) as rnk 
    from #mytable
) as t    

Debería excluir cualquier nulo de col_bpara obtener los mismos resultados que COUNT(DISTINCT).

Lennart
fuente
6

Esto es, en cierto modo, una extensión de la solución de Lennart , pero es tan feo que no me atrevo a sugerirlo como una edición. El objetivo aquí es obtener los resultados sin una tabla derivada. Puede que nunca sea necesario, y combinado con la fealdad de la consulta, todo el esfuerzo puede parecer un esfuerzo perdido. Sin embargo, todavía quería hacer esto como ejercicio, y ahora me gustaría compartir mi resultado:

SELECT
  Col_A,
  Col_B,
  DistinctCount = DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B ASC )
                + DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B DESC)
                - 1
                - CASE COUNT(Col_B) OVER (PARTITION BY Col_A)
                  WHEN COUNT(  *  ) OVER (PARTITION BY Col_A)
                  THEN 0
                  ELSE 1
                  END
FROM
  dbo.MyTable
;

La parte central del cálculo es esta (y en primer lugar me gustaría señalar que la idea no es mía, aprendí sobre este truco en otro lugar):

  DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B ASC )
+ DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B DESC)
- 1

Esta expresión se puede usar sin ningún cambio si Col_Bse garantiza que los valores nunca tendrán valores nulos. Sin embargo, si la columna puede tener valores nulos, debe tener en cuenta eso, y para eso está exactamente la CASEexpresión. Compara el número de filas por partición con el número de Col_Bvalores por partición. Si los números difieren, significa que algunas filas tienen un valor nulo Col_By, por lo tanto, el cálculo inicial ( DENSE_RANK() ... + DENSE_RANK() - 1) debe reducirse en 1.

Tenga en cuenta que debido a que - 1es parte de la fórmula central, elegí dejarlo así. Sin embargo, en realidad puede incorporarse a la CASEexpresión, en el intento inútil de hacer que la solución completa se vea menos fea:

SELECT
  Col_A,
  Col_B,
  DistinctCount = DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B ASC )
                + DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B DESC)
                - CASE COUNT(Col_B) OVER (PARTITION BY Col_A)
                  WHEN COUNT(  *  ) OVER (PARTITION BY Col_A)
                  THEN 1
                  ELSE 2
                  END
FROM
  dbo.MyTable
;

Esta demostración en vivo en logotipo de dbfiddledb <> fiddle.uk se puede usar para probar ambas variaciones de la solución.

Andriy M
fuente
2
create table #MyTable (
Col_A varchar(5),
Col_B int
)

insert into #MyTable values ('A',1)
insert into #MyTable values ('A',1)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',3)

insert into #MyTable values ('B',4)
insert into #MyTable values ('B',4)
insert into #MyTable values ('B',5)


;with t1 as (

select t.Col_A,
       count(*) cnt
 from (
    select Col_A,
           Col_B,
           count(*) as ct
      from #MyTable
     group by Col_A,
              Col_B
  ) t
  group by t.Col_A
 )

select a.*,
       t1.cnt
  from #myTable a
  join t1
    on a.Col_A = t1.Col_a
kevinnwhat
fuente
1

Alternativa si es levemente alérgico a subconsultas correlacionadas (respuesta de Erik Darling) y CTE (respuesta de kevinnwhat) como yo.

Tenga en cuenta que cuando se agregan valores nulos a la mezcla, ninguno de estos puede funcionar como le gustaría. (pero es bastante simple modificarlos al gusto)

Caso simple:

--ignore the existence of nulls
SELECT [mt].*, [Distinct_B].[Distinct_B]
FROM #MyTable AS [mt]

INNER JOIN(
    SELECT [Col_A], COUNT(DISTINCT [Col_B]) AS [Distinct_B]
    FROM #MyTable
    GROUP BY [Col_A]
) AS [Distinct_B] ON
    [mt].[Col_A] = [Distinct_B].[Col_A]
;

Igual que el anterior, pero con comentarios sobre qué cambiar para el manejo nulo:

--customizable null handling
SELECT [mt].*, [Distinct_B].[Distinct_B]
FROM #MyTable AS [mt]

INNER JOIN(
    SELECT 

    [Col_A],

    (
        COUNT(DISTINCT [Col_B])
        /*
        --uncomment if you also want to count Col_B NULL
        --as a distinct value
        +
        MAX(
            CASE
                WHEN [Col_B] IS NULL
                THEN 1
                ELSE 0
            END
        )
        */
    )
    AS [Distinct_B]

    FROM #MyTable
    GROUP BY [Col_A]
) AS [Distinct_B] ON
    [mt].[Col_A] = [Distinct_B].[Col_A]
/*
--uncomment if you also want to include Col_A when it's NULL
OR
([mt].[Col_A] IS NULL AND [Distinct_B].[Col_A] IS NULL)
*/
ap55
fuente