Database
 sql >> Baza danych >  >> RDS >> Database

Najlepsze podejścia do zgrupowanej mediany

W 2012 roku napisałem tutaj post na blogu, w którym przedstawiłem podejścia do obliczania mediany. W tym poście zajmowałem się bardzo prostym przypadkiem:chcieliśmy znaleźć medianę kolumny w całej tabeli. Od tego czasu wielokrotnie mi wspominano, że bardziej praktycznym wymogiem jest obliczenie mediany podzielonej na partycje . Podobnie jak w przypadku podstawowym, istnieje wiele sposobów rozwiązania tego problemu w różnych wersjach SQL Server; nic dziwnego, że niektóre działają znacznie lepiej niż inne.

W poprzednim przykładzie mieliśmy tylko ogólne kolumny id i val. Uczyńmy to bardziej realistycznym i powiedzmy, że mamy sprzedawców i liczbę sprzedaży, które dokonali w pewnym okresie. Aby przetestować nasze zapytania, utwórzmy najpierw prostą stertę z 17 wierszami i sprawdźmy, czy wszystkie dają oczekiwane wyniki (Sprzedaż 1 ma medianę 7,5, a Sprzedawca 2 ma medianę 6,0):

CREATE TABLE dbo.Sales(SalesPerson INT, Amount INT);
GO
 
INSERT dbo.Sales WITH (TABLOCKX)
(SalesPerson, Amount) VALUES
(1, 6 ),(1, 11),(1, 4 ),(1, 4 ),
(1, 15),(1, 14),(1, 4 ),(1, 9 ),
(2, 6 ),(2, 11),(2, 4 ),(2, 4 ),
(2, 15),(2, 14),(2, 4 );

