Soci (Soczó Zsolt) szakmai blogja

2014.04.30.

SQL Server 2014 újdonságok – 4. In-Memory OLTP – range indexek működése

Filed under: Adatbázisok,SQL Server,SQL Server 2014,Szakmai élet — Soczó Zsolt @ 08:44

A korábbi részekből láttuk, hogy a memória táblák sorait nem IAM page-ek vagy clustered index tartja egyben, hanem egy index, amit a primary key mögé kell létrehozni. Ez lehet hash index is, de lehet egy nonclustered index is. Ezt memória tábláknál range indexeknek is szokták hívni, mivel ezzel lehet kisebb-nagyobb operátorokkal tartományokra is keresni, a hash index csak point lookupra jó.
Aki jól ismeri a nonclustered indexeket diszk tábláknál, annak azonnal beugorhat, hogy a nonclu index csak kevés sor esetén szokott hatékony lenni, sok sor esetén csak akkor, ha miden adat benne van az indexben. A fő időt a nonclustered index végéből a valódi adatokra átnavigálás viszi el, ezt hívák bookmark lookup vagy key lookupnak a planben.
Memória indexek esetén NINCS ez az overhead. Minden nclu index cover indexnek tekinthető, amint elértünk a levélszintre, azonnal “ott” van az adat. Mivel a sorok nincsenek lapokba szervezve, nincsenek page latch-ek sem.
Hogyan vannak szervezve a memória táblák nonclustered indexei?
Nyilván ez is egy fa, a normál B-Tree módosítása, Bw-Tree a neve. Aki mélységeiben akarja tanulmányozni, itt egy link az MS Research cikkhez.
A cél itt is a lock és latch mentes kialakítás volt. Az indexlapok nem fix méretűek, hanem változóak. Ha egyszer elkészült egy lap, az többé nem módosul. A hash indexeknél láttuk, hogy a lock free adatstruktúrák egyik implementációja, amikor nem módosítanak valamit, immutable az objektum, mert ahhoz szinkronizálni kellene a hozzáférést, hanem készítenek egy új példányt, majd az eredetit egy mozdulattal kicserélik az újra. Ezt az elvet használják a BwTreeben is.

BwTree

Az ábrában az a trükk, hogy az index lapok NEM egymás címét tartalmazzák, hanem a másik lap sorszámát a bal oldalon látható Page Mapping Table-ben. Azaz beraktak egy indirekciót a fa lapjainak összeláncolásába. Így már lehet azzal játszani, hogy sose módosítanak egy index lapot, hanem készítenek egy újat, és azt beláncolják a Page Mapping Table-be. Ez a már ismert CompareExchange felhasználásával.
A root lap láthatóan 3 irányba ágazik el. A 10 index kulcs alattiak a 3-as lapon lesznek, a 11-20 közöttiek a 2-esen, és a 21-28 közöttiek a 14-esen. Látható tehát, hogy minden index bejegyzés egy kulcs értéket és egy lapszámot tartalmaz, ami a következő szintre visz le.
A levélszintű lapok már közvetlenül a sorokra mutatnak. Ha egy kulcs értékhez több sor is tartozik, azok össze vannak láncolva a hash indexnél látható módon. (Diszk tábláknál a clustered indexeknél ha nem egyedi az index kulcs, akkor egy 32 bites számot generálnak mellé, hogy egyedivé váljon. Ott nincs ilyen láncolás, ezért kell ez.)

Közbenső szinten tehát úgy módosítanak egy index lapot, hogy lemásolják, módosítják, majd kicserélik a Page Mapping Table-ben a régit az új verzióra. Ez lock-free, de eléggé erőforrás igényes. Levélszinten ezt már nem lehet eljátszani, mert túl nagy lenne a költsége. Ott más trükköt használnak. Amikor módosul egy kulcsérték, akkor beraknak egy delta lapot az index lap elé. Ez a lap azt írja le, hogy az index lapon levő információt milyen módosítások értek. Az ilyen index lap picike, hisz csak egy kulcsot tartalmaz, a módosítás jellegét (insert vagy delete), illetve mutatót a “módosított” lapra.

Az alábbi ábrán egy insert majd időben utána egy delete lenyomata látszik.

BwTreeUpdate

Először beszúrtak egy 50-es index kulcsú sort. A Page Mapping Table a művelet előtt a fekete nyíl mentén az eredeti lapra mutatott. Az insert után beláncolják az index lap elé a pirossal jelzett delta rekordot. Így összerakható az infó, hogy az index lapon látható információkon kívül még van egy új sorunk.
Ha utána törlik a 48-as kulcsú sort, akkor újra beláncolnak egy delta rekordot, ezt mutatja a lila nyúl.
Mi a poén ebben? A delta lapokat van idő összerakni, nem kell kapkodni, lockolni. Amikor kész, rámutat a következő index lapra, ami vagy egy delta page, vagy egy igazi index lap, akkor már csak egy CompareExchange, és a Page Mapping Table máris az új lapunkra mutat, beláncoltuk a listába. Ha megelőztek, semmi gond, nekifutunk még egyszer. Nyilván ezért hívják ezt optimista megközelítésnek, mert arra számítunk, kevés ütközés lesz, így megéri nekik néha újragenerálni adatstruktúrákat, jobban, mint állandóan várni a másikra.

Az update-et egy insert majd egy delete-ként valósítanak meg, az két delta rekordot eredményez.

A sok delta rekord persze jól belassítaná a műveleteket, ezért azokat gondozni kell. Ha egy művelet eredményeképpen 16-nál hosszabbá válik a delta rekord lista, akkor a változások hatását az eredeti index lapra összepakolják egy új index lapra, majd kicserélik a régi lapot az újra. A régi lapokat elviszi a Garbage Collector (erről majd későbbi részben írok, a poros sarkokkal együtt).

