操纵数据

关系型数据库最重要的对象就是表,对表数据的操作十分关键.其中包括有增、删、改、查四种操作.分别对应数据操纵语言的INSERTDELETEUPDATESELECT四个关键语句.下面我们来详细介绍它们.

插入数据

INSERT…INTO语句

INSERT…INTO是插入数据最基本的语句,一次只能插入一条数据,其语法如下:

INSERT INTO tb_or_view_name (column_list)
	VALUES (expression);
GO

插入时应该注意,expression中的常量里,数字可以直接写,字符串应该用单引号括起,Unicode字符前面+N等.

插入空值/默认值/省略某个列: 使用INSERT…INTO语句时,有一些列的数据是未知的,则可以插入空值/默认值,甚至不为其插入值,前提是:

该列设置有NULL属性,或该列设有DEFAULT属性,或该列可以自动获取数据(计算得来的列).

如下是一个例子:

USE tempdb;
GO

CREATE TABLE ##Temp1 (
	a INT NULL,
	b INT DEFAULT 0,
	aANDb AS a+b PERSISTED
)

INSERT INTO ##Temp1 (a,b) VALUES (5,15);
INSERT INTO ##Temp1 (a,b) VALUES (NULL,DEFAULT);	--以NULL和DEFAULT设置a,b的值;
INSERT INTO ##Temp1 (a,b) VALUES (1,DEFAULT);		--以DEFAULT设置b的值;
GO

SELECT * FROM ##Temp1;
GO

我们始终没有为aANDb列插入数值,也使用了NULLDEFAULT插入数值,结果如下:

注意两点:

  • 应在INSERT子句中指明插入列的集合,该集合的先后顺序不影响数据最终插入表中.
  • 应在VALUES子句中指明插入的值,其顺序和INSERT子句指定顺序一一对应.

INSERT…SELECT语句

为了解决INSERT…INTO语句一次只能插入一条数据的问题,我们引入INSERT…SELECT语句,其语法如下:

INSERT INTO tb_or_view_name
	SELECT statement;
GO

为了方便演示,我们再按照##Temp1的结构建立一个##Temp2:

USE tempdb;
GO

CREATE TABLE ##Temp2 (
	a INT NULL,
	b INT DEFAULT 0,
	aANDb AS a+b PERSISTED
)

INSERT INTO ##Temp2 (a,b) VALUES (NULL,DEFAULT);	--以NULL和DEFAULT设置a,b的值;
INSERT INTO ##Temp2 (a,b) VALUES (0,DEFAULT);		--以DEFAULT设置b的值;
GO

SELECT * FROM ##Temp2;
GO

脚本运行结束后##Temp2的值如下:

接下来我们就开始演示INSERT…SELECT语句:

USE tempdb;
GO

SELECT * FROM ##Temp1;		--先查看##Temp1;
GO

INSERT INTO ##Temp1 (a,b) 
	SELECT a,b FROM ##Temp1;	--从##Temp1取出数据并插入到Temp1;
GO

INSERT INTO ##Temp1 (a,b) 
	SELECT a,b FROM ##Temp2;	--从##Temp2取出数据并插入到Temp1;
GO

SELECT * FROM ##Temp1;		--查看改变之后的##Temp1;
GO

让我们先看看结果:

可以看到,将##Temp1中所有的行取出再插入##Temp1,相当于复制为两倍的行,此时##Temp1应有6行,而后又将##Temp2所有行取出插入##Temp1,则又新增了2行,于是最终##Temp1有8行数据.

从上面的例子中我们可以总结出INSERT…SELECT语句的要点 :

  • 使用SELECT子句时,既可以对要插入的表进行操作,也可以对别的表进行操作.
  • 要插入的表必须已经存在.
  • SELECT得到的数据必须和要插入表的数据相兼容.

SELECT…INTO语句

SELECT…INTOINSERT…SELECT很相似,都可以将数据批量插入到表中,但是它们还是显著区别的:

  • SELECT…INTO语句允许被插入的表不存在,该语句可以自动新建表并插入数据.
  • INSERT语句,无论是什么形式的,都要求被插入的表必须已经存在,否则就插入失败.
USE tempdb;
GO

SELECT a,b,aANDb INTO ##Temp3 		--从##Temp1取出数据并插入到Temp3(自动新建);
FROM ##Temp1;			
GO

SELECT * FROM ##Temp3;		--查看新建的##Temp3;
GO

以上脚本可以新建一个不存在的##Temp3,并将数据插入至其中:

BULK INSERT语句

BULK INSERT语句允许以用户指定的格式将数据文件导入到数据库表或视图中,对于大量数据尤为常用.

BULK INSERT通用语法如下:

BULK INSERT   
   [ database_name . [ schema_name ] . | schema_name . ] [ table_name | view_name ]   
      FROM 'data_file'   
     [ WITH   
    (   
   [ [ , ] BATCHSIZE = batch_size ]   
   [ [ , ] CHECK_CONSTRAINTS ]   
   [ [ , ] CODEPAGE = { 'ACP' | 'OEM' | 'RAW' | 'code_page' } ]   
   [ [ , ] DATAFILETYPE =   
      { 'char' | 'native'| 'widechar' | 'widenative' } ]   
   [ [ , ] DATASOURCE = 'data_source_name' ]
   [ [ , ] ERRORFILE = 'file_name' ]
   [ [ , ] ERRORFILE_DATA_SOURCE = 'data_source_name' ]   
   [ [ , ] FIRSTROW = first_row ]   
   [ [ , ] FIRE_TRIGGERS ]   
   [ [ , ] FORMATFILE_DATASOURCE = 'data_source_name' ]
   [ [ , ] KEEPIDENTITY ]   
   [ [ , ] KEEPNULLS ]   
   [ [ , ] KILOBYTES_PER_BATCH = kilobytes_per_batch ]   
   [ [ , ] LASTROW = last_row ]   
   [ [ , ] MAXERRORS = max_errors ]   
   [ [ , ] ORDER ( { column [ ASC | DESC ] } [ ,...n ] ) ]   
   [ [ , ] ROWS_PER_BATCH = rows_per_batch ]   
   [ [ , ] ROWTERMINATOR = 'row_terminator' ]   
   [ [ , ] TABLOCK ]   

   -- input file format options
   [ [ , ] FORMAT = 'CSV' ]
   [ [ , ] FIELDQUOTE = 'quote_characters']
   [ [ , ] FORMATFILE = 'format_file_path' ]   
   [ [ , ] FIELDTERMINATOR = 'field_terminator' ]   
   [ [ , ] ROWTERMINATOR = 'row_terminator' ]   
    )]

虽然看着很复杂,但其实就只用指定两个参数: 数据文件来源,文件导入格式.直白地说就是,读取数据文件,以何种方式读取该文件,多少个字节为一行?每两个列之间用什么符号分隔?诸如此类的限定.

关于这个语句并不在这里多说,详情请移步这里.查看具体参数的意义和用法.

更改数据

使用UPDATE来更新表中的数据,其标准语法如下:

UPDATE tb_name_or_view_name
SET column_name = expression,...
WHERE condition;
GO

其中WHERE指定要更改的列满足的条件,前面已经举过例子,就不再多说了.需要注意以下几点:

  • 当不使用WHERE语句时,UPDATE更新的是列的所有值.
  • 另外还可以有UPDATESETFROM…的搭配用法:
UPDATE tb_name_or_view_name
SET column_name = expression,...
FROM condition;
GO

  • 其中FROM的条件一般是在表/视图中选择特定的数据,例子就不举了.
  • 建议先使用SELECT语句查看将要更新的内容,再进行更新,以免出现错误.

删除数据

使用DELETE可以删除表中的数据,其标准语法如下:

DELETE FROM tb_name_or_view_name
WHERE condition;
GO

同样的,DELETE也可以使用FROM子句,且DELETE可以使用两个FROM子句,第一个用来指定要删除数据的表/视图,第二个指定条件:

UPDATE FROM tb_name_or_view_name
FROM condition;
GO

同样的,如果不指定条件,DELETE将删除所有数据.建议先用SELECT查看数据确认无误之后再删除.

除了DELETE,还有一种删除方式,使用TRUNCATE TABLE关键字:

TRUNCATE TABLE tb_name_or_view_name
GO

TRUNCATE TABLE语句可删除表中所有数据,且该过程不会记录日志,即该过程是不可逆的.

DELETE操作是记录日志的,操作以后是可以恢复数据的.因此当删除大量数据时,TRUNCATE TABLE要比DELETE高效.

检索数据

使用SELECT语句可以检索表中数据,该语句一般包含三个部分:

  • SELECT子句: 用于指定将要检索的列名称.
  • FROM子句: 用于指定将要检索的表/视图名.
  • WHERE子句: 用于指定检索应满足的条件.

基本形式是这样的:

SELECT column_name
FROM tb_name_or_view_name
WHERE condition;
GO

下面针对一些特定例子,来说明SELECT语句的具体用法:

指定列名

可以使用SELECT指定多个列名,应该注意以下三点:

  • 指定的列名必须和目标表中的列名相一致,否则将会出现意想不到的结果或错误.
  • 指定多个列名没有顺序的分别,只要列名合法即可.
  • SELECT语句只是显示数据,对表中数据存储没有任何影响.

以下是一个简单的演示:

USE AdventureWorks2017;
GO

SELECT ProductNumber, Name, SellEndDate, SellStartDate
FROM Production.Product;
GO

运行结果如下:

使用WHERE子句检索数据

在实际使用中,一个表通常具有大量的数据,使用SELECT选择全部的列数据显然会带来极大的开销,是很不现实的.因此我们通常的操作都是–只选择表中部分数据进行检索,再决定如何修改.使用WHERE子句就可以达到这样的目的,它的基本用法有以下几种:

简单条件搜索

所谓简单条件,就是只使用单个逻辑表达式为条件,来进行数据检索的一种形式,其中最为常用的是比较运算和逻辑非运算,如:

USE AdventureWorks2017;
GO

SELECT TOP(6) WITH TIES Name,StandardCost
FROM Production.Product
WHERE StandardCost > 2000
ORDER BY StandardCost
GO

以上代码是将表中StandardCost > 2000的部分选择出来,再按照StandardCost升序排序后显示出来:

模糊条件搜索

在检索数据时,通常检索条件都是不确切的,我们可以使用LIKE关键字指定某种模式,这样搜索出来的结果是和该模式匹配的所有结果,这样的搜索称为模糊搜索.LIKE的基本语法如下:

match_expression [NOT] LIKE pattern [ESCAPE escape_character];
GO

上述语法中,pattern是模式,NOT表示取反,是可选的参数,ESCAPE用于指定转义字符.其中pattern可以使用如下4中通配符:

  • % : 代表0或多个字符.
  • _ : 代表任一字符.
  • [] : 代表方括号集合内的任意单个字符.
  • [^] : 代表不在集合中的任意单个字符.

请看以下示例:

USE AdventureWorks2017;
GO

