并发控制

并发控制

一、常见的并发问题

​ 更新丢失:两个或多个事务同时更新一条记录,会发生更新丢失。导致更新结果不可控。可以分为回滚覆盖和提交覆盖。

​ 回滚覆盖:一个事务回滚操作,把其他事务已提交的数据覆盖了

​ 提交覆盖:一个事务提交操作,把其他事务已提交的数据覆盖了

​ 脏读:一个事务读取到了另一个事务修改但未提交的数据。

​ 不可重复读:一个事务中多次读取同一行记录,后面读取的和前面读取的不一致。

​ 幻读:一个事务中多次按相同条件查询,结果不一致。

二、锁分类

1.按粒度分

​ 表级锁:每次操作锁住整张表,粒度最大

​ 行级锁:每次操作锁住一行数据,粒度最小,仅Innodb引擎支持

​ 页等锁:每次锁定相邻的一组记录,粒度在表锁和行锁使用,仅BDB引擎支持

2.操作类型

​ 共享锁(读锁/S锁):对同一份数据,多个读操作可以同时进行。事务A对记录加了S锁,就可以对记录进行读操作,不能修改。其他事务也可以对记录追加S锁,但不能追加X锁。

​ 排它锁(写锁/X锁):当前写操作没有完成前,会阻断对其他事务获取该数据项上的写锁和读锁。事务A对记录添加了X锁,就可以对该记录读和写,其他事务不能对该记录进行读写。

意向锁

​ S、X锁都是表级锁。意向锁是表级锁。当对表中记录加S或X锁之前,会先对表加IS或IX锁。主要用于迅速判断能否加锁。 一个记录被显式加锁前,它的所有父级记录都要加上意向锁(例:给表中某行显式加锁,那这个表和这个表所在的数据库都要加意向锁)。这样一个事务就不必遍历整个文件树来判定它能否给一个记录加锁。

​ 意向共享锁(IS):如果一个记录被加上了IS锁,它的子记录只能加S锁。

​ 意向排它锁(IX):如果一个记录被加上了IX锁,表明它的走子记录可以加S锁或X锁。

3.操作性能

​ 乐观锁:一般实现方式是对记录数据版本进行比对,在数据更新提交时才会进行冲突检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。并发度很高

        乐观锁的实现:使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

​ 悲观锁:对一条数据修改的时候,为了避免同时被其他人修改,修改之前先锁定该记录,再修改。共享锁和排它锁都属于排他锁。

三、死锁

事务一

事务一

1.事务的基本定义

​ 事务是访问并可能更新各种数据项的一个逻辑单元。

​ 事务的ACID特性:原子性、隔离性、持久性、一致性

二、事务的ACID特性

1.原子性

​ 一个事务是不可分割的,要么执行其全部操作,要么全部不执行,这便是原子性。

​ 通常使用日志系统来保证事务的原子性。事务对数据库所做的每个修改先被记录到日志中。当事务未能成功执行,我们说这个事务中止了。这时,我们便使用日志中的数据来将数据库恢复到事务执行之前的状态,这称为事务的回滚

2.隔离性

​ 当有多个事务并行执行的时候,事务之间不能相互影响,每个事务在自己的视角来看,应该只有它自己在执行,这便是隔离性。实际上就是对并行执行的控制。数据库中完成这项任务的是并发控制部件

隔离性级别

​ 可串行化:保证事务可串行化的执行。串行执行即一个一个的执行,不存在事务的并行。

​ 可重复读:只允许读取已经提交的数据,并且进一步要求一个事务在两次读取一个数据项期间,其他事务不得更新该数据项。

​ 已提交读:只允许读取已经提交的数据。

​ 未提交度:允许读取未提交的数据

​ 注意:以上四种级别均不允许脏写。不允许脏写是如果一个数据项已经被一个未提交或中止的事物写过,那么其他事务禁止再对该数据项进行写操作。

隔离性级别的实现

​ 1.锁:通常我们使用两阶段封锁协议配合共享锁、排他锁来实现。两阶段封锁协议是指事务在第一个阶段只获得锁,第二个阶段只释放锁(实际上通常只有当事务提交或中止时才释放锁)。共享锁用于读,排他锁用于写。许多事务可以同时拥有数据项上的共享锁,但只有当其他任何事务在一个数据项上不持有任何锁的时候,一个事务才运行持有该数据项上的排他锁。