Ha egy lap mérete eléri a 8k-t a sok insert miatt, akkor azt kettévágják (lehetne nagyobb, nincs 8ks lapméret mint a diszktábláknál, csak kiegyensúlyozatlan lenne a fa, ha túl sok sor lenne egy lapon). Ezt két atomi CompareExchange-dzsel valósítják meg.

Ha viszont a törlések miatt a lap mérete 800 byte alá esik, vagy már csak 1 sort tartalmaz, hozzácsapják a szomszédhoz a sorait, ezt is két atomi lépésben.

Összegezve, ha nagyon gyors point lookup kell, azaz egyedi vagy nagyon jól szóró kulcs mentén kell felolvasni sorokat, akkor egy jó bucket counttal megáldott hash index lehet az optimális megoldás, mivel a hash indexnek nincs ez a módosítási overheadje.

Ha viszont tartományra kell keresni, vagy nem nagyon egyediek az értékek, akkor a range index lesz hatékony.

A következő részben a párhuzamos módosítások szervezését nézzük meg (optimistic concurrency).

2014.04.28.

TDD tanfolyam last minute

Filed under: Felhívás,Szakmai élet — Soczó Zsolt @ 18:09

Kedves barátaim/olvasóim, még van 2 helyem a jövő heti tanfolyamra, aki még nem szánta el magát, most van az utolsó alkalom, jelentkezzen.
Legközelebb már csak ősszel fogok indítani új kurzust.

Köszi,
soci

SQL Server 2014 újdonságok – 3. In-Memory OLTP – hash indexek belső kezelése

Filed under: Adatbázisok,SQL Server,SQL Server 2014,Szakmai élet — Soczó Zsolt @ 10:20

Az előző rész indexére alapozva nézzünk meg két lekérdezést, hogyan tudja támogatni a hash index.
Először egy pontszerű lekérdezés, egyenlő operátorral:

select * from Person.Person_InMem
where ModifiedDate = '20120112'

Végrehajtási terv:
HashIndeSeek

Szépen használja az indexet, seekel.

Kisebb operátor esetén:

select * from Person.Person_InMem
where ModifiedDate < '20120112'

HashIndexScan

Kénytelen végignézni az összes sort, mivel a hash indexekkel nem lehet csak egyenlőségre keresni, ezért a scan operátor a tervben.

Az összetett hash indexek nem használhatók kevesebb oszlopra szűrésre, még akkor sem, ha az első oszlopokból válogatunk. Azaz egy FirstName, LastName összetett hash index csak egyszerre erre a két oszlopra képes seekelve szűrni, csak a FirstName-re nem, mivel a hash a két értékre együtt került kiszámítása, külön nem értelmezhető.

Hogyan tárolja a szerver a hash indexeket?

InMemoryTableRow

Az ábrából látszik, hogy minden sor tartalmaz n * 8 byte-os pointert, minden egyes ilyen pointer egy indexehez tartozik.
A hash függvény alapján kiadódik egy hash érték, ezzel beleindexelnek a hash indexbe, ami egy sima lineáris tömb. A tömb adott eleme azaz egy vödre rámutat egy sorra. De mivel több sornak is lehet azonos a hash-e az adott index kulcsa alapján, ezeket láncolják hozzá az előző sorhoz az index pointeren keresztül. Nézzük ezt meg egy ábrán:

HashIndices

A bogotai Susannak és a bécsi Susannak is ugyanaz a hash-e, hisz azonos az értékük. Az 5-ös vödör rámutat az egyik sorra, majd onnan a sorból az első index mutató keresztül lehet továbbmenni a következő sorra (láncolt lista).
A 4-es Name hash esetén a két Gregnek és Jane-nek azonos a hashe, azért ők hárman lesznek összeláncolva.
A City oszlopon létrehozott hash index 6-ot rendelt hozzá Liszabonhoz, Bogotához és Bécshez is, ezért ők hárman vannak összeláncolva.

Az ábrából látható, hogy a hash indexek nagyon hatékonyak, ha rövid a láncolt listájuk, de mi a helyzet a módosítással? Miért nem akadnak össze a szálak, amikor pl. törölnek egy sort a láncolt listából? Hogyan oldják ezt meg lock nélkül?

Minden műveletet a korábban már említett Compare Exchange-zsel oldanak meg. Ez egy 3 argumentumú művelet, a .NET doksiból:

public static T CompareExchange<T>(
	ref T location1,
	T value,
	T comparand
)

Összehasonlítja a location1-et a comparanddal, és ha egyeznek, akkor value-t belerakja location1-be. A visszatérési érték a location1 eredeti értéke, ez azért kell, hogy tudjuk, sikerül-e berakni value-t location1-be. Mindezt atomi módon, a processzor támogatásával végzi a gép.

A következő rész natív spekuláció, doksiban nem láttam kirészletezve.

Tegyük fel két versengő folyamat is be akar szúrni egy-egy sort úgy, hogy mindkét sor ugyanabba a vödörbe esik. Tegyük fel a vödörben már van egy sor. Akkor ennek a sornak a következő sorra mutató pointere null lesz.
Bejön az első folyamat. Létrehozza az új sort a levegőben. Ránavigál a hash index vödörből az első sorra, majd végrehajt kb. egy ilyen kódot:

CompareExchange(ref első sor következő pointere, a beszúrandó sor címe, null)

