本文譯自 What is a NoSQL Database? Learn By Writing One In Python。

完整的示例程式碼已經放到了 GitHub 上(

https://

github。com/liuchengxu/h

ands-on-learning/blob/master/nosql。py

), 這僅是一個極簡的 demo, 旨在動手瞭解概念。

如果對譯文有任何的意見或建議,歡迎 提 issue 討論, 批評指正。

用 Python 寫一個 NoSQL 資料庫

NoSQL 這個詞在近些年正變得隨處可見。 但是到底 “NoSQL” 指的是什麼? 它是如何並且為什麼這麼有用? 在本文, 我們將會透過純 Python (我比較喜歡叫它, “輕結構化的虛擬碼”) 寫一個 NoSQL 資料庫來回答這些問題。

OldSQL

很多情況下, SQL 已經成為 “資料庫” (database) 的一個同義詞。 實際上, SQL 是

Strctured Query Language

的首字母縮寫, 而並非指資料庫技術本身。 更確切地說, 它所指的是從 RDBMS (關係型資料庫管理系統,

Relational Database Management System

) 中檢索資料的一門語言。 MySQL, MS SQL Server 和 Oracle 都屬於 RDBMS 的其中一員。

RDBMS 中的 R, 即 “Relational” (有關係,關聯的), 是其中內容最豐富的部分。 資料透過 表 (table) 進行組織, 每張表都是一些由 型別 (type) 相關聯的 列 (column) 構成。 所有表, 列及其類的型別被稱為資料庫的 schema (架構或模式)。 schema 透過每張表的描述資訊完整刻畫了資料庫的結構。 比如, 一張叫做 Car 的表可能有以下一些列:

Make: a string

Model: a string

Year: a four-digit number; alternatively, a date

Color: a string

VIN(Vehicle Identification Number): a string

在一張表中, 每個單一的條目叫做一 行 (row), 或者一條 記錄 (record)。 為了區分每條記錄, 通常會定義一個 主鍵 (primary key)。 表中的 主鍵 是其中一列 , 它能夠唯一標識每一行。 在表 Car 中, VIN 是一個天然的主鍵選擇, 因為它能夠保證每輛車具有唯一的標識。 兩個不同的行可能會在 Make, Model, Year 和 Color 列上有相同的值, 但是對於不同的車而言, 肯定會有不同的 VIN。 反之, 只要兩行擁有同一個 VIN, 我們不必去檢查其他列就可以認為這兩行指的的就是同一輛車。

Querying

SQL 能夠讓我們透過對資料庫進行 query (查詢) 來獲取有用的資訊。 查詢 簡單來說, 查詢就是用一個結構化語言向 RDBMS 提問, 並將其返回的行解釋為問題的答案。 假設資料庫表示了美國所有的註冊車輛, 為了獲取 所有的 記錄, 我們可以透過在資料庫上進行如下的 SQL 查詢 :

SELECT Make, Model FROM Car;

將 SQL 大致翻譯成中文:

“SELECT”: “向我展示”

“Make, Model”: “Make 和 Model 的值”

“FROM Car”: “對錶 Car 中的每一行”

也就是, “向我展示表 Car 每一行中 Make 和 Model 的值”。 執行查詢後, 我們將會得到一些查詢的結果, 其中每個都是 Make 和 Model。 如果我們僅關心在 1994 年註冊的車的顏色, 那麼可以:

SELECT Color FROM Car WHERE Year = 1994;

此時, 我們會得到一個類似如下的列表:

Black

Red

Red

White

Blue

Black

White

Yellow

最後, 我們可以透過使用表的 (primary key) 主鍵 , 這裡就是 VIN 來指定查詢一輛車:

SELECT * FROM Car WHERE VIN = ‘2134AFGER245267’

上面這條查詢語句會返回所指定車輛的屬性資訊。

主鍵被定義為唯一不可重複的。 也就是說, 帶有某一指定 VIN 的車輛在表中至多隻能出現一次。 這一點非常重要,為什麼? 來看一個例子:

Relations

假設我們正在經營一個汽車修理的業務。 除了其他一些必要的事情, 我們還需要追蹤一輛車的服務歷史, 即在該輛車上所有的修整記錄。 那麼我們可能會建立包含以下一些列的 ServiceHistory 表:

VIN | Make | Model | Year | Color | Service Performed | Mechanic | Price | Date

這樣, 每次當車輛維修以後, 我們就在表中新增新的一行, 並寫入該次服務我們做了一些什麼事情, 是哪位維修工, 花費多少和服務時間等。

