Slinkums jeb predikātu neizpilde

Šoreiz nebūs runa par cilvēcisko slinkumu, bet par datubāžu slinkumu. Parasti viens no galvenajiem mērķiem datubāzes darbībā ir, lai tā visus SQL teikumus apstrādātu pēc iespējas ātrāk. Tiecoties pēc šī mērķa, tiek veikti daudzi un dažādi uzlabojumi, tai skaitā arī daži vienkāršākie un vēsturiski senākie – datubāzes beidz SQL teikuma kritēriju (predikātu) pārbaudi tiklīdz rezultāts ir skaidrs atlikušos nemaz nerēķinot.
Paši vienkāršākie piemēri ir šādi:

  • ja vairāki predikāti ir apvienoti ar loģisko UN (AND), tad tiklīdz neizpildās (ir aplams) viens no tiem, nākošo vērtības vairs nav svarīgas, jo viss izteikums ir aplams;
  • ja vairāki predikāti ir apvienoti ar loģisko VAI (OR), tad tiklīdz izpildās kaut viens no tiem (ir patiess), nākošo vērtības vairs nav svarīgas, jo viss izteikums ir patiess.

SQL teikumā loģiskā UN gadījumā tas izskatītos šādi:

SELECT <lauku saraksts>
FROM <tabulas>
WHERE <nosacījums1> AND <nosacījums2>

Šai gadījumā tiklīdz nosacījuma1 rezultāts izrādās aplams, tā nosacījums2 nemaz netiek pildīts. Tiesa gan vispārīgā gadījumā jāņem vērā, ka kārtība, kādā nosacījumi tiek pārbaudīti, jeb kāds ir precīzais SQL teikuma izpildes plāns, ir atkarīga no daudziem un dažādiem faktoriem.
Tie var būt:

  • datu sadalījums,
  • indeksu esamība,
  • predikātu sarežģītība (vai pareizāk izsakoties, cik sarežģīti tie liekas datubāzei), piemēram, funkciju esamība predikātā,
  • vides uzstādījumi utt.

Attiecīgi iepriekš nav iespējams droši paredzēt, kādā kārtībā šie nosacījumi tiks pildīti. Tas diemžēl reizēm var novest pie sliktām sekām.
Biežākais slikto seku scenārijs mēdz būt kļūdas, kuras te it kā parādās, te pazūd. Ja kāds no nosacījumiem izpildās kļūdaini un tiek izpildīts tikai reizēm, tad līdz tā izpildei var nemaz nenonākt un kļūda nebūt. Bet, ja līdz tā izpildei nonāk, tad ir kļūda.
Mazs piemērs izmantojot Oracles visslavenāko tabulu DUAL. Lasītājiem, kas nezin tās saturu, tas ir šāds:

SQL> select * from dual;
D
-
X

Pieņemsim, ka man ir 2 predikāti, kas apvienoti izmantojot loģisko VAI. Tātad, tiklīdz pirmais predikāts ir patiess, tā tālāk izpildīt SQL teikumu vairs nav vērts, jo rezultāts ir skaidrs. Taču man otrajā predikātā ir kļūda – dalīšana ar nulli. Skatamies, kas notiek:

SQL> select * from dual where 1=1 or 1/0 > 1;
D
-
X
SQL> select * from dual where 1=2 or 1/0 > 1;
select * from dual where 1=2 or 1/0 > 1
                                 *
ERROR at line 1:
ORA-01476: divisor is equal to zero

Redzam, ka pirmajā gadījumā, kad predikāts 1=1 ir patiess, līdz dalīšanai ar nulli nemaz nenonāk un ieraksts tiek attēlots. Savukārt otrā gadījumā ir kļūda – dalīšana ar nulli. Protams, ka reālajā dzīvē Jums predikāti būs krietni sarežģītāki un kļūdas ne tik acīmredzamas, taču ideja paliek tā pati.
Otrs gadījums, kas paskaidrots tālāk, ir vēl potenciāli krietni nepatīkamāks. Skatījumi ir visai izplatīts līdzeklis, lai nodrošinātu lietotājiem pieeju tikai viņiem redzamajiem datiem. Drošības risinājuma ideja balstās uz to, ka uz datu īpašnieka (datubāzes lietotāja) bāzes tabulām tiek izveidoti vairāki skatījumi un citiem datubāzes lietotājiem tiek dotas tiesības tikai uz skatījumiem nevis bāzes tabulām. Atkarībā no skatījumā pielietotā kritērija lietotājam ir iespēja kādu ierakstu redzēt vai nē.
Diemžēl tiekšanās pēc maksimālas ātrdarbības un nosacītais slinkums šo pieeju reizēm var vismaz daļēji sabotēt – ir gadījumi, kad lietotājs var noskaidrot vismaz faktu vai ieraksts eksistē vai nē.
Skatāmies piemēru. Pieņemsim, ka mums ir tabula klienti (bāzes tabula) – piemēram kredītu nemaksātāji bankās, tā teikt “melnais saraksts”🙂 Bāzes tabulā nosacīti ir plaša info un savukārt ir otra tabula atļautie klienti, kurā tiek ievietoti tikai tie klienti, kas redzami citiem lietotājiem. Tiek izveidots skatījums, kas attēlo tikai informāciju par tiem klientiem, kas pieejami citiem lietotājiem un  citam lietotājam iedotas tiesības to redzēt. Protams, ka reālā dzīvē skatījumi būs daudzi un nosacījumi iespējams sarežģītāki, bet ideja nemainās.
Ar īpašnieka lietotāju es izpildu šādus SQL teikumus:

CREATE TABLE klienti (
  kln_id NUMBER NOT NULL PRIMARY KEY,
  kln_vards VARCHAR2(20) NOT NULL,
  kln_uzvards VARCHAR2(20) NOT NULL);
CREATE INDEX kln_uzvards_vards
  ON klienti (kln_uzvards, kln_vards);
CREATE TABLE atlautie_klienti (
  mln_vards VARCHAR2(20) NOT NULL,
  mln_uzvards VARCHAR2(20) NOT NULL);
INSERT INTO klienti values (1, 'JĀNIS', 'BĒRZIŅŠ');
INSERT INTO klienti values (2, 'PĒTERIS', 'KRŪMIŅŠ');
INSERT INTO atlautie_klienti values ('JĀNIS', 'BĒRZIŅŠ');
CREATE VIEW mani_klienti AS
SELECT * FROM klienti
WHERE (kln_vards, kln_uzvards) IN (
  SELECT mln_vards, mln_uzvards
  FROM atlautie_klienti);
GRANT SELECT ON mani_klienti TO GINTS1;

Kā redzams no augstāk esošajiem SQL teikumiem, tad ir 2 personas melnajā sarakstā – Jānis Bērziņš un Pēteris Krūmiņš, taču publiski pieejama informācija ir tikai par Jāni Bērziņu. Par Pēteri Krūmiņu informācija uz āru netiek dota, nav nekas zināms, vai viņš ir melnajā sarakstā, vai nē.
Tālāk pieslēdzos kā lietotājs gints1 un mēģinu ieraudzīt kādi klienti eksistē. Diemžēl tiesības uz bāzes tabulu lietotājam gints1 nav, kā redzams zemāk:

SQL> select * from gints.klienti;
select * from gints.klienti
*
ERROR at line 1:
ORA-00942: table or view does not exist
Bet viņš var redzēt sev iedoto skatījumu mani_klienti:
SQL> select * from gints.mani_klienti;
KLN_ID     KLN_VARDS            KLN_UZVARDS
---------- -------------------- -----------
1          JĀNIS                BĒRZIŅŠ

Bet lietotājam gints1 ļooooooooooooti interesē, vai Pēteris Krūmiņš un Žanis Ābele ir melnajā sarakstā, vai nav. Kā redzams no skatījuma uzzināt to nevar. Ja pieliekam papildus nosacījumus, tad arī, protams tie neparādās:

SQL> SELECT * FROM gints.mani_klienti
2  WHERE kln_vards = 'PĒTERIS'
3    AND kln_uzvards = 'KRŪMIŅŠ';
no rows selected
SQL> SELECT * FROM gints.mani_klienti
2  WHERE kln_vards = 'ŽANIS'
3    AND kln_uzvards = 'ĀBELE';
no rows selected

Taču skatamies, kas notiek tad, ja vaicājumus mazliet papildinam:

SQL> SELECT * FROM gints.mani_klienti
2  WHERE kln_vards = 'PĒTERIS'
3    AND kln_uzvards = 'KRŪMIŅŠ'
4    AND length(kln_uzvards)/0 >10;
AND length(kln_uzvards)/0 >10
*
ERROR at line 4:
ORA-01476: divisor is equal to zero
SQL> SELECT * FROM gints.mani_klienti
2  WHERE kln_vards = 'ŽANIS'
3    AND kln_uzvards = 'ĀBELE'
4    AND length(kln_uzvards)/0 >10;
no rows selected

Opā!!!! Vaicājumu rezultāti ir atšķirīgi! Pirmais izgāzās ārā ar kļūdu, bet otrais saka, ka ierakstu nav. Ko mēs no tā varam spriest? To, ka acīmredzot otrajā gadījumā tabulā ieraksta par Žani Ābeli patiešām nav, bet Pēteris Krūmiņš ir gan, jo izpilde nonāca līdz kļūdai.
Kāpēc tad galu galā tā sanāca? Te jāsaprot, kas tad ir skatījums. Tas faktiski ir SQL teikums, kuram piešķirts nosaukums un kas saglabāts datubāzē. Tajā brīdi, kad vaicājumā ir atsauce uz skatījumu, saglabātais SQL teikums tiek ielikts nosaukuma vietā un kopējais radītais SQL teikums analizēts kā viens vesels. Šai gadījumā galīgais SQL teikums, ko apstrādā datubāze izskatās šādi:

