Sunday, November 20, 2005

SS2K/SS2K5 vs Oracle part 1

This post will be in estonian as I wrote it in estonian and although I thought that this blog might be a english one for me. Still, I think when I have the energy, I'll translate it into english. For the record, it's about the MVCC and the locking.


Jutustan teile lukkudest ja versioonimisest.
Et saada aru SSi ja Oraakli lukustuspoliitikast, teeme ühe näite selle kohta läbi.

Olgu meil tabelid inimestega ja nende ostudega. Sisestagem alustuseks sinna mõned väärtused.
Märkus: panin nimele VARCHARi, mitte klassikalise VARCHAR2, et oleks MSSQLis vähem editida, kuid hiljem selgus, et seal pole NUMBER tüüpi ja "REFERENCES inimene" tuleb pisut pikemalt lahti kirjutada.. :P Aga see vahememmo.

martti@oracle1> create table inimene
2 (inimene_id int primary key,
3 nimi varchar(50),
4 saldo number
5 );


Table created.

martti@oracle1> create table ost
2 (ostu_id int primary key,
3 inimene_id references inimene,
4 hind number
5 );

Table created.

martti@oracle1> insert into inimene (inimene_id,nimi) values (1,'Martti');

1 row created.

martti@oracle1> insert into inimene (inimene_id,nimi) values (2,'Pärtel');

1 row created.

martti@oracle1> insert into ost (ostu_id, inimene_id, hind) values (100,1,11000);

1 row created.

martti@oracle1> insert into ost (ostu_id, inimene_id, hind) values (101,1,-27000);

1 row created.

-- Nüüd uuendagem isiku 1 saldoseisu.

martti@oracle1> update inimene set saldo =
2 (select sum(hind) from ost where ost.inimene_id=inimene.inimene_id)
3 where inimene.inimene_id=1;

1 row updated.

-- Olgu meil algseis selline:

martti@oracle1> select * from ost;

OSTU_ID INIMENE_ID HIND
---------- ---------- ----------
100 1 11000
101 1 -27000


martti@oracle1> select * from inimene;

INIMENE_ID NIMI SALDO
---------- -------------------------------------------------- ----------
1 Martti -16000
2 Pärtel


martti@oracle1> commit;

Commit complete.


-- Ok. Seni oli kõik ilus ja lihtne.
-- Võtame nüüd kaks konkureerivat sessiooni.
-- Selguse mõttes on üks oracle1 ja teine oracle2.

-- Pärtel sai millegipärast 19k.
oracle1>insert into ost(ostu_id, inimene_id, hind)
2 values (102,2,19000);

1 row created.

-- Teises sessioonis tuleb ntx teine teller ja vaatab, et oli
-- esimese ülekande valele inimesele omistanud..

oracle2>update ost set inimene_id = 2 where ostu_id = 100;

1 row updated.

-- Teise sessiooni tegelane mõtleb, et saab veel tehtud jama kiirelt
-- heaks teha ja uuendab mõjutatud inimeste saldod üle.

oracle2>update inimene set saldo =
2 (select sum(ost.hind) from ost
3 where ost.inimene_id = inimene.inimene_id)
4 where inimene.inimene_id in (1,2);

2 rows updated.

-- Esimeses sessioonis tahab teller Pärtli tehingu tõttu samuti saldot
-- uuendada, kuid ta ei saa - jääb ootele, kuna teine sessioon on
-- enda poolt uuendatud väljad lukustanud kuni transaktsiooni lõpuni.

oracle1>update inimene set saldo =
2 (select sum(ost.hind) from ost
3 where ost.inimene_id = inimene.inimene_id)
4 where inimene.inimene_id = 2;


-- Teine teller lõpetab transaktsiooni.

oracle2>commit;

Commit complete.

-- Esimene sessioon ütleb meile koheselt luku kadudes nüüd:

oracle1> 1 row updated.

-- Lõpetame ka esimese sessiooni transaktsiooni.

oracle1>commit;

Commit complete.


-- Vaatame tulemusi:

oracle1>select * from ost;

OSTU_ID INIMENE_ID HIND
---------- ---------- ----------
100 2 11000
101 1 -27000
102 2 19000

oracle1>select * from inimene;

INIMENE_ID NIMI SALDO
---------- -------------------------------------------------- ----------
1 Martti -27000
2 Pärtel 19000

-- Mis siis nüüd toimus?
-- Nimelt esimene sessioon kandis Pärtlile 19k üle.
-- Teine sessioon kandis üle 11k Martti->Pärtel.
-- Nüüd mõlemad sessioonid uuendasid hoolega saldosid, kuid lõppeks jäi vale seis,
-- kuna viimasena uuendas esimene sessioon, mis ei näinud teise sessiooni uuendust.
-- See demonstreerib tähtsat käitumuslikku aspekti: mõlemad sessioonid tahavad uuendada
-- samu ridu ja loevad tulemuse mällu. Üks jääb lihtsalt ootama, millal teise transaktsioon
-- on läbi ja uuendab siis mälus olevate väärtustega endale vajaliku rea.