但是等一下, 我們都知道,對於同一輛車而言,所有車輛自身資訊有關的列是不變的。 也就是說,如果把我的 Black 2014 Lexus RX 350 修整 10 次的話, 那麼即使 Make, Model, Year 和 Color 這些資訊並不會改變,每一次仍然重複記錄了這些資訊。 與無效的重複記錄相比, 一個更合理的做法是對此類資訊只儲存一次, 並在有需要的時候進行查詢。

那麼該怎麼做呢? 我們可以建立第二張表: Vehicle , 它有如下一些列:

VIN | Make | Model | Year | Color

這樣一來, 對於 ServiceHistory 表, 我們可以精簡為如下一些列:

VIN | Service Performed | Mechanic | Price | Date

你可能會問,為什麼 VIN 會在兩張表中同時出現? 因為我們需要有一個方式來確認在 ServiceHistory 表的 這 輛車指的就是 Vehicle 表中的 那 輛車, 也就是需要確認兩張表中的兩條記錄所表示的是同一輛車。 這樣的話,我們僅需要為每輛車的自身資訊儲存一次即可。 每次當車輛過來維修的時候, 我們就在 ServiceHistory 表中建立新的一行, 而不必在 Vehicle 表中新增新的記錄。 畢竟, 它們指的是同一輛車。

我們可以透過 SQL 查詢語句來展開 Vehicle 與 ServiceHistory 兩張表中包含的隱式關係:

SELECT Vehicle。Model, Vehicle。Year FROM Vehicle, ServiceHistory WHERE Vehicle。VIN = ServiceHistory。VIN AND ServiceHistory。Price > 75。00;

該查詢旨在查詢維修費用大於 $75。00 的所有車輛的 Model 和 Year。 注意到我們是透過匹配 Vehicle 與 ServiceHistory 表中的 VIN 值來篩選滿足條件的記錄。 返回的將是兩張表中符合條件的一些記錄, 而 “Vehicle。Model” 與 “Vehicle。Year” , 表示我們只想要 Vehicle表中的這兩列。

如果我們的資料庫沒有 索引 (indexes) (正確的應該是 indices), 上面的查詢就需要執行 表掃描 (table scan) 來定位匹配查詢要求的行。 table scan 是按照順序對錶中的每一行進行依次檢查, 而這通常會非常的慢。 實際上, table scan 實際上是所有查詢中最慢的。

可以透過對列加索引來避免掃描表。 我們可以把索引看做一種資料結構, 它能夠透過預排序讓我們在被索引的列上快速地找到一個指定的值 (或指定範圍內的一些值)。 也就是說, 如果我們在 Price 列上有一個索引, 那麼就不需要一行一行地對整個表進行掃描來判斷其價格是否大於 75。00, 而是隻需要使用包含在索引中的資訊 “跳” 到第一個價格高於 75。00 的那一行, 並返回隨後的每一行(由於索引是有序的, 因此這些行的價格至少是 75。00)。

當應對大量的資料時, 索引是提高查詢速度不可或缺的一個工具。當然, 跟所有的事情一樣,有得必有失, 使用索引會導致一些額外的消耗: 索引的資料結構會消耗記憶體,而這些記憶體本可用於資料庫中儲存資料。這就需要我們權衡其利弊,尋求一個折中的辦法, 但是為經常查詢的列加索引是 非常 常見的做法。

The Clear Box

得益於資料庫能夠檢查一張表的 schema (描述了每列包含了什麼型別的資料), 像索引這樣的高階特性才能夠實現, 並且能夠基於資料做出一個合理的決策。 也就是說, 對於一個數據庫而言, 一張表其實是一個 “黑盒” (或者說透明的盒子) 的反義詞?

當我們談到 NoSQL 資料庫的時候要牢牢記住這一點。 當涉及 query 不同型別資料庫引擎的能力時, 這也是其中非常重要的一部分。

Schemas

我們已經知道, 一張表的 schema , 描述了列的名字及其所包含資料的型別。它還包括了其他一些資訊, 比如哪些列可以為空, 哪些列不允許有重複值, 以及其他對錶中列的所有限制資訊。 在任意時刻一張表只能有一個 schema, 並且 表中的所有行必須遵守 schema 的規定 。

這是一個非常重要的約束條件。 假設你有一張資料庫的表, 裡面有數以百萬計的消費者資訊。 你的銷售團隊想要新增額外的一些資訊 (比如, 使用者的年齡), 以期提高他們郵件營銷演算法的準確度。 這就需要來 alter (更改) 現有的表 — 新增新的一列。 我們還需要決定是否表中的每一行都要求該列必須有一個值。 通常情況下, 讓一個列有值是十分有道理的, 但是這麼做的話可能會需要一些我們無法輕易獲得的資訊(比如資料庫中每個使用者的年齡)。因此在這個層面上,也需要有些權衡之策。

