Archive for the ‘SQL Server 2012’ Category

Nagy táblák joinolása

Tuesday, November 22nd, 2016

Egyik folyó munkámban több tízmilló soros táblákon végzett joinokat kellett optimalizálni. Általában ez nem kihívás, mert szinte mindig vannak szűrési feltételek, amelyeket kellő közelségbe víve a táblákhoz és rendes indexekat alápakolva már csak pár ezer joint kell végrehajtani, ami gyors lesz.
De most tényleg össze kellett joinolni sok millió sort, szűrés nélkül.
Mit lehet ezzel kezdeni? Sajnos itt már eléggé behatárolt területen mozgunk. A normál indexelős megoldások nem segítenek, mivel minden táblát teljes egészében be kell járni (nincs where).
Ráadásul ha *-os a select, akkor a cover NC index se játszik, hogy legalább az IO csökkenne.
Merge joinra lehet játszani clu indexekkel, de azért ez korlátos terület sok tábla esetén, illetve párhuzamos tervek esetén magától nem fog merge joint használni (itt írnak egy trace flagről, amivel mégis rá lehet venni).
Mit lehet tenni. Egyik lehetőség előre elkészíteni a join indexelt view-ban. Erre ügyesen ráharap az optimizer, ha van olyan join amit aztán többször futtatunk, akkor megéri ez a denormalizálás.
Ha viszont van újabb szerverünk (2016), akkor van sokkal durvább lehetőség: Columnstore index.
Az a baj ugye a nagy joinnal, hogy akárhogy is trükközünk, ez nagy meló a prociknak és az IO alrendszernek (vinkóknak). Az indexed view ezt úgy oldja meg, hogy egyszer kell megcsinálni, aztán sokszor élvezni az előre összepakolt adatokat.
A columnstore viszont (dióhéjban) azért piszok gyors mert:
1. 5-10-szeresen tömörítve tárolja az adatokat, kevesebb IO, illetve a memóriában a buffer cache-t is jobban ki tudja használni (mintha több RAM-unk lenne)
2. Képes az adatok csak egy részét felolvasni, ha csak kevés oszlop kell (select *-on ez nem segít persze)
3. Képes batch módban belülről párhuzamosan végrehajtani a műveletek egy részét (ez nagyon durván megdobja)
4. Képes a sorok egy részét felolvasni where feltétel alapján, mivel minden 1m sorhoz (szegmens) nyilván tarja az adott oszlop min és max értékét
5. Le tud nyomni operátorokat (pl. sum) a storage engine-be, így nem kell adatokat passzolgatni a rétegek között.

No, lássuk a medvét. Létrehoztam két másolatot egy 100 millió soros táblából. A tesztgép egy két éves laptop 2 core-ral és 8G RAM-mal, SSD-vel. Nem egy szerver.
A két táblát a kulcsai mentés join-olom, így mind a 100 millió sort végig kell néznie, és ennyi találat is lesz.

Először sima Clu index:
create clustered index IX_Clu1 on B1(Id)
create clustered index IX_Clu2 on B2(Id)

select count(*) from B1 join B2 on B1.Id = B2.Id

SQL Server parse and compile time:
CPU time = 15 ms, elapsed time = 18 ms.

(1 row(s) affected)
Table ‘B1’. Scan count 5, logical reads 1141262, physical reads 6,
read-ahead reads 1138814, lob logical reads 0, lob physical reads 0,
lob read-ahead reads 0.
Table ‘B2’. Scan count 5, logical reads 1140956, physical reads 4,
read-ahead reads 1138821, lob logical reads 0, lob physical reads 0,
lob read-ahead reads 0.
Table ‘Workfile’. Scan count 896, logical reads 480256, physical reads
2688, read-ahead reads 477568, lob logical reads 0, lob physical reads
0, lob read-ahead reads 0.
Table ‘Worktable’. Scan count 0, logical reads 0, physical reads 0,
read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob
read-ahead reads 0.

SQL Server Execution Times:
CPU time = 477262 ms, elapsed time = 377318 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 1 ms.

SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.

377 másodperc.

Jöhet a columnstore index:
create clustered columnstore index IX_CStore1 on B1
create clustered columnstore index IX_CStore2 on B2

select count(*) from B1 join B2 on B1.Id = B2.Id

SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 6 ms.

(1 row(s) affected)
Table ‘B2’. Scan count 4, logical reads 0, physical reads 0,
read-ahead reads 0, lob logical reads 105018, lob physical reads 0,
lob read-ahead reads 0.
Table ‘B2’. Segment reads 103, segment skipped 0.
Table ‘B1’. Scan count 4, logical reads 0, physical reads 0,
read-ahead reads 0, lob logical reads 104998, lob physical reads 0,
lob read-ahead reads 0.
Table ‘B1’. Segment reads 102, segment skipped 0.
Table ‘Worktable’. Scan count 0, logical reads 0, physical reads 0,
read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob
read-ahead reads 0.

SQL Server Execution Times:
CPU time = 79920 ms, elapsed time = 27834 ms.
SQL Server parse and compile time:
CPU time = 0 ms, elapsed time = 0 ms.

SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.

377 sec vs. 28 sec. Azért ez masszív különbség. :)

Érdekességképpen megnéztem NC Columnstore index-szel is, úgy 60 sec jön ki. Ez se rossz.

A jövő héten lehet ki tudjuk próbálni egy nagyobb géppel is, kíváncsi vagyok, ott mit tudunk vele kihozni.

Ha esetleg valakinek vannak már gyakorlati sikerei, érdekelnek a számok.

SQL Server memória hiány miatti lassú lekérdezés

Sunday, June 19th, 2016

Érdekes hibába futottam bele mostanában. A lapotopomban csak 8G RAM van, így az azon futó SQL servernek nem sok marad, amikor a Chrome, a Visual Studio 2015 és más memóriazabáló alkalmazások elkezdenek terjeszkedni.
A probléma az volt, hogy egy olyan lekérdezés, amely Sortot tartalmazott a szokásos 2-3 mp helyett fél-egy percig futott. A végrehajtási tervben volt egy figyelmeztetés, hogy a sort operátor kénytelen kipakolni a rendezést a tempdb-be (spill). De ettől még nem kellett volna ilyen lassúnak lennie. Csak 3500 sorról volt szó, ez azért belefért volna memóriába.