Oto zapytania, które będziemy testować (z dużo większą ilością danych!) względem powyższej sterty, a także z indeksami pomocniczymi. Odrzuciłem kilka zapytań z poprzedniego testu, które albo w ogóle się nie skalowały, albo nie były zbyt dobrze mapowane na partycjonowane mediany (mianowicie 2000_B, który używał tabeli #temp i 2005_A, który używał przeciwnego wiersza liczby). Dodałem jednak kilka ciekawych pomysłów z niedawnego artykułu autorstwa Dwaina Camps (@DwainCSQL), który powstał na podstawie mojego poprzedniego postu.

SQL Server 2000+

Jedyną metodą z poprzedniego podejścia, która działała wystarczająco dobrze na SQL Server 2000, aby uwzględnić ją nawet w tym teście, było podejście „min jednej połowy, maksimum drugiej”:

SELECT DISTINCT s.SalesPerson, Median = (
   (SELECT MAX(Amount) FROM
     (SELECT TOP 50 PERCENT Amount FROM dbo.Sales 
      WHERE SalesPerson = s.SalesPerson ORDER BY Amount) AS t)
 + (SELECT MIN(Amount) FROM
     (SELECT TOP 50 PERCENT Amount FROM dbo.Sales 
      WHERE SalesPerson = s.SalesPerson ORDER BY Amount DESC) AS b)
) / 2.0
FROM dbo.Sales AS s;

Szczerze próbowałem naśladować wersję tabeli #temp, której użyłem w prostszym przykładzie, ale w ogóle nie skalowała się dobrze. W 20 lub 200 rzędach działało dobrze; w 2000 roku zajęło to prawie minutę; na 1 000 000 zrezygnowałem po godzinie. Zamieściłem to tutaj dla potomności (kliknij, aby odkryć).

CREATE TABLE #x
(
  i           INT IDENTITY(1,1),
  SalesPerson INT,
  Amount      INT,
  i2          INT
);
 
CREATE CLUSTERED INDEX v ON #x(SalesPerson, Amount);
 
INSERT #x(SalesPerson, Amount)
  SELECT SalesPerson, Amount 
  FROM dbo.Sales
  ORDER BY SalesPerson,Amount OPTION (MAXDOP 1);
 
UPDATE x SET i2 = i-
(
  SELECT COUNT(*) FROM #x WHERE i <= x.i 
  AND SalesPerson < x.SalesPerson
)
FROM #x AS x;
 
SELECT SalesPerson, Median = AVG(0. + Amount) 
  FROM #x AS x 
  WHERE EXISTS
  (
    SELECT 1 
      FROM #x 
      WHERE SalesPerson = x.SalesPerson 
      AND x.i2 - (SELECT  MAX(i2) / 2.0 FROM #x WHERE SalesPerson = x.SalesPerson) 
      IN (0, 0.5, 1)
  )
  GROUP BY SalesPerson;
GO
DROP TABLE #x;

SQL Server 2005+ 1

Wykorzystuje to dwie różne funkcje okienkowania, aby uzyskać sekwencję i ogólną liczbę kwot przypadających na sprzedawcę.

SELECT SalesPerson, Median = AVG(1.0*Amount)
FROM
(
   SELECT SalesPerson, Amount, rn = ROW_NUMBER() OVER 
        (PARTITION BY SalesPerson ORDER BY Amount), 
      c = COUNT(*) OVER (PARTITION BY SalesPerson)
   FROM dbo.Sales
)
AS x
WHERE rn IN ((c + 1)/2, (c + 2)/2)
GROUP BY SalesPerson;

SQL Server 2005+ 2

To pochodzi z artykułu Dwaina Campsa, który robi to samo, co powyżej, w nieco bardziej rozbudowany sposób. To zasadniczo przestawia interesujące wiersze w każdej grupie.

;WITH Counts AS
(
   SELECT SalesPerson, c
   FROM
   (
      SELECT SalesPerson, c1 = (c+1)/2, 
        c2 = CASE c%2 WHEN 0 THEN 1+c/2 ELSE 0 END
      FROM
      (
        SELECT SalesPerson, c=COUNT(*)
        FROM dbo.Sales
        GROUP BY SalesPerson
      ) a
   ) a
   CROSS APPLY (VALUES(c1),(c2)) b(c)
)
SELECT a.SalesPerson, Median=AVG(0.+b.Amount)
FROM
(
   SELECT SalesPerson, Amount, rn = ROW_NUMBER() OVER 
     (PARTITION BY SalesPerson ORDER BY Amount)
   FROM dbo.Sales a
) a
CROSS APPLY
(
   SELECT Amount FROM Counts b
   WHERE a.SalesPerson = b.SalesPerson AND a.rn = b.c
) b
GROUP BY a.SalesPerson;

SQL Server 2005+ 3

Było to oparte na sugestii Adama Machanica w komentarzach do mojego poprzedniego postu, a także wzmocnione przez Dwaina w jego artykule powyżej.

;WITH Counts AS
(
   SELECT SalesPerson, c = COUNT(*)
   FROM dbo.Sales
   GROUP BY SalesPerson
)
SELECT a.SalesPerson, Median = AVG(0.+Amount)
FROM Counts a
CROSS APPLY
(
   SELECT TOP (((a.c - 1) / 2) + (1 + (1 - a.c % 2)))
      b.Amount, r = ROW_NUMBER() OVER (ORDER BY b.Amount)
   FROM dbo.Sales b
   WHERE a.SalesPerson = b.SalesPerson
   ORDER BY b.Amount
) p
WHERE r BETWEEN ((a.c - 1) / 2) + 1 AND (((a.c - 1) / 2) + (1 + (1 - a.c % 2)))
GROUP BY a.SalesPerson;

SQL Server 2005+ 4

Jest to podobne do „2005+1” powyżej, ale zamiast używać COUNT(*) OVER() aby uzyskać liczby, wykonuje samosprzężenie z izolowanym agregatem w tabeli pochodnej.

SELECT SalesPerson, Median = AVG(1.0 * Amount)
FROM
(
    SELECT s.SalesPerson, s.Amount,  rn = ROW_NUMBER() OVER 
      (PARTITION BY s.SalesPerson ORDER BY s.Amount), c.c
    FROM dbo.Sales AS s
    INNER JOIN 
    (
      SELECT SalesPerson, c = COUNT(*) 
      FROM dbo.Sales GROUP BY SalesPerson
    ) AS c
    ON s.SalesPerson = c.SalesPerson
) AS x
WHERE rn IN ((c + 1)/2, (c + 2)/2)
GROUP BY SalesPerson;

SQL Server 2012+ 1

Był to bardzo interesujący wkład od innego MVP SQL Server Petera „Peso” Larssona (@SwePeso) w komentarzach do artykułu Dwaina; używa CROSS APPLY i nowy OFFSET / FETCH funkcjonalność w jeszcze bardziej interesujący i zaskakujący sposób niż rozwiązanie Itzika do prostszego obliczania mediany.

SELECT	d.SalesPerson, w.Median
FROM
(
  SELECT SalesPerson, COUNT(*) AS y
  FROM dbo.Sales
  GROUP BY SalesPerson
) AS d
CROSS APPLY
(
  SELECT AVG(0E + Amount)
  FROM
  (
    SELECT z.Amount
     FROM dbo.Sales AS z
     WHERE z.SalesPerson = d.SalesPerson
     ORDER BY z.Amount
     OFFSET (d.y - 1) / 2 ROWS
     FETCH NEXT 2 - d.y % 2 ROWS ONLY
  ) AS f
) AS w(Median);

SQL Server 2012+ 2

Wreszcie mamy nową PERCENTILE_CONT() funkcja wprowadzona w SQL Server 2012.

SELECT SalesPerson, Median = MAX(Median)
FROM
(
   SELECT SalesPerson,Median = PERCENTILE_CONT(0.5) WITHIN GROUP 
     (ORDER BY Amount) OVER (PARTITION BY SalesPerson)
   FROM dbo.Sales
) 
AS x
GROUP BY SalesPerson;

Prawdziwe testy

Aby przetestować wydajność powyższych zapytań, zbudujemy znacznie bardziej rozbudowaną tabelę. Będziemy mieć 100 unikalnych sprzedawców, każdy z 10 000 wartościami sprzedaży, co daje łącznie 1 000 000 wierszy. Zamierzamy również uruchomić każde zapytanie względem sterty bez zmian, z dodanym indeksem nieklastrowym na (SalesPerson, Amount) i z indeksem klastrowym w tych samych kolumnach. Oto konfiguracja:

CREATE TABLE dbo.Sales(SalesPerson INT, Amount INT);
GO
 
--CREATE CLUSTERED INDEX x ON dbo.Sales(SalesPerson, Amount);
--CREATE NONCLUSTERED INDEX x ON dbo.Sales(SalesPerson, Amount);
--DROP INDEX x ON dbo.sales;
 
;WITH x AS 
(
  SELECT TOP (100) number FROM master.dbo.spt_values GROUP BY number
)
INSERT dbo.Sales WITH (TABLOCKX) (SalesPerson, Amount)
  SELECT x.number, ABS(CHECKSUM(NEWID())) % 99
  FROM x CROSS JOIN x AS x2 CROSS JOIN x AS x3;

A oto wyniki powyższych zapytań, względem sterty, indeksu nieklastrowego i indeksu klastrowego:


Czas trwania w milisekundach różnych zgrupowanych podejść do mediany (w stosunku do stos)


Czas trwania w milisekundach różnych zgrupowanych podejść do mediany (w stosunku do stos z indeksem nieklastrowym)


Czas trwania w milisekundach różnych zgrupowanych podejść do mediany (w stosunku do indeks klastrowy)

A co z Hekatonem?

Oczywiście byłem ciekaw, czy ta nowa funkcja w SQL Server 2014 może pomóc w którymkolwiek z tych zapytań. Utworzyłem więc bazę danych In-Memory, dwie wersje tabeli Sales In-Memory (jedna z indeksem skrótu na (SalesPerson, Amount) , a drugi po prostu (SalesPerson) ) i ponownie przeprowadziłem te same testy:

CREATE DATABASE Hekaton;
GO
ALTER DATABASE Hekaton ADD FILEGROUP xtp CONTAINS MEMORY_OPTIMIZED_DATA;
GO
ALTER DATABASE Hekaton ADD FILE (name = 'xtp', filename = 'c:\temp\hek.mod') TO FILEGROUP xtp;
GO
ALTER DATABASE Hekaton SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT ON;
GO
 
USE Hekaton;
GO
 
CREATE TABLE dbo.Sales1
(
  ID INT IDENTITY(1,1) PRIMARY KEY NONCLUSTERED,
  SalesPerson INT NOT NULL,
  Amount INT NOT NULL,
  INDEX x NONCLUSTERED HASH (SalesPerson, Amount) WITH (BUCKET_COUNT = 256)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
GO
 
CREATE TABLE dbo.Sales2
(
  ID INT IDENTITY(1,1) PRIMARY KEY NONCLUSTERED,
  SalesPerson INT NOT NULL,
  Amount INT NOT NULL,
  INDEX x NONCLUSTERED HASH (SalesPerson) WITH (BUCKET_COUNT = 256)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
GO
 
;WITH x AS 
(
  SELECT TOP (100) number FROM master.dbo.spt_values GROUP BY number
)
INSERT dbo.Sales1 (SalesPerson, Amount) -- TABLOCK/TABLOCKX not allowed here
  SELECT x.number, ABS(CHECKSUM(NEWID())) % 99
  FROM x CROSS JOIN x AS x2 CROSS JOIN x AS x3;
 
INSERT dbo.Sales2 (SalesPerson, Amount) 
  SELECT SalesPerson, Amount 
  FROM dbo.Sales1;

Wyniki:


Czas trwania w milisekundach dla różnych obliczeń mediany w pamięci stoły

Nawet przy prawidłowym indeksie mieszającym nie widzimy znaczących ulepszeń w stosunku do tradycyjnej tabeli. Co więcej, próba rozwiązania problemu mediany za pomocą natywnie skompilowanej procedury składowanej nie będzie łatwym zadaniem, ponieważ wiele konstrukcji językowych użytych powyżej jest niepoprawnych (kilka z nich też mnie zaskoczyło). Próba skompilowania wszystkich powyższych odmian zapytań spowodowała tę paradę błędów; niektóre wystąpiły wielokrotnie w ramach każdej procedury, a nawet po usunięciu duplikatów jest to nadal trochę komiczne:

Msg 10794, Level 16, State 47, Procedure GroupedMedian_2000
Opcja „DISTINCT” nie jest obsługiwana z natywnie skompilowanymi procedurami składowanymi.
Msg 12311, Level 16, State 37, Procedure GroupedMedian_2000
Podzapytania ( zapytania zagnieżdżone w innym zapytaniu) nie są obsługiwane z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, Poziom 16, Stan 48, Procedura GroupedMedian_2000
Opcja „PERCENT” nie jest obsługiwana w przypadku natywnie skompilowanych procedur składowanych.

Msg 12311, Level 16, State 37, Procedure GroupedMedian_2005_1
Podzapytania (zapytania zagnieżdżone w innym zapytaniu) nie są obsługiwane z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, Level 16, State 91 , Procedura GroupedMedian_2005_1
Funkcja agregująca „NUMER WIERSZY” nie jest obsługiwana z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, Poziom 16, Stan 56, Procedura GroupedMedian_2005_1
Operator „IN” nie jest obsługiwany z natywnie skompilowane procedury składowane.

Msg 12310, Poziom 16, stan 36, procedura GroupedMedian_2005_2
Common Table Expressions (CTE) nie są obsługiwane z natywnie kompilowanymi procedurami składowanymi.
Msg 12309, Level 16, State 35, Procedure GroupedMedian_2005_2
Wyciągi formularza INSERT…VALUES…, które wstawiają wiele wierszy, nie są obsługiwane z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, poziom 16, stan 53, procedura GroupedMedian_2005_2
Operator „ZASTOSUJ” nie jest obsługiwany w przypadku natywnie skompilowanych procedur składowanych.
Msg 12311, Level 16, State 37, Procedure GroupedMedian_2005_2
Podzapytania (zapytania zagnieżdżone w innym zapytaniu) nie są obsługiwane z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, Level 16, State 91, Procedure GroupedMedian_2005_2
Funkcja agregująca „ROW_NUMBER” nie jest obsługiwana z natywnie kompilowanymi procedurami składowanymi.

Msg 12310, Level 16, State 36, Procedure GroupedMedian_2005_3
Wspólne wyrażenia tabeli (CTE) są nieobsługiwane z natywnie skompilowanymi składowanymi procedur.
Msg 12311, Level 16, State 37, Procedure GroupedMedian_2005_3
Podzapytania (zapytania zagnieżdżone w innym zapytaniu) nie są obsługiwane z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, Level 16, State 91 , Procedura GroupedMedian_2005_3
Funkcja agregująca 'NUMER WIERSZ' nie jest obsługiwana z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, Poziom 16, Stan 53, Procedura GroupedMedian_2005_3
Operator 'ZASTOSUJ' nie jest obsługiwany z natywnie skompilowane procedury składowane.

Msg 12311, poziom 16, stan 37, procedura GroupedMedian_2005_4
Podkwerendy (zapytania zagnieżdżone w innym zapytaniu) nie są obsługiwane w przypadku natywnie skompilowanych procedur składowanych.
Msg 10794, poziom 16, stan 91, procedura GroupedMedian_2005_4
Funkcja agregująca „ROW_NUMBER” nie jest obsługiwana z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, Level 16, State 56, Procedure GroupedMedian_2005_4
Operator „IN” nie jest obsługiwany przez natywnie skompilowany magazyn ed procedur.

Msg 12311, Level 16, State 37, Procedure GroupedMedian_2012_1
Podzapytania (zapytania zagnieżdżone w innym zapytaniu) nie są obsługiwane z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, Poziom 16, stan 38, procedura GroupedMedian_2012_1
Operator „OFFSET” nie jest obsługiwany z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, Level 16, State 53, Procedure GroupedMedian_2012_1
Operator „APPLY” nie jest obsługiwany z natywnie skompilowanymi procedurami składowanymi.

Msg 12311, Level 16, State 37, Procedure GroupedMedian_2012_2
Podzapytania (zapytania zagnieżdżone w innym zapytaniu) nie są obsługiwane z natywnie skompilowanymi procedurami składowanymi.
Msg 10794, poziom 16, stan 90, procedura GroupedMedian_2012_2
Funkcja agregująca „PERCENTILE_CONT” nie jest obsługiwana w przypadku natywnie skompilowanych procedur składowanych.

Jak napisano obecnie, żadne z tych zapytań nie mogło zostać przeniesione do natywnie skompilowanej procedury składowanej. Być może coś, na co warto zwrócić uwagę, aby znaleźć kolejny post.

Wniosek

Odrzucenie wyników Hekaton, a gdy obecny jest indeks pomocniczy, zapytanie Petera Larssona („2012+ 1”) przy użyciu OFFSET/FETCH okazał się dalekim zwycięzcą w tych testach. Chociaż jest to nieco bardziej złożone niż równoważne zapytanie w testach niepartycjonowanych, jest to zgodne z wynikami, które zaobserwowałem ostatnio.

W tych samych przypadkach 2000 MIN/MAX podejście i 2012 PERCENTILE_CONT() wyszły jak prawdziwe psy; znowu, tak jak moje poprzednie testy w prostszym przypadku.

Jeśli nie korzystasz jeszcze z SQL Server 2012, następną najlepszą opcją jest „2005+ 3” (jeśli masz indeks pomocniczy) lub „2005+ 2”, jeśli masz do czynienia ze stertą. Przepraszam, że musiałem wymyślić dla nich nowy schemat nazewnictwa, głównie po to, aby uniknąć pomyłek z metodami w moim poprzednim poście.

Oczywiście są to moje wyniki w odniesieniu do bardzo konkretnego schematu i zestawu danych – podobnie jak w przypadku wszystkich zaleceń, należy przetestować te podejścia w odniesieniu do własnego schematu i danych, ponieważ inne czynniki mogą wpływać na różne wyniki.

Jeszcze jedna uwaga

Oprócz słabej wydajności i braku obsługi w natywnie skompilowanych procedurach składowanych, jeszcze jeden problem związany z PERCENTILE_CONT() jest to, że nie można go używać w starszych trybach zgodności. Jeśli spróbujesz, pojawi się ten błąd:

Msg 10762, Poziom 15, Stan 1
Funkcja PERCENTILE_CONT nie jest dozwolona w bieżącym trybie zgodności. Jest to dozwolone tylko w trybie 110 lub wyższym.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak pobrać zestaw znaków za pomocą SUBSTRING w SQL?

  2. Operatory SQL

  3. Jaka jest różnica między RANK a DENSE_RANK w SQL?

  4. Co to jest wiosenna integracja?

  5. Łatwa obsługa CRUD dzięki połączeniu z bazą danych PDO