Soci (Soczó Zsolt) szakmai blogja

2012.03.31.

NHibernate futures

Filed under: .NET,Adatbázisok,ADO.NET,Entity Framework,NHibernate,Szakmai élet — Soczó Zsolt @ 16:59

Korábban írtam, hogy a Future query-k milyen jók, mivel ha tudjuk, hogy sok lekérdezést egymás után akarunk végrehajtani, akkor össze lehet őket rakni egy batchbe, ezzel igen jelentős gyorsulást lehet elérni. Álljon itt egy konkrét példa:

private IQueryable LoadTradeStatisticsQueryable(ISession session, TaConfig config, TradeDirection dir)
{
return from ts in session.Query()
where ts.TaConfig == config && ts.Dir == dir
select ts;
}

public Dictionary> LoadTradeStatisticsBulk(TradeDirection dir, IEnumerable taSubPortfolios)
{
var futures = new Dictionary>();
using (var session = AtsNHibSessionFactory.Factory.OpenSession())
{
foreach (var taSubPortfolio in taSubPortfolios)
{
futures.Add(taSubPortfolio, LoadTradeStatisticsQueryable(session, taSubPortfolio.TaConfig, dir).ToFuture());
}
foreach (TaSubPortfolio p in futures.Keys.ToList())
{
futures[p] = futures[p].ToList();
}
}

return futures;
}

A trükk egyszerű. A lekérdezéseket nem hajtjuk végre, hanem a Queryable-öket a ToFuture segítségével betárazzuk későbbre. Aztán mikor mind megvan, a ToList()-tel kikényszerítjük az IQueryable-ök végrehajtását. A poén az, hogy ilyenkor az NHib tudja, hogy ezek future query-k, így a 2. ciklus első iterációjakor egyszerre kiadja az összes lekérdezést, így a ciklus 2..n. iterációjában már csak kivesszük az eredményeket.
Az adatbázisba ez sok-sok selectként megy be egy batchben, majd a több resultsetből az NHib szépen materializálja az entitásokat.
Sokszoros sebességnövekedést lehet elérni csak ezzel az egyszerű trükkel. Ez nem csak homogén lekérdezésekre megy, tetszőlegeseket össze lehet rakni így egybe.
EF-hez itt van hasonló kiegészítés.

2012.03.30.

Miért NHibernate?

Jeleztem korábban, hogy mióta megismertem az NHibernate-et, azóta nem nagyon foglalkozok az Entity Frameworkkel. Miért? Pár gondolatot leírok, aztán majd kifejtem őket bővebben is.

  • Sokféle kulcsgenerálást ismer. Az id generálásnak rendkívül nagy hatása van a módosítások felviteli sebességére. Eddig az assigneded és a HILO-t használtam élőben. SQL Serve Identity elfelejtve, considered harmful. Az SQL Server 2012 Sequence + HILO viszont egy jó kombináció lesz.
  • Batch-elt műveletek. Komplex objektum gráfok módosításakor ez is igen sokat jelent, mivel 1 roundtrip alatt lehet sok insert-update-delete műveletet végrehajtani. Ráadásul pl. Oracle esetén kihasználják az Array Bindingot, ami kb. olyan, mint SQL servernél a tábla típusú paraméter, így eszement gyorsan tud DML műveleteket végrehajtani.
  • Rendkívüli kibővíthetőség. Az IInterceptor interfészen keresztül mélyen bele lehet nyúlni a működésébe. Ezt pl. field szintű securityhoz használtuk ki, e nélkül nem nagyon tudtuk volna megcsinálni.
    Vagy pl. a tranzakciók megkezdése után, de még az SQL parancsok végrehajtása előtt be kellett tolni egy a kliensoldali felhasználót reprezentáló információt (audit miatt). Erre is volt kézenfekvő kibővítési pont.
    Vagy oraclenek collection típusú paramétert kellett átadni, ezt is meg tudtuk szépen oldani saját adattípus definiálásával.
  • Direkt DML műveltek. Update vagy Delete anélkül, hogy behúznánk az entitásokat memóriába. Lehet érvelni, hogy nem erre való egy O-R mapper, de ha már egyszer az ember entitásokban és nem táblákban fogalmazza meg a dolgait, egyszerűbb a DML-eknél is ezt használni. Főleg pl. egy öröklődéses esetben, amikor több táblában is kell törölni.
  • Flexibilis materializáló. Joinolt direkt SQL select kimenetét szépen bele lehet passzírozni egy objektumfába, egyszerűen.
  • Saját adattípusok használta. Az Oraclenek nincs GUID típusa, ezért RAW(16)-ként tároltuk a guidokat. Entitás oldalon viszont szeretjük a GUID-ot, ezért egy saját típussal oldottuk meg a leképezést, így mindkét oldal természetes absztrakciókkal dolgozik.
  • Enum mapping. Ha akarom intként menti el, ha akarom stringként. Hogy is van ez EF-nél?
  • Cascade műveletek gyerekekre való használata szabályozható.
  • Szabályozható, hogy csak a megváltozott property-ket updatelje, vagy mindet?
  • Van stateless sessionje (NHib Session kb. EF ObjectContext) nagytömegű adatkezeléshez
  • Tud natívan pl. tömböt adatbázisra mappelni. Azaz sorszáma van az elemeknek, ha kitörlök egy elemet, akkor a felette levő sorszámúakat automatikusan updateli.
  • Vannak current session kontérerei. Ezek pl. WCF szerviz és DI használata esetén nélkülözhetetlenek, így pl. a session élettartama a WCF OperationContexthez láncolható. Ezt annak idején letöltöttem EF-hez, elég bonyolult volt, de ez megy ott is.
  • Future Query-k: ezekkel több lekérdezést egybe lehet fűzni, hogy egy batchben hajtsa végre. EF-hez láttam ilyet a kiegészítő packben.
  • Secondary Cache. Ezt még nem használtam, de érzésre hatalmas potenciál van benne. Cache réteg, így a lekérdezések egy részét memóriából adja vissza.
  • Sokféle adatbázis támogatása.
  • Lazy properties. Betölti az entitást, de nem tölti be pl. a benne levő byte tömböt, ami egy blobra meppelődik a táblában. Csak akkor töltődik be, ha tényleg hivatkozunk a property-re. Ehhez ugyanazt a virtuális proxy megoldást használja, mint az EF.

