sql >> Databáze >  >> RDS >> Database

Výkonnostní překvapení a předpoklady:Libovolné TOP 1

V nedávném vláknu na StackExchange měl uživatel následující problém:

Chci dotaz, který vrátí první osobu v tabulce s GroupID =2. Pokud neexistuje nikdo s GroupID =2, chci první osobu s RoleID =2.

Zahoďme prozatím skutečnost, že „první“ je strašně definované. Ve skutečnosti bylo uživateli jedno, jakou osobu získal, zda to přišlo náhodně, svévolně nebo prostřednictvím nějaké explicitní logiky kromě jejich hlavních kritérií. Když to ignorujeme, řekněme, že máte základní tabulku:

CREATE TABLE dbo.Users
(
  UserID  INT PRIMARY KEY,
  GroupID INT,
  RoleID  INT
);

V reálném světě pravděpodobně existují další sloupce, další omezení, možná cizí klíče k jiným tabulkám a určitě další indexy. Ale nechme to jednoduché a pojďme s dotazem.

Pravděpodobná řešení

S tím designem stolu se řešení problému zdá být jednoduché, že? První pokus, který byste pravděpodobně udělali, je:

SELECT TOP (1) UserID, GroupID, RoleID
  FROM dbo.Users
  WHERE GroupID = 2 OR RoleID = 2
  ORDER BY CASE GroupID WHEN 2 THEN 1 ELSE 2 END;

Toto používá TOP a podmíněné ORDER BY považovat uživatele s GroupID =2 za vyšší prioritu. Plán pro tento dotaz je docela jednoduchý, přičemž většina nákladů se odehrává v operaci řazení. Zde jsou metriky běhu s prázdnou tabulkou:

Zdá se, že je to tak dobré, jak jen můžete – jednoduchý plán, který tabulku prohledá pouze jednou, a kromě otravného typu, se kterým byste měli být schopni žít, žádný problém, ne?

Další odpověď ve vláknu nabídla tuto složitější variantu:

SELECT TOP (1) UserID, GroupID, RoleID FROM 
(
  SELECT TOP (1) UserID, GroupID, RoleID, o = 1
  FROM dbo.Users
  WHERE GroupId = 2 
 
  UNION ALL
 
  SELECT TOP (1) UserID, GroupID, RoleID, o = 2
  FROM dbo.Users
  WHERE RoleID = 2
) 
AS x ORDER BY o;

Na první pohled byste si pravděpodobně mysleli, že tento dotaz je extrémně méně efektivní, protože vyžaduje dvě skenování seskupených indexů. V tom byste určitě měli pravdu; zde jsou metriky plánu a doby běhu proti prázdné tabulce:

Teď ale pojďme přidat data

Abych tyto dotazy otestoval, chtěl jsem použít některá realistická data. Nejprve jsem tedy naplnil 1 000 řádků ze sys.all_objects pomocí modulo operací proti object_id, abych získal nějakou slušnou distribuci:

INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (1000) ABS([object_id]), ABS([object_id]) % 7, ABS([object_id]) % 4
FROM sys.all_objects
ORDER BY [object_id]; 
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 126
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 248
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 26 overlap

Nyní, když spustím dva dotazy, zde jsou metriky běhu:

Verze UNION ALL se dodává s o něco menším počtem I/O (4 čtení oproti 5), kratší dobou trvání a nižšími odhadovanými celkovými náklady, zatímco podmíněná verze ORDER BY má nižší odhadované náklady na CPU. Údaje zde jsou dost malé na to, abychom o nich mohli dělat nějaké závěry; Chtěl jsem to jen jako kůl do země. Nyní změňme distribuci tak, aby většina řádků splňovala alespoň jedno z kritérií (a někdy obě):

DROP TABLE dbo.Users;
GO
 
CREATE TABLE dbo.Users
(
  UserID INT PRIMARY KEY,
  GroupID INT,
  RoleID INT
);
GO
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (1000) ABS([object_id]), ABS([object_id]) % 2 + 1, 
  SUBSTRING(RTRIM([object_id]),7,1) % 2 + 1
FROM sys.all_objects
WHERE ABS([object_id]) > 9999999
ORDER BY [object_id]; 
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 500
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 475
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 221 overlap

Tentokrát má podmíněná objednávka podle nejvyšší odhadované náklady na CPU i I/O:

Ale opět, při této velikosti dat je zde relativně nevýznamný dopad na trvání a čtení a kromě odhadovaných nákladů (které jsou stejně z velké části tvořeny) je těžké zde vyhlásit vítěze.

Přidejme tedy mnohem více dat

I když mě spíše baví vytvářet ukázková data z katalogových pohledů, protože je má každý, tentokrát budu kreslit do tabulky Sales.SalesOrderHeaderEnlarged z AdventureWorks2012, rozšířené pomocí tohoto skriptu od Jonathana Kehayiase. V mém systému má tato tabulka 1 258 600 řádků. Následující skript vloží milion těchto řádků do naší tabulky dbo.Users:

-- DROP and CREATE, as before
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (1000000) SalesOrderID, SalesOrderID % 7, SalesOrderID % 4
FROM Sales.SalesOrderHeaderEnlarged;
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 142,857
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 250,000
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 35,714 overlap

Dobře, teď, když spustíme dotazy, vidíme problém:variace ORDER BY přešla paralelně a vymazala jak čtení, tak CPU, což vedlo k téměř 120násobnému rozdílu v trvání:

Odstranění paralelismu (pomocí MAXDOP) nepomohlo:

(Plán UNION ALL vypadá stále stejně.)

A pokud změníme zešikmení na sudé, kde 95 % řádků splňuje alespoň jedno kritérium:

-- DROP and CREATE, as before
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (475000) SalesOrderID, 2, SalesOrderID % 7
FROM Sales.SalesOrderHeaderEnlarged
WHERE SalesOrderID % 2 = 1
UNION ALL
SELECT TOP (475000) SalesOrderID, SalesOrderID % 7, 2
FROM Sales.SalesOrderHeaderEnlarged
WHERE SalesOrderID % 2 = 0;
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (50000) SalesOrderID, 1, 1
FROM Sales.SalesOrderHeaderEnlarged AS h
WHERE NOT EXISTS (SELECT 1 FROM dbo.Users
  WHERE UserID = h.SalesOrderID);
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 542,851
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 542,851
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 135,702 overlap

Dotazy stále ukazují, že řazení je neúměrně drahé:

A s MAXDOP =1 to bylo mnohem horší (stačí se podívat na trvání):

A konečně, co asi 95 % zkosení v obou směrech (např. většina řádků splňuje kritéria GroupID nebo většina řádků splňuje kritéria RoleID)? Tento skript zajistí, že alespoň 95 % dat bude mít GroupID =2:

-- DROP and CREATE, as before
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (950000) SalesOrderID, 2, SalesOrderID % 7
FROM Sales.SalesOrderHeaderEnlarged;
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (50000) SalesOrderID, SalesOrderID % 7, 2
FROM Sales.SalesOrderHeaderEnlarged AS h
WHERE NOT EXISTS (SELECT 1 FROM dbo.Users
  WHERE UserID = h.SalesOrderID);
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 957,143
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 185,714
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 142,857 overlap

Výsledky jsou docela podobné (od této chvíle přestanu zkoušet MAXDOP):

A když to přechýlíme opačně, kde alespoň 95 % dat má RoleID =2:

-- DROP and CREATE, as before
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (950000) SalesOrderID, 2, SalesOrderID % 7
FROM Sales.SalesOrderHeaderEnlarged;
 
INSERT dbo.Users(UserID, GroupID, RoleID)
SELECT TOP (50000) SalesOrderID, SalesOrderID % 7, 2
FROM Sales.SalesOrderHeaderEnlarged AS h
WHERE NOT EXISTS (SELECT 1 FROM dbo.Users
  WHERE UserID = h.SalesOrderID);
 
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2; -- 185,714
SELECT COUNT(*) FROM dbo.Users WHERE RoleID = 2;  -- 957,143
SELECT COUNT(*) FROM dbo.Users WHERE GroupID = 2 AND RoleID = 2; -- 142,857 overlap

Výsledky:

Závěr

Ani v jednom případě, který jsem mohl vyrobit, „jednodušší“ dotaz ORDER BY – dokonce ani s jedním méně seskupeným indexovým skenem – nepřekonal složitější dotaz UNION ALL. Někdy musíte být velmi opatrní ohledně toho, co musí SQL Server udělat, když do sémantiky dotazu zavádíte operace, jako je řazení, a nespoléhat se pouze na jednoduchost plánu (nevadí žádné zaujatosti, které byste mohli mít na základě předchozích scénářů).

Váš první instinkt může být často správný, ale vsadím se, že jsou chvíle, kdy existuje lepší možnost, která navenek vypadá, jako by nemohla fungovat lépe. Jako v tomto příkladu. Jsem o něco lepší, pokud jde o zpochybňování předpokladů, které jsem si vytvořil z pozorování, a nedělám paušální prohlášení jako „skenování nikdy nefunguje dobře“ a „jednodušší dotazy vždy běží rychleji“. Pokud ze svého slovníku vyloučíte slova nikdy a vždy, možná zjistíte, že zkoušíte více těchto domněnek a obecných tvrzení a skončíte mnohem lépe.


  1. Vysvětlení operátora Oracle UNION

  2. Dotaz k získání pouze čísel z řetězce

  3. Jak získám poslední vložené ID tabulky MySQL v PHP?

  4. Jak zjistit, co zamyká moje stoly?