SELECT TOP(6) WITH TIES Name,StandardCost,ListPrice
FROM Production.Product
WHERE NAME LIKE '%Yellow%'								--NAME中含有模式串Yellow的均匹配上;
ORDER BY StandardCost
GO

运行结果如下:

注意: 当搜索条件出现了通配符,则需要使用ESCAPE来指定转义字符,以表示该符号是内容而不是通配符,如:

WHERE NAME LIKE '%RED!%%' ESCAPE '!';

上面的语句用于指定了’!‘是转义字符,表示非,因而’!%‘用来说明其后面的’%‘是内容而不是通配符.要匹配的模式就是’RED%’.

复合条件搜索

所谓复合条件,就是多个简单条件的合并,合并采用逻辑运算符,常用的有ANDNOTOR等.可以参见前面说过的逻辑运算符.

举一个简单例子如下:

USE AdventureWorks2017;
GO

SELECT TOP(6) WITH TIES Name,StandardCost,ListPrice
FROM Production.Product
WHERE NAME LIKE '%Red%' AND StandardCost > 2000 			--NAME中含有模式串Red且StandardCost > 2000的匹配上;
ORDER BY StandardCost
GO

运行结果如下:

使用字符串

在查询时,为了使查询结果的可读性进一步提升,通常可以加上一些相关的字符串,以此当作说明:

USE AdventureWorks2017;
GO

SELECT 'Product model: ', ProductNumber, 'Product name: ', Name
FROM Production.Product;
GO

运行结果如下:

使用以上方法,可以在每一个属性前面加上对应的说明字符串,大大提高了检索结果的可读性.

改变列标题

在查询时,可以为表中的列在结果中另起一个别名,达到重命名某列的效果,一般我们有两种做法:

  • 新标题 = 列名.
  • 列名 AS 新标题.

如下是一个示例:

USE AdventureWorks2017;
GO

SELECT '产品型号: ' =  ProductNumber, Name AS '产品名称: '
FROM Production.Product;
GO

运行结果如下:

可见在实时翻译列名时这种查询方式还是非常方便的.

数据运算

可以对检索出来的结果进行各种数据运算,且数据运算不限类型,可以在任何类型上进行,也可以使用各种函数进行运算.

普通数学运算:

USE AdventureWorks2017;
GO

SELECT Name AS '产品名称: ', 
	   SellStartDate, SellEndDate,
	   TimeLasting = SellEndDate - SellStartDate
FROM Production.Product;
GO

上面的例子计算销售周期,使用开始销售时间减去销售结束时间,该列在查询中的名称为: TimeLasting.

字符串函数运算:

USE AdventureWorks2017;
GO

SELECT UPPER(Name) AS '产品名称: ','产品型号: ' =  LOWER(ProductNumber)
FROM Production.Product;
GO

运行结果如下:

以上的例子很显然,就是运用字符串函数,把产品名称全部大写了,产品型号全部小写了.


数据类型强制转换

在对数据进行运算的过程中,时常需要把一种类型的数据强制转换为另一种类型,可以使用强制类型转换函数: CASTCONVERT:

  • CAST(expression AS type): 将表达式(expression)的类型转换为type.
  • CONVERT(type,expression): 同上,参数位置不同而已.

需要指出的是,CONVERT不仅能讲表达式的类型装换为type,还可以改变日期数据的样式, 用以下语法:

  • CONVERT(date_type, expression, style).

以上的style参数常用的取值如下:

不带世纪数位 (yy) (1)带世纪数位 (yyyy)Standard输出的日期格式
-0 或 100datetime 和 smalldatetime 的默认值mon dd yyyy hh:miAM(PM)
1101美国1 = mm/dd/yy 101 = mm/dd/yyyy
2102ANSI2 = yy.mm.dd 102 = yyyy.mm.dd
3103英国/法国3 = dd/mm/yy 103 = dd/mm/yyyy
4104德语4 = dd.mm.yy 104 = dd.mm.yyyy
5105意大利语5 = dd-mm-yy 105 = dd-mm-yyyy
6106-6 = dd mon yy 106 = dd mon yyyy
7107-7 = Mon dd, yy 107 = Mon dd, yyyy
8108-hh:mi:ss
-9 或 109默认格式 + 毫秒mon dd yyyy hh:mi:ss:mmmAM(或 PM)
10110USA10 = mm-dd-yy 110 = mm-dd-yyyy
11111日本11 = yy/mm/dd 111 = yyyy/mm/dd
12112ISO12 = yymmdd 112 = yyyymmdd
-13 或 113欧洲默认格式 + 毫秒dd mon yyyy hh:mi:ss:mmm(24h)
14114-hh:mi:ss:mmm(24h)
-20 或 120ODBC 规范yyyy-mm-dd hh:mi:ss(24h)
-21 或 121time、date、datetime2 和 datetimeoffset 的 ODBC 规范(带毫秒)默认值yyyy-mm-dd hh:mi:ss.mmm(24h)
-126ISO8601yyyy-mm-ddThh:mi:ss.mmm(无空格)
-127带时区 Z 的 ISO8601。yyyy-mm-ddThh:mi:ss.mmmZ(无空格)
-130回历dd mon yyyy hh:mi:ss:mmmAM
-131回历dd/mm/yyyy hh:mi:ss:mmmAM

关于CONVERTCAST的用法,如下例所示:

-- Use CAST  
USE AdventureWorks2017;  
GO  
SELECT SUBSTRING(Name, 1, 30) AS ProductName, ListPrice = CAST(ListPrice AS INT)
FROM Production.Product
GO  
  