A select * from sys.dm_exec_query_memory_grants lekérdezésből kiderült, hogy a lekérdezés sortjának kellett volna 10M memória, de nem tudott annyit kapni, ezért várakozott, és aztán 20 másodperc múlva egyszer csak timeout lett, és kényszeredetten csak nekifogott végrehajtani a sortot a tempdbben.

Azaz egy egy nem szokványos lassú lekérdezés volt, nem sok lapolvasással járt, mint a tipikus rosszul optimalizált lekérdezések, hanem a memory grantre várt.
A megoldás az lett, hogy beállítottam 1G min memory-t az SQL Servernek, így már kiszámíthatóan jól érzi magát.

Egy rendes szerveren valószínűleg ritkább az ilyen memória kényszer, de azért jó tudni róla.

2 karakter, és máris más az execution plan

Wednesday, June 24th, 2015

Az eredeti lekérdezésben az ORDER BY így nézett ki: order by BDT.
Ez a számított oszlopra vonatkozott, azért a szervernek meg kellett oldania a rendezést egy külön lépésben. A b.BDT után viszont már tudja használni az alatta levő index rendezettségét, így nem kell rendeznie. 4x teljesítménynövekedést okozott ez a 2 karakter.

select
dateadd(second, @dtModifier, BDT) BDT,
cast(O as real) O, 
cast(H as real) H, 
cast(L as real) L,
cast(C as real) C,
V
from dbo.Bar b with (nolock)
where b.TickerID = @TickerID
and b.BDT >= @StartDate
and I = @I
order by b.BDT

Intra query deadlock

Tuesday, June 16th, 2015

Ma láttam egy újfajta deadlockot, amiben az SQL Server által párhuzamosan végrehajtott lekérdezés szálai akadtak össze. Azaz ugyanaz a PID akadt össze magával.

Először arra gondoltam, lefogom 1 szálra a lekérdezést maxdoppal, de szerencsére nem volt jól optimalizálva, így jól fel lehetett gyorsítani. 240000 lapolvasásról 4-re. :) Így már magától soros lett a plan, volt deadlock, nincs deadlock.

Bob is írt már erről.

Exam 70-459: https://www.microsoft.com/learning/en-in/exam-70-459.aspx done

Friday, May 22nd, 2015

Na, ez is megvan. :)

Ez is upgrade, két vizsgát tartalmazott, és ebben már volt 2014-es tartalom. Két témakört érintettek, az egyik az In-Memory DB (mi más?), a másik a delayed durability. Ja, és a clustered columnstore index is előjött egy kérdésben.

Szokás szerint volt pár nem jól definiált kérdés, ezeknél már kommenteztem, mert utálom, hogy ennyire nem nézetik át a kérdéseket.

Az in-memory-snál elég sokat elidőztem, mivel belementek, hogy milyen izolációs szintek vannak benne, és melyik alkalmas az adott feladatra, amit a scenarióban leírtak. A scenariók itt is km hosszúak, kicsit be is tojtam az elején, hogy nem lesz elég időm. De kiderült, hogy sok nem scenarió alapú kérdés is volt, azokat gyorsan meg lehet válaszolni.

Csak az inmemory-s rész a maga kb. 5 kérdésével elvitt vagy 20 percet, de a többi már emészthetőbb volt.

Idén már csak két tervem van, az MVC vizsga és a BI-os SQL rész. Az MVC-t valszeg letolom jövő héten még, amíg tart a second shot, ki tudja, hátha egyszer megbukom, nyugalmat ad, hogy nem dobok ki 75EUR-t az ablakon.

Exam 70-457 Transition Your MCTS on SQL Server 2008 to MCSA: SQL Server 2012 done :)

Friday, May 15th, 2015

Ma volt egy szabad napom, gyorsan lenyomtam ezt is.

A vizsga a szokásos volt, 0,8-e normál, rendes kérdések, 20% nehezen megragadható marhaság. 50 kérdés volt, ezzel le van tudva a 70-461 és a 70-462. 700 ponttól lehet átmenni, nekem 833 volt a 461-es rész, 900 a 462-es. Látszik, hogy developer agyam van. :)

Ha jól emlékszem nem volt benne egyetlen 2014-es kérdés sem, ez számomra szomorú. Valójában én a 2014 vizsgákat akartam letenni, de nincsenek most ilyenek. Gondolom majd a 2016-osnál lesznek új vizsgák, nem akarják lejáratni két évente a certeket.

Ami érdekes volt, eléggé rámentek a Windowing Functionökre, erre érdemes mindenkinek gyúrni (egyébként is hasznosak). De csak a 2005-ös szinten kérdezték, nem mentek bele a 2012-es újdonságokba (framing), pedig itt vannak az igazi csemegék.

A következő lukas napon jön a 459, abban már tényleg várhatóak 2014-es kérdések, majd megírom, mire kíváncsiak.

SQL teljesítményoptimalizálás – imádom

Thursday, March 12th, 2015

Az elmúlt 3 hétben egy fejlesztési projektből kicsípve 3 napot három cégnél is SQL Server teljesítményoptimalizáltam (és még vannak cégek a queue-ban, ilyen erős évindulásom még soha nem volt :). Azt kell mondjam, ez a legkedvesebb munkám mindenek felett. 1 nap alatt általában igen látványos eredményeket lehet elérni, eddig még soha nem csalódtak bennem ügyfelek.

Az áprilisom és a májusom teljesen tele van már, de ha valakinek hasonlóra van igénye, jelezze nekem, ha becsúszik valahol egy luk, egy napra lehet el tudok ugrani. Júniustól egyelőre még laza a naptáram, oda könnyebb tervezni.