SS2k-s sama asi.
Kasutan lihtsuse mõttes osql-i.

osql -S MARTTI\SS2K -d pubs -E

create table inimene
(inimene_id int primary key,
nimi varchar(50),
saldo bigint
);
go

create table ost
(ostu_id int primary key,
inimene_id int
foreign key references inimene(inimene_id)
ON DELETE NO ACTION,
hind bigint
);
go

insert into inimene (inimene_id,nimi) values (1,'Martti');

insert into inimene (inimene_id,nimi) values (2,'Pärtel');

insert into ost (ostu_id, inimene_id, hind) values (100,1,11000);

insert into ost (ostu_id, inimene_id, hind) values (101,1,-27000);

-- Nüüd uuendagem isiku 1 saldoseisu.

update inimene set saldo =
(select sum(hind) from ost where ost.inimene_id=inimene.inimene_id)
where inimene.inimene_id=1;


-- Olgu meil algseis selline:

1> select * from ost;
2> go
ostu_id inimene_id hind
----------- ----------- --------------------
100 1 11000
101 1 -27000

(2 rows affected)

1> select * from inimene;
2> go
inimene_id nimi saldo
----------- ------------------------- ----------------
1 Martti -16000
2 Pärtel NULL

(2 rows affected)


-- Ok. Seni oli kõik ilus ja lihtne.
-- Võtame nüüd kaks konkureerivat sessiooni.
-- Selguse mõttes on üks ss2k1 ja teine ss2k2

-- Kuna SS2K-s commititakse automaatselt pärast iga DML-i,
-- tuleb transaktsiooni lahti hoidmiseks kasutada abi.
ss2k1>BEGIN TRANSACTION
ss2k1>go

ss2k2>BEGIN TRANSACTION
ss2k2>go

-- Pärtel sai millegipärast 19k.
ss2k1>insert into ost(ostu_id, inimene_id, hind)
values (102,2,19000);

(1 row affected)

-- Teises sessioonis tuleb ntx teine teller ja vaatab, et oli
-- esimese ülekande valele inimesele omistanud..

ss2k2>update ost set inimene_id = 2 where ostu_id = 100;

(1 row affected)

-- Teise sessiooni tegelane mõtleb, et saab veel tehtud jama kiirelt
-- heaks teha ja uuendab mõjutatud inimeste saldod üle.

ss2k2>update inimene set saldo =
(select sum(ost.hind) from ost
where ost.inimene_id = inimene.inimene_id)
where inimene.inimene_id in (1,2);

-- Nüüd SS2K-s jääb asi ootele, kuna esimene sessioon parajasti
-- uuendas ühte kirjet.


-- Esimeses sessioonis tahab teller Pärtli tehingu tõttu samuti saldot
-- uuendada, kuid ta ei saa - jääb ootele, kuna teine sessioon on
-- enda poolt uuendatud väljad lukustanud kuni transaktsiooni lõpuni.

ss2k1>update inimene set saldo =
(select sum(ost.hind) from ost
where ost.inimene_id = inimene.inimene_id)
where inimene.inimene_id = 2;

-- Nüüd mis juhtub. Mõlemad tahavad uuendada saldosid isikul 2.
-- Samas teise sessiooni uuendus oli juba lukus. Ja nüüd on seda
-- ka esimese uuendus. Sellist olukorda kutsutakse surmalukuks
-- ehk siis (välja-)maakeeli deadlock.
-- Kuidas käitub nüüd SQL Server? Ta valib randomiga ühe transaktsiooni
-- välja ja rullib kogu asja tagasi. Kõik muudatused kaovad.
-- Väljastatakse järgmine teade tagasirullumise kohta:

Msg 1205, Level 13, State 50, Server MARTTI\SS2K, Line 1
Transaction (Process ID 52) was deadlocked on {lock} resources with another
process and has been chosen as the deadlock victim. Rerun the transaction.

-- Lõpetame transaktsiooni ja mõlemad sessioonid näevad üht-sama
ss2k1> commit transaction
go

1> select * from ost;
2> go
ostu_id inimene_id hind
----------- ----------- --------------------
100 1 11000
101 1 -27000
102 2 19000

1> select * from inimene;
2> go
inimene_id nimi saldo
----------- ------------------- -----------
1 Martti -16000
2 Pärtel 19000


-- Seega võib öelda, et kuna Pärtel kasutaks SS2K-d, sain ma
-- puhta muidu 11k tema kulul :). (Omaette teema on muidugi, et kuidas
-- üldse oleks taoline kala tekkida, kuid näite mõttes kõlbab.)

