用 Python 寫一個 NoSQL 數據庫

NoSQL 這個詞在近些年正變得隨處可見. 可是到底 「NoSQL」 指的是什麼? 它是如何而且爲何這麼有用? 在本文, 咱們將會經過純 Python (我比較喜歡叫它, 「輕結構化的僞代碼」) 寫一個 NoSQL 數據庫來回答這些問題.git

OldSQL

加羣:923414804 免費獲取數十套PDF資料github

不少狀況下, 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 的表可能有如下一些列:sql

  • 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 來指定查詢一輛車:app

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 服務器用來傳遞消息
  • 一些像 INCREMENTDELETE , 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 進行查詢。 若是咱們須要支持更加豐富的查詢, 那麼事情就會變得複雜的多了。

相關文章
相關標籤/搜索