forward scan vs. backward scan

Évekkel ezelőtt volt egy érdekes felvetése egy "menő" tanácsadó cégnek (nem, nincs név :)), miszerint mindegy, hogy forward vagy backward scan van egy indexen, mert ugyan olyan jó. Ezzel én vitába szálltam, mert nem feltétlen igaz ez, főleg nem ott, ahol számít a teljesítmény. Részletekbe nem feltétlen megyek bele, de a lényeget megmutatom.

Demó környezet

A példához szükségem van egy relatív nagy táblára, amit most a tempdb-ben hozok létre:

USE [tempdb]
GO

DROP TABLE IF EXISTS [dbo].[T1]
GO
CREATE TABLE [dbo].[T1]
(
    [col1] INT IDENTITY PRIMARY KEY NONCLUSTERED,
    [col2] INT,
    [col3] DATETIME
)
GO

CREATE CLUSTERED INDEX [CI_T1] ON [dbo].[T1] ([col3])
GO

Ebbe a táblába létrehozok 5 millió sort (meg egy kicsit).

INSERT INTO [dbo].[T1]
(
    [col2],
    [col3]
)
SELECT
    CHECKSUM(NEWID()),
    DATEADD(ms,CHECKSUM(NEWID()), CURRENT_TIMESTAMP)
FROM
    master.sys.objects o 
CROSS JOIN
    master.sys.objects o1
GO

INSERT INTO [dbo].[T1]
(
    [col2],
    [col3]
)
SELECT TOP 5000000
    CHECKSUM(NEWID()),
        DATEADD(ms,CHECKSUM(NEWID()), CURRENT_TIMESTAMP)
FROM
    [dbo].[T1] a 
CROSS JOIN
    [dbo].[T1] b
GO

Forward scan

Lássunk egy egyszerű lekérdezést a csodálatos T1 táblára:

SELECT *
FROM   
    [dbo].[T1]
WHERE  
    [col3] BETWEEN '20170101' AND '20180101'
AND 
    [col2] BETWEEN 300000000 AND 699999999
ORDER BY [col3] ASC;

Ugye nem is bonyolult. Vajon mi a futási terv?

Nahát, ez egy parallel futási terv. Amit még érdemes megnézni az az Index Seek tulajdonságai:

Látható, hogy itt forward scan van, illetve parallel futási terv van, azaz több CPU-t is tudunk használni. Ez utóbbi az érdekes, és ezen volt a "vita". 

Backward scan

Most nézzük meg ugyan ezt a lekérdezést egy picit másként:

SELECT *
FROM   
    [dbo].[T1]
WHERE  
    [col3] BETWEEN '20170101' AND '20180101'
AND 
    [col2] BETWEEN 300000000 AND 699999999
ORDER BY [col3] DESC;

Amiben különbözik, az az ORDER BY rész. E miatt most másik futási tervünk van:

Azt is észre kell venni, hogy most egy szálon futott a lekérdezés! Nézzük csak meg az Index Seek operátor tulajdonságait:

Nahát, itt backward scan van, illetve nem parallel terv. 

Lássuk tudok e ezen változtatni.

SELECT *
FROM   
    [dbo].[T1]
WHERE  
    [col3] BETWEEN '20170101' AND '20180101'
AND 
    [col2] BETWEEN 300000000 AND 699999999
ORDER BY [col3] DESC
OPTION (MAXDOP 4) 

Sajnos nem, ami látszik is a futási tervben is:

Esetleg más módon? Pl. egy nem dokumentált query hint segítségével? :)

SELECT *
FROM   
    [dbo].[T1]
WHERE  
    [col3] BETWEEN '20170101' AND '20180101'
AND 
    [col2] BETWEEN 300000000 AND 699999999
ORDER BY [col3] DESC
OPTION(USE HINT('ENABLE_PARALLEL_PLAN_PREFERENCE'))

Aha, itt már jó lesz ez, de a futási tervben megjelent egy Sort operátor is. A költsége is jóval magasabb lett, mint az eddigi összes lekérdezésnek. Az ENABLE_PARALLEL_PLAN_REFERENCE hint SQL Server 2016 Sp1 CU2-től elérhető, de jelenleg nem dokumentált, azaz mindenki csak saját felelősségre használja. Ennek lehet alternatívája egy table hint, a FORCESCAN, ami esetében ugyan ezt a futási tervet kapom, illetve a költsége is ugyan olyan magas lesz.

Konklúzió

Alapvetően mindegy is, hogy backward vagy forward scan van, de akkor már nem, ha parallel futási tervvel (legalábbis némelyik operátor hasznot tud húzni a párhuzamos futásból) gyorsabban ki tudom szolgálni a lekérdezést. Backward scan esetén nem lehetséges mindenhol a parallel futás egyes operátoroknál, így ez egy szálon fog menni, ahogy a példában is látszik. Szóval a lényeg, hogy erre is érdemes figyelni. Aki kicsit mélyebbre megy a kódban, pár furcsa dolgot is észrevehet, pl az index sorrend és az ORDER BY. 

A query és table hint használatát is csak akkor javaslom, amikor a "hagyományos" optimalizálással már nem érünk célt, ez legyen az utolsó, amit bevetünk a lekérdezések "gyorsításához"

Akit mélyebben érdekel a téma, ajánlom a windbg használatát és ott meg lehet nézni a call stack-et is :)

Comments (3) -

Ez valószínűleg azért van, mert bár az index lapjai oda-vissza láncolva rendezettek tehát a visszafelé scan lehetséges lenne, de a lapon belül a sorok logikailag csak egyik irányban vannak rendezve, ha a másik irányba szeretnénk olvasni őket, akkor két dolgot kellene tenni:
1. Megkeresni a lap végén található RowOffset területen belül az utolsó sort (de ehhez először végig kell olvasni a normál sorrend szerint, mert a RowOffset terület hossza változó) és 2 bájtonként visszafelé olvasni a sorokat pointereit.
vagy
2. Beolvasni a sorokat a rendezettségüknek megfelelően és a lap sorai között egy utólagos rendezést csinálni visszafelé (gondolom ez az extra Sort operátor a példában).

Tehát a backward scan csak akkor lehet jó, ha maga az index is visszafelé rendezett.

János Berke 1/26/2018 11:00:26 AM

Mutex a kulcs Smile Wndbg-gal nézd meg, én is azt tettem Smile A plusz sorbarendezést egyébként is megjelenítené egy Sort operátorral, de az itt nincs. Mivel az indexek duplán linkeltek, így könnyen megvan a vége, már csak a b-tree miatt is, egyszerűen olyan extra feladatok vannak a vissza scan esetén, ami miatt nem lehet parallel futás, ez feature Smile

János Berke 2/6/2018 4:56:30 PM

bocs, még egy dolog: a lapon a sorok nincsenek rendezve! Erről volt meetup-on is szó, illetve itt is van egy blog bejegyzésem pont erről.

Add comment