Näide ei tulnud võibolla sellepärast kõige parem, et pidin justkui "sunniviisiliselt"
kasutama transaktsiooni loomiseks SS2K-s abi, kuid Oracles just nii transaktsioonid
töötavadki (võiks öelda, et transaktsioonid üldiseltki, ANSI järgi).
SS2K-s on autocommit peal seetõttu, et vastasel korral jääksidki asjad lukku ja
deadlockid oleks tavaline.
Püüdsin olukorda sarnaselt simuleerida.
Samuti on tihti vaja ka SS2K-s luua sarnaselt transaktsioone, sest mõned
muudatused peavad käima järjest. Seetõttu kui käsitsi taoliselt paar muudatust teha,
ei tule transaktsiooni kasutamise vajadus just väga teravalt esile.
(Oracles oleks pidanud SS2K autocommiti simuleerima sel juhul lihtsalt
iga DML-i taha "commit;" pannes, kuid atomaarse transaktsiooni
nõuete korral poleks see võimalik olnud.)

Kui oleks jätnud sarnaselt defaultina SS2K-s oleva autocommiti peale,
oleks siin aga tõusnud küsimuseks järgmine probleem, milleks on
erinevate uuenduste (UPDATE), täienduste (INSERT), kustutamiste (DELETE) ja
pärimiste (SELECT) tõttu pandavad lukud. Kusjuures oluline on just, et
SS2K paneb luku isegi SELECTi tehes.
Reaalses elus ei tehta kalli raha eest süsteeme ühele kasutajatele, reeglina..
Mõtle nüüd kui kümme/sada inimest tahavad tabelist korraga andmeid - tabel on lukus ja
kordamööda saadakse andmeid. Mõtle nüüd, kui keegi tahaks ühte veergu samal ajal
pisut uuendada - ta ei saaks seda teha, kuna tabel on tühipalja SELECT * -pärast lukus.
Korruta seda veel 10, 100, 1000.. jne kordadega ja sa saad täiesti hullumeelselt
eskaleeruvad rea- ja tabeli-taseme lukud.

SS2K-st backupi tegemine on selle pärast arvatavasti väga piinarikas (arvan).

Üks võimalus on ka lubada räpast lugemist ehk dirty read-i, mis sisuliselt tähendab,
et ei hoolita lukkudest ja loetakse kasvõi tabelit, mille uuendus on poole peal.
(Huvitav, kas see tähendab, et ntx poolte inimeste saldo on kalkuleeritud ja
teistel on pooleli? Arvan, et just seda see tähendakski.)
Igaljuhul räpane värk sellisel juhul.


Lühidalt: SS2K-s on lukud ja versioonimine implementeeritud täiesti erinevalt Oracle-st
ja see tekitab tihtipeale tõsiselt peavalusid. Räpane lugemine vs eskaleeruvad lukud. Pole
just parim valik. Näen võimalikke probleeme backuppimisel (mis on lugemine, mis samuti lukustab)
ja segamini uuendamisel, kus deadlocki tekkides ainult üks transaktsioon läbi läheb.
Ülejäänud peavad "uuesti üritama". Samas, kui tegu on protseduuriga, mis ühe korra uuendabki,
siis ei tule keegi "uuesti üritama" ja need muudatused ongi kadunud baasi jaoks.

Microsofti asjadega on väga lihtne lihtsaid asju teha (loe: õppekursusel näidatud
lihtsaid näiteid järgida), kuid reaalne elu on tihtipeale pisut teine.

Aga võibolla siin on tegu maailmavaatelise erinevusega?
Võibolla mõnedele ei olegi oluline kvaliteet, vaid muud asjad. Ühed nauduvad kvaliteetkultuuri,
kuid suuremjagu lepivad raadio-/telemüraga, kuna nii on lihtsam ja kiirem.


PS
Kui olin eelneva osa valmis kirjutanud, siis kõrvalmasinas installisin parajasti
SQL Server 2005-te, et proovida sama asja sellel. Kui lõpuks mahuka installiga maha sain,
siis läksin lootustandvalt "Server Management Studiosse" ja tegin sama asja läbi.
Ei midagi uut siin ilmas. Teade oli vist natike teistsugune. Polnud servu nime küljes.

Msg 1205, Level 13, State 51, Line 1
Transaction (Process ID 51) was deadlocked on lock resources with another process and
has been chosen as the deadlock victim. Rerun the transaction.


PPS
Mind ootas esialgu installimisel ees vägagi kole üllatus - nimelt nõuab isegi SS2K5
custom install, et vindil nimega C: oleks ~1.6GB vaba ruumi, kuigi ma kõik võimalikud
komponendid olin suunanud teisele partitsioonile. (Mistõttu ühele masinale mul ei õnnestunudki SS2K5 peale installida - ma ei saa nii palju vaba ruumi ühe näpuklõpsuga peapartitsioonil.)
Tegu on evaluation copyga, mistõttu sügavalt loodan, et selline jube viga on
tavariliisis ära parandet. Kus seda enne on nähtud, et ma ei saa valida programmi
installikataloogigi - custom mode'is?