Opcje dwuwartościowe - przechowywanie

2010-07-27 17:31:23

 

Często nasze tabele w bazie danych zawierają pola, która mogą przyjmować dwie wartości (dwa stany): 0 i 1. Na przykład tabela z użytkownikami zawiera pole ZABLOKOWANY, które mówi czy użytkownik jest zablokowany czy nie. Jeśli jest zablokowany to przyjmuje wartość 1 a jeśli nie jest - wartość 0. Wszystko ładnie pięknie gdy są to pojedyncze pola i nie zajmują dużo miejsca. Ale co w przypadku gdy nasza tabela zawiera kilka, kilkanaście lub nawet kilkadziesiąt opcji opisu (np. tabela z ofertami, która zawiera pola OPCJA_1, OPCJA_2, OPCJA_3, .... OPCJA_N) ? Czy mamy wówczas też tworzyć kilkadziesiąt pól na każdą z opcji o możliwych wartościach 0 i 1? Nie - tutaj z pomocą przyjdzie nam system dwójkowy (binarny) oraz operacje bitowe.

Całość przedstawię na podanym powyżej przykładzie: tabela z jakimiś ofertami. Tabela zawiera osiem pól. Każde z pól będzie typu TINYINT czyli zajmować będzie jeden bajt. Pola mogą mieć wartości 1 lub 0 oznaczające kolejno posiadanie danej opcji przez ofertę lub nie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE `oferta` (
  `ID` int unsigned NOT NULL auto_increment,
  `NAZWA` varchar(32) NOT NULL,
  `OPCJA_1` tinyint unsigned NOT NULL default 0,
  `OPCJA_2` tinyint unsigned NOT NULL default 0,
  `OPCJA_3` tinyint unsigned NOT NULL default 0,
  `OPCJA_4` tinyint unsigned NOT NULL default 0,
  `OPCJA_5` tinyint unsigned NOT NULL default 0,
  `OPCJA_6` tinyint unsigned NOT NULL default 0,
  `OPCJA_7` tinyint unsigned NOT NULL default 0,
  `OPCJA_8` tinyint unsigned NOT NULL default 0,
  PRIMARY KEY  (`ID`),
  KEY `OPCJA_1(`OPCJA_1`),
  KEY `OPCJA_2(`OPCJA_2`),
  KEY `OPCJA_3(`OPCJA_3`),
  KEY `OPCJA_4(`OPCJA_4`),
  KEY `OPCJA_5(`OPCJA_5`),
  KEY `OPCJA_6(`OPCJA_6`),
  KEY `OPCJA_7(`OPCJA_7`),
  KEY `OPCJA_8(`OPCJA_8`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Jak widać tabela już wizualnie jest duża. Stworzyliśmy 8 pól. Każde z pól zajmuje jeden bajt. Dodatkowo jeśli tych opcji chcemy używać przy wyszukiwaniu to dobrze też jest założyć na nie indeksy. Także mamy już 8 bajtów na opis opcji jednego rekordu plus indeksy

Ale możemy to znaczącą zmniejszyć. Wystarczy znać podstawy systemu dwójkowego oraz operacji bitowych. Nie będę tu tego opisywał, przedstawię jedynie podstawy do zrozumienia na czym cały myk polega.

Teoria

Jak zapewne wiecie jeden bajt zawiera 8 bitów. Bit to po prostu wartość 0 lub 1. Czyli skoro jeden bajt zawiera osiem bitów to na jednym bajcie możemy zapisać do ośmiu informacji zerojedynkowych. Teraz wystarczy "wirtualnie" określić, że ustawienie w danej pozycji bita stanowi wybór jednej z opcji i mamy problem z głowy.

1
2
3
4
5
6
7
8
9
10
11
zapis dwójkowy | wartość dziesiętna | opcja
0 0 0 0 0 0 0 0      0               żadna opcja
0 0 0 0 0 0 0 1      1               opcja 1 
0 0 0 0 0 0 1 0      2               opcja 2
0 0 0 0 0 1 0 0      4               opcja 3
0 0 0 0 1 0 0 0      8               opcja 4
0 0 0 1 0 0 0 0      16              opcja 5
0 0 1 0 0 0 0 0      32              opcja 6 
0 1 0 0 0 0 0 0      64              opcja 7
1 0 0 0 0 0 0 0      128             opcja 8 

Ustawiając wartości 1 na różnych bitach otrzymujemy inne wartości dzisiętne.

1
2
3
4
5
6
7
zapis dwójkowy | wartość dziesiętna | opcja
0 0 0 0 0 0 0 0      0               żadna opcja 
0 0 0 0 0 1 0 1      5               opcja 1 + opcja 3
1 0 0 0 0 1 0 1      133             opcja 1 + opcja 3 + opcja 8
0 0 1 1 0 0 0 0      48              opcja 5 + opcja 6
..............

Tyle teorii "dwójkowej". Teraz trochę teorii w php. Zapoznajcie się proszę z operatorami bitowymi

Chcąc w php dodać bitowo do siebie dwie liczby (ustawić wartość, która odpowiadać będzie zaznaczeniu dwóch naszych opcji) należy wykonać taki kod

1
2
3
4
5
6
<?php
//liczba 4 odpowiada opcji 3     0 0 0 0 0 1 0 0
//liczba 16 odpowiada opcji 5    0 0 0 1 0 0 0 0
$opcje 16//20
//liczba 20 odpowiada opcji 3 oraz opcji 5    0 0 0 1 0 1 0 0
?>

Ok, umiemy już dodawać opcje do siebie. Ale jak sprawdzić w wyszukiwaniu czy dana opcja jest zaznaczona mając podaną liczbę określającą zaznaczone opcje? Posłużymy się operatorem &

1
2
3
4
5
6
7
8
<?php
//$opcje = 20; opcja 3 oraz opcja 5
//sprawdzamy czy w zmiennej $opcje znajduje się opcja 3 (wartość 4)
//jeśli wynikem sprawdzania będzie liczba której szukamy, to znaczy że szukana opcja jest w naszym zbiorze
//jeśli wynikiem będzie inna liczba wówczas opcji nie ma
$res $opcje 4//wynikiem jest 4 - opcja 3 więc tu jest
$res $opcje 8//wynikiem jest 0 - nie ma tu więc opcji 4
?>

Możemy też jednocześnie poszukiwać kilku opcji

1
2
3
4
5
6
7
8
<?php
//szukamy czy występują jednocześnie opcje 1 (1) oraz 3 (4) czyli szukamy liczby 5 (0 0 0 0 0 1 0 1 )
//1 0 0 0 0 1 0 1      133                        opcja 1 + opcja 3 + opcja 8
$res 133 5;//wynikem jest 5 czyli zawarte są tu opcje 1 oraz 3

//szukamy czy występują jednocześnie opcje 2 (2) oraz 3 (4) czyli szukamy liczby 6 (0 0 0 0 0 1 1 0 )
$res 133 6;//wynikem jest 4 czyli liczba różna od szukanej. Nie ma więc tutaj jednocześnie opcji 2 oraz 3
?>

Powyższe obliczenia można również wykonywać bezpośrednio na bazie danych - przyda nam się to w praktycznym pisaniu wyszukiwarki.

Zauważcie, że jeśli szukamy tylko jednej opcji, to w przypadku gdy jej nie znajdziemy, wynikiem zawsze będzie 0. W przypadku poszukiwania kilku opcji, wynikiem negatywnym może być liczba większa od 0 ale inna niż ta, której szukaliśmy. Właściwość ta nam się przyda przy zaznaczaniu checkboxów, ale o tym za chwilę.

Praktyka

Tak więc nasze 8 pól opcji zamieniamy na jedno pole typu TINYINT. Typ ten zajmuje jeden bajt czyli osiem bitów. Jeśli byście chcieli przechowywać do 16 opcji, musicie użyć typu SMALLINT (2 bajty - 16 bitów). Zwiększając typy możecie przechowywać więcej opcji - ale to już chyba hardcore robić kilkadziesiąt opcji dla jednej tabeli ;)

1
2
3
4
5
6
7
8
CREATE TABLE `oferta` (
  `ID` int unsigned NOT NULL auto_increment,
  `NAZWA` varchar(32) NOT NULL,
  `OPCJE` tinyint unsigned NOT NULL default 0,
  PRIMARY KEY  (`ID`),
  KEY `OPCJE` (`OPCJE`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Porównując tę tabelę do naszej pierwszej od razu widać wizualnie różnicę. Pamięciowo też już jest o wiele lepiej.

Zajmiemy się teraz reprezentacją opcji. Opcje możemy przedstawić jako checkboxy

1
2
3
4
5
6
<input type="checkbox" name="opcja[]" value="1" />Opcja1 
<input type="checkbox" name="opcja[]" value="2" />Opcja2
<input type="checkbox" name="opcja[]" value="4" />Opcja3
....
<input type="checkbox" name="opcja[]" value="128" />Opcja8  

Zauważcie, iż użyłem nazwy checkboxa z [] - określa to nazwę tablicową. Dzięki temu po zaznaczeniu kilku opcji, formularz wyśle na serwer wszystkie zaznaczone opcje jako tablica wartości. Tablicę tę będzie można odebrać w php z $_POST['opcja'].

Jeśli pobierzemy z bazy wartość pola OPCJE i chcemy w formularzu zaznaczyć te opcje, które są zawarte w tym polu, korzystamy ponownie z operatora &

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

$opcje 
20//wartość pobrana z bazy z pola OPCJE

//poniżej zostaną zaznaczone opcja 3 oraz opcja 5
echo '<input type="checkbox" name="opcja[]" value="1" '.($opcje 'checked="checked"' '').'/> Opcja 1';
echo 
'<input type="checkbox" name="opcja[]" value="2" '.($opcje 'checked="checked"' '').'/> Opcja 2';
echo 
'<input type="checkbox" name="opcja[]" value="4" '.($opcje 'checked="checked"' '').'/> Opcja 3';
echo 
'<input type="checkbox" name="opcja[]" value="8" '.($opcje 'checked="checked"' '').'/> Opcja 4';
echo 
'<input type="checkbox" name="opcja[]" value="16" '.($opcje 16 'checked="checked"' '').'/> Opcja 5';
echo 
'<input type="checkbox" name="opcja[]" value="32" '.($opcje 32 'checked="checked"' '').'/> Opcja 6';
echo 
'<input type="checkbox" name="opcja[]" value="64" '.($opcje 64 'checked="checked"' '').'/> Opcja 7';
echo 
'<input type="checkbox" name="opcja[]" value="128" '.($opcje 128 'checked="checked"' '').'/> Opcja 8';
?>

Teraz jeśli chcemy zapisać zaznaczone checkboxy (opcje) do bazy to musimy użyć operatora |

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if (!empty($_POST)){ //jeśli wysłano formularz
    
$opcje 0;//zaczynamy od nie wybranej żadnej opcji
    
if (!empty($_POST['opcja'])){ //jeśli wybraliśmy jakieś checkboxy
        
foreach ($_POST['opcja'] as $op){ //przelatujemy po wartościach wybranych checkboxów
            
$op = (int)$op//rzutujemy wartość na inta by przypadkiem ktoś nam "kuku" nie zrobił
            //dodajemy bitowo opcje
            
$opcje |= $op//zapis ten jest równoważny do  $opcje = $opcje | $op;
        
}
    }
//teraz w zmiennej $opcje mamy zaznaczone opcje i możemy tę wartość zapisać do bazy
}
?>

Mamy już wczytywanie i zapisywanie opcji. Pozostało jeszcze zrobić nam wyszukiwanie. Wyszukiwarka również będzie zawierała formularz z checkboxami określającymi opcje. Wyszukanie opcji będzie podobne do ich zapisania - też trzeba najpierw zsumować zaznaczone opcje, a następnie przy pomocy operatora & wyszukać oferty, które je zawierają.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
$opcje 
0//na początku nic nie szukamy
//w wyszukiwarkach zazwyczaj korzysta się z get zamiast post
if (!empty($_GET['opcja'])){ //jeśli wybraliśmy jakieś checkboxy to ich będziemy szukać
    
foreach ($_GET['opcja'] as $op){ //przelatujemy po wartościach wybranych checkboxów
        
$op = (int)$op//rzutujemy wartość na inta by przypadkiem ktoś nam "kuku" nie zrobił
        //dodajemy bitowo opcje
        
$opcje |= $op//zapis ten jest rownoważny do  $opcje = $opcje | $op;
    
}
}

//tworzymy podstawowe zapytanie do pobrania ofert
$sql 'select * from oferta';

//jeśli wybraliśmy jakieś opcje, to jeszcze bedziemy ich szukac.
//dodajemy więc WHERE do zapytania
if (!empty($opcje))
    
$sql.=" where OPCJE & $opcje = $opcje"//jak już pisałem wcześniej, jeśli szukamy opcji to wynik musi nam zwrócić szukaną liczbę

//wykonujemy zapytanie
$res mysql_query($sql);

//i tutaj już sobie wyświetlamy znalezione wyniki
?>

I to już wszystko. Na pierwszy rzut oka może to się wydawać skomplikowane - wystarczy jednak przerobić to ze dwa razy i powinno już być lżej. A znajomość tej metody może Wam się przydać - ja z niej korzystam dość często.

Wiem, że kod można było napisać bardziej uniwersalnie, np. poprzez dodanie pętli zamiast generować wszystkie opcje ręcznie. Nie chciałem jednak wprowadzać dodatkowych "bajerów" by nie zaciemniać istoty problemu.

W dziale download znajdziecie paczkę, która zawiera przykładową bazę danych oraz kody do zapisywania i wyszukiwania opcji. Przeanalizujcie je dokładnie - pomogą Wam utrwalić przedstawione tu informacje.

I na koniec mały "bonus" dla tych wszystkich, którzy nie lubią 0 i 1 lecz wolą bardziej wartości tekstowe. Mysql udostępnia specjalny typ danych, który umożliwia w opisanej tu metodzie operować na tekstach: SET

 

Komentarze

 

2010-07-27 17:44:32 Michał

 
Ja używam ENUM oraz SET wygodniej po prostu

2010-07-27 18:15:37 nospor

 
Ja zaś jakoś przyzwyczaiłem się do cyferek i rzadko sięgam po te typy. Z ENUM ostatnio zacząłem korzystać tylko dlatego, by nie zapomnieć jak się z niego korzysta :)

2010-07-27 18:56:57 gość_cojack

 
Tak na prawdę to bajt może mieć 9 stanów, jeszcze same 0 nospor.

2010-07-27 19:34:36 nospor

 
@cojak nie mówimy tutaj że bajt ma stany a bity. Bajt ma 8 bitów. Za pomocą tych ośmiu bitów można również zapisać bajt o wartośći liczbowej 0. I według Ciebie oznacza to, iż dzięki temu można na jednym bajcie zapisać 9 opcji? Proszę więc o przykład :)
Jeśli zaś Twoja myśl o 9 stanach zmierzała do czegoś innego to też ją proszę rozwiń.

2010-07-27 19:52:11 gość_Kshyhoo

 
Fajny artykuł, przypomniał mi naszą "pogawędkę" na temat ENUM.

2010-07-27 22:44:47 gość_cojack

 
@nospor np gdybyś chciał zrobić grupy użytkowników na masce bitowej, pozwalając adminowi dodawać je, to jest to bardzo mało wygodne bo przez to się maska bitowa rozszerza. No ale to akurat nie wątek o tym. Tak bajt ma tylko 8 bitów i tutaj się nie będę kłócił ;D Ale jak sam napisałeś powyżej ( nie mogę wkleić kodu )

To patrz, jakąś opcją równie dobrze mogło by być stan zerowy nie prawda :> Jak masz możliwość 9 stanów to czemu i nie 9 opcji? A mam nadzieję że ten artykuł to tylko tak w ramach rozrywki pisany, a nie tak na poważnie, to mało czytelne jest programowanie. Ja tam nie lubię pól bitowych i masek, może dlatego że mało w C pisałem. Ale jakoś ich nie lubię ;p Jedynie co z bitów używam to w postgresie zamiast boolean bo mnie masakrycznie irytuje jak mi postgres wypluwa 't' lub 'f' jako wartość logiczną i weź się z tym później męcz w php.

2010-07-27 22:51:37 gość_Vokiel

 
Operatory bitowe to ciekawa opcja, chociaż nie często się z nią spotykałem.

Kiedyś zrobiłem mały system dostępu do aplikacji oparty. Modułów w aplikacji było dość dużo,a nie chciałem robić uprawnień dostępu na zasadzie ciągu cyfr rozdzielanego separatorem (przecinkiem, średnikiem). Sposób przez Ciebie tu opisany spisywał się bardzo dobrze. No i ćwiczyło się liczenie w pamięci w systemie dwójkowym, 12 - tylko dostęp do odczytu komunikatów, 255 - o super admin :D

2010-07-27 23:08:02 nospor

 
@cojak jeden z nas czegoś tu nie rozumie. Może ty mnie, może ja Ciebie ;)
Jak masz możliwość 9 stanów to czemu i nie 9 opcji?
Jak to sobie wyobrażasz? Skoro wykorzystasz 0 jako opcja 9 to jakim cudem w polu zapiszesz: "nie wybrano żadnej opcji"?
Przecież podałem to nawet na przykładzie:
1
2
3
4
5
6
7
8
9
10
11
zapis dwójkowy | wartość dziesiętna | opcja 
0 0 0 0 0 0 0 0      0                           żadna opcja 
0 0 0 0 0 0 0 1      1                           opcja 1  
0 0 0 0 0 0 1 0      2                           opcja 2 
0 0 0 0 0 1 0 0      4                           opcja 3 
0 0 0 0 1 0 0 0      8                           opcja 4 
0 0 0 1 0 0 0 0      16                         opcja 5 
0 0 1 0 0 0 0 0      32                         opcja 6  
0 1 0 0 0 0 0 0      64                         opcja 7 
1 0 0 0 0 0 0 0      128                        opcja 8 
Tu nie ma miejsca na opcje nr 9. Chcesz mieć 9 opcji to daj SMALLINT gdzie masz dwa bajty - 16 bitów - zmieścisz wówczas 16 opcji.
A mam nadzieję że ten artykuł to tylko tak w ramach rozrywki pisany, a nie tak na poważnie, to mało czytelne jest programowanie. Ja tam nie lubię pól bitowych i masek,
To, że coś jest mało czytelne nie znaczy, że jest złe. To że ty czegoś nie lubisz też nie znaczy, że jest złe. Dla mnie to co tu przedstawiłem to banalna sprawa - używam tego często. Nie widzę w tym nic "niepoważnego".
gdybyś chciał zrobić grupy użytkowników na masce bitowej, pozwalając adminowi dodawać je, to jest to bardzo mało wygodne
A ty gdybyś próbował samochodem lecieć to też to jest bardzo mało wygodne... Cojak, uważam Cię za rozsądanego chłopaka, ale Twoja argumentacja tutaj mnie przeraża. W życiu nie robiłem grup użytkowników na masce bitowej (choć jakby przysiąść to można się pobawić, może coś ciekawego by wyszło). Do jazdy używam samochodu, do latania używam samolotu. Do wbijania gwoździ używam młotka a nie packi na muchy. Na muchy używam packi na muchy a nie młotka. Zamiast używać bez sensu 8 pól w bazie używam jednego jeśli sytuacja na to pozwala. Artytkuł jest o tym jak tego używać. Jeśli ktoś znajdzie zastosowanie to proszę bardzo niech używa jeśli się przyda. Ale niech nikt na siłe nie wciska tego gdzie popadnie bo nie taki tego cel. A juz na pewno niech nikt z tego nie korzysta jeśli ma małe pojęcie o operatorach bitowych i ich nie lubi ;)

Opisałem tę metodę tutaj, gdyż na forum stosunkowo często pojawiał się taki problem. Ludzie się głowili, wymyślali cuda nie widy a rozwiązanie było banalne - bity :)
Nie raz udzielałem na forum tej odpowiedzi i sporej części początkujących ona przypadała do gustu. Postanowiłem więc opisać ją ciut dokładniej - nie jako żart, ale jako poważne rozwiązanie.

2010-07-27 23:18:47 nospor

 
@vokiel o widzisz. Może opisz swój przykład trochę dokładniej - zapewne by naświetlił wykorzystanie tej metody w innej trochę sytuacji niż opisanej tu przeze mnie

2010-07-28 00:09:29 gość_cojack

 
No przecież napisałem że to nie temat o grupach no coś się uczepił. Jakoś tak samo mi się z kontekstu wyrwało. Ale jest też inne rozwiązanie tego problemu, dajmy na to że produkt może mieć atrybuty, które są przypisane cechom, do której kategorii przynależy produkt. I taki produkt jednej cechy może mieć wiele atrybutów. Tego to już byś raczej na bitach nie poleciał, chociaż na upartego to wszystko można. No ale rozwiązanie takiego problemu przy zachowaniu normalizacji bazy danych należałoby utworzyć relację many-to-many i to chyba nie jedną. Także do Twojego problemu, też zastosowałbym tą opcję, łatwo można sobie jojnować crosować itp ;] A przy tych przesunięciach bitowych to bym się tępym ołówkiem pochlastał... ;)

2010-07-28 07:26:46 nospor

 
No przecież napisałem że to nie temat o grupach no coś się uczepił.
No bo robisz wstawki ni z gruszki nie z pietruszki i już sam nie wiem o co ci chodzi ;) Make php code not war ;)

2010-08-02 01:36:47 gość_Pan Rumcajs

 
Fajny artykuł i dobry pomysł do wykorzystania. Przydałoby się tylko jeszcze używać czcionki o stałej szerokości podczas prezentowania wycinków kodu - polepszyłoby to na pewno czytelność.
Dzięki!

Dodaj komentarz

 

Dostępne bbcode: b, u, i, color, size, quote, img, url, list, il (tylko w list), code, php, css, html, sql, js