SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因 - BIWORK - 博客园

mikel阅读(629)

来源: SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因 – BIWORK – 博客园

原本打算写有关 SSIS Package 中的事务控制过程的,但是发现很多基本的概念还是需要有 SQL Server 事务和事务的隔离级别做基础铺垫。所以花了点时间,把 SQL Server 数据库中的事务概念,ACID 原则,事务中常见的问题,问题造成的原因和事务隔离级别等这些方面的知识好好的整理了一下。

其实有关 SQL Server 中的事务,说实话因为内容太多, 话题太广,稍微力度控制不好就超过了我目前知识能力范围,就不是三言两语能够讲清楚的。所以希望大家能够指出其中总结的不足之处,对我来说多了提高的机会,更可以帮助大家加深对事务的理解。


本文涉及到的知识点:

  • SQL Server 数据库中事务的概念
  • ACID 原则 (加了一部分内容专门解释原子性,提到了显示事务以及 XACT_ABORT 机制来确保事务的原子性)
  • 列出事务中常见的问题以及原因:脏读,未提交读,不可重复读,幻读
  • SQL Server中 事务的隔离级别以及它们如何做到避免脏读,未提交读,不可重复读和幻读 (用代码描述了这些问题,并且使用时间序来解释产生的原因)

SQL Server 数据库中事务的概念

数据库中的事务是数据库并发控制的基本单位,一条或者一组语句要么全部成功,对数据库中的某些数据成功修改; 要么全部不成功,数据库中的数据还原到这些语句执行

之前的样子。比如网上订火车票,要么你定票成功,余票显示就减一张; 要么你定票失败获取取消订票,余票的数量还是那么多。不允许出现你订票成功了,余票没有减少或者你取消订票了,余票显示却少了一张的这种情况。这种不被允许出现的情况就要求购票和余票减少这两个不同的操作必须放在一起,成为一个完整的逻辑链,这样就构成了一个事务。


数据库中事务的 ACID 原则

原子性 (Atomicity):事务的原子性是指一个事务中包含的一条语句或者多条语句构成了一个完整的逻辑单元,这个逻辑单元具有不可再分的原子性。这个逻辑单元要么一起提交执行全部成功,要么一起提交执行全部失败。

一致性 (Consistency):可以理解为数据的完整性,事务的提交要确保在数据库上的操作没有破坏数据的完整性,比如说不要违背一些约束的数据插入或者修改行为。一旦破坏了数据的完整性,SQL Server 会回滚这个事务来确保数据库中的数据是一致的。

隔离性(Isolation):与数据库中的事务隔离级别以及锁相关,多个用户可以对同一数据并发访问而又不破坏数据的正确性和完整性。但是,并行事务的修改必须与其它并行事务的修改相互独立,隔离。 但是在不同的隔离级别下,事务的读取操作可能得到的结果是不同的。

持久性(Durability):数据持久化,事务一旦对数据的操作完成并提交后,数据修改就已经完成,即使服务重启这些数据也不会改变。相反,如果在事务的执行过程中,系统服务崩溃或者重启,那么事务所有的操作就会被回滚,即回到事务操作之前的状态。

我理解在极端断电或者系统崩溃的情况下,一个发生在事务未提交之前,数据库应该记录了这个事务的”ID”和部分已经在数据库上更新的数据。供电恢复数据库重新启动之后,这时完成全部撤销和回滚操作。如果在事务提交之后的断电,有可能更改的结果没有正常写入磁盘持久化,但是有可能丢失的数据会通过事务日志自动恢复并重新生成以写入磁盘完成持久化。

原子性的进一步理解

关于原子性,有必要在这里多补充一下,因为我们描述的概念是指在事务中的原子性。一条 SQL 语句和多条 SQL 语句在处理原子性上是有一些区别的,下面演示了这些区别。

先运行这些代码,创建一个非常简单的测试表,这张表只简单模拟了一个账户的 ID 和账户余额。

USE BIWORK_SSIS
GO

IF OBJECT_ID('dbo.Account') IS NOT NULL
DROP TABLE dbo.Account
GO

CREATE TABLE dbo.Account
(
  ID INT PRIMARY KEY,
  AccountBalance MONEY CHECK(AccountBalance >= 0)
)

单条 SQL 语句的原子性

插入一条测试语句,然后再查询一下结果。

这里提到了自动提交事务,这时 T-SQL 默认的事务方式,它是一种能够自动执行并能够自动回滚事务的处理方式。SQL Server 除了自动提交事务之外,还有显示事务和隐式事务,暂时不在这篇文章中讨论它们的区别了。

上面的两个自动提交事务中,每一个自动提交事务只包含一条 SQL 语句,不能再分,要么成功,要么失败。

再比如,在一条 SQL 语句中插入多条数据时,其中一条数据是符合约束的。但因为另外一条数据违反了检查约束,这样也会导致整个 Insert 语句失败,因此没有一条数据能够插入到数据表中。

多条 SQL 语句形成的一个整体的原子性

假设下面的这两条 Insert 语句构成一个具备原子性特征的逻辑单元,是一个整体需要形成一个事务,那么应该如何处理。

INSERT INTO dbo.Account VALUES(1004,-1)
INSERT INTO dbo.Account VALUES(1005,500)

很显然如果直接这么执行的话,1004 插入失败,1005 可以插入成功,这样就是两个不同的事务了。SQL Server 提供了两种方式来确保这种包含多组 SQL 语句的逻辑块具备原子性特征。

方式一 – 使用显示事务组合多条 SQL 语句构成一个整体以实现事务的原子性

第一种就是非常常见的显示事务,通过显示的使用 BEGIN TRANSACTION, COMMIT TRANSACTION 以及 ROLLBACK TRANSACTION 命令将一组 SQL 语句形成一个完整的事务来提交,提交要么成功,要么失败。

-- 开始一个事务
BEGIN TRANSACTION

-- TRY CATCH 语句
BEGIN TRY

 -- 这一条会违反检查约束,插入失败
    INSERT INTO dbo.Account VALUES(1004,-1)
 -- 这一条会插入成功,但此时事务还未真正提交
    INSERT INTO dbo.Account VALUES(1005,500)

END TRY
BEGIN CATCH
 -- 发生错误,事务回滚
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
END CATCH;

-- 没有进入 CATCH 块,提交事务
IF @@TRANCOUNT > 0
    COMMIT TRANSACTION;
GO

当然最终的结果就是事务回滚,一条数据都没有插入到数据表中,所以失败时就全部失败,确保了事务的原子性。

方式二 – 通过设置  XACT_ABORT 为 ON 来确保事务的原子性

先来看默认的设置,当  XACT_ABORT 为 OFF 状态的时候。

-- SET XACT_ABORT OFF - 默认的 SQL Server 设置
SET XACT_ABORT OFF
BEGIN TRANSACTION
 -- 这一条会违反检查约束,插入失败
    INSERT INTO dbo.Account VALUES(1004,-1)
 -- 这一条会插入成功
 INSERT INTO dbo.Account VALUES(1005,500)
COMMIT TRANSACTION

当  XACT_ABORT 为 OFF 状态即 SQL Server 默认设置下,上面的事务中,SQL Server 在通常情况下只会回滚执行失败的语句,也就是说只会回滚 1004 这条数据,而 1005 会插入成功。很显然,这违背了事务的原子性,因为我们也没有显示的写出要 ROLLBACK TRANSACTION 来。

OK!那我们将 XACT_ABORT 设置为 ON,这时就告诉了它后面的事务,如果遇到错误就立即终止事务并回滚。这样不通过显示的 ROLLBACK TRANSACTION 也可以确保事务的原子性。

在上面的这个例子中,只有事务 2 会成功提交,而事务1和3会回滚,插入操作执行失败。

注意一点,上面的每个事务后面加了一个 GO 关键字,如果不加 GO 这个关键字,一起执行这些 SQL 语句会导致事务2和3因为事务1的执行失败而不能执行到, GO 关键字形成了一个批处理,表示前面的一组 SQL 语句一起处理。