-- Use CONVERT.  
USE AdventureWorks2017;  
GO  
SELECT SUBSTRING(Name, 1, 30) AS ProductName, ListPrice = CONVERT(INT,ListPrice)
FROM Production.Product  
GO

以上例子将ListPrice列的数据转换为INT类型的数据,再显示出来,运行结果如下:

下面是一个对日期类型进行转换的例子:

SELECT   
   '2006-04-25T15:50:59.997' AS UnconvertedText,  
   CAST('2006-04-25T15:50:59.997' AS datetime) AS UsingCast,  
   CONVERT(datetime, '2006-04-25T15:50:59.997', 126) AS UsingConvertFrom_ISO8601 ;  
GO  

注意: 不是所有类型都支持强制类型转换,转换可能会有精度丢失/出错等情况出现,使用时应先确保两个类型可以隐式转换.

使用ALL和DISTINCT关键字

在检索数据时,我们可以用ALL关键字显示某个条件下所有的行,用DISTINCT来显示非重复的行.具体用法如下例:

USE AdventureWorks2017;
GO

SELECT ALL Color							--显示所有颜色;
FROM Production.Product;
GO

SELECT DISTINCT Color					--每种颜色仅显示一次;
FROM Production.Product;
GO

运行结果如下:

注: SQL server中,使用DISTINCT关键字来除去结果中重复的行,是一种非常有效的手段.

排序

不管使用何种高级语言,排序都是一种非常常用的操作,SQL server中也不例外.通常我们都是对结果集进行排序,如对SELECT的结果进行排序.可以使用ORDER BY子句来进行排序,如果默认不指定排序方式,则按照升序排列,请看一下例子:

USE AdventureWorks2017;
GO

SELECT ProductNumber,ProductID
FROM Production.Product
ORDER BY ProductID
GO

上述例子结果集按照ProductID升序排序,运行结果如下:

当然也可以指定复杂排序,比如 ORDER BY可以使用多列作为参数排序,在此时这些列的顺序是很重要的,因为此时是默认按照排在前面的列的优先级排序,只有当前面的列优先级相同时,才会按照后面的列进行排序,此时排在最前面的列拥有最高的优先级,其他的列都可以理解为备份(backup)列,仅当前面的优先级冲突时,才会启用后面的列进行优先级判断.在排序时可以指定DESC关键字修改为按照降序排序.

比如同样的脚本,使用一下DESC关键字其结果就会不一样:

USE AdventureWorks2017;
GO

SELECT ProductNumber,ProductID
FROM Production.Product
ORDER BY ProductID DESC
GO

使用ORDER BY排序以后,一般我们会和TOP函数一同使用来得到我们想要的结果,TOP的作用是显示结果集开头部分的某些行,如下:

TOP(n);								--显示前n行;

TOP(n) PERCENT;				--显示前n%的行;

如下例子我们可以选出按ProductID排序后的结果中前5和前5%的行:

USE AdventureWorks2017;
GO

SELECT TOP(5) ProductID,ProductNumber
FROM Production.Product
ORDER BY ProductID
GO

SELECT TOP(5) PERCENT ProductID,ProductNumber
FROM Production.Product
ORDER BY ProductID
GO

运行结果如下:

在使用TOP时,我们时常会遇到这样的情况: 按照某个优先级排序时,当n+1(或n+m)行和第n行有着相似之处(一般为优先级相同)时,我们希望尽可能将更多符合条件的行显示出来,就可以使用WITH TIES子句将它们显示出来,其用法如下:

USE AdventureWorks2017;
GO

SELECT TOP(6) WITH TIES Name,StandardCost
FROM Production.Product
ORDER BY StandardCost
GO

上述例子我们选择了前6行,但是使用了WITH TIES子句,实际上和第6行优先级相同的行也被认为是"合理的",因此结果多于6行:

聚合

聚合是指对一组数据进行聚合运算得到聚合值的过程,在SQL server中,一般使用聚合函数来达到聚合效果.常用三个语句来调用聚合函数,即: SELECTCOMPUTEHAVING语句,本节介绍前两个语句,HAVING在下节讲分组时介绍.

在SELECT中使用聚合函数

SELECT语句中可以使用聚合函数进行计算,计算结果将以一个新列的形式呈现出来.聚合运算表达式可以包括列名,常量以及由算术运算符连接起来的函数.以下例子使用聚合函数计算了一些值:

USE AdventureWorks2017;
GO

SELECT 
	表中数据量 = COUNT(*),
	表中标准成本不同的数据量 = COUNT(DISTINCT StandardCost),
	表中颜色非空的数据量 = COUNT(Color)
FROM Production.Product
GO

SELECT 
	标准成本最大值 = MAX(StandardCost),
	标准成本最小值 = MIN(StandardCost),
	标准成本平均值 = AVG(StandardCost),
	标准成本的标准差 = STDEV(StandardCost),
	标准成本的方差 = VAR(StandardCost)
FROM Production.Product
GO

就不解释上述语句了,已经写了中文参考了:

注意:

  • 之所以可以统计非空的数据量是因为大多数聚合函数忽略空值,如COUNT.
  • 但是当使用COUNT(*)时,不忽略空值!!!

我们再看一个例子:

USE AdventureWorks2017;
GO

SELECT 
	表中数据量 = COUNT(*),
	表中标准成本不同的数据量 = COUNT(DISTINCT StandardCost),
	StandardCost,
	表中颜色非空的数据量 = COUNT(Color)
FROM Production.Product
GO

运行却出现了一个错误:

根据这个错误提示我们容易想到 --> SELECT中不允许普通列和聚合函数一起选择,也就是说我们没有办法在SELECT语句的结果集中既得到聚合值又得到该聚合值的明细值,它提示我们可以使用GROUP BY解决,这个问题将会在讲解分组时说明.

