结果集大小如何影响并发性
这要从ODS(Open Data Service)"开放数据服务"说起。它的主要职责是管理连接;SQL的线程服务和将结果集、状态值及消息发送给客户。结果集使用TDS(Tabular data stream)表格数据流进行传送,它除了包含所需要的数据之外,还有一些描述信息:如列名、类型、通信的令牌等等。因此,这也是为什么我觉得在存储过程中只返回一行记录时,使用输出参数能减少网络字节数的原因,因为它少了很多其它要描述的信息。
在数据库的配置选项中,有对于Network Packet Size的选项,默认是4096字节。这个大小决定了每个输出缓冲区的大小。对于每个客户请求,SQL SERVER都会为其创建两个输入缓冲区(一个用于接收命令、另一个用于执行取消命令,这个大小还不知道在哪里配置,或者根本就是不能配置的)和一个输出缓冲区。发送结果集的条件是输出缓冲区已满或是批处理命令结束时(一个GO之前的所有命令为一个批)。
因此,当你的查询要返回的记录数很多时,这个输出缓冲区要与客户进行很多次的握手操作,在每次输出缓冲区已满时,必须要把这些结果发送给客户之后,这个缓冲区才可以继续被使用。如果这时网络受限,往返的时间就会很长。那么在这个等待期间,你所发出的select命令对所选的记录是要下共享锁(S)的。如果只是对记录下共享锁,可能影响会稍小些。但是SQLSERVER可能会因为你选择的记录数太多,把记录锁提升为页级别的共享锁,最糟糕的时候,可能会因为你执行的select没有索引,而对表进行了扫描,整个表都加了共享锁,不管怎样都会有IS锁存在。这时,如果有人想对表中的任意一行记录做修改操作都要先获取排它锁(X),因为S、IS与X不兼容,因此修改操作被阻塞。一直等到你把所有的结果都传送到客户端后才可以进行修改。
也许有人会问,SQLSERVER有没有那么智能,在扫描完一页后释放掉对改页的共享锁?这个我也不确定。不过就算是它足够智能,在扫描完一页时,释放掉了该页的共享锁,同样还会存在意向共享锁。意向锁的用途主要是用来快速判断锁请求是否有冲突所用的。为了能观察到锁的情况,运行下面的脚本:
GO
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
begin tran
SELECT * FROM Sales.SalesOrderHeader with(rowlock) WHERE SalesOrderID=43860
select * from sys.dm_tran_locks where request_session_id=@@spid
rollback tran
DBCC TRACEON(3604)
GO
--观察PAGE锁的页是否包含的信息,键值是哈稀后的结果
DBCC PAGE(AdventureWorks,1,8653,3)
GO
从上面的结果,我们看到除了键锁和页锁外,还有一个OBJECT类型的锁,根据resource_associated_entity_id的值得知它就是相应的SalesOrderHeader表。而那个DATABASE类型的锁是因为你打开了一个连接,每打开一个连接都会有一个数据库级的共享锁,这个锁存在的目的是防止有连接的时候删除数据库和还原数据库之用。
也许有人会想到,既然现在表上有了IS锁,那后续的更新操作是如何获取到排它锁呢?在内部,它会把这些操作请求排队,以防止所谓的"锁饥饿"现象。也就是锁的请求会按照队列在一定时间范围内决定是否有冲突产生的。
因此,即便SQLSERVER够智能,因为对象的IS锁存在,所有修改操作可能还是会被阻塞。
如果你的批命令中包含了很多条SQL语句。如果第一条SQL语句的结果集是很小的,它不能填满输出缓冲区,就只能等批命令执行完毕或是缓冲区已满才能发送到客户端。那么为了尽快能让结果集发送回客户端,我们是否应该把这条语句单独作为一个批命令呢?我想这和去超市买东西是一个道理:我想买一件衣服和一台电视,不可能先把衣服取回来,再去购买电视。这样的话总的时间开销会比一次购齐要花更多的体力。因此,这个Network Packet Size选项的大小是很难进行估算的,只有你的查询尽量返回较少的记录数时,才能不会影响到别人的操作。
所以说即便你的查询够快,不会在表中存在很长时间的共享锁,因为传送的原因,如果结果集很大,照样会影响并发性的操作。