Aktualizacja 18.12.2014
Przy przytłaczającej odpowiedzi na główne pytanie brzmi „Nie”, bardziej interesujące odpowiedzi skupiły się na części 2, w jaki sposób rozwiązać zagadkę wydajności w sposób wyraźny ORDER BY
. Chociaż zaznaczyłem już odpowiedź, nie zdziwiłbym się, gdyby istniało jeszcze lepsze rozwiązanie.
Oryginalny
To pytanie powstało, ponieważ jedyne niezwykle szybkie rozwiązanie konkretnego problemu działa tylko bez ORDER BY
klauzuli. Poniżej znajduje się pełny T-SQL potrzebny do wygenerowania problemu, wraz z moim proponowanym rozwiązaniem (używam SQL Server 2008 R2, jeśli to ma znaczenie).
--Create Orders table
IF OBJECT_ID('tempdb..#Orders') IS NOT NULL DROP TABLE #Orders
CREATE TABLE #Orders
(
OrderID INT NOT NULL IDENTITY(1,1)
, CustID INT NOT NULL
, StoreID INT NOT NULL
, Amount FLOAT NOT NULL
)
CREATE CLUSTERED INDEX IX ON #Orders (StoreID, Amount DESC, CustID)
--Add 1 million rows w/ 100K Customers each of whom had 10 orders
;WITH
Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows
Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows
Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows
Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows
Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows
Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows
FinalCte AS (SELECT ROW_NUMBER() OVER (ORDER BY C) AS Number FROM Cte5)
INSERT INTO #Orders (CustID, StoreID, Amount)
SELECT CustID = Number / 10
, StoreID = Number % 4
, Amount = 1000 * RAND(Number)
FROM FinalCte
WHERE Number <= 1000000
SET STATISTICS IO ON
SET STATISTICS TIME ON
--For StoreID = 1, find the top 500 customers ordered by their most expensive purchase (Amount)
--Solution A: Without ORDER BY
DECLARE @Top INT = 500
SELECT DISTINCT TOP (@Top) CustID
FROM #Orders WITH(FORCESEEK)
WHERE StoreID = 1
OPTION(OPTIMIZE FOR (@Top = 1), FAST 1);
--9 logical reads, CPU Time = 0 ms, elapsed time = 1 ms
GO
--Solution B: With ORDER BY
DECLARE @Top INT = 500
SELECT TOP (@Top) CustID
FROM #Orders
WHERE StoreID = 1
GROUP BY CustID
ORDER BY MAX(Amount) DESC
OPTION(MAXDOP 1)
--745 logical reads, CPU Time = 141 ms, elapsed time = 145 ms
--Uses Sort operator
GO
Oto plany wykonania odpowiednio dla rozwiązania A i B:
Rozwiązanie A zapewnia wydajność, której potrzebuję, ale nie mogłem zmusić go do działania z taką samą wydajnością przy dodawaniu dowolnej klauzuli ORDER BY (np. Patrz Rozwiązanie B). I z pewnością wydaje się, że Rozwiązanie A musiałoby dostarczać swoje wyniki w kolejności, ponieważ 1) tabela ma tylko jeden indeks, 2) wyszukiwanie jest wymuszone, co eliminuje możliwość użycia skanowania kolejności przydziału na podstawie stron IAM .
Więc moje pytania to:
Czy mam rację, że zagwarantuje to zamówienie w tym przypadku bez zamówienia według klauzuli?
Jeśli nie, to czy istnieje inna metoda narzucenia planu tak szybkiego jak Rozwiązanie A, najlepiej takiego, który pozwala uniknąć sortowania? Pamiętaj, że musiałby rozwiązać dokładnie ten sam problem (
StoreID = 1
znaleźć 500 najlepszych klientów uporządkowanych według ich najdroższej kwoty zakupu). Musiałby także nadal korzystać z#Orders
tabeli, ale inne schematy indeksowania byłyby OK.
źródło
ORDER BY
.Odpowiedzi:
Nie . Wyróżnienie przepływu, które zachowuje porządek (pozwalając
ORDER BY
bez sortowania) nie jest obecnie zaimplementowane w SQL Server. Można to zrobić w zasadzie, ale wtedy wiele rzeczy jest możliwych, jeśli pozwolimy na zmianę kodu źródłowego SQL Server. Jeśli potrafisz uzasadnić tę pracę programistyczną, możesz zasugerować to firmie Microsoft .Tak. (Wskazówki dotyczące tabel i zapytań są wymagane tylko w przypadku używania estymatora liczności liczebności sprzed 2014 r.):
Rozwiązanie SQL CLR
Poniższy skrypt pokazuje użycie wartościowej tabeli SQL CLR w celu spełnienia określonych wymagań. Nie jestem ekspertem w języku C #, więc kod może ulec poprawie:
Tabela testowa i przykładowe dane z pytania:
Test działania:
Plan wykonania (zwróć uwagę na potwierdzenie
ORDER
gwarancji):Na moim laptopie zwykle wykonuje się to w 80-100 ms. Nie jest to tak szybkie jak powyższe przepisywanie T-SQL, ale powinno wykazywać dobrą stabilność wydajności w obliczu różnych dystrybucji danych.
Kod źródłowy:
źródło
Bez
ORDER BY
wielu rzeczy może się nie udać. Wyłączyłeś wszystkie możliwe problemy, które mogę wymyślić, ale to nie znaczy, że nie ma problemu, ani nie będzie żadnego w przyszłym wydaniu.To powinno działać:
Wyciągnij partie 500 wierszy ze stołu w pętli i zatrzymaj się, gdy otrzymasz 500 różnych identyfikatorów klientów. Zapytanie pobierania może wyglądać następująco:
Spowoduje to wykonanie skanowanego zakresu zasięgu na indeksie.
Amount <= @lastAmountFetched
Orzecznikiem jest tam, aby ciągnąć stopniowo kolejne rekordy. Każde zapytanie dotknie tylko 500 rekordów. Oznacza to, że jest to O (1). Nie staje się droższy, im dalej przejdziesz do indeksu.Musisz zachować zmienną,
@lastAmountFetched
aby zmniejszyć do najmniejszej wartości pobranej w tej instrukcji.W ten sposób będziesz stopniowo skanować indeks w uporządkowany sposób. Przeczytasz co najwyżej (500-1) wierszy więcej niż byłaby optymalna ilość.
Będzie to o wiele szybsze niż zawsze agregowanie około 100 000 zamówień dla określonego sklepu. Prawdopodobnie potrzeba tylko kilku iteracji po 500 wierszy.
Zasadniczo jest to ręcznie kodowany odrębny operator przepływu.
Możesz też użyć kursora, aby pobrać jak najmniej wierszy. Będzie to o wiele wolniejsze, ponieważ wykonywanie 500 zapytań jednorzędowych najczęściej jest wolniejsze niż wykonywanie partii 500 wierszy.
Alternatywnie, po prostu odpytuj wszystkie wiersze bez
DISTINCT
w uporządkowany sposób i spraw, aby aplikacja kliencka zakończyła zapytanie po zwróceniu wystarczającej liczby (za pomocąSqlCommand.Cancel
).źródło
#fetchedOrders
że nie zawiera klientów, których już widzieliśmy? Prawdopodobnie wiąże się to z indeksem szukać w tabeli temp, co nie jest zupełnie tak samo jak „płynąć odrębne” i będzie drożeć im więcej wierszy widzieliśmy (choć nadal będzie pokonać roztwór B we wszystkich, ale najgorszy przypadek konieczności skanowania wszystkich wierszy, ponieważ jest tylko jeden klient, dla którego A i B będą działać identycznie).IGNORE_DUP_KEY
może to zrobić.