大家都在用的“關(guān)系數(shù)據(jù)庫”,竟是各種IT事故的根源
編者按:此文原題為《What I’m Telling Business People About Why Relational Databases Are So Bad》,作者是Lance Gutteridge博士,文章是其《避免IT災(zāi)難》一書里面的內(nèi)容,旨在向商業(yè)人士解釋為什么關(guān)系式數(shù)據(jù)庫是那么多企業(yè)系統(tǒng)問題的根源。為了向一般受眾解釋清楚,里面盡量避免了技術(shù)術(shù)語,但是技術(shù)受眾從中看看需要向非技術(shù)人員解釋些什么東西也能有所收獲。
1970年代,IBM的Codd博士和Date博士提出了一種新型的數(shù)據(jù)庫。這種數(shù)據(jù)庫叫做“關(guān)系式”數(shù)據(jù)庫。現(xiàn)在,我從來都沒見過有任何跡象表明Codd或者Date曾經(jīng)開發(fā)過任何真正的企業(yè)系統(tǒng)。如果有的話,我相信他們就會意識到關(guān)系式計(jì)算其實(shí)很不適合企業(yè)系統(tǒng)。反正又不需要讓系統(tǒng)正常工作,他們所需要做的只是給出人為的學(xué)術(shù)例子就行了。但任何做過企業(yè)系統(tǒng)的人都會明白,真正的系統(tǒng)要復(fù)雜得多。寫企業(yè)軟件很難。這是計(jì)算機(jī)和人類行為的交叉,要想滿足那些有時(shí)候會相互沖突的需求是很難的。
當(dāng)時(shí)我正在給一個(gè)大型協(xié)會寫軟件,我記得人人都在談關(guān)系式數(shù)據(jù)庫。其實(shí)這些人都不知道關(guān)系式數(shù)據(jù)庫究竟是什么,他們只是聽說了這個(gè)術(shù)語并且認(rèn)為它很好。當(dāng)時(shí)正值計(jì)算機(jī)術(shù)語進(jìn)入到普通商業(yè)場所之時(shí)。很多像《Byte》、《PC Mag》這樣的雜志開始出現(xiàn)到報(bào)攤,其中一些故事還被報(bào)紙報(bào)道了。數(shù)據(jù)庫的想法——把數(shù)據(jù)像文件柜一樣存儲起來的想法本來就挺吸引人。
然后就是“關(guān)系式”這個(gè)名字。有文章會告訴你它將如何讓你利用關(guān)系在數(shù)據(jù)中遍歷。類似于獲得項(xiàng)目的項(xiàng)目經(jīng)理所在部門的名字這樣。但實(shí)際上關(guān)系式中的關(guān)系這個(gè)詞指的是關(guān)系的數(shù)學(xué)概念,也就是一組數(shù)據(jù)。如果兩個(gè)東西在一組內(nèi)就可以說它們是相關(guān)的。實(shí)際上,關(guān)系的數(shù)學(xué)概念避開了相關(guān)聯(lián)對象之間的連接的概念,而是把關(guān)系定義為一組相關(guān)的東西。這樣的一個(gè)集合就是數(shù)學(xué)理論上的關(guān)系。這跟我們商業(yè)上的關(guān)系改變已經(jīng)完全脫離,前者要求必定存在某種連接。如果某人告訴你一組對象對是關(guān)聯(lián)的,你一般會尋求某種連接。這樣設(shè)置一定是有什么道理的,我們往往會設(shè)法去尋找其中隱藏的因素是什么。模式尋找是人特有的一種本能。但是數(shù)學(xué)理論上的關(guān)系并不關(guān)心這個(gè)。哪怕只是偶然配對在一起的也會被視為一種關(guān)系。
我在紐約大學(xué)教數(shù)理邏輯的時(shí)候教過一門關(guān)系數(shù)學(xué)理論的課程。一些學(xué)生總是沒有辦法擺脫關(guān)系必須有某種規(guī)則的理念。這門理論的抽象概念的基礎(chǔ)是他們所不能理解的。當(dāng)我聽說IBM提出了一種基于關(guān)系式計(jì)算的數(shù)據(jù)庫時(shí)我嚇了一跳。因?yàn)楫?dāng)時(shí)寫了很多企業(yè)軟件,我看不出把東西強(qiáng)制做成那么正式和抽象的結(jié)構(gòu)會有什么好處。
在我看來這是復(fù)雜性和系統(tǒng)超限的罪魁禍?zhǔn)字弧2焕斫猓壳衣犖衣纴怼?/p>
SQL注入
你有沒有看過1920、1930年代時(shí)候的老電影里面某人讀電報(bào)的場景?
那場景總是像這樣的:
已與DANNY私奔句號(STOP)會在馬德里度蜜月句號非常幸福句號
總是這樣,一堆的句號。這有什么用?
要想理解這個(gè),我們得回到布爾戰(zhàn)爭,這是發(fā)生在20世紀(jì)之交的一場英國人和布爾人為了爭奪南非殖民地而展開的骯臟戰(zhàn)爭。這場戰(zhàn)爭除了給世界引入了集中營和塹壕戰(zhàn)的概念以外,還是使用無線電報(bào)的第一場戰(zhàn)爭,部隊(duì)在前線就能通過電報(bào)接收到命令。前線是非常骯臟的地方,在20世紀(jì)之交的前線尤其如此,因?yàn)楸晃勰嗪婉R糞濺得到處都是。因?yàn)閾?dān)心紙上濺落的泥巴會被無人城逗號或者句號從而改變命令的意圖,英國戰(zhàn)爭部命令所有的標(biāo)點(diǎn)符號都需要拼寫出來,比方說COMMA(逗號)。至于句號他們選擇了用STOP來表示,這是一種更為英式的“句號”表示法。
電報(bào)碼中將句號寫成STOP成為了一種實(shí)踐。當(dāng)大家大聲讀出來的時(shí)候他們會本能地讀成單詞STOP而不是把它當(dāng)作表示句子結(jié)束的句號。
你可能會問“這跟關(guān)系式數(shù)據(jù)庫有什么關(guān)系?”給關(guān)系式數(shù)據(jù)庫下達(dá)的命令采用的是用可讀的文本形式。這種文本是一種叫做SQL(Structured Query Language,結(jié)構(gòu)化查詢語言)的語言。就像電報(bào)用單詞STOP來斷句一樣,SQL也使用標(biāo)點(diǎn)符號來分隔命令。就像電報(bào)一樣,從效果上來說數(shù)據(jù)庫以文本流的形式獲取命令,每句命令之間會有個(gè)STOP。
看看這個(gè)例子:
UPDATE CUSTOMER_TABLE SET NAME=“John Smith” WHERE CUSTOM_NO=2333 STOP UPDATE …
這是一條SQL語句的命令,意思是更新客戶記錄2333,將名字改為John Smith。
現(xiàn)在請留意一下引號之間的文本比如“John Smith”。這是哪兒來的?
好吧則其實(shí)是某人通過瀏覽器填寫表格輸入的。填表單的那個(gè)人輸入了“John Smith”然后點(diǎn)擊提交按鈕。網(wǎng)站代碼將輸入的引號之間的文本放到SQL語句里面然后發(fā)送給數(shù)據(jù)庫執(zhí)行。
現(xiàn)在假設(shè)這位客戶心存惡意這樣輸入他的名字:
John” STOP DELETE CUSTOMER_TABLE STOP
如果網(wǎng)站把它放進(jìn)SQL語句的話結(jié)果會變成這個(gè)樣子:
UPDATE CUSTOMER_TABLE SET NAME=“John” STOP DELETE CUSTOMER_TABLE STOP” STOP UPDATE …
可能你已經(jīng)看出來發(fā)生了什么變化了。這是一條將客戶表中的名字設(shè)為John的命令,會被正常執(zhí)行,但只有它就會執(zhí)行那位惡意客戶插入的下一條命令,這條會刪除整個(gè)表的數(shù)據(jù)。記住,這條命令是由具備更新數(shù)據(jù)庫的充分權(quán)限的任務(wù)來完成的。
這就是所謂的“SQL注入”。
SQL注入曾經(jīng)是破解網(wǎng)站和入侵公司最具破壞性的一項(xiàng)技術(shù)。超過90%的主流網(wǎng)站滲透都是通過SQL注入來完成的。你只需要google一下就會看到一堆的數(shù)據(jù)泄露,受累的信用卡、被吸干的銀行賬號以及被暴露的個(gè)人信息高達(dá)數(shù)億。
請?jiān)谧屑?xì)觀察一下這里發(fā)生的事情。數(shù)據(jù)庫命令是文本形式的,而互聯(lián)網(wǎng)用戶通過web表格輸入的數(shù)據(jù)會被并入該文本里面,這就給填表格的人愚弄數(shù)據(jù)庫讓后者不正確地解釋命令創(chuàng)造了機(jī)會。
如果說在你看來這似乎是很愚蠢的做事方式的話,你的感覺完全正確。
這大概是有史以來做出的最愚蠢的,被使用得最廣泛、代價(jià)最高昂的技術(shù)決定了。
這種軟件就相當(dāng)于核電站將控制室跟參觀室設(shè)在了一起。
把兩個(gè)東西拆開,也就是一個(gè)是命令,一個(gè)是來自表格的數(shù)據(jù),然后再合并到一起,接著反復(fù)進(jìn)行一場不要被愚弄把數(shù)據(jù)當(dāng)成命令的實(shí)際部分的技術(shù)戰(zhàn)爭,這種做法根本毫無意義。
為什么一開始就要把它們?nèi)嗟揭黄鹉兀?/p>
這就是一個(gè)非常糟糕的架構(gòu),這個(gè)架構(gòu)要為全球各個(gè)組織數(shù)十億美元的損失負(fù)責(zé)。
但是關(guān)系式數(shù)據(jù)庫的故事比這還糟。
不要重復(fù)自己
假設(shè)你有一份實(shí)現(xiàn)軟件的工作時(shí)間記錄表(timesheet)。這份記錄表有一個(gè)員工號。現(xiàn)在你要展示這份工作時(shí)間記錄表,同時(shí)你想將員工姓名找出來。但姓名是在員工表上的,所以你得創(chuàng)建一條像這樣的SQL語句:
SELECT EMPLOYEE WHERE EMPLOYEE.NUMBER EQUALS TIMESHEET.EMPLOYEE_NUMBER輸入
這條語句會查詢員工表將其與timesheet表進(jìn)行匹配。你在這里做的其實(shí)是定義員工與timesheet的關(guān)系。你可以說他們是通過timesheet上的員工號連接在一起的。
記住那份timesheet表格已經(jīng)描述給軟件了。你說timesheet上的字段是員工號,所以基本上此時(shí)軟件已經(jīng)知道了這一關(guān)系了。但是現(xiàn)在你卻要很麻煩地構(gòu)建一條SQL語句然后發(fā)給數(shù)據(jù)庫去執(zhí)行,然后在返回一組表的行記錄再從中選出你需要的信息。這完全是毫無必要!因?yàn)檫@份timesheet上與員工有關(guān)的信息已經(jīng)跟軟件溝通過了。采用關(guān)系式數(shù)據(jù)庫導(dǎo)致這一信息被無視并且還得用一種完全不同的語言去重新定義這種關(guān)系。
計(jì)算機(jī)科學(xué)有一條原則叫做DRY(Don’t Repeat Yourself),意思是不要重復(fù)自己。其主要信條是每個(gè)不同的代碼片段或數(shù)據(jù)僅在一個(gè)位置上出現(xiàn)。代碼你應(yīng)該編寫一次然后在計(jì)算需要的時(shí)候進(jìn)行調(diào)用。然而,這一原則也延伸到各個(gè)消除冗余性的地方。這是一條減少無序/復(fù)雜性的原則。相同的計(jì)算采用相同的代碼消除了兩個(gè)不同的實(shí)現(xiàn)走亂步伐的可能性。
用SQL表達(dá)已經(jīng)在不同的表格上被表示過的數(shù)據(jù)之間的“關(guān)系”完全就是對DRY原則的違背。在軟件里面信息是很寶貴的。信息被捕捉到之后就應(yīng)該物盡其用,永遠(yuǎn)都不應(yīng)該重新輸入這一信息。你永遠(yuǎn)都不應(yīng)該輸入某個(gè)可以通過之前輸入過的地方獲取的東西。這么做就會制造出一條信息兩個(gè)版本搞亂的可能性。
自從關(guān)系式數(shù)據(jù)庫被提出以來,我就一直對為什么這種似乎非常怪異的架構(gòu)還能存在感到困惑。
這就好像讓你的檔案室講外語然后所有的指令都要用那門語言編寫一樣。
但情況其實(shí)還要糟糕。當(dāng)你把那份timesheet保存進(jìn)關(guān)系式數(shù)據(jù)庫時(shí),你必須把它分開,頭信息放在一個(gè)表,所有分配工時(shí)給項(xiàng)目的明細(xì)記錄又是一行行記錄組成的另一張表。你必須把那張表拆開然后構(gòu)建SQL來操作和存儲它們。哦是的,如果你想按照同樣的次序還原那張工時(shí)記錄表的話,你還得給每一條明細(xì)記錄行分配一個(gè)序號。當(dāng)你想要要回那張表時(shí),你得編寫SQL指令將表格聯(lián)合起來然后你還得從返回結(jié)果中選擇所有的timesheet信息再拼湊成一份表格。
有人把這描述成每晚回家時(shí)你得把你的車拆卸下來,把部件掛到車庫墻上,然后早上再重新組裝才能開車。
這一切需要大量的額外編碼才能讓關(guān)系式數(shù)據(jù)庫與面向?qū)ο筌浖@兩個(gè)不同的世界能夠?qū)υ挕n~外的代碼意味著多余錯(cuò)誤的可能性。
對象—關(guān)系式阻抗不匹配
如果這還不夠糟,關(guān)系式數(shù)據(jù)庫中數(shù)據(jù)的存儲方式更適應(yīng)的是1980年代的編程語言而不是現(xiàn)代的面向?qū)ο笳Z言。在今天現(xiàn)代的面向?qū)ο笳Z言中所有數(shù)據(jù)都得編碼成這些原子的數(shù)據(jù)類型。
這有時(shí)候被稱為“對象—關(guān)系式阻抗不匹配”。嚴(yán)重嗎?揚(yáng)聲器與放大器之間的阻抗不匹配我還能理解,因?yàn)檫@是一種真正的物理現(xiàn)象。但這種背景下這個(gè)說法會制造技術(shù)上的含糊,其實(shí)應(yīng)該用“一個(gè)真正愚蠢的架構(gòu)的后果”來替代。
如果你想知道為什么企業(yè)系統(tǒng)經(jīng)常會失敗,這個(gè)不是全部單至少是主要原因之一。被迫用不同語言復(fù)制所有這些邏輯的必要性,以及用不同方式表達(dá)數(shù)據(jù),給ERP系統(tǒng)制造了大量的混亂/困惑。
多年來由不同的人對一個(gè)老一點(diǎn)的代碼庫進(jìn)行大量補(bǔ)充和修改,然后試圖針對新情況進(jìn)行定制,這一切會增加關(guān)系式數(shù)據(jù)庫的復(fù)雜性,項(xiàng)目就會被置于嚴(yán)重風(fēng)險(xiǎn)之中。
話雖如此,關(guān)系式數(shù)據(jù)庫無所不在倒是真的。多到有程序員從來都沒見過其中類型的數(shù)據(jù)庫,以為所有的數(shù)據(jù)庫都是關(guān)系式的。
關(guān)系式數(shù)據(jù)庫是有史以來敗壞了一個(gè)行業(yè)領(lǐng)域的最糟糕的技術(shù)。將如此大量的額外混亂傾倒到系統(tǒng)里面是企業(yè)系統(tǒng)為什么失敗會如此頻繁的主要原因。