mysql的nosql功能_“NoSQL”的定义、作用和使用方法详细说明

这仅是一个极简的demo,旨在动手了解概念.

8f2a549d441c40e1967fe66ab8efa12b.png

NoSQL这个词在近些年正变得随处可见.但是到底“NoSQL”指的是什么?它是如何并且为什么这么有用?在本文,我们将会通过纯Python(我比较喜欢叫它,“轻结构化的伪代码”)写一个NoSQL数据库来回答这些问题.

OldSQL

很多情况下,SQL已经成为“数据库”(database)的一个同义词.实际上,SQL是StrcturedQueryLanguage的首字母缩写,而并非指数据库技术本身.更确切地说,它所指的是从RDBMS(关系型数据库管理系统,RelationalDatabaseManagementSystem)中检索数据的一门语言.MySQL,MSSQLServer和Oracle都属于RDBMS的其中一员.

RDBMS中的R,即“Relational”(有关系,关联的),是其中内容最丰富的部分.数据通过表(table)进行组织,每张表都是一些由类型(type)相关联的列(column)构成.所有表,列及其类的类型被称为数据库的schema(架构或模式).schema通过每张表的描述信息完整刻画了数据库的结构.比如,一张叫做Car的表可能有以下一些列:

Make:astring

Model:astring

Year:afour-digitnumber;alternatively,adate

Color:astring

VIN(VehicleIdentificationNumber):astring

在一张表中,每个单一的条目叫做一行(row),或者一条记录(record).为了区分每条记录,通常会定义一个主键(primarykey).表中的主键是其中一列,它能够唯一标识每一行.在表Car中,VIN是一个天然的主键选择,因为它能够保证每辆车具有唯一的标识.两个不同的行可能会在Make,Model,Year和Color列上有相同的值,但是对于不同的车而言,肯定会有不同的VIN.反之,只要两行拥有同一个VIN,我们不必去检查其他列就可以认为这两行指的的就是同一辆车.

Querying

SQL能够让我们通过对数据库进行query(查询)来获取有用的信息.查询简单来说,查询就是用一个结构化语言向RDBMS提问,并将其返回的行解释为问题的答案.假设数据库表示了美国所有的注册车辆,为了获取所有的记录,我们可以通过在数据库上进行如下的SQL查询:

SELECTMake,ModelFROMCar;

将SQL大致翻译成中文:

“SELECT”:“向我展示”

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

“FROMCar”:“对表Car中的每一行”

也就是,“向我展示表Car每一行中Make和Model的值”.执行查询后,我们将会得到一些查询的结果,其中每个都是Make和Model.如果我们仅关心在1994年注册的车的颜色,那么可以:

SELECTColorFROMCarWHEREYear=1994;

此时,我们会得到一个类似如下的列表:

Black

Red

Red

White

Blue

Black

White

Yellow

最后,我们可以通过使用表的(primarykey)主键,这里就是VIN来指定查询一辆车:

SELECT*FROMCarWHEREVIN='2134AFGER245267'

上面这条查询语句会返回所指定车辆的属性信息.

主键被定义为唯一不可重复的.也就是说,带有某一指定VIN的车辆在表中至多只能出现一次.这一点非常重要,为什么?来看一个例子:

Relations

假设我们正在经营一个汽车修理的业务.除了其他一些必要的事情,我们还需要追踪一辆车的服务历史,即在该辆车上所有的修整记录.那么我们可能会创建包含以下一些列的ServiceHistory表:

VIN|Make|Model|Year|Color|ServicePerformed|Mechanic|Price|Date

这样,每次当车辆维修以后,我们就在表中添加新的一行,并写入该次服务我们做了一些什么事情,是哪位维修工,花费多少和服务时间等.

但是等一下,我们都知道,对于同一辆车而言,所有车辆自身信息有关的列是不变的。也就是说,如果把我的Black2014LexusRX350修整10次的话,那么即使Make,Model,Year和Color这些信息并不会改变,每一次仍然重复记录了这些信息.与无效的重复记录相比,一个更合理的做法是对此类信息只存储一次,并在有需要的时候进行查询。

那么该怎么做呢?我们可以创建第二张表:Vehicle,它有如下一些列:

VIN|Make|Model|Year|Color

这样一来,对于ServiceHistory表,我们可以精简为如下一些列:

VIN|ServicePerformed|Mechanic|Price|Date

你可能会问,为什么VIN会在两张表中同时出现?因为我们需要有一个方式来确认在ServiceHistory表的这辆车指的就是Vehicle表中的那辆车,也就是需要确认两张表中的两条记录所表示的是同一辆车。这样的话,我们仅需要为每辆车的自身信息存储一次即可.每次当车辆过来维修的时候,我们就在ServiceHistory表中创建新的一行,而不必在Vehicle表中添加新的记录。毕竟,它们指的是同一辆车。

我们可以通过SQL查询语句来展开Vehicle与ServiceHistory两张表中包含的隐式关系:

SELECTVehicle.Model,Vehicle.YearFROMVehicle,ServiceHistoryWHEREVehicle.VIN=ServiceHistory.VINANDServiceHistory.Price>75.00;

该查询旨在查找维修费用大于$75.00的所有车辆的Model和Year.注意到我们是通过匹配Vehicle与ServiceHistory表中的VIN值来筛选满足条件的记录.返回的将是两张表中符合条件的一些记录,而“Vehicle.Model”与“Vehicle.Year”,表示我们只想要Vehicle表中的这两列.

如果我们的数据库没有索引(indexes)(正确的应该是indices),上面的查询就需要执行表扫描(tablescan)来定位匹配查询要求的行。tablescan是按照顺序对表中的每一行进行依次检查,而这通常会非常的慢。实际上,tablescan实际上是所有查询中最慢的。

可以通过对列加索引来避免扫描表。我们可以把索引看做一种数据结构,它能够通过预排序让我们在被索引的列上快速地找到一个指定的值(或指定范围内的一些值).也就是说,如果我们在Price列上有一个索引,那么就不需要一行一行地对整个表进行扫描来判断其价格是否大于75.00,而是只需要使用包含在索引中的信息“跳”到第一个价格高于75.00的那一行,并返回随后的每一行(由于索引是有序的,因此这些行的价格至少是75.00)。

当应对大量的数据时,索引是提高查询速度不可或缺的一个工具。当然,跟所有的事情一样,有得必有失,使用索引会导致一些额外的消耗:索引的数据结构会消耗内存,而这些内存本可用于数据库中存储数据。这就需要我们权衡其利弊,寻求一个折中的办法,但是为经常查询的列加索引是非常常见的做法。

TheClearBox

得益于数据库能够检查一张表的schema(描述了每列包含了什么类型的数据),像索引这样的高级特性才能够实现,并且能够基于数据做出一个合理的决策。也就是说,对于一个数据库而言,一张表其实是一个“黑盒”(或者说透明的盒子)的反义词?

当我们谈到NoSQL数据库的时候要牢牢记住这一点。当涉及query不同类型数据库引擎的能力时,这也是其中非常重要的一部分。

Schemas

我们已经知道,一张表的schema,描述了列的名字及其所包含数据的类型。它还包括了其他一些信息,比如哪些列可以为空,哪些列不允许有重复值,以及其他对表中列的所有限制信息。在任意时刻一张表只能有一个schema,并且表中的所有行必须遵守schema的规定。

这是一个非常重要的约束条件。假设你有一张数据库的表,里面有数以百万计的消费者信息。你的销售团队想要添加额外的一些信息(比如,用户的年龄),以期提高他们邮件营销算法的准确度。这就需要来alter(更改)现有的表—添加新的一列。我们还需要决定是否表中的每一行都要求该列必须有一个值。通常情况下,让一个列有值是十分有道理的,但是这么做的话可能会需要一些我们无法轻易获得的信息(比如数据库中每个用户的年龄)。因此在这个层面上,也需要有些权衡之策。

此外,对一个大型数据库做一些改变通常并不是一件小事。为了以防出现错误,有一个回滚方案非常重要。但即使是如此,一旦当schema做出改变后,我们也并不总是能够撤销这些变动。schema的维护可能是DBA工作中最困难的部分之一。

Key/ValueStores