A teljes üzleti transzparencia jegyében, és hogy könnyű legyen kalkulálni leírom az óradíjamat: 20000 Ft + ÁFA / óra. Azaz egy nap igen intenzív (lóg a nyelvem a nap végére tényleg, nagyon intenzív gondolkodást igényel a munka) optimalizálás 160e + ÁFA. Valaki erre biztos azt mondja, ez sok, megértem. Valaki viszont, aki már találkozott 30-40 ezres konzulenssel, annak ez olcsónak számít, tudva, hogy nem szoktam az ügyfeleknél húzni az időt, hanem nyomom a munkát nagy erővel. Egy nap alatt ki lehet végezni legalább 5-15 top lekérdezést, amitől nagyon meg szoktak könnyebbülni a szerverek. Azaz ennyi pénzből garantáltan jelentősen fel fog gyorsulni a szerver. Nyilván csodát nem tudok tenni, ha egy alkalmazás több ezer apró lekérdezéssel old megy egy feladatot, akkor a network roundtrip idején nem tudok segíteni, hiába gyorsítok fel egy lekérdezést pár msra. Vagy ha össze kell szummázni egymilliárd sort, az nehéz gyorsan megcsinálni. Azaz architekurális problémákon nem tud segíteni a db oldali hangolás.

A másik, amire fel kell készülni, hogy néha módosítani kell a hívó kódokon is, illetve időnként módosítani kell az adatbázis szerkezetét is. Én demonstrálom, mivel jár, ha ezt megteszi az ügyfél, aztán a döntés az övé, meglépi-e? Mivel ezeket nem lehet azonnal meglépni, amikor ott vagyok, gyakori, hogy az éles rendszerbe bevezetett módosításokat még egyszer át kell néznem. Ezt már legtöbbször TeamViewerrel vagy RDP-vel szoktam itthonról megtenni, mivel ez 1-2 óránál már nem visz el többet, nem éri meg ennél többet utazni miatta.

Hogy hatékony legyen a munka annyit szoktam kérni, hogy legyen jogom monitorozni az éles szervert, és legyen egy tesztszerver, ami egy restorolt adatbázist vagy adatbázisokat tartalmaz az éles rendszerről.
Ezen dokumentáltam be tudom mutatni, hogy ha az éles szerveren végrehajtják azokat a változtatásokat, amiket javaslok, akkor mennyivel lesz gyorsabb a rendszerük.

A munka része még, amikor átbeszéljük a fejlesztők vagy üzemeltetők fejében felmerült kérdéseket.

Az optimalizálási munkának egy hátránya van: mivel mindig az ügyféllel együtt végzem a munkát, és közben részletesen elmondom, mit és miért csinálok, általában már nem hívnak legközelebb, mivel kitanulják, mit kell tenni a lassulás esetén. :)

Ps. jövő héten lejár a 25%-os, 150e-es TDD tanfolyam akció, utána már csak ősszel fogok indítani tanfolyamot, mivel kilátásban van egy hosszabb projektem, így nem lesz rá időm. Aki akar, most szálljon fel a vonatra.

FORCESEEK hint – szeretem

Thursday, March 5th, 2015

Mindig imádom, ha valami geekséget be tudok vetni a gyakorlatban. SQL Server 2012-ben jött be az az újdonság, hogy a FORCESEEK hintnek meg lehet adni egy index oszlopait is.

A tanfolyamok kedvéért lehet találni példákat, aminek segít egy hint, de végre találtam valamit, ami élőben is demonstrálja a dolgot.

Az alábbi lekérdezés 1 perces tőzsdei adatokban keres adathiányt. Az AllTH összeszedi minden napra a nyitvatartási időket.

;with AllTH as
(
	select 
	D, 
	dateadd(DAY, 1, D) D1,
	cast(DATEADD(HOUR, DATEPART(HOUR, OpeningTime), DATEADD(MINUTE, DATEPART(MINUTE, OpeningTime), cast(D as datetime2))) as datetime2) ExpectedOpeningTimeAsDate, 
	cast(DATEADD(HOUR, DATEPART(HOUR, ClosingTime), DATEADD(MINUTE, DATEPART(MINUTE, ClosingTime), cast(D as datetime2))) as datetime2) ExpectedClosingTimeAsDate, 
	OpeningTime, ClosingTime from TradingHoursFlat
	where TickerId = @thTickerId
	and D >= (select min(TradeTime) from Tick where TickerID = @tickerId and tradetime > dateadd(day, -180, getdate()))
	and D < dateadd(day, -1, GETDATE())
),
FilteredBars as
(
	select cast(TradeTime as datetime) TradeTime,
	t.D, t.OpeningTime, t.ClosingTime
	from AllTH t
	join Tick b
	with(forceseek (IX_Natural_Key (TickerId, TradeTime)))
	--on t.D = cast(TradeTime as date) 
	on TradeTime between t.D and t.D1
	--on TradeTime between t.D and t.D+1
	where b.TickerID = @tickerId
	--and cast(TradeTime as time) between t.OpeningTime and t.ClosingTime
	and tradetime between t.ExpectedOpeningTimeAsDate and t.ExpectedClosingTimeAsDate
),
T as
(
	select a.D, min(TradeTime) ActualOpeningTime, max(TradeTime) ActualClosingTime from FilteredBars b 
	right join AllTH a
	on a.D = cast(TradeTime as date)
	group by a.D
), U as
(
	select a.D, ExpectedOpeningTimeAsDate, ActualOpeningTime, ExpectedClosingTimeAsDate, ActualClosingTime,
	DATEDIFF(MINUTE, ExpectedOpeningTimeAsDate, ActualOpeningTime) OpeningGap,
	DATEDIFF(MINUTE, ActualClosingTime, ExpectedClosingTimeAsDate) ClosingGap
	from T
	right join AllTH a
	on a.D = cast(t.ActualOpeningTime as date)
)
,V as
(
	select * from U
	where OpeningGap > @tolerance or ClosingGap > @tolerance
	or ActualOpeningTime is null or ActualClosingTime is null
)
select 
ROW_NUMBER() over(order by D) Id,
ExpectedOpeningTimeAsDate ExpectedOpeningTime,
ActualOpeningTime,
ExpectedClosingTimeAsDate ExpectedClosingTime,
ActualClosingTime,
case when ActualOpeningTime is null then 'MissingDay' else 'MissingIntradayData' end GapKind 
from V
order by D

A hivatkozott Tick táblában ebben a pillanatban ennyi sor van: 61646572157. Nem írtam el, ez 61 milliárd sor!

Itt van az SQL 2012-es hint barátunk:
join Tick b with(forceseek (IX_Natural_Key (TickerId, TradeTime)))