GO 关键字非常有意思,GO 后面可以加上次数,表示前面的一条或者一组 SQL 执行几次。

通过上面的示例,应该可以理解原子性与事务的关系了,以及如何实现事务的原子性。


事务中常见的问题

了解完事务的 ACID 的原则后,再来看看在 SQL Server 中多用户并发的情况下,使用事务可能会遇到的一些情况:

脏读 (Dirty Reads) : 一个事务正在访问并修改数据库中的数据但是没有提交,但是另外一个事务可能读取到这些已作出修改但未提交的数据。这样可能导致的结果就是所有的操作都有可能回滚,比如第一个事务对数据做出的修改可能违背了数据表的某些约束,破坏了完整性,但是恰巧第二个事务却读取到了这些不正确的数据造成它自身操作也发生失败回滚。

不可重复读取(Non-Repeatable Reads):  A 事务两次读取同一数据,B事务也读取这同一数据,但是 A 事务在第二次读取前B事务已经更新了这一数据。所以对于A事务来说,它第一次和第二次读取到的这一数据可能就不一致了。

幻读(Phantom Reads): 与不可重复读有点类似,都是两次读取,不同的是 A 事务第一次操作的比如说是全表的数据,此时 B 事务并不是只修改某一具体数据而是插入了一条新数据,而后 A 事务第二次读取这全表的时候就发现比上一次多了一条数据,发生幻觉了。

更新丢失(Lost Update): 两个事务同时更新,但由于某一个事务更新失败发生回滚操作,这样有可能的结果就是第二个事务已更新的数据因为第一个事务发生回滚而导致数据最终没有发生更新,因此两个事务的更新都失败了。


SQL Server 中事务的隔离级别以及与脏读,不可重复读,幻读等关系(代码论证和时间序)

了解了在并发访问数据库的情况下可能会出现这些问题,就可以继续了解数据库隔离级别这样的一个概念,通俗一点讲就是:你希望通过何种方式让并发的事务隔离开来,隔离到什么程度?比如可以容忍脏读,或者不希望并发的事务出现脏读的情况,那么这些可以通过隔离级别的设置使得并发事务之间的隔离程度变得宽松或者很严峻。

隔离级别越高,读取脏数据或者造成数据不统一不完整的机会就越少,但是在高并发的系统中,性能降低就越严重。隔离级别越低,并发系统中性能上提升很大,但是数据本身可能不完整。

在 SQL Server 2012 中可以通过这样的语法来设置事务的隔离级别 (从低到高排列):

SET TRANSACTION ISOLATION LEVEL
    { READ UNCOMMITTED
    | READ COMMITTED
    | REPEATABLE READ
    | SNAPSHOT
    | SERIALIZABLE
    }
[ ; ]

下面通过代码示例来演示各个事务隔离级别的表现,运行下面 SQL 语句,插入一条测试语句。

TRUNCATE TABLE BIWORK_SSIS.dbo.Account
GO

INSERT INTO BIWORK_SSIS.dbo.Account VALUES(1001,1000)

SELECT * FROM BIWORK_SSIS.dbo.Account
GO

Read Uncommitted (未提交读)

隔离级别最低,容易产生的问题就是脏读,因为可以读取其它事务修改了的但是没有提交的数据。它的作用跟在事务中 SELECT 语句对象表上设置 (NOLOCK) 相同。

打开两个查询窗口,第一个窗口表示事务 A, 第二个窗口表示事务B。 事务A 保持默认的隔离级别,事务B 设置它们的隔离级别为 READ UNCOMMITTED, 可以通过 DBCC USEROPITIONS 查看更改后的结果。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 
DBCC USEROPTIONS

测试步骤:

先执行事务 A 的 SQL 代码

BEGIN TRANSACTION

UPDATE BIWORK_SSIS.dbo.Account
SET AccountBalance = 500 
WHERE ID  = 1001

WAITFOR DELAY '00:00:10'

ROLLBACK TRANSACTION

SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

马上接着再执行 事务 B 的 SQL 代码

-- 第1次查询 发生在 A 事务未提交或者回滚之前
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

WAITFOR DELAY '00:00:10'

-- 第2次查询 发生在 A 事务回滚之后
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

可以看出,事务 B 对 ID = 1001 的这条数据进行了两次读取,但是很显然第一次读取的数据是脏数据。下面模拟了一下它们发生的时序,虽然不算严谨,但是可以帮助理解脏读产生的原因。

还可以把事务B 的隔离级别改回来成为默认的  READ COMMITTED,然后运行完事务 A 之后马上运行带有 NOLOCK 的查询,效果和上面描述的也是一致的。 一旦加上 NOLOCK,可以认为它的作用就等同于隔离级别为 READ UNCOMMITTED。

SELECT * FROM BIWORK_SSIS.dbo.Account WITH(NOLOCK) WHERE ID = 1001

 

Read Committed (已提交读)

这是 SQL Server 的默认设置,已提交读,可以避免脏读,可以满足大多数要求。事务中的语句不能读取已由其它事务做出修改但是还未提交的数据,但是能够读取由其它事务做出修改并提交了的数据。也就是说,有可能会出现 Non-Repeatable Reads 不可重复读取和 Phantom Reads 幻读的情况,因为当前事务中可能出现两次读取同一资源,但是两次读取的过程之间,另外一事务可能对这一资源完成了读取更新并提交的行为,这样数据前后可能就不一致了。因此,这一个默认的隔离级别能够解决脏读但是解决不了 Non-Repeatable Reads 不可重复读。

接着上一个例子,看看如果将隔离级别设置为 READ COMMITTED,能否避免脏读? 还是先运行事务 A,再接着运行事务 B。

因为已提交读不能读取已由其它事物做出修改但是还未提交的数据,因此事务B 就必须等待事务 A 完成对数据的修改提交或者回滚之后才能开始读取。运行事务A 和事务B,明显事务B 有一个等待事务A提交或者回滚的过程,看看它们的时序图。

由此可以看出隔离级别 READ COMMITTED 可以避免脏读,但是也有可能出现其它的问题,请看这个例子。先执行事务A,接着直接执行事务 B。

从上面的执行结果来看,很明显在事务 A 中,同一个事务中对 ID  = 1001 的取值出现了前后不一致的情况。假设这里不是简单的查询,而是先查询账户余额有 1000元钱,然后后面的动作就是取 1000元钱,很明显第二次取的时候发现只有 500 元了。原因就是在第一次查询和取的间隙之间被事务 B 钻了空子,修改了余额。这种情况就是上面所介绍到的不可重复读取,请看下面的时序图。

所以 READ COMMITTED 已提交读隔离级别能够避免脏读,但是仍然会遇到不可重复读取的问题。

Repeatable Read (可重复读)

不能读取已由其它事务修改了但是未提交的行,其它任何事务也不能修改在当前事务完成之前由当前事务读取的数据。但是对于其它事务插入的新行数据,当前事务第二次访问表行时会检索这一新行。因此,这一个隔离级别的设置解决了 Non-Repeatable Reads 不可重复读取的问题,但是避免不了 Phantom Reads 幻读。

接着上面的例子做出一些修改,增加了一些查询,记得把 ID = 1001 的余额改回 1000。将事务 A 的隔离级别设置为 REPEATABLE READ 可重复读级别,来看看这个隔离级别的表现。

尽管在最后的查询结果中, ID  = 1001 的余额为 500 元,但是在事务 A 中的两次读取一次发生在 事务 B 开始之前,一次发生在 事务 B 提交之后,但是它们读取的余额是保持一致的,看不到事务 B 对这个值的修改。

从上面的时序图中可以看出,事务 A 第一次读取到的 ID = 1001 的余额值和第二次读取到的是一样的,可以理解为在事务 A 的查询期间是不允许事务 B 修改这个值的。 因为事务 A 确实没有看到这个变化,所以事务A 也确实认为事务B 听了它的话,没有做出 Update 的操作。但是实际上,事务 B 已经完成了这个操作,只不过由于 事务 A 中隔离级别设置为 REPEATABLE READ 可重复读,所以两次读取的结果始终保持着一致。