在“NoSQL”这个词存在前,像memcached这样的键/值数据存储(Key/ValueDataStores)无须tableschema也可提供数据存储的功能。实际上,在K/V存储时,根本没有“表(table)”的概念。只有键(keys)与值(values).如果键值存储听起来比较熟悉的话,那可能是因为这个概念的构建原则与Python的dict与set相一致:使用hashtable(哈希表)来提供基于键的快速数据查询。一个基于Python的最原始的NoSQL数据库,简单来说就是一个大的字典(dictionary).

为了理解它的工作原理,亲自动手写一个吧!首先来看一下一些简单的设计想法:

一个Python的dict作为主要的数据存储

仅支持string类型作为键(key)

支持存储integer,string和list

一个使用ASCLLstring的简单TCP/IP服务器用来传递消息

一些像INCREMENT,DELETE,APPEND和STATS这样的高级命令(command)

有一个基于ASCII的TCP/IP接口的数据存储有一个好处,那就是我们使用简单的telnet程序即可与服务器进行交互,并不需要特殊的客户端(尽管这是一个非常好的练习并且只需要15行代码即可完成)。

对于我们发送到服务器及其它的返回信息,我们需要一个“有线格式”。下面是一个简单的说明:

CommandsSupported

PUT

参数:Key,Value

目的:向数据库中插入一条新的条目(entry)

GET

参数:Key

目的:从数据库中检索一个已存储的值

PUTLIST

参数:Key,Value

目的:向数据库中插入一个新的列表条目

APPEND

参数:Key,Value

目的:向数据库中一个已有的列表添加一个新的元素

INCREMENT

参数:key

目的:增长数据库的中一个整型值

DELETE

参数:Key

目的:从数据库中删除一个条目

STATS

参数:无(N/A)

目的:请求每个执行命令的成功/失败的统计信息

现在我们来定义消息的自身结构。

MessageStructure

RequestMessages

一条请求消息(RequestMessage)包含了一个命令(command),一个键(key),一个值(value),一个值的类型(type).后三个取决于消息类型,是可选项,非必须。;被用作是分隔符。即使并没有包含上述可选项,但是在消息中仍然必须有三个;字符。

COMMAND;[KEY];[VALUE];[VALUETYPE]

COMMAND是上面列表中的命令之一

KEY是一个可以用作数据库key的string(可选)

VALUE是数据库中的一个integer,list或string(可选)

list可以被表示为一个用逗号分隔的一串string,比如说,“red,green,blue”

VALUETYPE描述了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;;

ReponseMessages

一个响应消息(ReponseMessage)包含了两个部分,通过;进行分隔。第一个部分总是True|False,它取决于所执行的命令是否成功。第二个部分是命令消息(commandmessage),当出现错误时,便会显示错误信息。对于那些执行成功的命令,如果我们不想要默认的返回值(比如PUT),就会出现成功的信息。如果我们返回成功命令的值(比如GET),那么第二个部分就会是自身值。

Examples

True;Key[foo]setto[1]

True;1

True;Key[bar]setto[['a','b','c']]

True;Key[bar]hadvalue[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}}

ShowMeTheCode!

我将会以块状摘要的形式来展示全部代码。整个代码不过180行,读起来也不会花费很长时间。

SetUp

下面是我们服务器所需的一些样板代码:

"""NoSQLdatabasewritteninPython"""

#Standardlibraryimports

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

}

很容易看到,上面的只是一个包的导入和一些数据的初始化。

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

defmain():

"""Mainentrypointforscript"""

SOCKET.bind(HOST,PORT)

SOCKET.listen(1)

while1:

connection,address=SOCKET.accept()

print('Newconnectionfrom[{}]'.format(address))

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

command,key,value=parse_message(data)

ifcommand=='STATS':

response=handle_stats()

elifcommandin('GET','GETLIST','INCREMENT','DELETE'):

response=COMMAND_HANDERS[command](key)

elifcommandin(

'PUT',

'PUTLIST',

'APPEND',):

response=COMMAND_HANDERS[command](key,value)

else:

response=(False,'Unknowncommandtype{}'.format(command))

update_stats(command,response[0])

connection.sandall('{};{}'.format(response[0],response[1]))

