从AdventureWorks学习数据库建模——实体分析
最近打算写写数据库建模的文章,所以打算分析微软官方提供的SQL Server示例数据库AdventureWorks,看看这个数据库中有哪些值得学习的地方。
首先我们需要下载安装一个SQL Server数据库引擎,然后下载示例数据库,这里笔者用的是SQL2008R2,所以下载的是AdventureWorks2008R2,下载地址:
http://msftdbprodsamples.codeplex.com/
下载数据库后附加到SQL Server中即可看到这个数据库。
这是一个自行车制造和销售公司的数据库,该公司建立自己的销售网站,提供在线销售。首先看看这个数据库的结构,其建立了多个Schema,通过Schema来划分表所在的模块,比如HumanResources,Person,Production,Purchasing和Sales。如果是非常通用的表,比如日志表,那么就不属于任何模块,使用系统默认的Schema:dbo。
对于这么一个复杂的模型,我们可以按照:主要实体、附属实体、事务实体关联关系的顺序进行分析。
主要实体
对于整个系统来说,BusinessEntity是最核心的实体,用于表示一个“人”,这里的人是打引号的,因为它既可以表示真实的自然人,也可以表示:公司、组织甚至一个商店,可以认为是一个法人。对于这个数据库模型来说,有3个实体继承自BusinessEntity,那就是Person,Store,Vendor。
对于Person自然人来说,他可能是公司的员工,也可能是客户,所以我们又关联出了两个实体Employee和Customer。这里需要注意的是,在这个模型中,他并不把一个自然人标识为一个客户,而是对不同的Store,会形成不同的客户。也就是说对于公司来说,他并没有客户主数据,同一个人在不同的店消费,那么就会在不同的店中记为一个客户。为什么要这么设计,确实很奇怪,可能是业务上的需求吧。
这里延伸到Employee,就可以把HumanResources下面的实体分析一下,很显然Department是主要实体,至于Employee和Department之间的关系,我们接下来再分析,这里我们只找主要实体。Employee如果在销售部门,那么就是一个SalesPerson,所以这个实体是继承自Employee。另外在Production中还有一个很重要的实体Product,用于表示生产和销售的产品。
附属实体
所谓附属实体,就是依附于主要实体而存在,对主实体的属性进行补充的实体,如果主要实体不存在,那么附属实体里面的数据就没有意义。对于前面找的主要实体,我们一个一个的分析:
BusinessEntity
BusinessEntity有两个附属实体:BusinessEntityContact和BusinessEntityAddress,对于联系人实体,是和Person形成多对多关系,所以BusinessEntityContact是多对多产生的中间表,另外再加上ContactType说明联系人的类型。而对于业务实体的地址,系统也抽象出了一个Address表,使得BusinessEntity和Address之间形成多对多关系。
Person
对于Person表,关联的表分为两类,一类是一对多或多对多的普通关联表,比如一个人有多个PersonPhone,一个人有多个EmailAddress,或者一个人持有多张信用卡PersonCreditCard。这里把CreditCard和Person设置成多对多的关系,我想应该这里的CreditCard包含公司商务卡的情况,这种卡的真正持有人是公司,但是公司会派发给Sales用,如果Sales离职了,那么这张卡会收回,派发给其他的员工用,所以这就形成了多对多关系。另外一类是一对一的拆分或继承关系,比如Password表。如果是简单的设计,我们完全可以把Password相关字段放在Person表中,而这里却独立出来形成一对一关系,主要可能是以下几方面的原因:
安全考虑:Password的内容很机密,独立成表后可以单独对这个表进行加密,权限分配等。
性能考虑:Password的内容只用于登录系统时验证,以后接下来的所有查询都用不到这些字段,所以不放在Person表中,系统在查询Person表时就不需要连带着把不需要的字段查出来。
Employee
这里主要涉及到的是HumanResources下的表,除了员工的基本信息外还记录了员工的履历,工资变动,部门变动情况。一个Employee对应多个JobCandidate,为什么是一对多关系呢?因为应聘者可以制作个人简历的多个版本,然后投公司的不同部门,最后如果应聘者被录取了,那么就可以把JobCandidate中的BusinessEntityID设置为Employee的ID,如果应聘失败,那么BusinessEntityID就是NULL。EmployeePayHistory是员工的工资表,但是不是发工资的记录表,只是记录员工的工资基本信息,如果工资变动就创建一条新的记录。Employee和Department是多对多的关系,并不是因为一个员工身兼数职,在多个部门同时干活,而是因为要记录员工的部门调动情况,所以保留了所有历史记录,形成了多对多关系。另外比普通公司的部门员工表不同,这个系统还有一个轮班表Shift,那是因为这是个制造业公司也有门店进行销售,所以会分为早班,中班和晚班,一个员工的轮班是固定的,如果发生变化,比如以前是上夜班,现在改为上早班,那么EmployeeDepartmentHistory中也会对应生成一条新的记录。
Sales
销售继承至Employee,主要有销售区域,销售配额等附加的属性。本身销售区域和销售配额可以看做是Sales表的属性,但是为了记录历史,所以独立出来了一对多的表:SalesTerritoryHistory和SalesPersonQuotaHistory。
这里需要说明一下SalesTerritory表并不是Sales的附属表,他本身是一个独立的实体。
Product
这个实体应该是各个主实体中属性最复杂的实体了。主要分为ProductModel和Product两块。
先说ProductModel,可以理解为样品,样机或者是模型,在进行量产前需要先生产ProductModel。对于ProductModel,主要有产品的部件关系图Illustration和描述ProductDescription。ProductModel和Illustration是普通的多对多关系,一个模型有多个部件关系图,一个部件关系图也可以用于多个样机中。而对于描述,除了普通的多对多关系外,还增加了一个多语言的关系。于是增加了Culture表,形成了三个表的多对多关系。实际上这种多语言模型并不好,很容易产生错误,对于多语言的处理,可以建立更好的模型。
接下来就是Product实体,可以将相关的表分为三类:
多对一:产品的分类Category和前面提到的ProductModel。
一对多:产品成本历史ProductCostHistory,产品的组成BillOfMaterials,产品的库存ProductInventory,产品价格历史ProductListPriceHistory,产品的复查ProductReview。
多对多:产品文档ProductDocument和产品照片ProductPhoto。
产品分类没啥好说的,就是普通的二级分类法,一级大分类在ProductCategory,二级小分类在ProductSubcategory,然后所有产品都必须归属到二级小分类上。ProductCostHistory和ProductListPriceHistory都是因为要记录基于时间段的历史而形成的一对多关系,其中必有StartDate和EndDate来划分时间区间。【历史数据记录】
关于产品文档和产品照片,由于存在复用的情况(比如产品的外观是一模一样的,只是某些内部参数不一样,那么产品照片就可以复用。)所以就形成了多对多关系,有多对多关系就会有中间表。产品图片由于会有细节照片,各个角度的照片,所以在多对多关系表中另外定义了一个Primary字段用于说明当前选用的照片是不是主体照片。
事务实体
前面分析的实体都是在主谓宾语句中当主语的对象,接下来我们要分析这些主语之间发生关联,进行事务操作后产生的宾语对象。
对于SalesPerson、Product、Customer在一起时,联想到的就是销售订单:
SalesOrder
只要是涉及表单的东西(销售订单、报销单、采购订单、发货单等)大部分情况都会分为Header和ItemDetail两个表,在销售订单中Header用于记录单据的销售的人员,客户,总金额等信息,而ItemDetail中记录了具体销售的产品,数量等信息。
下面先分析Header:
Header建立了SalesPerson与Customer的联系,另外还有范式化的一些字段,比如ShipToAddress,BillToAddress,ShipMethod等。除了这几个实体外,我们需要单独分析一下以下几个实体:
Territory,这是在前面介绍Sales的时候说到,这个销售区域和SalesPerson是有关联的,按理来说,Header表已经关联了SalesPerson表,我们就可以通过SalesPerson获得其下单时对应的Territory,为什么还需要额外添加这个Header到Territory的直接关系呢?这是出于性能的考虑而增加的冗余,对于有时效性的对象,最好是直接关联,而不是通过中间对象jion多个表去关联。让我们看看如果没有直接关联Territory,那么我们的查询到底有多复杂:
select h.*,st.*
from Sales.SalesOrderHeader h
left join Sales.SalesPerson sp
on h.SalesPersonID=sp.BusinessEntityID
left join Sales.SalesTerritoryHistory sth
on sp.BusinessEntityID=sth.BusinessEntityID and h.OrderDate between sth.StartDate and sth.EndDate
left join Sales.SalesTerritory st
on sth.TerritoryID=st.TerritoryID
下面再来看看币种和汇率的相关表Currency。在这个系统中,Header并没有直接说明用什么币种付款,什么币种结算,汇率是多少,而是把这几个字段放在CurrencyRate表中,通过引用CurrencyRate来表示。虽然说独立出来后没有直接放在Header表中直观,不过减少了冗余,只需要做一次Join就能拿到结果,所以性能上还是能接受的。【虽然从关系上需要Join了CurrencyRate后再JoinCurrency表才能完整,但是一般来说Currency表只用于CurrencyRate的限定,而不需要在查询时使用Currency表,因为CurrencyCode是国际标准编码,只需要显示Code就够了。】
Header和SalesReason是多对多关系,在客户下单的时候让用户复选购买原因,是因为促销,还是看了杂志广告之类的,简单多对多关系,这个没啥好说的。
SalesOrderDetail
Header和OrderDetail是一对多关系,Detail记录了具体购买了啥产品,购买单价,数量等,所以关联的是Product,但是在这个系统中,他并不是直接关联Product对象,而是在之间建立了SpecialOfferProduct,该表是Product和SpecialOffer的多对多中间表。
为什么会这么做呢?这主要是跟具体的业务相关。产品在生产出来以后有一个标价Product.ListPrice,但是在实际销售中,商家会有各种促销活动(比如买10个以上9.8折,25个以上9折等),所以会形成Product和SpecialOffer的多对多关系,维护了哪些产品能够有哪些折扣。为了统一模型,如果产品不做任何打折促销,也会在SpecialOffer中维护一条记录“No Discount”。
这里有一个特别的技巧,SpecialOfferProduct是没有自己独立的主键的,而是使用ProductId和SpecialOfferId作为联合主键,然后在OrderDetail引用具体的SpecialOfferProduct时,就会将ProductId和SpecialOfferId引用到其列中。所以在模型上来说,是OrderDetail关联SpecialOfferProduct,然后再关联Product,但是我们在实际查询中,完全可以忽略SpecialOfferProduct表,直接用OrderDetail去Join Product即可,所以性能上没有任何影响,这是一个漂亮的设计。
而当Employee、Product和Vendor在一起时,联想到的就是采购订单:
PurchaseOrder
和销售订单类似,采购订单也 分为PurchaseOrderHeader和PurchaseOrderDetail。
系统中先使用ProductVendor定义了哪些Vendor能供应哪些产品,在生成采购订单时会基于这里面的内容来生成,但是在模型上并不直接反应,因为Product属于Detail表,而Vendor是属于Header表,不能像前面说到的SpecialOfferProduct一样通过引用来传递这种限制。
Header记录的是采购人员Employee与供应商Vendor的关系。一个采购订单Header中会包含多个明细Detail,里面记录了采购哪些Product。采购订单比销售订单简单很多,最为买方,不会去记录促销,购买原因之类的信息。另外采购中没有涉及到币种汇率问题,我估计这是因为产品都在国内采购和结算,所以只有一种币种,而销售是面向世界各地,所以涉及到币种汇率。
WorkOrder
除了前面说到的销售订单和采购订单外,在生产过程中还有生产订单,用于表示产品的生产情况。主要有WorkOrder和WorkOrderRouting两个实体。
WorkOrder记录了生产某个产品的数量、报废和时间情况,而WorkOrderRouting记录的是在某个产品的具体生产过程中有哪些工序,每个工序的时间、成本等情况。总的来说,这是一个非常非常简化的生产工作订单模型。
其他实体
除了前面说到的实体外,还有其他几个独立出来的实体需要说明一下:
TransactionHistory
另外还有一个其归档表TransactionHistoryArchive,其结构和TransactionHistory一模一样,这里面记录的是生产工作订单或者采购订单或者销售订单这3个事务的产品、日期,数量等公共信息。这个表可以认为是一个事务的日志表,平时并不参与各个实体的查询,只有在审计或者跟踪数据变化时才用到。
TransactionHistory表中的数据是在各个Order表上建立Trigger自动插入进去的,而不是由外部程序代码去控制。由于本身事务表的数据量就比较大,而这个表却存了三个事务表中的数据,所以增长特别快,必须进行归档操作,把老数据搬移到另一个归档表中,这样才能保证查询新TransactionHistory表的速度。
AWBuildVersion
这是一个记录当前数据库定义创建时数据库的版本也可以定义当前数据库定义脚本的版本。对于通用的产品来说,这个表比较重要,因为产品可能需要升级,升级程序在升级前读取这个表,知道了当前数据库定义是什么个版本,然后就可以查询到将当前版本的数据库升级到新版的数据库所需要修改的SQL,然后执行这些SQL。
而应用程序在运行时第一件事就是检查这个表中的版本信息,保证数据库定义的版本与程序要求的版本匹配,这样程序才能正常运行。
对于企业内部系统,一般只有一个实例,而且由企业内部的IT人员开发维护,所以这个表没有也没什么问题。
DatabaseLog
这是记录数据库DDL(数据定义语言,比如CREATE, ALTER, DROP等)操作的日志表。这个表是由Database Trigger自动维护,当在这个数据库中执行了DDL的时候,系统会触发Trigger,往这个表中记录一条数据。这是一个好东西!
另外还有一些因为范式化抽象出来的码表,我在前面的模型中没有提到,比如CountryRegion,StateProvince等这些都比较简单,就不一一累述了。
这篇文章我只是简单分析了下实体和实体关系,下面一篇文章会进一步分析其中的细节,有哪些优缺点。
从AdventureWorks学习数据库建模——实体分析的更多相关文章
- 从AdventureWorks学习数据库建模——国际化
前一篇博客我已经把各个实体分析了一遍,从分析中可以看到,这个公司是做本地采购,生产,然后通过网站和门店进行国际销售的.所以这里会涉及到一些国际化的问题.接下来就来分析一下有哪些国际化需要注意的问题和数 ...
- 从AdventureWorks学习数据库建模——保留历史数据
在业务需求中,经常需要我们在系统中能够记录历史信息,能够查看到历史变动情况,这时我们可以通过增加开始结束时间字段来记录数据的历史版本.对数据的历史记录主要分为:关系.属性历史,实体历史和变更历史. 关 ...
- ArcGIS案例学习笔记4_2_水文分析批处理地理建模
ArcGIS案例学习笔记4_2_水文分析批处理地理建模 联系方式:谢老师,135_4855_4328,xiexiaokui#139.com 概述 计划时间:第4天下午 目的:自动化,批量化,批处理,提 ...
- 数据库建模工具 PD的使用
1.1. 数据库建模工具 PD的使用 安装12.5版本,进行破解 PD 是最专业数据建模工具, 是 Sybase 公司一个 产品 PD 提供四种模型文件 PDM 物理数据模型,面向数据库表结构设计,直 ...
- PowerDesigner数据库建模工具一缆
转自:http://blog.csdn.net/shanliwa/archive/2007/10/20/1834117.aspx Sybase PowerDesigner - 一个高端数据建模工具.你 ...
- 使用PowerDesigner进行数据库建模入门
阅读目录 两种重要模型 创建表和主外键 创建视图和存储过程 生成数据库 PowerDesigner(简称PD)是一种强大的数据库建模工具,使用PD可以创建业务模型,UML类图等,当然最主要的功能是数据 ...
- 新手学习数据库(一)用Powerdesigner设计数据库
说明: 一.学会用开发语言进行数据库编程,其关键是在于学会sql语言,开发语言只不过给程序员提供了一个操作数据库的接口罢了. 二. 本人也是初学者,采用的数据库设计软件是powerdesigner.利 ...
- Powerdesigner数据库建模--概念模型--ER图【转】
转自http://www.cnblogs.com/dekevin/archive/2012/07/18/2596745.html Powerdesigner数据库建模--概念模型--ER图 目标: ...
- NLP入门(五)用深度学习实现命名实体识别(NER)
前言 在文章:NLP入门(四)命名实体识别(NER)中,笔者介绍了两个实现命名实体识别的工具--NLTK和Stanford NLP.在本文中,我们将会学习到如何使用深度学习工具来自己一步步地实现N ...
随机推荐
- 【url重写】
一.原理void Application_BeginRequest(object sender, EventArgs e) { //url重写 HttpApplication a ...
- C语言字符串匹配、goto语句、关机命令使用
1.程序执行修改窗口字体颜色命令: 2.程序执行修改窗口标题命令: 3.程序执行关机倒计时命令: 4.根据提示输入团队名称JYHACK TEAM 根据提示输入团队网址:http://bbs.jyhac ...
- git版本回退, github版本回退
上周提交了更改,过了周末回来说要撤销上个story.于是,需要找到上周提交的版本,rollback回来. git版本管理命令,自从习惯使用管理工具之后就很少接触了,当突然寻找其他指令的时候就成浆糊了, ...
- 浅谈Hibernate入门
前言 最近打算做一个自己的个人网站,经过仔细思考,打算使用hibernate作为开发的ORM框架,因此各种找资料,由于本人是刚刚接触这技术的,所以就找了比较基础的知识来分享下 基本概述 Hiberna ...
- scikit-learn一般实例之一:绘制交叉验证预测
本实例展示怎样使用cross_val_predict来可视化预测错误: # coding:utf-8 from pylab import * from sklearn import datasets ...
- Visual Studio 2005 搭建Windows CE 6.0环境之准备
Microsoft Visual Studio 2005 Visual Studio 2005 Professional 官方90天试用版英文版:http://download.microsoft.c ...
- C# Winform 拦截关闭按钮触发的事件
用户关闭软件时,软件一般会给“是否确认关闭”的提示. 通常,我们把它写在FormClosing 事件中,如果确定关闭,就关闭:否则把FormClosingEventArgs 的 Cancel 属性设置 ...
- 小米网css3导航下拉菜单代码
效果:http://hovertree.com/texiao/css3/19/ 代码如下: <!doctype html> <!-- W3C规范 --> <html la ...
- java web学习总结(七) -------------------HttpServletResponse对象(一)
Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象.和代表响应的response对象.request和response对象即然代表请求和响应,那我们要 ...
- springmvc整合mybatis框架源码 bootstrap
A集成代码生成器 [正反双向(单表.主表.明细表.树形表,开发利器)+快速构建表单 下载地址 ; freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本,处理类 ...