创建你的 Mysql 数据库(全)
原文:
zh.annas-archive.org/md5/853FEC9D976A75004408D5A9A661EDD8
译者:飞龙
协议:CC BY-NC-SA 4.0
前言
1995 年发布的 MySQL 已成为最受欢迎的开源数据库系统。MySQL 和 phpMyAdmin 的普及使得许多非 IT 专家能够使用 MySQL 后端构建动态网站。本书是一本简短但完整的指南,向初学者展示如何为 MySQL 设计良好的数据结构。它教授如何规划数据结构以及如何使用 MySQL 模型实际实现它。
本书内容
第一章介绍了 MySQL 的概念,并讨论了 MySQL 日益增长的数据库系统。本章简要概述了关系模型和 Codd 规则,这些是设计目的所必需的。最后简要介绍了我们的案例研究——“汽车经销商”。
第二章展示了如何处理来自用户或其他来源的原始数据信息,以及可以帮助我们构建全面数据收集的技术。此外,本章涵盖了分析系统的精确限制,如何收集文档以及为我们的案例研究进行访谈活动。
第三章强调将收集过程中获取的数据元素转换为一组连贯的列名。本章还讨论了数据命名的概念。
第四章提供了将列名分组到表格中的技术。本章涵盖了表格布局规则、主键、唯一键、数据冗余和数据依赖等概念。
第五章介绍了多种提高数据结构安全性、性能和文档性的技术。汽车经销商案例研究的最终数据结构在本章末尾提供。
第六章涵盖了关于航空公司系统的补充案例研究。该案例研究涉及多个步骤,如收集文档、准备初步数据元素列表、准备表格列表、样本值和针对航空公司系统的查询。
本书所需
需要具备 SQL 基础知识。虽然可以使用“mysql”命令行工具,但重点是通过 phpMyAdmin 的 Web 界面重现示例。不需要 MySQL 服务器管理或任何特定操作系统的知识。
约定
本书中,你会发现多种文本样式用于区分不同类型的信息。以下是这些样式的示例及其含义的解释。
代码有三种样式。文本中的代码词如下所示:“在这种情况下,我们可以添加员工信息,将员工代码添加到car_event
表中”。
代码块将如下所示:
CREATE TABLE `event` (
`code` int(11) NOT NULL,
`description` char(40) NOT NULL,
PRIMARY KEY (`code`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
INSERT INTO `event` VALUES (1, 'washed');
当我们希望引起您对代码块特定部分的注意时,相关行或项将加粗显示:
CREATE TABLE `event` (
`code` int(11) NOT NULL,
`description` char(40) NOT NULL,
PRIMARY KEY (`code`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
INSERT INTO `event` VALUES (1, 'washed');
新术语和重要词汇以粗体字引入。屏幕上、菜单或对话框中出现的词汇,在我们的文本中这样表示:“将此“列”(例如特殊油漆颜色)链接到查找表变得不可能”。
注意
警告或重要注意事项以这样的框呈现。
注意
提示和技巧以这种方式呈现。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法,您喜欢或可能不喜欢的地方。读者反馈对我们开发真正让您受益匪浅的图书至关重要。
若要向我们提供一般反馈,只需发送电子邮件至<feedback@packtpub.com>
,确保在邮件主题中提及书名。
如果您需要某本书并希望我们出版,请通过www.packtpub.com上的建议书名表单或发送电子邮件至<suggest@packtpub.com>
告知我们。
如果您在某个主题上具有专业知识,并有意撰写或参与撰写一本书,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
既然您已是 Packt 图书的自豪拥有者,我们有一系列服务帮助您从购买中获得最大收益。
下载本书的示例代码
访问www.packtpub.com/support
,并从图书列表中选择本书以下载任何示例代码或额外资源。随后将显示可下载的文件。
可下载文件包含使用说明。
勘误
尽管我们已尽一切努力确保内容的准确性,但错误仍可能发生。如果您在我们的某本书中发现了错误——可能是文本或代码中的错误——我们将非常感激您能向我们报告。这样做可以避免其他读者感到沮丧,并有助于改进后续版本的图书。如发现任何勘误,请通过访问www.packtpub.com/support
,选择您的图书,点击提交勘误链接,并输入勘误详情来报告。一旦您的勘误经过验证,您的提交将被接受,勘误将被添加到现有勘误列表中。您可以通过选择www.packtpub.com/support
上的标题来查看现有勘误。
问题
如果您在阅读本书的某个方面遇到问题,可以通过电子邮件<questions@packtpub.com>
联系我们,我们将尽力解决。
第一章:介绍 MySQL 设计
数据设计是应用开发周期中的关键环节。类比建筑,构建应用如同建造房屋,拥有合适的工具固然重要,但我们更需要一个坚实的基础:数据结构。然而,创造一个良好的数据结构是一项艰巨的挑战;追求完美的数据结构可能引领我们进入新领域,那里有众多方法可供选择。哪种方法最佳?我们如何保持目标明确,避免浪费时间?
MySQL 数据库的数据设计既是科学也是艺术,必须在科学和经验方法之间取得良好平衡。科学方面涉及信息技术(IT)原则,而经验方面主要基于直觉和经验。
本书主要面向 MySQL 数据库,教授如何规划数据结构并利用 MySQL 模型实际实施。规划阶段有时被称为逻辑设计,但更理想的是将逻辑/物理过程视为一体。
MySQL 的普及与影响
MySQL(www.mysql.com)自 1995 年推出以来,已成为最受欢迎的开源数据库系统。几乎所有网络服务提供商都将 MySQL 作为其托管计划的一部分,通常在无处不在的 LAMP(Linux、Apache、MySQL、PHP)平台上。MySQL 普及的另一根源是其持续成功的 phpMyAdmin(www.phpmyadmin.net),这是一个成熟的基于 Web 的 MySQL 界面。因此,许多网站都采用 MySQL 作为其后端数据存储库。
对 MySQL 设计的需求
总体而言,MySQL 的普及吸引了众多网页开发者,其中一些并无 IT 背景。面对将静态网站转变为动态/交易型网站,或整合企业数据至网站的任务时,开发者有时倾向于即兴构建数据结构。这种结构(或缺乏结构)可能在一段时间内有效,但最终因缺乏深度而失败。或许系统起初因规模小、功能有限而运行良好,但当用户需求增加时便崩溃。设计不佳的数据结构只能修补到一定程度,也可能在初始测试仅涉及少量数据行时出现扩展问题。
使用工具的便捷性可能掩盖了数据库设计依赖于基本原则的事实。忽视这些原则可能导致应用程序维护成本高昂,因为在应用编码开始后纠正数据结构错误耗时费力。
"接下来我该做什么?"
这里有一个 MySQL 在非 IT 人群中影响的例子。我曾在 phpMyAdmin 讨论论坛上看到这个问题——我是凭记忆引用的:“我安装了 MySQL 和 phpMyAdmin,现在我需要指导:接下来我该做什么?”我回答说:“也许你可以创建一个表,然后插入一些数据。接下来你可以浏览你的数据。”
显然,这些工具被此人视为有趣,但我只能好奇在这次论坛对话后形成了什么样的表结构。
数据设计步骤
我们可以将数据设计视为一系列步骤,其目标是生成支持应用程序所需的物理 MySQL 数据库、表和列。
从外壳开始,我们首先需要通过收集数据来了解我们的数据。然后,我们开始通过适当地命名这些数据元素来组织它们。接下来,我们将数据元素重新组合成表格,考虑到所需的关键字。虽然之前的步骤可能只在纸上完成,但最后一步是在 MySQL 的结构中实现模型。
所有这些步骤都在本书的不同章节中有所涉及。
数据作为一种资源
在探讨各种可用设计技术之前,我们先来思考数据本身的概念。
组织和企业使用许多资产,例如建筑、家具、智慧,但或许最有价值的资产是信息或数据。我们注意到,数据记录了企业的流程,并将人们绑定到一个持续的信息交换中,称为信息流。计算机有助于规范这些数据,但我们必须记住,数据本身就存在。
但这是我的数据!
在构建数据设计时,我们必须与用户会面并了解企业的数据流。在理想情况下,每个部门,包括 IT 部门,以及每位用户都会协作,以便数据能在部门间轻松流动。然而,不时地,人们会目睹两种态度阻碍企业内正常的数据流动。第一种是某些 IT 部门,由于负责存储数据的计算机,开始认为数据属于他们。这导致一定程度的保密性,隐藏数据并可能阻断数据设计过程。第二种是第一种的变体,这次是由用户引起的——数据源自该用户,他有不愿分享的倾向。
以这种后一种态度为例,让我们考虑会计数据。在个人电脑时代之前,会计系统存在于大型机或小型机中,IT 部门管理所有数据,包括会计数据。自从微型计算机和电子表格应用程序出现以来,一名会计文员就能管理大量数据,并生成高质量的报告。然而,这些数据通常存储在他的电脑上;他输入数据,他制作报告,并因此从上司那里获得赞誉。所以,数据属于会计文员,对吧?这种思维方式阻碍了个人和部门之间的数据流动,并有可能导致整个组织中出现冗余、不连贯的数据。
数据设计过程之后,会在用户或部门创建的这些孤立数据岛之间建立桥梁,以便数据能够惠及整个企业。也可能出现岛屿减少和冗余数据被消除的情况。
数据建模
数据通常被组织成信息系统。这个系统可以与简单的活页夹相比较,但这本书描述了基于计算机的信息系统或数据库中的数据设计过程。此外,数据库遵循一种设计模型,我们将使用最流行的模型——关系模型。
企业的完整数据集合超出了我们模型的涵盖范围。
我们将构建一个仅表示数据频谱子集的模型。问题在于选择哪个子集?我们将在第二章中看到,我们必须为分析系统的数据范围设定边界。
要构建持久的信息系统,数据必须被驯服和塑造,以正确地反映现实。正确在这里意味着:
-
遵循组织的需求,包括系统的边界
-
符合所选的数据设计模型(这里指关系模型)
-
具有高度的适应性,以调整自身以适应不断变化的环境
关系模型概述
我们要感谢埃德加·F·科德博士提出了关系模型的概念,源自他 1970 年的论文《大型共享数据库的关系数据模型》(www.acm.org/classics/nov95/toc.html
)。科德博士后来通过定义一组规则——所谓的科德十二条规则(en.wikipedia.org/wiki/Codd%27s_12_rules
)来解释他的模型。理想的数据库管理系统(DBMS)会实现所有这些规则,但很少有能做到的。但在实践中,这并不是问题,因为即使在不完全应用所有规则的产品中,关系模型的优势也能实现。我们完全有能力使用 MySQL 等现有数据库产品构建高效的关系数据设计。
在处理数据设计时,我认为最重要的规则是第 1 条和第 2 条。以下是这两条 Codd 规则的摘要。
规则 #1
本规则指出,数据包含在表中。表逻辑上汇总了关于某个主题的信息,例如,汽车。表格格式——行和列——是这里的重要概念。一行描述关于单个项的信息,例如,特定汽车,而一列描述每个项的单一特征(或属性),例如,其颜色。我们将在第三章中看到,将数据分解成适当调整的列对于拥有一个灵活且有用的结构至关重要。
行与列的交点包含单个项的特定属性的值。我们有时将这个交点称为包含我们数据的单元格——这与电子表格中的概念相同。
规则 #2
数据不是通过物理位置检索或引用——查找此文件中的第三个记录。相反,数据必须通过引用表、唯一键——主键——以及一个或多个列名来获取。例如,在cars
表中,我们使用车辆序列号来检索该车的颜色。
本规则将在第四章中进行研究,其中我们将描述数据分组和选择键的概念。正确选择键至关重要。
简化设计技术
多年前,我开始使用关系模型来详细阐述数据结构。我使用的方法可以概括为这句话:“确定数据最适合放置在结构中的位置”。然后我了解到教授给 IT 专家的设计技术,这些技术从关系模型演变而来。
经常教授的技术包括构建一个实体-关系图。在这种图中,我们用实体表示名词,例如,一辆车、一个客户,并用动词表达它们之间的关系。两个实体之间关系的一个例子是“客户购买汽车”。完成图后,必须将其转换为包含表和列的模型,使用一种称为规范化的技术,该技术通过多个步骤将模型精炼成有效的数据结构。
这些技术产生报告、图表,并最终形成一个理论上可以在 DBMS 中物理实现的数据设计。
当我熟悉了那些传统技术后,我认为至少对我来说,它们是在浪费时间。这些方法教授了一种方式,但最终目标——一个可运行的关系数据库及其相关文档——可以通过更直接的方式实现。此外,这些技术存在一个问题:它们不能盲目且机械地应用。开发人员总是需要思考数据命名、数据分组以及选择键,同时试图平衡用户需求和以下约束:
-
硬件
-
选定的数据库管理系统
-
计划增长
-
时间
-
预算
我意识到传统技术无处不在地被教授,我尊重那些教授它们的老师。但请相信我,当需要交付一个应用程序时,无论其界面如何,重要的是避免浪费时间在中间产物上,而是直接开发一个工作原型。在数据设计阶段采用更直接的方法可以腾出更多时间来完善界面,捕捉未预见的需要并解决它们。
本书的目标是教授构建有效数据结构所需应用的最基本原则。
案例研究
通过两个案例研究,可以非常实际地解释数据设计的各个步骤。案例研究是解释那些没有真实例子就可能变得过于抽象的概念的最佳方式。第 1 至 5 章基于一个案例研究:“汽车经销商”。第六章则包含另一个案例研究,总结了前几章中提到的所有概念。
我们的汽车经销商
假设我们已与一家希望将其业务部分计算机化的汽车经销商联系。让我们简要描述一下这家企业。在第二章中,我们将更正式地检查我们系统的数据收集阶段。
这家汽车经销商仅在一个地址运营。他们雇佣了九名销售人员,这些销售人员尽职尽责地迎接潜在客户,并向他们展示展厅内可用的车型。此外,两名店铺助理负责车辆移动,一名办公室文员记录客户的预约情况。Fontax 和 Licorne 是该经销商提供的两个虚构品牌。每个品牌都有多种车型,例如 Mitsou、Wanderer 和 Gazelle。
系统的目标
我们希望保留有关汽车库存和销售的信息。以下是一些示例问题,展示了我们的系统将需要处理的信息类型:
-
我们库存中有多少辆 Fontax Mitsou 2007 款汽车?
-
去年有多少访客试驾了 Wanderer?
-
在某一特定时期内,我们售出了多少辆 Wanderer 汽车?
-
2007 年,谁是我们 Mitsou、Wanderer 或整体销售业绩最佳的销售人员?
-
购买者主要是男性还是女性(按车型划分)?
以下是这家汽车经销商所需的一些报告的标题:
-
每月详细销售额:销售人员、汽车数量、收入
-
每位销售人员的年度销售额
-
库存效率:汽车交付给经销商或客户的平均延迟时间
-
访客报告:尝试驾驶汽车的访客百分比;导致销售的试驾百分比
-
客户对销售人员的满意度
-
销售合同
除此之外,还需要构建屏幕应用程序以支持库存和销售活动。例如,能够查阅和更新预约日程;查阅下周的汽车交付日程。
在此数据模型构建完成后,应用程序开发周期的剩余阶段,如屏幕和报告设计,将为这家汽车经销商提供报告,以及在线应用程序以更好地管理汽车库存和销售。
过宽表格的故事
本书专注于在 MySQL 中表示数据。MySQL 及其他产品中表格的容器是数据库。在数据库中仅包含一张表,从而避免完全应用关系模型概念(其中表格通过共同值相互关联)是完全可能的;然而我们将按常规方式使用模型:拥有多张表并建立它们之间的关联。
注意
本节描述了一个数据被塞进一张巨大表格的例子,也称为过宽表格,因为它由太多列组成。这种过宽表格本质上是非关系型的。
有时需要对数据结构进行审查或评估,因为它可能基于数据命名约定、键选择和表数量方面的错误决策。最常见的问题可能是将所有数据放入一张大而宽的表格中。
这种常见结构(或缺乏结构)的原因是许多开发者从结果甚至打印结果的角度思考。也许他们知道如何构建电子表格,并试图将电子表格原则应用于数据库。假设构建数据库的主要目标是生成这份销售报告,该报告展示了每个月每位销售人员销售的汽车数量,描述品牌名称、汽车型号编号和名称。
Salesperson | Period | Brand Name | Car model number | Car model name and year | Quantity sold |
---|---|---|---|---|---|
Murray, Dan | 2006-01 | Fontax | 1A8 | Mitsou 2007 | 3 |
Murray, Dan | 2006-01 | Fontax | 2X12 | Wanderer 2006 | 7 |
Murray, Dan | 2006-02 | Fontax | 1A8 | Mitsou 2007 | 4 |
Smith, Peter | 2006-01 | Fontax | 1A8 | Mitsou 2007 | 1 |
Smith, Peter | 2006-01 | Licorne | LKC | Gazelle 2007 | 1 |
Smith, Peter | 2006-02 | Licorne | LKC | Gazelle 2007 | 6 |
在不深入考虑这种结构的影响的情况下,我们可以仅构建一张表,sales:
salesperson | brand | model_number | model_name_year | qty_2006_01 | qty_2006_02 |
---|---|---|---|---|---|
Murray, Dan | Fontax | 1A8 | Mitsou 2007 | 3 | 4 |
Murray, Dan | Fontax | 2X12 | Wanderer 2006 | 7 | |
Smith, Peter | Fontax | 1A8 | Mitsou 2007 | 1 | |
Smith, Peter | Licorne | LKC | Gazelle 2007 | 1 | 6 |
乍看之下,我们已经将报告中所需的所有信息进行了表格化。
注意
本书中的示例可通过mysql
命令行工具或更直观的网页界面 phpMyAdmin 进行复现。您可以参考 Packt 出版社的《Mastering phpMyAdmin 2.8 for Effective MySQL Management》(ISBN 1-904811-60-6)。在 phpMyAdmin 中,可以在 SQL 查询窗口中键入确切命令,或者利用菜单和图形对话框。本书将展示这两种方法。
以下是我们使用mysql
命令行工具创建sales
表的语句:
CREATE TABLE sales (
salesperson char(40) NOT NULL,
brand char(40) NOT NULL,
model_number char(40) NOT NULL,
model_name_year char(40) NOT NULL,
qty_2006_01 int(11) NOT NULL,
qty_2006_02 int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
在前述声明中,char(40)
表示一个包含 40 个字符的列,而int(11)
在 MySQL 中表示一个显示宽度为 11 的整数。
使用 phpMyAdmin 网页界面,我们将得到:
这里我们已将示例数据输入到我们的sales
表中:
INSERT INTO sales VALUES ('Murray, Dan', 'Fontax', '1A8', 'Mitsou 2007', 3, 4);
INSERT INTO sales VALUES ('Murray, Dan', 'Fontax', '2X12', 'Wanderer 2006', 7, 0);
INSERT INTO sales VALUES ('Smith, Peter', 'Licorne', 'LKC', 'Gazelle 2007', 1, 6);
INSERT INTO sales VALUES ('Smith, Peter', 'Fontax', '1A8', 'Mitsou 2007', 1, 0);
然而,这种结构存在许多维护问题。例如,我们应将 2006 年 3 月的数据存储在哪里?为了发现其他问题,让我们检查一下可以针对此表使用的示例 SQL 语句,以查询特定问题,以及这些语句的结果:
/* displays the maximum number of cars of a single model sold by each vendor in January 2006 */
SELECT salesperson, max(qty_2006_01)
FROM sales
GROUP BY salesperson
/* finds the average number of cars sold by our sales force taken as a whole, in February 2006 */
SELECT avg(qty_2006_02)
FROM sales
WHERE qty_2006_02 > 0
/* finds for which model more than three cars were sold in January */
SELECT model_name_year, SUM(qty_2006_01)
FROM sales
GROUP BY model_name_year
HAVING SUM(qty_2006_01) > 3
我们注意到,尽管上述 SQL 查询得到了我们寻找的答案,但为了获取其他月份的结果,我们不得不在查询中修改列名。此外,如果我们想知道哪个月份的销售额超过了年平均水平,情况会变得复杂,因为我们可能需要处理十二个列名。当尝试报告不同年份的数据,或比较两个年份时,还会出现另一个问题。
此外,需要新报告的情况可能揭示了这种结构的糟糕状态。一个过于依赖单一报告而非基于数据元素间内在关系的结构,其扩展性不佳,无法满足未来需求。
第四章将展开这些问题。
总结
我们看到,MySQL 的普及为许多用户提供了强大的工具;其中一些用户在设计技巧上并不匹配。数据是一项重要资源,我们必须将组织的数据视为整体。强大的关系模型可以帮助我们进行结构化活动。本书避免使用关于关系模型的专业学术词汇,而是专注于重要原则和构建良好结构所需的最少任务。随后,我们看到了主要案例研究,并注意到不幸的是,构建宽大低效的表是多么容易。
第二章:数据收集
为了构建数据结构,必须首先收集数据元素并确定该数据适用的领域。本章涉及来自用户或其他来源的原始数据信息,以及可以帮助我们构建全面数据收集的技术。该收集将成为我们所有进一步活动(如数据命名和分组)的输入。
为了构建数据收集,我们首先将确定系统的界限。接下来,我们将收集文档以查找重要的数据元素。下一步将是与关键用户进行访谈,以完善数据元素列表。本章中描述了所有这些步骤。
系统边界识别
让我们设定场景。我们被当地一家汽车经销商召集,提交一份关于新信息系统的提案。所述目标是生成关于汽车销售的报告并帮助跟踪汽车库存。报告当然是未来系统的输出。隐藏在报告背后的想法可能是提高销售、了解交付延迟或找出某些汽车消失的原因。数据结构本身在用户看来可能并不重要,但我们知道这对开发人员产生所需输出很重要。
在开始处理系统细节之前,首先查看项目范围很重要。项目是否涵盖:
-
整个企业
-
仅一个行政区域
-
多个行政区域
-
企业的一个功能
每个组织都有一个主要目的;它可以是销售汽车、教学或提供网络解决方案。除了这个主要目的,每个组织还有人力资源管理、工资和营销等子活动。数据收集的方法将根据我们处理的特定领域而有所不同。假设我们了解到我们的汽车经销商还经营一家维修店,该店有自己的库存,以及汽车租赁服务。我们是否将这些库存纳入我们的分析任务中?我们必须正确理解这个新信息系统在其上下文中的位置。
在准备数据模型时,最大的挑战可能是在哪里划清界限,明确说明何时停止。这具有挑战性,原因有很多:
-
我们的用户可能对他们想要的东西、他们期望从新系统中获得的好处只有一个模糊的想法
-
我们未来的用户之间可能存在利益冲突;他们中的一些人可能希望以与他人不同的方式优先处理问题,也许是因为他们涉及新系统承诺消除的繁琐任务
-
我们可能会试图改进超出此特定项目范围的企业范围信息流
平衡用户感知目标与整个组织的需求并非易事。
模块化开发
普遍认为,将一个问题或任务分解成更小的部分有助于我们专注于更易管理的单元,长远来看,这使我们能够实现更好的解决方案,以及完整的解决方案。拥有较小的段落意味着定义每个部分的目的更简单,测试过程也更容易,因为较小的段落包含的细节较少。这就是为什么在确定系统边界时,我们应该考虑通过模块进行开发。在我们的案例研究中,一个简单的模块划分方式如下:
-
模块 1:汽车销售
-
模块 2:汽车库存
通过增量步骤交付信息系统可以帮助客户对最终产品产生信心。定义模块及其时间表可以激励用户和开发者。有了公开的时间表,每个人都知道预期是什么。
随着模块概念的提出,预算和开发优先级的概念也随之而来。我们是应该先交付汽车销售模块还是库存模块?这些模块能否独立完成?是否存在必须解决的限制,例如首席执行官(CEO)需要在 6 月 20 日前得到一份关于汽车销售的新报告?另一个需要考虑的点是模块之间的关联。很可能一些数据会在模块间共享,因此为模块 1 准备的数据模型很可能在模块 2 的开发过程中被重用和细化。
模型灵活性
另一个与我们的用户不直接相关,但与我们作为开发者相关的点是:数据模型能否构建得灵活且更通用?这样,它就可以应用于其他汽车经销商,始终牢记开发者与用户之间的合同问题(谁将拥有这项工作?)。数据结构是否应该考虑到其他销售领域来开发?例如,这可能导致一个名为goods
的表,而不是cars
。也许这种泛化会有所帮助,也许不会,因为数据元素描述必须始终保持清晰。
文档收集
这一步可以在访谈之前完成。目标是收集有关该组织的文档,并开始设计我们访谈的问题。当然,汽车销售的数据模型与其他销售系统有一些共同之处,但关于汽车有一种特殊的文化。另一组文档将在访谈期间收集,同时我们了解受访者使用的表格。
一般阅读
以下是一些阅读建议:
-
企业年报
-
企业目标声明
-
总统演讲
-
宣传材料
-
公告板
我曾从一家杂货店的员工公告板上了解到许多关于信息流动的知识。管理层向员工发出的小便条解释了如何处理使用支票支付的客户(在接受支票前必须从客户那里获取哪些个人信息),并详细说明了病假员工替换的日程安排。公告板上还说明了使用收银机为使用商店信用卡支付的客户发放奖励积分的流程。这些信息有时比年度报告更有用,因为我们正从日常任务的执行者那里寻求细节。
表格
代表企业与外部合作伙伴或内部部门之间文书工作的表格应仔细审查。即使进一步分析显示这些数据未被使用、不准确或冗余,它们仍能揭示大量数据。许多组织遭受表格病——一种过度使用纸质或屏幕表格并制作过于复杂表格的倾向。然而,如果我们能够查看目前用于传达汽车库存或汽车销售信息的表格,例如汽车经销商向制造商发出的采购订单,我们可能会在这些表格上找到有关采购的关键数据,这些数据对完成我们的数据收集工作将非常有用。
现有计算机化系统
汽车经销商多年前已开始销售业务。为了支持这些销售,他们可能使用了某种计算机化系统,即使这可能只是一个电子表格。这个预先存在的系统肯定包含有趣的数据元素。我们应该尝试查看这个现有的信息系统,如果存在且允许的话。关于数据结构化过程本身,我们可以了解到一些在纸质表格上看不到的数据元素。此外,这有助于在实施新系统时简化过渡和培训。
访谈
进行访谈的目的是了解与所研究系统相关的词汇。本书是关于数据结构的,但访谈中收集的信息无疑有助于系统开发的后续活动,如编码、测试和优化。
访谈是整个过程中的关键部分。在我们的例子中,客户要求开发一个关于汽车销售和库存跟踪的系统。此时,许多用户无法进一步解释他们想要什么。问题恰恰在于此:作为开发者,我如何找出他们想要什么?经过访谈阶段后,情况变得清晰,因为我们已经收集了数据元素。此外,订购新系统的客户往往无法全面了解数据流;也可能出现这种情况,即该客户不会是与系统所有方面打交道的人,那些更针对文职人员的方面。
寻找合适的用户
建议的方法是联系最适合回答新系统问题的人。有时,负责人坚持认为他/她是最佳人选,这可能是真的,也可能不是。这可能会变得微妙,尤其是当我们最终遇到一个更了解情况的人时,即使是在非正式会议上。
思考以下问题有助于找到最佳候选人:
-
谁希望建立这个系统?
-
谁将从新系统中获益?
-
哪些用户最愿意合作?
显然,这可能导致与多个人员会面,以探索各个子领域。其中一些领域可能会有交集,可能产生负面影响——意见分歧,或可能产生正面影响——通过多个受访者验证事实。
感知
在访谈中,我们将遇到各种类型的用户。其中一些用户对汽车经销商的活动流程非常了解,例如会见潜在客户、邀请他们试驾和订购汽车。其他用户只了解整个流程的一部分,他们的知识范围有限。由于知识范围的不同,我们对同一主题会有不同的感知。
例如,在讨论如何识别一辆车时,我们会听到不同的意见。有些人希望用车辆序列号来识别,而其他人则希望使用自己的内部车辆编号。他们都从不同角度指代同一辆车。这些不同的意见在数据命名阶段必须得到调和。
提出正确的问题
有多种方式来考虑哪些问题相关,哪些问题将使我们能够收集到重要的数据元素。
现有信息系统
是否存在现有的信息系统:手动的还是计算机化的?这个现有系统将会发生什么?我们是从现有系统中导出相关数据以供新系统使用,还是完全淘汰旧系统,或者保留现有系统——暂时或永久。
如果我们必须保留现有系统,我们可能会在两个系统之间建立一个数据交换的桥梁。在这种情况下,我们需要单向桥梁还是双向桥梁?
按时间顺序的事件
谁为展厅订购汽车以及为什么;订单是如何下达的——电话、传真、电子邮件、网站;展厅中的汽车能否出售给客户?
来源和目的地
这里我们探讨信息、金钱、账单、商品和服务。例如,一辆车的来源是什么?它的目的地是哪里?购车者总是个人吗,还是也可以是另一家公司?
紧迫性
思考当前处理信息的方式,你认为哪些问题最迫切需要解决?
避免专注于报告和屏幕
过于集中于用户(感知)需求的方案可能导致数据结构上的差距,因为每个用户不一定能准确把握自己或其他用户的所有需求。在企业中,很少有人能全面理解整个数据图景,以及经常发生的复杂部门间互动。
这种偏差会在访谈中显现。用户通常对可见或可形象化的物品更为熟悉,而对概念则不太熟悉。然而,用户界面(UI)与底层数据之间存在区别。UI 设计考虑了人体工程学和美学问题,而数据结构化则必须遵循不同的、非视觉的规则以达到有效。
案例研究收集的数据
以下是在访谈期间草草记下的一份潜在数据元素和细节列表,这些似乎对当前信息流很重要。在收集过程中,不仅要记录数据元素的名称——姑且称之为“临时名称”——还要记录样本值。这一好处将在第三章中显现。在接下来的数据收集过程中,我们将在适当的地方用括号包含样本值。
来自总经理
我们的总经理朋友保留了买家填写的关于他们整体购车体验的调查问卷。这些问卷包含了对销售人员行为的评价。显然,这些信息是保密的,只有总经理和办公室文员有权访问。调查信息包括:
-
日期:(2006-01-02)
-
销售人员姓名:(Harper, Paul)
-
买家姓名:(Smith, Joe)
-
评估要点:礼貌、提供的信息质量等
-
每一点,买家给出的评分从一到十。
来自销售人员
销售人员准备的主要文件是销售合同,而这位销售人员当然希望准备很多这样的合同!以下是销售合同上出现的要素:
-
买家信息:姓名、地址、邮政编码、电话号码
-
经销商信息:姓名、地址、邮政编码、电话号码
-
销售人员信息:姓名、地址、邮政编码、电话号码
-
本次销售的车辆数量(通常为 1)
-
车辆描述:品牌、型号、年份(Fontax Mitsou 2007)
-
车辆状况:新车/二手车
-
车辆序列号:(D34HTT987)
-
车辆颜色:(aquamarine)
-
销售价格:(32,500)
-
保险公司名称:(MicMac Car Insurance Inc.)
-
保险单号:(J44-5764,但每家公司对此有自己的编码系统)
-
准备成本:(800)
-
税额:(2,400)
-
总价:(35,700)
-
交换车辆:
-
品牌:(Licorne)
-
型号:(Wanderer)
-
年份:(2006)
-
序列号:(D45TGH45738)
-
交换价格:(12,000)
-
首付:(4,000)
-
利率:(9%)
-
利息金额:(6345)
-
信贷利率类型:固定/浮动
-
首次及最后一次付款日期:(2007-07-01, 2011-06-01)
-
付款次数:(48)
-
金融机构信息:名称、地址、邮政编码、电话号码
来自商店助理
商店助理为每辆进入展厅的车辆分配一个车号。这有助于管理哪套钥匙属于哪辆车,我们这里指的是实体钥匙——用于解锁和启动汽车的钥匙,而非数据库键。车号并不指代车辆的序列号;它是按顺序分配的,仅供内部使用。
商店助理还准备了一份交付证书,其中包含以下信息:
-
买家姓名:(Joe Smith)
-
经销商编号:(53119)
-
车辆识别号:(1400)
-
钥匙号码:(81947)
-
四份签名及日期,来自买家、总经理、销售人员和商店助理
最后,商店助理保存了一份关于所有车辆移动的登记册。对于每辆车,卡片索引包含:
-
车辆识别号:(432)
-
车辆订购日期:(2007-02-03)
-
车辆到达日期:(2007-02-17)
-
车辆展示厅放置日期:(2007-02-19)
-
洗车日期:(2007-05-30)
-
汽车油箱加满日期:(2007-05-30)
-
车辆交付给买家日期:(2007-06-01)
其他备注
-
我们是否应在模型中包含客户用以交换其新车的旧车信息?
-
边界:在访谈中决定,目前模型将不包括经销商的汽车租赁活动及其维修服务,尽管有关汽车的大量信息可应用于这些活动。
后续章节将对这些数据的命名方面进行整理,并解释分组技巧。
概要
构建全面的数据元素集合对于数据结构化活动的成功至关重要。然而,我们需要明确分析系统的精确界限。然后,通过收集文件并进行访谈活动,我们可以记录一份潜在数据元素列表——我们未来的列名。
第三章:数据命名
在本章中,我们专注于将收集过程中获取的数据元素转化为一组连贯的列名。尽管本章有针对各个步骤的节,以实现高效的数据命名,但这些步骤的实际应用顺序并不固定。事实上,整个过程被分解为步骤,以便逐一阐明每个步骤,但实际的命名过程同时应用了所有这些步骤。此外,命名和分组过程之间的划分在某种程度上是人为的——你会看到,关于命名的某些决策会影响分组阶段,这是下一章的主题。
数据清理
从各种来源收集信息元素后,进行一些清理工作以提高这些元素的重要性是恰当的。每位受访者命名元素的方式可能不一致;此外,一个术语的重要性可能因人而异。因此,进行同义词检测是必要的。
鉴于我们已注意到样本值,现在是时候将我们的元素列表与这些样本值进行交叉参考了。以下是一个实际例子,使用车辆识别号。
当决定订购一辆车——比如 2007 年的 Mitsou——办公室职员会打开一个新文件,并给文件分配一个称为car_id number
的序号,例如 725。此时,尚未从任何汽车供应商处收到确认,因此职员不知道未来车辆的序列号——一个在发动机和其他关键部件上压印的唯一号码。
办公室职员将这辆车的识别号称为car_number
。注册车辆移动的商店助理使用名称stock_number
。但使用这个车辆号码或库存号码对于融资和保险目的并不具有意义;相反,车辆的序列号用于这些目的。
此时,必须通过说服用户认识到标准术语的重要性来达成共识。必须让每个人都清楚,car_number
这一术语不够精确,因此它将在数据元素列表中被car_internal_number
取代,很可能在任何用户界面(UI)或报告中也是如此。
有人可能会认为car_internal_number
应该被更恰当的东西取代;关键在于我们合并了两个同义词:car_number
和stock_number
,并明确了两个看似相似但实则不同的元素之间的区别,消除了一个混淆的源头。
因此我们得到了以下元素:
-
Car_serial_number
-
Car_internal_number
(前身为车辆识别号和库存号)
最终,在处理数据分组时,将不得不做出另一个决定:我们将车辆的物理钥匙号码与哪个号码——序列号还是内部号——关联起来。
细分数据元素
在本节中,我们试图找出某些元素是否应该分解成更简单的部分。这样做的原因是,如果一个元素由多个部分组成,应用程序将不得不为了排序和选择目的而分解它。因此,最好现在就在源头分解元素。在应用程序层面重新组合它将更容易。
分解元素在 UI 层面提供了更多清晰度。因此,在这个层面上,我们将尽量避免(尽可能)众所周知的姓/名颠倒问题。
为了说明这个问题,我们以买家的名字为例。在访谈中,我们注意到名字在表格上有多种表达方式:
表格 | 名字的表达方式 |
---|---|
交付证明 | 乔·史密斯先生 |
销售合同 | 史密斯,乔 |
我们注意到
-
有一个称呼元素,先生
-
元素
name
过于不精确;我们实际上有一个名字和一个姓氏 -
在销售合同上,我们的姓氏后面的逗号实际上应该从元素中排除,因为它只是一个格式化字符
因此,我们确定我们应该将名字细分为以下元素:
-
称呼
-
名字
-
姓氏
有时将一个元素进一步细分是有用的,有时则不然。让我们考虑日期元素。我们可以将每个日期细分为年、月和日(三个整数),但这样做会失去 MySQL 提供的日期计算可能性。其中包括,从日期中找出星期几,或确定某个日期三十天后的日期。因此,对于日期(和时间),单个列可以处理所有内容,尽管在 UI 层面,应为年、月和日显示单独的输入字段。这是为了避免任何混淆的可能性,也因为我们不能期望用户知道 MySQL 接受的有效日期是什么。有效值的范围有一定的灵活性,但我们可以理所当然地认为用户在输入无效值方面具有无限的创造力。如果 UI 上只有一个字段,应提供明确的指导以帮助正确填写此字段。
包含格式化字符的数据元素
我们最后要考察的是电话号码。在世界许多地方,电话号码遵循特定模式,并使用格式化字符以提高可读性。在北美,我们有区号、交换号和电话号码,例如,418-111-2222;电话号码后面可能还会加上分机号。然而,在实践中,只有区号和分机号被从其余部分分离出来,成为独立的数据元素。此外,人们经常输入格式化字符,如(418) 111-2222,并期望这些字符能被输出回来。因此,必须选择一个标准输出格式,然后正确设置模型中的子元素数量,以便能够重新创建预期的输出。
作为结果的数据
尽管似乎自然地为汽车的total_price
设置一个独立的元素,但实际上这并不合理。原因是总价是一个计算结果。在销售合同上打印总价构成了一种输出。因此,我们在列名列表中消除了这一信息。出于同样的原因,我们可以省略tax
列,因为它可以计算得出。
通过删除总价列,我们可能会遇到一个陷阱。我们必须确保能够从其他子总元素中重建这个总价,现在和将来都是如此。出于多种原因,这可能是不可能的:
-
总价包括位于另一个表中的金额,而这个表会随时间变化,例如税率。为了避免这个问题,请参阅第四章中随时间可扩展性部分的建议。
-
这个总价包含了一个任意值,由于某些特殊情况,例如,有特别促销,而系统中未计划回扣,或者幸运买家是总经理的姐夫!在这种情况下,可以做出决定:增加一个新列
other_rebate
。
数据作为列名或表名
现在是揭露可能是最不为人知的数据命名问题的时候了:数据隐藏在列名甚至表名中。
我们在第一章中有一个这样的例子。记得qty_2006_1
这个列名。尽管这是一个常见的错误,但它仍然是一个错误。这里我们明显有两个概念,数量和日期。当然,为了能够只使用两个列,关于键的一些工作必须完成——这在第四章中有所涉及。目前,我们应该在我们的元素列表中使用像quantity
和date
这样的元素,避免在列名中表示数据。
为了在我们的模型中找到这些有问题的案例,一种可能的方法是寻找数字。像address1, address2
或phone1, phone2
这样的列名应该看起来可疑。
现在,请看第二章中我们从店员那里得到的数据元素。你能找到一个数据隐藏在列名中的案例吗?
如果你完成了这个练习,你可能会发现许多过去分词隐藏在列名中,如已订购、已到达和已清洗。这些描述了发生在汽车上的事件。我们可以尝试预见所有可能的事件,但这可能证明是不可能的。谁知道什么时候会需要一个新的列car_provided_with_big_ribbon
?如果将这些事件作为不同的列名处理,必须通过
-
数据结构的变更
-
代码(UI 和报告)的变更
为了保持灵活性并避免宽表综合症,我们需要两个表:car_event
和event
。
以下是这些表的结构和示例值:
CREATE TABLE `event` (
`code` int(11) NOT NULL,
`description` char(40) NOT NULL,
PRIMARY KEY ('code')
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
INSERT INTO `event` VALUES (1, 'washed');
注意
此处使用反引号('event'),虽然不是标准 SQL,但这是 MySQL 扩展,用于包围和保护标识符。在特定情况下,它可以帮助我们在 MySQL 5.1 中,其中 event 关键字计划成为语言的一部分,用于其他目的(CREATE EVENT
)。在撰写本文时,MySQL 5.1.11 beta 版接受CREATE TABLE event
,但这可能不会一直成立。
以下图像显示了通过 phpMyAdmin 的插入子页面输入到event
表中的示例值:
CREATE TABLE `car_event` (
`internal_number` int(11) NOT NULL,
`moment` datetime NOT NULL,
`event_code` int(11) NOT NULL,
PRIMARY KEY ('internal_number')
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
INSERT INTO `car_event` VALUES (412, '2006-05-20 09:58:38', 1);
再次,示例值通过 phpMyAdmin 输入:
数据也可能隐藏在表名中。让我们考虑car
和truck
表。它们可能应该合并为一个vehicle
表,因为车辆的类别——卡车、汽车以及其他值如小型货车,实际上是特定车辆的一个属性。我们还可以为这个表名问题找到另一个案例:一个名为vehicle_1996
的表。
变更规划
在设计数据结构时,我们必须考虑如何管理其增长以及所选技术可能带来的影响。
假设需要支持一个未计划的轿车特性——重量。通常的解决方法是找到适当的表并添加一个列。确实,这是最佳解决方案;然而,需要有人更改表结构,可能还需要更改用户界面。
自由字段技术,也称为二级数据或EAV(实体-属性-值)技术,在这种情况下有时会被采用。简而言之,我们使用一个其值本身就是列名的列。
注意
尽管此处展示了此技术,但我不建议使用它,原因在下面的自由字段技术的陷阱部分中解释。
此技术与我们的car_event
表的区别在于,对于car_event
,各种属性都可以与一个共同主题——事件相关联。相反,自由字段可以存储任何类型的不同数据。这也可以是存储特定于单个实例或行表的数据的一种方式。
在以下示例中,我们使用car_free_field
表来存储关于内部编号为 412 的汽车的未计划信息。重量和特殊油漆未被计划,因此用户界面为用户提供了指定他们想要保留哪些信息以及相应值的机会。此处展示的是 phpMyAdmin 的截图,但很可能用户会看到另一种界面——例如,销售人员可能未受过数据库层面的操作培训。
CREATE TABLE `car_free_field` (
`internal_number` int(11) NOT NULL,
`free_name` varchar(30) NOT NULL,
`free_value` varchar(30) NOT NULL,
PRIMARY KEY ('internal_number','free_name')
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
INSERT INTO `car_free_field` VALUES (412, 'weight', '2000');
INSERT INTO `car_free_field` VALUES (412, 'special paint needed', 'gold');
自由字段技术的陷阱
尽管使用这种表格可以增加灵活性并避免用户界面维护,但出于多种原因,我们应避免使用它。
-
将这个“列”(例如所需的特殊油漆)与包含可能颜色的查找表通过外键约束关联起来变得不可能。
-
free_value
字段本身必须定义为VARCHAR
这样的通用字段类型,其大小必须足够宽以容纳所有可能对应的free_name
值的所有值。 -
这使得验证变得不易(例如,对于重量,我们需要一个数值)。
-
在这些自由字段上编写 SQL 查询变得更加复杂——例如
SELECT internal_number from car_free_field where free_name = 'weight' and free_value > 2000
。
命名建议
这里我们触及了一个可能变得敏感的话题。建立命名约定并不容易,因为它可能干扰设计师的心理。
设计师的创意
程序员和设计师通常认为自己是有想象力、有创造力的人;UI 设计和数据模型是他们想要表达这些品质的领域。由于命名即写作,他们希望在列和表名上留下个人印记。这就是为什么团队合作进行数据结构设计需要一定程度的谦逊,并且只有当每个人都成为优秀的团队成员时才能取得良好结果。
此外,在审视这一领域的他人工作时,有一种强烈的诱惑去改进数据元素的名称。在标准化方面需要有一定的纪律,并且所有团队成员必须协作。
缩写
可能是因为早期的数据库系统对变量和数据元素的表示有严格的限制,缩写的做法多年来一直被教授,并被许多数据结构设计师和程序员遵循。我使用过的编程语言只接受两个字符的变量名——我们不得不大量注释这些截断的变量与其含义之间的对应关系。
如今,我看不出有任何理由系统地缩写所有列和表名;毕竟,谁会理解你的T1
表或你的B7
字段的含义呢?
清晰度与长度:一门艺术
应采用一致的缩写风格。通常,一个句子中最有意义的词应该被放入名称中,省略介词和其他小词。以邮政编码为例,我们可以用不同的列名来表示这个元素:
-
the_postal_code
-
pstl_code
-
pstlcd
-
postal_code
我推荐最后一个,因其简洁性。
后缀
精心选择的后缀可以为列名增添清晰度。例如,对于首次付款日期元素,我建议使用first_payment_date
。实际上,列名的最后一个词通常用来描述内容的类型——如customer_no, color_code, interest_amount
。
复数形式
关于表名的另一个争议点:我们是否应使用复数形式的cars
表?可以认为答案是肯定的,因为该表包含多辆汽车——换句话说,它是一个集合。然而,我倾向于不使用复数形式,原因很简单,它并未增加任何信息量。我知道表是一个集合,因此使用复数形式会显得多余。也可以说,每一行描述了一辆汽车。
从查询的角度考虑主题,我们可以根据查询得出不同的结论。例如,针对汽车表的查询——select car.color_code from car where car.id = 34
,若不使用复数形式,则显得更为优雅,因为这里的主要意图是检索 ID 等于 34 的单辆汽车。而其他一些查询可能使用复数形式更有意义,如select count(*) from cars
。
综上所述,这一部分的争论尚未结束,但最关键的是选择一种形式并在整个系统中保持一致。
命名一致性
我们应确保存在于多个表中的数据元素在各处均以相同的列名表示。在 MySQL 中,列名并非独立存在,它总是位于表内。因此,遗憾的是,我们不能从一组标准化的列名池中挑选一致的列名并将其与表关联。相反,在创建每个表时,我们需明确指定所需的列名及其属性。因此,让我们避免使用不同的名称——如internal_number
和internal_num
,当它们指向同一实体时。
对此有一个例外:如果列名指向另一表中的键——如state
列——并且有多个列指向它,比如state_of_birth
、state_of_residence
。
MySQL 的可能性与可移植性
MySQL 允许在标识符——数据库、表和列名中使用比其竞争对手更多的字符。空格和重音字符均被接受。简单的权衡是,我们需要用反引号将这些特殊名称括起来,如'state of residence'
。这为数据元素的表达提供了极大的自由,尤其是对非英语设计者而言,但引入了不可移植的状态,因为这些标识符在标准 SQL 中不被接受。甚至某些 SQL 实现仅接受大写标识符。
在决定包含此类字符之前,我建议应极为谨慎。即便忠于 MySQL,在升级至 4.1 之前的版本时,也曾出现过可移植性问题。在 4.1.x 版本中,MySQL 开始以内置 UTF-8 编码表示标识符,因此在升级前必须进行重命名操作,确保数据库、表、列及约束名中不包含重音字符。在 24/7 系统可用性的背景下,这一繁琐操作并不实用。
表名转为列名
另一种常见风格是:人们会系统地将表名作为前缀添加到每个列名上。因此,car
表将包含以下列:car_id_number, car_serial_number
。我认为这显得多余,且在审视我们构建的查询时,这种做法的不优雅之处便显而易见:
select car_id_number from car
这还不算太糟,但当进行表连接时,我们得到的查询会是这样的:
select car.car_id_number,
buyer.buyer_name
from car, buyer
由于在应用层面,我们编写的大多数查询都是涉及多表的,如上例所示,使用表名(即使是缩写)作为列名的一部分,其笨拙性变得显而易见。当然,我们在命名一致性部分提到的例外情况同样适用:指向查找表的外键列通常会包含该表名作为列名的一部分。例如,在car_event
表中,我们有event_code
,它指向event
表中的code
列。
总结
为了获得清晰且易于理解的数据结构,恰当的数据元素命名至关重要。我们探讨了多种技巧,以确保构建出一致的表名和列名。
第四章:数据分组
在前几章中,我们构建了数据收集,并通过适当的命名开始清理它。我们在第一章中已经介绍了表的概念,它逻辑上汇总了关于某个主题的信息。我们在命名过程中将收集到的一些列分组到表中。在此过程中,我们注意到名称检查有时会引导我们将数据分解到更多的表中,就像我们对car_event
和event
表所做的那样。本章的目标是通过检查将列名分组到表中的技术,为我们的结构提供最后的润色。我们的数据元素不会“悬空”;它们必须被组织到表中。具体哪些列必须放在哪个表中,将在本节中考虑。
初始表列表
构建结构时,我们可以先找出那些看似适合数据分组的通用、自然主题。这些主题将提供我们初始的表列表——以下是一个简化的示例,展示了这个列表可能的样子:
-
车辆
-
客户
-
事件
-
车辆销售
-
客户满意度调查
我们将从考虑vehicle
表开始进行列分组工作。
表布局规则
可能存在多个正确解决方案,但任何正确解决方案都将倾向于遵循以下原则:
-
每个表都有一个主键
-
当考虑所有表作为一个整体时,不存在冗余数据
-
表中的所有列都直接依赖于主键的所有部分
这些原则将在以下章节中详细研究。
主键和表名
首先,我们来定义唯一键的概念。在定义了唯一键的列上,该表中不能有重复的值。主键由一个或多个列组成,它是可以用来唯一标识表中一行的值。为什么我们需要主键?MySQL 本身并不强制要求特定表必须有主键,也不强制要求有唯一键或其他类型的键。因此,MySQL 并不强制我们遵循 Codd 的规则。然而,在实践中拥有主键是很重要的;在构建 Web 界面和其他应用程序时获得的经验表明,能够通过一个唯一标识一行的键进行引用是非常有用的。在 MySQL 中,主键是一个所有列都必须定义为NOT NULL
的唯一键;这个键的名称是PRIMARY
。选择主键几乎与选择表名同时进行。
选择表名是一个微妙的过程。我们必须足够通用,以便为将来的扩展提供空间——例如,选择vehicle
表而不是car
和truck
。同时,我们试图避免出现空洞——即表中的空列。
为了决定是否应该有一个vehicle
表或两个单独的表,我们查看每种车辆的可能属性。它们是否足够常见?两种车辆类型都有颜色、型号、年份、序列号和内部编号。理论上,列的列表必须相同,我们才能决定一组列将属于单个表;但如果只有少数几个属性不同,我们可以稍微作弊。
假设我们决定创建一个vehicle
表。由于前面解释的原因,我们希望从订购车辆的那一刻起跟踪车辆——我们将使用其内部编号作为主键。在设计此表时,我们询问自己此表是否可以用于存储我们从客户那里交换得来的车辆信息。答案是肯定的,因为描述车辆与发生在车辆上的交易(新车销售、从客户那里购买的二手车)无关。验证结构部分提供了更多示例,可以帮助发现结构中的问题。以下是vehicle
表的版本 1,包含列名和示例值——我们用星号标记构成主键的列:
表:车辆 | 列名 | 示例值 |
---|---|---|
*内部编号 | 123 | |
序列号 | D8894JF | |
品牌 | Licorne | |
型号 | 瞪羚 | |
年份 | 2007 | |
颜色 | 海洋蓝 | |
状况 | 全新 |
我们应该在此表中包含销售信息,例如定价和销售日期吗?我们确定答案是否,因为可能会发生多种情况:
-
车辆可以转售
-
该表可能用于存储交换得来的车辆信息
我们现在必须检查我们的工作并验证我们是否遵守了原则。我们有一个主键,但冗余和依赖性呢?
数据冗余和依赖性
在可能的情况下,我们应该将冗余数据迁移到查找表(也称为参考表),并将代码的值仅存储在我们的主表中。我们不希望在每售出一辆“Licorne”时都在车辆表中重复“Licorne”。冗余数据浪费磁盘空间,并在进行数据库维护时增加处理时间:如果需要进行修改,则必须更新同一数据的所有实例。关于vehicle
表,在品牌、型号
和颜色
列中存储完整的描述性值将是多余的——存储三个代码就足够了。
我们必须小心处理冗余数据。例如,我们不会对年份进行编码;这样做没有节省——使用 A 代表 2006 年,B 代表 2007 年,在几千年的情况下并没有实际的空间节省!即使对于少量年份,节省的空间也不会显著;此外,我们将失去对年份进行计算的能力。
接下来,我们验证依赖性。每一列都必须直接依赖于主键。新/旧状态是否直接依赖于车辆?不,如果我们考虑时间维度。理论上,经销商可以出售一辆车,然后稍后接受它作为交换。条件更多地与特定日期的交易本身相关,因此它实际上属于销售
表——此处显示为非最终状态。我们现在有了版本 2:
表: 车辆 | 列名 | 示例值 |
---|---|---|
*内部 ID | 123 | |
序列号 | D8894JF | |
品牌代码 | L | |
模型代码 | G | |
年份 | 2007 | |
颜色代码 | 1A6 | |
表: 品牌 | 列名 | 示例值 |
--- | --- | --- |
*代码 | L | |
描述 | Licorne | |
表: 模型 | 列名 | 示例值 |
--- | --- | --- |
*代码 | G | |
描述 | Gazelle | |
表: 颜色 | 列名 | 示例值 |
--- | --- | --- |
*代码 | 1A6 | |
描述 | 海洋蓝 | |
表: 销售 | 列名 | 示例值 |
--- | --- | --- |
*日期 | 2006-03-17 | |
*内部 ID | 123 | |
条件代码 | N |
复合键
复合键,也称为组合键,是由多个列组成的一个键。
在布局我们的代码表时,我们必须验证数据分组原则是否也在这些表上得到遵守。使用示例数据,并通过我们的想象力补充不完整的示例数据,可以帮助揭示此领域的问题。在我们的版本 2 中,我们忽略了一种可能性。如果营销两个不同品牌的公司选择相同的颜色代码 1A6 来代表不同的颜色怎么办?模型代码也可能发生同样的情况,因此我们应该改进结构,将品牌代码——代表 Fontax、Licorne 或未来品牌名称——纳入模型
和颜色
表中。因此,版本 3 展示了从版本 2 更改的两个表:
表: 模型 | 列名 | 示例值 |
---|---|---|
*品牌代码 | L | |
*代码 | G | |
描述 | Gazelle | |
表: 颜色 | 列名 | 示例值 |
--- | --- | --- |
*品牌代码 | L | |
*代码 | 1A6 | |
描述 | 海洋蓝 |
无论是模型
还是颜色
表,最终都具有复合键。另一个复合键的例子是在第三章中看到的:汽车事件
表——参见数据作为列名或表名部分。在这些类型的表中,主键由多个元素组成。当我们必须描述与多个表相关的数据时,就会发生这种情况。通常,新形成的汽车事件
表包含汽车的内部编号和事件代码,还包含进一步的属性,如特定事件发生在特定汽车的日期。
当我们遇到诸如公司部门之类的子集时,另一种复合键的可能性出现。仅将员工 ID 与公司代码或部门代码关联,并不能正确描述情况。员工 ID 只有在同时考虑部门和公司时才是唯一的。
我们必须验证此表中所有非键数据元素是否直接依赖于整个键。以下是一个有问题的案例,其中company_name
列位置不当,因为它与dept_code
无关:
表:公司部门 | 列名 | 示例值 |
---|---|---|
*company_code | 1 | |
*dept_code | 16 | |
dept_name | Marketing | |
company_name | Fontax |
先前的例子并不理想,因为公司名称会出现在旨在描述每个部门的表的每一行中。先前例子的正确结构意味着需要使用两个表:
表:部门 | 列名 | 示例值 |
---|---|---|
*company_code | 1 | |
*code | 16 | |
name | Marketing | |
表:公司 | 列名 | 示例值 |
--- | --- | --- |
*code | 1 | |
name | Fontax |
改进结构
即使我们的表布局遵循规则,我们仍可以通过关注以下额外问题来进一步优化它。
时间上的可扩展性
在第三章(结果数据节)中,我们了解到,只要在参考表中拥有确切的税率,就可以避免为税额预留一列。然而,此税率可能变动,因此我们需要一个更完整的表,其中包含日期范围及相应的税率。这样,在时间维度上对系统进行投影时,我们能确保其适应税率波动。请注意,下面的sale
表并不完整:
表:销售 | 列名 | 示例值 |
---|---|---|
*date | 2006-03-17 | |
*internal_id | 123 | |
condition_code | N | |
表:条件 | 列名 | 示例值 |
--- | --- | --- |
*code | N | |
description | New |
通过比较sale
表中的date
列与以下tax_rate
表中的start_date
和end_date
,我们可以找到销售日期的确切税率:
表:税率 | 列名 | 示例值 |
---|---|---|
*start_date | 2006-01-01 | |
*end_date | 2006-04-01 | |
rate | .075 |
实际上,所有表都应进行分析,以确定是否考虑了时间因素。另一个例子是color
表。假设我们使用每个汽车制造商设计的颜色代码,制造商是否会在后续年份为不同颜色重复使用颜色代码?如果是这种情况,我们将在color
表中添加一个year
列。
空列
尽管空列不一定有问题,但某些行中一个或多个列的空缺可能揭示了结构问题:两个表合并成了一个。让我们考虑汽车移动。我们构建了一个具有汽车内部编号、事件代码和时刻的结构。但如果某些事件需要更多数据来描述呢?
在纸质表格中,我们发现当汽车被清洗时,进行清洗的店员的首字母会出现在表格上,并且在采访中,我们了解到这些首字母是一个重要的数据元素。
在这种情况下,我们可以将员工信息,即员工代码,添加到 car_event
表中。这将使系统能够识别哪个店员参与了与汽车相关的任何事件,从而提高质量控制。
另一个可能出现的问题是,对于特定事件(例如洗车),我们需要更多数据,如清洁产品的数量和洗车所用的时间。在这两个元素中,存储事件的开始和结束时间可能有助于改进我们的结构。但是,向 car_event
表添加类似 quantity_cleaning_product 的列需要仔细分析。除洗车外的所有事件,此列都将保持空白,导致应用程序中需要进行异常处理。如果我们为另一个特殊事件添加另一个相关列,结构只会变得更糟。
在这种情况下,最好创建另一个具有相同键和附加列的表。我们无法避免在这个新表名 car_washing_event
中包含一些数据元素。
表:car_washing_event | 列名 | 示例值 |
---|---|---|
*internal_number | 412 | |
quantity_cleaning_product | 12 |
避免使用 ENUM 和 SET
MySQL 和 SQL 通常提供看似方便的数据类型:ENUM
和 SET
类型。这两种类型允许我们为列指定可能值列表,以及默认值;区别在于 SET
列可以包含多个值,而 ENUM
只能包含其中一个潜在值。
我们在这里看到一个非常小的 sale
表,其中 credit_rate
列是一个 ENUM:
CREATE TABLE `sale` (
`internal_number` int(11) NOT NULL,
`date` date NOT NULL,
`credit_rate` ENUM('fixed','variable') NOT NULL,
PRIMARY KEY (`internal_number`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
当字段定义为 ENUM 或 SET 时,如果我们使用 phpMyAdmin 的插入或数据编辑面板,会显示一个值的下拉列表,这可能会诱使我们使用这些数据类型。
让我们来审视这类数据类型的优势:
-
与其存储完整值,MySQL 仅存储一个整数索引,该索引根据列表中的值数量使用一到两个字节。
-
MySQL 本身拒绝任何不在列表中的值
尽管考虑到了这些优势,但出于以下原因,仍建议不要使用 ENUM
和 SET
类型:
-
更改可能值列表需要开发人员操作,例如结构修改干预。
-
这些类型有其限制:列表中最多可有 65535 种可能值;同时,一个
SET
可以有 64 个活跃成员,即集合中选定的值。 -
保持系统更简单为佳,因为如果在某些情况下我们使用查找表,而在其他情况下使用
ENUM
或SET
类型,程序代码构建和维护起来会更加复杂。
有人可能会争辩说,问题一可以通过在应用程序中包含一些ALTER TABLE
语句来改变值列表来解决,但这似乎不是处理此事的常规方式。ALTER TABLE
是一个数据定义语句,应在系统开发期间使用,而不是在应用程序级别。
因此,ENUM
或SET
列应成为具有代码作为主键的单独表。然后,引用此代码的表只需将其作为外键包含。在SET
列的情况下,一个独立的表将包含主表的键以及包含这些SET
值的表的键。
表:销售 | 列名 | 示例值 |
---|---|---|
*内部编号 | 122 | |
*日期 | 2006-05-27 | |
信用评级代码 | F | |
表:信用评级 | 列名 | 示例值 |
--- | --- | --- |
*代码 | F | |
描述 | 固定 |
应用程序中的适当验证确保插入的代码属于查找表。
多语言规划
使用代码表的另一个好处是:如果我们存储车辆状况为新/旧,那么开发多语言应用程序会更加复杂。另一方面,如果我们对车辆状况进行编码,那么我们可以有一个condition
表和一个language
表:
表:条件 | 列名 | 示例值 |
---|---|---|
语言代码 | E | |
条件代码 | N | |
描述 | 新 | |
表:语言 | 列名 | 示例值 |
--- | --- | --- |
语言代码 | E | |
描述 | 英语 |
验证结构
验证是通过使用精确的例子进行的,我们自问是否有列来放置所有信息,涵盖所有情况。也许会有例外——我们该如何处理这些?我们的结构应该处理它们吗?我们可以评估与这些例外相关的风险因素,与处理它们的成本以及查询性能可能的损失进行比较。
一个异常情况的例子:一个顾客同一天购买了两辆车——这可能影响主键的选择,如果日期是该主键的一部分,那么添加一个列到该主键中将是有益的:销售当天的具体时间。
phpMyAdmin 工具在这里可能证明是有用的。使用此软件可以轻松构建表,而其索引管理功能允许我们设计主键。然后,我们可以使用多表查询生成器来模拟各种报告和假设情况。
总结
我们已经看到,我们的列列表需要放置在适当的表格中,每个表格都有一个主键,并遵循一些规则以提高效率和清晰度。我们还可以通过考虑可扩展性和多语言问题来改进模型;然后我们学习了一种验证此模型的方法。
第五章:数据结构调优
本章介绍各种技术以从安全性、性能和文档方面改进我们的数据结构。然后,我们展示汽车经销商案例研究的最终数据结构。
数据访问政策
我们在第一章中了解到,数据是一项重要资源,因此对该资源的访问必须受到控制并清晰记录。每条数据产生时,数据录入的责任必须明确确立。数据进入数据库后,必须有政策来控制对其的访问,这些政策通过 MySQL 的权限和使用视图来实施。
责任
我们应该确定企业中谁——以个人姓名或职能名称——负责每个数据元素。这应该被记录下来,一个好地方就是在数据库结构中直接记录。另一种选择是将数据责任记录在纸上,但纸质信息容易丢失,且有迅速过时的倾向。
在某些情况下,会有一个主要来源和一个审批级别来源。两者都应这样记录——有助于
-
应用程序设计中,当屏幕需要反映数据录入的权限链时
-
权限管理,如果直接向终端用户授予 MySQL 数据访问权限
phpMyAdmin 允许我们通过添加注释来描述每一列。如果当前的 MySQL 版本支持原生注释,则使用这些注释;否则,必须配置 phpMyAdmin 的链接表基础设施以启用将列注释作为元数据存储。我们将在相应的列注释中指出该列的责任细节。要访问允许我们在 phpMyAdmin 中输入注释的页面,我们使用左侧导航面板打开数据库(这里为marc
),然后是表(这里为car_event
)。然后我们点击结构并选择通过点击铅笔图标编辑字段结构(这里为event_code
)。
我们可以从结构页面使用 phpMyAdmin 的打印视图来获取带有注释的表列表。
安全和权限
考虑数据安全的两种方式。最常见的是在应用程序层面实施。通常,应用程序应请求凭证:用户名、密码,并使用这些凭证生成反映该用户允许执行的任务的网页或桌面屏幕。请注意,底层应用程序仍以开发人员账户的所有权限连接到 MySQL,但当然,仅根据用户的权利显示适当的数据。
另一个需要考虑的问题是,当用户直接访问 MySQL 时,无论是使用命令行实用程序还是像 phpMyAdmin 这样的界面。这可能是因为最终用户应用程序仅开发到一定程度,并且不允许维护代码表,例如。在这种情况下,应创建具有所需权限的特殊 MySQL 用户。MySQL 支持基于数据库、表、列和视图上的权限的访问矩阵。这样,我们可以向所有未经授权的人员隐藏特定列,例如销售价格。
视图
自 MySQL 5.0 起,我们可以创建视图,这些视图看起来像表,但实际上是基于查询的。这些视图可用于:
-
隐藏某些列
-
根据表列和对其使用的表达式生成修改后的信息
-
通过连接许多表来提供数据访问的快捷方式,使它们看起来像一个表
由于我们可以将权限关联到这些视图,而不授予对基础表的访问权限,因此视图可以证明在让用户直接访问 MySQL 并同时控制其操作方面很方便。
以下是一个视图示例,显示了汽车事件及其描述——这里,我们希望隐藏event_code
列:
create view explained_events as
select car_event.internal_number, car_event.moment, event.description
from car_event
left join event on car_event.event_code = event.code
在 phpMyAdmin 中浏览此视图会显示以下报告:
要求用户使用视图并不意味着该用户只能读取此数据。在许多情况下,视图可以更新。例如,允许以下语句:
UPDATE `explained_events`
SET `moment` = '2006-05-27 09:58:38'
WHERE `explained_events`.`internal_number` = 412;
存储引擎
MySQL 内部结构是这样的,存储和管理数据的低级任务由可插拔存储引擎架构实现。MySQL AB 和其他公司正在研发中,以改进存储引擎范围的供应。有关架构本身的更多信息,请参阅dev.mysql.com/tech-resources/articles/mysql_5.0_psea1.html
。
每次我们创建表时,即使我们没有注意到,我们也在要求 MySQL 服务器(无论是隐式还是显式)使用可用的存储引擎之一来物理存储我们的数据。
默认和传统的存储引擎名为MyISAM
。《MySQL 参考手册》(dev.mysql.com/doc/refman/5.0/en/storage-engines.html
)中有一整章描述了可用的引擎。我们的存储引擎选择可以根据表的不同而变化。不存在完美的存储引擎;我们必须根据需求选择最佳的。以下是选择时需要考虑的一些要点:
-
MyISAM
支持FULLTEXT
索引和压缩的只读存储,并且使用大约三分之一的磁盘空间比InnoDB
少,用于等量的数据 -
InnoDB
提供外键约束,多语句事务支持ROLLBACK
;此外,由于其锁定机制,它支持比MyISAM
更多的并发SELECT
查询 -
MEMORY
当然非常快,但内容(数据)并非永久存储在磁盘上,而表定义本身则存储在磁盘上 -
NDB
(网络数据库),也称为MySQL 集群
,提供服务器间的同步复制——集群中推荐的最小服务器数量为四台;因此,这样的集群中没有单点故障
简而言之,这里有一个通用指南:如果应用程序需要多语句事务和外键约束,我们应选择InnoDB
;否则,默认存储引擎MyISAM
是建议的选择。
外键约束
InnoDB
存储引擎(www.innodb.com
),包含在 MySQL 中,提供了一种在表结构中描述外键的机制。外键是一个列(或一组列),指向表中的一个键。通常,被指向的键位于另一个表中,并且是该表的主键。外键常用于查找表。在结构中直接描述这些关系有许多好处:
-
表的参照完整性由引擎维护——如果
event
表中不存在对应的代码,我们就不能向car_event
表添加事件代码,同样,如果car_event
表中仍有行引用该代码,我们就不能从event
表删除代码 -
我们可以编程设定 MySQL 在特定事件发生时的反应;例如,如果被引用代码更新,引用表中会发生什么
让我们将car_event
示例转换为InnoDB
。首先创建并填充被引用表event
——注意ENGINE=InnoDB
子句:
CREATE TABLE `event` (
`code` int(11) NOT NULL,
`description` char(40) NOT NULL,
PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `event` VALUES (1, 'washed');
INSERT INTO `event` VALUES (2, 'arrived');
接下来,引用表car_event
:
CREATE TABLE `car_event` (
`internal_number` int(11) NOT NULL COMMENT 'Resp.:Office clerk',
`moment` datetime NOT NULL COMMENT 'Resp.: store assistant',
`event_code` int(11) NOT NULL COMMENT 'Resp.: store assistant',
PRIMARY KEY (`internal_number`),
KEY `event_code` (`event_code`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `car_event` VALUES (412, '2006-05-27 09:58:38', 2);
INSERT INTO `car_event` VALUES (500, '2006-05-29 16:37:46', 1);
INSERT INTO `car_event` VALUES (600, '2006-05-30 16:38:51', 2);
INSERT INTO `car_event` VALUES (700, '2006-05-31 16:39:21', 2);
我们必须为event_code
列创建索引,以便能在InnoDB
外键约束中使用它,该约束在此定义:
ALTER TABLE `car_event`
ADD CONSTRAINT `car_event_ibfk_1` FOREIGN KEY (`event_code`)
REFERENCES `event` (`code`) ON UPDATE CASCADE;
注意
在初始的CREATE TABLE
语句中也可以定义car_event
中的外键。之前的示例使用ALTER TABLE
来展示外键可以在之后添加。
所有这些操作都可以通过 phpMyAdmin 以更直观的方式处理。操作子页面允许我们将引擎切换到InnoDB:
此外,当表处于InnoDB
存储引擎下时,phpMyAdmin 的关系视图使我们能够定义和修改外键及相关操作:
定义了ON UPDATE CASCADE
子句后,让我们看看当我们在event
表中修改代码值时会发生什么。我们决定将washed的代码从1改为10:
现在我们浏览car_event
表;果然,washed的代码已自动更改为值 10:
性能
如果我们想提高结构在访问速度或磁盘空间使用方面的效率,必须检查多个要点。
索引
在WHERE
子句中使用的列上添加索引是加快查询的常见方法。假设我们打算找到特定品牌的所有车辆。vehicle
表有一个brand_id
列,我们想在此列上创建索引。在这种情况下,索引不会是唯一的,因为每个品牌都由许多车辆代表。
使用 phpMyAdmin 创建索引有两种方法。首先,如果索引适用于单个列,我们可以打开表的结构页面,并在brand_id
列的同一行点击索引(闪烁)图标:
这会生成以下语句:
ALTER TABLE `vehicle` ADD INDEX(`brand_id`)
我们还可以在复合键上创建索引,例如model_id
加上year
。为此,我们在结构页面上输入索引的列数(两列),然后点击Go。
接下来,在索引管理页面上,我们选择哪些列将成为索引的一部分;然后我们为此索引发明一个名称(这里为model-year),点击Go来创建它。
此操作相关的 SQL 命令是
ALTER TABLE `vehicle` ADD INDEX `model-year` (`model_id`,`year`)
要确定特定查询使用了哪些索引,我们可以在查询前加上 EXPLAIN 关键字。例如,我们在 phpMyAdmin 的查询框中输入以下命令:
explain select * from vehicle where brand_id = 1
结果告诉我们,brand_id
列上的索引可能是检索的关键:
帮助查询优化器:分析表
当我们向 MySQL 服务器发送查询时,它会使用查询优化器来找到检索行的最佳方式。我们可以通过向表中加载数据,然后执行ANALYZE TABLE
语句来帮助查询优化器获得更好的结果。此语句要求 MySQL 存储表的键分布,这意味着它会计算每个索引的键数,并将此信息存储以供后续重用。例如,在vehicle
表上执行ANALYZE TABLE
后,MySQL 可能会注意到有 12 个不同的品牌,1000 个不同的车辆和 100 个不同的车型年份。如果我们以后发送使用这些索引之一的查询,此信息将被使用。因此,ANALYZE TABLE
应定期执行;确切的频率取决于此表的更新次数。
访问复制从服务器
MySQL 支持一种模式,即数据在主服务器和一台或多台从服务器之间进行单向异步复制。由于通常情况下,发送到 MySQL 的大多数请求都是SELECT
查询,我们可以通过将这些读请求发送到从服务器来提高响应时间。这会产生负载均衡的效果。必须注意将写类型语句,如INSERT, UPDATE
和DELETE
发送到主服务器。
在当前的 MySQL 版本(5.0.26)中,我们必须选择适当的应用程序级服务器来实现这种平衡;然而,MySQL 计划提供一个功能,该功能将自动将SELECT
查询发送到从属服务器。
注意
复制是 MySQL 的高级功能,应由经验丰富的 MySQL 管理员设置。
速度与数据类型
创建列时,我们必须为其指定数据类型。字符数据类型(CHAR, VARCHAR
)使用非常普遍。对于CHAR
,我们指定了列的长度(0 到 255),并且该列占用固定数量的空间。对于VARCHAR
,每个值仅占用表中所需的空间;指定的长度是最大长度——MySQL 5.0.3 之前为 255,自此版本起为 65532。数值类型——如INT, FLOAT
和DECIMAL
都是固定长度的。
总结一下,以下是一些数据类型及其存储方式的信息:
数据类型 | 存储方式 |
---|---|
CHAR | 固定 |
INT | 固定 |
FLOAT | 固定 |
DECIMAL | 固定 |
VARCHAR | 变长 |
我们应该意识到 MySQL 可能会悄悄决定将一种数据类型转换为另一种。这样做的原因在 MySQL 手册中有解释:dev.mysql.com/doc/refman/5.0/en/silent-column-changes.html
。这就是为什么在表创建后,我们应该重新检查其结构以验证是否发生了静默转换。
看起来我们似乎应该总是为字符字段选择VARCHAR
,因为使用这种数据类型,较短的值占用较少的空间,但仍有理由想要使用CHAR
:速度。
在表中,当所有字段都使用非变量数据类型时,MyISAM
存储引擎使用固定表格式。在这种格式下,MySQL 可以预测每行的大小,因此可以轻松找到从一个first_name
列到下一行的first_name
列的距离。这意味着对非索引列的查询执行得相对较快。相反,当表中有一个VARCHAR
列时,这种情况就不再可能,因为MyISAM
在这种情况下使用动态表格式。因此,必须在数据检索速度和使用固定长度列的空间开销之间做出决定。
在 phpMyAdmin 中查看表结构时,行统计信息部分会告诉我们格式是固定的还是动态的:
使用固定格式的另一个优点是,当行被删除时,这些行之前占用的空间——表中的空洞——可供未来的插入使用,因此表不会变得物理碎片化。
BLOB
和TEXT
数据类型也是可变长度的。BLOB
通常用于存储汽车或客户照片等二进制数据。MySQL 内部会负责将这些列与表的其余数据分开存储,因此它们对表的影响并不显著。
表大小缩减
实用工具myisampack
可将MyISAM
表转换为只读表并压缩数据。在某些情况下,表的物理大小可减少 70%。此技术仅在我们有权访问此命令行工具时可用——没有 SQL 查询能发送以实现此结果。
列内数据编码
我即将描述的情况发生在我为书目数据开发搜索引擎时,但我将其转用于汽车经销商系统。
当我们需要将数据从原有系统迁移到我们新生的数据结构时,可能会遇到以特殊方式格式化的数据。例如,汽车型号可能的颜色列表可以表示为一系列用分号分隔的颜色代码:
1A6;1A7;2B7;2T1A65
原有系统的用户习惯于以这种格式输入数据,在我经历的情况中,用户拒绝放弃这种数据录入方式——他们可以直接访问 MySQL 表。然而,从开发者的角度来看,这种格式使得查询生成的任务更为复杂。查找1A6
颜色需要拆分数据元素,并避免包含1A6
字符串的2T1A65
数据元素。
此案例的适当结构意味着完全摒弃基于分号的格式,并仅以表格形式存储纯数据:
表:model_color | 列名 | 示例值 |
---|---|---|
*model | 1 | |
color_code | 1A6 |
当分隔符之间存在多个元素(如姓名列表)时,找到与查询一致的数据甚至更为复杂:
Murray Dan; Smith Peter; Black Paul
在搜索Murray, Paul
时,必须特别小心以避免匹配此名称列表,因为Murray
和Paul
都出现在完整字符串中。这种情况仅加强了摆脱这种格式或至少——如果我们因政治问题必须保留此格式——建立一个中间表的必要性,该表将用于搜索。在这种情况下,每当主表内容发生变化时,特殊表必须同步。
案例研究的最终结构
在本节中,我们将审视案例研究的最终数据结构。有许多方式可以呈现此结构。首先,我们将看到所有相互关联的表——几乎所有表都如此——然后,我们将检查相关表组及其列。
以下模式由 phpMyAdmin 的 PDF 页面功能生成。要访问此功能,我们打开一个数据库并访问操作子页面。然后,我们点击编辑 PDF 页面。
在生成 PDF 架构时,我们还可以要求 phpMyAdmin 生成数据字典。为此,我们需在“显示 PDF 架构”对话框中勾选数据字典复选框。以下是描述person
表的该字典页面:
这个结合了数据字典和架构的功能有一个显著特点:我们可以在架构中点击表名以到达字典中该表的描述,反之亦然。
以下CREATE TABLE
命令直接来自 phpMyAdmin 的导出功能。要访问此功能,只需打开一个数据库,选择导出菜单,然后选中所有表,点击SQL复选框并点击执行。
这些命令被分组为相关表的小块,即使最终这些组内的表之间存在关联。你会注意到,phpMyAdmin 在导出文件中以注释的形式添加了与其他表的关系信息。另一个需要注意的是:大多数表的主键是id
,一个整数。因此,指向brand
表的id
列的列被命名为brand_id
。
车辆
--
-- Table structure for table `brand`
--
CREATE TABLE `brand` (
id` int(11) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `brand_color`
--
CREATE TABLE `brand_color` (
`brand_id` int(11) NOT NULL,
`id` int(11) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`brand_id`,`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `brand_color`:
-- `brand_id`
-- `brand` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `brand_model`
--
CREATE TABLE `brand_model` (
`brand_id` int(11) NOT NULL,
`id` int(11) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`brand_id`,`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `brand_model`:
-- `brand_id`
-- `brand` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `event`
--
CREATE TABLE `event` (
`id` int(11) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `vehicle`
--
CREATE TABLE `vehicle` (
`internal_number` int(11) NOT NULL,
`serial_number` varchar(50) NOT NULL,
`brand_id` int(11) NOT NULL,
`model_id` int(11) NOT NULL,
`year` year(4) NOT NULL,
`physical_key_id` int(11) NOT NULL,
`color_id` int(11) NOT NULL,
`category_id` int(11) NOT NULL,
PRIMARY KEY (`internal_number`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `vehicle`:
-- `brand_id`
-- `brand` -> `id`
-- `category_id`
-- `vehicle_category` -> `id`
-- `color_id`
-- `brand_color` -> `id`
-- `model_id`
-- `brand_model` -> `id`
--
-- --------------------------------------------------------
table structure vehicle table--
-- Table structure for table `vehicle_category`
--
CREATE TABLE `vehicle_category` (
`id` int(11) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `vehicle_event`
--
CREATE TABLE `vehicle_event` (
`internal_number` int(11) NOT NULL,
`moment` date NOT NULL,
`event_id` int(11) NOT NULL,
`person_id` int(11) NOT NULL,
PRIMARY KEY (`internal_number`,`moment`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `vehicle_event`:
-- `event_id`
-- `event` -> `id`
-- `internal_number`
-- `vehicle` -> `internal_number`
-- `person_id`
-- `person` -> `id`
--
人员
--
-- Table structure for table `gender`
--
CREATE TABLE `gender` (
`id` TINYINT(4) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `person`
--
CREATE TABLE `person` (
`id` int(11) NOT NULL,
`category_id` int(11) NOT NULL,
`gender_id` TINYINT(4) NOT NULL,
`salutation_id` TINYINT(4) NOT NULL,
`first_name` varchar(50) NOT NULL,
`last_name` varchar(50) NOT NULL,
`address` varchar(300) NOT NULL,
`city` varchar(50) NOT NULL,
`postal_code` varchar(20) NOT NULL,
`phone_area` varchar(20) NOT NULL,
`phone_number` varchar(20) NOT NULL,
`phone_extension` varchar(20) NOT NULL,
`email` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `person`:
-- `category_id`
-- `person_category` -> `id`
-- `gender_id`
-- `gender` -> `id`
-- `salutation_id`
-- `salutation` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `person_category`
--
CREATE TABLE `person_category` (
`id` int(11) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `salutation`
--
CREATE TABLE `salutation` (
`id` TINYINT(4) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
销售
--
-- Table structure for table `condition`
--
CREATE TABLE `condition` (
`id` int(11) NOT NULL,
`description` char(15) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `credit_rate`
--
CREATE TABLE `credit_rate` (
`id` int(11) NOT NULL,
`description` char(30) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `sale`
--
CREATE TABLE `sale` (
`internal_number` int(11) NOT NULL,
`date_sold` date NOT NULL,
`condition_id` int(11) NOT NULL,
`customer_id` int(11) NOT NULL,
`salesperson_id` int(11) NOT NULL,
`base_price` decimal(9,2) NOT NULL,
`insurance_id` int(11) NOT NULL,
`insurance_policy_number` varchar(40) NOT NULL,
`preparation_cost` decimal(9,2) NOT NULL,
`exchange_vehicle_id` int(11) NOT NULL,
`exchange_price` decimal(9,2) NOT NULL,
`down_payment` decimal(9,2) NOT NULL,
PRIMARY KEY (`internal_number`,`date_sold`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `sale`:
-- `condition_id`
-- `condition` -> `id`
-- `customer_id`
-- `person` -> `id`
-- `exchange_vehicle_id`
-- `vehicle` -> `internal_number`
-- `insurance_id`
-- `organization` -> `id`
-- `internal_number`
-- `vehicle` -> `internal_number`
-- `salesperson_id`
-- `person` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `sale_financing`
--
CREATE TABLE `sale_financing` (
`internal_number` int(11) NOT NULL auto_increment,
`date_sold` date NOT NULL,
`financial_id` int(11) NOT NULL,
`interest_rate` decimal(9,4) NOT NULL,
`credit_rate_id` int(11) NOT NULL,
`first_payment_date` date NOT NULL,
`term_years` int(11) NOT NULL,
PRIMARY KEY (`internal_number`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
--
-- RELATIONS FOR TABLE `sale_financing`:
-- `credit_rate_id`
-- `credit_rate` -> `id`
-- `financial_id`
-- `organization` -> `id`
-- `internal_number`
-- `vehicle` -> `internal_number`
--
-- --------------------------------------------------------
--
-- Table structure for table `tax_rate`
--
CREATE TABLE `tax_rate` (
`start_date` date NOT NULL,
`end_date` date NOT NULL,
`rate` decimal(9,4) NOT NULL,
PRIMARY KEY (`start_date`,`end_date`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
其他表
--
-- Table structure for table `parameters`
--
CREATE TABLE `parameters` (
`dealer_number` varchar(30) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `organization`
--
CREATE TABLE `organization` (
`id` int(11) NOT NULL,
`category_id` int(11) NOT NULL,
`name` varchar(50) NOT NULL,
`address` varchar(300) NOT NULL,
`city` varchar(50) NOT NULL,
`postal_code` varchar(20) NOT NULL,
`phone_area` varchar(20) NOT NULL,
`phone_number` varchar(20) NOT NULL,
`phone_extension` varchar(20) NOT NULL,
`email` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `organization`:
-- `category_id`
-- `organization_category` -> `id`
--
-- --------------------------------------------------------
table structure organization table--
-- Table structure for table `organization_category`
--
CREATE TABLE `organization_category` (
`id` int(11) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
--
-- Table structure for table `road_test`
--
CREATE TABLE `road_test` (
`internal_number` int(11) NOT NULL,
`date` date NOT NULL,
`customer_id` int(11) NOT NULL,
`salesperson_id` int(11) NOT NULL,
`customer_comments` varchar(255) NOT NULL,
PRIMARY KEY (`internal_number`,`date`,`customer_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `road_test`:
-- `customer_id`
-- `person` -> `id`
-- `internal_number`
-- `vehicle` -> `internal_number`
-- `salesperson_id`
-- `person` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `survey`
--
CREATE TABLE `survey` (
`id` int(11) NOT NULL,
`date` date NOT NULL,
`customer_id` int(11) NOT NULL,
`salesperson_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `survey`:
-- `customer_id`
-- `person` -> `id`
-- `salesperson_id`
-- `person` -> `id`
--
-- --------------------------------------------------------
table structure survey table--
-- Table structure for table `survey_answer`
--
CREATE TABLE `survey_answer` (
`survey_id` int(11) NOT NULL,
`question_id` int(11) NOT NULL,
`answer` varchar(30) NOT NULL,
PRIMARY KEY (`survey_id`,`question_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
--
-- RELATIONS FOR TABLE `survey_answer`:
-- `question_id`
-- `survey_question` -> `id`
-- `survey_id`
-- `survey` -> `id`
--
-- --------------------------------------------------------
--
-- Table structure for table `survey_question`
--
CREATE TABLE `survey_question` (
`id` int(11) NOT NULL,
`description` varchar(40) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
-- --------------------------------------------------------
总结
我们通过评估每个数据元素的责任人并将此信息存储在列注释中来改进数据结构的实现。然后,我们看到了如何使用权限和视图来提高安全性,如何为每个表选择最佳存储引擎,以及如何利用外键约束。考虑了性能问题,然后我们被展示了汽车经销商案例研究的最终模型。
第六章:补充案例研究
现在,是时候将我们新学的原则应用到一个完全不同的主题上了。我们从汽车升级到飞机,涉及一个简单的航空公司系统。
本章的案例研究并不试图涵盖真实航空公司的全部数据集合——它仅是一个样本。尽管如此,我们将看到之前学到的原则可以应用于构建和完善一个正确且连贯的数据结构。
通常,每家航空公司都有自己的信息系统。我们在此假设已获得授权,构建一个涵盖多家航空公司的信息系统。
文档收集阶段的结果
在审查了航空公司当前的网站、预订代理的网站、一些电子机票和登机牌后,我们收集了大量信息。我们将首先用句子表达这些信息,这些句子从较高层面介绍了系统和数据交换。每个句子后面都列出了我们可以从中推断出的数据元素。一个元素可能出现在多个句子中。有关每个数据元素的更多详细信息,请参阅表格和示例值部分。还有一些注释将帮助我们在命名和分组阶段。
魁北克航空的 456 号航班于 2007 年 10 月 2 日 22:45 从蒙特利尔-特鲁多机场起飞,前往巴黎的戴高乐机场。
以下是可从上述句子中获取的数据元素:
-
flight_number
-
airline_name
-
airport_name
-
flight_departure_moment
注意
我们需要指明机场是用于出发还是到达。
蒙特利尔-特鲁多机场的代码是 YUL,戴高乐机场的代码是 CDG。
上述句子中获取的数据元素是:
- airport_code
注意
我们是否应将 airport_code 作为主键?可能不行,考虑到空间因素。
该航班计划于次日 11:30(当地时间)降落。
获取的数据元素是:
- flight_arrival_moment
注意
我们是否需要将日期和时间拆分为两个字段?可能不必,以便利用日期和时间计算功能(考虑日期的情况下,飞行需要多少小时和分钟)。
一架来自 Fontax 的 APM-300 飞机服务于本次航班。
上述句子中获取的数据元素包括:
-
plane_model
-
plane_brand
注意
我们是否需要将飞机型号与航班关联,同时也要关联到具体的哪一架飞机。(可能存在多架 APM-300。)
本次航班的飞行员是 Dan Murray,乘务员是 Melanie Waters。其他机组人员待确认。
上述句子中获取的数据元素包括:
-
pilot_first_name
-
pilot_last_name
-
flight_attendant_first_name
-
flight_attendant_last_name
注意
我们应该使用机组类别这一概念进行泛化。
Peter Smith 通过预订代理 Fantastic Tour, Inc.购买了本次航班的机票,票号为 014 88417654。这是一张单程票。
上述句子中获取的数据元素包括:
-
passenger_first_name
-
乘客姓氏
-
预订代理名称
-
机票号
-
机票类型
注意
我们还需要为乘客设定一个主键,如果不用其代码,可能还需要为预订代理设定一个主键。机票本身是否应在表中表示,还是机票号作为更通用的预订信息的一部分?
对于此航班,史密斯先生坐在 19A 座位,位于飞机的经济舱。
从上述句子中获得的数据元素有:
-
乘客姓氏
-
座位 ID
-
飞机舱位
注意
飞机上可用的舱位不仅取决于飞机型号,还取决于航空公司。
此机票不可退款。
从上述句子中获得的数据元素有:
- 机票退款性
航班 456 可在起飞前 35 分钟于 74 号登机口登机。
从上述句子中获得的数据元素有:
-
航班号
-
登机口 ID
-
登机时间
经济舱乘客有权携带一件舱内行李和两件注册行李——总重不超过 50 公斤。史密斯先生有一件注册行李,标签号为 AQ636-84763。
从上述句子中获得的数据元素有:
-
飞机舱位
-
最大舱内行李数
-
最大注册行李数
-
最大注册行李重量(公斤)
-
标签 ID
注意
我们发现“class”是“section”的同义词。
机场的信息屏幕显示每趟航班的状态:准时、登机、延误或取消。
从上述句子中获得的数据元素有:
- 航班状态
注意
需要编码(ID 和描述)。
此航班提供两餐。魁北克航空与蒙特利尔厨师服务公司合作准备和配送食物。
从上述句子中获得的数据元素有:
-
餐食数量
-
航空公司名称
-
餐食供应商
魁北克航空拥有四架 Fontax APM-300 飞机,但 302 号飞机(代号 Charlie)计划于 2007 年 10 月进行维修。
从上述句子中获得的数据元素有:
-
航空公司名称
-
飞机品牌
-
飞机型号
-
飞机 ID
-
描述
-
飞机事件
-
飞机事件开始时间
-
飞机事件结束时间
注意
每架飞机都有一个亲切的昵称,此元素将被称为“描述”。关于维修,我们用事件的概念来概括,包括开始和结束的时刻。
史密斯乘客可以使用快速参考代码 A6BCUD 及其姓氏在航空公司网站上访问其航班信息。
从上述句子中获得的数据元素有:
-
乘客姓氏
-
网站快速参考
数据元素初步列表
我们在此列出从文档收集阶段推断出的数据元素。在许多情况下,它们并非已适合最终模型的格式,因为它们带有表名前缀。例如,标识为pilot_last_name
的数据元素将成为pilot
表中的列last_name
。每个数据元素的示例值和更详细信息将在下一部分展示。
数据元素 |
---|
航班起飞时间 |
航班到达时间 |
出发机场代码 |
到达机场代码 |
航空公司代码 |
航空公司名称 |
机场名称 |
飞机品牌 |
飞机型号 |
飞行员姓氏 |
飞行员名字 |
空乘姓氏 |
空乘名字 |
乘客姓氏 |
乘客名字 |
乘客编号 |
预订代理名称 |
机票号码 |
表格与示例值
为了准备表格列表,我们从文档收集阶段构建的句子中可以观察到的物理对象或人物开始。然后我们查看所有元素并构建新表格以容纳它们。
在以下表格描述中,表格布局后跟有适当的设计评论。
代码表
通常首先设计以下表格,因为它们更容易建模,并且对于建立更复杂表格的关系是必需的。
表格:机场 | 列名 | 示例值 |
---|---|---|
*id | 1 | |
国际代码 | YUL | |
描述 | 蒙特利尔-特鲁多 |
机场表可能包含其他列,如地址、电话和网站。
表格:航空公司 | 列名 | 示例值 |
---|---|---|
*id | 1 | |
描述 | 魁北克航空 | |
表格:飞机品牌 | 列名 | 示例值 |
--- | --- | --- |
*id | 1 | |
描述 | Fontax |
我们避免将此表命名为brand
,因为这是一个过于通用的名称。
表格:餐食供应商 | 列名 | 示例值 |
---|---|---|
*id | 9 | |
描述 | 蒙特利尔主厨服务 |
再次,此表可能包含有关代理机构的更多详细信息,如电话和地址。我们还可以通过添加一个标识公司类型的代码将此表与meal_supplier
表合并,但在当前模型中并未实现。
表格:机票类型 | 列名 | 示例值 |
---|---|---|
*id | 1 | |
描述 | 单程 | |
表格:机组类别 | 列名 | 示例值 |
--- | --- | --- |
*id | 1 | |
描述 | 飞行员 |
为了避免列如pilot_last_name, copilot_first_name
,我们创建了一个crew_category
表。另请参阅本章稍后的相关flight_crew
表。
表格:机票退款政策 | 列名 | 示例值 |
---|---|---|
*id | 1 | |
描述 | 不可退款 | |
表格:航班状态 | 列名 | 示例值 |
--- | --- | --- |
*id | 1 | |
描述 | 登机 |
如果我们需要在模型中包含其他类型的事件,这个事件
表将不得不更名为更精确的名称,如飞机事件
,并且我们当前用于关联事件与飞机的飞机事件
表也需要一个新的名称。
主题表
这些表比代码表更全面。每个表都涉及一个特定的主题,需要比简单的代码表更多的列。
表:飞机 | 列名 | 示例值 |
---|---|---|
*ID | 302 | |
航空公司 ID | 1 | |
品牌 ID | 1 | |
机型 ID | 2 | |
描述 | 查理 |
此表标识了哪架飞机属于哪家航空公司,描述字段是航空公司内部用来描述该特定飞机的方式。其他字段,如飞机序列号,也可以添加到这里。
表:机组 | 列名 | 示例值 |
---|---|---|
*ID | 9 | |
类别 ID | 1 | |
姓氏 | 默里 | |
名字 | 丹 |
乘客和机组人员不能物理上合并到一个表中,即使他们属于同一航班,因为用来描述乘客的列集合与描述机组人员的列集合不同。我们将在示例查询部分介绍如何生成飞机上所有人员的合并列表。
表:航班 | 列名 | 示例值 |
---|---|---|
*ID | 34 | |
航空公司 ID | 1 | |
航班号 | 456 | |
出发时间 | 2007-10-02 22:45 | |
到达时间 | 2007-10-03 11:30 | |
出发机场 ID | 1 | |
到达机场 ID | 2 | |
飞机 ID | 302 | |
餐食供应商 ID | 9 | |
餐食数量 | 2 | |
出发登机口 | 74 | |
到达登机口 | B65 | |
登机时间 | 2007-10-02 22:10 | |
状态 ID | 1 |
飞行概念是此系统的核心,因此我们将有一个航班
表。这意味着我们必须确定一个主键,乍一看,航班号似乎是个好选择——但实际上并非如此,原因是航班号并未涂在飞机上;它仅是一种逻辑方式,用来表达飞机在两个机场之间的移动,以及与此移动相关的人员或公司。我们注意到,航班号保持简短——三或四位数字,以便在所有印刷品和机场信息屏幕上更好地参考;因此,航班号只有在伴随补充信息(如航空公司代码(AQ)或公司名称)和日期时才有意义。
考虑到与该航班表关联的其他表,我们在这里有两种选择来确定主键:
-
创建一个代理键(一个人工主键,其值不由其他表的数据派生)
-
使用列的组合——航空公司 ID、航班号、出发时间
创建一个代理键id
会更好。这个id
将仅通过一个列传播到相关表中,这有助于加快检索时间,因为只需在表之间比较一个字段。使用航班的id
也将简化查询的编写。当然,我们在flight
表中包含航班number
——公众所知的信息,但不是作为主键。
表:reservation | 列名 | 示例值 |
---|---|---|
*flight_id | 34 | |
*passenger_id | 1302 | |
web_site_quick_reference | KARTYU | |
ticket_number | 014 88417654 | |
ticket_issued_moment | 2007-01-01 12:00 | |
booking_agency_id | 1 | |
ticket_refundability_id | 1 | |
ticket_type_id | 1 | |
seat | 19A | |
section_id | 2 |
在航班表中包含诸如passenger1, passenger2
或seat_1a, seat_1b
之类的列将是一个错误。这就是为什么我们使用reservation
表来保存与特定航班相关的乘客信息。此表也可以命名为flight_passenger
。
通常我们不需要在reservation
表中包含section_id
,因为我们可以通过seat_id
引用它,但seat_id
可能在预订时未知,因此座位分配可以延迟到登机牌发放时。
复合键表
这些表具有多个键,因为某些键段指向代码或主题表。
表:plane_brand_model | 列名 | 示例值 |
---|---|---|
*brand_id | 1 | |
*id | 2 | |
description | APM-300 |
在这里,brand_id
和唯一的id
构成了飞机型号的主键。我们想知道此型号指的是哪个品牌,并且仍然使用整数作为键,而不是使用 APM-300 作为键值。
表:plane_section | 列名 | 示例值 |
---|---|---|
*airline_id | 1 | |
*id | 1 | |
description | 经济舱 |
每家航空公司都可能以自己的方式描述飞机的舱段——有些使用hospitality而非经济舱。
表:airline_brand_model_restriction | 列名 | 示例值 |
---|---|---|
*airline_id | 1 | |
*brand_id | 1 | |
*model_id | 2 | |
max_number_in_cabin_bags | 1 | |
max_number_registered_bags | 2 | |
max_weight_registered_bags_kg | 50 | |
表:plane_section_seat | 列名 | 示例值 |
--- | --- | --- |
*airline_id | 1 | |
*brand_id | 1 | |
*model_id | 2 | |
*section_id | 1 | |
*seat | 19A |
plane_section_seat
表描述了特定飞机舱段中的座位位置。这是针对每家航空公司、品牌、型号和舱段的,因为不同的航空公司可能拥有相同类型的飞机,但使用不同的座位编号,或者商务舱比其他航空公司更大。此外,在某些情况下,可能存在座位 1A 和 1C,但 1B 可能不存在。因此,我们需要此表来保存所有现有座位的完整列表。
表:flight_crew | 列名 | 示例值 |
---|---|---|
*flight_id | 34 | |
*crew_id | 9 |
通过这些示例值,我们可以推断出 Dan Murray 是 Air-Quebec 的 456 航班的飞行员。此表中的另一个可能列是该机组人员对该航班的状态:准时到达、取消或替换。
表:plane_event | 列名 | 示例值 |
---|---|---|
*plane_id | 302 | |
*event_id | 1 | |
*start_moment | 2008-10-01 | |
end_moment | 2008-10-31 | |
表:reservation_registered_bags | 列名 | 示例值 |
--- | --- | --- |
*flight_id | 34 | |
*passenger_id | 1302 | |
*tag | AQ636-84763 |
此处还可以添加用于标签追踪的其他列。
航空公司系统数据模式
再次使用 phpMyAdmin 的 PDF 模式功能来显示表之间的关系和涉及的键。
示例查询
作为表格列表和数据库模式的补充,让我们看看我们的表格是如何运作的!我们将向表格中输入示例值,然后构建一些 SQL 查询以提取所需数据。
插入示例值
我们使用了上述表格列表中描述的示例值。请访问本书的支持网站(www.packtpub.com/support
)下载包含表格定义和示例值的代码。
登机牌
乘客可以在家中通过网站的快速参考为其预订打印登机牌,在我们的例子中,该参考是KARTYU
。以下是检索登机牌信息的生成查询:
select passenger.last_name,
passenger.first_name,
flight.number,
airline.description,
flight.departure_moment,
flight.departure_gate,
flight.boarding_moment,
reservation.seat,
plane_section.description
from reservation
inner join passenger on reservation.passenger_id = passenger.id
inner join flight on reservation.flight_id = flight.id
inner join airline on flight.airline_id = airline.id
inner join plane_section on (airline.id = plane_section.airline_id
and reservation.section_id = plane_section.id)
where reservation.web_site_quick_reference = 'KARTYU'
执行此查询会检索到以下结果:
乘客名单
在这里,航空公司需要一份特定航班的乘客名单;我们使用flight_id
,即flight
表的主键,而不是航班号,因为航班号并不唯一。
select
reservation.seat,
passenger.last_name,
passenger.first_name,
passenger.passport_info,
airline.description,
flight.number
from reservation
inner join passenger on reservation.passenger_id = passenger.id
inner join flight on reservation.flight_id = flight.id
inner join airline on flight.airline_id = airline.id
where reservation.flight_id = 34
order by reservation.seat
目前,这个航班不是很受欢迎,看起来彼得和安妮将能够一起聊天:
航班上的所有人员
在极不可能发生的飞机失事情况下,我们可能需要快速提取航班上所有人员的名单。以下查询正是为此目的:
select
passenger.last_name as 'last name',
passenger.first_name as 'first name',
'passenger' as 'type',
airline.description,
flight.number
from reservation
inner join passenger on reservation.passenger_id = passenger.id
inner join flight on reservation.flight_id = flight.id
inner join airline on flight.airline_id = airline.id
where reservation.flight_id = 34
union
select
crew.last_name as 'last name',
crew.first_name as 'first name',
'crew' as 'type',
airline.description,
flight.number
from flight_crew
inner join crew on flight_crew.crew_id = crew.id
inner join flight on flight_crew.flight_id = flight.id
inner join airline on flight.airline_id = airline.id
where flight_crew.flight_id = 34
order by 'last name', 'first name'
结果按姓氏和名字排序;注意“类型”列,它指示此人是乘客还是机组人员。
总结
通过对航空公司系统的一些文档进行研究,我们列出了可能的数据元素,这些元素被分组为列并归入表中。我们仔细为每个表选择了主键或键,并建立了这些表之间的关系,确保所有潜在的数据元素至少包含在一个表中。