文章目录
关系型数据库最重要的对象就是表,对表数据的操作十分关键.其中包括有增、删、改、查四种操作.分别对应数据操纵语言的INSERT、DELETE、UPDATE、SELECT四个关键语句.下面我们来详细介绍它们.
插入数据
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列插入数值,也使用了NULL和DEFAULT插入数值,结果如下:
注意两点:
- 应在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…INTO和INSERT…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更新的是列的所有值.
- 另外还可以有UPDATE…SET…FROM…的搭配用法:
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%’.
复合条件搜索
所谓复合条件,就是多个简单条件的合并,合并采用逻辑运算符,常用的有AND、NOT、OR等.可以参见前面说过的逻辑运算符.
举一个简单例子如下:
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
运行结果如下:
以上的例子很显然,就是运用字符串函数,把产品名称全部大写了,产品型号全部小写了.
数据类型强制转换
在对数据进行运算的过程中,时常需要把一种类型的数据强制转换为另一种类型,可以使用强制类型转换函数: CAST和CONVERT:
- CAST(expression AS type): 将表达式(expression)的类型转换为type.
- CONVERT(type,expression): 同上,参数位置不同而已.
需要指出的是,CONVERT不仅能讲表达式的类型装换为type,还可以改变日期数据的样式, 用以下语法:
- CONVERT(date_type, expression, style).
以上的style参数常用的取值如下:
不带世纪数位 (yy) (1) | 带世纪数位 (yyyy) | Standard | 输出的日期格式 |
---|---|---|---|
- | 0 或 100 | datetime 和 smalldatetime 的默认值 | mon dd yyyy hh:miAM(PM) |
1 | 101 | 美国 | 1 = mm/dd/yy 101 = mm/dd/yyyy |
2 | 102 | ANSI | 2 = yy.mm.dd 102 = yyyy.mm.dd |
3 | 103 | 英国/法国 | 3 = dd/mm/yy 103 = dd/mm/yyyy |
4 | 104 | 德语 | 4 = dd.mm.yy 104 = dd.mm.yyyy |
5 | 105 | 意大利语 | 5 = dd-mm-yy 105 = dd-mm-yyyy |
6 | 106 | - | 6 = dd mon yy 106 = dd mon yyyy |
7 | 107 | - | 7 = Mon dd, yy 107 = Mon dd, yyyy |
8 | 108 | - | hh:mi:ss |
- | 9 或 109 | 默认格式 + 毫秒 | mon dd yyyy hh:mi:ss:mmmAM(或 PM) |
10 | 110 | USA | 10 = mm-dd-yy 110 = mm-dd-yyyy |
11 | 111 | 日本 | 11 = yy/mm/dd 111 = yyyy/mm/dd |
12 | 112 | ISO | 12 = yymmdd 112 = yyyymmdd |
- | 13 或 113 | 欧洲默认格式 + 毫秒 | dd mon yyyy hh:mi:ss:mmm(24h) |
14 | 114 | - | hh:mi:ss:mmm(24h) |
- | 20 或 120 | ODBC 规范 | yyyy-mm-dd hh:mi:ss(24h) |
- | 21 或 121 | time、date、datetime2 和 datetimeoffset 的 ODBC 规范(带毫秒)默认值 | yyyy-mm-dd hh:mi:ss.mmm(24h) |
- | 126 | ISO8601 | yyyy-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 |
关于CONVERT和CAST的用法,如下例所示:
-- 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中,一般使用聚合函数来达到聚合效果.常用三个语句来调用聚合函数,即: SELECT、COMPUTE和HAVING语句,本节介绍前两个语句,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有两种形式: COUPUTE和COMPUTER 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
运行结果如下:
我们剖析一下上面的语句执行的过程,大致分为以下四步:
- 执行FROM子句,先将Production.Product表中所有数据检索出来.
- 执行WHERE Color IS NOT NULL,对1中数据进行过滤,将Color为空的数据剔除.
- 执行GROUP BY Color子句,对2的结果进行分组,对每组执行聚合函数COUNT(*)和MAX(SafetyStockLevel),将结果加入每一组中.
- 将以上处理的结果返回给SELECT,并按照其指定样式显示出来.
注: 上文没有提到的一个点,可以在WHERE中使用关键字:
column_name IS NULL : 表示筛选条件为column_name列为空
column_name IS NOT NULL : 表示筛选条件为colum_name列非空
ROLLUP和CUBE关键字
在GROUP BY语句中,未使用ROLLUP或CUBE关键字的分组称为普通分组.ROLLUP和CUBE关键字作用如下:
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行):
在这个结果集中,各行数据代表什么含义我就不再解释了,主要说一下CUBE和ROLLUP的主要区别:
- 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语句可以嵌套在其它语句中,这些语句包括SELECT、INSERT、UPDATE和DELETE等,这些嵌套的SELECT语句称为子查询.在某些查询中,查询语句复杂难以实现时,可以将其拆分为若干简单的子查询去写,一般的子查询可以用连接代替,在使用时为了性能应该优先使用连接操作,当连接复杂难以实现时,再考虑子查询.
可以在子查询中使用WHERE、GROUP BY、HAVING、TOP && 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.它是定义在SELECT、INSERT、UPDATE和DELETE语句中临时命名的结果集.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中提供了把列数据转化为行数据的方法,其中用到PIVOT和UNPIVOT运算符:
- 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
运行结果如下:
加密表中数据
加密表中数据只需要按照以下几步走即可:
- 使用CREATE MASTER KEY创建数据库主密钥.
- 使用CREATE CERTIFUCATE创建一个证书,用于加密对称秘钥.
- 使用CREATE SYMMETRIC KEY创建对称秘钥,采用2中的证书和适当的对称秘钥算法进行加密.
- 修改表的结构,使用ALTER TABLE增加一个用于存放加密数据的列.
- 使用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
运行结果如下:
以上就是关于表中数据操纵的基本内容,下篇会接着讨论索引和查询优化的问题.
上一篇: 表