MySQL事务——万字详解
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL事务——万字详解相关的知识,希望对你有一定的参考价值。
目录
1.5.5 begin开启的事务不会受MySQL事务提交方式的影响
前言
mysql是一个网络服务。大多数情况下,会有很多客户端连接MySQL服务。当多个客户端访问同一个表时,可能会出现问题。
比如:火车票售票系统,当两个客户端同时买票,操作同一张票数表。当客户端A检测还有一张票,将票买掉,但是还没有更新数据库。于此同时,客户端B,也在买票,也检测到还有一张票,客户端B也将票买了。这样就导致一张票被卖了两次。
于是MySQL需要对此现象加以控制。这就是事务解决的问题。
要解决上面的问题,至少需要满足下面的属性(拿买票的过程举例):
- 买票的过程得是原子的
- 多个客户端买票,互相之间不能影响
- 买完票后,数据应该是永久有效的
- 买票前各个客户端之间看到的票数要是一样的;买票后,各个客户端看到的票数是一样的
一.事务的概念
1.1 什么是事务
事务就是一组DML类的SQL语句,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。
简单来说,事务就是要完成一件事,所用到的所有SQL语句(至少一条)。这些语句要么全部执行成功,要么全部执行失败。即,其中一条执行失败,就全部执行失败。数据立马回滚到执行前的状态。
1.2 事务的属性
- 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成。不会结束在中间的某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务没有发生一样。
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。可以理解成,事务执行完后,就将数据刷新到磁盘中了。但是,这并没有这么简单,因为,MySQL是是一个应用程序,数据刷新需要操作系统来做。故MySQL需要先将数据放到内核缓冲区中,再由操作系统将数据刷新到磁盘。
- 隔离性:数据库允许多个事务并发运行,并且允许事务同时对数据进行读,写和修改。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据不一致的情况。但是,MySQL为了考虑效率,多个事务读,写或者修改同一数据时,并不一定是串行运行的。而是分成了不同的级别:读未提交(read uncommitted),读提交(read committed),可重复读(repeatable read)和串行化(serializable)。
- 一致性:在事务开始前和事务结束后,数据库的完整性。并且事务执行后,会完全符合结果。这需要MySQL和用户共同来保证的。即,MySQL保证了数据保护出错,用户需要保存流程正确。
上面四个属性,可以简称ACID。
- 原子性(Atomicity,又称不可分割性)
- 一致性(Consistency)
- 隔离性(Isolation,又称独立性)
- 持久性(Durability)
事务并不是伴随着数据库系统天生就有的,而是为应用层的服务的。
这样就使得用户在操作数据库时不需要考虑数据的安全问题,简化了编程模型。
1.3 事务的版本支持
事务是存储引擎提供的,在MySQL中只有使用了innodb存储引擎的数据库或者表才支持事务,MyISAM存储引擎不支持事务。
查看数据库存储引擎:
1.4 事务提交方式
事务提交方式有两种:
- 手动提交
- 自动提交
查看事务的提交方式:
修改事务的提交方式:
1.5 事务的常见操作
1.5.1 准备阶段:
手动启动一个事务:
begin/start transaction;--开始一个事务
...--对表进行操作
commit;--提交事务
设置隔离级别为读未提交,后面在隔离性有详细解释。
注意:设置完global.transaction_isolation,需要重启MySQL。
创建表:
create table if not exists account(
id int primary key,
name varchar(50) not null default '',
blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;
1.5.2 手动演示回滚操作
创建保存点:savepoint 保存点名;
回滚:rollback 回滚的保存点;
1.5.3 简单证明原子性
注意:现在的隔离等级是读为提交。
事务没有执行完发生了错误,会回滚到最开始,相当于这个事务没有发生一样。
1.5.4 简单证明持久性
当一个事务完成后,操作完的数据会永久保存,系统是否故障。
1.5.5 begin开启的事务不会受MySQL事务提交方式的影响
结论:begin开启的事务,不受MySQL事务提交方式的影响,必须手动commit提交。
1.5.6 MySQL中SQL与事务的关系
结论:在MySQL中没有手动begin开启事务,增删查改都会被MySQL封装成一个事务,即使只有一条SQL语句。
提交方式为:自动提交。
提交方式为:手动提交
正确:
1.5.7 总结
- 事务可以手动回滚,同时,操作异常会自动回滚。
- 使用begin/start transaction开启的事务,必须通过commit才能提交,才会持久化,与MySQL的提交方式无关。
- 对于MySQL存储引擎使用innodb,每一条SQL语句都会默认封装成事务,提交方式看auto_commit是否开启。
注意点:
- 如果没有设置保存点,也可以回滚,只能回滚到事务最开始。直接使用rollback,前提是事务还没有提交。
- 如果一个事务被提交(commit),则不可以回滚。
- 可以选择回滚到哪个保存点。
- Innodb可以支持事务,MyISAM不支持事务。
- 手动开始事务使用begin,或start transaction。
二.隔离级别
再次说明,MySQL是一个网络服务,同一时间可能有很多客户端连接。那么在多个事务在执行多个SQL时,有可能出现问题,比如:多个事务访问同一张表,同一行数据时。
一个事务的时间段可以分为执行前(该事务还没有开始),执行中(该事务正在执行),执行后(该事务已经提交)。
隔离性:保证了事务执行过程中尽量不受干扰。
为了提高效率,执行过程并不是串行化,而是允许事务受到不同程度的干扰,于是就有了一个重要的特征:隔离级别。
2.1 隔离级别介绍
注意:隔离级别是针对事务之间的。
- 读未提交(read uncommitted):在该隔离级别下,所有事务都能看到其它事务没有提交的执行结果。相当于没有任何隔离性。在实际生产中不会使用这一隔离级别,因为会有很多并发问题,如脏读,幻读,不可重复度等。
- 读提交(read committed):事务只能看到其它事务已经提交后执行结果。这种隔离级别是大多数数据库的默认隔离级别(不是MySQL的),这种隔离级别会引起不可重复读的问题。
- 可重复度(repeatable read):在该隔离级别下,在一时间段同时运行的事务,执行中,看不到其它事务已经提交的执行结果,必须等事务执行完才能看到其它事务提交后执行结果。可能会出现幻读的问题。这是MySQL的默认隔离级别。
- 串行化(serialization):强制事务在增删改同一张表的同一行时排序执行,使之不可能相互冲突。实际,它是在访问的每一个数据行上加了共享锁。这是事务最高隔离级别。但是这个的效率很低,可能导致超时和锁竞争。(这种隔离太极端,生活中基本不使用)
隔离基本上都是通过加锁来实现的,不同隔离级别,锁的使用是不同的。常见的有,表锁,行锁,读锁,写锁,间隙锁,Next-key锁(GAP+行锁)。
读提交和可重复读的区别:
有两个事务,事务A和事务B。
事务A修改数据,提交后(commit)。读提交,事务B在提交前(commit)可以看到事务A修改后的数据。可重复读,事务B在提交前(commit)看不到事务A修改后的数据,事务B在提交后,才能看到。
2.2 查看和设置隔离性
查看隔离性:
设置隔离等级:
这是设置当前会话或者全局隔离级别的语法。
set [session | global] transaction isolation level READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE;
设置当前会话隔离性,另起一个会话,隔离性不会被设置,只影响当前会话。
设置全局隔离级别,另起一个会话,会被影响。
会话隔离级别开始启动时,会和全局隔离级别一样。
设置了全局会话隔离级别,当前会话隔离级别并没有改变,需要重启MySQL才会改变当前会话隔离级别。
修改当前会话隔离级别:
- 直接修改会话隔离级别
- 修改全局隔离级别,重启MySQL
2.3 操作演示
隔离级别时针对事务和事务之间的。
2.3.1 读未提交
现象:如果隔离级别为读未提交,一个事务A增删改表的内容,未提交事务,在另外一个事务B中可以看到变化的内容。
但是,如果事务A没有提交异常退出了,内容会发生回滚,回滚到事务开始前的内容,原因是事务的原子性。事务A提交了,事务B,提交后也可以看到修改之后的内容,这是应为事务的持久性。
一个事务执行中,读到了另外一个事务的增删改,但是没有commit的数据,这种现象叫做脏读。
2.3.2 读提交
现象:两个事务,事务A和事务B,隔离等级为读提交。事务A增删改表的内容,未提交事务,在事务B中,看不到增删改的内容。当事务A提交后,事务B中可以看到增删改的内容。注意事务B没有提交。
问题:此时在事务B中,没有提交事务。但是同样的读取,在同一个事务内,在不同的时间段,读取到的值不同,这种现在叫做不可重复读取。
2.3.3 可重复读
现象:两个事务,事务A和事务B。事务A增删改表的内容,未提交,事务B看不到修改后的数据;事务A提交后,事务B没有提交,仍然看不到提交的数据;事务B提交后,由于持久化,可以看到修改后的数据。
注意:并不是,同一时段的两个事务,事务A提交的数据,事务B就一定看不到。这个取决于select 的位置。下面详细解释了。
问题:
幻读:在事务只执行中,不同时间段查询,会查找出来新的记录。即,事务A插入一条数据,事务B在未提交前,看到了插入的数据。
但是,在下面的演示中,在事务A向表中插入数据,事务B中在未提交前没有看到插入的数据。这是因为MySQL解决了幻读的问题。
但是,一般的数据库,在事务未提交前能够读到其它数据插入的数据。这是为什么呢?
因为隔离性实现是对数据加锁实现的,而insert数据,插入的数据,在表中并不存在,一般的加锁无法屏蔽这里问题。
而MySQL是如何解决的呢?
MySQL用的是Next-Key锁(GAP+行锁)解决的。
2.3.4 串行化
现象:当前有多个事务时,一个事务要增删改表的数据,会发生阻塞。当其它数据全部退出,才能正常进行增删改操作。使得增删改时,事务之间运行是串行的。效率比较低。
但是:如下图,我们发现,查找,并没有串行化,有多个事务同时运行时,查找不会被阻塞,串行化。这是因为查询并不会修改数据。
2.3.5 总结
- 隔离等级越高,安全性越高,并发性越低,因为加锁。往往要在两者之间找一个平衡点。
- 不可重复读是,同一条件下,事务在执行过程中,不同时间段,读取的数据不同。
- 幻读是,同样条件下,事务在执行过程中,不同时间段,读出来,数据量比之前增加了。
- 脏读:同样条件下,事务在执行过程中,读到了其它事务修改的数据。
- 事务也有长短的概念,事务之间相互影响,指的是,在事务执行过程中,都没有提交(commit),影响会比较大。
针对MySQL:
隔离级别 | 脏读 | 幻读 | 不可重复读 |
读未提交【
Read Uncommitted
】
| 有 | 有 | 有 |
读提交【
Read Committed
】
| 没有 | 有 | 有 |
可重复读【
Repeatable Read
】
| 没有 | 没有 | 没有 |
串行化【
serializable
】
| 没有 | 没有 | 没有 |
注意:
当多个事务,同时进行update, insert或者delete时,是会有加锁现象的。即,会发生阻塞。但是select查询和增删改并不冲突,即不会发生阻塞。这是通过读写锁(锁由行锁或者表锁)+MVCC完成的。
这里不好演示,直接给了结论。
2.4 一致性
- 事务执行结果,必须使得数据库从一个一致性的结果,变成另外一个一致性状态。如果系统运行时发生中断,某个事务,被迫中断,而未完成事务对数据的修改,此时数据库出于一种不一致的状态。
- 一致性性就是,在事务开始前,数据是一致的,事务完成后,数据也是一致的。
- 其实一致性和用户业务的逻辑性相关,需要MySQL提供技术支持,保证数据不会出错。还需要用户业务逻辑上的支持,在MySQL文档中,一致性,是由用户来决定的。
- 技术上,数据库的原子性,隔离性,持久性保证了一致性。
三.解决并发的原理MVCC
对数据库操作实际上就是对数据库进行读和写的操作,并发就是,多个事务同时对数据库进行读和写操作。而数据库正是对在效率和安全方面的考虑,解决读和写并发的问题。
3.1 数据库并发的三种场景
- 读和读:不会存在任何问题,不需要并发的控制。不会修改数据。
- 写和写:有安全问题,可能会存在数据更新丢失,比如:第一类更新丢失,第二类更新丢失。如事务A更新了数据,事务B回滚导致事务A更新的数据丢失了。需要加锁,串行运行。
- 读和写:有安全问题,但是为了效率的考虑,不一定加锁,串行运行。总和考虑效率和安全,所以有了隔离等级,但是,仍然可能有其它问题,脏读,幻读,不可重复读的问题。
读和读,写和写的问题好处理,但是读和写的问题就比较麻烦,下面主要讨论如何解决读和写并发问题。
3.2 读和写
多版本并发控制(Multiversion concurrency control),简称MVCC,是一种解决读写冲突的无锁并发控制。
主要是:为事务分配单向增长的事务ID,为每一个修改保存一个版本,版本与事务ID关联,读操作只读改事务开始前的数据的快照(快照,后面有解释,需要先理解,才能理解)。所以MVCC可以为数据库解决以下问题:
- 在并发读写数据库时,可以做到在读操作时,不用阻塞写操作,写操作时也不用阻塞读操作,因为读写的数据不是一个版本。提高了数据库并发读写的性能。
- 同时还解决了脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。
注意:没有同时启动的事务,事务的启动一定会有先后的。所以每个事务的ID一定不同。
想要理解MVCC需要知道三个前提知识:
- 3个记录隐藏字段
- undo日志
- read view
3.2.1 三个隐藏字段
当我们在数据库中创建一个表时,表中的字段不仅会有我们设计的字段,还有有几个隐藏字段。
这里主要介绍三个:
- DB_TRX_ID:6字节,保存最近修改(修改/插入)改数据的事务的ID
- DB_ROLL_PTR:7字节,回滚指针,执行这条记录的上一个版本。
- DB_ROW_ID:6字节,隐含的自增ID(隐藏主键)。在innodb存储引擎中,如果数据没有主键,innodb会自动创建一个隐藏主键来构建索引。这个隐藏主键就是DB_ROW_ID字段记录的主键。
补充:实际上还会有一个删除flag隐藏字段,即记录删除。实际上的删除一个字段,并不是真正的删除,而是将flag设置为删除标记。
演示:
此时:表的全部信息为:
我们目前并不知道创建该记录的事务ID,隐式主键。默认设置成NULL和1。第一条记录之前也没有其它版本,我们设置回滚指针为NULL。
3.2.2 undo日志
undo日志,实际上时MySQL内存缓冲区里的一段空间,即buffer pool中的一段空间,用来保存日志数据,之后再刷新到磁盘中。
MySQL是一个服务进程,所有操作都需要在内存中完成的。比如:修改数据,先将数据拿到内存中,修改后,再刷新到磁盘中。日志也是一样,先将日志信息写到MySQL内部缓冲区中,之后再刷新到磁盘中。
3.2.3 模拟MVCC过程
说明:现在有一个事务,事务ID为10,对student表中的字段做的修改(update),将name从张三,修改成了李四。
过程:
- 事务10,因为要修改字段,先给记录加上行锁。防止有其它记录同时修改。
- 修改前,先将当前记录拷贝到undo log中。
- 所以现在MySQL中有了两行相同的记录。现在修改原始记录,将名字改为李四。
- 修改隐藏字段DB_TRX_ID为当前事务10的事务ID,
- 修改回滚指针DB_ROLL_PTR,里面填入之前拷贝数据的起始地址,从而指向副本数据,表明这是上一个版本。
- 事务10提交,释放锁。
现在又有一个事务11,对student表进行修改(update):将年龄age由28改成38。
注意:修改,修改的是最新记录。
- 事务11,因为要修改,给最新记录加上行锁。
- 修改前,将当前记录拷贝到undo log中,产生新的副本,我们采用头插的方式,插入到undo log中。
- 现在修改原始记录中的年龄,改成38。
- 修改隐藏字段DB_TRX_ID为当前事务ID11.
- 修改回滚指针DB_ROLL_PTR列,保存undo log中新副本的起始地址,从而指向副本记录,表示上一个版本就是它。
- 事务11提交,释放锁。
于是这样就是形成了一个基于链表记录的版本链。所谓回滚,就是,用历史数据,覆盖当前数据。
快照:就是undo log中的一个个版本。
注意:只有在增删改动作的时候,才会形成快照。
上面主要讲的是修改数据undo log
如果是delete呢?删除数据了,这个字段就没有了,如何生成这个字段的快照呢?
上面补充的时候说了,在表中还有一个隐藏字段flag,来标记当前字段是否被删除。所以删除数据,并不是将数据真正删除了,而是将flag字段,设置成了删除标记。所以,删除字段也可以生成快照,保存在undo log中。
如果是insert呢?插入数据,之前没有插入数据前面没有历史版本,如果要回滚到插入前怎么办的?
起始在MySQL中由记录数据的log,就像上面一样。还由基于语句的log,记录相反的语句。比如:insert一条数据,在log中就会记录delete的语句。
如果是select呢?
select不会对数据进行修改,所以不需要为select维护多版本。
select读取的时候,是读取的最新数据还是,快照呢?
select既可以读取最新数据,也可以读取快照。
当前读:读取最新数据。增删改,都是当前读,因为需要修改最新的数据。select也可以当前读,(select lock in share mode,select for update),select当前读时,就需要对数据加锁,因为增删改也是当前读,避免数据错误。
快照读:读取快照。增删改是当前读,如果select采用快照读,这样不需要加锁,可以实现读写并发,这就是MVCC的意义所在。
那什么决定了select快照读还是当前读?
隔离级别决定的。不同的隔离级别,访问的数据版本不同。
比如:读提交:每次都可以访问到修改后的数据,当前读。读未提交:访问不到需改,但是未提交的数据,快照读。
事务操作哪个版本是在事务启动时确定的,事务总有先后。事务启动时,会分配一个事务id,通过对比事务id来确定操作哪个快照。
3.2.4 read view
Read view是事务进行快照读操作的时候生产的读视图(read view)。决定了这个select可以看到多少版本数据。这个读视图中,维护了系统当前活跃事务的id。
简单来说就是,在我们某个事务进行快照读时,对该记录创建一个read view,把他当作条件,用来判断当前事务能够看到哪个版本的数据。即,可能时最新版本,也可能是undo log中的版本。
在read view中主要由以下四个成员:
- m_ids:相当于一个数组。记录read view生成时刻,系统正在执行的事务。
- up_limit_id:记录m_ids列表中事务最小的ID。
- low_limit_id:read view生成时刻,系统尚未分配的下一个事务ID。
- creator_trx_id:创建read view的事务id。
我们读取数据版本链时,能够获得每一个版本对应的事务ID。即:DB_TRX_ID。
我们只需要拿read_view和DB_TRX_ID对比,就可以知道可以读取的id。
最终根据隔离级别,可以获得要读取的id。
-
Creator_trx_id == DB_TRX_ID || DB_TRX_ID < up_limit_id
在形成read view前已经提交的事务,形成的版本链数据,可以看到。
-
Up_limit_id <= DB_TRX_ID < low_limit_id
在形成read view时,正在执行的事务id保存在m_ids中。不在m_id中说明在形成快照前提交了,但是仍然在up_limit_id和low_limit_id范围内。
注意:m_ids中的事务id不一定是连续的,可能由中间的事务ID已经提交了。比如:我们有11,12,13,14,15号事务。在形成快照前12,14号事务提交了。形成快照后,m_ids中保存的就是11,13,15。up_limit_id等于11,low_limit_id等于16。
此时如果DB_TRX_ID没有m_ids中,说明已经提交(commit),可以被查询。
如果DB_TRX_ID在m_ids中,说明还没有被提交,不可以被查询,
看下面源代码就懂了。并且可重复读和读提交,是因为read view不同,所以呈现的现象不同。下面有详细介绍。
-
DB_TRX_ID >= low_limit_id
说明是形成read view后,形成的事务,对数据进行了修改。插入到了版本链中。
比如:此时有11,12,13,14,15号事务,形成read view。up_limit_id等于11,low_limit_id等于16。之后,又开始了一个事务16,并且修改了数据,所以版本链中会有大于low_limit_id的版本数据。
这种数据不能被查看。
对应源码策略:
- id就是版本链中的DB_TRX_ID
- 当Up_limit_id <= DB_TRX_ID < low_limit_id,只要m_ids中没有id,就可以被查询。
注意:
- read veiw的作用就是用来判断版本链中的数据是否可以看到。
- 从版本链第一个结点开始查看,如果查到不应该看到的版本,就遍历下一个版本,直到可以查看。
- read view是进行可见性判断的,即有了read view才直到哪些版本链的版本可见,哪些版本链的版本不可见。
- read view是在select时,自动生成的。
MVCC流程演示,方便理解:
假设当前有一条记录:
事务操作:
事务1(id = 1) | 事务2(id = 2) | 事务3(id = 3) | 事务4(id = 4) |
事务开始 | 事务开始 | 事务开始 | 事务开始 |
... | ... | ... | 修改并提交 |
进行中 | 快照读 | 进行中 | |
... | ... | ... |
事务修改了name,将name有张三修改成了李四。
当事务2对某行数据进行快照读,数据库为改行数据形成移和read view读视图。
该read view中保存的数据为:
m_id2 = 1,3;
up_limit_id = 1;
low_limit_id = 5;
creator_trx_id = 2;
版本链为:
只有事务4修改了数据,并在事务2快照读前,提交了事务。
事务2拿着版本链的DB_TRX_ID去跟up_limit_id和low_limit_id和活跃事务ID列表进行比较,判断当前版本是否可以查看。
此时
该read view中保存的数据为:
m_id2 = 1,3;
up_limit_id = 1;
low_limit_id = 5;
creator_trx_id = 2;
DB_TRX_ID = 4
比较步骤:
- DB_TRX_ID(4) < up_limit_id(1) 不小于,判断下一个条件。如果小于可以查看。
DB_TRX_ID(4) >= low_limit_id(5) ? 不大于,判断写一个条件。如果大于,不可以查看,遍历版本链中的下一个结点。 m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务 4 不在当前的活跃事务中。此时就需要隔离级别来决定是否可以查看。隔离级别为读提交,可以查看,可重复读,不可以查看。
四.RR和RC的本质区别
RR是可重复度,RC是读提交。
对于表:
设置当前会话的隔离级别为可重复读
介绍一个当前读语法:
select * from 表名 lock in share mode
案例1:
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
begin | 开启事务 | 开启事务 | begin |
select * from student | 快照读 | 快照读 | select * from student |
update student set age=30 where name='张三' | 更新名字为张三的年龄为30 | ||
commit | 提交事务 | ||
select没有读到张三的年龄为30 | select * from student | ||
select可以读到张三的年龄为30 | select * from student lock in share mode |
案例2:
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
begin | 开启事务 | 开启事务 | begin |
select * from student | 快照读,查到年龄为28 | ||
update student set age=30 where name='张三' | 更新名字为张三的年龄为30 | ||
commit | 提交事务 | ||
select可以读到张三的年龄为30 | select * from student | ||
select可以读到张三的年龄为30 | select * from student lock in share mode |
两测试用例的区别:
- 案例1在事务A提交和修改前,快照读了一次。
- 案例2在事务提交和修改前,没有快照读。
结论:
针对隔离级别是可重复读(repeatable read)
-
事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力。即,此时版本链中有多少结点。
- 在RR下,某个事务,只会对某条记录的第一次快照读会创建一个快照及Read view。
- 之后在调用快照读时,还是使用的同一个read view,对形成read view之后的的修改是不可见的。
- 在RC下,事务中,每一次快照读都会新生成一个快照和read view,这就是在RC级别下的事务中可以看到别的事务提交的更新的原因。
- RR和RC区别是因为read view的不同,RR只在第一次形成一个read veiw,并且之后都只会看这一个,而RC每一次快照读都会形成read view。
以上是关于MySQL事务——万字详解的主要内容,如果未能解决你的问题,请参考以下文章
「ABAP」万字详解,一文带你入门SAT事务码SQL优化必备