SQL-Zählung deutlich über Partition

10

Ich habe eine Tabelle mit zwei Spalten, ich möchte die unterschiedlichen Werte auf Col_B über (bedingt durch) Col_A zählen.

Mein Tisch

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

erwartetes Ergebnis

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

Ich habe den folgenden Code ausprobiert

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

count (different col_B) funktioniert nicht. Wie kann ich die Zählfunktion umschreiben, um unterschiedliche Werte zu zählen?

sara92
quelle

Antworten:

18

So würde ich es machen:

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;

Die GROUP BYKlausel ist angesichts der in der Frage angegebenen Daten redundant, bietet Ihnen jedoch möglicherweise einen besseren Ausführungsplan. Weitere Informationen finden Sie in den nachfolgenden Fragen und Antworten . CROSS APPLY erzeugt eine äußere Verknüpfung .

Überlegen Sie, ob Sie für eine OVER-Klausel-Erweiterungsanforderung stimmen möchten - DISTINCT-Klausel für Aggregatfunktionen auf der Feedback-Site, wenn Sie möchten, dass diese Funktion zu SQL Server hinzugefügt wird.

Erik Darling
quelle
6

Sie können es emulieren, indem Sie dense_rankden maximalen Rang für jede Partition verwenden und dann auswählen:

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    

Sie müssten alle Nullen ausschließen col_b, um die gleichen Ergebnisse wie zu erhalten COUNT(DISTINCT).

Lennart
quelle
6

Dies ist in gewisser Weise eine Erweiterung von Lennarts Lösung , aber es ist so hässlich, dass ich es nicht wage, es als Bearbeitung vorzuschlagen. Ziel ist es, die Ergebnisse ohne abgeleitete Tabelle zu erhalten. Möglicherweise besteht dafür nie die Notwendigkeit, und in Verbindung mit der Hässlichkeit der Abfrage scheint das gesamte Unternehmen wie eine vergebliche Anstrengung zu sein. Ich wollte dies dennoch als Übung machen und möchte nun mein Ergebnis teilen:

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
;

Der Kern der Berechnung ist folgender (und ich möchte zunächst darauf hinweisen, dass die Idee nicht meine ist, ich habe an anderer Stelle von diesem Trick erfahren):

  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

Dieser Ausdruck kann ohne Änderung verwendet werden, wenn Col_Bgarantiert ist, dass die Werte in niemals Nullen enthalten. Wenn die Spalte jedoch Nullen enthalten kann, müssen Sie dies berücksichtigen, und genau dafür ist der CASEAusdruck da. Es vergleicht die Anzahl der Zeilen pro Partition mit der Anzahl der Col_BWerte pro Partition. Wenn sich die Zahlen unterscheiden, bedeutet dies, dass einige Zeilen eine Null in haben Col_Bund daher die anfängliche Berechnung ( DENSE_RANK() ... + DENSE_RANK() - 1) um 1 reduziert werden muss.

Da das - 1Teil der Kernformel ist, habe ich es so belassen. Es kann jedoch tatsächlich in den CASEAusdruck aufgenommen werden, um die gesamte Lösung weniger hässlich erscheinen zu lassen:

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
;

Diese Live-Demo auf dbfiddle logodb <> fiddle.uk kann verwendet werden, um beide Variationen der Lösung zu testen.

Andriy M.
quelle
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
quelle
1

Alternative, wenn Sie leicht allergisch gegen korrelierte Unterabfragen (Erik Darlings Antwort) und CTEs (kevinnwhats Antwort) wie ich sind.

Beachten Sie, dass beim Einfügen von Nullen in die Mischung keine dieser Funktionen so funktioniert, wie Sie es möchten. (aber es ist ziemlich einfach, sie nach Geschmack zu modifizieren)

Einfacher Fall:

--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]
;

Wie oben, jedoch mit Kommentaren dazu, was für die Nullbehandlung geändert werden muss:

--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
quelle