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.
Could you hire me? Contact me if you like what I’ve done in this article and think I can create value for your company with my skills.
LEAVE A COMMENT
9 COMMENTS
Én ezt nem értem, vagy szelektíven olvastam.
A kliens lekér egy sorszámot, sőt egy egész tartományt, aztán áttol a szerveroldalra 1 vagy 1000 számlát fejlécestül-tételtáblástúl?
Mi van, ha sok kliens sok számlát állít ki párhuzamosan, aztán sok kliens közül jópár elhasal, miközben a többinek meg sikerül a mutatvány?
Lukas lesz a szekvencia?
Vagy tényleg szelektíven olvastam, és mégsem a kliens bűvészkedik a sorszámokkal, hanem egy köztesréteg? Dehát a köztesréteg egyik létjogosultsága a teljesítményigény, azaz ahol van köztesréteg, ott a köztesréteg leginkább többszereplős, tehát ami a köztesréteg nélküli klienseknél probléma, az probléma a köztesrétegben is, azaz szintés lukas lesz a szekvencia, ha valamelyik köztesrétegkiszolgáló-példány hasal el.
Szóval nekem sántít a példa, vagy csak szelektíven olvastam tényleg.
Én mindenesetre maradok a bevált megoldásnál: szekvenciát az erre kijelölt sp generál, amit tranzakcióból hív a számlabeszúró sp; a számlabeszúró sp-t meg a kliens hívja, átadva neki a fejlécet és a tételeket tartalmazó táblát. És igen, az sp pillanatok alatt beszúrja a fejlécet, közben visszakapja a sorszámot a SQL Servertől, azzal meg beszúrja a tételeket, aztán kész. Mindez pillanatok alatt.
Szóval nekem ez a sequence egy nagy szemfényvesztés, eddig is megvolt a szakma nélküle, és ezután is meglesz. Szerintem a Microsoft kínjában csinálta meg, hogy elmodhassa: ilyen is van, akinek erre van ingerenciája.
“az sp pillanatok alatt beszúrja a fejlécet, közben visszakapja a sorszámot a SQL Servertől,”
Itt egy kis szereptévesztést vittem véletlenül a szövegbe. Természetesen aem a SQL Server generálja ez esetben a sorszámot, hanem a szekveciát előállító sp, mait a számlabeszúró sp meghív, és az attól kapott sorszámmal besorszámozza a fejlécet és a tételeit.
Ha lukmentes számlasorszámozás kell, akkor nem tudok elképzelni mást, mint hogy áll a többi kliens a sorszámgeneráló sp nyilvántartó táblája előtt, amíg egy adott számla tranzakciója be nem fejeződik.
Ha nem követelmény a sorfolytonosság akkor az identity teljesen jó, és nem is kell a klienseknek várakozniuk a másik fejléc-teteltábla tranzakció befejeződésére, tehát sebességproblémát én nem látok ez utóbbi esetbe, és persze az előzőben sem, mert ott meg a sorfolytonosság miatt nem lehet mást csinálni.
Nem folytonos sorszámosztásról van szó, arra éppúgy nem jó a SEQUENCE mint az IDENTITY. Egyszerűen egyedi sorszámok generálásáról van szó, pont mint az IDENTITY-nél, pl. primary key céljára.
Akkor nyersz időt, ha EGY adott kliens vagy annak hatására működő DAL akar beszúrni egy komplex fát az adatbázisba, amiben TÖBB MINT EGY SZÜLŐ sor van. Nem minden alkalmazás arról szól, hogy van egy fejléc sor, meg pár gyerek, ebben az esetben identity-vel 2 batch, sequence-szel 1 batch, nem nagy nyereség. De bonyolultabb esetekben, amikor pl. egy OR mapper kiokád magából sok-sok DML műveleteket, akkor igen sokat lehet nyerni az inserteknél a körülfordulások eliminálása miatt, ha OR mapper képes SEQUENCE-eket használni.
Én inkább arra akartam rávilágítani, hogy semmi szükség nem volt erre az új szintaxsugörrel, mivel ezt eddig is problémamentesen meg lehetett csinálni; semmi másról nincs szó, mint aki hiányolta ezt a sugört, az most megkapta.
Ezt a két becst nem értem. Fogom a fejlécet a tételekkel, aztán átküldöm az adatbázisba, és kész. Ez egy becs.
Miért számolod ezt kettőnek?
Igazad van, egy fejléc + sok részletező sornál egy sp valóban be tudja szúrni a sort, én az OR mapperek miatt már nagyon sp nélkül, generált sqlben gondolkodok.
De mint hangsúlyozom, több mint egy szülő sornál van a SEQUENCE-nek jelentősége, mivel akkor már spvel is nehézkes (de nem lehetetlen) kevés batchben beszúrni a sorokat.
Eddig nem érintett meg az OR mapper, mint lehetőség, de azt gondolom, hogy a határidők miatt célszerű lenne áttérni rá.
Van esetleg valami jó linked? Nagyon tömör, jól érthető példákkal felszerelt bevezetőre gondolok.
EF 30 perces tutorial:
http://msdn.microsoft.com/en-us/library/bb399182.aspx
NHib:
http://nhforge.org/wikis/howtonh/your-first-nhibernate-based-application.aspx
http://danny-t.co.uk/index.php/tutorials/introduction-to-nhibernate-tutorial/
Thx!