connection.close()

if__name__=='__main__':

main()

我们创建了COMMAND_HANDLERS,它常被称为是一个查找表(look-uptable).COMMAND_HANDLERS的工作是将命令与用于处理该命令的函数进行关联起来。比如说,如果我们收到一个GET命令,COMMAND_HANDLERS[command](key)就等同于说handle_get(key).记住,在Python中,函数可以被认为是一个值,并且可以像其他任何值一样被存储在一个dict中。

在上面的代码中,虽然有些命令请求的参数相同,但是我仍决定分开处理每个命令。尽管可以简单粗暴地强制所有的handle_函数接受一个key和一个value,但是我希望这些处理函数条理能够更加有条理,更加容易测试,同时减少出现错误的可能性。

注意socket相关的代码已是十分极简。虽然整个服务器基于TCP/IP通信,但是并没有太多底层的网络交互代码。

最后还须需要注意的一小点:DATA字典,因为这个点并不十分重要,因而你很可能会遗漏它。DATA就是实际用来存储的key-valuepair,正是它们实际构成了我们的数据库。

CommandParser

下面来看一些命令解析器(commandparser),它负责解释接收到的消息:

defparse_message(data):

"""Returnatuplecontainingthecommand,thekey,and(optionally)the

valuecasttotheappropriatetype."""

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

这里我们可以看到发生了类型转换(typeconversion).如果希望值是一个list,我们可以通过对string调用str.split(',')来得到我们想要的值。对于int,我们可以简单地使用参数为string的int()即可。对于字符串与str()也是同样的道理。

CommandHandlers

下面是命令处理器(commandhandler)的代码.它们都十分直观,易于理解。注意到虽然有很多的错误检查,但是也并不是面面俱到,十分庞杂。在你阅读的过程中,如果发现有任何错误请移步这里进行讨论.

defupdate_stats(command,success):

"""UpdatetheSTATSdictwithinfoaboutifexecuting*command*wasa

*success*"""

ifsuccess:

STATS[command]['success']+=1

else:

STATS[command]['error']+=1

defhandle_put(key,value):

"""ReturnatuplecontainingTrueandthemessagetosendbacktothe

client."""

DATA[key]=value

return(True,'key[{}]setto[{}]'.format(key,value))

defhandle_get(key):

"""ReturnatuplecontainingTrueifthekeyexistsandthemessagetosend

backtotheclient"""

ifkeynotinDATA:

return(False,'Error:Key[{}]notfound'.format(key))

else:

return(True,DATA[key])

defhandle_putlist(key,value):

"""ReturnatuplecontainingTrueifthecommandsucceededandthemessage

tosendbacktotheclient."""

returnhandle_put(key,value)

defhandle_putlist(key,value):

"""ReturnatuplecontainingTrueifthecommandsucceededandthemessage

tosendbacktotheclient"""

returnhandle_put(key,value)

defhandle_getlist(key):

"""ReturnatuplecontainingTrueifthekeycontainedalistandthe

messagetosendbacktotheclient."""

return_value=exists,value=handle_get(key)

ifnotexists:

returnreturn_value

elifnotisinstance(value,list):

return(False,'ERROR:Key[{}]containsnon-listvalue([{}])'.format(

key,value))

else:

returnreturn_value

defhandle_increment(key):

"""ReturnatuplecontainingTrueifthekey'svaluecouldbeincremented

andthemessagetosendbacktotheclient."""

return_value=exists,value=handle_get(key)

ifnotexists:

returnreturn_value

elifnotisinstance(list_value,list):

return(False,'ERROR:Key[{}]containsnon-listvalue([{}])'.format(

key,value))

else:

DATA[key].append(value)

return(True,'Key[{}]hadvalue[{}]appended'.format(key,value))

defhandle_delete(key):

"""ReturnatuplecontainingTrueifthekeycouldbedeletedandthe

messagetosendbacktotheclient."""

ifkeynotinDATA:

return(

False,

'ERROR:Key[{}]notfoundandcouldnotbedeleted.'.format(key))

else:

delDATA[key]

defhandle_stats():

"""ReturnatuplecontainingTrueandthecontentsoftheSTATSdict."""

