一、SQL Server体系结构
1.1 数据库的物理布局
数据库在物理上由数据文件和事务日志文件组成,每个数据库必须至少有一个数据文件和一个日志文件。
(1)数据文件用于保存数据库对象数据。数据库必须至少有一个主文件组(Primary),而用户定义的文件组则是可选的。Primary文件组包括 主数据文件(.mdf),以及数据库的系统目录(catalog)。可以选择性地为Primary增加多个辅助数据文件(.ndf)。用户定义的文件组只能包含辅助数据文件。
(2)日志文件则用于保存SQL Server为了维护事务而需要的信息。虽然SQL Server可以同时写多个数据文件,但同一时刻只能以顺序方式写一个日志文件。
.mdf、.ldf和.ndf
.mdf代表Master Data File,.ldf代表Log Data File,而.ndf代表Not Master Data File(非主数据文件)
1.2 架构(Schema)和对象
一个数据库包含多个架构,而每个架构又包括多个对象。可以将架构看作是各种对象的容器,这些对象可以是表(table)、视图(view)、存储过程(stored procedure)等等。
此外,架构也是一个命名空间,用作对象名称的前缀。例如,架设在架构Sales中有一个Orders表,架构限定的对象名称是Sales.Orders。如果在引用对象时省略架构名称,SQL Server将采用一定的办法来分析出架构名称是什么。如果不显示指定架构,那么在解析对象名称时,就会要付出一些没有意义的额外代价。因此,建议都加上架构名称。
二、查询
2.1 单表查询
(1)关于SELECT子句:使用*号是糟糕的习惯
SELECT * FROM Sales.Shippers;
在绝大多数情况下,使用星号是一种糟糕的编程习惯,在此还是建议大家即使需要查询表的所有列,也应该显式地指定它们。
(2)关于FROM子句:显示指定架构名称
通过显示指定架构名称,可以保证得到的对象的确是你原来想要的,而且还不必付出任何额外的代价。
(3)关于TOP子句:T-SQL独有关键字
① 可以使用PERCENT关键字按百分比计算满足条件的行数
SELECT TOP (1) PERCENT orderid, orderdate, custid, empid
FROM Sales.Orders
ORDER BY orderdate DESC;
上面这条SQL就会请求最近更新过的前1%个订单。
② 可以使用WITH TIES选项请求返回所有具有相同结果的行
SELECT TOP (5) WITH TIES orderid, orderdate, custid, empid
FROM Sales.Orders
ORDER BY orderdate DESC;
上面这条SQL请求返回与TOP n行中最后一行的排序值相同的其他所有行。
(4)关于OVER子句:为行定义一个窗口以便进行特定的运算
OVER子句的优点在于能够在返回基本列的同时,在同一行对它们进行聚合;也可以在表达式中混合使用基本列和聚合值列。
例如,下面的查询为OrderValues的每一行计算当前价格占总价格的百分比,以及当前价格占客户总价格的百分比 。
SELECT orderid, custid, val,
100.0 * val / SUM(val) OVER() AS pctall,
100.0 * val / SUM(val) OVER(PARTITION BY custid) AS pctcust
FROM Sales.OrderValues;
(5)子句的逻辑处理顺序
(6)运算符的优先级
(7)CASE表达式
① 简单表达式:将一个值与一组可能的取值进行比较,并返回满足第一个匹配的结果;
SELECT productid,productname,categoryid,categoryname=(
CASE categoryid
WHEN 1 THEN 'Beverages'
WHEN 2 THEN 'Condiments'
WHEN 3 THEN 'Confections'
WHEN 4 THEN 'Dairy Products'
ELSE 'Unkonw Category'
END)
FROM Production.Products;
② 搜索表达式:将返回结果为TRUE的第一个WHEN逻辑表达式所关联的THEN子句中指定的值。如果没有任何WHEN表达式结果为TRUE,CASE表达式则返回ELSE子句中出现的值。(如果没有指定ELSE,则默认返回NULL);
SELECT orderid, custid, val, valuecategory=(
CASE
WHEN val < 1000.00 THEN 'Less than 1000'
WHEN val BETWEEN 1000.00 AND 3000.00 THEN 'Between 1000 and 3000'
WHEN val > 3000.00 THEN 'More than 3000'
ELSE 'Unknown'
END
)
FROM Sales.OrderValues
(8)三值谓词逻辑:TRUE、FALSE与UNKNOWN
SQL支持使用NULL表示缺少的值,它使用的是三值谓词逻辑,代表计算结果可以使TRUE、FALSE与UNKNOWN。在SQL中,对于UNKNOWN和NULL的处理不一致,这就需要我们在编写每一条查询语句时应该明确地注意到正在使用的是三值谓词逻辑。
例如,我们要请求返回region列不等于WA的所有行,则需要在查询过滤条件中显式地增加一个队NULL值得测试:
SELECT custid, country, region, city
FROM Sales.Customers
WHERE region <> N'WA'
OR region IS NULL;
另外,T-SQL对于NULL值得处理是先输出NULL值再输出非NULL值得顺序,如果想要先输出非NULL值,则需要改变一下排序条件,例如下面的请求:
select custid, region
from sales.Customers
order by (case
when region is null then 1 else 0
end), region;
当region列为NULL时返回1,否则返回0。非NULL值得表达式返回值为0,因此,它们会排在NULL值(表达式返回1)的前面。如上所示的将CASE表达式作为第一个拍序列,并把region列指定为第二个拍序列。这样,非NULL值也可以正确地参与排序,是一个完整解决方案的查询。
(9)LIKE谓词的花式用法
① %(百分号)通配符
SELECT empid, lastname
FROM HR.Employees
WHERE lastname LIKE N'D%';
② _(下划线)通配符:下划线代表任意单个字符
下面请求返回lastname第二个字符为e的所有员工
SELECT empid, lastname
FROM HR.Employees
WHERE lastname LIKE N'_e%';
③ [<字符列>]通配符:必须匹配指定字符中的一个字符
下面请求返回lastname以字符A、B、C开头的所有员工
SELECT empid, lastname
FROM HR.Employees
WHERE lastname LIKE N'[A-E]%';
⑤ [^<字符-字符>]通配符:不属于特定字符序列或范围内的任意单个字符
下面请求返回lastname不以A到E开头的所有员工:
SELECT empid, lastname
FROM HR.Employees
WHERE lastname LIKE N'[^A-E]%';
⑥ ESCAPE转义字符
如果搜索包含特殊通配符的字符串(例如'%','_','['、']'等),则必须使用转移字符。下面检查lastname列是否包含下划线:
SELECT empid, lastname
FROM HR.Employees
WHERE lastname LIKE N'%!_%' ESCAPE '!';
(10)两种转换值的函数:CAST和CONVERT
CAST和CONVERT都用于转换值的数据类型。
SELECT CAST(SYSDATETIME() AS DATE);
SELECT CONVERT(CHAR(8),CURRENT_TIMESTAMP,112);
需要注意的是,CAST是ANSI标准的SQL,而CONVERT不是。所以,除非需要使用样式值,否则推荐优先使用CAST函数,以保证代码尽可能与标准兼容。
2.2 联接查询
(1)交叉联接:返回笛卡尔积,即m*n行的结果集
-- CROSS JOIN
select c.custid, e.empid
from sales.Customers as c
cross join HR.Employees as e;
-- INNER CROSS JOIN
select e1.empid,e1.firstname,e1.lastname,
e2.empid,e2.firstname,e2.lastname
from hr.Employees as e1
cross join hr.Employees as e2;
(2)内联接:先笛卡尔积,然后根据指定的谓词对结果进行过滤
select e.empid,e.firstname,e.lastname,o.orderid
from hr.Employees as e
join sales.Orders as o
on e.empid=o.empid;
(3)外联结:笛卡尔积→对结果过滤→添加外部行
通过例子来理解外联结:根据客户的客户ID和订单的客户ID来对Customers表和Orders表进行联接,并返回客户和他们的订单信息。该查询语句使用的联接类型是左外连接,所以查询结果也包括那些没有发出任何订单的客户;
--LEFT OUTER JOIN
select c.custid,c.companyname,o.orderid
from sales.Customers as c
left outer join sales.Orders as o
on c.custid=o.custid;
另外,需要注意的是在对外联结中非保留值得列值进行过滤时,不要再WHERE子句中指定错误的查询条件。
2.3 子查询
(1)独立子查询:不依赖于它所属的外部查询
例如下面要查询Orders表中订单ID最大的订单信息,这种叫做独立标量子查询,即返回值不能超过一个。
select orderid, orderdate, empid, custid
from sales.Orders
where empid=(select MAX(o.orderid) from sales.Orders as o);
select orderid
from sales.Orders
where empid in (select e.empid
from hr.Employees as e
where e.lastname like N'D%');
(2)相关子查询:必须依赖于它所属的外部查询,不能独立地调用它
例如下面的查询会返回每个客户的订单记录中订单ID最大的记录:
select custid, orderid, orderdate, empid
from sales.Orders as o1
where orderid=(select MAX(o2.orderid)
from sales.Orders as o2
where o2.custid=o1.custid);
简单地说,对于o1表中的每一行,子查询负责返回当前客户的最大订单ID。如果o1表中某行的订单ID和子查询返回的订单ID匹配,那么o1中的这个订单ID就是当前客户的最大订单ID,在这种情况下,查询便会返回o1表中的这个行。
(3)EXISTS谓词:它的输入是一个查询,如果子查询能够返回任何行,则返回True,否则返回False
例如下面的查询会返回下过订单的西班牙客户:
select custid, companyname
from sales.customers as c
where c.country=N'Spain' and exists (
select * from sales.Orders as o
where o.custid=c.custid);
同样,要查询没有下过订单的西班牙客户只需要加上NOT即可:
select custid, companyname
from sales.customers as c
where c.country=N'Spain' and not exists (
select * from sales.Orders as o
where o.custid=c.custid);
对于EXISTS,它采用的是二值逻辑(TRUE和FALSE),它只关心是否存在匹配行,而不考虑SELECT列表中指定的列,并且无须处理所有满足条件的行。可以将这种处理方式看做是一种“短路”,它能够提高处理效率。
另外,由于EXISTS采用的是二值逻辑,因此相较于IN要更加安全,可以避免对NULL值得处理。
(4)高级子查询
① 如何表示前一个或后一个记录?逻辑等式:上一个->小于当前值的最大值;下一个->大于当前值的最小值;
-- 上一个订单ID
select orderid, orderdate, empid, custid,
(
select MAX(o2.orderid)
from sales.Orders as o2
where o2.orderid<o1.orderid
) as prevorderid
from sales.Orders as o1;
② 如何实现连续聚合函数?在子查询中连续计算
-- 连续聚合
select orderyear, qty,
(select SUM(o2.qty)
from sales.OrderTotalsByYear as o2
where o2.orderyear<=o1.orderyear) as runqty
from sales.OrderTotalsByYear as o1
order by orderyear;
执行结果如下图所示:
③ 使用NOT EXISTS谓词取代NOT IN隐式排除NULL值:当对至少返回一个NULL值的子查询使用NOT IN谓词时,外部查询总会返回一个空集。(前面提到,EXISTS谓词采用的是二词逻辑而不是三词逻辑)
-- 隐式排除NULL值
select custid,companyname from sales.Customers as c
where not exists
(select *
from sales.Orders as o
where o.custid=c.custid);
又如以下查询请求返回每个客户在2007年下过订单而在2008年没有下过订单的客户:
select custid, companyname
from sales.Customers as c
where exists
(select * from sales.Orders as o1
where c.custid=o1.custid
and o1.orderdate>='20070101' and o1.orderdate<'20080101')
and not exists
(select * from sales.Orders as o2
where c.custid=o2.custid
and o2.orderdate>='20080101' and o2.orderdate<'20090101');
三、表表达式
表表达式是一种命名的查询表达式,代表一个有效地关系表。可以像其他表一样,在数据处理中使用表表达式。MSSQL中支持4种类型的表表达式:
3.1 派生表
派生表(也称为表子查询)是在外部查询的FROM子句中定义的,只要外部查询一结束,派生表也就不存在了。
例如下面代码定义了一个名为USACusts的派生表,它是一个返回所有美国客户的查询。外部查询则选择了派生表的所有行。
select *
from (select custid, companyname
from sales.Customers
where country='USA') as USACusts;
3.2 公用表表达式
公用表达式(简称CTE,Common Table Expression)是和派生表很相似的另一种形式的表表达式,是ANSI SQL(1999及以后版本)标准的一部分。
举个栗子,下面的代码定义了一个名为USACusts的CTE,它的内部查询返回所有来自美国的客户,外部查询则选择了CTE中的所有行:
WITH USACusts AS
(
select custid, companyname
from sales.Customers
where country=N'USA'
)
select * from USACusts;
和派生表一样,一旦外部查询完成,CTE的生命周期也就结束了。
3.3 视图
派生表和CTE都是不可重用的,而视图和内联表值函数却是可重用,它们的定义存储在一个数据库对象中,一旦创建,这些对象就是数据库的永久部分。只有用删除语句显式地删除,它们才会从数据库中移除。
下面仍然继续上面的例子,创建一个视图:
IF OBJECT_ID('Sales.USACusts') IS NOT NULL
DROP VIEW Sales.USACusts;
GO
CREATE VIEW Sales.USACusts
AS
SELECT
custid, companyname, contactname, contacttitle, address,
city, region, postalcode, country, phone, fax
FROM Sales.Customers
WHERE country=N'USA';
GO
使用该视图:
SELECT * FROM Sales.USACusts;
执行结果如下:
3.4 内联表值函数
内联表值函数能够支持输入参数,其他方面就与视图类似了。
下面演示如何创建函数:
IF OBJECT_ID('dbo.fn_GetCustOrders') IS NOT NULL
DROP FUNCTION dbo.fn_GetCustOrders;
GO
CREATE FUNCTION dbo.fn_GetCustOrders
(@cid AS INT) RETURNS TABLE
AS
RETURN
SELECT
orderid, custid, empid, orderdate, requireddate,
shippeddate, shipperid, freight, shipname, shipaddress, shipcity,
shipregion, shippostalcode, shipcountry
FROM Sales.Orders
WHERE custid=@cid;
GO
如何使用函数:
SELECT orderid, custid
FROM dbo.fn_GetCustOrders(1) AS CO;
执行结果如下:
总结:
借助表表达式可以简化代码,提高代码地可维护性,还可以封装查询逻辑。
当需要使用表表达式,而且不计划重用它们的定义时,可以使用派生表或CTE,与派生表相比,CTE更加模块化,更容易维护。
当需要定义可重用的表表达式时,可以使用视图或内联表值函数。如果不需要支持输入,则使用视图;反之,则使用内联表值函数。
四、集合运算
4.1 UNION 并集运算
在T-SQL中。UNION集合运算可以将两个输入查询的结果组合成一个结果集。需要注意的是:如果一个行在任何一个输入集合众出现,它也会在UNION运算的结果中出现。T-SQL支持以下两种选项:
(1)UNION ALL:不会删除重复行
-- union all
select country, region, city from hr.Employees
union all
select country, region, city from sales.Customers;
结果得到100行:
(2)UNION:会删除重复行
-- union
select country, region from hr.Employees
union
select country, region from sales.Customers;
结果得到34行:
4.2 INTERSECT 交集运算
在T-SQL中,INTERSECT集合运算对两个输入查询的结果取其交集,只返回在两个查询结果集中都出现的行。
INTERSECT集合运算在逻辑上会首先删除两个输入集中的重复行,然后返回只在两个集合中中都出现的行。换句话说:如果一个行在两个输入集中都至少出现一次,那么交集返回的结果中将包含这一行。
例如,下面返回既是官员地址,又是客户地址的不同地址:
-- intersect
select country, region, city from hr.Employees
intersect
select country, region, city from sales.Customers;
执行结果如下图所示:
这里需要说的是,集合运算对行进行比较时,认为两个NULL值相等,所以就返回该行记录。
4.3 EXCEPT 差集运算
在T-SQL中,集合之差使用EXCEPT集合运算实现的。它对两个输入查询的结果集进行操作,反会出现在第一个结果集中,但不出现在第二个结果集中的所有行。
EXCEPT结合运算在逻辑上首先删除两个输入集中的重复行,然后返回只在第一个集合中出现,在第二个结果集中不出现的所有行。换句话说:一个行能够被返回,仅当这个行在第一个输入的集合中至少出现过一次,而且在第二个集合中一次也没出现过。
此外,相比UNION和INTERSECT,两个输入集合的顺序是会影响到最后返回结果的。
例如,借助EXCEPT运算,我们可以方便地实现属于A但不属于B的场景,下面返回属于员工抵制,但不属于客户地址的地址记录:
-- except
select country, region, city from hr.Employees
except
select country, region, city from sales.Customers;
执行结果如下图所示:
4.4 集合运算优先级
SQL定义了集合运算之间的优先级:INTERSECT最高,UNION和EXCEPT相等。
换句话说:首先会计算INTERSECT,然后按照从左至右的出现顺序依次处理优先级相同的运算。
-- 集合运算的优先级
select country, region, city from Production.Suppliers
except
select country, region, city from hr.Employees
intersect
select country, region, city from sales.Customers;
上面这段SQL代码,因为INTERSECT优先级比EXCEPT高,所以首先进行INTERSECT交集运算。因此,这个查询的含义是:返回没有出现在员工地址和客户地址交集中的供应商地址。
4.5 使用表表达式避开不支持的逻辑查询处理
集合运算查询本身并不持之除ORDER BY意外的其他逻辑查询处理阶段,但可以通过表表达式来避开这一限制。
解决方案就是:首先根据包含集合运算的查询定义一个表表达式,然后在外部查询中对表表达式应用任何需要的逻辑查询处理。
(1)例如,下面的查询返回每个国家中不同的员工地址或客户地址的数量:
select country, COUNT(*) as numlocations
from (select country, region, city from hr.Employees
union
select country, region, city from sales.Customers) as U
group by country;
(2)例如,下面的查询返回由员工地址为3或5的员工最近处理过的两个订单:
select empid,orderid,orderdate
from (select top (2) empid,orderid,orderdate
from sales.Orders
where empid=3
order by orderdate desc,orderid desc) as D1
union all
select empid,orderid,orderdate
from (select top (2) empid,orderid,orderdate
from sales.Orders
where empid=5
order by orderdate desc,orderid desc) as D2;
五、透视、逆透视及分组
所谓透视(Pivoting)就是把数据从行的状态旋转为列的状态的处理。其处理步骤为:
相信很多人在笔试或面试的时候被问到如何通过SQL实现行转列或列转行的问题,可能很多人当时懵逼了,没关系,下面我们通过例子来理解。
(1)准备数据
--1.0准备数据
USE tempdb;
IF OBJECT_ID('dbo.Orders', 'U') IS NOT NULL DROP TABLE dbo.Orders;
GO
CREATE TABLE dbo.Orders
(
orderid INT NOT NULL,
orderdate DATE NOT NULL, -- prior to SQL Server 2008 use DATETIME
empid INT NOT NULL,
custid VARCHAR(5) NOT NULL,
qty INT NOT NULL,
CONSTRAINT PK_Orders PRIMARY KEY(orderid)
);
INSERT INTO dbo.Orders(orderid, orderdate, empid, custid, qty)
VALUES
(30001, '20070802', 3, 'A', 10),
(10001, '20071224', 2, 'A', 12),
(10005, '20071224', 1, 'B', 20),
(40001, '20080109', 2, 'A', 40),
(10006, '20080118', 1, 'C', 14),
(20001, '20080212', 2, 'B', 12),
(40005, '20090212', 3, 'A', 10),
(20002, '20090216', 1, 'C', 20),
(30003, '20090418', 2, 'B', 15),
(30004, '20070418', 3, 'C', 22),
(30007, '20090907', 3, 'D', 30);
SELECT * FROM dbo.Orders;
这里使用了MS SQL2008的VALUES子句格式语法,这时2008版本的新特性。如果你使用的是2005及以下版本,你需要多个INSERT语句。最后的执行结果如下图所示:
(2)需求说明
假设我们要生成一个报表,包含每个员工和客户组合之间的总订货量。用以下简单的分组查询可以解决这个问题:
select empid,custid,SUM(qty) as sumqty
from dbo.Orders
group by empid,custid;
该查询的执行结果如下:
不过,假设现在要求要按下表所示的的格式来生成输出结果:
这时,我们就需要进行透视转换了!
(3)使用标准SQL进行透视转换
Step1.分组:GROUP BY empid;
Step2.扩展:CASE WHEN custid='A' THEN qty END;
Step3.聚合:SUM(CASE WHEN custid='A' THEN qty END);
--1.1标准SQL透视转换
select empid,
SUM(case when custid='A' then qty end) as A,
SUM(case when custid='B' then qty end) as B,
SUM(case when custid='C' then qty end) as C,
SUM(case when custid='D' then qty end) as D
from dbo.Orders
group by empid;
执行结果如下图所示:
(4)使用T-SQL PIVOT运算符进行透视转换
自SQL Server 2005开始引入了一个T-SQL独有的表运算符-PIVOT,它可以对某个源表或表表达式进行操作、透视数据,再返回一个结果表。
PIVOT运算符同样涉及前面介绍的三个逻辑处理阶段(分组、扩展和聚合)以及同样的透视转换元素,但使用的是不同的、SQL Server原生的语法。
下面是使用PIVOT运算符实现上面一样的效果:
select empid,A,B,C,D
from (select empid,custid,qty
from dbo.Orders) as D
pivot (sum(qty) for custid in (A,B,C,D)) as P;
其中,PIVOT运算符的圆括号内要指定聚合函数(本例中SUM)、聚合元素(本例中的qty)、扩展元素(custid)以及目标列名称的列表(本例中的A、B、C、D)。在PIVOT运算符的圆括号后面,可以为结果表制定一个别名。
Tip:使用PIVOT运算符一般不直接把它应用到源表(本例中的Orders表),而是将其应用到一个表表达式(该表表达式只包含透视转换需要的3种元素,不包含其他属性。)此外,不需要为它显式地指定分组元素,也就不需要再查询中使用GROUP BY子句。
5.2 逆透视
所谓逆透视(Unpivoting)转换是一种把数据从列的状态旋转为行的状态的技术,它将来自单个记录中多个列的值扩展为单个列中具有相同值得多个记录。换句话说,将透视表中的每个源行潜在地转换成多个行,每行代表源透视表的一个指定的列值。
还是通过一个栗子来理解:
(1)首先还是准备一下数据:
USE tempdb;
IF OBJECT_ID('dbo.EmpCustOrders', 'U') IS NOT NULL DROP TABLE dbo.EmpCustOrders;
SELECT empid, A, B, C, D
INTO dbo.EmpCustOrders
FROM (SELECT empid, custid, qty
FROM dbo.Orders) AS D
PIVOT(SUM(qty) FOR custid IN(A, B, C, D)) AS P;
SELECT * FROM dbo.EmpCustOrders;
下面是对这个表EmpCustOrders的查询结果:
(2)需求说明
要求执行你透视转换,为每个员工和客户组合返回一行记录,其中包含这一组合的订货量。期望的输出结果如下图所示:
(3)标准SQL进行逆透视转换
Step1.生成副本:CROSS JOIN 交叉联接生成多个副本
Step2.提取元素:通过CASE语句生成qty数据列
Step3.删除不相关的交叉:过滤掉NULL值
select *
from (select empid, custid,
case custid
when 'A' then A
when 'B' then B
when 'C' then C
when 'D' then D
end as qty
from dbo.EmpCustOrders
cross join (VALUES('A'),('B'),('C'),('D')) as Custs(custid)) as D
where qty is not null;
执行结果如下图所示:
(4)T-SQL UNPIVOT运算符进行逆透视转换
和PIVOT类似,在SQL Server 2005引入了一个UNPIVOT运算符,它的作用刚好和PIVOT运算符相反,即我们可以拿来做逆透视转换工作。UNPIVOT同样会经历我们上面提到的三个阶段。继续上面的栗子,我们使用UNPIVOT来进行逆透视转换:
select empid, custid, qty
from dbo.EmpCustOrders
unpivot (qty for custid in (A,B,C,D)) as U;
其中,UNPIVOT运算符后边的括号内包括:用于保存源表列值的目标列明(这里是qty),用于保存源表列名的目标列名(这里是custid),以及源表列名列表(A、B、C、D)。同样,在UNPIVOT括号后面也可以跟一个别名。
Tip:对经过透视转换所得的表再进行逆透视转换,并不能得到原来的表。因为你透视转换只是把经过透视转换的值再旋转岛另一种新的格式。
5.3 分组
首先了解一下分组集:分组集就是分组(GROUP BY子句)使用的一组属性(或列名)。在传统SQL中,一个聚合查询只能定义一个分组集。为了灵活而有效地处理分组集,SQL Server 2008引入了几个重要的新功能(他们都是GROUP BY的从属子句,需要依赖于GROUP BY子句):
(1)GROUPING SETS从属子句
使用该子句,可以方便地在同一个查询中定义多个分组集。例如下面,我们定义了4个分组集:(empid,custid),(empid),(custid)和():
--3.1GROUPING SETS从属子句
select empid,custid,SUM(qty) as sumqty
from dbo.Orders
group by
GROUPING SETS
(
(empid,custid),
(empid),
(custid),
()
);
这个查询相当于执行了四个group by查询的并集。
(2)CUBE从属子句
CUBE子句为定义多个分组集提供了一种更简略的方法,可以把CUBE子句看作是用于生成分组的幂集。例如:CUBE(a,b,c)等价于GROUPING SETS[(a,b,c),(a,b),(a,c),(b,c),(a),(b),(c),()]。下面我们用CUBE来实现上面的例子:
--3.2CUEE从属子句
select empid,custid,SUM(qty) as sumqty
from dbo.Orders
group by cube(empid,custid);
(3)ROLLUP从属子句
ROLLUP子句也是一种简略的方法,只不过它与CUBE不同,它强调输入成员之间存在一定的层次关系,从而生成让这种层次关系有意义的所有分组集。例如:CUBE(a,b,c)会生成8个可能的分组集,而ROLLUP则认为3个输入成员存在a>b>c的层次关系,所以只会生成4个分组集:(a,b,c),(a,b),(a),()。
下面我们假设想要按时间层次关系:订单年份>订单月份>订单日,以这样的关系来定义所有分组集,并未每个分组集返回其总订货量。可能我们用GROUPING SETS需要4行,然后使用ROLLUP却只需要一行:group by rollup(YEAR(orderdate),MONTH(orderdate),DAY(orderdate));
完整SQL查询如下:
--3.3ROLLUP从属子句
select
YEAR(orderdate) as orderyear,
MONTH(orderdate) as ordermonth,
DAY(orderdate) as orderday,
SUM(qty) as sumqty
from dbo.Orders
group by rollup(YEAR(orderdate),MONTH(orderdate),DAY(orderdate));
执行结果如下图所示:
(4)GROUPING_ID函数
如果一个查询定义了多个分组集,还想把结果行和分组集关联起来,也就是说,为每个结果行标注它是和哪个分组集关联的。SQL Server 2008中引入了一个GROUPING_ID函数,简化了关联结果行和分组集的处理,可以容易地计算出每一行和哪个分组集相关联。
例如,继续上面的例子,我们想要将empid,custid作为输入:
select
grouping_id(empid,custid) as groupingset,
empid, custid, SUM(qty) as sumqty
from dbo.Orders
group by cube(empid,custid);
执行结果中会出现groupingset为0,1,2,3,分别代表了empid,custid的4个可能的分组集((empid,custid),(empid),(custid),())。
六、数据修改
6.1 插入与删除数据
6.1.1 看我花式插入数据
① INSERT VALUES语句 :这个语句恐怕我们再熟悉不过了把,在任何一本数据库的书上面都可以看到这个语句的身影。
INSERT INTO dbo.Orders(orderid, orderdate, empid, custid)
VALUES(10001, '20090212', 3, 'A');
需要了解的是,前面也提到过,SQL Server 2008增强了VALUES语句的功能,允许在一条语句中指定由逗号分隔开的多行记录。例如下面的语句向Orders中插入了4行数据:
INSERT INTO dbo.Orders
(orderid, orderdate, empid, custid)
VALUES
(10003, '20090213', 4, 'B'),
(10004, '20090214', 1, 'A'),
(10005, '20090213', 1, 'C'),
(10006, '20090215', 3, 'C');
② INSERT SELECT语句 :将一组由SELECT查询返回的结果行插入到目标表中。
INSERT INTO dbo.Orders(orderid, orderdate, empid, custid)
SELECT orderid, orderdate, empid, custid
FROM TSQLFundamentals2008.Sales.Orders
WHERE shipcountry = 'UK';
③ INSERT EXEC语句:将存储过过程或动态SQL批处理返回的结果集插入目标表。
下面的示例演示了如何执行存储过程usp_getorders并将结果插入到Orders表中:
INSERT INTO dbo.Orders(orderid, orderdate, empid, custid)
EXEC TSQLFundamentals2008.Sales.usp_getorders @country = 'France';
④ SELECT INTO语句:它会创建一个目标表,并用查询返回的结果来填充它。需要注意的是:它不是一个标准的SQL语句(即不是ANSI SQL标准的一部分),不能用这个语句向已经存在的表中插入数据。
--保证目标表不存在
IF OBJECT_ID('dbo.Orders', 'U') IS NOT NULL DROP TABLE dbo.Orders;
SELECT orderid, orderdate, empid, custid
INTO dbo.Orders
FROM TSQLFundamentals2008.Sales.Orders;
⑤ BULK INSERT语句:用于将文件中的数据导入一个已经存在的表,需要制定目标表、源文件以及一些其他的选项。
下面的栗子演示了如何将文件"C:\testdata\orders.txt"中的数据容量插入(bulk insert)到Orders表,同时还指定了文件类型为字符格式,字段终止符为逗号,行终止符为换行符(\t):
BULK INSERT dbo.Orders FROM 'C:\testdata\orders.txt'
WITH
(
DATAFILETYPE = 'char',
FIELDTERMINATOR = ',',
ROWTERMINATOR = '\n'
);
6.1.2 看我花式删除数据
① DELETE语句:标准SQL语句,大家最常见的用法。
DELETE FROM dbo.Orders
WHERE orderdate < '20070101';
② TRUNCATE语句:不是标准的SQL语句,永于删除表中的所有行,不需要过滤条件。
Tip:TRUNCATE与DELETE在性能上差异巨大,对一个百万行级记录的表,TRUNCATE几秒内就可以解决,而DELETE可能需要几分钟。因为TRUNCATE会以最小模式记录日志,而DELETE则以完整模式记录日志。所以,各位,谨慎使用TRUNCATE。因此,我们可以创建一个虚拟表(Dummy Table),让虚拟表包含一个指向产品表的外键,这样就可以保护产品表了。
③ 基于联接的DELETE:也不是标准SQL语句,可以根据另一个表中相关行的属性定义的过滤器来删除表中的数据行。
例如,下面语句用以删除美国客户下的订单:
DELETE FROM O
FROM dbo.Orders AS O
JOIN dbo.Customers AS C
ON O.custid = C.custid
WHERE C.country = N'USA';
当然,如果要使用标准SQL语句,也可以采用下面的方式:
DELETE FROM dbo.Orders
WHERE EXISTS
(SELECT *
FROM dbo.Customers AS C
WHERE Orders.custid = C.custid
AND C.country = N'USA');
6.2 更新与合并数据
6.2.1 花式更新数据
① UPDATE语句:不解释了,大家都在用
下面来看两个不一样的栗子,第一个是关于同时操作的性质。看看下面的UPDATE语句:
UPDATE dbo.T1
SET col1 = col1 + 10, col2 = col1 + 10;
假设T1表中的col1列为100,col2列为200。在计算后是多少呢?
答案揭晓:col=110,col=110。
再来看一个栗子,假设我们要实现两个数的交换该怎么做?我们可能迫不及待的说出临时变量。然而,在SQL中所有赋值表达式好像都是同时计算的,解决这个问题就不需要临时变量了。
UPDATE dbo.T1
SET col1 = col2, col2 = col1;
② 基于联接的UPDATE语句:同样不是SQL标准语法,联接在此与基于联接的DELETE一样是起到过滤作用。
UPDATE OD
SET discount = discount + 0.05
FROM dbo.OrderDetails AS OD
JOIN dbo.Orders AS O
ON OD.orderid = O.orderid
WHERE custid = 1;
同样,要使用标准SQL语法的话,可以用子查询替代联接:
UPDATE dbo.OrderDetails
SET discount = discount + 0.05
WHERE EXISTS
(SELECT * FROM dbo.Orders AS O
WHERE O.orderid = OrderDetails.orderid
AND custid = 1);
③ 赋值UPDATE:这是T-SQL特有的语法,可以对表中的数据进行更新的同时为变量赋值。你不需要使用单独的UPDATE和SELECT语句,就能完成同样的任务。
假设我们有一个表Sequence,它只有一列val,全是序号数字。我们可以通过赋值UPDATE得到一个新的序列值:
DECLARE @nextval AS INT;
UPDATE Sequence SET @nextval = val = val + 1;
SELECT @nextval;
6.2.2 新玩法:合并数据
SQL Server 2008引入了一个叫做MERGE的语句,它能在一条语句中根据逻辑条件对数据进行不同的修改操作(INSERT/UPDATE/DELETE)。MERGE语句是SQL标准的一部分,而T-SQL版本的MERGE语句也增加了一些非标准的扩展。
下面我们看看如何合并,首先我们准备两张表Customers和CustomersStage:
--merge data
USE tempdb;
IF OBJECT_ID('dbo.Customers', 'U') IS NOT NULL DROP TABLE dbo.Customers;
GO
CREATE TABLE dbo.Customers
(
custid INT NOT NULL,
companyname VARCHAR(25) NOT NULL,
phone VARCHAR(20) NOT NULL,
address VARCHAR(50) NOT NULL,
CONSTRAINT PK_Customers PRIMARY KEY(custid)
);
INSERT INTO dbo.Customers(custid, companyname, phone, address)
VALUES
(1, 'cust 1', '(111) 111-1111', 'address 1'),
(2, 'cust 2', '(222) 222-2222', 'address 2'),
(3, 'cust 3', '(333) 333-3333', 'address 3'),
(4, 'cust 4', '(444) 444-4444', 'address 4'),
(5, 'cust 5', '(555) 555-5555', 'address 5');
IF OBJECT_ID('dbo.CustomersStage', 'U') IS NOT NULL DROP TABLE dbo.CustomersStage;
GO
CREATE TABLE dbo.CustomersStage
(
custid INT NOT NULL,
companyname VARCHAR(25) NOT NULL,
phone VARCHAR(20) NOT NULL,
address VARCHAR(50) NOT NULL,
CONSTRAINT PK_CustomersStage PRIMARY KEY(custid)
);
INSERT INTO dbo.CustomersStage(custid, companyname, phone, address)
VALUES
(2, 'AAAAA', '(222) 222-2222', 'address 2'),
(3, 'cust 3', '(333) 333-3333', 'address 3'),
(5, 'BBBBB', 'CCCCC', 'DDDDD'),
(6, 'cust 6 (new)', '(666) 666-6666', 'address 6'),
(7, 'cust 7 (new)', '(777) 777-7777', 'address 7');
-- Query tables
SELECT * FROM dbo.Customers;
SELECT * FROM dbo.CustomersStage;
执行结果如下图所示:
现在我们想要增加还不存在的客户,并更新已经存在的客户。源表:CustomersStage,目标表:Customers。
MERGE INTO dbo.Customers AS TGT
USING dbo.CustomersStage AS SRC
ON TGT.custid = SRC.custid
WHEN MATCHED THEN
UPDATE SET
TGT.companyname = SRC.companyname,
TGT.phone = SRC.phone,
TGT.address = SRC.address
WHEN NOT MATCHED THEN
INSERT (custid, companyname, phone, address)
VALUES (SRC.custid, SRC.companyname, SRC.phone, SRC.address);
Tips:MERGE语句必须以分号结束,而对于T-SQL中的大多数其他语句来说是可选的。但是,推荐遵循最佳实践,以分号结束。
6.3 高级数据更新方法
① 通过表表达式修改数据
-- 基于联接的UPDATE
UPDATE OD
SET discount = discount + 0.05
FROM dbo.OrderDetails AS OD
JOIN dbo.Orders AS O
ON OD.orderid = O.orderid
WHERE custid = 1;
-- 基于表表达式(这里是CTE)的UPDATE
WITH C AS
(
SELECT custid, OD.orderid,
productid, discount, discount + 0.05 AS newdiscount
FROM dbo.OrderDetails AS OD
JOIN dbo.Orders AS O
ON OD.orderid = O.orderid
WHERE custid = 1
)
UPDATE C
SET discount = newdiscount;
② 带有TOP选项的数据更新
-- 删除前50行
DELETE TOP(50) FROM dbo.Orders;
-- 更新前50行
UPDATE TOP(50) dbo.Orders
SET freight = freight + 10.00;
-- 基于CTE删除前50行
WITH C AS
(
SELECT TOP(50) *
FROM dbo.Orders
ORDER BY orderid
)
DELETE FROM C;
-- 基于CTE更新前50行
WITH C AS
(
SELECT TOP(50) *
FROM dbo.Orders
ORDER BY orderid DESC
)
UPDATE C
SET freight = freight + 10.00;
6.4 OUTPUT子句
在某些场景中,我们希望能够从修改过的行中返回数据,这时就可以使用OUTPUT子句。SQL Server 2005引入了OUTPUT子句,通过在修改语句中添加OUTPUT子句,就可以实现从修改语句中返回数据的功能。
① 带有OUTPUT的INSERT语句
INSERT INTO dbo.T1(datacol)
OUTPUT inserted.keycol, inserted.datacol
SELECT lastname
FROM TSQLFundamentals2008.HR.Employees
WHERE country = N'USA';
② 带有OUTPUT的DELETE语句
DELETE FROM dbo.Orders
OUTPUT
deleted.orderid,
deleted.orderdate,
deleted.empid,
deleted.custid
WHERE orderdate < '20080101';
③ 带有OUTPUT的UPDATE语句
UPDATE dbo.OrderDetails
SET discount = discount + 0.05
OUTPUT
inserted.productid,
deleted.discount AS olddiscount,
inserted.discount AS newdiscount
WHERE productid = 51;
④ 带有OUTPUT的MERGE语句
MERGE INTO dbo.Customers AS TGT
USING dbo.CustomersStage AS SRC
ON TGT.custid = SRC.custid
WHEN MATCHED THEN
UPDATE SET
TGT.companyname = SRC.companyname,
TGT.phone = SRC.phone,
TGT.address = SRC.address
WHEN NOT MATCHED THEN
INSERT (custid, companyname, phone, address)
VALUES (SRC.custid, SRC.companyname, SRC.phone, SRC.address)
OUTPUT $action, inserted.custid,
deleted.companyname AS oldcompanyname,
inserted.companyname AS newcompanyname,
deleted.phone AS oldphone,
inserted.phone AS newphone,
deleted.address AS oldaddress,
inserted.address AS newaddress;
以上MERGE语句使用OUTPUT子句返回被修改过的行的新旧版本的值。对于INSERT操作不存在旧版本的值,因此所有deleted列的值都返回NULL。$action函数会告诉我们输出行是UPDATE还是由INSERT操作生成的。
七、事务和并发
考虑到这一章的内容比较多而且十分重要,故将其单独整理成一篇文章,赶紧点击这里:《Microsoft SQL Server中的事务与并发》
另外,园子里还有一些十分不错的博文,请园友们自行前往学习浏览:
(1)Jackson,《30分钟全面解析-SQL事务+隔离级别+阻塞+死锁》
(2)BIWork,《SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因》
八、可编程对象
8.1 变量与批处理
(1)变量:DECLARE+SET/SELECT
DECLARE语句可以声明一个或多个变量,然后使用SET/SELECT语句可以把一个变量设置成指定的值。
① SET语句每次只能针对一个变量进行操作
--set方式
declare @i as int
set @i=10;
--SQL Server 2008可以在同一语句同时声明和初始化变量
declare @i as int = 10;
② SELECT语句允许从同一行中获得的多个值分配给多个变量。
--select方式
declare @firstname as nvarchar(20), @lastname as nvarchar(40);
select
@firstname = firstname,
@lastname = lastname
from hr.Employees
where empid=3;
select @firstname as firstname, @lastname as lastname;
(2)批处理:客户端应用程序发送到SQL Server的一组单条或多条T-SQL语句,SQL Server将批处理语句作为单个可执行的单元。
下面是一个批处理的示例,但要注意的是如果批处理中存在语法错误,整个批处理是不会提交到SQL Server执行的。
-- A Batch as a Unit of Parsing
-- Valid batch
PRINT 'First batch';
USE TSQLFundamentals2008;
GO
-- Invalid batch
PRINT 'Second batch';
SELECT custid FROM Sales.Customers;
SELECT orderid FOM Sales.Orders; -- 这一句有语法错误,故整个批处理不能提交到SQL Server执行
GO
-- Valid batch
PRINT 'Third batch';
SELECT empid FROM HR.Employees;
GO
Tip:批处理和事务不同,事务是工作的原子工作单元,而一个批处理可以包含多个事务,一个事务也可以在多个批处理中的某些部分提交。当事务在执行中被取消或者回滚时,SQL Server会撤销自事务开始以来的部分活动,而不考虑批处理是从哪里开始的。
8.2 流程控制
(1)IF...ELSE
这个大家应该都知道,但是需要注意的是:T-SQL使用的是三值逻辑,当条件取值为FALSE或UNKNOWN时,都可以激活ELSE语句块。如果条件取值可能为FALSE或UNKNOWN(例如,涉及到NULL值),而且对每种情况需要进行不同的处理时,必须用IS NULL谓词对NULL值进行显式地测试。
下面的IF-ELSE代码演示了:如果今天是一个月的第一天,则对数据库进行完整备份;如果今天是一个月的最后一天,则对数据库进行差异备份(所谓差异备份,就是指只保存上一次完整备份以来做过的更新)。
IF DAY(CURRENT_TIMESTAMP) = 1
BEGIN
PRINT 'Today is the first day of the month.';
PRINT 'Starting a full database backup.';
BACKUP DATABASE TSQLFundamentals2008
TO DISK = 'C:\Temp\TSQLFundamentals2008_Full.BAK' WITH INIT;
PRINT 'Finished full database backup.';
END
ELSE
BEGIN
PRINT 'Today is not the first day of the month.'
PRINT 'Starting a differential database backup.';
BACKUP DATABASE TSQLFundamentals2008
TO DISK = 'C:\Temp\TSQLFundamentals2008_Diff.BAK' WITH INIT;
PRINT 'Finished differential database backup.';
END
GO
这里假设备份的文件路径目录C:Temp已经存在。
(2)WHILE:不解释了,各位应该都懂。
DECLARE @i AS INT;
SET @i = 1;
WHILE @i <= 10
BEGIN
PRINT @i;
SET @i = @i + 1;
END;
GO
8.3 游标
T-SQL中支持一种叫做游标的对象,可以用它来处理查询返回的结果集中的各行,以指定的顺序一次只处理一行。这种处理方式与使用基于集合的查询相反,普通的查询是把集合作为一个整体来处理,不依赖任何顺序。
换句话说,使用游标,就像是用鱼竿钓鱼,一次只能勾到一条鱼一样。而使用集合,就像用渔网捕鱼,一次能捕到整整一网鱼。因此,使用游标的场景我们应该多多斟酌。一般来说,如果按固定顺序一次处理一行的游标方式涉及到的数据访问要比基于集合的方式少得多,则使用游标会更加有效,前一篇提到的连续聚合就是这样的一个例子。
如何使用游标呢?
下面来看看一个实例,它使用游标来计算CustOrders视图中每个客户每个月的连续总订货量(连续聚合案例):
-- Example: Running Aggregations
SET NOCOUNT ON;
USE TSQLFundamentals2008;
DECLARE @Result TABLE
(
custid INT,
ordermonth DATETIME,
qty INT,
runqty INT,
PRIMARY KEY(custid, ordermonth)
);
DECLARE
@custid AS INT,
@prvcustid AS INT,
@ordermonth DATETIME,
@qty AS INT,
@runqty AS INT;
DECLARE C CURSOR FAST_FORWARD /* read only, forward only */ FOR
SELECT custid, ordermonth, qty
FROM Sales.CustOrders
ORDER BY custid, ordermonth;
OPEN C
FETCH NEXT FROM C INTO @custid, @ordermonth, @qty;
SELECT @prvcustid = @custid, @runqty = 0;
WHILE @@FETCH_STATUS = 0
BEGIN
IF @custid <> @prvcustid
SELECT @prvcustid = @custid, @runqty = 0;
SET @runqty = @runqty + @qty;
INSERT INTO @Result VALUES(@custid, @ordermonth, @qty, @runqty);
FETCH NEXT FROM C INTO @custid, @ordermonth, @qty;
END
CLOSE C;
DEALLOCATE C;
SELECT
custid,
CONVERT(VARCHAR(7), ordermonth, 121) AS ordermonth,
qty,
runqty
FROM @Result
ORDER BY custid, ordermonth;
GO
执行结果如下图所示:
8.4 临时表
有时需要把数据临时保存到表中,而且在有些情况下,我们可能不太想要使用永久性的表。在这种情况下,使用临时表可能会更方便。
(1)局部临时表:只对创建它的会话在创建级和对调用对战的内部级(内部的过程、函数、触发器等)是可见的,当创建会话从SQL Server实例断开时才会自动删除它。
创建临时局部表,只需要在命名时以单个#号作为前缀:
IF OBJECT_ID('tempdb.dbo.#MyOrderTotalsByYear') IS NOT NULL
DROP TABLE dbo.#MyOrderTotalsByYear;
GO
SELECT
YEAR(O.orderdate) AS orderyear,
SUM(OD.qty) AS qty
INTO dbo.#MyOrderTotalsByYear
FROM Sales.Orders AS O
JOIN Sales.OrderDetails AS OD
ON OD.orderid = O.orderid
GROUP BY YEAR(orderdate);
SELECT Cur.orderyear, Cur.qty AS curyearqty, Prv.qty AS prvyearqty
FROM dbo.#MyOrderTotalsByYear AS Cur
LEFT OUTER JOIN dbo.#MyOrderTotalsByYear AS Prv
ON Cur.orderyear = Prv.orderyear + 1;
GO
(2)全局临时表:可以对其他所有会话都可见,当创建临时表的会话断开数据库的连接,而且也没有活动在引用全局临时表时,SQL Server才会自动删除相应的全局临时表。
创建全局局部表,只需要在命名时以两个#号作为前缀:
-- Global Temporary Tables
CREATE TABLE dbo.##Globals
(
id sysname NOT NULL PRIMARY KEY,
val SQL_VARIANT NOT NULL
);
8.5 动态SQL
SQL Server允许用字符串来动态构造T-SQL代码地一个批处理,接着再执行这个批处理,这种功能叫做动态SQL(Daynamic SQL)。
(1)使用EXEC(EXECUTE的缩写)命令
-- Simple example of EXEC
DECLARE @sql AS VARCHAR(100);
SET @sql = 'PRINT ''This message was printed by a dynamic SQL batch.'';';
EXEC(@sql);
GO
(2)使用sp_executesql存储过程
sp_executesql存储过程有两个输入参数和一个参数赋值部分:第一个参数需要指定包含想要运行的批处理代码地Unicode字符串,第二个参数是一个Unicode字符串,包含第一个参数中所有输入和输出参数的生命。接着为输入和输出参数指定取值,各参数之间用逗号分隔。
-- Simple example using sp_executesql
DECLARE @sql AS NVARCHAR(100);
SET @sql = N'SELECT orderid, custid, empid, orderdate
FROM Sales.Orders
WHERE orderid = @orderid;';
EXEC sp_executesql
@stmt = @sql,
@params = N'@orderid AS INT',
@orderid = 10248;
GO
Tips:
①sp_executesql存储过程在执行性能上比EXEC要好,因为它的参数化有助于重用缓存过的执行计划。
②sp_executesql存储过程在安全上也比EXEC要好,它的参数化也可以不必受SQL注入的困扰。
8.6 例程:用户定义函数、存储过程与触发器
(1)用户定义函数:封装计算的逻辑处理,有可能需要基于输入的参数,并返回结果。
下面的示例创建了一个用户定义函数dbo.fn_age,对于给定出生日期和事件日期,这个函数可以返回某个人在时间日期当时的年龄:
IF OBJECT_ID('dbo.fn_age') IS NOT NULL DROP FUNCTION dbo.fn_age;
GO
CREATE FUNCTION dbo.fn_age
(
@birthdate AS DATETIME,
@eventdate AS DATETIME
)
RETURNS INT
AS
BEGIN
RETURN
DATEDIFF(year, @birthdate, @eventdate)
- CASE WHEN 100 * MONTH(@eventdate) + DAY(@eventdate)
< 100 * MONTH(@birthdate) + DAY(@birthdate)
THEN 1 ELSE 0
END
END
GO
(2)存储过程:封装T-SQL代码地服务器端例程,可以有输入和输出参数,可以返回多个查询的结果集。
下面的示例创建了一个存储过程usp_GetCustomerOrders,它接受一个客户ID和日期范围作为输入参数,返回Orders表中由指定客户在指定日期范围内所下的订单组成的结果集,同时也将受查询影响的行为作为输出参数。
IF OBJECT_ID('Sales.usp_GetCustomerOrders', 'P') IS NOT NULL
DROP PROC Sales.usp_GetCustomerOrders;
GO
CREATE PROC Sales.usp_GetCustomerOrders
@custid AS INT,
@fromdate AS DATETIME = '19000101',
@todate AS DATETIME = '99991231',
@numrows AS INT OUTPUT
AS
SET NOCOUNT ON;
SELECT orderid, custid, empid, orderdate
FROM Sales.Orders
WHERE custid = @custid
AND orderdate >= @fromdate
AND orderdate < @todate;
SET @numrows = @@rowcount;
GO
DECLARE @rc AS INT;
EXEC Sales.usp_GetCustomerOrders
@custid = 1, -- Also try with 100
@fromdate = '20070101',
@todate = '20080101',
@numrows = @rc OUTPUT;
SELECT @rc AS numrows;
GO
Tips:存储过程可以封装业务逻辑处理,更好地控制安全性(有助于避免SQL注入),提高执行性能(减少网络通信流量)。
(3)触发器:一种特殊的存储过程,只要特定事件发生,就会调用触发器,运行它的代码。SQL Server支持两种类型相关的触发器,分别是:DML触发器和DDL触发器。
下面的示例演示了一个简单的DML触发器,对插入到表的数据进行审核(插入到Audit审核表)。
CREATE TRIGGER trg_T1_insert_audit ON dbo.T1 AFTER INSERT
AS
SET NOCOUNT ON;
INSERT INTO dbo.T1_Audit(keycol, datacol)
SELECT keycol, datacol FROM inserted;
GO
8.7 错误处理
T-SQL代码中提供了一种成为TRY...CATCH的结构,在SQL Server 2005中引入的。
BEGIN TRY
PRINT 10/2;
PRINT 'No error';
END TRY
BEGIN CATCH
PRINT 'Error';
END CATCH
GO
来源:CSDN
作者:懒笔头
链接:https://blog.csdn.net/csdnsunyf/article/details/103906760