這僅是一個(gè)極簡的 demo, 旨在動(dòng)手了解概念.
NoSQL 這個(gè)詞在近些年正變得隨處可見. 但是到底 “NoSQL” 指的是什么? 它是如何并且為什么這么有用? 在本文, 我們將會(huì)通過純 Python (我比較喜歡叫它, “輕結(jié)構(gòu)化的偽代碼”) 寫一個(gè) NoSQL 數(shù)據(jù)庫來回答這些問題.
OldSQL
很多情況下, SQL 已經(jīng)成為 “數(shù)據(jù)庫” (database) 的一個(gè)同義詞. 實(shí)際上, SQL 是 Strctured Query Language 的首字母縮寫, 而并非指數(shù)據(jù)庫技術(shù)本身. 更確切地說, 它所指的是從 RDBMS (關(guān)系型數(shù)據(jù)庫管理系統(tǒng), Relational Database Management System ) 中檢索數(shù)據(jù)的一門語言. MySQL, MS SQL Server 和 Oracle 都屬于 RDBMS 的其中一員.
RDBMS 中的 R, 即 “Relational” (有關(guān)系,關(guān)聯(lián)的), 是其中內(nèi)容最豐富的部分. 數(shù)據(jù)通過 表 (table) 進(jìn)行組織, 每張表都是一些由 類型 (type) 相關(guān)聯(lián)的 列 (column) 構(gòu)成. 所有表, 列及其類的類型被稱為數(shù)據(jù)庫的 schema (架構(gòu)或模式). schema 通過每張表的描述信息完整刻畫了數(shù)據(jù)庫的結(jié)構(gòu). 比如, 一張叫做 Car 的表可能有以下一些列:
Make: a string
Model: a string
Year: a four-digit number; alternatively, a date
Color: a string
VIN(Vehicle Identification Number): a string
在一張表中, 每個(gè)單一的條目叫做一 行 (row), 或者一條 記錄 (record). 為了區(qū)分每條記錄, 通常會(huì)定義一個(gè) 主鍵 (primary key). 表中的 主鍵 是其中一列 , 它能夠唯一標(biāo)識(shí)每一行. 在表 Car 中, VIN 是一個(gè)天然的主鍵選擇, 因?yàn)樗軌虮WC每輛車具有唯一的標(biāo)識(shí). 兩個(gè)不同的行可能會(huì)在 Make, Model, Year 和 Color 列上有相同的值, 但是對(duì)于不同的車而言, 肯定會(huì)有不同的 VIN. 反之, 只要兩行擁有同一個(gè) VIN, 我們不必去檢查其他列就可以認(rèn)為這兩行指的的就是同一輛車.
Querying
SQL 能夠讓我們通過對(duì)數(shù)據(jù)庫進(jìn)行 query (查詢) 來獲取有用的信息. 查詢 簡單來說, 查詢就是用一個(gè)結(jié)構(gòu)化語言向 RDBMS 提問, 并將其返回的行解釋為問題的答案. 假設(shè)數(shù)據(jù)庫表示了美國所有的注冊(cè)車輛, 為了獲取 所有的 記錄, 我們可以通過在數(shù)據(jù)庫上進(jìn)行如下的 SQL 查詢 :
SELECT Make, Model FROM Car;
將 SQL 大致翻譯成中文:
“SELECT”: “向我展示”
“Make, Model”: “Make 和 Model 的值”
“FROM Car”: “對(duì)表 Car 中的每一行”
也就是, “向我展示表 Car 每一行中 Make 和 Model 的值”. 執(zhí)行查詢后, 我們將會(huì)得到一些查詢的結(jié)果, 其中每個(gè)都是 Make 和 Model. 如果我們僅關(guān)心在 1994 年注冊(cè)的車的顏色, 那么可以:
SELECT Color FROM Car WHERE Year = 1994;
此時(shí), 我們會(huì)得到一個(gè)類似如下的列表:
Black
Red
Red
White
Blue
Black
White
Yellow
最后, 我們可以通過使用表的 (primary key) 主鍵 , 這里就是 VIN 來指定查詢一輛車:
SELECT * FROM Car WHERE VIN = '2134AFGER245267'
上面這條查詢語句會(huì)返回所指定車輛的屬性信息.
主鍵被定義為唯一不可重復(fù)的. 也就是說, 帶有某一指定 VIN 的車輛在表中至多只能出現(xiàn)一次. 這一點(diǎn)非常重要,為什么? 來看一個(gè)例子:
Relations
假設(shè)我們正在經(jīng)營一個(gè)汽車修理的業(yè)務(wù). 除了其他一些必要的事情, 我們還需要追蹤一輛車的服務(wù)歷史, 即在該輛車上所有的修整記錄. 那么我們可能會(huì)創(chuàng)建包含以下一些列的 ServiceHistory 表:
VIN | Make | Model | Year | Color | Service Performed | Mechanic | Price | Date
這樣, 每次當(dāng)車輛維修以后, 我們就在表中添加新的一行, 并寫入該次服務(wù)我們做了一些什么事情, 是哪位維修工, 花費(fèi)多少和服務(wù)時(shí)間等.
但是等一下, 我們都知道,對(duì)于同一輛車而言,所有車輛自身信息有關(guān)的列是不變的。 也就是說,如果把我的 Black 2014 Lexus RX 350 修整 10 次的話, 那么即使 Make, Model, Year 和 Color 這些信息并不會(huì)改變,每一次仍然重復(fù)記錄了這些信息. 與無效的重復(fù)記錄相比, 一個(gè)更合理的做法是對(duì)此類信息只存儲(chǔ)一次, 并在有需要的時(shí)候進(jìn)行查詢。
那么該怎么做呢? 我們可以創(chuàng)建第二張表: Vehicle , 它有如下一些列:
VIN | Make | Model | Year | Color
這樣一來, 對(duì)于 ServiceHistory 表, 我們可以精簡為如下一些列:
VIN | Service Performed | Mechanic | Price | Date
你可能會(huì)問,為什么 VIN 會(huì)在兩張表中同時(shí)出現(xiàn)? 因?yàn)槲覀冃枰幸粋€(gè)方式來確認(rèn)在 ServiceHistory 表的 這 輛車指的就是 Vehicle 表中的 那 輛車, 也就是需要確認(rèn)兩張表中的兩條記錄所表示的是同一輛車。 這樣的話,我們僅需要為每輛車的自身信息存儲(chǔ)一次即可. 每次當(dāng)車輛過來維修的時(shí)候, 我們就在 ServiceHistory 表中創(chuàng)建新的一行, 而不必在 Vehicle 表中添加新的記錄。 畢竟, 它們指的是同一輛車。
我們可以通過 SQL 查詢語句來展開 Vehicle 與 ServiceHistory 兩張表中包含的隱式關(guān)系:
SELECT Vehicle.Model, Vehicle.Year FROM Vehicle, ServiceHistory WHERE Vehicle.VIN = ServiceHistory.VIN AND ServiceHistory.Price > 75.00;
該查詢旨在查找維修費(fèi)用大于 $75.00 的所有車輛的 Model 和 Year. 注意到我們是通過匹配 Vehicle 與 ServiceHistory 表中的 VIN 值來篩選滿足條件的記錄. 返回的將是兩張表中符合條件的一些記錄, 而 “Vehicle.Model” 與 “Vehicle.Year” , 表示我們只想要 Vehicle表中的這兩列.
如果我們的數(shù)據(jù)庫沒有 索引 (indexes) (正確的應(yīng)該是 indices), 上面的查詢就需要執(zhí)行 表掃描 (table scan) 來定位匹配查詢要求的行。 table scan 是按照順序?qū)Ρ碇械拿恳恍羞M(jìn)行依次檢查, 而這通常會(huì)非常的慢。 實(shí)際上, table scan 實(shí)際上是所有查詢中最慢的。
可以通過對(duì)列加索引來避免掃描表。 我們可以把索引看做一種數(shù)據(jù)結(jié)構(gòu), 它能夠通過預(yù)排序讓我們?cè)诒凰饕牧猩峡焖俚卣业揭粋€(gè)指定的值 (或指定范圍內(nèi)的一些值). 也就是說, 如果我們?cè)?Price 列上有一個(gè)索引, 那么就不需要一行一行地對(duì)整個(gè)表進(jìn)行掃描來判斷其價(jià)格是否大于 75.00, 而是只需要使用包含在索引中的信息 “跳” 到第一個(gè)價(jià)格高于 75.00 的那一行, 并返回隨后的每一行(由于索引是有序的, 因此這些行的價(jià)格至少是 75.00)。
當(dāng)應(yīng)對(duì)大量的數(shù)據(jù)時(shí), 索引是提高查詢速度不可或缺的一個(gè)工具。當(dāng)然, 跟所有的事情一樣,有得必有失, 使用索引會(huì)導(dǎo)致一些額外的消耗: 索引的數(shù)據(jù)結(jié)構(gòu)會(huì)消耗內(nèi)存,而這些內(nèi)存本可用于數(shù)據(jù)庫中存儲(chǔ)數(shù)據(jù)。這就需要我們權(quán)衡其利弊,尋求一個(gè)折中的辦法, 但是為經(jīng)常查詢的列加索引是 非常 常見的做法。
The Clear Box
得益于數(shù)據(jù)庫能夠檢查一張表的 schema (描述了每列包含了什么類型的數(shù)據(jù)), 像索引這樣的高級(jí)特性才能夠?qū)崿F(xiàn), 并且能夠基于數(shù)據(jù)做出一個(gè)合理的決策。 也就是說, 對(duì)于一個(gè)數(shù)據(jù)庫而言, 一張表其實(shí)是一個(gè) “黑盒” (或者說透明的盒子) 的反義詞?
當(dāng)我們談到 NoSQL 數(shù)據(jù)庫的時(shí)候要牢牢記住這一點(diǎn)。 當(dāng)涉及 query 不同類型數(shù)據(jù)庫引擎的能力時(shí), 這也是其中非常重要的一部分。
Schemas
我們已經(jīng)知道, 一張表的 schema , 描述了列的名字及其所包含數(shù)據(jù)的類型。它還包括了其他一些信息, 比如哪些列可以為空, 哪些列不允許有重復(fù)值, 以及其他對(duì)表中列的所有限制信息。 在任意時(shí)刻一張表只能有一個(gè) schema, 并且 表中的所有行必須遵守 schema 的規(guī)定 。
這是一個(gè)非常重要的約束條件。 假設(shè)你有一張數(shù)據(jù)庫的表, 里面有數(shù)以百萬計(jì)的消費(fèi)者信息。 你的銷售團(tuán)隊(duì)想要添加額外的一些信息 (比如, 用戶的年齡), 以期提高他們郵件營銷算法的準(zhǔn)確度。 這就需要來 alter (更改) 現(xiàn)有的表 — 添加新的一列。 我們還需要決定是否表中的每一行都要求該列必須有一個(gè)值。 通常情況下, 讓一個(gè)列有值是十分有道理的, 但是這么做的話可能會(huì)需要一些我們無法輕易獲得的信息(比如數(shù)據(jù)庫中每個(gè)用戶的年齡)。因此在這個(gè)層面上,也需要有些權(quán)衡之策。
此外,對(duì)一個(gè)大型數(shù)據(jù)庫做一些改變通常并不是一件小事。為了以防出現(xiàn)錯(cuò)誤,有一個(gè)回滾方案非常重要。但即使是如此,一旦當(dāng) schema 做出改變后,我們也并不總是能夠撤銷這些變動(dòng)。 schema 的維護(hù)可能是 DBA 工作中最困難的部分之一。
Key/Value Stores
在 “NoSQL” 這個(gè)詞存在前, 像 memcached 這樣的 鍵/值 數(shù)據(jù)存儲(chǔ) (Key/Value Data Stores) 無須 table schema 也可提供數(shù)據(jù)存儲(chǔ)的功能。 實(shí)際上, 在 K/V 存儲(chǔ)時(shí), 根本沒有 “表 (table)” 的概念。 只有 鍵 (keys) 與 值 (values) . 如果鍵值存儲(chǔ)聽起來比較熟悉的話, 那可能是因?yàn)檫@個(gè)概念的構(gòu)建原則與 Python 的 dict 與 set 相一致: 使用 hash table (哈希表) 來提供基于鍵的快速數(shù)據(jù)查詢。 一個(gè)基于 Python 的最原始的 NoSQL 數(shù)據(jù)庫, 簡單來說就是一個(gè)大的字典 (dictionary) .
為了理解它的工作原理,親自動(dòng)手寫一個(gè)吧! 首先來看一下一些簡單的設(shè)計(jì)想法:
一個(gè) Python 的 dict 作為主要的數(shù)據(jù)存儲(chǔ)
僅支持 string 類型作為鍵 (key)
支持存儲(chǔ) integer, string 和 list
一個(gè)使用 ASCLL string 的簡單 TCP/IP 服務(wù)器用來傳遞消息
一些像 INCREMENT, DELETE , APPEND 和 STATS 這樣的高級(jí)命令 (command)
有一個(gè)基于 ASCII 的 TCP/IP 接口的數(shù)據(jù)存儲(chǔ)有一個(gè)好處, 那就是我們使用簡單的 telnet 程序即可與服務(wù)器進(jìn)行交互, 并不需要特殊的客戶端 (盡管這是一個(gè)非常好的練習(xí)并且只需要 15 行代碼即可完成)。
對(duì)于我們發(fā)送到服務(wù)器及其它的返回信息,我們需要一個(gè) “有線格式”。下面是一個(gè)簡單的說明:
Commands Supported
PUT
參數(shù): Key, Value
目的: 向數(shù)據(jù)庫中插入一條新的條目 (entry)
GET
參數(shù): Key
目的: 從數(shù)據(jù)庫中檢索一個(gè)已存儲(chǔ)的值
PUTLIST
參數(shù): Key, Value
目的: 向數(shù)據(jù)庫中插入一個(gè)新的列表?xiàng)l目
APPEND
參數(shù): Key, Value
目的: 向數(shù)據(jù)庫中一個(gè)已有的列表添加一個(gè)新的元素
INCREMENT
參數(shù): key
目的: 增長數(shù)據(jù)庫的中一個(gè)整型值
DELETE
參數(shù): Key
目的: 從數(shù)據(jù)庫中刪除一個(gè)條目
STATS
參數(shù): 無 (N/A)
目的: 請(qǐng)求每個(gè)執(zhí)行命令的 成功/失敗 的統(tǒng)計(jì)信息
現(xiàn)在我們來定義消息的自身結(jié)構(gòu)。
Message Structure
Request Messages
一條 請(qǐng)求消息 (Request Message) 包含了一個(gè)命令(command),一個(gè)鍵 (key), 一個(gè)值 (value), 一個(gè)值的類型(type). 后三個(gè)取決于消息類型,是可選項(xiàng), 非必須。; 被用作是分隔符。即使并沒有包含上述可選項(xiàng), 但是在消息中仍然必須有三個(gè) ; 字符。
COMMAND; [KEY]; [VALUE]; [VALUE TYPE]
COMMAND是上面列表中的命令之一
KEY是一個(gè)可以用作數(shù)據(jù)庫 key 的 string (可選)
VALUE是數(shù)據(jù)庫中的一個(gè) integer, list 或 string (可選)
list 可以被表示為一個(gè)用逗號(hào)分隔的一串 string, 比如說, “red, green, blue”
VALUE TYPE描述了VALUE應(yīng)該被解釋為什么類型
可能的類型值有:INT, STRING, LIST
Examples
"PUT; foo; 1; INT"
"GET; foo;;"
"PUTLIST; bar; a,b,c ; LIST"
"APPEND; bar; d; STRING"
"GETLIST; bar; ;"
STATS; ;;
INCREMENT; foo;;
DELETE; foo;;
Reponse Messages
一個(gè) 響應(yīng)消息 (Reponse Message) 包含了兩個(gè)部分, 通過 ; 進(jìn)行分隔。第一個(gè)部分總是 True|False , 它取決于所執(zhí)行的命令是否成功。 第二個(gè)部分是命令消息 (command message), 當(dāng)出現(xiàn)錯(cuò)誤時(shí),便會(huì)顯示錯(cuò)誤信息。對(duì)于那些執(zhí)行成功的命令,如果我們不想要默認(rèn)的返回值(比如 PUT), 就會(huì)出現(xiàn)成功的信息。 如果我們返回成功命令的值 (比如 GET), 那么第二個(gè)部分就會(huì)是自身值。
Examples
True; Key [foo] set to [1]
True; 1
True; Key [bar] set to [['a', 'b', 'c']]
True; Key [bar] had value [d] appended
True; ['a', 'b', 'c', 'd']
True; {'PUTLIST': {'success': 1, 'error': 0}, 'STATS': {'success': 0, 'error': 0}, 'INCREMENT': {'success': 0, 'error': 0}, 'GET': {'success': 0, 'error': 0}, 'PUT': {'success': 0, 'error': 0}, 'GETLIST': {'success': 1, 'error': 0}, 'APPEND': {'success': 1, 'error': 0}, 'DELETE': {'success': 0, 'error': 0}}
Show Me The Code!
我將會(huì)以塊狀摘要的形式來展示全部代碼。 整個(gè)代碼不過 180 行,讀起來也不會(huì)花費(fèi)很長時(shí)間。
Set Up
下面是我們服務(wù)器所需的一些樣板代碼:
"""NoSQL database written in Python"""
# Standard library imports
importsocket
HOST = 'localhost'
PORT = 50505
SOCKET = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
STATS = {
'PUT': {'success': 0,'error': 0},
'GET': {'success': 0,'error': 0},
'GETLIST': {'success': 0,'error': 0},
'PUTLIST': {'success': 0,'error': 0},
'INCREMENT': {'success': 0,'error': 0},
'APPEND': {'success': 0,'error': 0},
'DELETE': {'success': 0,'error': 0},
'STATS': {'success': 0,'error': 0},
}
很容易看到, 上面的只是一個(gè)包的導(dǎo)入和一些數(shù)據(jù)的初始化。
Set up(Cont’d)
接下來我會(huì)跳過一些代碼, 以便能夠繼續(xù)展示上面準(zhǔn)備部分剩余的代碼。 注意它涉及到了一些尚不存在的一些函數(shù), 不過沒關(guān)系, 我們會(huì)在后面涉及。 在完整版(將會(huì)呈現(xiàn)在最后)中, 所有內(nèi)容都會(huì)被有序編排。 這里是剩余的安裝代碼:
COMMAND_HANDERS = {
'PUT': handle_put,
'GET': handle_get,
'GETLIST': handle_getlist,
'PUTLIST': handle_putlist,
'INCREMENT': handle_increment,
'APPEND': handle_append,
'DELETE': handle_delete,
'STATS': handle_stats,
}
DATA = {}
defmain():
"""Main entry point for script"""
SOCKET.bind(HOST,PORT)
SOCKET.listen(1)
while1:
connection,address = SOCKET.accept()
print('New connection from [{}]'.format(address))
data = connection.recv(4096).decode()
command,key,value = parse_message(data)
ifcommand == 'STATS':
response = handle_stats()
elifcommand in('GET','GETLIST','INCREMENT','DELETE'):
response = COMMAND_HANDERS[command](key)
elifcommand in(
'PUT',
'PUTLIST',
'APPEND',):
response = COMMAND_HANDERS[command](key,value)
else:
response = (False,'Unknown command type {}'.format(command))
update_stats(command,response[0])
connection.sandall('{};{}'.format(response[0],response[1]))
connection.close()
if__name__ == '__main__':
main()
我們創(chuàng)建了 COMMAND_HANDLERS, 它常被稱為是一個(gè) 查找表 (look-up table) . COMMAND_HANDLERS 的工作是將命令與用于處理該命令的函數(shù)進(jìn)行關(guān)聯(lián)起來。 比如說, 如果我們收到一個(gè) GET 命令, COMMAND_HANDLERS[command](key) 就等同于說 handle_get(key) . 記住,在 Python 中, 函數(shù)可以被認(rèn)為是一個(gè)值,并且可以像其他任何值一樣被存儲(chǔ)在一個(gè) dict 中。
在上面的代碼中, 雖然有些命令請(qǐng)求的參數(shù)相同,但是我仍決定分開處理每個(gè)命令。 盡管可以簡單粗暴地強(qiáng)制所有的 handle_ 函數(shù)接受一個(gè) key 和一個(gè) value , 但是我希望這些處理函數(shù)條理能夠更加有條理, 更加容易測(cè)試,同時(shí)減少出現(xiàn)錯(cuò)誤的可能性。
注意 socket 相關(guān)的代碼已是十分極簡。 雖然整個(gè)服務(wù)器基于 TCP/IP 通信, 但是并沒有太多底層的網(wǎng)絡(luò)交互代碼。
最后還須需要注意的一小點(diǎn): DATA 字典, 因?yàn)檫@個(gè)點(diǎn)并不十分重要, 因而你很可能會(huì)遺漏它。 DATA 就是實(shí)際用來存儲(chǔ)的 key-value pair, 正是它們實(shí)際構(gòu)成了我們的數(shù)據(jù)庫。
Command Parser
下面來看一些 命令解析器 (command parser) , 它負(fù)責(zé)解釋接收到的消息:
defparse_message(data):
"""Return a tuple containing the command, the key, and (optionally) the
value cast to the appropriate type."""
command,key,value,value_type = data.strip().split(';')
ifvalue_type:
ifvalue_type == 'LIST':
value = value.split(',')
elifvalue_type == 'INT':
value = int(value)
else:
value = str(value)
else:
value = None
returncommand,key,value
這里我們可以看到發(fā)生了類型轉(zhuǎn)換 (type conversion). 如果希望值是一個(gè) list, 我們可以通過對(duì) string 調(diào)用 str.split(',') 來得到我們想要的值。 對(duì)于 int, 我們可以簡單地使用參數(shù)為 string 的 int() 即可。 對(duì)于字符串與 str() 也是同樣的道理。
Command Handlers
下面是命令處理器 (command handler) 的代碼. 它們都十分直觀,易于理解。 注意到雖然有很多的錯(cuò)誤檢查, 但是也并不是面面俱到, 十分龐雜。 在你閱讀的過程中,如果發(fā)現(xiàn)有任何錯(cuò)誤請(qǐng)移步 這里 進(jìn)行討論.
defupdate_stats(command,success):
"""Update the STATS dict with info about if executing *command* was a
*success*"""
ifsuccess:
STATS[command]['success'] += 1
else:
STATS[command]['error'] += 1
defhandle_put(key,value):
"""Return a tuple containing True and the message to send back to the
client."""
DATA[key] = value
return(True,'key [{}] set to [{}]'.format(key,value))
defhandle_get(key):
"""Return a tuple containing True if the key exists and the message to send
back to the client"""
ifkey notinDATA:
return(False,'Error: Key [{}] not found'.format(key))
else:
return(True,DATA[key])
defhandle_putlist(key,value):
"""Return a tuple containing True if the command succeeded and the message
to send back to the client."""
returnhandle_put(key,value)
defhandle_putlist(key,value):
"""Return a tuple containing True if the command succeeded and the message
to send back to the client"""
returnhandle_put(key,value)
defhandle_getlist(key):
"""Return a tuple containing True if the key contained a list and the
message to send back to the client."""
return_value = exists,value = handle_get(key)
ifnotexists:
returnreturn_value
elifnotisinstance(value,list):
return(False,'ERROR: Key [{}] contains non-list value ([{}])'.format(
key,value))
else:
returnreturn_value
defhandle_increment(key):
"""Return a tuple containing True if the key's value could be incremented
and the message to send back to the client."""
return_value = exists,value = handle_get(key)
ifnotexists:
returnreturn_value
elifnotisinstance(list_value,list):
return(False,'ERROR: Key [{}] contains non-list value ([{}])'.format(
key,value))
else:
DATA[key].append(value)
return(True,'Key [{}] had value [{}] appended'.format(key,value))
defhandle_delete(key):
"""Return a tuple containing True if the key could be deleted and the
message to send back to the client."""
ifkey notinDATA:
return(
False,
'ERROR: Key [{}] not found and could not be deleted.'.format(key))
else:
delDATA[key]
defhandle_stats():
"""Return a tuple containing True and the contents of the STATS dict."""
return(True,str(STATS))
有兩點(diǎn)需要注意: 多重賦值 (multiple assignment) 和代碼重用. 有些函數(shù)僅僅是為了更加有邏輯性而對(duì)已有函數(shù)的簡單包裝而已, 比如 handle_get 和 handle_getlist . 由于我們有時(shí)僅僅是需要一個(gè)已有函數(shù)的返回值,而其他時(shí)候卻需要檢查該函數(shù)到底返回了什么內(nèi)容, 這時(shí)候就會(huì)使用 多重賦值 。
來看一下 handle_append . 如果我們嘗試調(diào)用 handle_get 但是 key 并不存在時(shí), 那么我們簡單地返回 handle_get 所返回的內(nèi)容。 此外, 我們還希望能夠?qū)?handle_get 返回的 tuple 作為一個(gè)單獨(dú)的返回值進(jìn)行引用。 那么當(dāng) key 不存在的時(shí)候, 我們就可以簡單地使用 return return_value .
如果它 確實(shí)存在 , 那么我們需要檢查該返回值。并且, 我們也希望能夠?qū)?handle_get 的返回值作為單獨(dú)的變量進(jìn)行引用。 為了能夠處理上述兩種情況,同時(shí)考慮需要分開處理結(jié)果的情形,我們使用了多重賦值。 如此一來, 就不必書寫多行代碼, 同時(shí)能夠保持代碼清晰。 return_value = exists, list_value = handle_get(key) 能夠顯式地表明我們將要以至少兩種不同的方式引用 handle_get 的返回值。
How Is This a Database?
上面的程序顯然并非一個(gè) RDBMS, 但卻絕對(duì)稱得上是一個(gè) NoSQL 數(shù)據(jù)庫。它如此易于創(chuàng)建的原因是我們并沒有任何與 數(shù)據(jù) (data) 的實(shí)際交互。 我們只是做了極簡的類型檢查,存儲(chǔ)用戶所發(fā)送的任何內(nèi)容。 如果需要存儲(chǔ)更加結(jié)構(gòu)化的數(shù)據(jù), 我們可能需要針對(duì)數(shù)據(jù)庫創(chuàng)建一個(gè) schema 用于存儲(chǔ)和檢索數(shù)據(jù)。
既然 NoSQL 數(shù)據(jù)庫更容易寫, 更容易維護(hù),更容易實(shí)現(xiàn), 那么我們?yōu)槭裁床皇侵皇褂?mongoDB 就好了? 當(dāng)然是有原因的, 還是那句話,有得必有失, 我們需要在 NoSQL 數(shù)據(jù)庫所提供的數(shù)據(jù)靈活性 (data flexibility) 基礎(chǔ)上權(quán)衡數(shù)據(jù)庫的可搜索性 (searchability).
Querying Data
假如我們上面的 NoSQL 數(shù)據(jù)庫來存儲(chǔ)早前的 Car 數(shù)據(jù)。 那么我們可能會(huì)使用 VIN 作為 key, 使用一個(gè)列表作為每列的值, 也就是說, 2134AFGER245267 = ['Lexus', 'RX350', 2013, Black] . 當(dāng)然了, 我們已經(jīng)丟掉了列表中每個(gè)索引的 涵義 (meaning) . 我們只需要知道在某個(gè)地方索引 1 存儲(chǔ)了汽車的 Model , 索引 2 存儲(chǔ)了 Year.
糟糕的事情來了, 當(dāng)我們想要執(zhí)行先前的查詢語句時(shí)會(huì)發(fā)生什么? 找到 1994 年所有車的顏色將會(huì)變得噩夢(mèng)一般。 我們必須遍歷 DATA 中的 每一個(gè)值 來確認(rèn)這個(gè)值是否存儲(chǔ)了 car 數(shù)據(jù)亦或根本是其他不相關(guān)的數(shù)據(jù), 比如說檢查索引 2, 看索引 2 的值是否等于 1994,接著再繼續(xù)取索引 3 的值. 這比 table scan 還要糟糕,因?yàn)樗粌H要掃描每一行數(shù)據(jù),還需要應(yīng)用一些復(fù)雜的規(guī)則來回答查詢。
NoSQL 數(shù)據(jù)庫的作者當(dāng)然也意識(shí)到了這些問題,(鑒于查詢是一個(gè)非常有用的 feature) 他們也想出了一些方法來使得查詢變得不那么 “遙不可及”。一個(gè)方法是結(jié)構(gòu)化所使用的數(shù)據(jù),比如 JSON, 允許引用其他行來表示關(guān)系。 同時(shí), 大部分 NoSQL 數(shù)據(jù)庫都有名字空間 (namespace) 的概念, 單一類型的數(shù)據(jù)可以被存儲(chǔ)在數(shù)據(jù)庫中該類型所獨(dú)有的 “section” 中,這使得查詢引擎能夠利用所要查詢數(shù)據(jù)的 “shape” 信息。
當(dāng)然了,盡管為了增強(qiáng)可查詢性已經(jīng)存在 (并且實(shí)現(xiàn)了)了一些更加復(fù)雜的方法, 但是在存儲(chǔ)更少量的 schema 與增強(qiáng)可查詢性之間做出妥協(xié)始終是一個(gè)不可逃避的問題。 本例中我們的數(shù)據(jù)庫僅支持通過 key 進(jìn)行查詢。 如果我們需要支持更加豐富的查詢, 那么事情就會(huì)變得復(fù)雜的多了。
Summary
至此, 希望 “NoSQL” 這個(gè)概念已然十分清晰。 我們學(xué)習(xí)了一點(diǎn) SQL, 并且了解了 RDBMS 是如何工作的。 我們看到了如何從一個(gè) RDBMS 中檢索數(shù)據(jù) (使用 SQL 查詢 (query)). 通過搭建了一個(gè)玩具級(jí)別的 NoSQL 數(shù)據(jù)庫, 了解了在可查詢性與簡潔性之間面臨的一些問題, 還討論了一些數(shù)據(jù)庫作者應(yīng)對(duì)這些問題時(shí)所采用的一些方法。
即便是簡單的 key-value 存儲(chǔ), 關(guān)于數(shù)據(jù)庫的知識(shí)也是浩瀚無窮。雖然我們僅僅是探討了其中的星星點(diǎn)點(diǎn), 但是仍然希望你已經(jīng)了解了 NoSQL 到底指的是什么, 它是如何工作的, 什么時(shí)候用比較好。如果您想要分享一些不錯(cuò)的想法, 歡迎 討論.
-
SQL
+關(guān)注
關(guān)注
1文章
775瀏覽量
44274 -
python
+關(guān)注
關(guān)注
56文章
4811瀏覽量
85076
原文標(biāo)題:用 Python 寫一個(gè) NoSQL 數(shù)據(jù)庫
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論