那么这里的示例是事务B在修改数据,如果是新增加一行记录呢?

事务 A 又开始晕菜了!居然两次查询的结果不一样,第二次查询多了一条数据,这就是幻读!

SNAPSHOT (快照隔离)

可以解决幻读 Phantom Reads 的问题,当前事务中读取的数据在整个事务开始到事务提交结束之间,这个数据版本是一致的。其它的事务可能对这些数据做出修改,但是对于当前事务来说它是看不到这些变化。有点类似于当前事务拿到这个数据的时候是拿到这个数据的快照,因此在这个快照上做出的操作同一事务中前后几次操作都是基于同一数据版本。因此,这一个隔离级别的设置可以解决 Phantom Reads 幻读问题。但是要注意的是,其它事务是可以在当前事务完成之前修改由当前事务读取的数据。

在使用 SNAPSHOT 之前要注意,默认情况下数据库不允许设置 SNAPSHOT 隔离级别,直接设置会出现类似于这样的错误:

DBCC execution completed. If DBCC printed error messages, contact your system administrator.

Msg 3952, Level 16, State 1, Line 8

Snapshot isolation transaction failed accessing database ‘BIWORK_SSIS’ because snapshot isolation is not allowed in this database. Use ALTER DATABASE to allow snapshot isolation.

所以要使用 SET 命令开启这个支持

ALTER DATABASE BIWORK_SSIS
SET ALLOW_SNAPSHOT_ISOLATION ON

并且在开始前先清空其它的 ID,只保留 ID = 1001 的这条记录。

DELETE FROM BIWORK_SSIS.dbo.Account
WHERE ID <> 1001

这样通过设置隔离级别是 SNAPSHOT就解决了幻读的问题,保证了在事务 A 中查询的数据行版本是前后一致的。

但是大家发现没有?无论在事务 A 中使用 Repeatable Read 还是 Snapshot 仍然不可避免的阻止事务B 对共享的资源做出了修改,尽管这个修改没有被事务 A 发现,事务 A 中的数据还是保持了一致,但是实际上还是做出了修改。只要事务 A 一提交结束,马上就可以看到事务 B 做出的这些修改已经生效了。回顾之前提到的,如果我第一次查询有1000元,第二次动作可能就是取1000元。在这两次动作之间另外的一个事务对金额做出了修改,尽管我两次读取都是1000元,但是实际上是不符合常理的。要么,我先查询然后再取款这个动作是连贯的,然后另外一个事务再对金额做出修改。要么,其它事务先对金额做出修改,比如扣去500元,那么我再查询再取款这个钱数还是一致的。也就是说,在事务 A 对某一个资源做出操作的时候,形成了独占,事务 B 进不来。或者事务 B 在对这个资源做操作的时候,事务 A 也必须等待事务 B 结束后才能开始它的事务,那么这里就要使用到最严格的隔离级别了 – SERIALIZABLE。

 

SERIALIZABLE(序列化)

性能最低,隔离级别最高最严格,可以几乎上面提到的所有问题。比如不能读取其它已由其它事务修改但是没有提交的数据,不允许其它事务在当前事务完成修改之前修改由当前事务读取的数据,不允许其它事务在当前事务完成修改之前插入新的行。它的作用与在事务内所有 SELECT 语句中的所有表上设置 HOLDLOCK 相同,并发级别比较低但又对安全性要求比较高的时候可以考虑使用。如果并发级别很高,使用这个隔离级别,性能瓶颈将非常严重。

将事务 A 的隔离级别调整成 SERIALIZABLE,然后执行 A 然后再执行 B。

在这里可以看到事务B 的执行基本上是在事务A提交之后才开始的,当事务 A 在执行的时候,事务 B 因为也要访问这个资源所以一直阻塞在那里直到事务 A 提交。 并不是说事务 B 没有开始,而是说在执行 SELECT 查询的时候因为事务 A 占用了这个资源,所以处于等待状态。

在 SQL Server 中设置隔离级别要注意:一次只能设置一个隔离级别的选项,并且设置的隔离级别对当前连接一直有效直到显式修改为止。事务中执行的所有读取操作也都会在指定的隔离级别规则下运行,除非在 SELECT 操作语句中对表指定了其它的锁或者版本控制行为。

注:上面的时序图只是用来帮助理解事务的隔离级别,只是一个大概的执行顺序,当然也跟我执行事务 A 和 事务 B 的时间点相关,所以并不能真正反映实际过程中 SQL 语句提交和执行的实际顺序,真正提交的过程可以通过 SQL Profiler 去跟踪看看。

解决sql 2008 事务日志收缩与截断无法减小日志物理文件的方法_zhen520的专栏-CSDN博客

mikel阅读(771)

来源: (7条消息)解决sql 2008 事务日志收缩与截断无法减小日志物理文件的方法_zhen520的专栏-CSDN博客

本文的重点是与大家分享日志截断、收缩后,物理文件的尺寸依然没有减小到期望尺寸的问题。

解决方案

   我的一个数据库,数据文件10+G ,事务日志达20+G,而且使用常规的截断、收缩方法均无法减小日志物理文件的尺寸,经过一番寻找,终于找到了解决方法。 查看日志信息 在查询分析器中执行如下代码来查看日志信息: 1 DBCC LOGINFO(‘数据库名称’) 我们看到status=0的日志,代表已经备份到磁盘的日志文件;而status=2的日志还没有备份。当我们收缩日志文件时,收缩掉的空间其实就是status=0的空间,如果日志物理文件无法减小,这里一定能看到非常多status=2的记录。接下来分析为什么会有这么多status=2的记录 查看日志截断延迟原因 活跃(active)的日志无法通过收缩来截断,有各种原因会使日志截断延迟,具体表现就是事务日志的物理文件无法通过截断、收缩来减小,通过下面的代码可以看到实例上每个数据库的日志截断延迟原因: 1 USE [master] 2 SELECT [name] ,[database_id] ,[log_reuse_wait] ,[log_reuse_wait_desc] FROM [sys].[databases] 各种原因及解释如下: log_reuse_wait_desc 值 说明 NOTHING 当前有一个或多个可重复使用的虚拟日志文件。 CHECKPOINT 自上次日志截断之后,尚未出现检查点,或者日志头部尚未跨一个虚拟日志文件移动(所有恢复模式)。 这是日志截断延迟的常见原因。有关详细信息,请参阅检查点和日志的活动部分。 LOG_BACKUP 需要日志备份,以将日志的头部前移(仅适用于完整恢复模式或大容量日志恢复模式)。 注意:日志备份不会妨碍截断。 完成日志备份后,日志的头部将前移,一些日志空间可能变为可重复使用。 ACTIVE_BACKUP_OR_RESTORE 数据备份或还原正在进行(所有恢复模式)。 数据备份与活动事务的运行方式相同。数据备份在运行时,将阻止截断。有关详细信息,请参阅本主题后面的“数据备份操作与还原操作”部分。 ACTIVE_TRANSACTION 事务处于活动状态(所有恢复模式)。 一个长时间运行的事务可能存在于日志备份的开头。在这种情况下,可能需要进行另一个日志备份才能释放空间。有关详细信息,请参阅本主题后面的“长时间运行的活动事务”部分。 事务被延迟(仅适用于 SQL Server 2005 Enterprise Edition 及更高版本)。“延迟的事务 ”是有效的活动事务,因为某些资源不可用,其回滚受阻。有关导致事务延迟的原因以及如何使它们摆脱延迟状态的信息,请参阅延迟的事务。 DATABASE_MIRRORING 数据库镜像暂停,或者在高性能模式下,镜像数据库明显滞后于主体数据库(仅限于完整恢复模式)。 有关详细信息,请参阅本主题后面的“数据库镜像与事务日志”部分。 REPLICATION 在事务复制过程中,与发布相关的事务仍未传递到分发数据库(仅限于完整恢复模式)。 有关详细信息,请参阅本主题后面的“事务复制与事务日志”部分。 DATABASE_SNAPSHOT_CREATION 正在创建数据库快照(所有恢复模式)。 这是日志截断延迟的常见原因,通常也是主要原因。 LOG_SCAN 正在进行日志扫描(所有恢复模式)。 这是日志截断延迟的常见原因,通常也是主要原因。 针对延迟日志截断原因的部分解决方案 LOG_BACKUP 备份日志后再执行收缩即可 REPLICATION 这是我遇到的情况,但我根本没有启用过REPLICATION,据查,这好像是SQLServer2008的一个BUG,解决方法是给标有“REPLICATION”的数据库任意一个表创建数据库事务复制(TRANSACTION REPLICATION),然后再删除,执行数据库与日志备份后,就可以收缩了。 小技巧一般收缩日志的代码中都要求指定日志的文件名称,下面的代码则可以自动获取日志文件名称: 1 USE [数据库名称] 2 DECLARE @LogFileLogicalName sysname 3 SELECT @LogFileLogicalName=Name FROM sys.database_files WHERE Type=1 4 PRINT @LogFileLogicalName 5 DBCC SHRINKFILE (@LogFileLogicalName, 1);