Hirtelen ennyi jut az eszembe. Egyetlen, de ez nagy negatívum az EF-fel szemben a Linq providere. Az egyszerűen szar, félkész, még most, 2012. márciusában is. Ami kész van, az is elég ostoba, az EF-é viszont okos, tudja, mi a jó az adatbázisnak, így optimalizálja a generált SQL kódot.
Pl. az alábbi NHibernate LINQ-ból: … where !a.IsValid ezt csinálta Oracle alatt:
where not(a.IsValid).
Ez persze table scant okoz orán is. Így kellett átírni: where a.IsValid == false. Ebből lett: where a.IsValid = 0. Ez már seek. De basszus, ezt elvárnám a LINQ providertől!
Nincs a LINQ providerében outer join implementálva! Vagy amikor írtam egy allekérdezéses, Take-es majd select-es nagyobb LINQ kifejezést egyszerűen NotImplementedExceptiont kaptam, és láttam, az egyik NHib fejlesztő megindokolta, hogy ez miért jó így.
Szóval ez a része beteg, párszor LINQ helyett Criteria vagy HQL alapú lekérdezést kellett írnom, ez bosszantó, ront a jó benyomáson.

Tudom, hogy ez a bejegyzés egyesek agyát felhúzza majd. Lesz, aki utána fog nézni, hogy amiket pozitívumként írtam, valójában megvan vagy lesz EF-ben, vagy az EF Extensions packben. Lehet, nem tudok mindent én se fejből.
Nyissunk vitát, nézzük meg, melyik-miben jó, ebből mindenki tanulhat.

SQL Server 2012 újdonságok – 2. Sequence

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

Update:
A cikkben NEM folytonos sorszámgenerálásról írok, mint pl. amit számla sorszámozásnál használnak, hanem pusztán egyedi kulcsok generálását, amik nem ismétlődnek, monoton nőnek, de nem folytonosak.

