转一篇MYSQL文章《数据库表设计,没有最好只有最适合》
http://mp.weixin.qq.com/s/a8klpzM5iam0_JYSw7-U4g
我们在设计数据库的时候,是否会突破常规,找到最适合自己需求的设计方案,下面来举个例子:
常用的邻接表设计,都会添加 一个 parent_id 字段,比如区域表(国、省、市、区):
CREATE TABLE Area (
[id] [int] NOT NULL,
[name] [nvarchar] (50) NULL,
[parent_id] [int] NULL,
[type] [int] NULL );
name:地域的名称, parent_id 是父ID,省的父ID是国,市的父ID 为省,以此类推。
type 是区域的阶级: 1:国,2:省,3:市,4:区
在层级比较确定的情况下,这么设计表格没有什么问题,调用起来也很方便。
但是使用这种邻接表设计方式,并不能满足所有的需求,当我们不确定层级的情况下,假设我有下面一个评论结构:
用邻接表记录这个评论的数据(comments 表):
大家有没发现,这么设计表,如果要查询一个节点的所有后代,是很难实现的,你可以使用关联查询来获取一条评论和他的后代:
SELECT c1.*, c2.* FROM comments c1 LEFT OUTER JOIN comments c2 ON c2.parent_id = c1.comment_id;
然而这个查询只能获取两层的数据。这种树的特性就是可以任意深地拓展,你需要有相应的方法来获取它的深度数据。比如,可能需要计算一个评论分支的数量,或者计算一个机械设备的所有的总开销。
某些情况下,在项目中使用邻接表正好适用。邻接表设计的优势在于能快速的获取一个给定节点的直接父子节点,它也很容易插入新节点。如果这样的需求就是你的项目对于分层数据的全部操作,那使用邻接表就可以很好的工作了。
遇到上述的树模型,有几种方案是可以考虑下的:路径枚举、嵌套集以及闭包表。这些解决方案通常看上去比邻接表复杂很多,但它们的确使得某些使用邻接表比较复杂或很低效的操作变得更简单。如果你的项目确实需要提供这些操作,那么这些设计会是邻接表更好的选择。
一、路径枚举
在comments 表中,我们使用类型varchar 的path 字段来替代原来的parent_id 字段。这个path 字段所存储的内容为当前节点的最顶层祖先到它的自己的序列,就像UNIX的路径一样,你甚至可以使用 ‘/’ 作为路径的分隔符。
你可以通过比较每个节点的路径来查询一个节点祖先。比如:要找到评论#7, 路径是 1/4/5/7一 的祖先,可以这么做:
SELECT * FROM comments AS c WHERE '1/4/5/7' LIKE c.path || '%' ;
这句话查询语句会匹配到路径为 1/4/5/%,1/4/% 以及 1/% 的节点,而这些节点就是评论#7的祖先。
同时还可以通过将LIKE 关键字两边的参数互换,来查询一个给定节点的所有后代。比如查询评论#4,路径path为 ‘1/4’ 的所有后代,可以使用如下语句:
SELECT * FROM comemnts AS c WHERE c.path LIKE '1/4' || '%' ;
这句查询语句所有能找到的后台路径分别是:1/4/5、1/4/5/6、1/4/5/7。
一旦你可以很简单地获取一棵子树或者从子孙节点到祖先节点的路径,你就可以很简单地实现更多的查询,如查询一颗子树所有节点上值的总和。
插入一个节点也可以像使用邻接表一样地简单。你所需要做的只是复制一份要插入节点的父亲节点路径,并将这个新节点的ID追加到路径末尾即可。
路径枚举也存在一些缺点,比如数据库不能确保路径的格式总是正确或者路径中的节点确实存在。依赖于应用程序的逻辑代码来维护路径的字符串,并且验证字符串的正确性开销很大。无论将varchar 的长度设定为多大,依旧存在长度的限制,因而并不能够支持树结构无限扩展。
二、 嵌套集
嵌套集解决方案是存储子孙节点的相关信息,而不是节点的直接祖先。我们使用两个数字来编码每个节点,从而表示这一信息,可以将这两个数字称为nsleft 和 nsright。
每个节点通过如下的方式确定nsleft 和nsright 的值:nsleft的数值小于该节点所有后代ID,同时nsright 的值大于该节点的所有后代的ID。这些数字和comment_id 的值并没有任何关联。
确定这三个值(nsleft,comment_id,nsright)的简单方法是对树进行一次深度优先遍历,在逐层深入的过程中依次递增地分配nsleft的值,并在返回时依次递增地分配nsright的值。得到数据如下:
一旦你为每个节点分配了这些数字,就可以使用它们来找到指定节点的祖先和后代。比如搜索评论#4及其所有后代,可以通过搜索哪些节点的ID在评论 #4 的nsleft 和 nsright 范围之间,例:
SELECT c2.* FROM comments AS c1 JOIN comments AS c2 ON c2.nsleft BETWEEN c1.nsleft
AND c1.nsright WHERE c1.comment_id = 4;
比如搜索评论#6及其所有祖先,可以通过搜索#6的ID在哪些节点的nsleft 和 nsright 范围之间,例:
SELECT c2.* FROM comments AS c1 JOIN comments AS c2 ON c1.nsleft BETWEEN c2.nsleft
AND c2.nsright WHERE c1.comment_id = 6;
使用嵌套集设计的主要优势是,当你想要删除一个非叶子节点时,它的后代会自动替代被删除的节点,成为其直接祖先节点的直接后代。就是说已经自动减少了一层。
然而,某些在邻接表的设计中表现得很简单的查询,比如获取一个节点的直接父亲或者直接后代,在嵌套集设计中会变得比较复杂。在嵌套集中,如果需要查询一个节点的直接父亲,我们会这么做,比如要找到评论#6 的直接父亲:
SELECT parent.* FROM comments AS c JOIN comments AS parent ON c.nsleft BETWEEN parent.nsleft ANDparent.nsright
LEFT OUTER JOIN comments AS in_between ON c.nsleft BETWEEN in_between.nsleft AND in_between.nsright
AND in_between.nsleft BETWEEN parent.nsleft AND parent.nsright WHERE c.comment_id = 6
AND in_between.comment_id IS NULL;
总之有些复杂。
对树进行操作,比如插入和移动节点,使用嵌套集会比其它设计复杂很多。当插入一个新节点时,你需要重新计算新插入节点的相邻兄弟节点、祖先节点和它祖先节点的兄弟,来确保他们的左右值都比这个新节点的左值大。同时,如果这个新节点时一个非叶子节点,你还要检查它的子孙节点。
如果简单快速查询是整个程序中最重要的部分,嵌套集是最好的选择,比操作单独的节点要方便快捷很多。然而,嵌套集的插入和移动节点是比较复杂的,因为需要重新分配左右值,如果你的应用程序需要频繁的插入、删除节点,那么嵌套集可能并不合适。
三、闭包表
闭包表是解决分级存储的一个简单而优雅的解决方案,它记录了树中所有节点间的关系,而不仅仅只有那些直接的父子节点。
在设计评论系统时,我们额外创建了一个叫 tree_paths 表,它包含两列,每一列都指向 comments 中的外键。
我们不再使用comments 表存储树的结构,而是将树中任何具有(祖先 一 后代)关系的节点对都存储在treepaths 表里,即使这两个节点之间不是直接的父子关系;同时,我们还增加一行指向节点自己。
通过treepaths 表来获取祖先和后代比使用嵌套集更加的直接。例如要获取评论#4的后代,只需要在 treepaths 表中搜索祖先是评论 #4的行就行了。同样获取后代也是如此。
要插入一个新的叶子节点,比如评论#6的一个子节点,应首先插入一条自己到自己的关系,然后搜索 treepaths 表中后代是评论#6 的节点,增加该节点和新插入节点的“祖先一后代”关系(新节点ID 应该为8):
INSERT INTO treepaths (ancestor, descendant)
SELECT t.ancestor, 8
FROM treepaths AS t
WHERE t.descendant = 6
UNION ALL SELECT 8, 8;
要删除一个叶子节点,比如评论#7, 应删除所有treepaths 表中后代为评论 #7 的行:
DELETE FROM treepaths WHERE descendant = 7;
要删除一颗完整的子树,比如评论#4 和它所有的后代,可删除所有在 treepaths 表中后代为 #4的行,以及那些以评论#4后代为后代的行。
闭包表的设计比嵌套集更加的直接,两者都能快捷地查询给定节点的祖先和后代,但是闭包表能更加简单地维护分层信息。这两个设计都比使用邻接表或者路径枚举更方便地查询给定节点的直接后代和祖先。
然而你可以优化闭包表来使它更方便地查询直接父亲节点或者子节点: 在 treepaths 表中添加一个 path_length 字段。一个节点的自我引用的path_length 为0,到它直接子节点的path_length 为1,再下一层为2,以此类推。这样查询起来就方便多了。
总结:你该使用哪种设计?
每种设计都各有优劣,如何选择设计,依赖于应用程序的哪种操作是你最需要性能上的优化。
层级数据设计比较
1、邻接表是最方便的设计,并且很多程序员都了解它
2、如果你使用的数据库支持WITH 或者 CONNECT BY PRIOR 的递归查询,那能使得邻接表的查询更高效。
3、枚举路径能够很直观地展示出祖先到后代之间的路径,但同时由于它不能确保引用完整性,使得这个设计非常脆弱。枚举路径也使得数据的存储变得比较冗余。
4、嵌套集是一个聪明的解决方案,但可能过于聪明,它不能确保引用完整性。最好在一个查询性能要求很高而对其他要求一般的场合来使用它。
5、闭包表是最通用的设计,并且以上的方案也只有它能允许一个节点属于多棵树。它要求一张额外的表来存储关系,使用空间换时间的方案减少操作过程中由冗余的计算所造成的消耗。
这几种设计方案只是我们日常设计中的一部分,开发中肯定会遇到更多的选择方案。选择哪一种方案,是需要切合实际,根据自己项目的需求,结合方案的优劣,选择最适合的一种。
我遇到一些开发人员,为了敷衍了事,在设计数据库表时,只考虑能否完成眼下的任务,不太注重以后拓展的问题,不考虑查询起来是否耗性能。可能前期数据量不多的时候,看不出什么影响,但数据量稍微多一点的话,就已经显而易见了(例如:可以使用外联接查询的,偏偏要使用子查询)。
我觉得设计数据库是一个很有趣且充满挑战的工作,它有时能体现你的视野有多宽广,有时它能让你睡不着觉,总之痛并快乐着。
转一篇MYSQL文章《数据库表设计,没有最好只有最适合》的更多相关文章
- 数据库表设计时一对一关系存在的必要性 数据库一对一、一对多、多对多设计 面试逻辑题3.31 sql server 查询某个表被哪些存储过程调用 DataTable根据字段去重 .Net Core Cors中间件解析 分析MySQL中哪些情况下数据库索引会失效
数据库表设计时一对一关系存在的必要性 2017年07月24日 10:01:07 阅读数:694 在表设计过程中,我无意中觉得一对一关系觉得好没道理,直接放到一张表中不就可以了吗?真是说,网上信息什么都 ...
- Innodb IO优化 — 数据库表设计 转
数据库表设计这块学问比较多,我这里单从互联网角度出发同时结合Innodb的特性给出一些设计方法供大家参考.本文构建大概分两分部分:Innodb的特性及设计中如何利用这种特性. Innodb特性: In ...
- Oracle数据库表设计时的注意事项
表是Oracle数据库中最基本的对象之一.万丈高楼从平地起,这个基础对象对于数据库来说,非常重要.因为其设计是否合理,直接跟数据库的性能相关.从Oracle数据库菜鸟到数据库专家这个过程中,在表设计与 ...
- MySQL更改数据库表的存储引擎
MySQL更改数据库表的存储引擎 1.查看表的原存储引擎 show create table user; 'user', 'CREATE TABLE `user` (\n `id` int(11) N ...
- springmvc 项目完整示例01 需求与数据库表设计 简单的springmvc应用实例 web项目
一个简单的用户登录系统 用户有账号密码,登录ip,登录时间 打开登录页面,输入用户名密码 登录日志,可以记录登陆的时间,登陆的ip 成功登陆了的话,就更新用户的最后登入时间和ip,同时记录一条登录记录 ...
- MySQL查看数据库表容量大小
本文介绍MySQL查看数据库表容量大小的命令语句,提供完整查询语句及实例,方便大家学习使用. 1.查看所有数据库容量大小 select table_schema as '数据库', sum(table ...
- MySQL中将数据库表名修改成大写的存储过程
原文:MySQL中将数据库表名修改成大写的存储过程 MySQL中将数据库表名修改成大写的存储过程 创建存储过程的代码: DROP PROCEDURE IF EXISTS uppercaseTablen ...
- mysql设计与优化以及数据库表设计与表开发规范
一.设计问题? 1.主键是用自增还是UUID ? Innodb 中的主键是聚簇索引. 如果主键是自增的,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的 ...
- 数据库设计-Mysql数据库表设计的过程中几个关键点
一.表设计过程中应该注意的数据类型 1)更小的通常更好 控制字节长度 2)使用合适的数据类型: 如tinyint只占8个位,char(1024)与varchar(1024)的对比,char用于类似定长 ...
随机推荐
- Linux磁盘和文件系统知识总结
硬盘操作 为什么要给硬盘分区? 如果你需要在一块硬盘上用到多个文件系统,那么你就需要对硬盘进行分区,以便用不同的分区支持不同的文件系统.(但一个硬盘只能有一个分区表!)反过来说,如果你整块硬盘都用同样 ...
- mysql外键,锁
mysql外键: 场景:用于建立两个表之间的联系,让A表中一个字段,可以在另一个表中字段值的范围去查找 注意事项: (1)被参照表和参照表字段属性必须一致 (2)参照表必须设置主键 (3)必须选择支持 ...
- 拥有webkit内核浏览器的平台
1-拥有webkit内核的浏览器: IOS safari Android 的浏览器 Google chrome 猎豹浏览器 百度浏览器 Opera 以上可知手机的浏览器均为webkit内核 2-拥有其 ...
- React简单教程-6-单元测试
前言 我想大部分人的前端测试,都是运行项目,直接在浏览器上操作,看看功能正不正常.虽然明明有测试库可以使用,但是因为"要快"的原因,让好好做测试变成了一件影响效率的事. 因为这种无 ...
- 面试突击55:delete、drop、truncate有什么区别?
在 MySQL 中,删除的方法总共有 3 种:delete.truncate.drop,而三者的用法和使用场景又完全不同,接下来我们具体来看. 1.delete detele 可用于删除表的部分或所有 ...
- VmWare安装Centos8注意事项
VmWare安装Centos8注意事项 1.需选择稍后安装操作系统 2.选择操作系统版本 3.修改虚拟机配置 4.配置完成点击开启虚拟机(注意要将鼠标放在屏幕中央,点击一下后才能使用上下键进行选择) ...
- JS:函数的形参与实参
形参: 函数显式参数在函数定义时列出. 函数调用未传参时,参数会默认设置为: undefined. function fn(a,b,c){ //a,b,c为形参 //此时有一个隐式操作:var a,v ...
- 如何使用lerna进行多包(package)管理
为什么要用lerna 将大型代码仓库分割成多个独立版本化的 软件包(package)对于代码共享来说非常有用.但是,如果某些更改 跨越了多个代码仓库的话将变得很 麻烦 并且难以跟踪,并且, 跨越多个代 ...
- 【Github】 Github修改仓库的基本信息
前言 我们通常在刚开始了解学习使用github时,一般都是测试的使用,有时我们向里面添加了一些代买,如果想要修改信息并且是删除仓库重新创建提交,可以采用下面方法修改仓库信息,名称.描述等. 修改仓库描 ...
- BUUCTF-神秘龙卷风
神秘龙卷风 通过提示知道压缩包密码是四位纯数字,通过爆破得到 得到一串编码 看样子应该是brainfuck编码 flag{e4bbef8bdf9743f8bf5b727a9f6332a8}