SQL Server 数据库开启日志CDC记录,导致SQL Server 数据库日志异常增大 - 蓝涩街灯 - 博客园

mikel阅读(1045)

来源: SQL Server 数据库开启日志CDC记录,导致SQL Server 数据库日志异常增大 – 蓝涩街灯 – 博客园

这几天单位的SQL Server业务数据生产库出现数据库日志增长迅速,导致最终数据无法写入数据库,业务系统提示“数据库事务日志已满”,经过多方咨询和请教,终于将日志异常的数据库处理完毕,现总结下处理过程,希望可以帮助到同样遇到此问题的小伙伴。–在菜鸟的路上越走越远

–第一步:查询服务器上所有日志大小情况
DBCC SQLPERF(LOGSPACE)

–第二步:查询日志设置方式
SELECT name,recovery_m odel_desc,log_reuse_wait,log_reuse_wait_desc
FROM sys.databases where log_reuse_wait=6

–第三步:定位数据库
USE BAK
GO

–第四步:查看是否开启cdc功能
select is_tracked_by_cdc,*from sys.tables where is_tracked_by_cdc=1

–第五步:清理到活动日志,把日志标记为已经发布状态
EXEC sp_repldone @xactid = NULL, @xact_segno = NULL, @numtrans = 0, @time = 0, @reset = 1
–第六步:
EXEC sys.sp_cdc_disable_db
GO

–第七步:
EXEC sp_removedbreplication BAK
GO

–第八步:手动收缩数据库日志,详细收缩方式https://www.cnblogs.com/since-1995/p/11694373.html

 

 

SQLServer数据库中开启CDC导致事务日志空间被占满的原因_woailyoo0000的博客-CSDN博客_数据库

mikel阅读(773)

—-收缩数据库日志 DBCC ShrinkFile(‘TestLogFull_Log’, 100)

来源: SQLServer数据库中开启CDC导致事务日志空间被占满的原因_woailyoo0000的博客-CSDN博客_数据库

SQLServer中开启CDC之后,在某些情况下会导致事务日志空间被占满的现象为:

在执行增删改语句(产生事务日志)的过程中提示,

The transaction log for database ‘***’ is full due to ‘REPLICATION’

(数据库“***”的事务日志已满,原因为“REPLICATION”).

解决办法:

EXEC SP_Repldone @xactid=NULL ,
@xact_segno = NULL,
@numtrabs = 0 ,
@time = 0
@reset = 1

CDC以及复制的基本原理粗略地讲,对于日志的使用步骤如下:

1,每当基础表(开启了CDC或者replication的表)产生事务性操作(增删改)之后,对应的事务日志写入日志文件,

2,此时的日志被状态被标记为Replication,也即处于待复制状态,这个活动状态跟数据库的还原模式无关,即便是简单还原模式,

3,然后有后台进程来读取这个日志,根据事务日志的内存写入目标表,

这个目标对于cdc来说是记录数据变化的系统表,

对于replication来说是写入distribution这个库

4,步骤3完成之后,事务日志被标记为正常状态,如果是简单还原模式,被后台进程解析过的事务日志被截断,可以重用如果上述中间的第三个步骤出现问题,也即后台进程无法解析日志后释放可用的日志空间,再次往数据库中写入操作,就会出现:数据库“TestDB”的事务日志已满,原因为“REPLICATION”的情况

本文通过通过演示开启CDC的情况下日志空间被占满的现象,以及对应的处理办法

测试环境搭建

首先建立一个测试数据库

  1. USE master
  2. GO
  3. CREATE DATABASE TestLogFull ON PRIMARY
  4. (
  5. NAME = N‘TestLogFull’,
  6. FILENAME = N‘D:\DBFile\TestLogFull\TestLogFull.mdf’ ,
  7. SIZE = 500MB ,
  8. MAXSIZE = UNLIMITED,
  9. FILEGROWTH = 100MB
  10. )
  11. LOG ON
  12. (
  13. NAME = N‘TestLogFull_log’,
  14. FILENAME = N‘D:\DBFile\TestLogFull\TestLogFull_Log.ldf’ ,
  15. SIZE = 1MB ,
  16. MAXSIZE = 512MB
  17. )

这里指定日志文件的最大为512M,主要是为了演示日志空间被占满的现象

接着开启新建一个表同时开启CDC来测试

  1. USE TestLogFull
  2. –启用CDC
  3. EXECUTE sys.sp_cdc_enable_db;
  4. GO
  5. –创建一张测试表
  6. create table test_cdc
  7. (
  8. id int identity(1,1) primary key,
  9. name nvarchar(50),
  10. mail varchar(50),
  11. address nvarchar(50),
  12. lastupdatetime datetime
  13. )
  14. –对表启用CDC
  15. EXEC sys.sp_cdc_enable_table
  16. @source_schema = ‘dbo’,
  17. @source_name = ‘test_cdc’,
  18. @role_name = ‘cdc_admin’,
  19. @capture_instance = DEFAULT,
  20. @supports_net_changes = 1,
  21. @index_name = NULL,
  22. @filegroup_name = DEFAULT
  23. –查询数据库是否开启了CDC
  24. SELECT name, is_tracked_by_cdc
  25. FROM sys.tables
  26. WHERE OBJECT_ID = OBJECT_ID(‘dbo.test_cdc’)

这里演示对某些表开启CDC的情况下日志文件文件被占满的情况

1. 代理服务器未启动导致日志空间被占满

文中一开始提到的步骤3,对于CDC,进程就是SQL Server Agent中的cdc.***_capture作业或者复制代理作业来读取日志
如果SQL Server Agent在开启了CDC或者复制之后被关闭,或者重启服务器之后SQL Server Agent没有随机自动启动
就有可能造成步骤2中的日志积压,也就是记录数据变化之后的事务日志处于replication状态,无法重用,导致没有可以使用的日志致使发生操作数据库的时候提示The transaction log for database ‘***’ is full due to ‘REPLICATION’.

这里暂时关闭代理服务(仅仅是为了测试演示这一现象)

增删改都可以产生事务日志,这里就演示insert数据的情况,做一个写数据的SQL,往开启了CDC的表中写数据库
在建库的时候日志文件有限制成了512M,因为这个表上开启了CDC,写数据这个过程会产生事务日志,日志有空空间限制在写入数据的过程中,一开始是没有问题的,随着数据的不断写入(Replication状态的日志不断积压),当日志全部使用之后,下面的报错就会产生了

  1. while 1=1
  2. begin
  3. Insert Into test_cdc values(newid(), newid(), getdate())
  4. end

此时观察事务日志的使用情况,发现已经是完全使用了,

  1. –查询日志使用率
  2. DBCC SQLPERF(LOGSPACE)

因为日志空间被完全使用了,那么观察一下日志的等待状态,是Replication状态

  1. —-查询等待日志模式
  2. SELECT NAME,DATABASE_ID, LOG_Reuse_Wait, Log_Reuse_Wait_Desc
  3. FROM
  4. Sys.DATABASES
  5. WHERE Name = ‘TestLogFull’

