故而 Python 写一个 NoSQL 数据库

本文译自 What is a NoSQL Database? Learn By Writing One In
Python.

  • 整的演示代码已经放了 GitHub 上, 请
    点击这里,
    这就是一个极简的 demo, 旨在着手了解概念.
  • 若果对译文有外的意或者建议,欢迎 提
    issue
    讨论, 批评指正.

继往开来要发创新,可见
博客
.

nosql.png


NoSQL 这个词在近日正巧换得随处可见. 但是到底 “NoSQL” 指的凡啊?
它是何许以为什么如此来因此? 在本文, 我们拿会晤通过纯 Python
(我于欣赏让它, “轻结构化的伪代码”) 写一个 NoSQL 数据库来报这些问题.

OldSQL

成百上千景象下, SQL 已经改成 “数据库” (database) 的一个相同词. 实际上,
SQLStrctured 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 查询语句来进展 VehicleServiceHistory
两摆表中带有的隐式关系:

SELECT Vehicle.Model, Vehicle.Year FROM Vehicle, ServiceHistory WHERE Vehicle.VIN = ServiceHistory.VIN AND ServiceHistory.Price > 75.00;

欠查询旨在寻找维修费用逾 $75.00 的享有车辆的 Model 和 Year.
注意到我们是由此匹配 VehicleServiceHistory 表中的 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 的 dictset 相一致: 使用
hash table (哈希表) 来提供基于键的飞数据查询。 一个冲 Python
的尽原始之 NoSQL 数据库, 简单的话即使是一个万分之字典 (dictionary) .

为了解它的办事原理,亲自动手写一个咔嚓!
首先来拘禁一下有些粗略的计划性想法:

  • 一个 Python 的 dict 作为重要的数码存储
  • 无非支持 string 类型作为键 (key)
  • 支撑存储 integer, string 和 list
  • 一个施用 ASCLL string 的简 TCP/IP 服务器用来传递信息
  • 一些像 INCREMENT, DELETE , APPENDSTATS 这样的高档命令
    (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_gethandle_getlist .
由于我们有时候只是是亟需一个已经生函数的回值,而另时候可要检查该函数到底回了哟内容,
这下就会见以 大多还赋值

来拘禁一下 handle_append . 如果我们尝试调用 handle_get 但是 key
并无存时时, 那么我们大概地回到 handle_get 所返的情节。 此外,
我们尚想会将 handle_get 返回的 tuple
作为一个单身的回值进行引用。 那么当 key 不存在的时,
我们就是足以简单地利用 return return_value .

如果它 委是 , 那么我们要检查该返回值。并且, 我们呢想能用
handle_get 的返回值作为单身的变量进行引用。
为了能够处理上述两栽情景,同时考虑待分开处理结果的场面,我们运用了大多更赋值。
如此一来, 就不必书写多执代码NoSQL, 同时能够保持代码清晰。
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 到底指的是啊, 它是怎么样行事的,
什么时用比较好。如果你想如果享用部分毋庸置疑的想法, 欢迎
讨论.

网站地图xml地图