sql >> Databasteknik >  >> RDS >> Database

Prestanda överraskningar och antaganden:Godtycklig TOPP 1

I en ny tråd om StackExchange hade en användare följande problem:

Jag vill ha en fråga som returnerar den första personen i tabellen med ett GroupID =2. Om ingen med ett GroupID =2 existerar vill jag ha den första personen med ett RollID =2.

Låt oss för tillfället förkasta det faktum att "först" är fruktansvärt definierat. I själva verket brydde användaren sig inte om vilken person de fick, om det kom slumpmässigt, godtyckligt eller genom någon explicit logik utöver deras huvudkriterier. Om du ignorerar det, låt oss säga att du har en grundläggande tabell:

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

I den verkliga världen finns det förmodligen andra kolumner, ytterligare begränsningar, kanske främmande nycklar till andra tabeller och säkert andra index. Men låt oss hålla det här enkelt och komma med en fråga.

Sannolika lösningar

Med den bordsdesignen verkar det enkelt att lösa problemet, eller hur? Det första försöket du förmodligen skulle göra är:

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;

Detta använder TOP och en villkorlig ORDER BY att behandla de användare med ett GroupID =2 som högre prioritet. Planen för den här frågan är ganska enkel, där det mesta av kostnaderna sker i en sorts operation. Här är körtidsstatistik mot en tom tabell:

Det här ser ut att vara ungefär så bra som du kan göra – en enkel plan som bara skannar tabellen en gång, och förutom en irriterande sort som du borde kunna leva med, inga problem, eller hur?

Nåväl, ett annat svar i tråden gav denna mer komplexa variant:

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;

Vid första anblicken skulle du förmodligen tro att den här frågan är extremt mindre effektiv, eftersom den kräver två klustrade indexskanningar. Du skulle definitivt ha rätt i det; här är plan- och körtidsmåtten mot en tom tabell:

Men nu, låt oss lägga till data

För att testa dessa frågor ville jag använda lite realistisk data. Så först fyllde jag i 1 000 rader från sys.all_objects, med modulo-operationer mot object_id för att få en anständig distribution:

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

Nu när jag kör de två frågorna, här är körtidsstatistiken:

UNION ALL-versionen kommer in med något mindre I/O (4 gånger jämfört med 5), lägre varaktighet och lägre uppskattad totalkostnad, medan den villkorade ORDER BY-versionen har lägre uppskattad CPU-kostnad. Uppgifterna här är ganska små att dra några slutsatser om; Jag ville bara ha det som en insats i marken. Låt oss nu ändra fördelningen så att de flesta rader uppfyller minst ett av kriterierna (och ibland båda):

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

Den här gången har den villkorade beställningen de högsta uppskattade kostnaderna för både CPU och I/O:

Men återigen, vid den här datastorleken är det relativt obetydlig påverkan på varaktighet och läsningar, och bortsett från de uppskattade kostnaderna (som i alla fall till stor del består av), är det svårt att utse en vinnare här.

Så, låt oss lägga till mycket mer data

Även om jag hellre njuter av att bygga exempeldata från katalogvyerna, eftersom alla har sådana, tänker jag den här gången rita på bordet Sales.SalesOrderHeaderEnlarged från AdventureWorks2012, utökat med det här skriptet från Jonathan Kehayias. På mitt system har den här tabellen 1 258 600 rader. Följande skript kommer att infoga en miljon av dessa rader i vår dbo.Users-tabell:

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

Okej, nu när vi kör frågorna ser vi ett problem:ORDER BY-variationen har gått parallellt och har raderat både läsningar och CPU, vilket ger en nästan 120X skillnad i varaktighet:

Att eliminera parallellism (med MAXDOP) hjälpte inte:

(Union ALLA-planen ser fortfarande likadan ut.)

Och om vi ändrar skevningen till att vara jämn, där 95 % av raderna uppfyller minst ett kriterium:

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

Frågorna visar fortfarande att sorten är oöverkomligt dyr:

Och med MAXDOP =1 var det mycket värre (se bara på varaktigheten):

Slutligen, vad sägs om 95 % skevhet i endera riktningen (t.ex. de flesta rader uppfyller GroupID-kriterierna, eller de flesta rader uppfyller RollID-kriterierna)? Detta skript säkerställer att minst 95 % av data har 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

Resultaten är ganska lika (jag ska bara sluta prova MAXDOP-grejen från och med nu):

Och sedan om vi snedställer åt andra hållet, där minst 95 % av datan har RollID =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

Resultat:

Slutsats

I inte ett enda fall som jag kunde tillverka överträffade den "enklare" ORDER BY-frågan – även med en mindre klustrad indexskanning – den mer komplexa UNION ALL-frågan. Ibland måste du vara mycket försiktig med vad SQL Server måste göra när du introducerar operationer som sortering i din frågesemantik, och inte förlita dig enbart på enkelheten i planen (kasta inte emot någon fördom du kan ha baserat på tidigare scenarier).

Din första instinkt kan ofta vara korrekt, men jag slår vad om att det finns tillfällen då det finns ett bättre alternativ som på ytan ser ut som att det omöjligt kunde fungera bättre. Som i detta exempel. Jag blir ganska bättre på att ifrågasätta antaganden jag har gjort från observationer och att inte göra generella uttalanden som "skanningar fungerar aldrig bra" och "enklare frågor går alltid snabbare." Om du tar bort orden aldrig och alltid från ditt ordförråd, kan du komma på att du sätter fler av dessa antaganden och allmänna påståenden på prov, och att du hamnar mycket bättre.


  1. Android Room kompileringstid varning om kolumn i främmande nyckel som inte ingår i ett index. Vad betyder det?

  2. Förstå Pivot Operator i SQL

  3. Synkronisera offline SQLite databas med online MySQL databas

  4. int(11) vs. int(något annat)