此外,對一個大型資料庫做一些改變通常並不是一件小事。為了以防出現錯誤,有一個回滾方案非常重要。但即使是如此,一旦當 schema 做出改變後,我們也並不總是能夠撤銷這些變動。 schema 的維護可能是 DBA 工作中最困難的部分之一。

Key/Value Stores

在 “NoSQL” 這個詞存在前, 像 memcached 這樣的 鍵/值 資料儲存 (Key/Value Data Stores) 無須 table schema 也可提供資料儲存的功能。 實際上, 在 K/V 儲存時, 根本沒有 “表 (table)” 的概念。 只有 鍵 (keys) 與 值 (values) 。 如果鍵值儲存聽起來比較熟悉的話, 那可能是因為這個概念的構建原則與 Python 的 dict 與 set 相一致: 使用 hash table (雜湊表) 來提供基於鍵的快速資料查詢。 一個基於 Python 的最原始的 NoSQL 資料庫, 簡單來說就是一個大的字典 (dictionary) 。

為了理解它的工作原理,親自動手寫一個吧! 首先來看一下一些簡單的設計想法:

一個 Python 的 dict 作為主要的資料儲存

僅支援 string 型別作為鍵 (key)

支援儲存 integer, string 和 list

一個使用 ASCLL string 的簡單 TCP/IP 伺服器用來傳遞訊息

一些像 INCREMENT, DELETE , APPEND 和 STATS 這樣的高階命令 (command)

有一個基於 ASCII 的 TCP/IP 介面的資料儲存有一個好處, 那就是我們使用簡單的 telnet 程式即可與伺服器進行互動, 並不需要特殊的客戶端 (儘管這是一個非常好的練習並且只需要 15 行程式碼即可完成)。

對於我們傳送到伺服器及其它的返回資訊,我們需要一個 “有線格式”。下面是一個簡單的說明:

Commands Supported

PUT

引數: Key, Value

目的: 向資料庫中插入一條新的條目 (entry)

GET

引數: Key

目的: 從資料庫中檢索一個已儲存的值

PUTLIST

引數: Key, Value

目的: 向資料庫中插入一個新的列表條目

APPEND

引數: Key, Value

目的: 向資料庫中一個已有的列表新增一個新的元素

INCREMENT

引數: key

目的: 增長資料庫的中一個整型值

DELETE

引數: Key

目的: 從資料庫中刪除一個條目

STATS

引數: 無 (N/A)

目的: 請求每個執行命令的 成功/失敗 的統計資訊

現在我們來定義訊息的自身結構。

Message Structure

Request Messages

一條 請求訊息 (Request Message) 包含了一個命令(command),一個鍵 (key), 一個值 (value), 一個值的型別(type)。 後三個取決於訊息型別,是可選項, 非必須。; 被用作是分隔符。即使並沒有包含上述可選項, 但是在訊息中仍然必須有三個 ; 字元。

COMMAND; [KEY]; [VALUE]; [VALUE TYPE]

COMMAND

是上面列表中的命令之一

KEY

是一個可以用作資料庫 key 的 string (可選)

VALUE

是資料庫中的一個 integer, list 或 string (可選)

list 可以被表示為一個用逗號分隔的一串 string, 比如說, “red, green, blue”

VALUE TYPE

描述了

VALUE

應該被解釋為什麼型別

可能的型別值有: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

一個 響應訊息 (Reponse Message) 包含了兩個部分, 透過 ; 進行分隔。第一個部分總是 True|False , 它取決於所執行的命令是否成功。 第二個部分是命令訊息 (command message), 當出現錯誤時,便會顯示錯誤資訊。對於那些執行成功的命令,如果我們不想要預設的返回值(比如 PUT), 就會出現成功的資訊。 如果我們返回成功命令的值 (比如 GET), 那麼第二個部分就會是自身值。

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!

我將會以塊狀摘要的形式來展示全部程式碼。 整個程式碼不過 180 行,讀起來也不會花費很長時間。

Set Up

下面是我們伺服器所需的一些樣板程式碼:

“”“NoSQL database written in Python”“”

# Standard library imports

import socket

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},

}

很容易看到, 上面的只是一個包的匯入和一些資料的初始化。

Set up(Cont’d)

接下來我會跳過一些程式碼, 以便能夠繼續展示上面準備部分剩餘的程式碼。 注意它涉及到了一些尚不存在的一些函式, 不過沒關係, 我們會在後面涉及。 在完整版(將會呈現在最後)中, 所有內容都會被有序編排。 這裡是剩餘的安裝程式碼:

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 = {}