在COMPUTE中使用聚合函数(Abandoned)

COMPUTE语句可以解决上文提及的SELECT无法同时显示聚合值和明细值的问题.它是从属于SELECT的一个子句,用于对某些列套用聚合函数,而这些列的内容还是原样作为SELECT的结果显示出来.COMPUTE得出的结果将会单独新建列并在原来的结果上追加.

COUPUTE有两种形式: COUPUTECOMPUTER BY.前者表示对所有明细值计算,后者对明细值分组,并计算给出每一组的聚合值.

下面是没有使用BY的例子:

USE AdventureWorks2017;
GO

SELECT CustomerID,OrderDate,SubTotal,TotalDue
FROM Sales.SalesOrderHeader
ORDER BY OrderDate
COMPUTE SUM(SubTotal),SUM(TotalDue)
GO

然后是使用了BY的例子:

USE AdventureWorks2017;
GO

SELECT SalesPersonID,CustomerID,OrderDate,SubTotal,TotalDue
FROM Sales.SalesOrderHeader
ORDER BY SalesPersonID,OrderDate
COMPUTE SUM(SubTotal),SUM(TotalDue) BY SalesPersonID
GO

在这一段我没有办法给出截图演示,因为SQL server 2017的COMPUTE已经不是SELECT子句,而是作为SHAPE的子句,其功能也和现在所演示的不一样,我只能说,在SQL server 2008这个版本中,是可以这么使用的.需要注意以下几点:

  • 必须先使用ORDER BY对数据进行排序才能使用COMPUTE BY这种分组聚合操作.
  • COMPUTE BY后面的列一定是ORDER BY后面列的真子集,即COMPUTE BY只能在ORDER BY分好的组里进行选择,符合上一条.
  • COMPUTE BY是SQL server(2008及以前)内部的分组聚合关键字,无法在外部程序(SSMS以外)调用,具有较大的局限性.

分组

聚合函数只能产生一个单一的汇总数据,使用分组技术,配合聚合函数就可以生成分组的汇总数据.分组使用GROUP BY语句,该语句是符合标准SQL语法标准的,相比COUPUTE BY,该语句可以在外部应用程序中直接使用,是推荐的分组方式,其使用有如下几种类型:

普通分组

GROUP BY的基本语法如下:

GROUP BY column_name;
GO

以上就是没有条件的分组,仅仅指定按照某个列进行分组.若要添加条件,可以使用HAVING子句:

GROUP BY column_name
HAVING Condition;
GO

下面是一个分组查询的例子:

USE AdventureWorks2017;
GO

SELECT Color,
	颜色相同的产品数量 = COUNT(*),
	颜色相同的产品数量最大安全库存 = MAX(SafetyStockLevel)
FROM Production.Product
WHERE Color IS NOT NULL
GROUP BY Color;
GO

运行结果如下:

到这里或许有人会觉得,这个分组操作是否具有必要性,因为看起来这就是一个很正常的SELECT操作.那么我们把分组语句注释掉:

似曾相识,不是么?这其实是前文说过的SELECT不允许将普通列选择操作和聚合函数一起使用,因为这样会导致出现一些问题,比如COUNT(*)这个用法,按照常理将统计所有的产品数量(不管它们的颜色相同或不同),这样就是会在每一个颜色后面跟着一个总值,问题是该值表示"所有颜色的产品的所有数量",而非"该类颜色所有数量",这样就会导致使用聚合函数得出的结果并没有实际意义,因此SQL server规定普通列选择和聚合函数不可以一起使用,除非按照某种规则分组,这样聚合函数的操作范围就对应到每一个组中,就不会出现问题了.

下面是一个使用HAVING条件分组的例子:

USE AdventureWorks2017;
GO

SELECT Color,
	颜色相同的产品数量 = COUNT(*),
	颜色相同的产品数量最大安全库存 = MAX(SafetyStockLevel)
FROM Production.Product
WHERE Color IS NOT NULL
GROUP BY Color
HAVING COUNT(*) > 25;		--组内非空数量 > 25才显示出来;
GO

运行结果如下:

我们剖析一下上面的语句执行的过程,大致分为以下四步:

  1. 执行FROM子句,先将Production.Product表中所有数据检索出来.
  2. 执行WHERE Color IS NOT NULL,对1中数据进行过滤,将Color为空的数据剔除.
  3. 执行GROUP BY Color子句,对2的结果进行分组,对每组执行聚合函数COUNT(*)MAX(SafetyStockLevel),将结果加入每一组中.
  4. 将以上处理的结果返回给SELECT,并按照其指定样式显示出来.

注: 上文没有提到的一个点,可以在WHERE中使用关键字:

column_name IS NULL : 表示筛选条件为column_name列为空

column_name IS NOT NULL : 表示筛选条件为colum_name列非空

ROLLUP和CUBE关键字

GROUP BY语句中,未使用ROLLUPCUBE关键字的分组称为普通分组.ROLLUPCUBE关键字作用如下:

ROLLUP : 得到各组的单项组合.

CUBE : 得到各组的任意组合.

下面通过几个例子说明其功能:

USE AdventureWorks2017;
GO

SELECT Name,Color,
	颜色相同的产品数量最大安全库存 = MAX(SafetyStockLevel)
FROM Production.Product
WHERE Color IS NOT NULL
GROUP BY Name,Color
HAVING MAX(SafetyStockLevel) > 800;
GO