Ha ez a folyamat volt a gyorsabb, akkor az első sor következő pointerét nullként látta, tehát ő tudja hozzárendelni a sorát a next pointerbe, ő tudta magát beláncolni a sorba.
A lassabb folyamatnak már nem nullt lát localtion1-ben, azaz az első sor next pointerében, ezt észreveszi abból, hogy a CompareExchange visszatérési értéke nem null. Ilyenkor mit kell tennie? Ránavigálni a következő (a másik által az előbb beszúrt sorra), és ott újra bepróbálkozni. Előbb-utóbb csak ő is első lesz.
Miért nagyon gyors ez a folyamat? Mert egyáltalán nincs zárolás nem hogy nagyobb felbontással, lap szinten, de még sor szinten sem.
A törlések kezelése már macerásabb lenne, ha így akarnák megoldani. Csakhogy a törlések teljesen másképp mennek, mivel a snapshot izolációs szint miatt meg kell tartani a sor korábbi verzióját is, nem lehet csak úgy kitörölni. Ennek megvalósításáról is lesz szó a párhuzamosságról szóló részben, de előtte még megnézzük a normál indexek hogyan működnek, a következő részben.

2014.04.22.

Computing a Cartesian Product with LINQ

Filed under: .NET,.NET 4,C#,Linq,Szakmai élet — Soczó Zsolt @ 19:11

Na, ezzel izzadtam volna, ha magamtól kell kitalálni.

SQL Server 2014 újdonságok – 2. In-Memory OLTP – hash indexek

Filed under: Adatbázisok,SQL Server,SQL Server 2014,Szakmai élet — Soczó Zsolt @ 08:50

A memóriatáblákhoz kötelező minimum 1 indexet létrehozni, és ez az index egy hash index kell legyen. Mivel kötelező rajtuk egy primary key is, ezért az első indexen nem kell sokat gondolkodni, az a pk mögötti index lesz.
A hash index hasonló mint a .NET Dictionary vagy bármely más rendszer hash táblája. Az index értékekből hasht képeznek, az azonos hash-ű sorokat összeláncolják pointerek mentén. Az adatok NEM lapokon vannak elhelyezve, mivel a lapok a diszk adatok miatt voltak kialakítva a normál táblák esetén. Mivel nincsenek lapok, nem veszekednek a szálak a lapok latch-elésén se (mint a .NET lock kulcsszó), így sokkal nagyobb párhuzamosságot érnek el, mint a korábbi struktúra esetén. Diszk tábláknál ha egy lapon sokan matatnak egyszerre, be kell várják egymást. Itt minden sor szinten van tárolva, azaz sokkal finomabb felbontású régiókat kell védeni, ráadásul a módosítás sose helyben történik, de erről majd a konkurenciáról szóló részben írok.

Nincsenek GAM, SGAM, PFS, IAM és más egyéb szokásos adatösszetartó lapok, csakis az indexek tartják egyben egy tábla sorait. Ezért kell minimum 1 index a táblára.
További indexeket is létre lehet hozni, összesen max. 8-at. Azért ennyit, mert az ütköző hash-ek miatt össze kell láncolni a sorokat, így minden sorba be van építve fixen hash index számú pointer a láncoláshoz, de be kellett határolni a számukat, ne fogjanak el túl sok helyet. Így néz ki egy sor, a végén vannak a láncolt listákhoz a pointerek :

InMemoryTableRow

Az első mezőkről majd a párhuzamosság kezeléséről (concurrency) szóló részben írok.

A hash indexnél fontos a BUCKET_COUNT, ez szabályozza mennyire szórjon a hash függvény. Ha túl kicsire vesszük, hosszú láncolt listák alakulnak ki egy bucketen belül. Ha túl nagy, feleslegesen eszi a memóriát, és lassítja a scaneket, mivel minden bucketet végig kell néznie scan során. Mivel 64 biten dolgozunk, egy pointer 8 byte, így hash index mérete BUCKET_COUNT * 8 byte, csak mindig a legközelebbi, 2 hatványra kerekítik belülről. Azaz 1000-et megadva 1024 vödör keletkeze, ami 8192 byte (ez persze a gyakorlatban nagyon pici lenne, nem lenne hatékony index). Érdemes akkorára venni, mint ahány különböző értéke van a kulcsnak * 1-2. Érhető, hisz így nem nagyon lesz sokelemű a láncolt lista egy bucketen belül, kevés lesz az ütközés.
Primary keynél könnyű a helyzet, mivel egyedi értékeket tartalmaz. Ha a tábla például 3 millió sort tartalmaz, és várhatóan a közeljövőben se nő mondjuk 5 millió fölé, akkor egy hasonló nagyságrendű BUCKET_COUNT jó lehet. Később, ha sokkal több sor lesz, majd újra létrehozzuk a táblát más BUCKET_COUNT-tal. Ebből látszik, hogy a normál táblákkal szemben itt nem annyira önhangoló minden, mint megszoktuk.
Normál oszlopoknál egy select count(distinct oszlop) lekérdezéssel könnyű megnézni, mennyi egyedi érték van a táblában. Ha mondjuk 100 különböző érték van egy 5000 soros táblában, akkor egy nagy BUCKET_COUNT esetén is csak 100 bucketben lesz érték, azaz a bucketekben átlagban 50 sor lesz, amiben már csak lineárisan lehet keresni. Magyarul, erre nem jó a hash index, ide sima fa alapú indexre lesz szükség, mivel azt is létre lehet hozni a memória táblákon.

Nézzük meg a gyakorlatban!

Hozzunk létre egy memória táblát az AdventureWorks Person táblája adataival:

CREATE TABLE [Person].[Person_InMem]
(
	[BusinessEntityID] [int] NOT NULL,
	[PersonType] [nchar](2) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[NameStyle] [bit] NOT NULL CONSTRAINT [DF_Person_InMem_NameStyle]  DEFAULT ((0)),
	[Title] [nvarchar](8) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[FirstName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[MiddleName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[LastName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[Suffix] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[EmailPromotion] [int] NOT NULL CONSTRAINT [DF_Person_InMem_EmailPromotion]  DEFAULT ((0)),
	[rowguid] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Person_InMem_rowguid]  DEFAULT (newid()),
	[ModifiedDate] [datetime] NOT NULL CONSTRAINT [DF_Person_InMem_ModifiedDate]  DEFAULT (getdate()),

CONSTRAINT [PK_Person_InMem_BusinessEntityID] PRIMARY KEY NONCLUSTERED HASH 
(
	[BusinessEntityID]
)WITH ( BUCKET_COUNT = 1000)
)WITH ( MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA )
GO

INSERT INTO [Person].[Person_InMem]
           ([BusinessEntityID]
           ,[PersonType]
           ,[NameStyle]
           ,[Title]
           ,[FirstName]
           ,[MiddleName]
           ,[LastName]
           ,[Suffix]
           ,[EmailPromotion]
           ,[rowguid]
           ,[ModifiedDate])
SELECT [BusinessEntityID]
      ,[PersonType]
      ,[NameStyle]
      ,[Title]
      ,[FirstName]
      ,[MiddleName]
      ,[LastName]
      ,[Suffix]
      ,[EmailPromotion]
      ,[rowguid]
      ,[ModifiedDate]
  FROM [Person].[Person]

Az eredeti táblából kihagytam az xml oszlopokat, azok nem támogatottak memória táblákban. Hash bucket számosságnak 1000-et adtam meg. A táblában 19972 sor van. Mivel primary key-ről van szó, ugyanennyi különböző érték van benne. De csak 1024 bucketünk van, azért átlagosan kb. 20 sor kerül egy bucketbe. Nézzük meg, igaz-e az elmélet, az új dm_db_xtp_hash_index_stats nézettel:

SELECT 
   object_name(hs.object_id) AS 'object name', 
   i.name as 'index name', 
   hs.total_bucket_count,
   hs.empty_bucket_count,
   floor((cast(empty_bucket_count as float)/total_bucket_count) * 100) AS 'empty_bucket_percent',
   hs.avg_chain_length, 
   hs.max_chain_length
FROM sys.dm_db_xtp_hash_index_stats AS hs 
   JOIN sys.indexes AS i 
   ON hs.object_id=i.object_id AND hs.index_id=i.index_id
where object_name(hs.object_id) = 'Person_InMem'
object name    index name                        total_bucket_count   empty_bucket_count   empty_bucket_percent   avg_chain_length     max_chain_length
-------------- --------------------------------- -------------------- -------------------- ---------------------- -------------------- --------------------
Person_InMem   PK_Person_InMem_BusinessEntityID  1024                 0                    0                      19                   38

Látható, hogy az átlagos lánc hossza egy bucketben 19, de van legalább egy 38 hosszú is, mivel a hash függvény nem teljesen egyenletes.
Jó ez nekünk? Nagyon nem. Ideális esetben 1-2 tétel van csak egy bucketben, de 10 fölött már erősen érezhető lesz a teljesítményveszteség. Látható, hogy az üres bucketek (áttérek a vödör szóra, de elég hülyén hangzik) száma 0. Ez is probléma, “nincs hely” az új sorok részére, azaz még nagyobb torlódás lesz később. Az útmutató szerint az üres vödrök száma legalább 33% kell legyen, hogy legyen hely a táblának növekedni.

Próbáljuk meg magasabbra venni a vödrök számát, mondjuk 10000-re. Mivel memória tábláknál nincs alter table, újra létre kell hozni, más paraméterekkel, majd újratölteni. Ez után a hash statisztikák:

object name    index name                        total_bucket_count   empty_bucket_count   empty_bucket_percent   avg_chain_length     max_chain_length
-------------- --------------------------------- -------------------- -------------------  ---------------------- -------------------- --------------------
Person_InMem   PK_Person_InMem_BusinessEntityID  16384                4933                 30                      1                   8

Na, ez már sokkal jobban néz ki. 16384 vödör van (10000 után ez volt a következő 2 hatvány), 30%-a a vödröknek üres, átlagosan 1 elem van minden vödörben (nyilván kicsit több, mivel 19000 sor van 16000 vödörre).

De nem mindig lehet csak a vödrök számával ilyen szép helyzetet előállítani, ha túl sok ismétlődés van az adatokban. Hozzunk létre egy második indexet is, ezúttal a FirstName oszlopra:

SELECT count(distinct FirstName) FROM [AdventureWorks2012].[Person].[Person]; --1018

CREATE TABLE [Person].[Person_InMem]
(
	[BusinessEntityID] [int] NOT NULL,
	[PersonType] [nchar](2) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[NameStyle] [bit] NOT NULL CONSTRAINT [DF_Person_InMem_NameStyle]  DEFAULT ((0)),
	[Title] [nvarchar](8) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[FirstName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[MiddleName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[LastName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[Suffix] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[EmailPromotion] [int] NOT NULL CONSTRAINT [DF_Person_InMem_EmailPromotion]  DEFAULT ((0)),
	[rowguid] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Person_InMem_rowguid]  DEFAULT (newid()),
	[ModifiedDate] [datetime] NOT NULL CONSTRAINT [DF_Person_InMem_ModifiedDate]  DEFAULT (getdate()),

CONSTRAINT [PK_Person_InMem_BusinessEntityID] PRIMARY KEY NONCLUSTERED HASH 
(
	[BusinessEntityID]
)WITH ( BUCKET_COUNT = 10000)
,
INDEX [ix_FirstName] NONCLUSTERED HASH
(
	[FirstName]
) WITH ( BUCKET_COUNT = 1000)
)
WITH ( MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA )

De nem jön létre az index, mert:
Indexes on character columns that do not use a *_BIN2 collation are not supported with indexes on memory optimized tables.

Ez van, a hasht gyorsan kell tudni előállítani, ebben a verzióban legalábbis nem mentek bele, hogy a nyelvek nemzeti sajátosságait kihasználva is képezzenek hasht. Ezekre lehet majd rendes fa alapú indexet létrehozni, de most koncentráljunk még a hash indexekre.

Lássuk a dátum oszlopon:

SELECT count(distinct [ModifiedDate]) FROM [AdventureWorks2012].[Person].[Person] --1290

CREATE TABLE [Person].[Person_InMem]
(
	[BusinessEntityID] [int] NOT NULL,
	[PersonType] [nchar](2) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[NameStyle] [bit] NOT NULL CONSTRAINT [DF_Person_InMem_NameStyle]  DEFAULT ((0)),
	[Title] [nvarchar](8) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[FirstName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[MiddleName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[LastName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[Suffix] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[EmailPromotion] [int] NOT NULL CONSTRAINT [DF_Person_InMem_EmailPromotion]  DEFAULT ((0)),
	[rowguid] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Person_InMem_rowguid]  DEFAULT (newid()),
	[ModifiedDate] [datetime] NOT NULL CONSTRAINT [DF_Person_InMem_ModifiedDate]  DEFAULT (getdate()),

CONSTRAINT [PK_Person_InMem_BusinessEntityID] PRIMARY KEY NONCLUSTERED HASH 
(
	[BusinessEntityID]
)WITH ( BUCKET_COUNT = 10000)
,
INDEX [ix_ModifiedDate] NONCLUSTERED HASH
(
	[ModifiedDate]
) WITH ( BUCKET_COUNT = 1000)
)
WITH ( MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA )

Dátum oszlopra már megy a hash index.

A hash statisztikák:

object name    index name       total_bucket_count   empty_bucket_count   empty_bucket_percent   avg_chain_length     max_chain_length
-------------- --------------- -------------------- -------------------  ---------------------- -------------------- --------------------
Person_InMem   ix_ModifiedDate                 1024                 262                      25                   26                  143

Csapnivalóak az eredmények. Bár van 25% szabadon, azaz a vödrök számát nem lőttük alá, mégis átlagosan 26 sor van egy vödörben. Miért? Mert minden dátum kb. 20-szor szerepel a kulcsban, nem elég egyedi az oszlop. Az ilyenekre nem igazán hatékony a hash index.
A következő részben megnézzük, hogyan tárolja és hogyan használja a hash indexeket az SQL Server.

Memória táblák meghajtása OR mapperrel

Az SQL Server 2014-es memóriatáblákat csak akkor lehet a lehető legközvetlenebb módon elérni, ha natív kódra fordított tárolt eljárásokkal érjük el. Az OR mapperek viszont alapban sima, dinamikus SQL-eket generálnak. Ha így akarunk CRUD műveleteket végrehajtani egy memória táblán, akkor az átmegy a szerver interop rétegén, ami jelentősen belassítja azt. Többszörösére. Emiatt -jelen tudásom szerin-, ha OR mapperel akarjuk meghajtani a memória táblákat, akkor saját spket kell írni. Kipróbáltam a dolgot, de falakba ütköztem. Ugyanis az OR mapperek elvárják, hogy pl. egy update után visszaadja a szerver az xxx rows affected information message-et, ebből tudja az OR mapper, hogy sikerült a sor módosítása. Ha nem jön vissza semmi, akkor azt hiszi, hogy az optimista konkurencia ellenőrzés miatt nem sikerült a sor módosítása (azt hiszi valaki közben módosította vagy törölte a sort), így exceptiont dob. Viszont a memória táblás update NEM adott vissza XXX rows affected üzenetet, így az OR mapper exceptiont dob (NHibernate.StaleObjectStateException és ).
A megoldás tehát valószínűleg az lehet, hogy ki kell kapcsolni az OR mapper optimista konkurrencia ellenőrzőjét.
Egyelőre NHibernate-nél nem találtam megoldást rá, legalábbis a Conf ORM-es “code first” mapping módszerrel. EF-en még nem próbáltam.
Mindenesetre kiírtam msékhez a kérdést, ha jön válasz, megírom.

2014.04.18.

Az ORM batching perf vonzata

Az egyik ügyfelemnél letöltenek pár száz sort memóriába Entity Frameworkkel. Ott C# kóddal mindenféle komplex módon kitalálják, melyik entitást hogyan kell módosítani, majd a módosítások eredményét az EF betolja adatbázisba. Bár a szerveren nem volt nagy terhelés, mégis volt amikor 1000 fölött volt a batch request/second.

Miért? Mert az EF nem tudja összenyalábolni a módosításokat, hanem egyesével küldi be őket. Azaz, ha 300 sor módosult, akkor 300 update fog bemenni, ennyi roundtrip lesz. Ez annyira gáz, hogy egyszerűen nem értem az MS-t, miért nincs még batching a 6-os EF-ben.

Mutatok egy példát, mekkora hatása van ennek. Az NHibernate bármennyire is elhanyagolt manapság, de ő szépen tud batchelni.
A következő példában letöltök 20000 sort, módosítom mindet, majd visszamentem a módosításokkal. EF-fel ez 11mp.
NHibernate-tel meg tudom mondani, hány utasítást küldjön be egyszerre. Ettől függően az alábbi számok jönnek ki:

NHib test - BatchSize: 1000000, Duration: 00:00:00.9390957
NHib test - BatchSize: 100000, Duration: 00:00:00.8363326
NHib test - BatchSize: 10000, Duration: 00:00:00.8463299
NHib test - BatchSize: 1000, Duration: 00:00:01.0654756
NHib test - BatchSize: 100, Duration: 00:00:01.3361271
NHib test - BatchSize: 10, Duration: 00:00:02.4312424
NHib test - BatchSize: 1, Duration: 00:00:10.4669159

Elég nagy batch méretnél a 10 mp lemegy 1mp alá!
EF-nél 11, és kész. Cáfoljon meg valaki, hogy rosszul tudom, és az EF is tudja ezt.

2014.04.17.

SQL Server 2014 újdonságok – 1. In-Memory OLTP – bevezetés

Filed under: Szakmai élet — Soczó Zsolt @ 16:40

Talán a legnagyobb durranással kezdem a sorozatot: a memória táblákkal.
A nagyon kemény OLTP (CUD, insert, update, delete) forgalommal terhelt táblákat meg lehet jelölni, mint memória táblákat. A memória táblák éppúgy garantálják az ACID jellemzőket, mint a normál táblák. Azaz Atomic, Consistent, Isolated, Durable. Atomic: támogatja a tranzakciókat. Consistent: ha egy tranzakció előtt konzisztensek voltak az adatok, akkor a tranzakció lezárása után is azok lesznek. Isolated: az egymással versengő tranzakciók nem hatnak egymásra (pontosabban pontosan specifikált, mennyire hatnak egymásra). Durable: ha egyszer azt mondja, elmentett egy adatot, akkor az tényleg tartósan el lesz tárolva, akkor is, ha pont elmegy az áram a commit után. Ez utóbbit gondolná, hogy nem teljesük a memória táblák esetén, de ez nem igaz, ez is teljesen Durable, mint a diszk alapú táblák.
A tranzakciók dolgozhatnak egyszerre diszk és memória táblákon is. Ráadásul a csak memória táblákat elérő tárolt eljárásokat le lehet fordítani natív kódra is, erről majd külön részben írok.
Mit várhatunk tőle? A dokumentáció szerint 5-20-szor gyorsabbak lesznek a műveletek, mint a diszk alapú tábláknál. Ezt majd ki is mérjük, hogy lássuk a saját szemünkkel.
Mire jók a memória táblák?

  • Nagysebességű adatbeszúrásokhoz, pl. mérési adatok gyűjtése
  • Átmeneti táblákhoz adatbetöltőkhöz illetve adat transzformációs műveletekhez
  • Átmeneti adatok tárolására, ahol eddig @-os táblákat használtunk
  • Tábla típusú változóként változó számú paraméter átadására data access layerből

Az In-Memory táblák TELJESEN MÁS architektúra mentén vannak felépítve, mint a diszk alapú táblák. Valójában az SQL Servertől függetlenül fejlesztették ki, kérdés volt, hogy külön termék legyen vagy integrálják bele az SQL Serverbe. Utóbbi mellett döntöttek, ami nekünk nagyon kényelmes, csak a fejlesztőknek volt iszonyatos munka az integráció. De mi jól jártuk vele, mivel kényelmes, megszokott módon használhatjuk, mint a diszk alapú táblákat, ráadásul egymás mellett él a kétféle táblatípus, így lehet őket joinolni, stb. Így a nagyon erősen terhelt táblákat át lehet tervezni memória alapúra, a többi meg maradhat diszk alapú. Ez óriási fegyvertény.

Az új engine kifejlesztésére az volt a motiváció, hogy az SQL Server architektúrája az utóbbi legalább 20 évben nem sokat változott. Az alap feltételezés az volt, hogy minden adat diszken van, és csak egy részhalmazuk van benn a memóriában, mivel drága és korlátos a RAM, illetve a tárolandó adathalmaz mérete sokkal nagyobb, mint a RAM mérete. Ez ma már sokszor nem igaz. Emiatt minden szinkronizációs és tárolási adatszerkezet ebből az előfeltételezésből lett kialakítva. Sok processzor és memóriaintenzív feldolgozás esetén azonban az eddigi adatstruktúrák elkezdenek bajba kerülni. Egyre több idő megy el várakozással, amit Latch Contentionként illetve Lock Contentionként látunk a monitoring eszközökben.
Az új engine kódneve Hekaton volt, ami görögül 100-at jelent. Azért ez a kódneve, mert az volt a cél, hogy 100-szor gyorsabb motort rakjanak össze. Nyilván ez magas léc, de mint motiváló erő jó a fejlesztőknek.
Egyetlen egy critical section van csak benne, és az is 1 utasítás hosszú, ami egy növelés művelet, a timestamp generáláshoz használják (bővebben később). A többi többszálú műveletet a processzor által támogatott, így nagyon gyors interlock compare exchange művelettel valósítják meg. (.NET-ben ez az Interlocked.CompareExchange-re van kivezetve.) Egy lock a normál adatbázisban több 10000 órajelciklust igényel, egy latch több ezret, egy compare exchange csak 10-20-at. Szép, mi?
Az új motor természetesen sok korlátot is magával hoz. Ezek egy része természetes, pl. nem támogatnak blobokat (nvarchar(max)), más részük meg lehet csak az 1.0 miatt van, pl. nincsenek check contraintek (ez csak az én véleményem). Ahogy az egyik SQL fejlesztő mondta, hiába akarunk felhőkarcolót építeni, előbb az alapokat kell stabilra építeni. Erről szól ez az iteráció, az SQL Server 2014 RTM.
A memória táblákat lehet SCHEMA_ONLY durability-vel is megjelölni, ekkor a szerver újraindulása után elveszik belőle minden adat, de cserébe tényleg nem nyúl diszkhez, mivel nem is ígér tartóssági garanciákat. De veszett gyors lesz.
Hogy lássunk valami konkrétabbat, így néz ki egy durable memória tábla, azaz, ami restart után is megtartja az adatokat:

CREATE TABLE [Person].[Person_InMem]
(
	[BusinessEntityID] [int] NOT NULL,
	[PersonType] [nchar](2) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[NameStyle] [bit] NOT NULL CONSTRAINT [DF_Person_InMem_NameStyle]  DEFAULT ((0)),
	[Title] [nvarchar](8) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[FirstName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[MiddleName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[LastName] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
	[Suffix] [nvarchar](10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[EmailPromotion] [int] NOT NULL CONSTRAINT [DF_Person_InMem_EmailPromotion]  DEFAULT ((0)),
	[rowguid] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Person_InMem_rowguid]  DEFAULT (newid()),
	[ModifiedDate] [datetime] NOT NULL CONSTRAINT [DF_Person_InMem_ModifiedDate]  DEFAULT (getdate()),

CONSTRAINT [PK_Person_InMem_BusinessEntityID] PRIMARY KEY NONCLUSTERED HASH 
(
	[BusinessEntityID]
)WITH ( BUCKET_COUNT = 1048576)
)WITH ( MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA )

A MEMORY_OPTIMIZED = ON miatt lesz memória és nem diszk alapú a tábla. A SCHEMA_AND_DATA miatt marad meg restart után.
Ha módosulnak az adatok, csak a tranzakció lezárásakor írja ki a módosításokat diszkre a szerver. Az indexek módosításait NEM logolja, mivel újrainduláskor újra létrehozza őket a recovery fázisban. Ezzel sokkal kevesebb és hatékonyabb IO-val oldja meg a tartós adattárolást a normál táblákhoz viszonyítva.

A következő részben a hash indexeket részletezem ki. Ha tetszett a cikk, örülök a like-nak, ösztönöz, hogy hamar megírjam a többi részt is. :)

2014.04.16.

SQL Server 2014 AdventureWorks

Filed under: Adatbázisok,SQL Server,SQL Server 2014,Szakmai élet — Soczó Zsolt @ 10:29

Pár napja már letölthető, lehet vele játszani.

Site id load balancing környezetben

Filed under: Szakmai élet — Soczó Zsolt @ 08:32

Az közismert, hogy ha több ASP.NET-et kiszolgáló IIS van load balancer mögött, akkor a machineKey-eket egyformára kell venni a machine.configban, illetve külső, osztott session state tárolót kell használni:

<configuration>
  <system.web>
    <machineKey validationKey="ugyanaz az érték minden szerverre" decryptionKey="ugyanaz az érték minden szerverre" validation="SHA256" decryption="AES" />
    <sessionState mode="SQLServer" stateNetworkTimeout="30" stateConnectionString="..." allowCustomSqlDatabase="true" sqlConnectionString="..." cookieless="false" timeout="1441" />
  </system.web>
</configuration>

Azonban ez nem elég, és a következő dolog nincs agyondokumentálva. Minden egyes sitenak azonosnak kell lenni az id-jának:

AppId

“In short: under Advanced Settings of each site in IIS, there’s a numeric ID which is configurable but apparently auto-incremented from 1 in order of site creation on that server. These IDs are used in the ASPStateTempApplications table, and were different between our servers, resulting in different Session IDs.”

Itt írnak róla, meg itt.

2014.04.15.

Entity Framework 6 null kezelés

Ha egy nullos oszlopon szűrünk, akkor előfordulhat, hogy a bemeneti paraméter is null.
where oszlop = @param

Ebben az esetben az SQL Server alapban az ansi nullságot alkalmazza, így a null != nullal, azaz nem jön vissza egy sor se. Ezért EF6-ban az SQL generátor más sqlt generál, az ilyen esetekben egy IS NULL-os ággal megoldja, hogy nullokra is menjen a keresés.
Viszont ennek drámai mellékhatásai lehetnek. Lássunk egy egyszerű példát:

using (var e = new AdventureWorks2012Entities())
{
var matthew = "Matthew";
e.Person.FirstOrDefault(p => p.LastName == matthew);
}

Ez esik ki az EF-ből:

declare @p__linq__0 nvarchar(4000) = N'Matthew'

SELECT TOP (1) 
    [Extent1].[BusinessEntityID] AS [BusinessEntityID], 
    [Extent1].[PersonType] AS [PersonType], 
    [Extent1].[NameStyle] AS [NameStyle], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[MiddleName] AS [MiddleName], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[Suffix] AS [Suffix], 
    [Extent1].[EmailPromotion] AS [EmailPromotion], 
    [Extent1].[AdditionalContactInfo] AS [AdditionalContactInfo], 
    [Extent1].[Demographics] AS [Demographics], 
    [Extent1].[rowguid] AS [rowguid], 
    [Extent1].[ModifiedDate] AS [ModifiedDate]
    FROM [Person].[Person] AS [Extent1]
    WHERE (([Extent1].[LastName] = @p__linq__0) 
	AND ( NOT ([Extent1].[LastName] IS NULL OR @p__linq__0 IS NULL))) 
	OR (([Extent1].[LastName] IS NULL) AND (@p__linq__0 IS NULL))

Látható, hogy bool algebrával összehozták, hogy ha a paraméter null és az oszlop is null (is nullal), akkor lejönnek szépen a sorok. Viszont az ilyen query-ket utálja az sql server, nehezen tudja őket optimalizálni. option(recompile) segítene rajta, de ezt meg nem lehet kiadni EF-en keresztül.

Szerencsére vissza lehet állítani a régi kódgenerátort is:

using (var e = new AdventureWorks2012Entities())
{
    e.Configuration.UseDatabaseNullSemantics = true;
    var matthew = "Matthew";
    e.Person.FirstOrDefault(p => p.LastName == matthew);
}

Generált SQL:

	SELECT TOP (1) 
    [Extent1].[BusinessEntityID] AS [BusinessEntityID], 
    [Extent1].[PersonType] AS [PersonType], 
    [Extent1].[NameStyle] AS [NameStyle], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[MiddleName] AS [MiddleName], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[Suffix] AS [Suffix], 
    [Extent1].[EmailPromotion] AS [EmailPromotion], 
    [Extent1].[AdditionalContactInfo] AS [AdditionalContactInfo], 
    [Extent1].[Demographics] AS [Demographics], 
    [Extent1].[rowguid] AS [rowguid], 
    [Extent1].[ModifiedDate] AS [ModifiedDate]
    FROM [Person].[Person] AS [Extent1]
    WHERE [Extent1].[LastName] = @p__linq__0

Így már egy teljesen tiszta szűrést kapunk, de ez NEM menne null bemeneti paraméterre. Ha nem is kell, hogy menjen, akkor viszont ez sokkal gyorsabb lehet, mint az első, megfelelő indexek esetén.

A két lekérdezés tervét egymás mellé rakva jól látható a különbség:
EFUseDatabaseNullSemanticsDiff

Nagyobb tábláknál a hatás sokkal radikálisabb lenne, érdemes szem előtt tartani ezt.

2014.04.10.

Háklis MVC binder

Filed under: Szakmai élet — Soczó Zsolt @ 14:11

Debugoztam egy jóízűt az MVC forrásában. A default binder nem akarta visszaállítani egy komplex model gyerek ojjektumait.
Bősz debugolás után kiderült, hogy az zavarta meg, hogy a controller actionban a modellt fogadó paraméter nevét pont ugyanarra állítottam, mint a gyökér model objektum gyerekét tároló property nevét.

  public class RegistrationModel
    {
        public Registration R { get; set; }
...

 [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(RegistrationModel r)
        {

Az R property név a RegistrationModelben és a RegistrationModel r paraméter az actionben, ez zavarta meg. Így jár, aki idióta neveket alkot. :)

Még van pár hely a májusi TDD-re.

Overposting vagy Mass Assignment támadás ASP.NET MVC-ben

Filed under: .NET,ASP.NET,mvc,Szakmai élet — Soczó Zsolt @ 09:25

Nagyon hasznos a binder az MVC-ben, nem kell kézzel kiszedegetni a form értékeket és átmásolni a modellbe. De ha a modell több property-t tartalmaz mint amit a html formba kigenerálunk, akkor ki vagyunk téve egy támadásnak. Ugyanis minden további nélkül be lehet rakni a post kérésbe olyan mezőket is, amelyek nincsenek a formon, de benne vannak a modellben, így alapban, ha nem korlátozzuk le a binder szépen feltölti a modellt a fake bepostázott értékekkel is. Ez durva secu támadásokra ad lehetőséget.
A kivédésére itt bemutatnak sokféle megoldást. A legjobb szerintem az utolsó, amikor tényleg az adott view részére hozunk létre egy specifikus modellt, nem pedig a bindert próbáljuk meg lebeszélni a felesleges adatmásolásokról.

Ps. ma végzek a jelentkezési lappal mvc-ben a TDD tanfolyamra, nem véletlenül szól mvc-ról a post. :)

Ha Chrome-ban nem akarod inline megjeleníteni a pdfeket

Filed under: Szakmai élet — Soczó Zsolt @ 07:59

Ez engem azért zavart, mert a böngésző readerében nem lehet a nyomtatást olyan finoman szabályozni, mint egy külső olvasóban.

“If you don’t want to automatically view pdf files, but prefer to download them, you can disable both plug-ins this way:
type “about:plugins” in the address bar (without the quotes), hit enter, and then click on details (near the top right),
disable Chrome PDF Viewer and disable the Adobe one and any other plug-ins that maybe related to PDF viewing/reading.”

2014.04.02.

A T nem hazudik

Filed under: Élet — Soczó Zsolt @ 23:08

Tavaly egy hétig nem volt telefonvonalunk. Jeleztem nekik, hogy térítsék meg a kiesést, ahogy azt a szerződés előírja (nem érdekel 2-3e Ft, az elv érdekel, a gerinctelen sunnyogás, ami rothasztja társadalmunkat). Elhajtottak a fba, mivel, ahogy ők írták az smsben (1 hét múlva), nem is náluk volt a baj. Panaszt tettem, K. Zoltánnak jutott az a hálátlan feladat, hogy megírja a lerázó levelet. Nem irigylem a munkáját.

De van egy kis bökkenő. Rögzítettem a velük történt beszélgetést.

2013.12.07. 20:28-kor jeleztem nekik a hibát. Pár nap múlva lezárták a hibát, hogy nehogy kifussanak a 3 napból. Aztán kérésemre újra megnyitották, 2013.12.11-én. 12-én este felhívtam őket újra, hogy WTF? És ekkor kaptam az alábbi érdekes infót:

“Úgy látom, hogy hozzácsatolták egy nagyobb, központi hibához, ott lehet a probléma. Igazából még hibaelhárítás alatt van. Kis türelmét kérjük.”

T válasz a panaszra:
“A hiba behatárolása során megállapítottuk, hogy a hiba nem társaságunk felelősségi körébe tartozik.” Hogyne.

Eredeti hanggal itt érhető el.

2014.04.01.

SQL Server 2014 letölthető

Filed under: SQL Server 2014,Szakmai élet — Soczó Zsolt @ 18:00

Innen.
Már töltődik is. :)

A következő hetekben, -amennyire időm engedi- szeretnék írni a benne levő újdonságokról.

Powered by WordPress