def main():

“”“Main entry point for script”“”

SOCKET。bind(HOST, PORT)

SOCKET。listen(1)

while 1:

connection, address = SOCKET。accept()

print(‘New connection from [{}]’。format(address))

data = connection。recv(4096)。decode()

command, key, value = parse_message(data)

if command == ‘STATS’:

response = handle_stats()

elif command in (‘GET’, ‘GETLIST’, ‘INCREMENT’, ‘DELETE’):

response = COMMAND_HANDERS[command](key)

elif command 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()

我們建立了 COMMAND_HANDLERS, 它常被稱為是一個 查詢表 (look-up table) 。 COMMAND_HANDLERS 的工作是將命令與用於處理該命令的函式進行關聯起來。 比如說, 如果我們收到一個 GET 命令, COMMAND_HANDLERS[command](key) 就等同於說 handle_get(key) 。 記住,在 Python 中, 函式可以被認為是一個值,並且可以像其他任何值一樣被儲存在一個 dict 中。

在上面的程式碼中, 雖然有些命令請求的引數相同,但是我仍決定分開處理每個命令。 儘管可以簡單粗暴地強制所有的 handle_ 函式接受一個 key 和一個 value , 但是我希望這些處理函式條理能夠更加有條理, 更加容易測試,同時減少出現錯誤的可能性。

注意 socket 相關的程式碼已是十分極簡。 雖然整個伺服器基於 TCP/IP 通訊, 但是並沒有太多底層的網路互動程式碼。

最後還須需要注意的一小點: DATA 字典, 因為這個點並不十分重要, 因而你很可能會遺漏它。 DATA 就是實際用來儲存的 key-value pair, 正是它們實際構成了我們的資料庫。

Command Parser

下面來看一些 命令解析器 (command parser) , 它負責解釋接收到的訊息:

def parse_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(‘;’)

if value_type:

if value_type == ‘LIST’:

value = value。split(‘,’)

elif value_type == ‘INT’:

value = int(value)

else:

value = str(value)

else:

value = None

return command, key, value

這裡我們可以看到發生了型別轉換 (type conversion)。 如果希望值是一個 list, 我們可以透過對 string 呼叫 str。split(‘,’) 來得到我們想要的值。 對於 int, 我們可以簡單地使用引數為 string 的 int() 即可。 對於字串與 str() 也是同樣的道理。

Command Handlers

下面是命令處理器 (command handler) 的程式碼。 它們都十分直觀,易於理解。 注意到雖然有很多的錯誤檢查, 但是也並不是面面俱到, 十分龐雜。 在你閱讀的過程中,如果發現有任何錯誤請移步 這裡 進行討論。

def update_stats(command, success):

“”“Update the STATS dict with info about if executing *command* was a

*success*”“”

if success:

STATS[command][‘success’] += 1

else:

STATS[command][‘error’] += 1

def handle_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))

def handle_get(key):

“”“Return a tuple containing True if the key exists and the message to send

back to the client”“”

if key not in DATA:

return (False, ‘Error: Key [{}] not found’。format(key))

else:

return (True, DATA[key])

def handle_putlist(key, value):

“”“Return a tuple containing True if the command succeeded and the message

to send back to the client。”“”

return handle_put(key, value)

def handle_putlist(key, value):

“”“Return a tuple containing True if the command succeeded and the message

to send back to the client”“”

return handle_put(key, value)

def handle_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)

if not exists:

return return_value

elif not isinstance(value, list):

return (False, ‘ERROR: Key [{}] contains non-list value ([{}])’。format(

key, value))

else:

return return_value

def handle_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)

if not exists:

return return_value

elif not isinstance(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))

def handle_delete(key):

“”“Return a tuple containing True if the key could be deleted and the

message to send back to the client。”“”

if key not in DATA:

return (

False,

’ERROR: Key [{}] not found and could not be deleted。‘。format(key))

else:

del DATA[key]

def handle_stats():

“”“Return a tuple containing True and the contents of the STATS dict。”“”

return (True, str(STATS))

有兩點需要注意: 多重賦值 (multiple assignment) 和程式碼重用。 有些函式僅僅是為了更加有邏輯性而對已有函式的簡單包裝而已, 比如 handle_get 和 handle_getlist 。 由於我們有時僅僅是需要一個已有函式的返回值,而其他時候卻需要檢查該函式到底返回了什麼內容, 這時候就會使用 多重賦值 。

