Función de partición COUNT () OVER posible usando DISTINCT

88

Estoy tratando de escribir lo siguiente para obtener un total acumulado de NumUsers distintos, así:

NumUsers = COUNT(DISTINCT [UserAccountKey]) OVER (PARTITION BY [Mth])

Management Studio no parece muy feliz con esto. El error desaparece cuando elimino la DISTINCTpalabra clave, pero luego no será un recuento distinto.

DISTINCTno parece ser posible dentro de las funciones de partición. ¿Cómo hago para encontrar el recuento distinto? ¿Utilizo un método más tradicional , como una subconsulta correlacionada?

Analizando esto un poco más, tal vez estas OVERfunciones funcionen de manera diferente a Oracle en la forma en que no se pueden usar SQL-Serverpara calcular los totales acumulados.

Agregué un ejemplo en vivo aquí en SQLfiddle donde intento usar una función de partición para calcular un total acumulado .

por qué
fuente
2
COUNTcon en ORDER BYlugar de PARTITION BYestá mal definido en 2008. Me sorprende que te permita tenerlo. Según la documentación , no se le permite una ORDER BYfunción agregada.
Damien_The_Unbeliever
sí, creo que me estoy confundiendo con algunas funciones de Oracle; estos totales
acumulados

Respuestas:

177

Hay una solución muy simple usando dense_rank()

dense_rank() over (partition by [Mth] order by [UserAccountKey]) 
+ dense_rank() over (partition by [Mth] order by [UserAccountKey] desc) 
- 1

Esto le dará exactamente lo que estaba pidiendo: La cantidad de UserAccountKeys distintas dentro de cada mes.

David
fuente
23
Una cosa con la que hay que tener cuidado dense_rank()es que contará NULL, mientras COUNT(field) OVERque no. No puedo emplearlo en mi solución debido a esto, pero sigo pensando que es bastante inteligente.
bf2020
1
Pero estoy buscando un total acumulado de claves de cuenta de usuario distintas durante los meses de cada año: ¿no estoy seguro de cómo esto responde a eso?
whytheq
4
@ bf2020, si no puede haber NULLvalores en el UserAccountKey, entonces es necesario agregar este término: -MAX(CASE WHEN UserAccountKey IS NULL THEN 1 ELSE 0 END) OVER (PARTITION BY Mth). La idea está tomada de la respuesta de LarsRönnbäck a continuación. Esencialmente, si UserAccountKeytiene NULLvalores, debe restar extra 1del resultado, porque DENSE_RANKcuenta NULL.
Vladimir Baranov
1
@ahsteele gracias hombre, me voló la cabeza y resolvió mi problema
Henrique Donati
Aquí una discusión sobre el uso de esta dense_ranksolución cuando la función de ventana tiene un marco. SQL Server no permite el dense_rankuso con un marco de ventana: stackoverflow.com/questions/63527035/…
K4M
6

Nigromante:

Es relativamente sencillo emular COUNT DISTINCT sobre PARTITION BY con MAX a través de DENSE_RANK:

;WITH baseTable AS
(
    SELECT 'RM1' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM1' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR2' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR2' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR3' AS ADR
    UNION ALL SELECT 'RM3' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM3' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM3' AS RM, 'ADR2' AS ADR
)
,CTE AS
(
    SELECT RM, ADR, DENSE_RANK() OVER(PARTITION BY RM ORDER BY ADR) AS dr 
    FROM baseTable
)
SELECT
     RM
    ,ADR

    ,COUNT(CTE.ADR) OVER (PARTITION BY CTE.RM ORDER BY ADR) AS cnt1 
    ,COUNT(CTE.ADR) OVER (PARTITION BY CTE.RM) AS cnt2 
    -- Not supported
    --,COUNT(DISTINCT CTE.ADR) OVER (PARTITION BY CTE.RM ORDER BY CTE.ADR) AS cntDist
    ,MAX(CTE.dr) OVER (PARTITION BY CTE.RM ORDER BY CTE.RM) AS cntDistEmu 
FROM CTE

Nota:
Esto asume que los campos en cuestión son campos NO anulables.
Si hay una o más entradas NULL en los campos, debe restar 1.

Stefan Steiger
fuente
5

Utilizo una solución similar a la de David anterior, pero con un giro adicional si algunas filas deben excluirse del recuento. Esto supone que [UserAccountKey] nunca es nulo.

-- subtract an extra 1 if null was ranked within the partition,
-- which only happens if there were rows where [Include] <> 'Y'
dense_rank() over (
  partition by [Mth] 
  order by case when [Include] = 'Y' then [UserAccountKey] else null end asc
) 
+ dense_rank() over (
  partition by [Mth] 
  order by case when [Include] = 'Y' then [UserAccountKey] else null end desc
)
- max(case when [Include] = 'Y' then 0 else 1 end) over (partition by [Mth])
- 1

Aquí se puede encontrar un violín SQL con un ejemplo extendido.

Lars Rönnbäck
fuente
1
Su idea puede usarse para hacer la fórmula original (sin las complejidades de las [Include]que está hablando en su respuesta) con dense_rank()trabajo cuando UserAccountKeysea ​​posible NULL. Añadir este término a la fórmula: -MAX(CASE WHEN UserAccountKey IS NULL THEN 1 ELSE 0 END) OVER (PARTITION BY Mth).
Vladimir Baranov
5

Creo que la única forma de hacer esto en SQL-Server 2008R2 es usar una subconsulta correlacionada o una aplicación externa:

SELECT  datekey,
        COALESCE(RunningTotal, 0) AS RunningTotal,
        COALESCE(RunningCount, 0) AS RunningCount,
        COALESCE(RunningDistinctCount, 0) AS RunningDistinctCount
FROM    document
        OUTER APPLY
        (   SELECT  SUM(Amount) AS RunningTotal,
                    COUNT(1) AS RunningCount,
                    COUNT(DISTINCT d2.dateKey) AS RunningDistinctCount
            FROM    Document d2
            WHERE   d2.DateKey <= document.DateKey
        ) rt;

Esto se puede hacer en SQL-Server 2012 usando la sintaxis que sugirió:

SELECT  datekey,
        SUM(Amount) OVER(ORDER BY DateKey) AS RunningTotal
FROM    document

Sin embargo, el uso de DISTINCTtodavía no está permitido, por lo que si se requiere DISTINCT y / o si la actualización no es una opción, creo que OUTER APPLYes su mejor opción.

GarethD
fuente
genial, gracias. Encontré esta respuesta SO que presenta la opción APLICAR EXTERIOR que intentaré. ¿Ha visto el enfoque de ACTUALIZACIÓN en bucle en esa respuesta ... es bastante lejano y aparentemente rápido. La vida será más fácil en 2012: ¿es una copia directa de Oracle?
whytheq