锋利的SQL(第2版)
上QQ阅读APP看书,第一时间看更新

1 基础篇

第1章 SQL简介

SQL的全称是结构化查询语言(Structured Query Language),是一种在关系数据库管理系统(Relational Database Management Systems,RDBMS)中查询和管理数据的标准语言。这是一种非常易读和易用的语言,只要稍微有一点英语基础,一些简单的数据查询、操作语句几乎都可以理解。但是,要想精通SQL,并不是一件很容易的事情,一是因为在数据处理方面有许许多多的个案,要处理好这些个案,除了具有缜密的逻辑思维,还需要多练习和实践;二是与其他编程语言相比, SQL是一种描述性语言,你只要说出想要什么结果,中间过程由数据库引擎去处理,其他编程语言则需要通过循环语句、判断语句等自己进行处理,如果出现什么问题,可以很快找到导致错误的语句,而对于SQL你就需要对寥寥几行的查询语句深入思考——数据库引擎为什么没有理解我的要求?所以说,数据库引擎的这种“封箱”操作给查找问题带来了麻烦,通常的解决方法就是将一个复杂语句拆解为几个简单语句,逐步验证中间结果的正确性,然后再重新组合为一条语句。从实际工作经验看,我不建议你写很复杂的语句来证明自己的高深,简单明了的语句对于后期的系统维护很有帮助,除非你的语句确实影响了处理效率。从本章开始,打好坚实的基础,并在此基础上不断提升自己的理论知识体系,当感受某个成功喜悦的时候,或许就会发现自己已经站在了SQL的巅峰。

本章我们将讲述一些最基本的SQL内容,如SQL的历史起源、ANSI是什么和一些基本的数据库理论,以及SQL的语法元素和执行SQL的工具等。基本知识学习起来可能比较枯燥。但是,如果你是初学SQL,这些基本知识对于学习好本书的后续内容却起着至关重要的作用。我也曾见过许多资深的数据库管理员会犯一些低级错误,一上午在不停地分析语句,最后却发现仅仅是“三值”逻辑问题。

1.1 SQL的历史起源

在20世纪60年代,网状数据库系统(如CODASYL)和分层数据库系统(如IMS TM)是用于自动化银行业务、记账和订单处理系统的一流技术,这些系统是由于商业大型计算机的引入才启用的。而SQL是在20世纪70年代创建的一种基于关系数据库管理系统模型的数据查询、操作语言。

1.1.1 CODASYL

CODASYL是美国数据系统语言协会(Conference on Data System Language)的英文缩写,该协会成立于1957年,主要目的是开发一种用于创建商业应用的通用语言。1959年5月28日,该协会召开了首次会议,就语言开发进行讨论。这个语言实际上就是Cobol语言。

1963年6月10日,美国加利福尼亚州的系统开发公司(System Development Corporation)举办了一个题为“基于计算机的数据库开发和管理”(Development and Management of a Computer-centered Data Base)的研讨会,首次提出并定义了数据库(Database)术语,即一组文件(表)的集合,其中文件是数据项(行)的有序集合,而每个数据项由数据以及一个或多个键组成。

1965年,CODASYL成立了“列表处理任务组”(List Processing Task Force),后更名为“数据库任务组”(Data Base Task Group)。1971年4月,任务组发布了一份重要的报告,报告概述了网状数据模型,被称为CODASYL或DBTG(即Data Base Task Group的缩写)数据模型。这个模型定义了数据库的几个关键概念,包括定义模式的语法、定义子模式的语法和数据操作语言。

1.1.2 IMS

IMS是信息管理系统(Information Management System)的英文缩写,是IBM公司的产品,这是一款分层数据库管理和事务处理系统。

IMS最初的开发目的是支持美国的阿波罗太空计划。1966年,IBM公司的12名成员、美国洛克维尔(American Rockwell)公司的10名成员和卡特彼勒公司(Caterpillar Tractor)的3名成员被集合起来,开始开发信息控制系统(Information Control System,ICS)和数据语言/接口(Data Language/Interface,DL/I),用于辅助跟踪建造太空船所需要的材料。其中,ICS部分是用于存储和获取数据的数据库部分,而DL/I部分则是用来与之交互的查询语言。

在开发过程中,IBM小组转移到加利福尼亚州南部的洛杉矶,并增加至21名开发人员。1967年,IBM团队完成了ICS的第一个版本。1968年4月开始进行安装测试。1968年8月14日,第一个安装成功,信息显示在美国国家航空航天局洛克维尔部门的IBM 2740打印机终端上。

1969年,ICS被更名为信息管理系统,即IMS。自第一个版本问世以来,IBM一直在不断开发和完善IMS的功能。2007年,IBM推出了IMS 10版本。IMS 10具备了增强的XML和网络服务功能,并且也是第一个将标准XML查询语言应用于层次结构化数据的数据库系统。

1.1.3 RDBMS和SQL

无论是CODASYL,还是IMS,虽然这些系统为早期系统提供了良好的基础,但它们的基本体系结构是将数据的物理操作与逻辑操作混合在一起。当数据的物理位置改变时,也必须更新应用程序来引用新的位置,给数据查询带来了不便。

SQL是一种关系型数据库查询语言,要介绍SQL的起源,就不得不介绍IBM公司的两个重量级人物——E. F. Codd博士和Don Chamberlin博士。E. F. Codd博士最早提出了关系数据库管理系统(Relational Database Management System,RDBMS)模型,而Don Chamberlin博士则是SQL和XQuery语言的主要创造者之一。他们对数据库的变革起到了革命性的作用。