此时尝试收缩也是无效的,因为日志都是出于活动状态,活动状态的日志是无法收缩的

  1. —-收缩数据库日志
  2. DBCC ShrinkFile(‘TestLogFull_Log’, 100)

可见,因为代理被关闭,读取日志的作业无法执行,造成日志堵塞,那么开启代理来看看到底行不行?
开启代理,查看CDC作业的执行情况,会发现,此时代理作业也不好使了,作业执行的时候并没有成功,一样提示说事务日志已满

此时观察测试表的cdc目标表没有任何数据,说明此时即便开启了代理,cdc的作业依然没有成功执行
那么这里为什么CDC的代理作业也无法正常执行?

其实也不难理解,cdc的作业也是读取事务日志写数据的,这中间也相当于有事务性操作,必须要借助日志来实现,而此时又没有可用的日志空间,

这个作业当然要失败了。

那么此时怎么办?

既然是日志堵塞了,就想办法清理到这部分活动日志,尝试将事务日志标记为已分发(虽然这里是CDC,但是对于日志的使用应该是跟复制一样的)

  1. –将日志中复制的事务标识改为已分发
  2. EXEC SP_Repldone @xactid=NULL ,
  3. @xact_segno = NULL,
  4. @numtrabs = 0 ,
  5. @time = 0
  6. @reset = 1

据本人的测试,在执行上面的语句,将复制的事物标记为已分发之后,再次查看日志使用率,发现还是100%,但是尝试写入数据的时候是成功的,再次写入数据(一条即可)之后,日志空间开始释放,应该是写入时候的时候触发被标记为已分发的日志截断,也就是将上面占用了100%的日志空间释放出来然后再观察日志的使用率,发现如预期的,这部分日志已被截断,日志空间不再是被完全占用了,日志变成Nothing状态(可重用)

这个测试说明,如果开启了CDC,SQL Server代理没有正常启动或者对应的作业没有正常启动,日志空间会随着不断产生的事物被占满,导致数据库无法进行写入性操作

这里是用过手动标记日志为已分发的方式来释放日志的,这种情况下会导致cdc日志断裂的情况,也就是手动释放的日志无法传递到下游(cdc日志表)

毕竟不是一个太好的办法,下面会说明另外一种办法。

2,短时间内较大的事务性操作导致的日志空间被占满的情况

对去上面所说的代理服务被关闭导致日志堵塞的情况不同,这里直接开启代理服务,依旧拿着下面的脚本往表中写数据(比如实际业务中批量导入数据之类的)

在写入一段时间之后,依然出现了事务日志被填满的情况,这又是为什么?

还要从CDC的代理任务说起,这个代理的JOB虽然是连续执行的,但是因为上面写数据的时候也是连续写入的,也就是日志是连续产生的,

因为限制了日志文件的大小(这里为了方便演示,限制为512M),日志文件有最大使用空间的限制。

这里可以认为是一个Session消耗日志空间(Insert操作),一个进程解析日志之后释放日志空间(代理作业),

但是消耗的速度要高于释放的速度,一旦日志空间被使用完,CDC的代理作业也无法完成,

这样就又造成了上面的情况:日志空间被填满,数据库无法执行任何写入操作,CDC作业也无法执行从而释放可重用的日志空间,

上面是通过手动标记事务日志的状态来解决日志文件被填满的,

直接手动标记日志为已分发的做法是有点不合适的,

一旦标记日志状态为已分发,接下来他就不会传递给CDC的系统表或者订阅端了

这里通过另外一种方法来解决此问题:既然当前日志占满了,就在添加一个日志,注意新加日志初始化的空间不要太小。

(有兴趣测试的盆友,这里添加完日志文件后注意耐心等待一两分钟)然后随后的CDC作业会借助新加的这个日志空间会继续执行

此种情况说明,如果限制了日志的大小(或者存储日志的磁盘空间不足),数据库中开启了CDC或者复制,

一旦数据出现大批量持续性写入操作(增删改),此时会出现SQL Server代理解析并释放日志的速度跟不上,也有可能造成日志被占满的情况

3,不增加日志文件空间或者添加日志文件情况下重启SQLServer服务

这个办法也是本人在重现这一现象并尝试解决的时候试出来的,可行性不是太强,但还是说明一下,那就是重启大法,同时重启之后日志文件也发生了一些有意思的变化

建库的时候日志文件限制为最大512M,同时没有手动标记标记日志为已分发状态,但是重启SQLServer服务之后,如果存放日志的磁盘有空间,这个日志会自动扩充一部分

然后有了这部分扩充出来的日志,代理job就可以解析Replication状态的日志(之后)就可以释放日志空间了(需要一段时间来解析并释放日志,根据待复制的日志量有关)

下图可以明显看到,日志限制为512MB,但是初始化为556MB,明显大过最大日志大小,这个是归功于重启SQLServer服务的结果

一下是在SQL Server 2014 SP2版本下测试的现象,

如果是SQL Server 2014(非SP2补丁版),开启CDC的方式占满日志则不会出现如下的情况,也就是说重启有日志并不会自动扩充一部分,我也是醉了,验证个东西真不容易,这些小细节跟补丁版本也有关系,不过这种偏门的方法不能作为经验!

总结:

当开启了CDC之后,在相关表上的变化会写入事务日志(日志状态为Replication状态),代理任务会解析日志,解析完日之后标记日志为可重建状态(如果是简单还原模式,是可重用,如果是完整还原模式,日志备份也无法截断Replication状态的日志),这种状态下如果限制了日志的最大大小比较小,或者没有限制,存储日志的磁盘空间不足,在大批量写入数据(增删改)的时候,有可能产生的日志占满日志文件的情况,会导致释放日志的代理作业无法进行,代理作业无法进行又无法释放日志,仿佛是死循环。

此时要么新增日志文件或者增加日志文件的最大大小,要么通过执行系统存储过程sp_repldone来标记事务为已分发(标记事务日志可重用)来解决这一问题。

以上所述是小编给大家介绍的SQLServer数据库中开启CDC导致”事务日志空间被占满的原因分析和解决办法(REPLICATION),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

SQL Server中Rowcount与@@Rowcount的用法 - Nina - 博客园

mikel阅读(694)

select @@Rowcount

来源: SQL Server中Rowcount与@@Rowcount的用法 – Nina – 博客园

rowcount的作用就是用来限定后面的SQL在返回指定的行数之后便停止处理,比如下面的示例,

set rowcount 10
select * from 表A

这样的查询只会返回表A中的前10条数据。它和 “select top 10 * from 表A” 的作用一样。注意一点,set rowcount 的设置会在整个会话中有效。比如下面的SQL示例:

set rowcount 10
select * from 表A
go
select * from 表B

表A和表B都只会返回前10条数据。
要取消set rowcount的限定,只要设置 set rowcount 0 就可以了。

从上面的示例来看,好像rowcount没有多大的用处,限制查询结果的数据,我们使用top就可以了,而且还不用担心如果忘记取消rowcount的设置而对后面的sql的影响。 但在下面的情况下,rowcount的设置就会给我们带来很大的方便哦。

我们都知道select top 后面不能加参数,只能使用一个具体的int类型的数字。如果我们想实现top后面跟参数的功能,就只有构造sql字符串,然后使用exec来执行了。比如:

declare @n int
declare @sql nvarchar(1000)
set @n=10
set @sql=’select top ‘+cast(@n as varchar(10))+’ * from 表A’
exec(@sql)

先不说上面语句中exec的性能,单从sql的可读性上来看就很不友好。但如果我们使用rowcount来解决,就显的很优雅了,因为set rowcount后面是可以使用参数的。示例如下:

declare @n int
set @n=10
set rowcount @n
select * from 表A

注意:set rowcount的限定对修改,删除一样有效。比如下面的示例:

set rowcount 10
update 表a set qty=10 where id<100

这样,上面语句最多只会修改表a中id<100的前10条数据(假设id<100的数据数量大于10)

删除也是一样

set rowcount 10
delete from 表a

这样,上面的语句最多只会删除表a中前10条数据。