上述例子按照产品名称和颜色选择并过滤了一些数据,使用MAX(SafetyStockLevel) > 800是为了简化结果集数据,易于演示:

这是我们再使用一下ROLLUP关键字,看看结果如何:

USE AdventureWorks2017;
GO

SELECT Name,Color,
	颜色相同的产品数量最大安全库存 = MAX(SafetyStockLevel)
FROM Production.Product
WHERE Color IS NOT NULL
GROUP BY Name,Color WITH ROLLUP
HAVING MAX(SafetyStockLevel) > 800;
GO

结果如下(比初始结果多5行):

其结果解说如下:

  • 第二行表示Name为Chainring,Color为任意值的产品的最大安全库存为1000.
  • 第四行表示Name为Chainring Bolts,Color为任意值的产品的最大安全库存为1000.
  • 第六行表示Name为Chainring Nut,Color为任意值的产品的最大安全库存为1000.
  • 第八行表示Name为Lock Ring,Color为任意值的产品的最大安全库存为1000.
  • 第九行表示Name为任意值,Color为任意值的产品的最大安全库存为1000.

于是我们可以得出: NULL在这里表示"任意、所有"的意思.

下面我们再使用一下CUBE关键字,看结果如何:

USE AdventureWorks2017;
GO

SELECT Name,Color,
	颜色相同的产品数量最大安全库存 = MAX(SafetyStockLevel)
FROM Production.Product
WHERE Color IS NOT NULL
GROUP BY Name,Color WITH CUBE
HAVING MAX(SafetyStockLevel) > 800;
GO

运行结果如下(比初始结果多7行):

在这个结果集中,各行数据代表什么含义我就不再解释了,主要说一下CUBEROLLUP的主要区别:

  • ROLLUP按照GROUP BY的第一个列参数进行任意匹配,比如上文按照Name匹配任意Color,并求一个组合值.
  • CUBE按照GROUP BY所有列关键字进行任意匹配,比如上文按照Name匹配任意Color,又反过来按照Color匹配任意Name,求组合值.
  • 因此说ROLLUP进行单项匹配,CUBE进行多项匹配,参数相同的情况下,CUBE要显示更多的结果集.

以上的例子由于简化了数据,所以并不很直观,读者可以自己建立表并测试.建表的诀窍在于控制单一变量,考虑尽可能多的情况.

表的连接

在实际使用中,经常会遇到需要对两个或以上的表同时进行检索的操作(因为数据可能分散在两个或以上表中),举个例子:

教师表: Teacher(T#,TName,Title)

课程表: Course(C#,CName,T#)

学生表: Student(S#,SName,Age,Sex)

学生选课表: StudentCourse(S#,C#,Score)

此时若我要检索选择课程为C1的学生的信息,则需要现在StudentCourse找到课程标识符列C#,并定位课程C1,再根据C1的选课情况,定位学生标识符(S#),从而到Student表去找到该学生对应信息.

我们把从两个或以上表中同时检索数据的操作称为表的连接操作,具体可以分为以下三类连接操作:

交叉连接(笛卡尔积)

关于笛卡尔积的定义,离散数学和数据库原理均有提及,就不在此赘述了.简而言之,笛卡尔积可以包含两个表数据项的所有组合.

使用CROSS JOIN关键字来创建笛卡尔积的结果表,如下:

USE AdventureWorks2017;
GO

SELECT PP.ProductID,Name,ProductNumber,LocationID,Quantity
FROM Production.Product PP CROSS JOIN Production.ProductInventory PPI	--从PP和PPI的笛卡儿积结果中选择;
GO

运行结果如下:

需要注意的是,上例中使用了别名,PP指代Production.Product表,PPI指代Production.ProductInventory表,因为这两个表有相同的列ProductID,不使用别名而是直接SELECT列名会出现错误.

笛卡尔积其实应用的并不多,但它却是内连接和外连接的基础,因此理解笛卡尔积很重要.

内连接(连接)

内连接对应数据库原理的连接操作.即,在笛卡尔积的基础上,仅保留那些符合条件的行,将两个表连接成第三个表.

当满足的条件是用等号’='连接的,则称这样的连接为等值连接.内连接的基本语法如下:

SELECT column_name1,column_name2,...,column_namen
FROM tb1_name INNER JOIN tb2_name
ON condition;
GO

其中INNER JOIN表示内连接操作,ON关键字后面跟的是内连接的条件(即新生成的表每一个属性满足何种条件),示例如下:

USE AdventureWorks2017;
GO

SELECT PP.ProductID,Name,ProductNumber,SafetyStockLevel,LocationID,Quantity
FROM Production.Product PP INNER JOIN Production.ProductInventory PPI	
ON PP.ProductID = PPI.ProductID									--表示同一个产品;
GO

运行结果如下:

上面的例子中,从ProductID到SafetyStockLevel这四列都来自Production.Product表,最后两列来自Production.ProductInventory表,其中ProductID是这两个表共有的列,它在这两个表中的值是一致的.做内连接的条件是PP.Product = PPI.Product,即表示同一个产品,这是将该产品分散在两个表中的六种属性给整合在一起,最后显示出来.

外连接(自然连接)

外连接对应数据库原理的自然连接.它要求两个表必须有公共的列属性,在连接时将共同的列属性合并在一起形成一个新的表,但是外连接又和自然连接不完全相同,实际上,自然连接只能算是外连接的一种情况,外连接扩充了自然连接,共有三种情况:

进行外连接的表,顺序很重要,事先规定,在前面的为左表,后面的为右表,即有 -> “左表 外连接 右表” 一说.

  • LEFT OUTER JOIN : 包括了左表中全部满足条件的数据,右表中不满足条件的数据将显示为NULL.
  • RIGHT OUTER JOIN : 包括了右表中全部满足条件的数据,左表中不满足条件的数据将显示为NULL.
  • FULL OUTER JOIN : 不限制条件,直接将整个左表和右表所有选择的数据连接起来,这就是标准的自然连接.

先说一下语法:

SELECT column_name1,column_name2,...,column_namen
FROM tb1_name FULL OUTER JOIN | [LEFT OUTER JOIN] | [RIGHT OUTER JOIN] tb2_name
ON condition;
GO

其上参数就不再解释了,和内连接并无二样,我们来看一个例子:

USE AdventureWorks2017;
GO

SELECT PP.Name,SOD.SalesOrderID
FROM Production.Product PP FULL OUTER JOIN Sales.SalesOrderDetail SOD	
ON PP.ProductID = SOD.ProductID									--表示同一个产品;
ORDER BY PP.Name;
GO

运行结果如下:

上述例子对Production.Product表和Sales.SalesOrderDetail表作了自然连接.其中不符合条件的用NULL填充,可以看到有的商品有NAME却没有订单号,这说明该商品并没有销量,所以没有创建订单.


注: 在使用表的连接时,往往两个表都会有重合部分(为了让连接有意义),此时务必要使用别名进行操作,以免发生错误.

子查询

SELECT语句可以嵌套在其它语句中,这些语句包括SELECTINSERTUPDATEDELETE等,这些嵌套的SELECT语句称为子查询.在某些查询中,查询语句复杂难以实现时,可以将其拆分为若干简单的子查询去写,一般的子查询可以用连接代替,在使用时为了性能应该优先使用连接操作,当连接复杂难以实现时,再考虑子查询.

可以在子查询中使用WHEREGROUP BYHAVINGTOP && ORDER BY子句,不能使用COMPUTE子句,子查询必须用圆括号括起来.

以下是一些子查询的用法:

WHERE expression [NOT] IN (subquery);

WHERE expression comparison_operator [ANY/ALL] (subquery);

WHERE [NOT] EXISTS (subquery);

下面是一个例子:

USE AdventureWorks2017;
GO

SELECT SalesOrderID,OrderDate,DueDate,
	ProductAmount = (
		SELECT COUNT(*)
		FROM Sales.SalesOrderDetail SSOD
		WHERE SSOD.SalesOrderID = SSOH.SalesOrderID
	)
FROM Sales.SalesOrderHeader SSOH
WHERE SalesOrderID IN (
	SELECT TOP(3) SalesOrderID
	FROM Sales.SalesOrderDetail
	GROUP BY SalesOrderID
	ORDER BY COUNT(SalesOrderID) DESC
)
GO

运行结果如下:

只需记住,查询是由下往上递归进行的即可,即最外层查询的条件先执行,在进入内层查询,最后返回到最外层作为总的结果.

集合运算

查询结果是一个集合.因此可以对查询结果进行集合运算.这些运算包括集合的交、并、差等运算,具体使用方法如下:

运算符作用
UNION求两个集合的并集
EXCEPT求两个集合的差集
INTERSECT求两个集合的交集

注意: 集合运算的两个集合数据类型必须兼容,否则就会出现错误.

为了方便说明,我们先建两个表:

USE Temp;
GO

CREATE TABLE test.T1(Column1 INT);
GO

CREATE TABLE test.T2(Column2 INT);
GO

INSERT INTO test.T1 VALUES(3);
INSERT INTO test.T1 VALUES(5);
INSERT INTO test.T2 VALUES(3);
INSERT INTO test.T2 VALUES(8);
GO

SELECT * FROM test.T1;
SELECT * FROM test.T2;
GO

然后再对这两个表的查询结果进行集合运算,如下:

USE Temp;
GO

SELECT Column1 FROM test.T1
UNION
SELECT Column2 FROM test.T2;
GO

SELECT Column1 FROM test.T1
EXCEPT
SELECT Column2 FROM test.T2;
GO

SELECT Column1 FROM test.T1
INTERSECT
SELECT Column2 FROM test.T2;
GO

运行结果如下:

既然我们强调了是集合运算,集合是具有唯一性的,因此对于多个重复数据,只显示一个,也建议尽量不要在表中插入重复数据项.

公用表表达式

公用表表达式(Common Table Expression),即CTE.它是定义在SELECTINSERTUPDATEDELETE语句中临时命名的结果集.CTE也可以在视图中定义,其定义包含对自身的引用,因此也被称为递归CTE.

CTE的主要作用是实现递归,以完成一些较为复杂的操作,下面是它的通用定义语法:

[ WITH <common_table_expression> [ ,...n ] ]  
  
<common_table_expression>::=  
    expression_name [ ( column_name [ ,...n ] ) ]  
    AS  
    ( CTE_query_definition )

下面是一个简单CTE定义的例子:

USE AdventureWorks2017;
GO

WITH Sales_CTE (SalesPersonID, SalesOrderID, SalesYear)  
AS  
-- Define the CTE query.  
(  
    SELECT SalesPersonID, SalesOrderID, YEAR(OrderDate) AS SalesYear  
    FROM Sales.SalesOrderHeader  
    WHERE SalesPersonID IS NOT NULL  
)  
-- Define the outer query referencing the CTE name.  
SELECT SalesPersonID, COUNT(SalesOrderID) AS TotalSales, SalesYear  
FROM Sales_CTE  
GROUP BY SalesYear, SalesPersonID  
ORDER BY SalesPersonID, SalesYear;
GO

运行结果如下:

可以把CTE本身理解为一种查询的实现,查询CTE就是查询CTE的规则下的表/视图的结果集,当然也可以递归,这里就不再讨论了.

PIVOT和UNPIVOT

SQL server中提供了把列数据转化为行数据的方法,其中用到PIVOTUNPIVOT运算符:

  • PIVOT是一个运算符,它可以把某一列的数据转化为输出中的多个列.
  • UNPIVOT则刚好相反,它可以把多个列转化为输出中的一个列.

这两个运算符的用法如下:

SELECT <non-pivoted column>,  
    [first pivoted column] AS <column name>,  
    [second pivoted column] AS <column name>,  
    ...  
    [last pivoted column] AS <column name>  
FROM  
    (<SELECT query that produces the data>)   
    AS <alias for the source query>  
PIVOT  
(  
    <aggregation function>(<column being aggregated>)  
FOR   
[<column that contains the values that will become column headers>]   
    IN ( [first pivoted column], [second pivoted column],  
    ... [last pivoted column])  
) AS <alias for the pivot table>  
<optional ORDER BY clause>;

使用FOR关键字指定要转化的列,下面是一个例子:

USE AdventureWorks2017;
GO

SELECT DaysToManufacture, AVG(StandardCost) AS AverageCost   
FROM Production.Product  
GROUP BY DaysToManufacture;

-- Pivot table with one row and five columns  
SELECT 'AverageCost' AS Cost_Sorted_By_Production_Days, [0], [1], [2], [3], [4]  
FROM (
	SELECT DaysToManufacture, StandardCost   
    FROM Production.Product
) AS SourceTable  
PIVOT (  
	AVG(StandardCost)  
	FOR DaysToManufacture IN ([0], [1], [2], [3], [4])  
) AS PivotTable;

同样的结果采用了不同的显示方式,其运行结果如下:

经过对比我们可以发现,按照这样的指定,原来的DaysToManufacture的每一个值变成了新表的列名,原来的AverageCost的每一个值变成了第二行每一列的值,即使这样会出现一个空的值(因为原来DaysToManufacture没有值为3的数据,而在指定时却指定了).

下面是一个更复杂的例子:

USE AdventureWorks2017;
GO

SELECT VendorID, [250] AS Emp1, [251] AS Emp2, [256] AS Emp3, [257] AS Emp4, [260] AS Emp5  
FROM (
	SELECT PurchaseOrderID, EmployeeID, VendorID  
	FROM Purchasing.PurchaseOrderHeader
) p  
PIVOT (  
	COUNT (PurchaseOrderID)  
	FOR EmployeeID IN  ( [250], [251], [256], [257], [260] )  
) AS pvt  
ORDER BY pvt.VendorID;

运行结果如下:

注意:如果聚合函数与PIVOT一起使用,则计算聚合时将不考虑出现在值列中的任何空值.

至于关键字,不多说了,拿微软官方文档的一个例子演示:

USE Temp;
GO

-- Create the table and insert values as portrayed in the previous example.  
CREATE TABLE pvt (VendorID int, Emp1 int, Emp2 int,  
    Emp3 int, Emp4 int, Emp5 int);  
GO  
INSERT INTO pvt VALUES (1,4,3,5,4,4);  
INSERT INTO pvt VALUES (2,4,1,5,5,5);  
INSERT INTO pvt VALUES (3,4,3,5,4,4);  
INSERT INTO pvt VALUES (4,4,2,5,5,4);  
INSERT INTO pvt VALUES (5,5,1,5,5,5);  
GO  
-- Unpivot the table.  
SELECT VendorID, Employee, Orders  
FROM   
   (SELECT VendorID, Emp1, Emp2, Emp3, Emp4, Emp5  
   FROM pvt) p  
UNPIVOT  
   (Orders FOR Employee IN   
      (Emp1, Emp2, Emp3, Emp4, Emp5)  
)AS unpvt;  
GO

运行结果如下:

加密表中数据

加密表中数据只需要按照以下几步走即可:

  1. 使用CREATE MASTER KEY创建数据库主密钥.
  2. 使用CREATE CERTIFUCATE创建一个证书,用于加密对称秘钥.
  3. 使用CREATE SYMMETRIC KEY创建对称秘钥,采用2中的证书和适当的对称秘钥算法进行加密.
  4. 修改表的结构,使用ALTER TABLE增加一个用于存放加密数据的列.
  5. 使用OPEN SYMMETRIC KEY打开对称秘钥,然后使用ENCRYPTBYKEY函数加密数据.

最后就可以查看加密的数据了,示例如下:

USE AdventureWorks2017;
GO

CREATE MASTER KEY ENCRYPTION
	BY PASSWORD = '1346798520'
GO

CREATE CERTIFICATE StudentID WITH SUBJECT = 'The encrypted sid'
GO

CREATE SYMMETRIC KEY Student_ID WITH ALGORITHM = AES_256
	ENCRYPTION BY CERTIFICATE StudentID
GO

ALTER TABLE tom_schema.Student
	ADD EncryptedSID VARBINARY (128) NULL
GO

OPEN SYMMETRIC KEY Student_ID DECRYPTION BY CERTIFICATE StudentID
GO

UPDATE tom_schema.Student
	SET EncryptedSID = ENCRYPTBYKEY(KEY_GUID('Student_ID'),CAST(sid AS VARCHAR(10)));
GO

SELECT * FROM tom_schema.Student;
GO

运行结果如下:

以上就是关于表中数据操纵的基本内容,下篇会接着讨论索引和查询优化的问题.

上一篇:
下一篇: 索引和查询优化

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值