return(True,str(STATS))

有两点需要注意:多重赋值(multipleassignment)和代码重用.有些函数仅仅是为了更加有逻辑性而对已有函数的简单包装而已,比如handle_get和handle_getlist.由于我们有时仅仅是需要一个已有函数的返回值,而其他时候却需要检查该函数到底返回了什么内容,这时候就会使用多重赋值。

来看一下handle_append.如果我们尝试调用handle_get但是key并不存在时,那么我们简单地返回handle_get所返回的内容。此外,我们还希望能够将handle_get返回的tuple作为一个单独的返回值进行引用。那么当key不存在的时候,我们就可以简单地使用returnreturn_value.

如果它确实存在,那么我们需要检查该返回值。并且,我们也希望能够将handle_get的返回值作为单独的变量进行引用。为了能够处理上述两种情况,同时考虑需要分开处理结果的情形,我们使用了多重赋值。如此一来,就不必书写多行代码,同时能够保持代码清晰。return_value=exists,list_value=handle_get(key)能够显式地表明我们将要以至少两种不同的方式引用handle_get的返回值。

HowIsThisaDatabase?

上面的程序显然并非一个RDBMS,但却绝对称得上是一个NoSQL数据库。它如此易于创建的原因是我们并没有任何与数据(data)的实际交互。我们只是做了极简的类型检查,存储用户所发送的任何内容。如果需要存储更加结构化的数据,我们可能需要针对数据库创建一个schema用于存储和检索数据。

既然NoSQL数据库更容易写,更容易维护,更容易实现,那么我们为什么不是只使用mongoDB就好了?当然是有原因的,还是那句话,有得必有失,我们需要在NoSQL数据库所提供的数据灵活性(dataflexibility)基础上权衡数据库的可搜索性(searchability).

QueryingData

假如我们上面的NoSQL数据库来存储早前的Car数据。那么我们可能会使用VIN作为key,使用一个列表作为每列的值,也就是说,2134AFGER245267=['Lexus','RX350',2013,Black].当然了,我们已经丢掉了列表中每个索引的涵义(meaning).我们只需要知道在某个地方索引1存储了汽车的Model,索引2存储了Year.

糟糕的事情来了,当我们想要执行先前的查询语句时会发生什么?找到1994年所有车的颜色将会变得噩梦一般。我们必须遍历DATA中的每一个值来确认这个值是否存储了car数据亦或根本是其他不相关的数据,比如说检查索引2,看索引2的值是否等于1994,接着再继续取索引3的值.这比tablescan还要糟糕,因为它不仅要扫描每一行数据,还需要应用一些复杂的规则来回答查询。

NoSQL数据库的作者当然也意识到了这些问题,(鉴于查询是一个非常有用的feature)他们也想出了一些方法来使得查询变得不那么“遥不可及”。一个方法是结构化所使用的数据,比如JSON,允许引用其他行来表示关系。同时,大部分NoSQL数据库都有名字空间(namespace)的概念,单一类型的数据可以被存储在数据库中该类型所独有的“section”中,这使得查询引擎能够利用所要查询数据的“shape”信息。

当然了,尽管为了增强可查询性已经存在(并且实现了)了一些更加复杂的方法,但是在存储更少量的schema与增强可查询性之间做出妥协始终是一个不可逃避的问题。本例中我们的数据库仅支持通过key进行查询。如果我们需要支持更加丰富的查询,那么事情就会变得复杂的多了。

Summary

至此,希望“NoSQL”这个概念已然十分清晰。我们学习了一点SQL,并且了解了RDBMS是如何工作的。我们看到了如何从一个RDBMS中检索数据(使用SQL查询(query)).通过搭建了一个玩具级别的NoSQL数据库,了解了在可查询性与简洁性之间面临的一些问题,还讨论了一些数据库作者应对这些问题时所采用的一些方法。

即便是简单的key-value存储,关于数据库的知识也是浩瀚无穷。虽然我们仅仅是探讨了其中的星星点点,但是仍然希望你已经了解了NoSQL到底指的是什么,它是如何工作的,什么时候用比较好。如果您想要分享一些不错的想法,欢迎讨论.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值