Furcsa módon hint nélkül a becsült plan 60x olcsóbb, de mégis, a hinttel rákényszerített (számomra, aki ismeri az adatokat logikus) plan sokkal gyorsabban fut le.
Ha nem írom ki az oszlopokat, vagy csak foreceseek vagy forcessek + index név nem veszi rá, hogy az én tervemet valósítsa meg.

Ezzel nem azt sugallom, hogy hinteljetek mint az állat, csak azt, hogy egyes határesetekben jól jöhet, ha tudunk róla.

Egy tipp még. Vigyázni kell, hogy ne keverjük a datetime2 különböző hosszúságú változatait, mert ezeket implicit konvertálni fogja a szerver, megint elesve a seekektől.

Hosszú műveletek állapotának megfigyelése SQL Serveren

Monday, December 22nd, 2014

Már fél napja fut egy alter index reorganize, honnan tudhatom, mennyi van még hátra?

Innen.

Az infó a sys.dm_exec_requestsben van. Ha még csak 0%-on áll a művelet, akkor nem jön vissza sor, pár perc kell ezeknek a lassú parancsoknak, hogy beinduljanak.

AlwaysOn ReadOnly replika olvasás

Monday, July 28th, 2014

Hangyál Zoli hívta fel a figyelmem egy finomságra. AlwaysOn, ReadOnly replika, szinkron kapcsolat. Egy adott módosítás hatása látszik-e azonnal a replikán, ha a primary-n a tranzakció commitról kaptunk visszajelzést?

SQL Server Change Tracking

Monday, May 19th, 2014

Ezt a cikket a technetben lehetett olvasni régebben, de kezdenek kiesni a régebbi tartalmak, azért bemásoltam ide a blogba is. Mivel tele van képekkel és táblázattal, inkább átkonvertáltam pdf-re, és nem htmlesítettem.

A teljes cikk itt olvasható:
SQL Server 2008 Change Tracking

Snapshot isolation level

Friday, May 2nd, 2014

Egy korábbi, 2008-ban írt cikkemet emeltem be ide, mivel láttam, hogy többen meg akarták nézni nálam, de nem sikerült, mert a technetre mutató küldő link már nem élt.

A snapshot szint az In-Memory tábláknál is fontos, így érdemes elolvasni a cikket, mivel a következő 2014-es cikk a párhuzamosságról fog szólni, ahol már ismertnek tételezem a snapshot ismeretét.

SQL Server magas CPU használat nyomozás és megoldás

Wednesday, March 19th, 2014

Az utóbbi hetekben szinte minden nap SQL Servert hegesztek valahol. Leírom az összes eset tanulságát, lássuk a legutóbbit.
Adott egy igen magas forgalmú SQL Server, 1000 batch/sec körüli a terhelése. A lekérdezések derekasan optimalizálva voltak a megrendelő által, másképp nem is bírta volna produkálni a szerver ezt az áteresztő képességet (de gáz ez a szó, nem egy intim betétről írok).
A lassú lekérdezések listájában volt egy gyanúsan egyszerű, de rengetegszer meghívott lekérdezés. Ez nem volt indokolt, ezért ezt most már csak egyszer hívják meg a kliensben, aztán eltárolják az eredményét. Ezt jó észben tartani, hiába 2 lapolvasásos, seekes egy kveri, ha nagyon sokszor hívják meg, le tudja ez is fogni a szervert.
Tovább nézve látszott, hogy egy olyan trigger belseje vitte el az időt, amit még én javasoltam nekik 1-2 éve. :) A trigger feladata eladásoknál módosítani a készletet, így nem kell szummákkal kiszámítani listázáskor az aktuális készletet. Denormalizált adatokat frissített. Ezzel azt lehet majd tenni, hogy a forrástábla nem minden oszlopa módosítja a készletet, így egy IF UPDATE(…)-tel le lehet majd csökkenteni a számítások számát.
A következő pont a recompile-ésönök vizsgálata volt. A legtöbb lekérdezés ad-hoc queryként ment be, a procedure cache kihasználása gyenge volt, 60% körüli. Mivel a hibajelenség a nagy CPU terhelés volt, gyanús volt, hogy a sok fordítás-tervgenerálás eszi a procikat (8 mag, 16 logikai proc 90%-on).
A lekérdezéseket megvizsgálva kiderült a gyenge cache reuse oka: a batch-ek elejére kommentek voltak írva, amelyek kliensenként egyediek voltak. Így ugyanaz az ad-hoc query sok százféle verzióban is bement a szerverre, ami így nem tudta hatékonyan cachelni.
A kommenteket kivéve a plan reuse felment 90% fölé. :)
A harmadik dolog még egy update statistics with fullscan volt. A statisztikák rendkívül fontosak az SQL Server működéséhez, aki nem hallott még róla olvasson utána, megéri. Normál esetben, nagy tábláknál csak mintavételezve frissíti a statisztikákat a szerver, érthető módon spórolva az erőforrásokkal. Viszont egyenetlen eloszlású oszlopok esetén ez rossz hisztogramokat eredményezhet, ami miatt rossz tervet fog generálni a szerver. Ezen segíthet, ha időnként futtatunk full scanes update statisticset is. Például hétvégenként. Erre van egy nagyszerű kis sql csomag, hamarosan írok róla.

Zárásként tanulságul, az SQL kommentek messze nem olyan kis ártatlan teremtések, mint gondolnánk.

Update: az elfelejtettem leírni, hogy az eredeti 90%-ról lement 50%-ra a procihasználat.

SQL Server mirroring monitorozása

Thursday, August 30th, 2012

Ha a mirroringgal gond van, arról fontos azonnal tudni. Két esetet szoktunk figyelni. Az egyik a state change, azaz ha valamiért átvándorol a kiszolgálás az egyik lábról a másikra, illetve, ha feltorlódnak a tranzakciók valamelyik oldalon.

A monitorozásra vannak beépített dolgok, ezekről itt lehet bővebben olvasni, itt szinte minden infó megvan az alertezés felépítéséhez.
Két problémás pontba akadtunk bele. Az előbbi cikkben a WMI események hatására csak számok jöttek vissza státuszként, ezek nem túl beszédesek egy emailben. Ezeket elég könnyű visszafejteni stringekké:

declare @msg nvarchar(4000) = 'State of $(ESCAPE_SQUOTE(WMI(DatabaseName))) database changed to ''';
declare @state nvarchar(50) = '$(ESCAPE_SQUOTE(WMI(State)))';
declare @newStateString nvarchar(100) =
case @state
when '2' then 'Synchronized Principal without Witness'
when '7' then 'Manual Failover'
...
end;

set @msg += @newStateString + ''' on $(ESCAPE_SQUOTE(WMI(StartTime)))';

EXEC msdb..sp_send_dbmail @profile_name='SQL Server 2008 Mirroring Notifications',
@recipients='foo@bar.com',
@subject= 'DB Mirroring Alert',
@body=@msg;

Lehetne elegánsabban is lookup táblával, de a célnak ez megfelelt.

A másik kérdés macarásabb. Egy job percenként mintavételezi a mirror queue-jainak a mértét, és ezeket beírja egy rendszertáblába. Ha valamelyik túlmegy egy megadott határértéken, akkor ez beírja az event logba, onnan egy alert ki tudja olvasni, és emailt küldeni róla. Csak éppen a lényeg nincs ebben benne, melyik adatbázissal van a baj. Ez bug, ez van.
Hogy valami képünk mégis legyen már email alapján mi a gond, az alábbi jobot futtatjuk le a threshold alert alapján:

EXEC msdb..sp_send_dbmail @profile_name='SQL Server 2008 Mirroring Notifications',
@recipients='foo@bar.com',
@subject= 'DB Mirroring Alert',
@execute_query_database = 'msdb',
@query = '
select * from (select distinct(database_id) id from dbo.dbm_monitor_data) d
cross apply
(select top 5 cast(DB_NAME(database_id) as nvarchar(20)) db, local_time, redo_queue_size, send_queue_size
from dbo.dbm_monitor_data
where ((send_queue_size > 0 or redo_queue_size > 0) and local_time > GETDATE() - 3  and database_id = d.id)) t
order by db, local_time desc'

Ez az utóbbi 3 nap torlódásait küldi el emailben, ebből már látszik, melyik db akadt el.

Megoldás a deadlockos cikkhez

Friday, July 20th, 2012

Heuréka! :)

Előzmény.

Az előzményben látható első deadlock lenyomata xmlben így néz ki:

<deadlock-list>
	<deadlock victim="process3c3dc8">
		<process-list>
			<process id="process3c3dc8"
					 taskpriority="0"
					 logused="236"
					 waitresource="KEY: 6:72057594959101952 (8194443284a0)"
					 waittime="8196"
					 ownerId="724347"
					 transactionname="user_transaction"
					 lasttranstarted="2012-07-17T13:30:06.643"
					 XDES="0x9dec3b0"
					 lockMode="U"
					 schedulerid="3"
					 kpid="2744"
					 status="suspended"
					 spid="85"
					 sbid="0"
					 ecid="0"
					 priority="0"
					 trancount="1"
					 lastbatchstarted="2012-07-17T13:30:06.643"
					 lastbatchcompleted="2012-07-17T13:30:06.643"
					 clientapp=".Net SqlClient Data Provider"
					 hostname="AlmaDEVWEB02"
					 hostpid="1312"
					 loginname="BuildAgentUser"
					 isolationlevel="repeatable read (3)"
					 xactid="724347"
					 currentdb="6"
					 lockTimeout="4294967295"
					 clientoption1="673316896"
					 clientoption2="128056">
				<executionStack>
					<frame procname="AlmaDev.dbo.spExecuteTransaction"
						   line="136"
						   stmtstart="7378"
						   stmtend="7554"
						   sqlhandle="0x0300060069e52875f254de0091a000000100000000000000">
						SELECT @toBalance = Balance FROM Account WITH (UPDLOCK) WHERE Id = @toAccountID
					</frame>
				</executionStack>
				<inputbuf>
					Proc [Database Id = 6 Object Id = 1965614441]
				</inputbuf>
			</process>
			<process id="processbe41288"
					 taskpriority="0"
					 logused="236"
					 waitresource="KEY: 6:72057594959101952 (8194443284a0)"
					 waittime="2505"
					 ownerId="724314"
					 transactionname="user_transaction"
					 lasttranstarted="2012-07-17T13:30:06.610"
					 XDES="0x9af4e80"
					 lockMode="X"
					 schedulerid="2"
					 kpid="4716"
					 status="suspended"
					 spid="108"
					 sbid="0"
					 ecid="0"
					 priority="0"
					 trancount="2"
					 lastbatchstarted="2012-07-17T13:30:06.610"
					 lastbatchcompleted="2012-07-17T13:30:06.610"
					 clientapp=".Net SqlClient Data Provider"
					 hostname="AlmaDEVWEB02"
					 hostpid="1312"
					 loginname="BuildAgentUser"
					 isolationlevel="repeatable read (3)"
					 xactid="724314"
					 currentdb="6"
					 lockTimeout="4294967295"
					 clientoption1="673316896"
					 clientoption2="128056">
				<executionStack>
					<frame procname="AlmaDev.dbo.spExecuteTransaction"
						   line="138"
						   stmtstart="7556"
						   stmtend="7830"
						   sqlhandle="0x0300060069e52875f254de0091a000000100000000000000">
						UPDATE Account SET
						Balance += (@amount - @commissionAmount)
						WHERE Id = @toAccountID
					</frame>
				</executionStack>
				<inputbuf>
					Proc [Database Id = 6 Object Id = 1965614441]
				</inputbuf>
			</process>
		</process-list>
		<resource-list>
			<keylock hobtid="72057594959101952"
					 dbid="6"
					 objectname="AlmaDev.dbo.Account"
					 indexname="PK_Account"
					 id="lockc99fc00"
					 mode="U"
					 associatedObjectId="72057594959101952">
				<owner-list>
					<owner id="processbe41288"
						   mode="U"/>
				</owner-list>
				<waiter-list>
					<waiter id="process3c3dc8"
							mode="U"
							requestType="convert"/>
				</waiter-list>
			</keylock>
			<keylock hobtid="72057594959101952"
					 dbid="6"
					 objectname="AlmaDev.dbo.Account"
					 indexname="PK_Account"
					 id="lockc99fc00"
					 mode="U"
					 associatedObjectId="72057594959101952">
				<owner-list>
					<owner id="process3c3dc8"
						   mode="S"/>
				</owner-list>
				<waiter-list>
					<waiter id="processbe41288"
							mode="X"
							requestType="convert"/>
				</waiter-list>
			</keylock>
		</resource-list>
	</deadlock>
</deadlock-list>

Figyeljük meg, hogy mindkét processz a “KEY: 6:72057594959101952 (8194443284a0)” kulcson várakozik, ezen vesztek össze.

Viszont az xmlben nem az van, mint a képen!!!

Az xml vége felé látszik ki-mit ownol. owner-list/owner elemek:

		<resource-list>
				<owner-list>
					<owner id="processbe41288" mode="U"/>
				</owner-list>
				<waiter-list>
					<waiter id="process3c3dc8" mode="U" requestType="convert"/>
				</waiter-list>
			</keylock>
				<owner-list>
					<owner id="process3c3dc8" mode="S"/> <!-- Ez nem U !!! -->
				</owner-list>
				<waiter-list>
					<waiter id="processbe41288" mode="X" requestType="convert"/>
				</waiter-list>
			</keylock>
		</resource-list>

A második processz nem U lockot tart az erőforráson, hanem S-t, Shared lockot! De miért U látszik a képen? A bugos kutya életbe!
Így már kristálytiszta a dolog. Azt nem értettem, hogy tudott kiadni az SQL Server két U lockot ugyanarra az erőforrásra. Sehogy, az elmélet jó. Csak az SSMS bugos.

Tehet, mit látunk itt tképpen? Az egyik processz tart U lockot a közös erőforrásra (owner id=”processbe41288″ mode=”U”), a másik pedig egy Shared lockot (owner id=”process3c3dc8″ mode=”S”). Miért shared az egyik? Mivel valamelyik korábbi sorban (ez nem látszik az xmlből, már lefutott) lemaradt a with(updlock) a select utáni tábla mögül.
Ez az alapállás. Aztán az egyik Update lockra akar konvertálni (waiter id=”process3c3dc8″ mode=”U” requestType=”convert”, ez nem teljesen világos miért, mivel eleve az van neki. A másik Sharedről eXclusive-ra akar konvertálni. Ez már a nem megy, mivel az X nem kompatibilis az U-val, így az nem adható ki, a második processznek várnia kell, de mivel az első által kért U sem a U-val, így összeakadnak.
Míg most sem teljesen tiszta minden, az XML-ben sem látni az összes részletet, pl. miért van az U-U konverzió, de a megoldás viszonylag 100%-ra a következő: át kell nézni a beteg tárolt eljárást, és meg kell keresni benne olyan select-et, amely az Account táblát nem updlockkal érik el. Ezekre fel kell rakni az updlockot, és valószínűleg kiesik a deadlock.
Tanulság: nem elég a deadlock graph grafikus nézetét megnézni, bele kell kukkantani az xml-be is, ha valami nem logikus.
Ezt tudva már más is találkozott ezzel: http://rusanu.com/2010/05/12/the-puzzle-of-u-locks-in-deadlock-graphs/
Szóval még nem teljesen tiszta minden, de az IT már csak ilyen, félinformációk alapján is tudni kell hibát keresni.

Ártatlan kis 1 lapos lekérdezések

Friday, July 20th, 2012

Csalóka az adatbázis. Sokan félreértik, mire való, és a 90-es évek stílusában minden vackot rábíznak, ettől persze megfekszik szegény. A félreértés onnan ered, hogy az MS akkor azt mondta, spben kell megírni az üzleti logikát (mivel a VB használhatatlan volt, a C++ meg a legtöbb ember kezében csak bug gyártásra jó). A Delphi ezért élte a fénykorát.
Na szóval, amikor szegény dbre sok üzleti logikát bíznak, akkor mivel őbenne nem lehet a hagyományos módon cache-elni mint egy appszerveren, így az összes piszlicsáré, a büdös életben nem változik de legyen a dbben mert az jó adatot is táblákból kell kiolvasni. Pl. az enumok értékét, amit azért nem szoktunk megváltoztatni a dbben, mert ehhez úgyis a programozott enum értékeket is át kéne írni, ráadásul a kettőt egyidőben szinkronban.
Így aztán a tízmilliós vasú SQL Server olyan dolgokon dolgozik, ami nem az ő feladata. 1 lapolvasás nem a világ, és különben is memóriában vannak a pici táblák, nem jelent ez nagy terhelést.
Valódi cacheből, appszerveren felolvasni egy pl. egy stringet pár nanonsecundum.

Nézzük meg ezt SQL Serveren belülről:

declare @i int = 1000000, @a varchar(200)

while (@i > 0)
begin
	select @a = s.Value from Setting s where s.Name = 'TestSetting1';
	set @i-=1;
end

A Setting tábla két soros, és rendes voltam, raktam clustered indexet a Name oszlopra. 15mp-ig tart a ciklus, a select nélkül kevesebb, mint 1 mp (kb. 800 millisec).
Azaz a select általi kiolvasás kb. 15us, míg ugyanez az appszerveren nanosec nagyságrendű. Nem mindegy.
Ha már ennyire szerelmesek vagyunk az adatbázisba, akkor még azt meg lehet tenni, hogy paraméterként adjuk át neki ezeket a well-known beállításokat.

Pl.

create proc Teszt1
as
declare @a varchar(200)
select @a = s.Value from Setting s where s.Name = 'TestSetting1';
go

create proc Teszt2
@par varchar(200)
as
declare @a varchar(200) = @par
go

declare @i int = 1000000, @a varchar(200)
while (@i > 0)
begin
	exec Teszt1;
	set @i-=1;
end
go

declare @i int = 1000000, @a varchar(200)
select @a = s.Value from Setting s where s.Name = 'TestSetting1';
while (@i > 0)
begin
	exec Teszt2 @a;
	set @i-=1;
end

28 vs 12 mp, látszólag nem nagy deal, de egy terhelt szervernél minden gramm számít.

Ps. sok cégnél láttam ilyeneket, senki ne vegye magára.

Furcsa deadlockok

Thursday, July 19th, 2012

Mostanában valahogy utolérnek a code review jellegű munkák, amiket egyébként nagyon szeretek, mivel ezek általában intenzív, agyalós nyomozások szoktak lenni, amit imádok.
Az előzőben deadlockok jelentek meg egy SQL Server alapú rendszerben, nagyobb terhelés esetén. A deadlockokat általában elég egyszerű megfogni, az SQL Server Profiler Deadlock graph tálcán kínálja az összeakadt folyamatokat és erőforrásokat. A legtöbb deadlock vagy conversion deadlock, amelyek repeatable read izolációs szinten végrehajtott select-update vált ki, vagy a más sorrendben elért táblák miatt előálló deadlock.
Esetünkben a selectek ki voltak egészítve with(updlock)-kal, amely pont a konverziós deadlockot hivatott megakadályozni (nem kell a shared lockot átkonvertáltni update lockká, mivel már a select is update lockot használ).
No, de ami most más volt mint szokott, hogy általam eddig nem látott volt a deadlock graph. Megörökítettük az utókornak, íme (shift clickkel kell megnyitni, így külön ablakban nyílik meg, így össze lehet nézni a szöveggel):

Mi a furcsa benne? Középen van a két erőforrás, amely esetünkben ugyanaz a tábla. Ez még ok, de a HoBtId is ugyanaz, azaz ugyanarról a sorról van szó.

Update: ez marhaság. a HobtID csak azt mondja meg, hogy azonos allokációs egység, heap vagy index (btree). De az, hogy ugyanarra a sorra várakoznak igaz, a waitresource=”KEY: 6:72057594959101952 (8194443284a0)” attributumból látszik, de ez nem látszik a grafikus nézeten.

Még ez is rendben van. Nézzük meg, milyen lockok vannak kiosztva! A bal és a jobb oldali processz is update lockot tart az erőforrásokon, amelyek a korábbiak alapján úgy néz ki egy erőforrást reprezentálnak. Na, itt van a bibi. Két update lock nem kompatibilis, pont erre találták ki őket, hogy ne szeressék egymást. Akkor hogy tud kiosztani a szerver mégis ilyeneket? Igazából ez a talány, utána már természetes, nem tudnak fogni még egy update és egy exclusive lockot a közös sorokra. Mi a túró történik itt?

Nekem van egy bűnbakom, aki ezt a furcsaságot csinálja, de csak pár nap múlva írom le, amikor már többet tudok róla, hogy a módosítások után megjavult-e a rendszer.

Aki látott már ilyet, kérem ossza meg velünk.

Zárásul csemegének itt van egy nagyobb gráfocska, amikor többen is összeakadnak:

SQL prospektus kiküldve

Monday, June 4th, 2012

Feladtam postára az eddig beérkezett igényeket, aki szerencsés, már holnap megkapja, aki nem, az soha, ismerve a minőségéről híres postát. Ha valaki nem kapja meg két héten belül jelezze, elküldöm még egyszer.

Jó olvasgatást. :)

SQL Server 2012 újdonságok – 6. Spatial fejlesztések

Wednesday, May 30th, 2012

Geometriai adatok tárolása és kezelése az SQL Server 2008-ban jelent meg. A különböző alakzatok adatait nem csak tárolni képes a szerver, hanem sokféle műveletet végezni rajtuk, és képes indexelni a síkbeli és a föld felületén értelmezett görbült adatokat, így például két alakzat közötti távolságkeresést nagyon gyorsan tudja végrehajtani.
Az SQL Server 2012-ben a következő főbb fejlesztések történtek (nem teljes lista):

  • A földgömb felszínén értelmezett geography típus metódusai most már mindent tudnak, amit a geometry típus (eddig egyes metódusok nem voltak rajta implementálva).
  • Az a korlátozás, hogy egy alakzat nem nyúlhat át a másik félgömbre megszűnt
  • Új alakzatok jelentek meg (CircularString, CompoundCurve, CurvePolygon). Ezen íves alakzatok járulékos előnye, hogy a Buffer metódus mellett, amely képes körbevenni egy alakzatot egy másik (elnagyoltabb és távolabbi) alakzattal, megjelent a BufferWithCurves, amely nem csak egyenes vonalakkal dolgozik, így sokkal kevesebb alakzatból létre tudja hozni a körvonalat.
  • Részletes hibaüzenetek, ha nem valid egy alakzat (nem zárt, stb.).
  • Sok új metódus, többek között:
    • ShortestLineTo: legrövidebb út két alakzat között egyenesként visszaadva.
    • Új aggregáló függvények: UnionAggregate, EnvelopeAggregate, CollectionAggregate, ConvexHullAggregate. Ezek igen hasznosak, amikor sok kis darabból nagyobb alakzatokat összerakni.
  • A belső számítások eddig 27, most már 48 bites pontossággal dolgoznak.
  • Auto grid. Az alakzatok indexeléséhez a síkot felosztják egyre kisebb négyzetekre, és rögzítik, hogy az alakzat mely négyezeteket fedi le. Eddig nekünk kellett megadni a maximum 4 szint felbontását (hányszor hányas négyzetrácsra fektessék rá az alakzatot). Most már automatikusan képes dönteni a szerver a felbontásról, ráadásul olyan jól, hogy az indexelt keresések általában jelentősen gyorsabbak lesznek, mint a korábbi verzióban.
create spatial index idxgeog
on table(column) using GEOGRAPHY_AUTO_GRID;
  • Tömöríthető spatial indexek: kb. felére nyomhatók össze a spatial indexek, 5-10% CPU veszteség árán.
  • A leggyakoribb feladatra, amikor alakzatok között kell legkisebb távolságot számolni (pl. útvonaltervezésnél) 10-30-szoros gyorsulás érhető el az SQL 2008-hoz képest, ha a lekérdezésünket úgy írjuk meg, hogy az optimizer az ún. Nearest Neighbor Query Plan-t tudja használni.
  • sp_help_spatial_geography_histogram és sp_help_spatial_geometry_histogram tárolt eljárások az adatok eloszlását mutatja meg (az SQL Server Management Studio Spatial fülén vizuálisan is).

Íme egy Nearest Neighbor Query Plan-t használó lekérdezés:

DECLARE @g geography = 'POINT(18.919358	47.460025)';	--Budaörs
SELECT TOP(10) geom.ToString(), 
geom.STBuffer(5), 
Telepules, Utca, Hazszam1
FROM [HungarySpatial].[dbo].[Cim] 
--with(index (idx_Cim_Spatial_1))
with(index (idx_Cim_Spatial_Uj))
WHERE geom.STDistance(@g) IS NOT NULL
ORDER BY geom.STDistance(@g);

--Index nélkül: 280mp
--2008-as index-szel 13mp
--2012-es index-szel 0.1mp!

--Régi index
CREATE SPATIAL INDEX idx_Cim_Spatial_1 ON Cim (geom)
USING  GEOGRAPHY_GRID 
WITH (GRIDS =(LEVEL_1 = MEDIUM, LEVEL_2 = MEDIUM, LEVEL_3 = MEDIUM, LEVEL_4 = MEDIUM), 
CELLS_PER_OBJECT = 16)

--Új, automatikus felbontású spatial index
SET ARITHABORT ON
SET CONCAT_NULL_YIELDS_NULL ON
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
SET NUMERIC_ROUNDABORT OFF

CREATE SPATIAL INDEX idx_Cim_Spatial_Uj
ON Cim (geom) 
USING GEOGRAPHY_AUTO_GRID;

A spatial támogatás egy gyakran mellőzött része az SQL Servernek, pedig még az ingyenes Express is támogatja. Az Entity Framework kliensoldalról könnyen elérhetővé teszi a térbeli adatokat, így egyszerűen írhatunk izgalmas és hasznos alkalmazásokat, amelyek térbeli adatokkal dolgoznak (legközelebbi célállomás, térkép vizualizáció, stb.)

SQL Server 2012 újdonságok – 5. A THROW parancs

Tuesday, May 29th, 2012

Az SQL Server 2005-ben bevezetett TRY/CATCH hibakezelés óriási előrelépést jelentett az átlátható TSQL hibakezelés terén. Más nyelvekhez viszonyítva egy valami azonban hiányzott a képből, a THROW, amivel ki lehet váltani egy kivételt (exceptiont). Volt persze egy hasonló, a RAISERROR, ám ez kicsit más, mint a többi programnyelv throw-ja. A throw ugyanis általában kétféle dolgot tud. Az egyik, hogy feldob egy kivételt, a másik, hogy egy elkapott kivételt továbbdob. Ez utóbbi re-thrownak hívják, és azért nagyon hasznos, mert try-catch blokkban le tudjuk kezelni az eredeti hibát, pl. visszagörgetjük a tranzakciót, naplózunk vagy bármi egyéb akciót hajtunk végre, majd a catch ág végén újra feldobhatjuk az eredeti hibát. Így a kliens alkalmazás vagy SQL hívó pontosan megkapja az eredeti hiba részleteit, például a kivétel sora nem a catch ágba rakott (re)throw-ra mutat, hanem az eredeti hibát kiváltó SQL sorra.
A THROW tehát hasonló helyeken használatos, mint a RAISERROR, de van közöttük pár fontos különbség. A legfontosabbak: a THROW nem tud rendszer kivételeket generálni (SQL Server belső hibaüzeneteket), nem használja a sys.messages táblát (ebben vannak az előre definiált hibaüzenetek) és mindig 16-os severity levelű kivételeket dob.
Az alábbi példában egy update az, ami hibát dobhat, ha beleütközik egy idegen kulcsba. A kivételt elkapjuk, lelogoljuk, majd újradobjuk a kliens részére. A re-throw miatt a hibanaplózó logika nem szól bele a kliens által kapott hibarészletekbe.

BEGIN TRY
	--Ez hibázhat
	update HumanResources.Employee 
	set BusinessEntityID = -1 
	where BusinessEntityID = 2;
END TRY
BEGIN CATCH
	--Naplózzuk a hibát
	INSERT INTO 
	[dbo].[ErrorLog]
    ([ErrorTime]
    ,[UserName]
    ,[ErrorNumber]
    ,[ErrorSeverity]
    ,[ErrorState]
    ,[ErrorProcedure]
    ,[ErrorLine]
    ,[ErrorMessage])
	VALUES(SYSDATETIME(), 
	SUSER_NAME(),
	ERROR_NUMBER(),
	ERROR_SEVERITY(), 
	ERROR_STATE(), 
	ERROR_PROCEDURE(), 
	ERROR_LINE(), 
	ERROR_MESSAGE());
  
  --Újradobjuk a kivételt a kliens részére
  THROW;

END CATCH

A kliens ezt kapja:

Msg 547, Level 16, State 0, Line 3
The UPDATE statement conflicted with the REFERENCE constraint "FK_EmployeeDepartmentHistory_Employee_BusinessEntityID". The conflict occurred in database "AdventureWorks2012", table "HumanResources.EmployeeDepartmentHistory", column 'BusinessEntityID'.

Látható, hogy ez az eredeti hiba, a 3. sorra hivatkozik, nem a 29.-re, ahol valójában a THROW van. Az errorlog táblánkba ugyanez az információ kerül be:

Nyilván nem csak újradobásra jó a THROW, hanem olyan helyeken is, ahol eddig RAISERROR-ral dobtunk fel saját hibát.
Ez:

RAISERROR(N'Az alkalmazott nem törölhető, mert vannak hozzá kapcsolt élő feladatok', 16, 1);

így néz ki THROW-val:

THROW 50000, N'Az alkalmazott nem törölhető, mert vannak hozzá kapcsolt élő feladatok', 1;

A hívó mindkét esetben ezt kapja:

Msg 50000, Level 16, State 1, Line 1
Az alkalmazott nem törölhető, mert vannak hozzá kapcsolt élő feladatok

Összegezve, ha saját hibákat szeretnénk a kliensek felé közvetíteni, illetve, ha meg kell tartani az eredeti hibáról szóló információkat, akkor a THROW-t érdemes használni. Speciális esetekben, pl. nem 16-os szintű hibákhoz, vagy ha a sys.messagesben előre definiált hibaüzenetet kell feldobni, akkor továbbra is elérhető a RAISERROR.
Egyébként az a tény, hogy a THROW nem használja a sys.messages táblát igen fontos lépés, mert így könnyebb költöztetni az adatbázist másik szerverre, nem kell átvinni egyedi hibaüzeneteket a master táblák között. Ez kapcsolódik az itt nem tárgyalt, SQL 2012 újdonság Contained Databases újításhoz is.