@@Rowcount的用法

@@Rowcount与Rowcount看起来很像,只相差了两个@,但它们的功能是不一样的,@@Rowcount主要是返回上次sql语句所影响的数据行数,比如:

select top 2 * from 表A
select @@Rowcount

如果表A中的数据量大于或等于2,那么select @@Rowcount就会返回2,如果只有1条或0条数据,那么select @@Rowcount就会返回1或者0。

注意,不要把@@Rowcount理解为只返回查询的结果数量,删除,修改,新增等语句,也会正确的返回@@Rowcount值。比如:

update 表A set gid=’a’ where gid=’a’
select @@Rowcount

如果表A中存在gid=’a’的数据,那么select @@Rowcount就会返回它所修改数据的行数,如果不存在gid=’a’的数据,那么select @@Rowcount就会返回0,删除与新增都是同样。

那么,哪些地方我们会用到@@Rowcount呢?
一、可能我们见到@@Rowcount身影最多的地方是触发器中,好的触发器,一般都会在最前面加上if @@rowcount=0 return语句,比如:

create trigger ti_tablea on tablea after update
as
if @@rowcount=0 return
……

这样,如果tablea被修改的数据行数为0,那么触发器ti_tablea就会直接退出,而不用执行后面的代码了。

二、第二个可能用到的地方就是我们可以使用@@rowcount来作递归或循环。比如下面示例:

declare @n int
set @n=1
select * from client_goods where id=@n

while @@rowcount>0
begin
set @n=@n+1
select * from client_goods where id=@n
end

这个示例是先查询client_goods中是否有id=1的数据,如果有,再查询是否有id=2的数据,一直查下去,直到id没有连续为止。当然大家在看这个示例的时候不要考虑这个示例的意义,它只是说明了@@rowcount可以作为循环条件来用。

rowcount与@@rowcount的其它用法,欢迎大家在评论中补充

 

http://www.lmwlove.com/ac/ID943

SqlServer 实现rownum 的功能_数据库_dragoo1的专栏-CSDN博客

mikel阅读(790)

来源: SqlServer 实现rownum 的功能_数据库_dragoo1的专栏-CSDN博客

SQLServer 实现rownum 的功能::

SQL Server 的语法:

SELECT TOP number|percent column_name(s)
FROM table_name

方法1:

with temp as

( select row_number() over(order by cityID) as rownum,cityNamefrom city )

select * from temp where rownum between 10 and 20

go

解释:

1 此方法把括号里的查询结果放到变量:temp 里面( 我也不确定是不是变量), 并用row_number()函数进行一个行号跟踪, 再用over 函数进行一个列的排序规则( 是这必须的), 并指定列名为’rownum’

2 紧接着在下面的语句可以对 ‘rownum’ 进行一个指定行号的查询

3 此批语句执行完毕后, 变量:temp 释放

方法2:

select identity(int,1,1) as rownum, cityName into #temp fromcity

select * from #temp where rownum between 10 and 20

go

解释:

此方法跟上面的差不多的意思, 只不过把 row_number() 函数换成了 identity() 函数

并把结果集放在一个临时表里面, 当批语句执行完毕, 此临时表还可以使用

—————————————

 

如何让SELECT 查询结果额外增加自动递增序号_SQL技巧

 

如果数据表本身并不内含自动地增编号的字段时,要怎么做才能够让SELECT查询结果如图表1所示,额外增加自动递增序号呢?我们提供下列五种方法供您参考:

USE北风贸易;
GO

软件开发网

 

SELECT序号= (SELECT COUNT(客户编号) FROM 客户 AS LiMing
WHERE LiMing.客户编号<= Chang.客户编号),
客户编号,公司名称
FROM客户 AS Chang ORDER BY 1;
GO

SELECT RANK() OVER (ORDER BY 客户编号 DESC) AS 序号,
客户编号,公司名称
FROM客户;
GO

SELECT序号= COUNT(*), LiMing.客户编号, LiMing.公司名称
FROM 客户 AS LiMing, 客户AS Chang
WHERE LiMing.客户编号>= Chang.客户编号
GROUP BY LiMing.客户编号, LiMing.公司名称
ORDER BY 序号;
GO

SELECT序号= IDENTITY(INT,1,1),管道,程序语言,讲师,资历
INTO #LiMing
FROM问券调查一;
GO
SELECT * FROM #LiMing;
GO
DROP TABLE #LiMing;
GO

WITH排序后的图书 AS
(SELECT ROW_NUMBER() OVER (ORDER BY 客户编号 DESC) AS 序号,
客户编号,公司名称
FROM 客户)
SELECT * FROM 排序后的图书
WHERE序号 BETWEEN 2 AND 4;
GO

 

——————————

例如:
在ORACLE中:
“select   *   from  ”   &  m_tablename   &  ”   WHERE  ROWNUM <=300 ”        取前三百行记录

“SELECT   ROWNUM   AS  序号,A.*   FROM  控制点查询信息   A    WHERE   A.查询编码= ‘888888 ‘  ORDER   BY  序号 ”
增加序号字段

但在SQL   SERVER  中不能运行,好像不识别ROWNUM。如果要替换(最好能直接替换的,因为本系统是ORACEL与SQL  SERVER都可以运行的),应该用什么啊?

释:

1.–rownum在oracle里面就是物理顺序,那么

SQLServer就直接select   top  300,不用管用什么排序

2.–查询后排序,例如:
select   序号=(select   count(1)  from   TableName  where   id<=a.id),*   from  TableName   a

3.select * from 表 where id >= ALL (select id from表);
select * from 表 where id <= ALL (select id from表);
select * from 表 where id = (select max(distinct id) from 表);
select * from 表 where id = (select min(distinct id) from 表);

转自:http://blog.sina.com.cn/s/blog_670dd1b10100kj5e.html

SQL优化案例—— RowNumber分页 - czperfectaction - 博客园

mikel阅读(807)

来源: SQL优化案例—— RowNumber分页 – czperfectaction – 博客园

将业务语句翻译成SQL语句不仅是一门技术,还是一门艺术。

下面拿我们程序开发工程师最常用的ROW_NUMBER()分页作为一个典型案例来说明。

先来看看我们最常见的分页的样子:

WITH CTE AS(
    SELECT    ROW_NUMBER() OVER ( ORDER BY (A.CreateTime ) AS OrderNo ,
        Table_A.ID ,    --主键
        Table_A.其它字段
    FROM      Table_A WITH ( NOLOCK )
    WHERE     RecID = 220051
) 
SELECT * FROM CTE
WHERE   OrderNo BETWEEN 1 AND 50;

的确,这样的写法很符合我们的思维逻辑,并且我们在RecID上建立非聚集索引,那么它的效率看上去也是不错的。当然根据这条SQL,最佳索引实践应该是:

CREATE INDEX IX_Table_A_RecID_CreateTime_Inc
ON Table_A(RecID,CreateTime)
INCLUDE(Table_A.其它字段)

但是,这真的是最佳的了吗?当SQL的Where条件变多,Table_A.其它字段变得越来越多,OVER()子句中的OrderBy字段越来越多或者变成Order By ColumnA/ColumnB这样的计算表达式,这条语句变得越来越不堪重负,最终性能问题凸现出来,另外,作为DBA,我们总是尽量维持索引的简单性、可重用度,而不想建立成为某个语句专用的索引。举例来说,在Include中,我们总不能把Table_A.其它字段中的所有字段都放进去吧,个数少还行,如果遇上几十个字段或者有大容量字符字段,维护成本将大大增加,那将是我们不愿意看到的。

这个时候就要求我们看看是否能对语句做出一些优化了。
在上面的SQL中,我们看它的执行计划,我已经建立了索引,该索引并未Include SELECT列表中的其它字段:
CREATE INDEX IX_Table_A_RecID_CreateTime_Inc
ON Table_A(RecID,CreateTime)
image

根据上图的执行计划,可以看到,WHERE条件走的是我刚刚建立的索引,下面的键查找与其并行,我们先不讨论该执行计划的具体细节,下面我们来设想几个问题:

在WHERE条件简单,并且索引合适,统计信息正确的前提下,SQL Server可以很容易获得那50行,并且回到聚集索引中找到属于它的其它字段的数据,这是SQL Server的智能编译的结果,也是我们希望看到的返回方式。

但是,在WHERE条件较为复杂,多个WHERE条件均为范围字段或者状态字段时,执行计划也许并没有我们想象的那么智能了,比如它可能采用这样的方式:

image

当SQL Server无法准确的取出你要的那些行时,那么它便会取回全部的行数后,再去聚集索引中找回属于它的其它字段的数据,当where条件可以返回几十万数据时,你可以想象它的效率有多低,它会仍然使用上文中类似的执行计划,这显然不是我们希望看到的。

我们想看到的是什么?

1、根据WHERE条件和排序规则,先取出那50条数据所属的主键。

SELECT ROW_NUMBER() OVER ( ORDER BY A.CreateTime ) AS OrderNo ,
Table_A.ID –主键

INTO #1
FROM Table_A WITH ( NOLOCK )
WHERE RecID = 220051

2、利用上个步骤中返回的主键,去原始表取回这50条记录的其它字段数据。

SELECT B.*,A.其它字段 FROM Table_A A WITH ( NOLOCK )
    INNER JOIN #1 B ON A.ID=B.ID
WHERE   B.OrderNo BETWEEN 1 AND 50;

那么,上面两个步骤合在一起:

WITH CTE AS(
    SELECT    ROW_NUMBER() OVER ( ORDER BY A.CreateTime ) AS OrderNo ,
        Table_A.ID    --主键
    FROM      Table_A WITH ( NOLOCK )
    WHERE     RecID = 220051
) 
SELECT CTE.*,A.其它字段 FROM Table_A A WITH ( NOLOCK )
    INNER JOIN CTE ON A.ID=CTE.ID
WHERE   CTE.OrderNo BETWEEN 1 AND 50;

很好,现在我们再来看一下这个SQL的执行计划:

image

Binggo!这才是我们理想中的样子!

针对这个SQL,我们只需要建立一个合适的索引,而不用顾忌SELECT列表中那些烦人的其它列,因为他们回聚集索引取数据,也不过几百个IO而已(需要返回的行数*Index_Level)。它不需要再为过期的统计信息或者错误的执行计划而付出沉重的代价!

总结:SQL优化,是一门艺术。

 

WITH查询(公共表表达式)

mikel阅读(703)

来源: WITH查询(公共表表达式)

7.8. WITH查询(公共表表达式)

WITH提供了一种方式来书写在一个大型查询中使用的辅助语句。这些语句通常被称为公共表表达式或CTE,它们可以被看成是定义只在一个查询中存在的临时表。在WITH子句中的每一个辅助语句可以是一个SELECTINSERTUPDATEDELETE,并且WITH子句本身也可以被附加到一个主语句,主语句也可以是SELECTINSERTUPDATEDELETE

7.8.1. WITH中的SELECT

WITHSELECT的基本价值是将复杂的查询分解称为简单的部分。一个例子:

WITH regional_sales AS (
        SELECT region, SUM(amount) AS total_sales
        FROM orders
        GROUP BY region
     ), top_regions AS (
        SELECT region
        FROM regional_sales
        WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales)
     )
SELECT region,
       product,
       SUM(quantity) AS product_units,
       SUM(amount) AS product_sales
FROM orders
WHERE region IN (SELECT region FROM top_regions)
GROUP BY region, product;

它只显示在高销售区域每种产品的销售总额。WITH子句定义了两个辅助语句regional_salestop_regions,其中regional_sales的输出用在top_regions中而top_regions的输出用在主SELECT查询。这个例子可以不用WITH来书写,但是我们必须要用两层嵌套的子SELECT。使用这种方法要更简单些。

可选的RECURSIVE修饰符将WITH从单纯的句法便利变成了一种在标准SQL中不能完成的特性。通过使用RECURSIVE,一个WITH查询可以引用它自己的输出。一个非常简单的例子是计算从1到100的整数合的查询:

WITH RECURSIVE t(n) AS (
    VALUES (1)
  UNION ALL
    SELECT n+1 FROM t WHERE n < 100
)
SELECT sum(n) FROM t;

一个递归WITH查询的通常形式总是一个非递归项,然后是UNION(或者UNION ALL),再然后是一个递归项,其中只有递归项能够包含对于查询自身输出的引用。这样一个查询可以被这样执行:

递归查询求值

  1. 计算非递归项。对UNION(但不对UNION ALL),抛弃重复行。把所有剩余的行包括在递归查询的结果中,并且也把它们放在一个临时的工作表中。
  2. 只要工作表不为空,重复下列步骤:
    1. 计算递归项,用当前工作表的内容替换递归自引用。对UNION(不是UNION ALL),抛弃重复行以及那些与之前结果行重复的行。将剩下的所有行包括在递归查询的结果中,并且也把它们放在一个临时的中间表中。
    2. 用中间表的内容替换工作表的内容,然后清空中间表。

注意: 严格来说,这个处理是迭代而不是递归,但是RECURSIVESQL标准委员会选择的术语。

在上面的例子中,工作表在每一步只有一个行,并且它在连续的步骤中取值从1到100。在第100步,由于WHERE子句导致没有输出,因此查询终止。

递归查询通常用于处理层次或者树状结构的数据。一个有用的例子是这个用于找到一个产品的直接或间接部件的查询,只要给定一个显示了直接包含关系的表:

WITH RECURSIVE included_parts(sub_part, part, quantity) AS (
    SELECT sub_part, part, quantity FROM parts WHERE part = 'our_product'
  UNION ALL
    SELECT p.sub_part, p.part, p.quantity
    FROM included_parts pr, parts p
    WHERE p.part = pr.sub_part
  )
SELECT sub_part, SUM(quantity) as total_quantity
FROM included_parts
GROUP BY sub_part

在使用递归查询时,确保查询的递归部分最终将不返回元组非常重要,否则查询将会无限循环。在某些时候,使用UNION替代UNION ALL可以通过抛弃与之前输出行重复的行来达到这个目的。不过,经常有循环不涉及到完全重复的输出行:它可能只需要检查一个或几个域来看相同点之前是否达到过。处理这种情况的标准方法是计算一个已经访问过值的数组。例如,考虑下面这个使用link域搜索表graph的查询:

WITH RECURSIVE search_graph(id, link, data, depth) AS (
        SELECT g.id, g.link, g.data, 1
        FROM graph g
      UNION ALL
        SELECT g.id, g.link, g.data, sg.depth + 1
        FROM graph g, search_graph sg
        WHERE g.id = sg.link
)
SELECT * FROM search_graph;

如果link关系包含环,这个查询将会循环。因为我们要求一个“depth”输出,仅仅将UNION ALL 改为UNION不会消除循环。反过来在我们顺着一个特定链接路径搜索时,我们需要识别我们是否再次到达了一个相同的行。我们可以项这个有循环倾向的查询增加两个列pathcycle

WITH RECURSIVE search_graph(id, link, data, depth, path, cycle) AS (
        SELECT g.id, g.link, g.data, 1,
          ARRAY[g.id],
          false
        FROM graph g
      UNION ALL
        SELECT g.id, g.link, g.data, sg.depth + 1,
          path || g.id,
          g.id = ANY(path)
        FROM graph g, search_graph sg
        WHERE g.id = sg.link AND NOT cycle
)
SELECT * FROM search_graph;

除了阻止环,数组值对于它们自己的工作显示到达任何特定行的“path”也有用。

在通常情况下如果需要检查多于一个域来识别一个环,请用行数组。例如,如果我们需要比较域f1f2

WITH RECURSIVE search_graph(id, link, data, depth, path, cycle) AS (
        SELECT g.id, g.link, g.data, 1,
          ARRAY[ROW(g.f1, g.f2)],
          false
        FROM graph g
      UNION ALL
        SELECT g.id, g.link, g.data, sg.depth + 1,
          path || ROW(g.f1, g.f2),
          ROW(g.f1, g.f2) = ANY(path)
        FROM graph g, search_graph sg
        WHERE g.id = sg.link AND NOT cycle
)
SELECT * FROM search_graph;