A sorszámgenerálás igen általános feladat adatbázisalapú alkalmazásokban. Az egyik leggyakoribb eset, amikor egész számokra épített mesterséges elsődleges kulcsoknak kell értéket generálni. Erre eddig a leggyakrabban alkalmazott megoldás az IDENTITY-vel megjelölt oszlop használata volt. IDENTITY-nél az oszlopon kijelölt sorszámgenerálás és az insert művelet össze volt ragadva. A SEQUENCE nevű új adatbázis objektum segítségével mindentől teljesen független sorszámgenerátorokat hozhatunk létre, amelyeket bármikor meghívhatunk, nem csak insertekhez.
Miért jó ez a sorszámgenerálás és a felhasználása elválasztása? Képzeljük el azt az estet, hogy el kell menteni megrendeléseket. Van 1000 megrendelés fejlécünk (Order Header), és mindegyikhez van átlagban 5 megrendelés tétel (Order Detail). IDENTITY -t használva egyesével be kell szúrni a fejléc sorokat, visszavezetni az adatelérő rétegbe a generált identity értéket, átírni az adott fejléc alá tartozó megrendelés tételek szülő fejlécre mutató idegen kulcsát a kapott értékkel, majd beszúrni a megrendelés tételeket. A megrendelés tételeket be lehet szúrni egy batch-ben, azaz egy körülfordulás alatt. Azonban a fejléc sorok identity-jének visszavezetése miatt 1000 külön batch-et kell beküldeni a szülő sorok beszúrásához, majd további 1000-et a gyerek sorok kedvéért. Ez összesen 2000 hálózati körülfordulás, pedig már kötegeltük a gyerekek beszúrását, e nélkül 1000+5000=6000 körülfordulás lenne. Ha egy hálózati körülfordulás 10ms (ez realisztikus, és magában foglalja az insert idejét is), akkor a 2000 művelet 20 másodperc alatt fut le.
Ha azonban SEQUENCE segítségével generáljuk a kulcsokat, akkor az adatelérő réteg minden további nélkül lekérhet egy nagyobb sorszámtartományt, azaz nem 1-gyel lépteti előre a SEQUENCE-t, hanem pl. 10000-rel. Így az az adatelérő réteg kap 10000 sorszámot, amit más garantáltan nem kap meg, így ezzel ő gazdálkodhat. Mit fog tenni egy okos az adatelérő réteg? Előre kiosztja minden szülő sornak a kulcsértéket a kapott tartományból, utána állítja a gyerekeket, és 1 azaz egy batch-ben képes beszúrni az összes szülő és gyerek sort! Praktikusan ez azt jelenti, hogy valószínűleg 1 mp-en belül beszúrásra kerül mind a 6000 sor.
Óriási nyereség ez, amelyet a fejlettebb Objektum Relációs Mapper-ek (pl. NHibernate) HiLo stratégiaként ismernek, és tudnak SEQUENCE-t használni a kiosztott id-k (elsődleges kulcsok) kezelésére.
A megoldás kulcsa tehát az, hogy még a sorok beszúrása előtt tudunk azoknak id-t generálni, ezáltal extra optimalizálási lehetőségeink vannak. Minek köszönhető ez? Annak, hogy az id generálás és a beszúrás műveletek nincsenek összekötve időben, köszönhetően a SEQUENCE-eknek.
Mivel a SEQUENCE által generált sorszámot tetszés szerint használhatjuk, készíthetünk vele például táblák között megosztott id generátort is, így pár tábla úgy kap elsődleges kulcsot, hogy a táblák között nézve se lesz ütközés közöttük.
Nézzünk pár példát rá.
Integer alapú SEQUENCE létrehozás, amely 1-től indul és 1-esével lépked:

create sequence HumanResources.Sequence1 as int
start with 1
increment by 1;

A következő sorszám lekérése:

select next value for HumanResources.Sequence1;

Trükkösebb SEQUENCE-ek is vannak, amelyek miután elérek egy felső határt, átfordulnak, és újrakezdik a számlálást:

create sequence HumanResources.SequenceWithCycle
as int
start with 1
increment by 1
minvalue 1
maxvalue 5
cycle;

Miután ellépkedett 5-ig, újra 1 lesz a következő generált érték. A cycle kulcsszó nélkül hibát kapnánk az 5 után meghívott next value-ra:

The sequence object ‘SequenceWithCycle’ has reached its minimum or maximum value. Restart the sequence object to allow new values to be generated.

