Partie 14 : Attributs nullifiables
Lecture 6 min. âą
Table des matiĂšres
- Null Table
- Ajout des Value::Null
- Modification du check_values
- Implémentation de la null table
- Modification de Schema
- Testons !
- Conclusion
Les articles de la série
- Partie 1 : Construire le REPL
- Partie 2 : Sérialisation des données
- Partie 3 : Database
- Partie 4 : Tables
- Partie 5 : Parser les commandes, création de la boßte à outils
- Partie 6 : Parser les commandes SQL
- Partie 7 : Tuples de données
- Partie 8 : Contraindre les données via un schéma
- Partie 9 : Découper le stockage des tables en pages
- Partie 10 : Clefs primaires
- Partie 11 : Expressions Logiques SQL
- Partie 12 : Scans et filtrage
- Partie 13 : UTF-8 et caractÚres échappés
- Partie 14 : Attributs nullifiables
- Partie 15 : Index secondaires
- Partie 16 : Directive EXPLAIN
- Partie 17 : Logical Plan
- Partie 18 : Exécution du plan logique
- Partie 19 : Suppression d'enregistrements
- Partie 20 : Réindexation et compactation de tables
Bonjour Ă toutes et tous đ
Lorsque nous avons introduit les clef primaires, jâai dĂ©fini deux contraintes appliquable sur les colonnes.
- PRIMARY KEY
- NOT NULL
PRIMARY KEY
on sâen est dĂ©jĂ occupĂ©, NOT NULL
par contre, on lâa laissĂ© sur le bas cĂŽtĂ©.
Il est temps dây remĂ©dier.
Le but va ĂȘtre de pouvoir insĂ©rer de la donnĂ©es sans dĂ©finir toutes les colonnes systĂ©matiquement.
INTEGER NOT NULL PRIMARY KEY, name TEXT(50) NOT NULL, age INTEGER);
(id INSERT INTO Users (id, name, age) VALUES (1, 'Max', 20);
INSERT INTO Users (id, name) VALUES (2, 'Amy');
Letâs go !
Null Table
La pierre angulaire de notre systĂšme va ĂȘtre un concept que lâon nomme en informatique une null table.
Quâest-ce que NULL
, dâailleurs ?
NULL matĂ©rialise le concept dâabsence de donnĂ©es, pas un 0, pas un FALSE. Son absence.
Du coup stocker du vide, ça commence Ă ĂȘtre tendax, tout est des bytes, des 0 et des 1. On nâa pas de troisiĂšme Ă©tat qui permet de dĂ©finir le nĂ©ant.
On va diviser pour mieux régner.
On va définir une table qui dire si oui ou non la données est présente ou pas.
Prenons un exemple avec un tableau dâentiers possĂ©dant une valeur NULL.
[1, NULL, 3]
Si on veut sĂ©rialiser ça, nous allons devoir faire un choix de reprĂ©sentation du NULL, je vais arbitrairement dire que câest 0x0.
Ce qui nous donne.
0x1 0x0 0x2
Mais rien ne distingue pour le moment 0x0 dâun vrai 0 ou dâun NULL.
LâidĂ©e est de crĂ©er une table de vĂ©ritĂ© de nos donnĂ©es:
- 1 elle existe
- 0 non
Ce qui donne la table suivante:
[1, 0, 1]
Que lâon sĂ©rialise en
0x1 0x0 0x1 0x1 0x0 0x2
^ ^
null table data
On peut alors utiliser le 0x0
de la null table pour dĂ©terminer si câest du 0 ou du NULL.
Par contre, on double notre stockage. Câest pas tip-top.
Heureusement, nous nâavons pas explorĂ© toutes les possibilitĂ©s.
Lorsque lâon Ă©crit 0x5
Ce quâil faut lire câest
0b101
Et donc un tableau de 1 et de 0.
On peut compactifier notre null table avec un simple nombre.
0b101 0x1 0x0 0x2
^ ^
null table data
Question
Ok, mais comment Ă partir de
0x5
je suis sensé savoir que le deuxiÚme élément des données est NULL ?
Et bien grùce à deux opérations binaires extrÚmement optimisées par le CPU:
- le masque
- le décalage binaire
Le dĂ©calage binaire permet comme son nom lâindique de dĂ©caler un bit, il est symbolisĂ© par <<
quand il décale vers la gauche.
0b001 << 1 = 0b010
Ici on décale à gauche notre bit.
Note
Dans les faits câest une multiplication par 2^n
1 << 1 = 1 * 2 = 0b01 << 1 = 0b10 = 2 1 << 2 = 1 * 2^2 = 0b01 << 2 = 0b100 = 4
Mais contrairement à la puissance qui coûte trÚs cher en CPU, décaler des bits est évident pour le CPU car directement cùblé physiquement.
Le masque est également une opération cùblée dans le CPU qui permet de vérifier que deux bit sont égaux.
On utilise pour ça une opération de ET représenté par &
0b101
& 0b110
-----------
0b100
On peut alors fusionner les deux opérations.
null_table & (1 << bit)
Si on reprend notre exemple, cela donne:
0b101 // null_table
& 0b010 // 0b001 << 1
-----------
0b000 = 0x0
Par contre si on veut la premiĂšre valeur
0b101 // null_table
& 0b001 // 0b001 << 0
-----------
0b001 = 0x1
Ou la troisiĂšme
0b101 // null_table
& 0b100 // 0b001 << 2
-----------
0b100 = 0x4
Et donc notre algo est:
null_table & (1 << bit) == 0
Question
Compris, mais un byte ne comporte que 8 bits, on fait comment si notre tuple a plus de 8 attributs?
RĂ©ponse courte, on utilise 2 nombres au lieu dâun! đ
Réponse plus élaborée.
Pour ce set de data :
[NULL, 2, 3, NULL, 5, 6, 7, 8, 9, NULL]
On a 3 positions qui sont NULL:
- 0
- 3
- 9
Pour les position 0 et 3 pas de souci, câest notre null-table classique : 0b01101111
. Par contre la suite on la met oĂč ?
Ben Ă la suite !
Le deuxiĂšme groupe de 8 attributs donne la null-table suivante: 0b01
On fusionne les deux.
Notre null-table complĂšte est donc:
0b0101101111
Mais comme ça ne rentre pas dans notre sérialisation en byte, on scinde en deux.
0b01101111 0b01
^ ^
partition 0 partition 1
On va appeler chacun des groupe une partition.
Et donc si on sérialise
[NULL, 2, 3, NULL, 5, 6, 7, 8, 9, NULL]
Cela devient
0x6f 0x01 0x0 0x2 0x3 0x0 0x5 0x6 0x7 0x8 0x9 0x0
^ ^
null table data
On fera difficilement plus compact. đ
Question
Et avec ce tableau de 2 nombres, je fais comment pour savoir si lâattribut est null ou pas ?
Et bien, on sait que la partition fera forcément 1 octet, donc 8 bits.
Si on recherche le bit 12, il ne peut pas se retrouver dans la partition 0 qui ne va que jusquâĂ lâindex 7.
Donc pour trouver la partition, il suffit de diviser cet index par la taille de la partition ici 8.
partition = index / 8
Et pour trouver le bit qui doit ĂȘtre masquĂ© on rĂ©cupĂšre le reste de la division par le modulo de cette division
bit = index % 8
Finalement il vient
partition = index / 8
bit = index % 8
null_table[partition] & (1 << bit) == 0
Et lĂ on a tout ! đ
Ajout des Value::Null
Maintenant que nous avons bien thĂ©orisĂ© tout cela, nous allons pouvoir nous attaquer Ă lâimplĂ©mentation de la fonctionnalitĂ©.
Pour cela, nous allons rajouter une nouvelle variante à notre énumération Value
.
On en profite pour implémenter le Serializable pour la nouvelle variante.
Modification du check_values
Maintenant que nous avons défini notre Value::Null
, nous allons améliorer notre méthode check_values qui a pour but de vérifier la conformité de la donnée.
On se rajoute pour lâoccasion une nouvelle erreur.
Celle-ci permet dâinterdire une valeur dâĂȘtre NULL si le schĂ©ma ne le permet pas.
Implémentation de la null table
Nous devons maintenant déterminer la null table associer au tuple de donnée.
/// Taille de la partition de la null table d'un octet
pub const PARTITION_SIZE: usize = 8;
Ce qui donne ce fonctionnement.
La deuxiĂšme chose que lâon a besoin de dĂ©finir est lâopĂ©ration de dĂ©tection de NULL value.
Et qui peut ĂȘtre utilisĂ© de cette maniĂšre:
Et nous allons définir une derniÚre méthode qui à partir du schéma vient définir la taille de la null table de chaque tuple de la page.
Pour cela, on recherche le multiple de 8 supĂ©rieur ou Ă©gal au nombre dâattributs du tuple.
Cela donne:
Je me permets également de rajouter la possibilité de sérialiser du u8.
Modification de Schema
La premiĂšre chose Ă faire est de corriger la taille du tuple dans la page, en effet, il faut maintenant rajouter la null_table en plus.
Ensuite, il faut redéfinir la méthode de sérialisation des données en prenant en compte que certains champs ne seront pas présent.
Il faut alors définir et encoder la null_table.
Puis pour chaque attribut, si la valeur est null, ne rien sérialiser du tout, sinon sérialiser la donnée.
Et lâopĂ©ration inverse de dĂ©sĂ©rialisation, qui consiste Ă rĂ©cupĂ©rer la null table, puis pour chaque attribut si la null table indique un NULL, passer la dĂ©sĂ©rialisation et renvoyer un Value::Null
.
Et on peut tester notre merveille !
Testons !
On définit deux champs NOT NULL
- id
- name
Et on laisse nullifiable lâĂąge.
INTEGER NOT NULL PRIMARY KEY, nom TEXT(50) NOT NULL, Ăąge INTEGER);
(id
On peut alors insérer partiellement de la donnée.
INSERT INTO Users (id, nom, Ăąge) VALUES (1, 'Max', 20);
INSERT INTO Users (id, nom) VALUES (2, 'Amy');
Par contre, si on va trop loin dans le âpartielâ et que lâon oublie de remplir le nom
INSERT INTO Users (id) VALUES (3);
Insertion(Serialization(ColumnDefinition(NonNullableColumn { column_name: "nom" })))
On peut alors sélectionner nos données
SELECT * FROM Users;
[Integer(1), Text("Max"), Integer(20)]
[Integer(2), Text("Amy"), Null]
Nous avons bien âAmyâ qui est sans Ăąge ^^
Conclusion
Pas du tout non ? On commence Ă entrevoir du sql, la route est encore longue, mais lâon progresse. đ
Dans la prochaine partie, nous verrons comment gérer les index secondaires.
Merci de votre lecture â€ïž