來看一下 handle_append 。 如果我們嘗試呼叫 handle_get 但是 key 並不存在時, 那麼我們簡單地返回 handle_get 所返回的內容。 此外, 我們還希望能夠將 handle_get 返回的 tuple 作為一個單獨的返回值進行引用。 那麼當 key 不存在的時候, 我們就可以簡單地使用 return return_value 。

如果它 確實存在 , 那麼我們需要檢查該返回值。並且, 我們也希望能夠將 handle_get 的返回值作為單獨的變數進行引用。 為了能夠處理上述兩種情況,同時考慮需要分開處理結果的情形,我們使用了多重賦值。 如此一來, 就不必書寫多行程式碼, 同時能夠保持程式碼清晰。 return_value = exists, list_value = handle_get(key) 能夠顯式地表明我們將要以至少兩種不同的方式引用 handle_get 的返回值。

How Is This a Database?

上面的程式顯然並非一個 RDBMS, 但卻絕對稱得上是一個 NoSQL 資料庫。它如此易於建立的原因是我們並沒有任何與 資料 (data) 的實際互動。 我們只是做了極簡的型別檢查,儲存使用者所傳送的任何內容。 如果需要儲存更加結構化的資料, 我們可能需要針對資料庫建立一個 schema 用於儲存和檢索資料。

既然 NoSQL 資料庫更容易寫, 更容易維護,更容易實現, 那麼我們為什麼不是隻使用 mongoDB 就好了? 當然是有原因的, 還是那句話,有得必有失, 我們需要在 NoSQL 資料庫所提供的資料靈活性 (data flexibility) 基礎上權衡資料庫的可搜尋性 (searchability)。

Querying Data

假如我們上面的 NoSQL 資料庫來儲存早前的 Car 資料。 那麼我們可能會使用 VIN 作為 key, 使用一個列表作為每列的值, 也就是說, 2134AFGER245267 = [’Lexus‘, ’RX350‘, 2013, Black] 。 當然了, 我們已經丟掉了列表中每個索引的 涵義 (meaning) 。 我們只需要知道在某個地方索引 1 儲存了汽車的 Model , 索引 2 儲存了 Year。

糟糕的事情來了, 當我們想要執行先前的查詢語句時會發生什麼? 找到 1994 年所有車的顏色將會變得噩夢一般。 我們必須遍歷 DATA 中的 每一個值 來確認這個值是否儲存了 car 資料亦或根本是其他不相關的資料, 比如說檢查索引 2, 看索引 2 的值是否等於 1994,接著再繼續取索引 3 的值。 這比 table scan 還要糟糕,因為它不僅要掃描每一行資料,還需要應用一些複雜的規則來回答查詢。

NoSQL 資料庫的作者當然也意識到了這些問題,(鑑於查詢是一個非常有用的 feature) 他們也想出了一些方法來使得查詢變得不那麼 “遙不可及”。一個方法是結構化所使用的資料,比如 JSON, 允許引用其他行來表示關係。 同時, 大部分 NoSQL 資料庫都有名字空間 (namespace) 的概念, 單一型別的資料可以被儲存在資料庫中該型別所獨有的 “section” 中,這使得查詢引擎能夠利用所要查詢資料的 “shape” 資訊。

當然了,儘管為了增強可查詢性已經存在 (並且實現了)了一些更加複雜的方法, 但是在儲存更少量的 schema 與增強可查詢性之間做出妥協始終是一個不可逃避的問題。 本例中我們的資料庫僅支援透過 key 進行查詢。 如果我們需要支援更加豐富的查詢, 那麼事情就會變得複雜的多了。

Summary

至此, 希望 “NoSQL” 這個概念已然十分清晰。 我們學習了一點 SQL, 並且瞭解了 RDBMS 是如何工作的。 我們看到了如何從一個 RDBMS 中檢索資料 (使用 SQL 查詢 (query))。 透過搭建了一個玩具級別的 NoSQL 資料庫, 瞭解了在可查詢性與簡潔性之間面臨的一些問題, 還討論了一些資料庫作者應對這些問題時所採用的一些方法。

即便是簡單的 key-value 儲存, 關於資料庫的知識也是浩瀚無窮。雖然我們僅僅是探討了其中的星星點點, 但是仍然希望你已經瞭解了 NoSQL 到底指的是什麼, 它是如何工作的, 什麼時候用比較好。如果您想要分享一些不錯的想法, 歡迎 討論。

編譯:liuchengxu 英文:Jeff Knupp

http://

segmentfault。com/a/1190

000009436035

你想更深入瞭解學習Python知識體系,你可以看一下我們花費了一個多月整理了上百小時的幾百個知識點體系內容:

【超全整理】《Python自動化全能開發從入門到精通》筆記全放送

新增群聊515237230,和500+知乎小夥伴一起學Python!