Hogyan implementálták a SEQUENCE-t, hogy elég gyors legyen? Ha tisztán memóriában növelgetnék a sorszámot, akkor a sorszám a szerver újraindulása után újraindulna, ezért mindenképpen kell valamilyen tartós tároló (diszk, sql tábla, stb.) mögé. Lehetne azt csinálni, hogy amikor leállítják a szervert, akkor kiírnák az számláló állását, majd újraindítás után visszaolvassák azt. Ezzel viszont az a baj, hogy ha váratlanul lehal a szerver processz, vagy elmegy az áram, akkor nem lesz kiírva az utolsó érték, így legközelebb újra kioszt már kiadott sorszámokat, ami nyilvánvaló logikai hibákat okozna.
Lehetne minden sorszám letépése után kiírni az aktuális értéket, de ez meg nagyon lassú lenne. A SQL Server által választott megoldás kompromisszum a két oldal között, egyfajta cache-elés.
Memóriában tárolja sorszámot, de úgy, hogy egyszerre lekér egy nagyobb tartományt diszkről, és azt osztogatja ki, tisztán memóriában. Mikor elfogyott a tartomány, akkor megint növel egy nagyobb harapást rajta, kiírja a diszkre, és elkezdi újra memóriából osztani. Mi történik, ha elhasal a szerver? Csak annyi, hogy pár sorszám, ami még nem került kiosztásra a lekért tartományból kimarad, és elkezd egy új tartományt osztani a szerver. Luk lesz a sorszámokban, de ez nem okoz problémát, se az IDENTITY-nél, sem a SEQUENCE-nél nem építhetünk a folytonos sorszámokra, csak azt garantálják, hogy kétszer nem adják ki ugyanazt a sorszámot.
A SQUENCE létrehozásakor meg lehet adni, mekkora tartományt használjon cache-ként:

create sequence HumanResources.SequenceWithLargerCache
as int
start with 1
increment by 1
cache 100;

Kis cache esetén kicsi a valószínűsége a luknak, nagy cache esetén gyorsabb a sorszámosztás. Értelemszerűen intenzív SEQUENCE használó programoknál érdemes nagy cache értéket használni, de érdekes módon pár 10-es méret után már nem érhetünk el jelentős nyereséget.
A SEQUENCE-eket nem csak egyesével lehet léptetni, például a korábban leírt HiLo id generátor adatelérő stratégia egyszerre pár száz vagy ezer sorszámot kér magának, amivel aztán maga gazdálkodik. Tetszőleges számú előreléptetést így kell kérni:

declare @RangeFirstValue sql_variant ;
declare @RangeLastValue sql_variant ;

exec sp_sequence_get_range
@sequence_name = N'HumanResources.CounterSeq'
, @range_size = 50
, @range_first_value = @RangeFirstValue output
, @range_last_value = @RangeLastValue output;

SELECT @RangeFirstValue, @RangeLastValue;

Kimenet: 2336 2385

A SEQUENCE egyébként más adatbázisokról való áttéréskor (Oracle, Firebird) az egyik legproblémásabb pont volt, SQL Server 2012-től ez is megoldódott.
A másik problémás pont a soronként működő triggerek, de ebben nem lépett előre a 2012.

2012.03.28.

SQL Server 2012 újdonságok – 1. File Table

Filed under: Adatbázisok,SQL Server,SQL Server 2012,Szakmai élet — Soczó Zsolt @ 13:38

Aki még emlékszik rá, valamikor 2004 táján volt egy olyan ötlet, hogy az NTFS valamiféle adatbázis irányba megy el, így a Vista mögött WinFS néven lesz valami hibrid tároló. Még demóztam is annak idején, de aztán nem lett belőle semmi.
Most, 2012-ben az FileTable SQL Server újdonságot látva WinFS feelingem támadt. Lássuk, mi ez?

