CPU
到达服务器的每个请求(即每个“批次”)都将与一个“任务”相关联,请参阅sys.dm_os_tasks。该任务在“调度程序”上排队,粗略地说是一个 CPU 内核,请参阅sys.dm_os_schedulers。每个调度程序都有几个“工作人员”(即线程或光纤,请参阅sys.dm_os_workers),一个空闲工作人员将从调度程序的队列中获取下一个任务并“逃跑”,执行它直到任务完成(即。请求已完成)。这种调度机制适用于 SQL 内部的一切,包括系统任务、CLR 运行代码等等。
可以创建的任务数量受可用内存的限制。请求(“批处理”)并不等同于一对一的任务,因为一些请求一旦开始就会安排更多任务执行,并行查询是典型的例子。系统中的工作人员数量是动态的,但受“max worker threads”配置设置的限制。如果达到工作人员上限,则新的计划任务将在调度程序中排队,但直到工作人员释放(完成任务)并变得可用时才会被拾取。当达到此条件时,称为“工人饥饿”并导致服务器无响应,因为新客户端登录握手需要执行登录任务(服务器似乎拒绝连接)并且现有客户端的新请求将在等待任务之后排队(服务器需要很长时间来响应琐碎的请求)。
因此,如果您有大量并行、长时间运行的查询,您将消耗大量工作人员来执行许多长时间运行的任务。这减少了空闲工作人员池的大小,导致可用于服务于服务器的其他短任务(如 OLTP 请求、登录握手等)的工作人员减少。服务器似乎没有响应,因为任务在调度程序队列中堆积(这可以在sys.dm_os_schedulers DMV work_queue_count 列中看到)。在极端情况下,您可以有效地使工作人员系统处于饥饿状态,使服务器完全没有响应,直到一些工作人员空闲为止。
内存
包含并行操作的查询计划通常与大索引(大表)的全扫描相关联。扫描索引是通过遍历其叶页来完成的,读取大表中的所有叶页意味着所有这些页必须在查询执行期间一次或一次地存在于内存中。这反过来又会从缓冲池中产生对空闲页面的需求,以容纳扫描的页面。对空闲页面的需求会产生内存压力,导致通知缓存开始驱逐旧条目,并删除缓冲池中旧访问的数据页面。缓存通知可以在sys.dm_os_memory_cache_clock_hands 中看到。可以通过检查良好的 ole'Page Life Expectancy 性能计数器来控制数据页驱逐。
驱逐缓存条目的效果是,下次需要被驱逐的条目(作为编译计划、权限令牌或其他)时,它必须从头开始创建,导致消耗更多的 CPU、内存和 IO,即使在长时间运行的查询完成后,效果也会显现出来。
现在的情况可能是,您的系统安装了如此庞大的 RAM,以至于扫描几个大表并没有什么区别,您的 RAM 可以容纳整个数据库并有剩余空间。在这种情况下没有问题。但大多数时候情况并非如此。
IO
这与上面的一点(记忆)有关。所有为满足索引扫描而读取的页面都必须传输到内存中,这意味着(可能很大)一部分 IO 带宽被长时间运行的查询所消耗。此外,所有从缓冲池中清除的脏数据页都必须写入磁盘,从而导致更多的 IO。而且被驱逐的干净页面可能会在未来某个时间被需要,因此需要更多的 IO。
如果扫描生成的 IO 超出系统带宽,则 IO 操作开始在磁盘控制器上排队。这可以在Physical Disk/Avg Queue Length 性能计数器中轻松检查。
争用
最后,最大的问题:锁争用。如前所述,并行查询几乎总是意味着表扫描。表扫描在它们访问的每一行上都使用一个共享锁。确实,一旦在正常操作模式下读取记录,他们就会释放锁,但您仍然保证您将在表中的每一行上请求 S 锁时间>。这几乎可以保证这些扫描将命中被更新锁定 X 的行。当发生这种情况时,扫描必须停止并等待 X 锁被释放,这发生在更新事务最终提交时。结果是即使表上的适度 OLTP 活动也会阻止长时间运行的查询。理想情况下,这就是所有发生的事情,结果只是性能不佳。但是,如果长时间运行的查询做了一些花哨的事情,比如获取页锁而不是行锁,事情就会变得很糟糕。由于这些扫描是端到端遍历索引,并且保证会与更新发生冲突,因此这些查询获取的更高粒度的锁不再只是与更新锁发生冲突,而是实际上会导致死锁。解释这是如何发生的超出了本回复的重点。
为了消除争用,当查询合法进行全面扫描时,最好的替代方法是使用神奇快照:为报告创建database snapshots,或使用snapshot isolation 级别。请注意,有些人可能会建议使用脏读,我还没有找到实际可以接受的情况。