​ 2.时间戳:它为每个事务分配一个时间戳,通常是事务开始的时候。对与每个数据项,系统维护两个时间戳。读时间戳:保留最近读取的该数据项的事务的时间戳。写时间戳:写过该数据项当前值的事务的时间戳。事务依照事务时间戳来访问各个数据项,当不能访问时,违例事务被中止,并分配一个新的时间戳重新开始。

​ 3.多版本和快照隔离:每个事务开始的时候有它自己需要的数据项的快照(即副本)。它从这个私有版本中读取数据,以此来避免冲突。更新同样发生在快照里,当事务被提交的时候,这些更新被真正地写到数据库里。当一个事务进入部分提交状态(即事务最后一个语句被执行后)时,只有在没有其他并发事务修改了该事务想要更新的数据的情况下,才能进入提交状态。不能被提交的事务被中止。

3.一致性

​ 一致性是指系统从一个正-确的状态,迁移到另一个正确的状态。什么叫正确的状态呢?就是当前的状态满足预定的约束就叫做正确的状态.而事务具备ACID里C的特性是说通过事务的AID来保证我们的一致性。

​ 例:转账系统,转账前我的两张卡里钱的总和是1000,我把A中的钱转到B后,总和仍应该是1000。

4.持久性

​ 一旦事务成功执行,它对数据库的改变应该是永久的,即使出现系统故障。

三、事务调度

1.可恢复调度

​ 对于事务A,B如果B读取了由A之前所写过的数据项,那么A的提交操作应该出现在B的提交操作之前。

2.无级联调度

​ 因为单个事务失效而导致一系列事务回滚的现象称为级联回滚

​ 无级联调度是对于每对事物A,B,如果B读取了A中所写的一个数据项,那么A的提交必须出现在B的这一读操作之前。

索引的应用

索引的应用

1.创建索引

1
2
3
4
create index 索引名 on 表名 (属性A,属性B...)	//创建索引
create unique 索引名 on 表名 (属性A,属性B...) //唯一索引

drop index 索引名 //删除
应该创建索引的列:经常被搜索的列

​ 在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构
​ 在经常用在连接(JOIN)的列上,这些列主要是一外键,可以加快连接的速度。因为大多连接都在外码和主码属性之间进行。
​ 在经常需要根据范围(<,<=,=,>,>=,BETWEEN,IN)进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连 续的
​ 在经常需要排序(order by)的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
​ 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。

全文索引

​ 对于大量文本检索操作,使用like效率很低,这时可以使用全文索引来提高效率。它必须建立在字符串上。

​ 全文索引字段值必须建立在最大字段值和最小字段值之间才有效(Innodb:3-84)

​ 默认使用等值匹配,如a匹配a,而不匹配ab。可以使用against(‘a*’ in boolean mode)进行模糊匹配

1
2
3
4
5
6
create fulltext 索引名 on 表名 (属性A,属性B...)

//使用
select *
from
where match(name/*属性*/) against('aaa'/*要匹配的串*/)

二、回表查询和覆盖索引

​ 回表查询指通过索引项定位到数据后,还需要将相应数据行从磁盘读入主存。当select中包含不是搜索码的属性时,就会产生回表查询。

​ 覆盖索引值要查询的属性就是索引项的搜索码,属性的值就在索引项里存储,可以直接返回索引项中存储的数据,效率高。

三、最左前缀原则

​ 当使用复合索引时遵循最左前缀原则。顾名思义,所谓最左前缀,就是最左优先,即查询中使用到最左边的搜索码,索引会生效,否则索引失效。例:一个复合索引包括(name,age,time),那么单独查询name,或查询name和age,或name、age、time,索引都会生效。若查询age或age,name。等,则不会使用索引。但这只是理论上,实际上mysql的查询优化器会自动选择最优的查询顺序

​ 实际上,建立一个索引,对于索引中的字段,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

四、一些索引细节的面试题

1.mysql使用like模糊查询时,索引能不能起作用?

​ 1. like %keyword 索引失效,使用全表扫描。但可以通过翻转函数+like前模糊查询+建立翻转函数索引=走翻转函数索 引,不走全表扫描。

​ 2.like keyword% 索引有效。

​ 3.like %keyword% 索引失效,也无法使用反向索引。