Nagy fájlok és a relációs adatok együttes tárolása gyakran visszatérő feladat. Pl. egy website fényképeket publikál, amelyekhez le kell tárolni a fényképeket mint nagytömegű bináris adatot, és hozzá tucatnyi metaadatot a kép méretéről, készítőjéről, stb. A metaadatok könnyen letárolhatók relációs sémában, de a fájl adatok mindig is problémát jelentettek, mivel a relációs adatbáziskezelők nem nagyméretű adatok tárolására tervezettek, hanem sok kicsire.
Az SQL Server 2008-ban bevezetett FILESTREAM attribútum segítségével már tudtunk nagytömegű adatokat hatékonyan tárolni, amely látszólag adatbázisban tárolja az adatokat, valójában fájlrendszerben, a tranzakcionális épség megtartása mellett. Az így tárolt adatokat el lehet érni SQL SELECT paranccsal is, mintha relációs adat lenne, de tranzakcionális fájl apin keresztül is, amely hatékonyabb elérést biztosít. Utóbbi viszont csak tranzakció megnyitása után engedélyezte a kipublikált fájl megosztás elérését, így azt azt az arre fel nem készített alkalmazások közvetlenül nem tudták elérni fájl apival (CreateFile függvény, .NET FileStream osztály, stb.).
Az SQL Server 2012-ben bevezetett FileTable a FILESTREAM funkciót fejleszti tovább. Belül FILESTREAM alapon látszólag táblában, valójában fájlokban tárolja az adatokat, kívülről viszont teljesen úgy lehet elérni őket, mintha egy fájlmegosztás fájljait és könyvtárait látnánk. A FileTable adatok eléréséhez nem kell (de lehet) tranzakciót nyitni, így közönséges módon, mintha tényleg fájlokat látnánk elérhetőek az adatok.
Közelebbről, a FileTable valamely sora egy fájlt vagy egy könyvtárat képes tárolni. Az adatok a fájlrendszerhez hasonlóan hierarchikusan vannak elrendezve. Belül contraintekkel és triggerekkel biztosítják, hogy az igazi fájlrendszerhez hasonlóan működjön. 10 fájl attribútumot is tárolnak (hidden, readonly, stb.). Tartalmaz egy fájl típust azonosító oszlopot is, hogy a FullText kereső tudja, milyen tartalom van benne (rtf, doc, stb.)
SQL szemszögből az adatok elérhetők SQL parancsokkal is, és a backup/restore is kezeli, azaz teljesen integráns része lett az SQL Servernek, míg kívülről teljesen az az élményünk, hogy fájlrendszert látunk.
Nézzünk egy egyszerű példát FileTable létrehozására:

alter database AdventureWorks2012
ADD FILEGROUP FsGroup CONTAINS FILESTREAM;

alter database AdventureWorks2012
ADD FILE (NAME = Fs, FILENAME = 'c:\data\filestream1')
TO FILEGROUP FsGroup;

ALTER DATABASE AdventureWorks2012
SET FILESTREAM (NON_TRANSACTED_ACCESS = FULL, DIRECTORY_NAME = N'gyoker');

CREATE TABLE FtStore AS FileTable
WITH (FileTable_Directory = 'Ft',
	FileTable_Collate_Filename = Latin1_General_100_CI_AS);

Intézőben közönséges megosztásként látszik a FileTable, létrehozhatunk benne könyvtárakat és fájlokat:

File Table in Windows Explorer

File Table in Windows Explorer

Így érhető el a FileTable SQL oldalról:

SELECT * FROM [AdventureWorks2012].[dbo].[FtStore];

Az eredményhalmaz ketté van vágva a könnyebb olvashatóság kedvéért:

FileTable from SQL side

FileTable from SQL side

FileTable from SQL side

FileTable from SQL side

A FILESTREAM vagy FileTable adatokat igazán hatékonyan a megosztáson keresztül, fájlkezelő műveletekkel lehet jól kiaknázni, mivel ilyenkor az adatok nem kerülnek be a Buffer Cache-be, így nem tolják ki onnan a már cache-elt kisebb relációs adatokat.

Egyetlen fura pont van csak az egészben, hogy notepadban nem lehet megnyitni és elmenteni a FileTableben tárolt szövegeket, mert a notepad Memory Mapped File apival nyitja meg a szöveget, amit nem támogat a FileTable redirectora… Csak sima CreateFile api vagy .NET-ből a FileStream az, amivel meg lehet nyitni a benne levő dolgokat. A WordPad például nem trükközik, nem használ Memory Mapped File-t.

A várakozás vége

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

Kedves Blogolvasóim!

Örömmel jelentem, hogy vége a hallgatásnak. :) Március elején véget ért az a munka, amely minden erőmet és időmet elvitt, így újra lesz időm blogolni. Ami miatt ez most bizonyosabb, hogy áprilisban lesz egy konferencia, amin az SQL Server 2012-ről fogok beszélni (az, hogy ezt elvállaltam is jelzi, hogy lett újra időm élni), és ehhez írtam 22 oldalnyi anyagot és sok példakódot, amelyet apránként bedolgozok ide a blogba is. Az elsőt ma tolom is be.

Jó tanulást, köszönöm a kitartást. :)

Powered by WordPress