提示: 在通常情况下只有一个域需要被检查来识别一个环,可以省略ROW()语法。这允许使用一个简单的数组而不是一个组合类型数组,可以获得效率。

提示: 递归查询计算算法使用宽度优先搜索顺序产生它的输出。你可以通过让外部查询ORDER BY一个以这种方法构建的“path”,用来以深度优先搜索顺序显示结果。

当你不确定查询是否可能循环时,一个测试查询的有用技巧是在父查询中放一个LIMIT。例如,这个查询没有LIMIT时会永远循环:

WITH RECURSIVE t(n) AS (
    SELECT 1
  UNION ALL
    SELECT n+1 FROM t
)
SELECT n FROM t LIMIT 100;

这会起作用,因为PostgreSQL的实现只计算WITH查询中被父查询实际取到的行。不推荐在生产中使用这个技巧,因为其他系统可能以不同方式工作。同样,如果你让外层查询排序递归查询的结果或者把它们连接成某种其他表,这个技巧将不会起作用,因为在这些情况下外层查询通常将尝试取得WITH查询的所有输出。

WITH查询的一个有用的特性是在每一次父查询的执行中它们只被计算一次,即使它们被父查询或兄弟WITH查询引用了超过一次。因此,在多个地方需要的昂贵计算可以被放在一个WITH查询中来避免冗余工作。另一种可能的应用是阻止不希望的多个函数计算产生副作用。但是,从另一方面来看,优化器不能将来自父查询的约束下推到WITH查询中而不是一个普通子查询。WITH查询通常将会被按照所写的方式计算,而不抑制父查询以后可能会抛弃的行(但是,如上所述,如果对查询的引用只请求有限数目的行,计算可能会提前停止)。

以上的例子只展示了和SELECT一起使用的WITH,但是它可以被以相同的方式附加在INSERTUPDATEDELETE上。在每一种情况中,它实际上提供了可在主命令中引用的临时表。

7.8.2. WITH中的数据修改语句

你可以在WITH中使用数据修改语句(INSERTUPDATEDELETE)。这允许你在同一个查询中执行多个而不同操作。一个例子:

WITH moved_rows AS (
    DELETE FROM products
    WHERE
        "date" >= '2010-10-01' AND
        "date" < '2010-11-01'
    RETURNING *
)
INSERT INTO products_log
SELECT * FROM moved_rows;

这个查询实际上从products把行移动到products_logWITH中的DELETE删除来自products的指定行,以它的RETURNING子句返回它们的内容,并且接着主查询读该输出并将它插入到products_log

上述例子中好的一点是WITH子句被附加给INSERT,而没有附加给INSERT的子SELECT。这是必需的,因为数据修改语句只允许出现在附加给顶层语句的WITH子句中。不过,普通WITH可见性规则应用,这样才可能从子SELECT中引用到WITH语句的输出。

正如上述例子所示,WITH中的数据修改语句通常具有RETURNING子句。它是RETURNING子句的输出,不是数据修改语句的目标表,它形成了剩余查询可以引用的临时表。如果一个WITH中的数据修改语句缺少一个RETURNING子句,则它形不成临时表并且不能在剩余的查询中被引用。但是这样一个语句将被执行。一个非特殊使用的例子:

WITH t AS (
    DELETE FROM foo
)
DELETE FROM bar;

这个例子将从表foobar中移除所有行。被报告给客户端的受影响行的数目可能只包括从bar中移除的行。

数据修改语句中不允许递归自引用。在某些情况中可以采取引用一个递归WITH的输出来操作这个限制,例如:

WITH RECURSIVE included_parts(sub_part, part) AS (
    SELECT sub_part, part FROM parts WHERE part = 'our_product'
  UNION ALL
    SELECT p.sub_part, p.part
    FROM included_parts pr, parts p
    WHERE p.part = pr.sub_part
  )
DELETE FROM parts
  WHERE part IN (SELECT part FROM included_parts);

这个查询将会移除一个产品的所有直接或间接子部件。

WITH中的数据修改语句只被执行一次,并且总是能结束,而不管主查询是否读取它们所有(或者任何)的输出。注意这和WITHSELECT的规则不同:正如前一小节所述,直到主查询要求SELECT的输出时,SELECT才会被执行。

The sub-statements in WITH中的子语句被和每一个其他子语句以及主查询并发执行。因此在使用WITH中的数据修改语句时,指定更新的顺序实际是以不可预测的方式发生的。所有的语句都使用同一个snapshot执行(参见第 13 章),因此它们不能“看见”在目标表上另一个执行的效果。这减轻了行更新的实际顺序的不可预见性的影响,并且意味着RETURNING数据是在不同WITH子语句和主查询之间传达改变的唯一方法。其例子

WITH t AS (
    UPDATE products SET price = price * 1.05
    RETURNING *
)
SELECT * FROM products;

外层SELECT可以返回在UPDATE动作之前的原始价格,而在

WITH t AS (
    UPDATE products SET price = price * 1.05
    RETURNING *
)
SELECT * FROM t;

外部SELECT将返回更新过的数据。

在一个语句中试图两次更新同一行是不被支持的。只会发生一次修改,但是该办法不能很容易地(有时是不可能)可靠地预测哪一个会被执行。这也应用于删除一个已经在同一个语句中被更新过的行:只有更新被执行。因此你通常应该避免尝试在一个语句中尝试两次修改同一个行。尤其是防止书写可能影响被主语句或兄弟子语句修改的相同行。这样一个语句的效果将是不可预测的。

当前,在WITH中一个数据修改语句中被用作目标的任何表不能有条件规则、ALSO规则或INSTEAD规则,这些规则会扩展成为多个语句。

sql server 2008 rownumber 分页sql语句 - zyf - 博客园

mikel阅读(656)

来源: sql server 2008 rownumber 分页sql语句 – zyf – 博客园

SELECT
ID,TSRID, plid,CustomerName,SellCode,Mobile ,ReserveTime,CrtTime,hjtime,jieshuma ,
InsureBillCode, tname,bname,shoucang,xingbie,ActivityName ,sydate,AddInsuranceID
FROM
———————————————–
(

SELECT *,ROW_NUMBER() OVER (ORDER BY hjtime desc) AS RowNo

FROM [C_V_SellData]

where
CrtUser=358
and StoreType=1
and EndCode <> 1
and EndCode <> 5
and hjtime  >= cast(convert(varchar,’2014/1/18′,111) as datetime)
and hjtime <  cast(convert(varchar,’2014/2/18′,111) as datetime) + 1
and isnull(AddInsuranceID,”)<>’N’  ) AS A

————————————————–
WHERE   RowNo>=1  and RowNo<=50

Sql Server RowNumber和表变量分页性能优化小计 - 寻自己 - 博客园

mikel阅读(522)

来源: Sql Server RowNumber和表变量分页性能优化小计 – 寻自己 – 博客园

直接让代码了,对比看看就了解了

当然,这种情况比较适合提取字段较多的情况,要酌情而定

 

性能较差的:

WITH #temp AS
(                                   
Select column1,column2,column3,column4,column5,column6,column7,column8,column9,column10, row_number() over (Order by column100 desc) as RowNumber                                    
From tables1
Where .....                                 
)                                    
SELECT  *
FROM #temp
WHERE RowNumber BETWEEN 1601 and 1620 
ORDER BY RowNumber

 

优化后,性能较好的

WITH #temp AS
(                                   
Select column1, row_number() over (Order by column100 desc) as RowNumber                                    
From tables1
Where .....                                 
)                                    
SELECT  column1,column2,column3,column4,column5,column6,column7,column8,column9,column10
FROM tables1
where column1 in
(
select column1 from #temp
WHERE RowNumber BETWEEN 1601 and 1620                   
ORDER BY RowNumber
)

 

相关文章:Sql Server 2012 分页方法分析(offset and fetch)
原文地址:Sql Server RowNumber和表变量分页性能优化小计