2.mysql中如果某一列有null值,那么包含该列的索引是否有效?

​ 索引是有效的。虽然MySQL可以在含有null的列上使用索引,但不代表null和其他数据在索引中是一样的。null值通常需要额外的空间来记录null值。不建议列上允许为空。最好限制not null,并设置一个默认值,比如0''空字符串等,如果是datetime类型,可以设置成'1970-01-01 00:00:00'这样的特殊值。

​ 对MySQL来说,null是一个特殊的值,。比如:不能使用=,<,>这样的运算符,对null做算术运算的结果都是nullcount时不会包括null行等,某列可为null比not null可能需要更多的存储空间等。

五、索引和排序

​ mysql中支持filesort和index两种排序方式。

​ filesort:先把结果查出,然后在缓存中进行排序,效率低。它有两种算法:单路排序和双路排序。

​ 单路排序:从磁盘查询所需的所有列的数据,然后在内存中排序并返回。如果查询数据超出sortbuffer,会导致多次磁盘io,并创建临时表,降低效率。解决方案:少使用select *;增加sort_buffer_size容量和max_length_for_sort_data容量。

​ 双路排序:需要两次磁盘io,第一次将需要排序的字段(列)读出来进行排序,第二次读取其他字段数据。

​ index:使用索引自动实现排序,不需另做排序操作,效率高。

以下几种情况会使用index排序

​ 1.order by子句索引列组合满足索引最左前缀原则

1
select id from  表 order by id; //对应(id),(id,name,...)索引有效

​ 2.where子句+order by子句索引列组合满足索引最左前缀原则

1
select id from 表 where id=3 order by name //对应(id,name)索引有效

以下会使用filesort

​ 1.对索引列同时使用asc和desc

​ 2.where子句+order by子句索引列组合满足索引最左前缀原则 ,但where使用了范围查询

​ 3.order by子句或where子句+order by子句索引列组合不满足最左前缀原则。

​ 4.order by子句或where子句+order by子句使用了不同的索引。

1
select id from 表 order by name,age; //分别在name和age上建立了索引,但不是复合索引

​ 5.order by子句或where子句+order by子句中索引列使用了表达式。

索引

一.索引的概念

​ 索引:索引实际上是一组键值对,键是表中的某些属性,值是指向相应文件内容的指针

​ 顺序索引:基于值的顺序排序。

​ 散列索引:基于将值平均分布到若干桶中,一个值所属于哪个桶,是由一个散列函数决定的。(就是哈希表的思想)

​ 搜索码:用于在文件中查找记录的属性或属性集。即索引的“键”

二.顺序索引

​ 顺序索引按照排好的顺序存储搜索码的值,并将每个搜索码与包含该搜索码的记录关联起来。

​ 被搜索文件本身也可以按一定顺序来排列。

​ 聚集索引:搜索码排序和被搜索文件内容排序一致的索引,它定义了文件中内容的次序。

​ 非聚集索引(辅助索引):搜索码排序与文件内容不一致

1.稠密索引

​ 对于文件中的每个搜索码值都有一个索引,即对表中的每一个元组都有索引。索引项包括搜索码值一个指向具有该搜索码值的第一条数据记录的指针,其他拥有相同搜索码值的数据会顺序存储在第一条之后。

2.稀疏索引

​ 在稀疏索引中,只为某些是搜索码建立索引值。只有当表中数据按搜索码依次排序存储的时候才能用稀疏索引。

3.多级索引

​ 为了提高查找效率,我们可以使用多级索引。先来看一下索引的大小对查找效率的影响,如果一个索引非常大,它不能被存入主存,那么我们就需要去磁盘上读取一些索引项,这会大大降低效率。

​ 所以我们引入了多级索引,我们在原始索引上构建一个稀疏的外层索引,这个过程可以多次重复。这样可以将磁盘io的次数降到最低。例:我们现在有两层索引,原始索引有100个块,每个块有100条数据,二级索引存储了指向这些块的指针,共100条,它在主存中。我们通过二分查找找到最大搜索码值小于等于要搜索的数据的搜索码的索引项,进而得到相应的块。将该块读入主存,再去遍历或二分搜索具体数据。这个过程只需要1次磁盘io。如果不用多级索引,直接在原始索引上二分,我们读log2 100 = 7次磁盘io。

4.索引的增删

插入

​ 稠密索引:如果该搜索码未出现在索引中,就在索引中的适当位置插入带有该搜索码的索引项。否则,如果索引项存储的是所有指向具有相同搜索码的数据的指针,那么就在该索引项中添加一个新的指针;如果索引项存储的是具有相同搜索码的第一条记录的指针,就将待插入记录放到具有相同搜索码值的其他记录之后。

​ 稀疏索引:如果它的值不在目前的块的范围内,那就新建一个块,它是新块中的第一个索引项。如果这条插入记录具有所在块的最小值,就更新指向块的索引项。否则不做改动。例:现在有稀疏索引 1、5、10,如果插入15,那么系统将新建一个块,它的第一个索引项是15;如果插入5,那么指向5-9块的索引项指针将被更新,指向这个新插入的索引项。其他情况,稀疏索引不动。

删除

​ 稠密索引:如果要删除的记录是唯一记录,那就直接删。否则,如果索引项存储的是所有指向具有相同搜索码的数据的指针,那么就从索引项中删除指向待删除记录的指针。如果索引项存储的是具有相同搜索码的第一条记录的指针,如果待删除记录是具有该搜索码值的第一条记录,那就将索引项指向下一条记录。

​ 稀疏索引:如果索引值不包含具有待删除记录搜索码值的索引项,索引不动。否则,如果待删除记录是具有该搜索码值的唯一记录,就用下一个搜索码值的索引记录来替换相应的索引记录,如果下一个搜索码值已经有了一个索引项,那就删除而不是替换该索引项。如果它不是唯一记录,那就用具有相同搜索码值的下一条记录更新索引项。

5.辅助索引

​ 辅助索引必须是稠密的。每个索引项需要包含指向所有具有该索引项搜索码值的记录的指针。

6.B+树索引文件

​ 顺序索引文件一般被组织成B+树,它随着数据量的增长,具有比较稳定的性能。

7.非唯一性搜索码

​ 由于搜索码可以是表中的任意属性,所以它不一定是候选码。为了保证效率和避免一些负责的问题,大多数据库的B+树都实现只处理唯一搜索码。它们会自动添加记录ID或其他属性,使得非唯一搜索码变得唯一。

二、散列索引

​ 散列是在主存中构建索引的常用技术。散列索引的组织结构等同于哈希表。

SQL基础二

SQL基础二

一、内连接

1.自然连接

​ 自然连接运算作用于两个表,自然连接的结果包括在两个表中都出现的属性上的数值均相等的那些元组。如A,B两表均有id属性,那么A natural join B的结果等价于

1
2
3
select *
from A,B
where A.ID=B.ID
2.连接条件

​ 可以使用on来指定连接的条件。即按on子句指定的谓词进行连接。在内连接中,带on条件的连接表达式可以用不带on的表达式等价替换。但是在外连接中,on条件的表现和where不同

1
2
select * 
from A join B on A.ID=B.ID

二、外连接

​ 在内连接中,如果有一个表中的连接属性(即两表都具有的属性)取值为null,那么该元组不会被加入内连接的结果集。此时就需要外连接。

​ 外连接运算在内连接的结果中创建包含null的元组,来保留内些在连接中丢失的元组。

​ 外连接包括左外连接(left outer join)、右外连接、全外连接三种。左外连接保留在运算左边的关系中的元组,右外连接相反。全外连接都保留。

​ 对于左外连接(LEFT JOIN),如果在连接过程中发现左侧表中的某些行无法与右侧表匹配,则这些行仍然会出现在最终结果集中,但是右侧表相关的列将会被设置为 NULL 值。

on对外连接的影响

​ 首先,来看下面的SQL查询语句

1
2
3
4
select 
*
from a left join b on a.id = b.a_id
where b.name='apple'
1
我期望的结果是:表a的数据全部展示出来,不符合表b条件的数据字段用null填充。

​ 但是,答案却出乎我的意料,所有数据都被过滤掉了,最终结果并不符合left join的语义,表a是主表,无论如何是应该展示出来的,但是并没有。
问题出在哪里呢?

​ 这里牵扯到的是sql语句中on后的条件和where后的条件的执行顺序问题。

​ 数据库在通过连接两张或多张表返回记录时,首先根据连接条件生成一张临时表,然后,where条件过滤临时表,最后,将结果返回给用户。
​ 这里就说的很明白了,SQL语句执行先使用on条件,然后,使用where条件。

​ 这里,有必要再次明确一下left join…on… 语法的准确含义。

​ LEFT JOIN 关键字从左表(table1)返回所有的行,即使右表(table2)中没有匹配。如果右表中没有匹配,则右表结果用 NULL填充。
​ 展开来解释一下就是:左表是主表,总是全部返回,而右表的结果是根据on条件来决定的。如果on条件为真,那么,就返回匹配上的右表结果,如果on条件为假,那么就把右表的结果用null填充。
这里的on条件不仅限于一个,也可以是多个。

有了上面的基础知识,就可以进一步去理解阐述了。

  • 如果b.name='apple'作为where条件,那么,按照sql语句的执行流程,首先生成连接查询临时表结果,这一步是没有问题的,然后使用where条件过滤,这时候,b表中并没有符合条件的数据,这样,所有数据就被过滤掉了。返回的结果,就不符合预期了。
  • 如果b.name='apple'作为on条件,那么,按照sql语句的执行流程,首先生成连接查询临时表结果,这一步也是没有问题的,依然,能达到我们的预期。

三、视图

​ 视图是一种“虚拟关系”,概念上包括查询的结果。实质上就是存储了一条sql语句,当使用视图时,去运行该sql语句计算结果。

1
create view v as 查询表达式
物化视图

​ 物化视图的结果会存储在数据库中,当用于定义视图的表发生改变,视图也跟着修改并保持最新。

​ 一般不使用试图进行修改操作

去除重复字母-力扣316

去除重复字母-力扣316

题目

​ 给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

示例 1:

1
2
输入:s = "bcabc"
输出:"abc"

示例 2:

1
2
输入:s = "cbacdcbc"
输出:"acdb"

提示:

  • 1 <= s.length <= 104
  • s 由小写英文字母组成

解析

​ 思路:贪心+单调栈

​ 要解决这个题,首先得明白什么是字典序列。字典项就是对字符串的排序,从头开始,挨个比对每个字符,优先级从左到右依次降低,字符排在前面的字符串小。如abc < acb,abcc<acba,字典序大小和长度无关

​ 先来看第一个样例,不要拿整个串来分析,我们一个一个看,逐步扩展得到答案。第一个字符是‘b’,如果只有它的话,那它自己就是答案,所以暂时不动。接下来再看“bc”,“bc”及是包含b,c两个字母的最小字典序,它也暂时不动。看“bca”,这时矛盾出现,“bca”不是最小字典序,所以如果a后面还有c,那么我们就应该抛弃a前面的c,这时“bca”–>“ba”,同理,如果a后面还有b,我们也应该抛弃b。

​ 很明显,a前面的字符符合先进后出的规律,直接想到栈结构。此时我们相当于维护了一个栈内元素 分组 递增的栈,这便是单调栈。(实际上单调栈内元素不一定分组递增,满足单调性即可)

​ 再来看第二个样例,当我们按上面思路分析到d的时候,虽然这时字符串不是最小字典序,但是d只出现了一次,题目要求不能改变字符相对位置,所以对于只出现一次的字符,我们不能抛弃。这也就是为什么栈内元素是分组递增的

​ 最后说说贪心的体现:我们希望得到最小字典序,那么就要使答案字符串中位置靠前的字符尽可能地小,当我们从小到大扩展分析问题时,它的等价论述是要使答案字符串的尾字符尽可能的小,因为字符串中的每个字符都曾经是尾字符,所以二者等价。单调栈正是实现了这一点。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
string removeDuplicateLetters(string s) {
vector<int> vis(26),num(26);//vis标记栈内的元素,num表示字符串中各个字符的出现次数
for(char ch : s)
{
num[ch-'a']++;
}
string stk;
for(char ch:s)
{
if(!vis[ch-'a'])
{
while(!stk.empty()&&stk.back()>ch)
{
if(num[stk.back()-'a']>0)
{
vis[stk.back()-'a'] = 0;
stk.pop_back();
}
else
break;
}
vis[ch-'a'] = 1;
stk.push_back(ch);
}
num[ch-'a'] -= 1;
}
return stk;
}
};

sql基础一

sql基础一

一、一些基本概念

​ 属性:列;元组:行

​ 超码:一个或多个属性(列)的集合,它能唯一地标识出表中的某个行。

​ 候选码:最小的超码,即超码的最小真子集。

​ 主码(键):表中用于区分不同元组的主要方式的候选码。注:候选码不一定是主码,主码一定是候选码。主码一般选择不变或很少变化的属性。主码也称为主码约束

​ 外码约束:A表的某属性是B表的主码。即A中该属性的所有值必须在B中该属性的取值集合中出现。此时A表中的该属性称为A引用B的外码。A表为引用表,B表为被引用表。

​ 引用完整性约束:外码约束的推广,A中该属性的所有值必须在B中该属性的取值集合中出现。但该属性不一定是B表的主码。实际上,外码约束可以看做是引用完整性约束的特例。

二、基础sql语句的组件

​ 1.from:定义一个在该子句中所列出关系(表)的笛卡尔积,等效代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for(auto t1 : 表一)
{
for(auto t2:表2)
{
....
for(auto tm:表m)
{
把t1,t2....tm连接成单个元组t
把t加入结果集
}


}
}

​ 2.where:查询谓词,限制from中的结果。

​ 3.聚合函数:聚合函数是以值集(集合或多重集合)为输入,并返回单个值的函数。注意,除了count(*)之外的聚集函数都忽略输入集合中的空值

​ 4.group by:如果希望将聚合函数作用在一组元组上,我们可以使用group by子句。在分组子句中的所有属性取值上相同的元组将被放到一组。 如果使用了分组,聚合函数的作用对象将从单个元组变成元组集(分组)。注意:要确保出现在select语句中没有被聚集的属性只能是出现在group by子句中的属性。换句话说,任何没有出现在group by子句中的属性如果出现在select语句中,那它只能是聚合函数的参数

1
2
3
4
select dept_name,ID,avg(salary)
from instructor
group by dept_name
//这是错误的查询语句,因为ID即不是group by子句里的属性,也不是聚合函数的参数

​ 5.having:对分组限制条件。该限制不针对单个元组,只针对group by生成的每个元组集合(分组)。同样,任何出现在having子句中,但没有被聚集的属性必须出现在group by子句中。

执行顺序

​ from->where->group by->having->select/聚合函数

三、嵌套子查询

1.where下的嵌套子查询

​ 通过将子查询嵌套在where中,可以用来执行对集合成员资格的测试、对集合的比较以及对集合基数的确定

​ (1)in:连接词in测试集合成员资格,这里集合指由select生成的一组值。A in B表示A在B中存在,not in同理

​ (2)集合比较:some,表示至少比某一个要…。如A <some B表示A至少比B中某一个小。A <>some B表示B中至少有一个 和A不等。=some等价于in,<>some不等价于not in。

​ all,表示比所有的都…..,用法同some。<>all等价于not in,=all不等价于in

​ (3)对集合基数的确定:exists,在作为参数的子查询非空时返回true。用法:exists(子查询)。not exists同理。

​ unique,在作为参数的子查询中没有重复元组时返回true,用法同exists。

2.from下的嵌套子查询

​ 正常使用即可。可以使用with子句来定义临时关系,这个定义只对包含with的子句有效,以此来使sql语句更简洁清晰。 用法:with 临时关系名 as (子查询)

3.标量子查询

​ 当子查询只返回一个包含单个属性的元组,这样的子查询成为标量子查询。主要由子查询中使用聚合函数来实现。

模板方法模式

模板方法模式

1.何为模板方法模式

​ 定义一个操作中算法的骨架(稳定),而将一些步骤(变化)延迟到子类中。这使得我们可以在不改变算法结构的情况下,重写算法的某些步骤。

2.模板方法模式的意义和使用场景

​ 在软件构建中,对某一任务,它常常有稳定的算法结构,但子步骤经常变化(如框架和应用之间)。模板方法模式为算法提供了灵活的扩展点,是代码复用的基本操作。核心思想就是晚绑定,“不要调用我,让我调用你(框架开发人员如是说)”,即让框架去调用用户重写的代码。

3.模板方法的实现

​ 将算法中某些变化的步骤声明为虚函数,使用者通过重写实现所需的步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 抽象基类 - 包含模板方法和具体方法
class AbstractClass {
public:
void templateMethod() {
// 调用抽象方法,由子类实现
primitiveOperation1();
// 调用具体方法
concreteOperation();
// 调用抽象方法,由子类实现
primitiveOperation2();
}
virtual void primitiveOperation1() = 0;
virtual void primitiveOperation2() = 0;
void concreteOperation() {
std::cout << "This is a concrete operation" << std::endl;
}
};
// 具体子类 - 实现抽象方法
class ConcreteClass : public AbstractClass {
public:
void primitiveOperation1() override {
std::cout << "Concrete Class: Primitive Operation 1" << std::endl;
}
void primitiveOperation2() override {
std::cout << "Concrete Class: Primitive Operation 2" << std::endl;
}
};

策略模式

策略模式

1.何为策略模式

​ 定义一系列算法,把它们封装起来,并使它们可以互相替换(变化),该模式使得算法可以独立于它的客户程序(稳定)而变化(拓展、子类化)。本质上是允许我们定义通用的算法框架,然后以组件的形式提供框架内部流程的具体实现。

2.策略模式的意义和使用场景

​ 在软件构建中,某些对象使用的算法可能多种多样,十分不稳定。策略模式将算法和对象本身解耦。策略模式为组件提供了一系列可重用的算法,从而可以使组件在运行时根据需要在各个算法直接切换,十分灵活。它消除提供了条件判断语句之外的另一种选择,这就是在解耦。根据性能需要,算法本身可以使用单例模式。

3.策略模式的实现。

​ 在抽象策略基类中定义算法接口。具体算法继承于该基类,去实现不同的算法。在组件中包含抽象策略基类的指针,创建对象时将具体算法传递给组件即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 抽象策略类
class Strategy {
public:
virtual int doOperation(int num1, int num2) = 0;
};
// 具体策略类 - 加法策略
class AddStrategy : public Strategy {
public:
int doOperation(int num1, int num2) override {
return num1 + num2;
}
};
// 具体策略类 - 减法策略
class SubtractStrategy : public Strategy {
public:
int doOperation(int num1, int num2) override {
return num1 - num2;
}
};
// 上下文类 - 它包含一个策略对象,可以根据策略对象调用不同的方法
class Context {
private:
Strategy* strategy;

public:
Context(Strategy* strategy) : strategy(strategy) {}

void setStrategy(Strategy* strategy) {
this->strategy = strategy;
}

int executeStrategy(int num1, int num2) {
return strategy->doOperation(num1, num2);
}
};

状态模式

状态模式

1.何为状态模式

​ 允许一个对象在其内部状态改变时改变它的行为。从而使对象看似修改了它的行为。本质上是将状态封装为对象。

2.状态模式的意义和使用场景

​ 在软件构建过程中,某些对象的行为会随着状态的变化而变化。而状态通常是不稳定的(可能增加、变化),状态模式通过将状态封装为对象实现在运行时根据对象的状态改变对象的行为,实现了具体行为和状态转换之间的解耦。为不同状态创建不同状态对象同时使得状态转换更加明确。

3.状态模式的实现

​ 创建一个状态抽象基类,在抽象基类中定义接口。具体状态从该基类继承而来,并重写接口。在需要使用状态的对象内部包含状态抽象基类指针,实现运行时根据对象的状态改变对象的行为。可以结合单例模式来节约内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 抽象状态类
class State {
public:
State* pnext;//下一状态
virtual void handle() = 0;
};

// 具体状态类 - 开始状态
class StartState : public State { //这个继承是继承接口
static State* m_state;
public:
static State* getInstance()
{
if(m_state == nullptr)
{
m_state = new StartState();
}
return m_state;
}
void handle() override {
std::cout << "The current state is start state" << std::endl;
pnext = EndState::getInstance();
}
};
// 具体状态类 - 结束状态
class EndState : public State {
tatic State* m_state;
public:
static State* getInstance()
{
if(m_state == nullptr)
{
m_state = new EndState();
}
return m_state;
}
void handle() override {
std::cout << "The current state is end state" << std::endl;
}
};
// 上下文类 - 它包含一个状态对象,可以根据状态对象调用不同的方法
class Context {
private:
State* state;

public:
Context(State* state) : state(state) {}

void setState(State* state) {
this->state = state;
}

void execute() {
state->handle();
state = state->pnext;
}
};