Don Chamberlin最初在IBM的T. J. Watson研究中心(http://www.watson.ibm.com/)工作,当时该中心的主要研究方向是操作系统。Chamberlin一开始从事的项目是System A,但项目很快便以失败而告终。当时担任项目经理的Leonard Liu(现为Augmentum公司CEO,http://www.augmentum.com)很有远见地预见到数据库的美好前景,他转变了整个小组的研究方向。Chamberlin因此在数据库软件和查询语言方面进行了大量研究,并成为了小组中最好的网状数据库CODASYL专家。

此时的E. F. Codd博士在IBM的San Jose研究中心(即现在的Almaden研究中心, http://www.almaden.ibm.com/)工作。1969年,Codd在IBM的研究报告“Derivability, Redundancy, and Consistency of Relations Stored in Large Data Banks”(大型数据库中关系模型存储的可导、冗余和一致性)中第一次提出了关系模型,修订版本是Codd于1970年在一篇名为“A Relational Model of Data for Large Shared Data Banks”(大型共享数据库的数据关系模型)的文章中提出的,发表在《美国计算机学会通讯》杂志上。这是一篇里程碑性的论文,它确立了关系数据库的概念。但是,由于IBM正在从事IMS的开发,这种思想对IBM本身产品造成了威胁,所以公司内部最初持压制态度。当然这也与Codd采用了大量的数学方法、不容易理解有关。

1973年,IBM在外部竞争的压力下,开始加强在关系数据库方面的投入。Chamberlin被调到San Jose研究中心,加入新成立的项目System R。System R基于Codd提出的关系数据库管理系统模型。

System R项目包括研究高层的关系数据系统(Relational Data System,RDS)和研究底层的存储系统(Research Storage System,RSS)两个小组,Chamberlin担任RDS组的经理。RDS实际上就是一个数据库语言编译器,由于Codd提出的关系代数和关系演算过于数学化,影响了易用性。于是Chamberlin选择了自然语言作为研究方向,其结果就是诞生了结构化英语查询语言(Structured English Query Language,SEQUEL)。后来,由于商标之争,SEQUEL更名为SQL。

System R是一个具有开创性意义的项目。它第一次实现了结构化查询语言,并已成为标准的关系数据查询语言。同时,它也是第一个证明了关系数据库管理系统可以提供良好事务处理性能的系统。System R系统中的设计决策,以及一些基本算法选择(如查询优化中的动态编程算法)对以后的关系系统都产生了积极影响。

System R本身作为原型虽然并未问世,但鉴于其影响,计算机协会(Association for Computing Machinery,ACM,http://www.acm.org/)还是把1988年的“软件系统奖”授予了System R开发小组。

1.1.4 ANSI和SQL方言

ANSI是美国国家标准学会(American National Standards Institute)的英文缩写,该学会成立于1918年。当时,美国的许多企业和专业技术团体,已开始了标准化工作,但因彼此间没有协调,存在不少矛盾和问题。为了进一步提高效率,数百个科技学会、协会组织和团体,均认为有必要成立一个专门的标准化机构,并制定统一的通用标准。1918年,美国材料试验协会(ASTM)、美国机械工程师协会(ASME)、美国矿业与冶金工程师协会(ASMME)、美国土木工程师协会(ASCE)和美国电气工程师学会(AIEE)等组织,共同成立了美国工程标准委员会(AESC)。美国政府的商务部、陆军部和海军部也参与了该委员会的筹备工作。1928年,美国工程标准委员会改组为美国标准协会(ASA)。为致力于国际标准化事业和消费品方面的标准化,1966年8月,又改组为美利坚合众国标准学会(USASI)。1969年10月6日改成现名:美国国家标准学会(ANSI)。

虽然IBM首创了关系数据库理论,但Oracle却是第一家在市场上推出了这套技术的公司。随着时间的推移,SQL的简洁、直观,在市场上获得了不错的反响,从而引起了ANSI的关注,分别在1986年、1989年、1992年、1999年及2003年发布了SQL标准。SQL Server 2000遵循ANSI SQL:1992标准,而SQL Server 2005和2008还实现了ANSI SQL:1999和ANSI SQL:2003中的一些重要特性。

数据库生产商在遵循ANSI标准的同时,也会根据自己产品的特点对SQL进行了一些改进和增强,于是也就有了SQL Server的Transact-SQL、Oracle的PL/SQL等语言,我们称之为SQL方言。在本书中,我们将以Transact-SQL为基础进行SQL语言的介绍。实际上,在学习过程中,大家也没有必要刻意关心哪些语句或关键字是SQL标准,哪些是Transact-SQL的扩展。其实常见的数据库操作,在绝大多数支持SQL语言的数据库中差别并不大,所以数据库开发人员在跨越不同的数据库产品时,一般不会遇到什么障碍。但是对于数据库管理员来说,则需要面对很多挑战,不同数据库产品在管理、维护和性能调整方面区别很大。

1.2 SQL的理论基础

E. F. Codd博士提出的关系数据库管理系统模型基于两个数学分支:集合理论和谓词逻辑。理解集合理论,对于构建数据库结构,减少数据冗余,保证数据一致性方面具有重要意义。所以,即使你是一个资深的DBA,也建议你重温本节内容。

1.2.1 集合理论

集合理论(集合论)是由数学家Georg Cantor创建的,这是一个基于关系模型的数学分支。Cantor的集合定义如下:

任意集合体M是我们感知或想到的,能够确定的互异对象m(称之为M的元素)的整体。

——Joseph W. Dauben和Georg Cantor(普林斯顿大学出版社,1990)

看似简短的一句话,但是定义中的每个字都有着深刻和重要的意义。一个“集合”应将其视为单个实体,你的焦点应该放在对象的集合上,而不是组成集合的单个对象上。例如,当你对数据库中的雇员表编写查询时,应该将雇员的集合看作一个整体,而不是单个的雇员。这听起来可能并不重要并且很简单,但是很多程序员没有这种思维方式。

“互异”的含义是指集合中的每个元素必须是唯一的。对于数据库中的表,我们可以通过定义键约束来强制表中行的唯一性。没有键的话,就不能唯一地标识行,这样的表也就不能称之为“集合”,只能将其看作一个多重集合或一个无序的单位组。

“对象”可以是汽车或雇员这样的物理对象,也可以是抽象对象,如质数或线条。

“我们感知或想到的”这句话意味着集合的定义是主观的。例如,在教室中,“人”可以被认定为是“学生”的集合,也可以是“教师”的集合。因此,在定义集合方面你具有很大的自由度。在为数据库设计数据模型时,设计过程应仔细考虑应用程序的主观需求,从而为相关实体确定恰当的定义。

需要注意的是,Cantor的集合定义省略掉的内容同样重要。定义中没有提到集合元素间的任何顺序,集合元素的列出顺序并不重要。因为与顺序无关,{b, a, c}与{b, c, a}表示的是同一集合。对于属性(SQL中称之为列)集合,它们组成了关系(SQL中称之为表)的表头,元素应该是按名称标识,而不是按顺序位置标识。

同样,对于元组(SQL中称之为行)而言,它们构成了关系的主体,元素由其键值进行标识,而不是按位置标识。许多程序员很难适应这种观念,对于查询表而言,行之间没有顺序。换句话说,对表的查询可以按任意顺序返回表中的行,除非你基于特定展现目的,明确要求数据以特定的方式进行排序。

1.2.2 谓词逻辑

谓词逻辑的渊源可追溯到古希腊,是基于关系模型的另一个数学分支。谓词是一个属性或是一个表示“持有”或“不持有”的表达式,换句话说,也就是“真”或“假”。关系模型就是依靠谓词来维护数据的逻辑完整性并定义其结构的。例如,在一个名为Employees的表中定义了约束,仅允许工资(Salary)大于0的雇员存储在表中。谓词是“工资大于0”(T-SQL表达式:salary > 0)。

当通过筛选数据来界定子集等操作时,也可以使用谓词。例如,如果需要查询Employees表并要求只返回销售部门的雇员行时,可以在查询筛选器中使用谓词“部门(Department)等于销售(Sales)”(SQL表达式:department = 'sales')。

在集合理论中,可以使用谓词定义集合,这是非常有用的,因为不能总是通过列出所有元素来定义集合(例如,无限集合),通过特征定义集合往往更加方便简洁。例如,这是使用谓词定义无限集合的例子:所有质数集合的谓词定义是——x是一个仅能被1和自身整除的大于1的正整数。对于任意指定值,谓词可能为“真”,也可能为“假”,而全部质数的集合是谓词为“真”的所有元素的集合。例如,这是一个使用谓词定义有限集合的例子,集合{0,1,2,3,4,5,6,7, 8,9}可以被定义为下面谓词为“真”的所有元素的集合——x是一个大于或等于0且小于或等于9的整数。

1.2.3 关系模型

前面提过,关系模型是基于集合理论和谓词逻辑进行数据管理和操作的语义模型,它涉及命题、谓词、关系、元组和属性等概念。关系模型的目标是确保数据的一致性表示,最小化或是没有冗余且不牺牲完整性,并将定义数据的完整性(强制数据一致性)作为模型的一部分。RDBMS应实施关系模型并提供方法来存储、管理、实施完整性和查询数据。

1.命题、谓词和关系

很多人认为“关系”是指表之间的关系,这是不正确的。在数学的集合理论中,关系是集合的表现形式。在关系模型中,关系是相关信息的集合,与SQL中相对应的就是一个表——尽管不能完全对应。关系模型中的一个关键点是,单个的关系应代表一个单个集合(如客户)。需要注意的是,对多个关系操作(例如,两个关系之间的联接)的结果也是一个关系。

关系是由表头和主体组成的。表头包含一组属性(SQL中称之为列),其中每个元素由一个属性名称和类型名称标识。主体包含一个元组(SQL中称之为行)集合,其中每个元素都由键标识。为了简单起见,将表称之为行集。

当为数据库设计数据模型时,要使用关系(表)来表示所有数据。首先要确定需要在数据库中表示的命题。命题应当是一个断定或必须为“真”或“假”的语句。例如,语句“雇员A出生于1971年2月12日,在IT部门工作”是一个命题,如果这是一个真命题,它将表现为Employees (雇员)表中的一个行,如果是假命题,则根本不会出现。这个假设被称为“封闭世界假设”(close world assumption,CWA)。

下一步是将命题形式化,即取出实际数据(关系的主体)并定义结构(关系的表头)。可以以参数化命题方式思考谓词,关系的表头包含一个属性集合。属性由属性名称和类型名称进行标识,例如,一个Employees(雇员)关系的表头可能包含下列属性(属性名称和类型名称以成对方式出现):employeeid整型、firstname字符串型、lastname字符串型、birthdate日期型、departmentid整型。

类型是关系最基本的构造块,它约束了属性是一个可能或有效值的确定集合。例如,INT类型是范围自-2147483648至2147483647所有整数的集合。类型是数据库中谓词的最简单形式之一,因为它限制了该属性的允许值。例如,数据库不会接受一个雇员的出生日期为1971年2月31日的命题(更不用提像“abc”这样的生日)。注意,类型不受像整型或字符串型等基本类型的限制,它也可以是可能值的枚举,例如枚举可能的工作岗位。

2.缺失值

关系模型的一个方面是对于“是否谓词应限制于二值逻辑”的激情辩论。也就是说,在二值谓词逻辑中谓词是“真”或“假”。如果谓词不是“真”,那肯定是“假”。不过,也有人说具有三值(甚至四值)谓词逻辑空间,例如,像账号缺失这种情况。对于谓词,涉及缺失值就会出现既不是“真”也不是“假”的问题——因为它是未知的。例如,一个Employees(雇员)关系的手机属性,假设某些雇员的手机号码丢失了,怎么把这一事实表示在数据库中?这要使用三值逻辑实现,手机属性应当允许有一个代表缺失值的特定标记。然后,在这种缺失值情况下,将该手机属性与一些指定数字比较,谓词也将产生未知。三值谓词逻辑是指来自谓词结果的3种可能逻辑值——“真”“假”和“未知”(true、false和unknown)。

SQL通过支持NULL标记实现了三值谓词逻辑,以表示缺失值的通用概念。SQL中对NULL和三值谓词逻辑的支持是造成极大混乱和复杂性的根源,一些资深的DBA在对故障查询语句进行分析时,费尽周折却发现是数据中存在NULL值导致的。当然,最好的方法是使用二值谓词逻辑,这是不会造成问题的,但是在实际工作中,你会发现这很难。尤其是对一些现有数据系统改造时,数据质量很差,你只能是尽可能地规范这些数据,而无法将其完全规范到二值逻辑中。

3.约束

关系模型的最大好处是能够把定义数据的完整性作为模型的一部分。数据完整性通过在数据模型中定义的约束规则实现,并由RDBMS实施。实施完整性的最简单方式是指定属性类型的nullability(是否支持或不支持NULL标记)标志。约束也可以通过模型自身实施,例如,限定关系Orders(orderid, orderdate, duedate, shipdate)中每笔订单有3个互异日期,关系Employees(empid)与Employee Children(empid, childname)允许每个雇员有0或多个子集。

约束的其他例子还包括提供实体完整性的候选键和提供引用完整性的外键。候选键是定义了一个或多个属性的键,防止关系中出现多个相同的元组(SQL中的行),基于候选键的谓词可以唯一地标识行(如雇员)。你可以在关系中定义多个候选键,例如,在Employees关系中,可以在employeeid、SSN(社会安全号码)等属性上定义候选键。通常,可以任意选择一个候选键作为主键(例如,Employees关系中的employeeid),并作为标识行的首选方式。其他的所有候选键称为备用键。

外键用于强制引用完整性。外键定义了关系的一个或多个属性(称为引用关系)引用另一个关系(或同一关系)的候选键,此约束限定了引用关系的外键属性中的值,应是出现在被引用关系(父表)的候选键属性中的值。例如,假设Employees关系具有一个定义在departmentid属性上的外键,它引用Departments关系中的主键属性departmentid,这意味着Employees.departmentid中的值会出现在Departments.departmentid中的值。

4.规范化

关系模型还定义了规范化规则(也称为范式),用于确保每个实体都由单一关系表示。在规范化的数据库中,要在数据修改过程中避免异常,并在不牺牲完整性的情况下保持最低限度冗余。下面简要介绍一下由Codd提出的前3个范式(1NF、2NF和3NF)。

(1)1NF

第一范式是说关系(表)中的元组(行)必须是唯一的,并且属性是原子化的。也就是说,如果表正确地表示了关系,它已经符合了第一范式。

通过为表定义一个唯一键就可以实现唯一行。

你只能执行属性类型所定义的操作,我们知道,属性的原子性是主观的,这与集合的定义是主观的一样。例如,Employees关系中雇员姓名应当使用1个(fullname)、2个(firstname和lastname)还是3个(firstname、middlename和lastname)属性表示呢?答案取决于应用程序。如果应用程序需要分别处理雇员的姓名部分(如处于搜索目的),则有必要把它们分开,否则,则不需要。

同样,基于应用程序的需求,属性可能不被完全原子化,也有可能被亚原子化。例如,如果地址属性被作为一个特定应用程序的原子,不将“城市”作为地址的一部分会违反第一范式。

(2)2NF

第二范式涉及两个规则。一个规则是数据必须满足第一范式,另一个规则提及非键属性和候选键属性之间的关系。对于每个候选键,每个非键属性必须是对整个候选键的完全函数依赖。也就是说,如果要获取任何非键属性值,需要提供相同元组中候选键的所有属性的值;如果知道候选键的所有属性的值,就可以检索到任何元组的任何属性的任何值。

下面是一个违反第二范式的例子,假设定义了一个名为Orders的关系,表示订单和订单行的信息,如图1-1所示。Orders关系包含下列属性:orderid、productid、 orderdate、qty、customerid和companyname。主键定义为orderid和productid。

图1-1中违反了第二范式,因为有非键属性仅依赖于候选键(即该示例的主键)的一部分。例如,你可以仅通过orderid找到订单的orderdate,以及customerid和companyname。要符合第二范式,需要将原来的关系拆分为两个关系,即Orders和Order Details,如图1-2所示。Orders关系将包括orderid、orderdate、customerid和companyname属性,主键定义为orderid。Order Details关系将包括orderid、productid和qty,主键定义为orderid和productid。

图1-1 符合2NF之前的数据模型

图1-2 3NF之前符合2NF的数据模型

(3)3NF

第三范式也有两个规则。数据必须满足第二范式,同时,所有非键属性必须依赖于非传递的候选键。通俗地讲,该规则的意思是所有非键属性必须相互独立。换句话说,一个非键属性不能依赖于另一个非键属性。

先前所述的Orders和Order Details关系现在已经符合第二范式。请记住,此时的Orders关系包含orderid、orderdate、customerid和companyname属性,主键定义为orderid。customerid和companyname均依赖于主键——orderid。例如,你需要通过orderid主键来查找代表订单中客户的customerid,同样,你需要通过orderid主键查找订单中客户的公司名称(companyname)。然而, customerid和companyname也是互相依靠的。为满足第三范式,需要添加包含customerid(作为主键)和companyname的Customers关系,如图1-3所示,然后就可以从Orders关系中删除companyname属性。

图1-3 符合3NF后的数据模型

通俗地讲,2NF和3NF通常以这句话概括:“每个非键属性依赖于键,依赖于整个键,并且除了键别无他物”。

1.3 Transact-SQL语言的类型

在介绍了SQL的起源后,来看一下Transact-SQL包括哪些语言类型。首先,为了遵循ANSI SQL标准,Transact-SQL提供了数据定义语言(Data Definition Language,DDL)语句和数据操纵语言(Data Manipulation Language,DML)语句;其次,为了增强灵活性,Transact-SQL还提供了用于编程的流控制语句和其他语句。

对于语言类型,读者仅做大致了解就可以。在实际应用中,就像没必要区分哪些是SQL标准,哪些是SQL扩展一样,也没有必要区分DDL和DML,它们是一个协同工作的整体。

1.3.1 DDL语句

DDL语句用于创建数据库对象,如表、视图、索引等,表1-1中列出了一些常用的DDL语句。

表1-1 DDL常用语句

1.3.2 DML语句

DML语句用来检索和修改数据库的内容,表1-2中列出了一些常用的DML语句。

表1-2 DML常用语句

1.3.3 编程和流控制语句

许多人在学习编程时,经常询问编程工具的好坏,哪种语言好学,或是在开发上更具备优势。实际上,编程工具可能还有优劣之分,但是编程语言之间差别并不大。尤其是像SQL这样的数据处理语言,只要具备了顺序语句、判断语句和循环语句,就可以处理所有业务逻辑问题。表1-3中列出了一些常用的编程和流控制语句。

表1-3 DDL常用语句

下面将对一些常用的编程语句进行介绍。

1.IF...ELSE语句

当条件为“真”时,将执行IF关键字后面的语句,当条件为“假”时,将执行ELSE后面的语句。例如,下面示例的含义是:如果变量@i > 0,设置@My Var1 = 100,否则设置@My Var1 = 10,最后使用PRINT语句输出@My Var1的值。其中的“--”是注释字符。

      DECLARE @i AS int, @My Var1 AS int;  -- 声明变量,详细信息参考1.5节的介绍
      SET @My Var1 = 0;  -- 设置变量值
      SET @i = 1;

      IF @i > 0
          SET @My Var1 = 100;
      ELSE
          SET @My Var1 = 10;
      PRINT CONVERT(char(12),@My Var1); --输出@My Var1变量的值

2.BEGIN...END

BEGIN...END提供了执行一组SQL语句的方法,此语句对编写IF...ELSE和WHILE循环非常有用。现在将上面示例中IF...ELSE语句改写为下列形式:

      IF @i > 0
        SET @My Var1 = 100;
      ELSE
        BEGIN
            SET @My Var1 = 10;
            PRINT CONVERT(char(12),@My Var1); --输出@My Var1变量的值
        END

此时IF...ELSE语句的含义是:如果@i > 0,设置@My Var1 = 100,否则设置@My Var1 = 10,并使用PRINT语句输出@My Var1的值。也就是说,SET @My Var1 = 10和PRINT语句此时都属于ELSE部分。

3.WHILE循环

在WHILE关键字后面,可以编写一个控制循环执行的条件。在WHILE语句循环体的每一次执行前,都要测试条件。如果条件为真,则执行循环体;否则,将执行循环体后面的语句。

例如,下面的示例中使用了循环和判断语句。首先声明3个变量,@My Var1用于累加1~100之间的奇数值,@My Var2用于累加1~100之间的偶数值。

      DECLARE @i AS int, @My Var1 AS int, @My Var2 AS int;  -- 声明变量
      SET @My Var1 = 0;  -- 设置变量值
      SET @My Var2 = 0;
      SET @i = 1;

      WHILE @i < 100  -- 开始循环
      BEGIN  -- 指定包含在循环中的语句
          IF @i % 2 <> 0  -- 判断是否为奇数
            SET @My Var1 = @My Var1 + @i;  -- 累加奇数
          ELSE
            SET @My Var2 = @My Var2 + @i;  -- 累加偶数
          SET @i = @i + 1;  -- 递增@i
      END    -- 结束循环

      SELECT @My Var1, @My Var2, @i;  -- 显示变量值

4.GOTO语句和标签

GOTO语句用于将执行流更改到标签处,虽然Transact-SQL和PL/SQL都提供了该语句,但是作为编程而言,我们不推荐使用此编程技术。要编写一个标签,应当在标识符后面加一个冒号。例如,下面的示例使用GOTO语句代替WHILE循环,与上面的WHILE循环示例功能相同。

      DECLARE @i AS int, @My Var1 AS int, @My Var2 AS int;  -- 声明变量
      SET @My Var1 = 0;  -- 设置变量值
      SET @My Var2 = 0;
      SET @i = 1;

      table_loop:   -- 指定标签
          IF @i % 2 <> 0  -- 判断是否为奇数
            SET @My Var1 = @My Var1 + @i;  -- 累加奇数
          ELSE
            SET @My Var2 = @My Var2 + @i;  -- 累加偶数
          SET @i = @i + 1;  -- 递增@i
      IF (@i < 100) GOTO table_loop  --跳转到标签处
      SELECT @My Var1, @My Var2, @i;  -- 显示变量值

5.使用WAITFOR语句

WAITFOR语句用于延迟后面语句的执行,可以指定延迟的时间长度或是具体的时间。参考下面的语句:

      WAITFOR DELAY '00:1:15';
      PRINT N'到时间了';

      -- 另一种形式
      WAITFOR TIME '10:02:10';
      PRINT N'到时间了';

第一个WAITFOR语句使用DELAY关键字指定在1分15秒后执行后面的PRINT语句,最长延迟时间为24小时。第二个WAITFOR语句使用了TIME关键字,指定在10点2分10秒的时候执行后面的PRINT语句。

此语句通常用于并发测试,实际应用中并不多见。例如,可以指定在同一个时间执行多个相同的SQL语句。

1.3.4 SQL语句的批处理

应用程序可以将多个SQL语句作为一个批发送到服务器,然后服务器将该批中的语句编译成一个执行计划。在SQL Server的Management Studio工具,可以使用GO作为批发送的分隔符号。例如,在图1-4所示的语句中共包含3个批,选中后单击“执行”按钮一起执行。但是由于包含3个GO,所以会被分批发送,其中第1和第2个批都能正常执行,第3个批中由于最后一行INSERT语句的VALUSE关键字错了(正确的应当为VALUES),这个批在将这3个INSERT语句编译成一个执行计划时,将发生错误。因此,这3条INSERT语句都不会被执行,而不是仅仅发生错误的INSERT语句。从这个示例可以看出,理解批处理Transact-SQL语句的执行方式很重要。

图1-4 分批执行SQL语句

1.4 Transact-SQL语法

Transact-SQL具有一些大多数语句都使用或受之影响的元素,包括标识符、数据类型、函数、表达式、运算符和保留关键字等。

1.4.1 标识符

数据库对象的名称即为其标识符,如服务器、数据库和数据库对象(例如表、视图、列、索引、触发器、过程、约束及规则等)都可以有标识符。对象标识符是在定义对象时创建的,创建完成后便可以使用标识符引用该对象。例如,下列语句创建一个名为Table X的表,其中包含Key Col和Description列,则Table X、Key Col和Description都是标识符。

      CREATE TABLE Table X (Key Col INT PRIMARY KEY, Description nvarchar(80));

无论是Table X、还是Key Col和Description,这些中间无空格的字符,都称为常规标识符。常规标识符格式规则取决于数据库兼容级别(可以使用sp_dbcmptlevel存储过程设置该级别)。当兼容级别为90(SQL Server 2005)、100(SQL Server 2008)或110(SQL Server 2012)时,常规标示符使用下列规则。

第一个字符必须是下列字符之一。

Unicode标准3.2所定义的字母。Unicode中定义的字母包括拉丁字符a~z和A~Z,以及来自其他语言的字母字符。

下划线(_)、at符号(@)或者数字符号(#)。

在SQL Server中,某些位于标识符开头位置的符号具有特殊意义。以at符号开头的标识符表示局部变量或参数。以一个数字符号开头的标识符表示临时表或过程。以两个数字符号(##)开头的标识符表示全局临时对象。

某些Transact-SQL函数的名称以两个at符号(@@)开头。为了避免与这些函数混淆,不应使用以“@@”开头的名称。

后续字符可以包括以下几类。

如Unicode标准3.2中所定义的字母。

基本拉丁字符或其他国家/地区字符中的十进制数字。

at符号、美元符号($)、数字符号或下划线。

此外,常规标识符不能是Transact-SQL保留字,不允许嵌入空格或其他特殊字符,不允许使用Unicode标准之外的增补字符。

如果标示符中必须使用空格(如My Table)或其他不符合常规标示符规则的字符,则必须包含在双引号(")或者方括号([ ])内,否则SQL Server无法正确识别它们。双引号和方括号被称为分隔标示符。例如,下面语句中的My Table和order必须包含在分隔标识符内,因为My Table中间有空格,order是SQL Server用于ORDER BY子句的保留字。

      SELECT * FROM [My Table] WHERE [order] = 10;

常规标识符和分隔标识符包含的字符数必须在1~128。对于本地临时表,标识符最多可以有116个字符。

在使用双引号作为分隔符时,SQL Server遵从的规则受SET QUOTED_IDENTIFIER设置影响。设置为ON(默认值)时,双引号只能分隔标识符,文字必须由单引号分隔;设置为OFF时,标识符不能加引号,且必须符合所有常规标识符规则。

如果字符串中已经包含有单引号,则应在该单引号前再添加一个单引号。例如,下面的语句用于从My Table表中查找Last Name为O'Brien的行。

      SELECT * FROM "My Table"
      WHERE "Last Name" = 'O''Brien';

1.4.2 数据类型

大多数SQL语句并不显式引用数据类型,但是,由于语句中所引用对象的数据类型间的交互作用,语句的返回结果会受到影响。下列对象具有数据类型。

表和视图中的列。

存储过程中的参数。

变量。

返回一个或多个特定数据类型数据值的Transact-SQL函数。

具有返回代码(始终为integer数据类型)的存储过程。

为对象分配数据类型时可以为对象定义以下4个属性。

对象包含的数据种类。

所存储值的长度或大小。

数值的精度(仅适用于数字数据类型)。

数值的小数位数(仅适用于数字数据类型)。

1.基本类型

SQL Server所支持的数据类型大体可分为精确数字、近似数字、日期和时间、字符串、Unicode字符串、二进制字符串和其他数据类型7种类别,详细信息如表1-4所示。

表1-4 SQL Server的数据类型

续表

2.用户自定义数据类型

除了上面介绍的数据类型,还可以在SQL Server中创建三种用户自定义数据类型。

一种是从基本数据类型创建的别名数据类型,这样做的目的是为了更清楚地说明对象中值的类型。例如,下面的语句创建了一个基于datetime的birthday数据类型,用于在employee的emp_birthday列中存储生日数据。

      -- 创建一个允许null的birthday数据类型
      CREATE TYPE birthday
      FROM datetime NULL;
      GO
      -- 创建一个使用新数据类型的表
      CREATE TABLE employee
        (emp_id  char(5),  emp_first_name  char(30),  emp_last_name  char(40),  emp_birthday
      birthday);

另一种是CLR用户定义数据类型,它是在Microsoft .NET Framework公共语言运行时(CLR)使用编程方法创建的,这是从SQL Server 2005开始提供的一种新功能。此外,包括触发器、存储过程、函数、聚合函数,都可以利用CLR提供的丰富的编程模型来扩展SQL Server的功能。

最后一种是用户定义表数据类型,也就是说用户可以定义一个表示表结构的数据类型。这是从SQL Server 2008开始提供的一种新功能。下面的语句首先创建一个名为Location Table Type的表数据类型,然后创建一个基于该类型的变量,并向其中插入数据和查询数据。

      -- 创建一个表数据类型Location Table Type
      CREATE TYPE Location Table Type AS TABLE
      (
        First Name VARCHAR(50),
        Last Name VARCHAR(50)
      );
      GO

      -- 创建一个基于Location Table Type的变量
      DECLARE @My Table AS Location Table Type;
      -- 向变量中插入数据行
      INSERT INTO @My Table VALUES('Ken','Levy');
      INSERT INTO @My Table VALUES('Sara','Ford');
      -- 查询数据
      SELECT * FROM @My Table;
      GO

3.数据类型的隐式转换

当两个不同数据类型的表达式用运算符组合后,数据类型优先级规则指定将优先级较低的数据类型转换为优先级较高的数据类型。如果此转换不是所支持的隐式转换,则返回错误。当两个操作数表达式具有相同的数据类型时,运算的结果便为该数据类型。优先级顺序是:用户定义数据类型(最高)、sql_variant、xml、datetime、smalldatetime、float、real、decimal、money、smallmoney、bigint、int、smallint、tinyint、bit、ntext、text、image、timestamp、uniqueidentifier、nvarchar、nchar、varchar、char、varbinary、binary(最低)。

1.4.3 函数

与其他程序设计语言中的函数相似,SQL Server的函数可以有零个、一个或多个参数,并返回一个标量值或表格形式的值的集合。

1.4.4 表达式

表达式是标识符、值和运算符的组合,SQL Server可以对其求值以获取结果。访问或更改数据时,可在多个不同的位置使用数据。例如,可以将表达式用作要在查询中检索的数据的一部分,也可以用作查找满足一组条件的数据时的搜索条件。

表达式可以是下列任何一种形式:

常量

函数

列名

变量

子查询

CASE、NULLIF或COALESCE

1.4.5 运算符

运算符是表达式的组成部分之一,可以使用运算符执行算术、比较、串联或赋值操作。例如,表达式Price Column * 1.1中的乘号(*)使价格提高百分之十。

1.4.6 注释

注释是程序代码中不执行的文本字符串,也称为备注。注释可用于对代码进行说明或暂时禁用正在进行诊断的部分、SQL语句和批。使用注释对代码进行说明,便于将来对程序代码进行维护。

SQL Server支持两种类型的注释字符。

--(双连字符)。该注释字符可与要执行的代码处在同一行,也可另起一行。从双连字符开始到行尾的内容均为注释。对于多行注释,必须在每个注释行的前面使用双连字符。

/* ... */(正斜杠-星号字符对)。这些注释字符可与要执行的代码处在同一行,也可另起一行,甚至可以在可执行代码内部。开始注释对(/*)与结束注释对(*/)之间的所有内容均视为注释。对于多行注释,必须使用开始注释字符对(/*)来开始注释,并使用结束注释字符对(*/)来结束注释。批中的注释没有最大长度限制。

下面是一些有效注释的示例。

      USE Adventure Works2014;
      GO
      -- 这是我的单行注释

      SELECT Employee ID, Title
      FROM Human Resources.Employee;
      GO

      /* 这是多行注释的第1行,
        这是多行注释的第2行。*/
      SELECT Name, Product Number, Color
      FROM Production.Product;
      GO

      -- 在一个SQL语句中添加注释
      SELECT Contact ID, /* First Name, */ Last Name
      FROM Person.Contact;

      -- 在代码行后使用注释
      USE Adventure Works2014;
      GO
      UPDATE Production.Product
      SET List Price = List Price * .9;  -- 我在代码行使用注释
      GO

1.4.7 保留关键字

SQL Server保留某些关键字供自己专用。例如,在osql或SQL Server代码编辑器会话中使用DUMP关键字或BACKUP关键字,即表示让SQL Server备份全部或部分数据库或者备份日志。

除SQL Server定义的位置以外,在其他任何位置上,如果在SQL语句中使用保留关键字,均为非法。数据库中对象的名称不能与保留关键字相同。如果有这样的名称,则必须始终使用带分隔符的标识符来引用这个对象。尽管这个方法允许存在名称为保留关键字的对象,还是建议不要用与保留关键字相同的名称命名任何数据库对象。

1.5 常量和变量

常量和变量是程序设计过程中必不可少的元素,在前面的内容也涉及了对变量的简单介绍。

1.5.1 常量

常量,也称为文字值或标量值,是表示一个特定数据值的符号。常量的格式取决于它所表示的值的数据类型。

1.字符串常量

字符串常量包含在单引号内,可以由字母数字字符(a-z、A-Z和0-9)以及特殊字符(如!、@和#)组成。例如:

      '这是我的字符串常量'

如果单引号中的字符串包含一个嵌入的引号,可以使用两个单引号表示嵌入的单引号。对于嵌入在双引号中的字符串则没有必要这样做。例如,下面是“I'm Tom”常量的正确书写方式。

      'I''m Tom'

空字符串用中间没有任何字符的两个单引号表示。

2.Unicode字符串常量

Unicode字符串的格式与普通字符串相似,但它前面有一个N标识符(N代表SQL-92标准中的区域语言)。N前缀必须是大写字母。例如,'Michel'是字符串常量,而N'Michel'则是Unicode常量。

Unicode常量被解释为Unicode格式数据,并且不使用代码页进行计算。对于字符数据,存储Unicode数据时,每个字符使用2个字节,而不是每个字符1个字节。

3.二进制常量

二进制常量具有前辍0x,并且是十六进制数字字符串。这些常量不使用引号括起。例如,下面是二进制字符串的示例:

      0x AE
      0x12Ef
      0x69048AEFDD010E
      0x (空二进制常量)

4.bit常量

bit常量使用数字0或1表示,并且不括在引号中。如果使用一个大于1的数字,则该数字将转换为1。

5.datetime常量

datetime常量使用特定格式的字符日期值来表示,并被单引号括起来。SQL Server可识别下列格式中用单引号(')括起来的日期和时间。

字母日期,如'April 15, 1998'。

数值日期格式,如'4/15/1998'。

未分隔的字符串格式,如'19981207'指1998年12月7日。

下面分别是使用24和12小时制方式表示的时间常量:

      '14:30:24'
      '04:24 PM'

SQL Server所支持的所有时间格式,请参考1.7.4节的表1-16。

6.integer常量

integer常量以没有用引号括起来并且不包含小数点的数字字符串来表示。integer常量必须全部为数字,并且不能包含小数。例如:

      1894
      2

7.decimal常量

decimal常量由没有用引号括起来并且包含小数点的数字字符串来表示。例如:

      1894.1204
      2.0

8.float和real常量

float和real常量使用科学记数法来表示。例如:

      101.5E5
      0.5E-2

9.money常量

money常量是以可选的货币符号作为前缀为的一串数字。money常量可以包含小数点,但是不能使用引号括起来。例如:

      $12
      $542023.14

10.uniqueidentifier常量

uniqueidentifier常量是表示GUID的字符串。可以使用字符或二进制字符串格式指定。例如,下面的示例指定的是相同的GUID:

      '6F9619FF-8B86-D011-B42D-00C04FC964FF'
      0xff19966f868b11d0b42d00c04fc964ff

11.指定负数和正数

可以在数字前面添加“+”和“-”一元运算符来表示正数和负数。如果没有添加一元运算符,则默认为正数。例如:

      -- integer表达式
      -2147483648
      -- decimal表达式
      +145345234.2234
      -2147483648.10
      -- float表达式
      -12E5
      -- money表达式
      -$45.56

1.5.2 变量

变量对应内存中的一个存储空间。与常量不同,变量的值在运行过程中可以随时改变。

1.局部变量和全局变量

根据变量的作用范围不同,可以分为局部变量和全局变量。

局部变量是用户在程序中定义的变量,它仅在定义的程序范围内有效。局部变量可以用来保存从表中读取的数据,也可以作为临时变量保存计算的中间结果。在批处理和脚本中的变量通常用于:

作为计数器计算循环执行的次数或控制循环执行的次数;

保存数据值以供控制流语句测试;

保存存储过程返回代码要返回的数据值或函数返回值。

局部变量名称的第一个字符必须为一个@。

在SQL Server的早期版本中,如果变量以@@开头,则被称为全局变量。这些变量实际上是SQL Server的系统函数,它们的语法遵循函数的规则。用户可以在程序中使用这些函数测试系统特性和SQL命令的执行情况。

2.声明变量

变量只有在声明后才能使用,可以使用DECLARE语句来声明变量。在声明变量时可以指定变量的数据类型和长度。例如,下面的DECLARE语句使用int数据类型创建名为@My Counter的局部变量。

      DECLARE @My Counter int;

如果要声明多个局部变量,需要在定义的局部变量后使用一个逗号,然后指定下一个局部变量名称和数据类型。例如,下面的DECLARE语句创建三个局部变量,名称分别为@Last Name、@First Name和@Salary。

      DECLARE @Last Name nvarchar(30), @First Name nvarchar(20), @Salary decimal(7,2);

在使用变量时,需要注意变量的作用域。变量具有局部作用域,只在定义它们的批处理或过程中可见。作用域范围从声明变量的地方开始到声明变量的批处理或存储过程的结尾。例如,下面的脚本存在语法错误,因为在一个批处理中引用了在另一个批处理中声明的变量。

      USE Adventure Works2014;  -- 指定使用的数据库
      GO
      DECLARE @My Variable int;
      SET @My Variable = 1;
      GO -- 该语句将结束批
      -- @My Variable已经超出了范围并不再存在

      -- 下面的SELECT语句会发生语法错误,因为它引用了一个不存在的变量
      SELECT *
      FROM Human Resources.Employee
      WHERE Employee ID = @My Variable;

3.为变量设置值

在声明变量后,变量值被默认设置为NULL。要为变量赋值,可以使用SET或SELECT语句。其中,SET是为变量赋值的首选方法。

仍旧使用上面的示例,将声明的My Variable变量赋值为1,并在SELECT语句的WHERE子句中使用该变量。语句如下:

      USE Adventure Works2014;  -- 指定使用的数据库
      GO
      DECLARE @My Variable int;
      SET @My Variable = 1;  -- 设置变量值为1

      SELECT * FROM Human Resources.Employee
      WHERE Employee ID = @My Variable;
      GO

变量也可以通过选择列表中当前所引用的值赋值。如果在选择列表中引用变量,则它应当被赋以标量值或者SELECT语句应仅返回一行。例如:

      USE Adventure Works2014;  -- 指定使用的数据库
      GO
      DECLARE @Emp IDVariable int;

      SELECT @Emp IDVariable = MAX(Employee ID)
      FROM Human Resources.Employee;

      SELECT @Emp IDVariable;  --显示@Emp IDVariable的值
      GO

注意 如果在单个SELECT语句中有多个赋值子句,SQL Server并不保证表达式求值的顺序。只有当赋值之间有引用时才能看到影响。

如果SELECT语句返回多行而且变量引用一个非标量表达式,则变量被设置为结果集最后一行中表达式的返回值。例如,下面的语句将@Emp IDVariable设置为所返回的最后一行的Employee ID字段的值。

      USE Adventure Works2014;
      GO
      DECLARE @Emp IDVariable int;

      SELECT @Emp IDVariable = Employee ID
      FROM Human Resources.Employee
      ORDER BY Employee ID DESC;

      SELECT @Emp IDVariable;  --显示@Emp IDVariable的值
      GO

1.6 运算符

运算符是一种符号,用来指定要在一个或多个表达式中执行的操作。SQL Server所使用的运算符可以分为算术运算符、赋值运算符、位运算符、比较运算符、逻辑运算符、字符串串联运算符和一元运算符,共7个类别。

1.6.1 算术运算符

算术运算符可以对两个表达式执行数学运算,这两个表达式可以是数值数据类型类别的任何数据类型。可用算术运算符如表1-5所示。

表1-5 算术运算符

加(+)和减(-)运算符也可用于对datetime和smalldatetime型值执行算术运算。

在进行算术运算时,需要注意计算结果的精度、小数位数和数据类型长度的变化。所谓精度,是指数字中的数字个数。小数位数是数中小数点右边的数字个数。例如,数123.45的精度是5,小数位数是2。

numeric和decimal数据类型的默认最大精度为38,而早期的SQL Server版本中,默认的最大精度为28。

数字数据类型的长度是指存储此数所占用的字节数。字符串或Unicode数据类型的长度是字符个数。binary、varbinary和image数据类型的长度是字节数。例如,int数据类型可以有10位数,用4个字节存储,不接受小数点。int数据类型的精度是10,长度是4,小数位数是0。

除了decimal类型之外,数字数据类型的精度和小数位数是固定的。如果算术运算符有两个相同类型的表达式,结果就为该数据类型,并且具有对此类型定义的精度和小数位数。如果运算符有两个不同数字数据类型的表达式,则由数据类型优先级决定结果的数据类型。

表1-6列出了当运算结果是decimal类型时,如何计算结果的精度和小数位数。只有当下列任一条件成立时,结果才为decimal数据类型。

两个表达式都是decimal类型。

一个表达式是decimal类型,而另一个是比decimal优先级低的数据类型。

操作数表达式由表达式e1(精度为p1,小数位数为s1)和表达式e2(精度为p2,小数位数为s2)来表示。非decimal类型的任何表达式的精度和小数位数,采用该表达式的当前设置。

表1-6 decimal类型数据的精度和小数位数的计算方式

注意 结果精度和小数位数的绝对最大值为38。当结果精度大于38时,相应的小数位数会减少,以避免结果的整数部分被截断。

1.6.2 赋值运算符

等号(=)是唯一的赋值运算符。它通常与SET语句一起使用,为变量赋值。例如,下面将创建一个@My Counter变量,然后使用赋值运算符为其赋值。

        DECLARE @My Counter INT
        SET @My Counter = 1

也可以使用赋值运算符在列标题和定义列值的表达式之间建立关系。下面的语句将显示列标题First Column Heading和Second Column Heading。在所有行的First Column Heading列中均显示字符串“xyz”。然后,在Second Column Heading列中列出来自Product表的每个产品ID。

        USE Adventure Works
        GO
        SELECT First Column Heading = 'xyz',
              Second Column Heading = Product ID
        FROM Production.Product;
        GO

1.6.3 位运算符

位运算符在两个表达式之间执行按位运算,这两个表达式可以是整数数据类型类别中的任何数据类型。可用的位运算符如表1-7所示。

表1-7 位运算符

位运算符的操作数可以是整数或二进制字符串数据类型类别中的任何数据类型(image数据类型除外),但两个操作数不能同时是二进制字符串数据类型类别中的某种数据类型。表1-8列出了所支持的操作数数据类型。

表1-8 SQL Server支持的位运算的数据类型

对于按位进行与运算、或运算和异或运算的计算规则如表1-9所示。

表1-9 按位进行与运算、或运算和异或运算的计算规则

1.6.4 比较运算符

比较运算符测试两个表达式是否相同。除了text、ntext或image数据类型的表达式外,比较运算符可以用于所有的表达式。表1-10列出了可用的比较运算符。

表1-10 比较运算符

使用比较运算符的表达式的计算结果为布尔数据类型,它有3种值:TRUE、FALSE和UNKNOWN。与其他SQL Server数据类型不同,布尔数据类型不能被指定为表列或变量的数据类型,也不能在结果集中返回。

1.6.5 逻辑运算符

逻辑运算符对某些条件进行测试,以获得其真实情况。逻辑运算符和比较运算符一样,返回带有TRUE或FALSE值的布尔数据类型。表1-11列出了可用的逻辑运算符。

表1-11 逻辑运算符

1.6.6 字符串串联运算符

加号(+)是字符串串联运算符,可以用它将字符串串联起来。其他所有字符串操作都使用字符串函数(如SUBSTRING)进行处理。

例如,下面使用加号将“abc”和“efg”串联起来,得到的结果是“abcefg”。

      'abc'+'efg'

在进行字符传串联时,需要注意字符串长度的变化。

在将两个char、varchar、binary或varbinary表达式串联时,所生成表达式的长度是两个源表达式长度之和,或是8000字符,以二者中少者计算。

在将两个nchar或nvarchar表达式串联时,所生成表达式的长度是两个源表达式长度之和,或是4000字符,以二者中少者计算。

1.6.7 一元运算符

一元运算符只对一个表达式执行操作,该表达式可以是数字数据类型类别中的任何一种数据类型。表1-12列出了可用的一元运算符。

表1-12 一元运算符

+和-运算符可以用于数字数据类型类别中任意一数据类型的任意表达式。~运算符只能用于整数数据类型类别中任意一数据类型的表达式。

1.7 常用函数

为满足通常的程序设计需要,Transact-SQL语言提供了非常丰富的函数,包括聚合函数、配置函数、游标函数、日期和时间函数、数学函数等。本节将介绍一些常用的函数类型。

1.7.1 聚合函数

聚合函数对一组值执行计算并返回单个值。除了COUNT以外,聚合函数都会忽略空值。聚合函数经常与SELECT语句的GROUP BY子句一起使用。

表1-13 聚合函数及其功能

例如,下面的语句使用COUNT函数来获取Employee表中的员工数量。

      USE Adventure Works2014;  -- 指定使用的数据库
      SELECT COUNT(Employee ID)
      FROM Human Resources.Employee;
      GO

又如,下面的语句使用SUM函数计算Sales Order Detail表中的销售总额。

      USE Adventure Works2014;
      SELECT SUM(Line Total)
      FROM Sales.Sales Order Detail;
      GO

1.7.2 配置函数

配置函数用于返回当前配置选项的设置信息。

表1-14 配置函数及其功能

例如,下面的语句使用@@LANGUAGE函数返回当前会话的语言名称。

      SELECT @@LANGUAGE AS 语言名称;

1.7.3 游标函数

游标函数用于返回有关游标的信息。

表1-15 游标函数及其功能

下面的示例使用@@FETCH_STATUS函数和WHILE循环,逐步检索游标中的行,直至检索完毕。

      DECLARE Employee_Cursor CURSOR FOR
      SELECT Employee ID, Title FROM Adventure Works.Human Resources.Employee;  -- 建立游标
      OPEN Employee_Cursor;  -- 打开游标
      FETCH NEXT FROM Employee_Cursor;  -- 开始检索
      WHILE @@FETCH_STATUS = 0
        BEGIN
            FETCH NEXT FROM Employee_Cursor;  -- 检索下一行
        END
      CLOSE Employee_Cursor;  -- 关闭游标
      DEALLOCATE Employee_Cursor;  -- 删除游标引用
      GO

1.7.4 日期和时间函数

日期和时间函数用于对日期和时间输入值执行操作,并返回一个字符串、数字值或日期和时间值。

表1-16 日期和时间函数及其功能

续表

续表

1.7.5 数学函数

数学函数根据提供的输入值执行计算后返回一个数值。其中,算术函数(如ABS、CEILING、DEGREES、FLOOR、POWER、RADIANS和SIGN)返回与输入值相同数据类型的值。三角函数和其他函数(包括EXP、LOG、LOG10、SQUARE和SQRT)将输入值转换为float并返回float值。

表1-17 数学函数及其功能

续表

续表

1.7.6 数据类型转换函数

数据类型转换函数仅包括CAST和CONVERT函数,二者的功能类似。使用这两个函数可以显式地将一种数据类型的表达式转换为另一种数据类型的表达式。

下面分别是CAST和CONVERT函数的语法格式:

      CAST ( expression AS data_type [ (length ) ])
      CONVERT ( data_type [ ( length ) ] , expression [ , style ] )

expression是任何有效的表达式。data_type是目标数据类型(仅限于系统数据类型),包括xml、bigint和sql_variant。length是nchar、nvarchar、char、varchar、binary或varbinary数据类型的可选参数,对于CONVERT函数,如果未指定length,则默认为30个字符。

style用于以下三个方面。

指定在将datetime或smalldatetime数据转换为字符数据(nchar、nvarchar、char、varchar、nchar或nvarchar数据类型)时的日期格式的样式。

用于将float、real、money或smallmoney数据转换为字符数据时的字符串格式的样式。

用于指定二进制和字符型十六进制值之间数据转换时的格式(仅限于SQL Server 2008)。

表1-18列出了将datetime或smalldatetime数据转换为字符数据时的可用值。左侧的两列是style的值,将style值加100,将返回包括世纪数的四位年份格式。

表1-18 转换datetime或smalldatetime为字符数据时的style可用值

表1-19列出了在将float或real转换为字符数据时的style可用值。

表1-19 转换float或real为字符数据时的style可用值

表1-20列出了在将money或smallmoney转换为字符数据时的style可用值。

表1-20 转换money或smallmoney为字符数据时的style可用值

在将numeric或decimal数据转换为字符数据时,如果要删除结果集尾随的零,可以使用128作为style的值。

表1-21列出了在将字符串转换为xml数据时的style可用值。

表1-21 转换字符串为xml数据时的style可用值

下面的语句使用CAST函数将Production.Product表中的List Price列由money转换为varchar数据类型。

      USE Adventure Works2014;
      SELECT 'The list price is ' + CAST(List Price AS varchar(12)) AS List Price
      FROM Production.Product;
      GO

返回的结果类似下列形式:

      List Price
      --------------------------
      The list price is 8.09
      The list price is 108.00
      The list price is 7.16

使用CONVERT函数同样可以实现上面的功能,参考下面的语句。

      USE Adventure Works2014;
      SELECT 'The list price is ' + CONVERT(varchar(12), List Price) AS List Price
      FROM Production.Product;
      GO

表1-22列出了二进制和字符型十六进制值转换时的style可用值。

表1-22 二进制和字符型十六进制值转换时的style可用值

参考下面的示例代码:

    -- 转换二进制值0x4E616d65到一个字符值
    SELECT CONVERT(char(8), 0x4E616d65, 0) AS 'Style 0, 二进制到字符';
    -- 下面的示例演示了Style为1的情况下,如何强行截断结果值
    -- 产生的结果值由于包含字符0x ,所以被截断
    SELECT CONVERT(char(8), 0x4E616d65, 1) AS 'Style 1, 二进制到字符';
    -- 下面的示例演示了Style为2的情况下,没有截断结果值
    -- 这是因为0x字符未包含在结果中
    SELECT CONVERT(char(8), 0x4E616d65, 2) AS 'Style 2, 二进制到字符';
    -- 转换字符值Name到一个二进制值
    SELECT CONVERT(binary(8), 'Name', 0) AS 'Style 0, 字符到二进制';
    SELECT CONVERT(binary(4), '0x4E616D65', 1) AS 'Style 1, 字符到二进制';
    SELECT CONVERT(binary(4), '4E616D65', 2) AS 'Style 2, 字符到二进制';

结果如图1-5所示。

图1-5 转换结果

1.7.7 字符串函数

使用字符串函数可以对字符串输入值进行剪裁、替换等操作,并返回字符串或数值。字符串函数及其功能如表1-23所示。

表1-23 字符串函数及其功能

续表

续表

1.7.8 文本和图像函数

使用文本和图像函数可以对文本或图像输入值或列执行操作。文本和图像函数及其功能如表1-24所示。

表1-24 文本和图像函数及其功能

1.7.9 逻辑函数

从SQL server 2012开始,增加了IIF和CHOOSE两个逻辑函数。这两个函数都是非标准的,目的是为了支持更容易地从Microsoft Access数据库进行迁移。函数功能如表1-25所示。

表1-25 逻辑函数及其功能

1.8 查询工具

在本章的前面介绍了SQL的语法规则、常量和变量、运算符、函数等基本知识,到底通过什么工具来执行SQL语句呢?对于程序开发人员来而言,可以从客户端应用程序发送SQL语句到服务器端执行。此外,出于服务器管理和SQL测试等需要,SQL Server也提供了Management Studio、sqlcmd、bcp和sqlps工具,可以通过它们执行SQL语句。

其中,bcp用于大量行插入SQL Server表,但该工具不需要具有Transact-SQL知识。sqlps是一个Microsoft C#命令提示实用工具,用于以交互方式即席运行Power Shell命令或是运行Power Shell脚本文件。由于这两个工具的功能超出了本书的范围,我们仅对SQL Server Management Studio和sqlcmd做一下介绍。

1.8.1 Management Studio

在Windows中依次选择“开始”→“程序”→“Microsoft SQL Server 2014”,单击SQL Server 2014 Management Studio(在Vista中要以管理员身份运行),将打开登录窗口,选择身份验证方式后,将打开Management Studio。

单击工具栏中的“新建查询”按钮可以打开一个查询窗口,如图1-6所示。可以在查询窗口中输入SQL语句后,单击“执行”按钮执行查询。如果希望仅执行其中的部分语句,可以选定要执行的语句,然后再单击“执行”按钮。

图1-6 SQL Server Management Studio

1.8.2 sqlcmd

在Windows中依次选择“开始”→“程序”→“附件”→“命令提示符”(在Vista中要以管理员身份运行),将打开“命令提示符”窗口。

要连接到SQL Server服务器,必须指定服务器名称。安装在命名实例中的,还必须指定实例名。默认情况下,sqlcmd使用Windows身份验证。如果要使用SQL Server身份验证连接到SQL Server的,则还必须提供连接用户名和密码。例如,如果要连接到名为server1的服务器,则需要使用下列参数:

      sqlcmd -S server1 -U Sql User Account -P Sql Password

如果是受信任的Windows用户,则可以省略-U和-P参数。例如,图1-7所示的sqlcmd窗口中使用sqlcmd -S (local)命令连接到服务器,并从Human Resources.Employee表中查找Employee ID为1的雇员。注意其中的GO命令,该命令用于执行所输入的SQL语句。

图1-7 sqlcmd窗口

要退出sqlcmd,可以执行exit或quit命令。

1.9 SQL书写规范

书写规范与语法规范是两个完全不同的概念,违反语法规范会导致程序执行错误,而违反书写规范虽然不会导致错误,但是会导致阅读困难和代码的通用性。这些书写规范是根据大多数人阅读代码时的习惯而提出的,并不是必须完全遵守的。

1.9.1 大小写规范

1.在名称中仅使用字母、数字和下划线

之所以要在名称中仅使用字母、数字和下划线,因为这些字符可以被移植到任何其他编程语言中。在应用程序的数据库和宿主语言中能够使用相同的名称,会非常方便。

但是,也存在一些特殊情况。例如,在SQL Server中临时表名称需要以“#”开头,而它在其他编程语言中具有特殊含义。如果必须使用临时表,则只能使用“#”。此外,参数名称也存在这种情况,它需要以“@”开头。但是,无论怎样,在名称中尽量避免使用特殊符号是一个非常正确的选择。

不要将下划线作为名称的第一个或最后一个字母,因为这看上去像少了一部分一样。

2.列名、参数和变量等标量小写

通常情况下,小写单词比大写容易阅读。曾经做过测试,阅读小写文本的速度比大写的速度快5%~10%。当名称由两个单词组合而成时,为便于阅读,应当采用大小写混合的写法。例如,下面按由易至难的方式列出了存放修改日期列的三种书写方法:

      Modified Date     -- 比较容易阅读
      modifieddate     -- 阅读难度增加
      MODIFIEDDATE     -- 阅读最困难

但是,也有一种观点认为大小写混合的写法阅读起来比全部小写要难一些,原因是在全部小写的情况下,会把modifieddate看作一个单词,而Modified Date这种形式会被看作两个单词,分散注意力。总之,在列名、参数和变量中全部使用大写字母是一个非常糟糕的选择。

3.模式对象名首字母大写

模式对象包括表、视图和存储过程等,在创建这些名称时,应当将首字母大写,表示为专有名词。

4.保留关键字大写

保留关键字是Transact-SQL语言语法的一部分,用于定义、操作和访问数据库。将保留关键字大写后,会起到一种突出效果,使整个语句重点突出、结构清晰。看一下下面的语句:

      select a, b, c from My Table where id = 1;

对比一下:

      SELECT a, b, c FROM My Table WHERE id = 1;

阅读上面的两个语句,看一下能否快速找出每个子句,而下面的书写格式则阅读起来会更清晰。

      SELECT a, b, c
      FROM My Table
      WHERE id = 1;

下面列出了SQL Server的保留关键字:

      ADD | ALL | ALTER | AND | ANY | AS | ASC | AUTHORIZATION
      BACKUP | BEGIN | BETWEEN | BREAK | BROWSE | BULK | BY
      CASCADE | CASE | CHECK | CHECKPOINT | CLOSE | CLUSTERED | COALESCE
      COLLATE | COLUMN | COMMIT | COMPUTE | CONSTRAINT | CONTAINS
      CONTAINSTABLE | CONTINUE | CONVERT | CREATE | CROSS | CURRENT
      CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP | CURRENT_USER
      CURSOR
      DATABASE | DBCC | DEALLOCATE | DECLARE | DEFAULT | DELETE | DENY
      DESC | DISK | DISTINCT | DISTRIBUTED | DOUBLE | DROP | DUMP
      ELSE | END | ERRLVL | ESCAPE | EXCEPT | EXEC | EXECUTE | EXISTS
      EXIT | EXTERNAL
      FETCH | FILE | FILLFACTOR | FOR | FOREIGN | FREETEXT | FREETEXTTABLE
      FROM | FULL | FUNCTION
      GOTO | GRANT | GROUP
      HAVING | HOLDLOCK
      IDENTITY | IDENTITY_INSERT | IDENTITYCOL | IF | IN | INDEX | INNER | INSERT
      INTERSECT | INTO | IS
      JOIN
      KEY | KILL
      LEFT | LIKE | LINENO | LOAD
      MERGE |
      NATIONAL | NOCHECK | NONCLUSTERED | NOT | NULL | NULLIF
      OF | OFF | OFFSETS | ON | OPEN | OPENDATASOURCE | OPENQUERY | OPENROWSET
      OPENXML | OPTION | OR | ORDER | OUTER | OVER
      PERCENT | PIVOT | PLAN | PRECISION | PRIMARY | PRINT | PROC
      PROCEDURE | PUBLIC
      RAISERROR | READ | READTEXT | RECONFIGURE | REFERENCES | REPLICATION
      RESTORE | RESTRICT | RETURN | REVERT | REVOKE | RIGHT | ROLLBACK
      ROWCOUNT | ROWGUIDCOL | RULE
      SAVE | SCHEMA | SECURITYAUDIT | SELECT | SEMANTICKEYPHRASETABLE
      SEMANTICSIMILARITYDETAILSTABLE | SEMANTICSIMILARITYTABLE
      SESSION_USER | SET | SETUSER | SHUTDOWN | SOME | STATISTICS | SYSTEM_USER
      TABLE | TABLESAMPLE | TEXTSIZE | THEN | TO | TOP | TRAN | TRANSACTION
      TRIGGER | TRUNCATE | TRY_CONVERT | TSEQUAL
      UNION | UNIQUE | UNPIVOT | UPDATE | UPDATETEXT | USE | USER
      VALUES | VARYING | VIEW
      WAITFOR | WHEN | WHERE | WHILE | WITH | WITHIN GROUP | WRITETEXT

1.9.2 使用空格

在语言标记之间放置一个空格,尽量地符合英语书写习惯,可以增强语句的可阅读性。

1.等号两边使用空格

在书写赋值语句时,应当在等号两边使用空格分隔,如SET @i = 1比SET @i=1更容易阅读。

2.逗号后面使用空格

应当遵循在逗号后面使用空格的原则,因为英语中逗号和句号很容易混淆。例如:

      SELECT My Table.a,My Table1.b,My Table2.c
      FROM My Table,My Table1,My Table2;

下面的形式会更容易阅读一些:

      SELECT My Table.a, My Table1.b, My Table2.c
      FROM My Table, My Table1, My Table2;

当表或列名称比较长时,下面的形式则更好一些。

      SELECT Employee ID,
        Title,
        Birth Date,
        Marital Status
      FROM Human Resources.Employee;

1.9.3 使用缩进

必要的缩进会使语句的层次和逻辑关系更加清晰,通常是缩进2个空格。例如,在下面的语句中,AND关键词连接了两个筛选条件,缩进后会更加突出WHERE子句。

      SELECT *
      FROM Human Resources.Employee
      WHERE Manager ID = 16
        AND Employee ID > 100;

下面是一个左外连接的语句,首先将Human Resources.Employee和Person.Contact表中列分别放在了单独的行中,以便进行区分;然后LEFT缩进后表示与FROM后面的表进行连接,ON再次缩进表示是LEFT的连接条件。

      SELECT E.Employee ID, E.Title,
        P.First Name, P.Last Name, P.Email Address
      FROM Human Resources.Employee AS E
        LEFT OUTER JOIN Person.Contact AS P
          ON E.Employee ID = P.Contact ID
      WHERE E.Manager ID = 16
        AND E.Employee ID > 100;

1.9.4 使用垂直空白道

还有一种观点认为,在关键词与参数之间应当使用垂直空白道的方式进行分隔,会增强可阅读性。例如:

      SELECT E.Employee ID, E.Title,
            P.First Name, P.Last Name, P.Email Address
        FROM Human Resources.Employee AS E
        LEFT OUTER JOIN Person.Contact AS P
          ON E.Employee ID = P.Contact ID
       WHERE E.Manager ID = 16
        AND E.Employee ID > 100;

又如,下面的语句使用了垂直空白道分隔,并对子查询使用了缩进。

      SELECT DISTINCT Cust Name
        FROM Customers AS C
       WHERE NOT EXISTS
              (SELECT *
                FROM Order Header
              WHERE Cust ID = Customers.Cust ID);

1.9.5 使用分组

存在多行Transact-SQL的情况下,相关语句之间可以直接换行书写,而对于两个步骤之间的语句应当间隔一个空行。如果需要的话,也可以加入一些适当的注释语句。例如:

      USE Adventure Works2014;
      GO

      -- 读取 Employee 表的数据
      SELECT *
      FROM Human Resources.Employee;
      GO