SELECT "KLIENTI"."KLN_ID" "KLN_ID",
  "KLIENTI"."KLN_VARDS" "KLN_VARDS",
  "KLIENTI"."KLN_UZVARDS" "KLN_UZVARDS"
FROM GINTS."ATLAUTIE_KLIENTI" "ATLAUTIE_KLIENTI",
     GINTS."KLIENTI" "KLIENTI"
WHERE "KLIENTI"."KLN_VARDS"='PĒTERIS'
AND "KLIENTI"."KLN_UZVARDS"='KRŪMIŅŠ'
AND LENGTH("KLIENTI"."KLN_UZVARDS")/0>12
AND "KLIENTI"."KLN_VARDS"="ATLAUTIE_KLIENTI"."MLN_VARDS"
AND "KLIENTI"."KLN_UZVARDS"="ATLAUTIE_KLIENTI"."MLN_UZVARDS"

Tā izpildes plāns ir šāds:

----------------------------------------------+
| Id  | Operation           | Name            |
----------------------------------------------+
| 0   | SELECT STATEMENT    |                 |
| 1   |  HASH JOIN SEMI     |                 |
| 2   |   TABLE ACCESS FULL | KLIENTI         |
| 3   |   TABLE ACCESS FULL | ATLAUTIE_KLIENTI|
----------------------------------------------+
Predicate Information:
----------------------
1 - access("KLN_VARDS"="MLN_VARDS" AND "KLN_UZVARDS"="MLN_UZVARDS")
2 - filter(("KLN_VARDS"='PĒTERIS' AND "KLN_UZVARDS"='KRŪMIŅŠ'
  AND LENGTH("KLN_UZVARDS")/0>12))
3 - filter(("MLN_UZVARDS"='KRŪMIŅŠ' AND "MLN_VARDS"='PĒTERIS'))
Saskaņā ar izpildes plānu vispirms tiek pildīts 2 solis, kā redzams tajā ir filtra predikāts
"KLN_VARDS"='PĒTERIS' AND "KLN_UZVARDS"='KRŪMIŅŠ'
  AND LENGTH("KLN_UZVARDS")/0>12

Tā kā tiek atrasts ieraksts kuram izpildās, ka uzvārds ir Krūmiņš un vārds Pēteris, tad nākas pārbaudīt arī kļūdu izraisošo predikātu LENGTH(“KLN_UZVARDS”)/0>12. Savukārt gadījumā ar Žani Ābeli, jau sākumā izrādās, ka nav ieraksta ar tādu vārdu, tāpēc līdz kļūdu izraisošajam predikātam datubāze nemaz netiek.
Cilvēkiem, kuriem interesē, kā var izrakt, kāds ir SQL teikums, ko Oracle datubāze optimizē – šai gadījumā ir jāizmanto sesijas trasēšana izmantojot notikumu (event) 10053. Kā to darīt un cita papildus info par šo eventu rodama šajā izvērstajā dokumentā.
Dotais piemērs, protams, nenozīmē, ka tagad visas idejas par datu drošību izmantojot skatījumus ir jāmet miskastē, jo ir vairāki nosacījumi, lai vispār pie šāda SQL teikuma izpildes tiktu, ne vienmēr izpildes plāns būs tads, kas atklās slēpto info un pie tam galu galā neko ļoti daudz jau uzzināt nevar. Bet faktu ir vērts paturēt prātā.

Kopsavilkums par predikātu neizpildi

Augstāk redzamie piemēri bija Oracle datubāzē, taču neievērojot sintaktiskas atšķirības, šādas problēmas Jūs varētu sagaidīt jebkurā DBVS, jo predikātu neizpilde līdz galam, ja zināms rezultāts, ir plaši izmantota prakse. Galu galā vairumā gadījumu, tas mums ietaupa ne vienu vien dzīves mirkli, kas jānosēž mazāk pie datora un ko var izmantot, lai iepazītu, piemēram, sniegoto dabu

Sarma

Sarma

vai uzceltu kādu sniegavīru🙂

Sniegavīri

Sniegavīri

Komentēt

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Mainīt )

Twitter picture

You are commenting using your Twitter account. Log Out / Mainīt )

Facebook photo

You are commenting using your Facebook account. Log Out / Mainīt )

Google+ photo

You are commenting using your Google+ account. Log Out / Mainīt )

Connecting to %s

%d bloggers like this: