數據庫中用一個值來保存多種狀況:二進制和按位異或

例如,某個房間可從[燈,牀,桌,椅,杯子,飲水機……]這些器具中挑選,從而組成這個房間的裝潢。咱們可能會設計一個房間表,再設計一個器具表,再設計一個關係表,經過這個關係表來保存它們之間的對應關係。可是這樣的效率明顯是比較差的,須要同時查詢三張表才能完成。php

爲了避免適用關係表,咱們還能夠在房間表中設計一個字段,經過一個有規律的字符串來保存器具表的器具ID,例如:算法

1,2,3,7

下面,咱們提供一種經過一個值來計算便可得到這一器具組合的結果,方法以下:sql

array(
  '1' => '燈'
  '2' => '牀',
  '4' => '桌',
  '8' => '椅子',
  '16' => '飲水機',
  ……
);

若是咱們將5保存到數據庫中,咱們能夠立馬知道,這個房間有「燈」和「桌」,而若是保存的是23,則必定有「燈」「牀」「桌」和「飲水機」。數據庫

給每個器具一個給定的值,這個值必定是2的n次方(n>=0),這樣就能夠保證相加以後的值能夠反解。這個狀況的核心原理在於,給定任何數值的前面數值相加和,必定小於當前數值。如何進行反解呢?編程

例如咱們拿到一個值爲N,那麼咱們能夠首先找到最大的2^n,肯定2^n是必定有的,若是沒有2^n,就不可能相加獲得N。數組

接下來咱們得到M = N - 2^n,找到最大的2^m,再進行M - 2^m,如此推論下去,直到減完爲止。函數

那麼怎得到最大的2^n呢?設計

$n = (int)log(N,2);

log函數在PHP4+以後內置,用於取對數,返回值爲float類型,但咱們僅須要整數部分,所以前面加(int)。code

例如N=22,那麼$n=4,再去計算2^4,就是16。排序

經過這個方法,咱們能夠很是順利的在一個數據表中用一個值保存多種狀況。可是,這也有必定的適用範圍,好比這些狀況最好是固定不變的,2n值不能太大等等。經過這種方法能夠用該值進行權重設計,進行排序,可是不能用於條件檢索,好比你想檢索數據庫中包含「牀」的房間,你就很差進行檢索,由於大部分房間的該值可能都大於2.因此,在使用這種方法時,應該根據實際須要進行考慮。

更新:

在數據庫中,咱們可使用一種序列化的類二進制字符串來保存多個值,當這個二進制值是以01組成時,實際上就能夠換算成爲一個十進制數,從而也就實現了一個十進制值保存多種狀況的目的。

下面咱們來作一個演示。

例如咱們在訂票系統中,規定某一個活動天天分爲6個場次,每一個場次2個小時,所以實際上就把一天的12個小時分爲了6份,分別是9:00-11:00,11:00-13:00,13:00-15:00,15:00-17:00,17:00-19:00:19:00-21:00,咱們用「xxxxxx」(x取0或1)來表示,如今,咱們要記錄這些場次是否所有被定完了,用1表示所有被訂完,因此「010110」就表示11:00-13:00,15:00-17:00,17:00-19:00這三個場次已經被訂完了,不能再對外售票。

咱們在數據庫中怎麼保存呢?

php提供了將二進制轉換爲十進制的函數bindec(),咱們先將二進制值轉換爲十進制值後,再保存到數據庫中。而當咱們要使用時,從數據庫中取出十進制值,再使用decbin()將值轉換爲二進制值,固然,咱們要補全最後獲得的二進制值的位數,也就是前面加0,而後再進行字符串數組處理,進行對比。

在編程世界中,還有一個比較好玩的算法,叫「按位異或」。按位,就是以二進制的形式進行計算,「按位異或」就是兩個位的值不一樣時返回1,不然返回0。經過這個運算,咱們能夠獲得看上去很是複雜的結果。在php中,運算爲「^」。下面咱們來進行一下演算。

001011 ^ 011010 = 010001 (1式,注意,開頭的0會被忽略,所以不要把開頭的0也算進來)

提按位異或有什麼意義呢?由於二進制值能夠和十進制值進行轉換,所以咱們將二進制值轉換爲十進制值進行按位異或以後,獲得的值也是十進制的,咱們只有將這些十進制數轉換爲二進制字串後,才能發現規律,可是若是咱們直接用十進制進行計算,卻能快速獲得結果。

下面咱們就來演算一次,咱們拿(1式)來看。若是將二進制數轉換爲十進制,咱們就能獲得

11 ^ 26 = 17

那事實的結果是否是這樣呢?你能夠在你的php程序中寫上:

<?php
echo 11 ^ 26;

是的,結果就是這樣。但是,這個複雜的運算有什麼用呢?它能夠用於比較。好比咱們的數據庫中存放了11,轉換爲二進制就是「001011」,也就是表示這一天的場次中,對應的那三個時段已經滿票了。可是若是咱們如今正好要進行對比,看看這一天中17:00-19:00這個時段是否滿票,咱們怎麼能準確知道11這個值轉換爲001011後,第5個位上的值是否爲1呢?

咱們只須要用這種思路來解決便可:

xxxxxx ^ 000010 = ?

其中xxxxxx是咱們要對比的值,好比當它等於11時,也就是001011時,等式的右邊會獲得001001(9)。咱們再來看另外一個算式:

xxxxxx ^ 000000 = ?

等式右邊會獲得自己。

若是咱們再用001001(9)去按位異或000010,則會獲得001011(11)。

咱們獲得的結論就是,凡是用xxxxx去按位異或yyyyyy(其中只有一個y爲1,其餘全爲0),獲得的結果比自身小的,則對應位置上的值爲1,獲得的結果比自身大的,對應的位置上爲0。經過這種方法,也就找到了哪一個時間段是被訂滿票的。

爲何大於自身的,對應的位置上就必定爲0呢?由於0^1=1,而二進制數是01構成的,也就是說0和1碰上0時,都不會變化,而只有0碰上1時纔會變化。說白了,用任何一個二進制數去按位異或000100,結果發生的狀況就兩種,一種是第四個位置上的值由1變爲0(結果值相對於自己值而言),這種狀況下該值變小,一種是第四個位置上的值由0變爲1,這種狀況下該值變大。瞭解了這個原理以後,咱們只須要在數據庫中保存二進制轉換而來的十進制值,在查詢時,用對比值(二進制轉換而來的十進制值)去按位異或一下,便可獲得咱們想要的結果。

咱們建立以下表結構,sale_over在實際存儲時,咱們轉換爲十進制整數進行存儲,這裏方便演示用二進制表示。每次在用戶下訂單時對票數進行檢查,若是該時段已經有20張票被訂出,就在下表中更新一條記錄,把對應的時段改成1.

tablename = objectorder

id object_id day sale_over
1 5 2015-08-23 011000
2 8 2015-08-24 100101
3 5 2015-08-25 010001

例如:

SELECT COUNT(id) FROM object_order WHERE object_id=8 AND day='2015-08-20' AND (hours ^ 2)<hours;

這樣就能夠判斷出8月20號這天17:00-19:00這個時間段是否被訂滿(若是返回1,則表示被訂滿了)。

若是咱們不滿意用大小比較來進行判斷,咱們還能夠深刻發現,按位異或結果與原值之間的差值,正好是用來異或的值,也就是知足下面的等式:

|m ^ n - m| = n (n爲yyyyyy,只有一個y爲1,其餘爲0)

|x|是指絕對值,當不取以爲值,獲得的爲負數時,說明結果變小了,那麼原值對應的位置上也就是1,而若是獲得的爲正數,說明結果變大,對應的位置上就爲0。因此,上述sql,咱們還能夠這樣去改:

**SELECT COUNT(id) FROM object_order WHERE object_id=8 AND day='2015-08-20' AND (hours ^ 2 + 2)=hours;**

若是查到告終果,說明8這個活動8月20號這天17:00-19:00這個時間段被訂滿。

這種魔術般的使用方法,你是否思考過呢?

再議

實際上,一個二進制數,咱們將它轉換爲十進制時,將它的各個位置值(從右往左,以0爲開始)做爲次數求2的次冪,再乘以該位置上的數,再相加,即獲得該二進制數對應的十進制數,例如:

10100 = 0(2^0) + 0(2^1) + 1(2^3) + 0(2^4) + 1*(2^5) = 8 + 32 = 40

這樣去觀察,就發現實際上8和32,就是咱們第一次接觸這種算法時,將它們做爲一個數組的索引值,進行物品的索引進行計算。

接下來,咱們要更換場景,每一個時段僅能夠被一我的預訂,用戶每一次下訂單完成以後,造成一條記錄,這些記錄以上述形式存儲,獲得以下訂單數據表:

tablename = userorder

id user_id object_id day hours
1 2 5 2015-08-23 011000
2 3 8 2015-08-24 100000
3 2 5 2015-08-24 000001

相似這樣的訂單記錄,hours字段中每一個位置上的1最多出現1次,怎麼樣肯定某一天的全部票都已經定出去了呢?

其實這是最簡單的,就是對該字段進行求和,例如:

SELECT SUM(hours) FROM user_order WHERE object_id=8 AND day='2015-08-20';

若是最終獲得的值爲111111,也就是十進制的63,則說明該天各個時段已訂滿,不能再進行預訂。

最後一種狀況則是對上面兩張場景的結合,也就是每一個時段最多能夠被預訂20張票,數據庫中記錄的是單個用戶的訂單。

固然,遇到這種狀況,其實咱們能夠準備兩張表,一張是用戶的訂單表:

tablename = userorder

id user_id object_id day hours
1 2 5 2015-08-23 011000
2 3 8 2015-08-24 100000
3 2 5 2015-08-24 000001

(第一條記錄表示用戶2在2015-08-23這天預訂了5這個活動的11點13點這兩個時段的票)

一張用來在每次用戶訂單完成時,對該時段進行判斷,若是這個時段已經賣出20張,就改成1,進行更新操做的場次預訂狀況表:

tablename = objectorder

id object_id day sale_over
1 5 2015-08-23 011000
2 8 2015-08-24 100101
3 5 2015-08-25 010001

可是這樣的話,咱們經過該表,僅能判斷是否賣完,而不知道已經賣了多少張。爲了解決這個問題,咱們誇張的作法是,直接在這個表的基礎上進行擴展,增長20個字段,每一個字段對應一個時段,用來記錄所賣出的票數,可是這樣實在太蠢了。因爲二進制方式,沒法在每一個位置上表示實際的值,例如在第2個位置上用3來表示賣出3張,這是咱們沒法作到的,因此,咱們能夠經過前面一張用戶下的訂單列表來進行計算,從而找出某個位置上是否已經存在20個1.

實際上,咱們如今要解決的,就是查出每一個時段已經訂出了多少張票。

咱們能夠用

SELECT COUNT(id) FROM user_order WHERE object_id=8 AND day='2015-08-20' AND (hours ^ 2 + 2)=hours;

這種方法就能夠查出來某個時段的被訂數量,若是返回值等於20,則說明該時段已經被定完了。可是,咱們如何從全部的記錄中,找出那些天的席位被所有定光呢?由於咱們不打算使用objectorder表來記錄,而是想直接經過userorder進行查詢,因此咱們不只要判斷某個位置上的爲1的記錄數是否爲20,並且要判斷全部的位置。

最笨的方法就是連續判斷6次,對每一個位置都進行統計,最終進行判斷。可是這明顯不符合咱們的要求。

實際上,咱們仍然使用求和便可完成,咱們在前面進行求和時,只須要用111111進行對比,也就是十進制的63進行對比,而此次,咱們用20個111111進行對比,也就是63*20 = 1260進行對比便可。

SELECT SUM(hours) FROM user_order WHERE object_id=8 AND day='2015-08-20';

若是獲得的返回值等於1260,說明這一天的全部場次已經徹底訂出去了。

用這種方法處理數據庫中保存有規律的多種狀況保存,就變得輕鬆有趣了。

相關文章
相關標籤/搜索