精华内容
下载资源
问答
  • 数据库MySQL详解

    万次阅读 多人点赞 2018-07-24 20:03:47
    什么是数据库管理系统 数据库管理系统(DataBase Management System,DBMS):指一种操作和管理数据库的大型软件,用于建立、使用和维护数据库,对数据库进行统一管理和控制,以保证数据库的安全性和完整性。...

    全网最详细MySQL教程,2021.1再次更新70%的内容,2021.1再更花了我连续半个月时间,此时使用MySQL 8.0 + Navicat 15图形化界面演示,欢迎大家一键三连 重点更新第7、8、10、11、13章,应付大学考试、考研复试、求职笔试应该说是完全足够的

    后续几个月一直在更新MySQL专栏,也是我的读书笔记,都是MySQL原理和底层一点的东西,可能比一般的面试文都深入,详情见这里MySQL专栏

    目录

    第1章 数据库

    1.1 数据库概述

    1.2 数据库表

    1.3 表数据

    第2章 MySql数据库

    2.1 MySql安装

    2.2 登录MySQL数据库

    2.3 SQLyog(MySQL图形化开发工具,我个人用的Navicat)

    2.4 MySQL配置文件

    第3章 SQL语句

    3.1 SQL语句

    3.2 SQL通用语法

    3.3 数据库操作:database

    3.4 表结构相关语句

    3.4.1 创建表

    3.4.2 查看表

    3.4.3 删除表

    3.4.4 修改表结构格式

    3.5 DOS操作数据乱码解决

    第4章 字段属性

    4.1 主键

    4.1.1增加主键

    4.1.2 主键约束

    4.1.3 更新主键 & 删除主键

    4.1.4 主键分类

    4.2 自动增长

    4.2.1 新增自增长

    4.2.2 自增长使用

    4.2.3 修改自增长

    4.2.4 删除自增长

    4.3 唯一键

    4.3.1 增加唯一键

    4.3.2 唯一键约束

    4.3.3 更新唯一键 & 删除唯一键

    4.4 外键

    4.4.1 增加外键

    4.4.2 修改外键&删除外键

    4.4.3 外键作用

    4.4.4 外键条件

    4.4.5 外键约束

    4.4.6 创建外键约束的要求

    4.4.7 外键约束的闭环问题

    4.5 索引

    4.5.1 创建索引

    4.5.2 添加索引

    4.5.3 查询索引

    4.5.4 删除索引

    4.5.5 索引的使用原则

    4.5.6 索引的意义

    4.5.7 MySQL索引原理图解、B+树应用场景大全、索引优化、索引成本计算等

    第5章 关系

    5.1 一对一

    5.2 一对多

    5.3多对多

    第6章 范式

    6.1 1NF

    6.2 2NF

    6.3 3NF

    6.4 逆规范化

    第7章 数据高级操作

    7.1 新增数据

    7.1.1 IGNORE关键字

    7.1.2 主键冲突

    7.1.3 蠕虫复制

    7.2 更新数据

    7.2.1 UPDATE语句中的内连接

    7.2.2 UPDATE语句中的外连接

    7.3 删除数据

    7.3.1 DELETE语句中的内连接

    7.3.2 DELETE语句中的外连接

    7.3.3 快速删除数据表全部记录

    7.4 查询数据

    7.4.1 Select语句

    7.4.2 去重查询

    7.4.3 字段别名

    7.4.4 数据源

    7.4.5 Where子句

    7.4.6 聚合函数

    7.4.7 Group by子句

    7.4.8 Having子句

    7.4.9 Order by子句

    7.4.10 Limit子句

    7.4.11 select语句中各关键字的先后顺序

    第8章 连接查询

    8.1 连接查询分类

    8.2 交叉连接

    8.3 内连接

    8.4 外连接

    8.5 自然连接

    8.6 子查询

    8.6.1 子查询分类

    8.6.2 单行子查询和多行子查询

    8.6.3 WHERE子句中的多行子查询

    8.6.4 子查询的EXISTS关键字

    第9章 视图

    9.1 创建视图

    9.2 查看视图

    9.3 使用视图

    9.4 修改视图

    9.5 删除视图

    9.6 视图意义

    9.7 视图数据操作

    9.7.1 新增数据

    9.7.2 删除数据

    9.7.3 更新数据

    9.8 视图算法

    第10章 数据备份与还原

    10.1 数据表备份

    10.2 单表数据备份

    10.3 SQL备份与还原

    10.4 增量备份

    10.5 大文件备份和还原(图形化操作,推荐!)

    第11章 事务安全

    11.1 事务操作

    11.2 自动事务处理

    11.3 事务原理

    11.4 回滚点

    11.5 事务ACID属性

    11.6 事务的隔离级别

    11.6.1 read uncommitted

    11.6.2 read committed

    11.6.3 repeatable read

    11.6.4 serializable

    第12章 触发器

    12.1 创建触发器

    12.2 查看触发器

    12.3 使用触发器

    12.4 修改触发器&删除触发器

    12.5 触发器记录

    第13章 函数

    13.1 数字函数

    13.2 日期函数

    13.2.1 获取系统时间函数

    13.2.2 日期格式化函数

    13.2.3 日期偏移计算

    13.2.4 计算日期之间相隔的天数

    13.3 字符函数

    13.4 条件函数

    13.4.1 简单条件判断

    13.4.2 复杂条件判断

    13.5 自定义函数

    13.5.1 创建函数

    13.5.2 查看函数

    13.5.3 修改函数&删除函数

    13.5.4 函数参数

    13.5.5 作用域

    第14章 存储过程

    14.1 创建过程

    14.2 查看过程

    14.3 调用过程

    14.4 修改过程&删除过程

    14.5 过程参数


    第1章 数据库

    1.1 数据库概述

    什么是数据库

    数据库就是存储数据的仓库,其本质是一个文件系统,数据按照特定的格式将数据存储起来,用户可以对数据库中的数据进行增加,修改,删除及查询操作。

    什么是数据库管理系统

    数据库管理系统(DataBase Management System,DBMS):指一种操作和管理数据库的大型软件,用于建立、使用和维护数据库,对数据库进行统一管理和控制,以保证数据库的安全性和完整性。用户通过数据库管理系统访问数据库中表内的数据。

    常见的数据库管理系统

    MYSQL :开源免费的数据库,小型的数据库.已经被Oracle收购了.MySQL6.x版本也开始收费。

    Oracle :收费的大型数据库,Oracle公司的产品。Oracle收购SUN公司,收购MYSQL。

    DB2 :IBM公司的数据库产品,收费的。常应用在银行系统中.

    SQLServer:MicroSoft 公司收费的中型的数据库。C#、.net等语言常使用。

    SyBase :已经淡出历史舞台。提供了一个非常专业数据建模的工具PowerDesigner。

    SQLite : 嵌入式的小型数据库,应用在手机端。

    Java相关的数据库:MYSQL,Oracle.

    这里使用MySQL数据库。MySQL中可以有多个数据库,数据库是真正存储数据的地方。

    数据库与数据库管理系统的关系

    1.2 数据库表

    数据库中以表为组织单位存储数据。

    表类似我们的Java类,每个字段都有对应的数据类型。

    那么用我们熟悉的java程序来与关系型数据对比,就会发现以下对应关系。

    类----------表

    类中属性----------表中字段

    对象----------记录

    1.3 表数据

    根据表字段所规定的数据类型,我们可以向其中填入一条条的数据,而表中的每条数据类似类的实例对象。表中的一行一行的信息我们称之为记录。

    表记录与java类对象的对应关系

    第2章 MySql数据库

    2.1 MySql安装

    安装

    自行百度

    安装后,MySQL会以windows服务的方式为我们提供数据存储功能。开启和关闭服务的操作:右键点击我的电脑→管理→服务→可以找到MySQL服务开启或停止。

    也可以在DOS窗口,通过命令完成MySQL服务的启动和停止(必须以管理运行cmd命令窗口)

    2.2 登录MySQL数据库

    MySQL是一个需要账户名密码登录的数据库,登陆后使用,它提供了一个默认的root账号,使用安装时设置的密码即可登录。

    格式1:cmd>  mysql –u用户名 –p密码

    例如:mysql -uroot –proot

    格式2:cmd>  mysql --host=ip地址 --user=用户名 --password=密码

    例如:mysql --host=127.0.0.1  --user=root --password=root

    2.3 SQLyog(MySQL图形化开发工具,我个人用的Navicat)

    安装:

    提供的SQLyog软件为免安装版,可直接使用。【其实我建议使用Navicat,具体怎么破解得百度,公司都用的这个,学习阶段用SQLyog也没关系】

    使用:

    输入用户名、密码,点击连接按钮,进行访问MySQL数据库进行操作

    在Query窗口中,输入SQL代码,选中要执行的SQL代码,按F8键运行,或按执行按钮运行。

    2.4 MySQL配置文件

    看到你的C:\ProgramData\MySQL\MySQL Server 8.0目录,注意ProgramData是隐藏目录,你需要设置查看隐藏文件才能看得到。

    发现下面有个my.ini,这就是MySQL数据库的配置文件,比如字符集、端口号、目录地址等信息都可以在这里配置。

    从大体上我们可以看到,my.ini里面有3个部分。

    [client]和[mysql]是客户端配置信息,[mysqld]是数据库配置信息

    提示:[mysql]中默认no-beep表示当数据库发生错误的时候,不要让主板发出蜂鸣器的声音

    [mysqld]大致说明如下(已去掉默认注释,不然篇幅太长)

    第3章 SQL语句

    数据库是不认识JAVA语言的,但是我们同样要与数据库交互,这时需要使用到数据库认识的语言SQL语句,它是数据库的代码。

    结构化查询语言(Structured Query Language)简称SQL,是一种数据库查询和程序设计语言,用于存取数据以及查询、更新和管理关系数据库系统。

    创建数据库、创建数据表、向数据表中添加一条条数据信息均需要使用SQL语句。

    3.1 SQL语句

    SQL分类:

    数据定义语言:简称DDL(Data Definition Language),用来定义数据库对象:数据库,表,列等。关键字:create,alter,drop等

    数据操作语言:简称DML(Data Manipulation Language),用来对数据库中表的记录进行更新。关键字:insert,delete,update等

    数据控制语言:简称DCL(Data Control Language),用来定义数据库的访问权限和安全级别,及创建用户。

    数据查询语言:简称DQL(Data Query Language),用来查询数据库中表的记录。关键字:select,from,where等

    3.2 SQL通用语法

    1.SQL语句可以单行或多行书写,以分号结尾

    2.可使用空格和缩进来增强语句的可读性

    3.MySQL数据库的SQL语句不区分大小写,建议使用大写,例如:SELECT * FROM user。

    4.同样可以使用/**/的方式完成注释

    5.MySQL中的我们常使用的数据类型如下

    详细的数据类型如下

    分类

    类型名称

    说明

    整数类型

    tinyInt

    很小的整数,1字节

    smallint

    小的整数,2字节

    mediumint

    中等大小的整数,3字节

    int(integer)

    普通大小的整数,4字节

    bigint大整数,8字节

    小数类型

    float

    单精度浮点数,4字节

    double

    双精度浮点数,8字节

    decimal(m,d)

    压缩严格的定点数, m表示数字总位数,d表示保留到小数点后d位,不足部分就添0,如果不设置m、d,默认保存精度是整型

    日期类型

    year

    年份 YYYY  1901~2155,1字节

    time

    时间 HH:MM:SS  -838:59:59~838:59:59,3字节

    date

    日期 YYYY-MM-DD 1000-01-01~9999-12-3,3字节

    datetime

    日期时间 YYYY-MM-DD HH:MM:SS 1000-01-01 00:00:00~ 9999-12-31 23:59:59,8字节

    timestamp

    时间戳 YYYY-MM-DD HH:MM:SS  1970~01~01 00:00:01 UTC~2038-01-19 03:14:07UTC,4字节

    文本、二进制类型

    CHAR(M)

    M为0~255之间的整数,固定长度为M,不足后面补全空格

    VARCHAR(M)

    M为0~65535之间的整数

    TINYBLOB

    允许长度0~255字节

    BLOB

    允许长度0~65535字节

    MEDIUMBLOB

    允许长度0~167772150字节

    LONGBLOB

    允许长度0~4294967295字节

    TINYTEXT

    允许长度0~255字节(0 ~ 2^8 - 1)

    TEXT

    允许长度0~65535字节(0 ~ 2^16 - 1)

    MEDIUMTEXT

    允许长度0~167772150字节(2^24 - 1)

    LONGTEXT

    允许长度0~4294967295字节(2^32 - 1)

    VARBINARY(M)

    允许长度0~M个字节的变长字节字符串

    BINARY(M)

    允许长度0~M个字节的定长字节字符串

    需要注意的是:

    > BOOLEAN在数据库保存的是tinyInt类型,false为0,true就是1

    > char是定长,varchar是变长,char存储时,如果字符数没有达到定义的位数,后面会用空格填充到指定长度,而varchar没达到定义位数则不会填充,按实际长度存储。

    > char长度固定,char存取速度还是要比varchar要快得多,方便程序的存储与查找;但是char也为此付出的是空间的代价,因为其长度固定,所以会占据多余的空间,可谓是以空间换取时间效率。varchar则刚好相反,以时间换空间。

    3.3 数据库操作:database

    创建数据库

    格式:

    create database 数据库名;

    create database 数据库名 character set 字符集;

    例如:

    #创建数据库 数据库中数据的编码采用的是安装数据库时指定的默认编码 utf8

    CREATE DATABASE day21_1;

    #创建数据库 并指定数据库中数据的编码

    CREATE DATABASE day21_2 CHARACTER SET gbk;

      

    #如果创建之后 修改数据库编码

    ALTER DATABASE day21_2 CHARACTER SET=utf8;

    查看数据库

    查看数据库MySQL服务器中的所有的数据库:

    show databases;

    查看某个数据库的定义的信息:

    show create database 数据库名;

    例如:

    show create database day21_1;

    删除数据库

    drop database 数据库名称;

    例如:

    drop database day21_2;

    其他的数据库操作命令

    切换数据库:

    格式:use 数据库名;

    例如:

    use day21_1;

    查看正在使用的数据库:

    select database();

     图形化结果类似于下图

     

    3.4 表结构相关语句

    3.4.1 创建表

    格式:

    create table 表名(
       字段名 类型(长度) 约束,
       字段名 类型(长度) 约束
    );

    例如:

    创建分类表

    CREATE TABLE sort (
      sid INT, #分类ID
      sname VARCHAR(100) #分类名称
    );
    
    

    温馨提示:你创建了数据库,就创建了一块逻辑空间,实际在磁盘上创建了一个文件夹,你创建了一个表,实际磁盘生成了一个.ibd文件,你可以在C:\ProgramData\MySQL\MySQL Server 8.0\Data目录下验证一下,路径中的ProgramData是隐藏文件夹。

    举个例子,你创建了test数据库,然后你执行建表语句如下

    CREATE TABLE temp(/*实验精度丢失问题*/
    	id INT UNSIGNED PRIMARY KEY,
    	num DECIMAL(20, 10) /*数字总位数20,保留小数点后10位*/
    )

    实际在你的磁盘上是这样存储的

    3.4.2 查看表

    查看数据库中的所有表:

    格式:

    show tables;

     图形化结果类似于下图

    这里的命名就告诉了你是 test 数据库里面的表

    查看表结构:

    有两种方式

    方法一: desc 表名;

    方法二: SHOW COLUMNS FROM 表名;

    例如:

    DESC student;
    
    SHOW COLUMNS FROM student;
    
    /* 这两种方式结果一模一样,第一种更常见,显然命令更短你也更愿意用 */

     图形化结果类似于下图

    3.4.3 删除表

    格式:drop table 表名;

    例如:

    drop table sort;

    3.4.4 修改表结构格式

    alter table 表名 add 列名 类型(长度) 约束;

    作用:修改表添加列.

    例如:

    #1,为分类表添加一个新的字段为 分类描述 varchar(20)

    ALTER TABLE sort ADD sdesc VARCHAR(20);

    当然,想添加多个字段分类怎么做呢?

    /*添加多个列方法一*/
    ALTER TABLE student
    ADD address VARCHAR(200) NOT NULL,
    ADD home_tel CHAR(11) NOT NULL;
    /*add语句之间用逗号分隔,最后用分号结束*/
    
    /*添加多个列方法二*/
    ALTER TABLE student
    ADD (address VARCHAR(200) NOT NULL,home_tel CHAR(11) NOT NULL);

    值得注意的是:

    如果表需要添加多列,而有一列字段home_tel之前已经添加过了,结果会显示Duplicate column name 'home_tel',那么你本次添加的多列字段都是无效的,即全部添加失败

    如果我想将这个字段添加到表中间而不是末尾怎么办呢?

    alter table 表名 add 列名 类型(长度) 约束 after 某个字段;

    比如我想在age字段的后面加一个字段sex,而不是在最后一个字段末尾添加

    alter table student add column sex char(1) not null comment '性别' after age;

    alter table 表名 modify 列名 类型(长度) 约束;

    作用:修改表修改列的类型长度及约束.

    例如:

    #2, 为分类表的分类名称字段进行修改,类型varchar(50) 添加约束 not null

    ALTER TABLE sort MODIFY sname VARCHAR(50) NOT NULL; /* 添加约束NOT NULL */
    
    ALTER TABLE student
    MODIFY home_tel VARCHAR(20) NOT NULL; /*CHAR(11)修改为VARCHAR(200)*/

    同理,和add类似,需要修改多个列的类型长度及约束,那么modify语句之间用逗号分隔,最后一句的末尾用分号结束。

    alter table 表名 change 旧列名 新列名 类型(长度) 约束;

    作用:修改表修改列名.

    例如:

    #3, 为分类表的分类名称字段进行更换 更换为 snamesname varchar(30)

    ALTER TABLE sort CHANGE sname snamename VARCHAR(30);

    同理,和add类似,需要修改多个列的字段名,那么change语句之间用逗号分隔,最后一句的末尾用分号结束。

    直接来个例题:

    假设有2个选项, 选择哪一个

    A. ALTER TABLE cource CHANGE cname VARCHAR(30) NOT NULL FIRST;

    B. ALTER TABLE cource MODIFY  cname VARCHAR(30) NOT NULL FIRST;

    请注意CHANGE和MODIFY的区别, MODIFY可以修改字段类型、字段属性,而CHANGE可修改字段名称,并且CHANGE需要旧列名和新列名,答案是B

    alter table 表名 drop 列名;

    作用:修改表删除列.

    例如:

    #4, 删除分类表中snamename这列

    ALTER TABLE sort DROP snamename;
    
    ALTER TABLE student
    DROP home_address,
    DROP home_tel;

    同理,和add类似,需要删除多列,那么drop语句之间用逗号分隔,最后一句的末尾用分号结束。

    来一道选择题,题目是:删除数据表中多余的列的语句是哪些,有同学上去就选了个B,认为删除就是DELETE,这里的答案是AC。

    rename table 表名 to 新表名;

    作用:修改表名

    例如:

    #5, 为分类表sort 改名成 category

    RENAME TABLE sort TO category;

    alter table 表名 character set 字符集;

    作用:修改表的字符集

    例如:

    #6, 为分类表 category 的编码表进行修改,修改成 gbk

    ALTER TABLE category CHARACTER SET gbk;

    3.5 DOS操作数据乱码解决

    我们在dos命令行操作中文时,会报错

    insert into user(username,password) values(‘张三’,’123’);

    ERROR 1366 (HY000): Incorrect string value: '\xD5\xC5\xC8\xFD' for column 'username' at row 1

    原因:因为mysql的客户端编码的问题我们的是utf8,而系统的cmd窗口编码是gbk

    解决方案(临时解决方案):修改mysql客户端编码。

    show variables like 'character%'; 查看所有mysql的编码

    在图中与客户端有关的编码设置:

    client connetion result 和客户端相关

    database server system 和服务器端相关

    将客户端编码修改为gbk.

    set character_set_results=gbk; / set names gbk;

    以上操作,只针对当前窗口有效果,如果关闭了服务器便失效。如果想要永久修改,通过以下方式:

    在mysql安装目录下有my.ini文件

    default-character-set=gbk 客户端编码设置

    character-set-server=utf8 服务器端编码设置

    注意:修改完成配置文件,重启服务

    第4章 字段属性

    主键, 唯一键和自增长.

    4.1 主键

    主键: primary key,主要的键. 一张表只能有一个字段可以使用对应的键, 用来唯一的约束该字段里面的数据, 不能重复: 这种称之为主键.

    一张表只能有最多一个主键, 主键请尽量使用整数类型而不是字符串类型

    4.1.1增加主键

    SQL操作中有多种方式可以给表增加主键: 大体分为三种.

    方案1: 在创建表的时候,直接在字段之后,跟primary key关键字(主键本身不允许为空)

    优点: 非常直接; 缺点: 只能使用一个字段作为主键

    方案2: 在创建表的时候, 在所有的字段之后, 使用primary key(主键字段列表)来创建主键(如果有多个字段作为主键,可以是复合主键)

    方案3: 当表已经创建好之后, 额外追加主键: 可以通过修改表字段属性, 也可以直接追加.

    Alter table 表名  add primary key(字段列表);

    前提: 表中字段对应的数据本身是独立的(不重复)

    4.1.2 主键约束

    创建约束的目的就是保证数据的完整性和一致性。

    主键对应的字段中的数据必须唯一,且不能为NULL, 一旦重复,数据操作失败(增和改)

    建议主键使用数字类型,因为数字的检索速度非常快,并且主键如果是数字类型,还可以设置自动增长。

    主键的原理其实就是一个计数器。

    4.1.3 更新主键 & 删除主键

    没有办法更新主键: 主键必须先删除,才能增加.

    Alter table 表名 drop primary key;

    4.1.4 主键分类

    在实际创建表的过程中, 很少使用真实业务数据作为主键字段(业务主键,如学号,课程号); 大部分的时候是使用逻辑性的字段(字段没有业务含义,值是什么都没有关系), 将这种字段主键称之为逻辑主键.

    Create table my_student(
    
    Id int primary key auto_increment comment ‘逻辑主键: 自增长’, -- 逻辑主键
    
    Number char(10) not null  comment ‘学号’,
    
    Name varchar(10) not null
    
    )
    
    

    4.2 自动增长

    自增长: 当对应的字段,不给值,或者说给默认值,或者给NULL的时候, 会自动的被系统触发, 系统会从当前字段中已有的最大值再进行+1操作,得到一个新的在不同的字段.

    自增长的字段必须定义为主键,默认起始值是1而不是0

    4.2.1 新增自增长

    自增长特点: 

       任何一个字段要做自增长必须前提是本身是一个索引(key一栏有值),auto_increment表示自动编号

       自增长字段必须是数字(整型)

      一张表最多只能有一个自增长

    4.2.2 自增长使用

    当自增长被给定的值为NULL或者默认值的时候会触发自动增长.

    自增长如果对应的字段输入了值,那么自增长失效: 但是下一次还是能够正确的自增长(从最大值+1)

    如何确定下一次是什么自增长呢? 可以通过查看表创建语句看到.

    4.2.3 修改自增长

    自增长如果是涉及到字段改变: 必须先删除自增长,后增加(一张表只能有一个自增长)

    修改当前自增长已经存在的值: 修改只能比当前已有的自增长的最大值大,不能小(小不生效)

    Alter table 表名 auto_increment  = 值;

    向上修改可以

    思考: 为什么自增长是从1开始?为什么每次都是自增1呢?

    所有系统的变现(如字符集,校对集)都是由系统内部的变量进行控制的.

    查看自增长对应的变量: show variables like ‘auto_increment%’;

    可以修改变量实现不同的效果: 修改是对整个数据修改,而不是单张表: (修改是会话级)

    Set auto_increment_increment = 5; -- 一次自增5

    测试效果: 自动使用自增长

    4.2.4 删除自增长

    自增长是字段的一个属性: 可以通过modify来进行修改(保证字段没有auto_increment即可)

    Alter table 表名 modify 字段 类型;

    4.3 唯一键

    一张表往往有很多字段需要具有唯一性,数据不能重复: 但是一张表中只能有一个主键: 唯一键(unique key)就可以解决表中有多个字段需要唯一性约束的问题.

    唯一键的本质与主键差不多: 唯一键默认的允许自动为空,而且可以多个为空(空字段不参与唯一性比较)

    4.3.1 增加唯一键

    基本与主键差不多: 三种方案

    方案1: 在创建表的时候,字段之后直接跟unique/ unique key

    方案2: 在所有的字段之后增加unique key(字段列表); -- 复合唯一键

    方案3: 在创建表之后增加唯一键

    4.3.2 唯一键约束

    唯一键与主键本质相同: 唯一的区别就是唯一键默认允许为空,而且是多个为空.

    如果唯一键也不允许为空: 与主键的约束作用是一致的.

    4.3.3 更新唯一键 & 删除唯一键

    更新唯一键

    先删除后新增(唯一键可以有多个: 可以不删除).

    删除唯一键

    Alter table 表名 drop unique key; -- 错误: 唯一键有多个

    Alter table 表名 drop index 索引名字; -- 唯一键默认的使用字段名作为索引名字

    4.4 外键

    外键: foreign key, 外面的键(键不在自己表中): 如果一张表中有一个字段(非主键)指向另外一张表的主键,那么将该字段称之为外键.

    4.4.1 增加外键

    外键可以在创建表的时候或者创建表之后增加(但是要考虑数据的问题).

    一张表可以有多个外键.

    创建表的时候增加外键: 在所有的表字段之后,使用foreign key(外键字段) references 外部表(主键字段)

    在新增表之后增加外键: 修改表结构

    Alter table 表名 add [constraint 外键名字] foreign key(外键字段) references 父表(主键字段);

    4.4.2 修改外键&删除外键

    外键不可修改

    只能先删除后新增.

    删除外键语法

    Alter table 表名 drop foreign key 外键名; -- 一张表中可以有多个外键,但是名字不能相同

    4.4.3 外键作用

    外键默认的作用有两点: 一个对父表,一个对子表(外键字段所在的表)

    对子表约束: 子表数据进行写操作(增和改)的时候, 如果对应的外键字段在父表找不到对应的匹配: 那么操作会失败.(约束子表数据操作)

    对父表约束: 父表数据进行写操作(删和改: 都必须涉及到主键本身), 如果对应的主键在子表中已经被数据所引用, 那么就不允许操作

    4.4.4 外键条件

    1.外键要存在: 首先必须保证表的存储引擎是innodb(默认的存储引擎): 如果不是innodb存储引擎,那么外键可以创建成功,但是没有约束效果.
    2.外键字段的字段类型(列类型)必须与父表的主键类型完全一致.
    3.一张表中的外键名字不能重复.
    4,增加外键的字段(数据已经存在),必须保证数据与父表主键要求对应.

    4.4.5 外键约束

    所谓外键约束: 就是指外键的作用.

    之前所讲的外键作用: 是默认的作用; 其实可以通过对外键的需求, 进行定制操作.

    需要注意的是:外键约束的定义是写在子表上的,但是不推荐使用外键约束

    MySQL字段约束有四种,主键约束,非空约束,唯一约束,外键约束。外键约束是唯一不推荐的约束

    提示:主键约束其实就是非空约束和唯一约束合二为一的情况

    外键约束有三种约束模式: 都是针对父表的约束(子表约束父表)

    District: 严格模式(默认的), 父表不能删除或者更新一个已经被子表数据引用的记录

    Cascade: 级联模式: 父表的操作, 对应子表关联的数据也跟着被删除

    Set null: 置空模式: 父表的操作之后,子表对应的数据(外键字段)被置空

    通常的一个合理的做法(约束模式): 删除的时候子表置空, 更新的时候子表级联操作

    指定模式的语法

    Foreign key(外键字段) references 父表(主键字段) on delete set null on update cascade;

    更新操作: 级联更新

    删除操作: 置空

    删除置空的前提条件: 外键字段允许为空(如果不满足条件,外键无法创建)

    外键虽然很强大, 能够进行各种约束: 但是对于PHP来讲, 外键的约束降低了PHP对数据的可控性: 通常在实际开发中, 很少使用外键来处理.

    4.4.6 创建外键约束的要求

    创建外键约束的目的是保持数据一致性和完整性,以及实现一对一或者一对多的关系。

    创建外键约束要求有以下几点:

    1. 父表和子表必须使用相同的存储引擎,而且禁止使用临时表。

    注意:具有外键列的表称为子表;子表所参照的表称为父表。

    2. 数据表的存储引擎只能是InnoDB。

    3. 外键列和参照列必须具有相似的数据类型。其中数字的长度或是否有符号位必须相同;而字符的长度则可以不同。

    注意:加 FOREIGN KEY 关键字的列称为外键列;外键列所参照的列称为参照列。

    4. 外键列和参照列必须创建索引。如果外键列不存在索引的话,MySQL将自动创建索引。如果参照列不存在索引的话,MySQL不会自动创建索引。

    注意:MySQL会为主键自动创建索引。

    4.4.7 外键约束的闭环问题

    比如说我们创建了2张表

    /*先创建父表*/
    CREATE TABLE t_dept(
    	deptno INT UNSIGNED PRIMARY KEY,
    	dname VARCHAR(20) NOT NULL UNIQUE,
    	tel CHAR(4) UNIQUE
    )
    /*再创建子表*/
    CREATE TABLE t_emp(
    	empno INT UNSIGNED PRIMARY KEY,
    	ename VARCHAR(20) NOT NULL,
    	sex ENUM("男", "女") NOT NULL,
    	deptno INT UNSIGNED NOT NULL,
    	hiredate DATE NOT NULL,
    	FOREIGN KEY (deptno) REFERENCES t_dept(deptno)
    );

    父表t_dept加一个数据如下:

    子表t_emp加一个数据如下:

    此时我想删除父表的数据,结果报错

    结果发现有子表t_emp外键约束着父表,删除失败。必须先删除子表的约束数据才能删除父表的数据,那这样就失去了增减改查的灵活性了,并且更严重的是,

    如果形成外键闭环,我们将无法删除任何一张表的数据记录。

    如上图,A约束B,B约束C......,这样每一个表都算作父表,所谓的先删除子表的数据就是不可能的。因为有外键闭环的存在,所以我们不推荐外键约束

    4.5 索引

    几乎所有的索引都是建立在字段之上.

    索引: 系统根据某种算法, 将已有的数据(未来可能新增的数据),单独建立一个文件: 文件能够实现快速的匹配数据, 并且能够快速的找到对应表中的记录.

    4.5.1 创建索引

    建表的时候创建索引,也可以在已存在的表上添加索引。

    CREATE TABLE 表名称(
           ......,
           INDEX [索引名称] (字段),
           ......
    );

    CREATE TABLE t_message(
    	id INT UNSIGNED PRIMARY KEY,
    	content VARCHAR(200) NOT NULL,
    	type ENUM("公告", "通报", "个人通知") NOT NULL,
    	create_time TIMESTAMP NOT NULL,
    	INDEX idx_type (type)
    );

    4.5.2 添加索引

    向已存在的表中添加索引的方式如下

    普通索引:

    CREATE INDEX 索引名称 ON 表名(字段);  /*添加索引方式1*/

    ALTER TABLE 表名 ADD INDEX 索引名称(字段); /*添加索引方式2*/

    唯一索引:

    CREATE UNIQUE INDEX 索引名称 ON 表名(字段)

    联合索引:

    CREATE INDEX 索引名称 ON 表名(字段1,字段2...)

    -- 普通索引:
    CREATE INDEX idx_type ON t_message(type); /*添加索引方式1*/
    
    ALTER TABLE t_message ADD INDEX idx_type(type);/*添加索引方式2*/
    
    -- 唯一索引:
    CREATE UNIQUE INDEX uidx_type ON t_message(type);
    
    -- 联合索引
    CREATE INDEX idx_type1_type2 ON t_message(type1, type2);

     经常被用来做检索条件的字段需要加上索引,原理是B+树,所以查询很快。如果是几千条数据,不必加索引,全表扫描也很快

    练习题:

    已有新闻表(tb_news),表中有type字段,下列选中项中能为该字段添加索引的是?

    这个就是记忆题目,记住语法即可,答案是AC

    4.5.3 查询索引

    SHOW INDEX FROM 表名;

    /*查看t_message表的索引*/
    SHOW INDEX FROM t_message;

     查出来如下,有添加的普通索引和主键索引

    4.5.4 删除索引

    DROP INDEX 索引名称 ON 表名;

    /* 在t_message表中删除idx_type索引 */
    DROP INDEX idx_type ON t_message;

    4.5.5 索引的使用原则

    1. 数据量很大,且经常被查询的数据表可以设置索引  (即读多写少的表可以设置索引)

    2. 索引只添加在经常被用作检索条件的字段上 (比如电子商城需要在物品名称关键字加索引)

    3.不要在大字段上创建索引 (比如长度很长的字符串不适合做索引,因为查找排序时间变的很长)

    4.5.6 索引的意义

    提升查询数据的效率
    约束数据的有效性(唯一性等)
    增加索引的前提条件: 索引本身会产生索引文件(有时候有可能比数据文件还大) ,会非常耗费磁盘空间.

    如果某个字段需要作为查询的条件经常使用, 那么可以使用索引(一定会想办法增加);

    如果某个字段需要进行数据的有效性约束, 也可能使用索引(主键,唯一键)

    Mysql中提供了多种索引

    主键索引: primary key
    唯一索引: unique key
    全文索引: fulltext index
    普通索引: index
    全文索引: 针对文章内部的关键字进行索引

    全文索引最大的问题: 在于如何确定关键字

    英文很容易: 英文单词与单词之间有空格

    中文很难: 没有空格, 而且中文可以各种随意组合(分词: sphinx)

    4.5.7 MySQL索引原理图解、B+树应用场景大全、索引优化、索引成本计算等

    我这里会比你所有看到的一些面试文章讲的都细致,更深入,所以我另外开篇来讲。

    比如:

    图文并茂说MySQL索引——入门进阶必备https://blog.csdn.net/qq_34115899/article/details/118004118icon-default.png?t=L9C2https://blog.csdn.net/qq_34115899/article/details/118004118
    MySQL中B+树索引的应用场景大全https://blog.csdn.net/qq_34115899/article/details/118308424icon-default.png?t=L9C2https://blog.csdn.net/qq_34115899/article/details/118308424

    要想通过面试,MySQL的Limit子句底层原理你不可不知https://blog.csdn.net/qq_34115899/article/details/120727513icon-default.png?t=L9C2https://blog.csdn.net/qq_34115899/article/details/120727513

    更多文章请见专栏https://blog.csdn.net/qq_34115899/category_7832712.html

    第5章 关系

    将实体与实体的关系, 反应到最终数据库表的设计上来: 将关系分成三种: 一对一, 一对多(多对一)和多对多.

    所有的关系都是指的表与表之间的关系.

    5.1 一对一

    一对一: 一张表的一条记录一定只能与另外一张表的一条记录进行对应; 反之亦然.

    学生表: 姓名,性别,年龄,身高,体重,婚姻状况, 籍贯, 家庭住址,紧急联系人

    Id(P)

    姓名

    性别

    年龄

    体重

    身高

    婚姻

    籍贯

    住址

    联系人

    表设计成以上这种形式: 符合要求. 其中姓名,性别,年龄,身高,体重属于常用数据; 但是婚姻,籍贯,住址和联系人属于不常用数据. 如果每次查询都是查询所有数据,不常用的数据就会影响效率, 实际又不用.

    解决方案: 将常用的和不常用的信息分离存储,分成两张表

    常用信息表

    Id(P)

    姓名

    性别

    年龄

    体重

    身高

    1

    不常用信息表: 保证不常用信息与常用信息一定能够对应上: 找一个具有唯一性(确定记录)的字段来共同连接两张表

    Id(P)

    婚姻

    籍贯

    住址

    联系人

    2

    1

    一个常用表中的一条记录: 永远只能在一张不常用表中匹配一条记录;反过来,一个不常用表中的一条记录在常用表中也只能匹配一条记录: 一对一的关系

    5.2 一对多

    一对多: 一张表中有一条记录可以对应另外一张表中的多条记录; 但是返回过, 另外一张表的一条记录只能对应第一张表的一条记录. 这种关系就是一对多或者多对一.

    母亲与孩子的关系: 母亲,孩子两个实体

    妈妈表

    ID(P)

    名字

    年龄

    性别

    孩子表

    ID(P)

    名字

    年龄

    性别

    以上关系: 一个妈妈可以在孩子表中找到多条记录(也有可能是一条); 但是一个孩子只能找到一个妈妈: 是一种典型的一对多的关系.

    但是以上设计: 解决了实体的设计表问题, 但是没有解决关系问题: 孩子找不出妈,妈也找不到孩子.

    解决方案: 在某一张表中增加一个字段,能够找到另外一张表的中记录: 应该在孩子表中增加一个字段指向妈妈表: 因为孩子表的记录只能匹配到一条妈妈表的记录.

    妈妈表

    ID(P)

    名字

    年龄

    性别

    孩子表

    ID(P)

    名字

    年龄

    性别

    妈妈ID

    妈妈表主键

    5.3多对多

    多对多: 一张表中(A)的一条记录能够对应另外一张表(B)中的多条记录; 同时B表中的一条记录也能对应A表中的多条记录: 多对多的关系

    老师教学: 老师和学生

    老师表

    T_ID(P)

    姓名

    性别

    1

    A

    2

    B

    学生表

    S_ID(P)

    姓名

    性别

    1

    张三

    2

    小芳

    以上设计方案: 实现了实体的设计, 但是没有维护实体的关系.

    一个老师教过多个学生; 一个学生也被多个老师教过.

    解决方案: 在学生表中增加老师字段: 不管在哪张表中增加字段, 都会出现一个问题: 该字段要保存多个数据, 而且是与其他表有关系的字段, 不符合表设计规范: 增加一张新表: 专门维护两张表之间的关系

    老师表

    T_ID(P)

    姓名

    性别

    1

    A

    2

    B

    学生表

    S_ID(P)

    姓名

    性别

    1

    张三

    2

    小芳

    中间关系表: 老师与学生的关系

    ID

    T_ID(老师)

    S_ID(学生)

    1

    1

    1

    2

    1

    2

    3

    2

    1

    4

    增加中间表之后: 中间表与老师表形成了一对多的关系: 而且中间表是多表,维护了能够唯一找到一表的关系; 同样的,学生表与中间表也是一个一对多的关系: 一对多的关系可以匹配到关联表之间的数据.

    学生找老师: 找出学生id -> 中间表寻找匹配记录(多条) -> 老师表匹配(一条)

    老师找学生: 找出老师id -> 中间表寻找匹配记录(多条) -> 学生表匹配(一条)

    第6章 范式

    范式: Normal Format, 是一种离散数学中的知识, 是为了解决一种数据的存储与优化的问题: 保存数据的存储之后, 凡是能够通过关系寻找出来的数据,坚决不再重复存储: 终极目标是为了减少数据的冗余.

    范式: 是一种分层结构的规范, 分为六层: 每一次层都比上一层更加严格: 若要满足下一层范式,前提是满足上一层范式.

    六层范式: 1NF,2NF,3NF...6NF, 1NF是最底层,要求最低;6NF最高层,最严格.

    Mysql属于关系型数据库: 有空间浪费: 也是致力于节省存储空间: 与范式所有解决的问题不谋而合: 在设计数据库的时候, 会利用到范式来指导设计.

    但是数据库不单是要解决空间问题,要保证效率问题: 范式只为解决空间问题, 所以数据库的设计又不可能完全按照范式的要求实现: 一般情况下,只有前三种范式需要满足.

    范式在数据库的设计当中是有指导意义: 但是不是强制规范.

    6.1 1NF

    第一范式: 在设计表存储数据的时候, 如果表中设计的字段存储的数据,在取出来使用之前还需要额外的处理(拆分),那么说表的设计不满足第一范式。

    第一范式要求字段的数据具有原子性: 不可再分.

    第一范式是数据库的基本要求,不满足第一范式就不是关系型数据库

    让我们简单化这个问题:

    1NF---原子性

    eg1:

    数据表的每一列都是不可分割的基本数据项,同一列中不能有多个值,也不能存在重复的属性。

    eg2:

    讲师代课表

    讲师

    性别

    班级

    教室

    代课时间

    代课时间(开始,结束)

    朱元璋

    Male

    php0226

    D302

    30天

    2014-02-27,2014-05-05

    朱元璋

    Male

    php0320

    B206

    30天

    2014-03-21,2014-05-30

    李世民

    Male

    php0320

    B206

    15天

    2014-06-01,2014-06-20

    上表设计不存在问题: 但是如果需求是将数据查出来之后,要求显示一个老师从什么时候开始上课,到什么时候节课: 需要将代课时间进行拆分: 不符合1NF, 数据不具有原子性, 可以再拆分.

    解决方案: 将代课时间拆分成两个字段就解决问题.

    6.2 2NF

    第二范式: 在数据表设计的过程中,如果有复合主键(多字段主键), 且表中有字段并不是由整个主键来确定, 而是依赖主键中的某个字段(主键的部分): 存在字段依赖主键的部分的问题, 称之为部分依赖: 第二范式就是要解决表设计不允许出现部分依赖.

    定义太绕了,简单点:

    2NF---唯一性

    数据表中的每条记录必须是唯一的。为了实现区分,通常要为表加上一个列来存储唯一标识,这个唯一属性列被称作主键列

    eg1:

    学号为230的学生在2018-07-15考试第一次58没及格,然后当天补考第二次还是58没及格,于是数据库就有了重复的数据。解决办法就是添加一个流水号,让数据变得唯一。

    eg2:

    讲师带课表

    以上表中: 因为讲师没有办法作为独立主键, 需要结合班级才能作为主键(复合主键: 一个老师在一个班永远只带一个阶段的课): 代课时间,开始和结束字段都与当前的代课主键(讲师和班级): 但是性别并不依赖班级, 教室不依赖讲师: 性别只依赖讲师, 教室只依赖班级: 出现了性别和教室依赖主键中的一部分: 部分依赖.不符合第二范式.

    解决方案1: 可以将性别与讲师单独成表, 班级与教室也单独成表.

    解决方案2: 取消复合主键, 使用逻辑主键

    ID = 讲师 + 班级(业务逻辑约束: 复合唯一键)

    6.3 3NF

    要满足第三范式,必须满足第二范式

    第三范式: 理论上讲,应该一张表中的所有字段都应该直接依赖主键(逻辑主键: 代表的是业务主键), 如果表设计中存在一个字段, 并不直接依赖主键,而是通过某个非主键字段依赖,最终实现依赖主键: 把这种不是直接依赖主键,而是依赖非主键字段的依赖关系称之为传递依赖. 第三范式就是要解决传递依赖的问题.

    定义很绕,我们简单点:

    3NF---关联性

    每列都与主键有直接关系,不存在传递依赖

    eg1:

    根据主键爸爸能关联儿子女儿,但是女儿的玩具、衣服都不是依赖爸爸的,而是依赖女儿的,这些东西不是与爸爸有直接关系,所以拆分两个表。

    儿子女儿依赖于爸爸,女儿的玩具、衣服依赖于女儿。

    满足第三范式后,检索、提取数据非常方便,如果不满足,虽然表也能建成功,但是检索就会花费很多时间,比如如果是第一个表,逻辑上要找女儿的衣服,去查找女儿是找不到的,此时女儿不是主键。数据表拆分之后,根据主键列女儿陈婷婷,可以很快的找到女儿的衣服校服。主键查找是很快的。

    依照第三范式,数据可以拆分到不同的数据表,彼此保持关联

    eg2:

    讲师带课表

    以上设计方案中: 性别依赖讲师存在, 讲师依赖主键; 教室依赖班级,班级依赖主键: 性别和教室都存在传递依赖.

    解决方案: 将存在传递依赖的字段,以及依赖的字段本身单独取出,形成一个单独的表, 然后在需要对应的信息的时候, 使用对应的实体表的主键加进来.

    讲师代课表

       讲师表                                                                               班级表

                                 

               讲师表: ID = 讲师                                                               班级表中: ID = 班级

    6.4 逆规范化

    有时候, 在设计表的时候,如果一张表中有几个字段是需要从另外的表中去获取信息. 理论上讲, 的确可以获取到想要的数据, 但是就是效率低一点. 会刻意的在某些表中,不去保存另外表的主键(逻辑主键), 而是直接保存想要的数据信息: 这样一来,在查询数据的时候, 一张表可以直接提供数据, 而不需要多表查询(效率低), 但是会导致数据冗余增加.

    如讲师代课信息表

    逆规范化: 磁盘利用率与效率的对抗

    第7章 数据高级操作

    数据操作: 增删改查

    7.1 新增数据

    基本语法

    Insert into 表名 [字段1,字段2,......] values (值1,值2,......); /*插入单条记录*/

    Insert into 表名 [字段1,字段2,......] values (值1,值2,......), (值1,值2,......); /*插入多条记录*/

    表名后面不写字段列表也可以插入数据,但是会影响速度。Mysql会进行词法分析,找到对应表结构,然后自动给你补上字段列表。所以表名后面不写字段列表,数据库难以高效的操作。

    INSERT INTO t_dept(deptno, dname, loc)
    VALUES(50, "技术部", "北京");

    INSERT INTO t_dept(deptno, dname, loc)
    VALUES(60, "后勤部", "北京"),(70,"保安部","北京");

     

    eg:向技术部添加一条员工记录

    分析:测验insert语句里面子查询的问题,并且这个子查询是单行子查询,不能是多行子查询,还必须是单行单列的。

    INSERT INTO t_emp
    (empno, ename, job, mgr, hiredate, sal, comm, deptno)
    VALUES(8001, "刘娜", "SALESMAN", 8000, "1988-12-20", 2000, NULL,
    (SELECT deptno FROM t_dept WHERE dname="技术部"));

    练习题

    答案选D,A错在scholarship字段没有数据去匹配,数据库会报错,可以填写NULL解决错误,或者删掉INSERT字段列表中的字段。B错在部门编号deptno却写成"食品工程系",C错在人名"赵菲菲"没写成字符串形式,要加上影号。

    INSERT语句方言

    MySQL的INSERT语句还有一种方言语法

    INSERT INTO 表名 SET 字段1=值1, 字段2=值2......

    为什么称之为方言语法呢?就是因为这个语法只能在MySQL使用,不能在Oracle使用,当然你只用MySQL就可以使用这种方言语法,很简洁。

    INSERT INTO t_emp
    SET empno=8002,ename="JACK",job="SALESMAN",mgr=8000,
    hiredate="1985-3-14",sal=2500,comm=NULL,deptno=50;

     

    在数据插入的时候, 假设主键对应的值已经存在: 插入一定会失败!

    7.1.1 IGNORE关键字

    IGNORE关键字只会插入数据库不存在的记录。比如主键冲突、唯一性冲突,数据库会报错,加上IGNORE之后数据库会忽略这条数据不会报错。

    INSERT [IGNORE] INTO 表名 ......;

    INSERT IGNORE INTO t_dept(deptno, dname, loc)
    VALUES(70, "A", "北京"), (80, "B", "上海"); /*70部门已经存在*/

     

    7.1.2 主键冲突

    当主键存在冲突的时候(Duplicate key),你可以添加ignore关键字选择忽略,数据库不会报错,但是确实非得添加这个记录怎么办呢?可以选择性的进行处理: 更新和替换

    主键冲突:更新操作

    Insert into 表名[(字段列表:包含主键)] values(值列表) on duplicate key update 字段 = 新值; (这个语法sql单独执行没问题,在mybatis会报错,找不到你想要的参数)

    要想兼容mysql和mybatis两者,这里强烈建议不要用等号赋值
    Insert into 表名[(字段列表:包含主键)] values(值列表) on duplicate key update 字段 = values(字段);

    下图例子我忘记改了,应该改为...on duplicate key update room = values(room),而不是room = 'B205',记住不要用等号直接赋值,mybatis会报错。

    主键冲突: 替换

    Replace into 表名 [(字段列表:包含主键)] values(值列表);

    7.1.3 蠕虫复制

    蠕虫复制: 从已有的数据中去获取数据,然后将数据又进行新增操作: 数据成倍的增加.

    表创建高级操作: 从已有表创建新表(复制表结构)

    Create table 表名 like 数据库.表名;

    蠕虫复制: 先查出数据, 然后将查出的数据新增一遍

    Insert into 表名[(字段列表)] select 字段列表/* from 数据表名;

    蠕虫复制的意义

    从已有表拷贝数据到新表中
    可以迅速的让表中的数据膨胀到一定的数量级: 测试表的压力以及效率
     

    7.2 更新数据

    基本语法

    UPDATE [IGNORE] 表名 SET 字段1=值1, 字段2=值2, ......
    [WHERE 条件1 ......]
    [ORDER BY ......]
    [LIMIT ......];

    注意,如果这里有limit关键字,那么后面只能跟一个参数,即表示取前多少条数据,这里的limit不能有2个参数,ignore表示更新失败就直接忽略而不是报错。

    eg1:把每个员工的编号和他上司的编号+1,用order by子句完成

    UPDATE t_emp SET empno=empno+1, mgr=mgr+1
    ORDER BY empno DESC;

    eg2:把月收入前三名的员工底薪减100元,用LIMIT子句完成

    UPDATE t_emp
    SET sal=sal-100
    ORDER BY sal+IFNULL(comm,0) DESC
    LIMIT 3;

    eg3:把10部门中,工龄达到20年的员工,底薪增加200元

    UPDATE t_emp
    SET sal=sal+200
    WHERE deptno=10 AND DATEDIFF(NOW(),hiredate)/365 >= 20

    eg:更新未排序的前3条数据,前3个出现的name为a的改为name为c

    练习题

    答案选A,因为B是升序排列,应该按照降序才取得到前3名,C项UPDATE子句就是错误的用法,D项LIMIT子句参数只能写一个,LIMIT子句在UPDATE中只能包含有一个参数,代表取前3条数据。

    7.2.1 UPDATE语句中的内连接

    因为相关子查询效率非常低,所以我们可以利用表连接的方式来改造UPDATE语句

    UPDATE 表1 JOIN 表2 ON 条件
    SET 字段1=值1, 字段2=值2, ......;

    引申出另一种写法

    UPDATE 表1 JOIN 表2
    SET 字段1=值1, 字段2=值2, ......
    WHERE 条件;

    表连接的UPDATE语句可以修改多张表的记录

    eg:把ALLEN调往RESEARCH部门,职务调整为ANALYST

    /*表连接的几种写法*/
    UPDATE t_emp e JOIN t_dept d ON e.ename="ALLEN" AND d.dname="RESEARCH"
    SET e.deptno=d.deptno, e.job="ANALYST"
    
    UPDATE t_emp e JOIN t_dept d
    SET e.deptno=d.deptno, e.job="ANALYST"
    WHERE e.ename="ALLEN" AND d.dname="RESEARCH"
    
    UPDATE t_emp e,t_dept d
    SET e.deptno=d.deptno, e.job="ANALYST"
    WHERE e.ename="ALLEN" AND d.dname="RESEARCH"

     

    分析:其实利用的是笛卡尔积,笛卡尔积一般对于我们连接没什么用,恰恰这里就起了作用,这个例子可以好好推敲一下,表连接的条件直接将ALLEN这个人连接到RESEARCH部门,RESEARCH部门号是20,赋值给ALLEN的部门号就成功修改,接着修改职务即可。

    eg:把底薪低于公司平均底薪的员工,底薪增加150元

    sql语句如下

    UPDATE t_emp e JOIN
    (SELECT AVG(sal) avg FROM t_emp) t
    ON e.sal<t.avg
    SET e.sal=e.sal+150;

    执行结果就不演示了,从逻辑上也很好理解。

    练习题

    答案选B,和我们上面讲的例子一模一样,即学即用,A项错在标点符号,stu,deptno,这里不是逗号i而是点,C项错在where条件是and而不是or,D项错在update子句不用join的写法连接表,后面条件只能跟where而不是on。

    7.2.2 UPDATE语句中的外连接

    UPDATE语句的表连接既可以是内连接,又可以是外连接。

    基本语法

    UPDATE 表1 [LEFT | RIGHT] JOIN 表2 ON 条件
    SET 字段1=值1, 字段2=值2, ......;

    eg:把没有部门的员工,或者SALES部门低于2000元底薪的员工,都调往20部门

    UPDATE t_emp e LEFT JOIN t_dept d ON e.deptno=d.deptno
    SET e.deptno=20
    WHERE e.deptno IS NULL OR (d.dname="SALES" AND e.sal<2000);

    练习题

    答案选C,根据题意,需要保留没有系别的学生,肯定是左连接,A错,B选项的左连接没有写on条件,B错,D选项筛选数据时是and最后会导致没有数据,应该是or。

    7.3 删除数据

    基本语法

    DELETE [IGNORE] FROM 表名
    [WHERE 条件1, 条件2, ...]
    [ORDER BY ...]
    [LIMIT ...];

    子句执行顺序:FROM -> WHERE -> ORDER BY -> LIMIT -> DELETE

    ignore表示删除失败就直接忽略而不是报错。

    有了前面新增、更新数据的基础,下面的例子我就不展示数据表的变化了,基本语法比较容易理解。

    eg1:删除10部门中,工龄超过20年的员工记录

    DELETE from t_emp
    WHERE deptno=10 AND DATEDIFF(NOW(),hiredate)/365 >20;

    eg2:删除20部门中工资最高的员工记录

    DELETE FROM t_emp
    WHERE deptno=20
    ORDER BY sal+IFNULL(comm,0) DESC
    LIMIT 1;

    提示:如果表中存在主键自增长,那么当删除之后, 自增长不会还原,下一条数据记录插入会在上一次计数的基础继续增加

    练习题

    答案选A,B错在这里的limit只能写一个参数,C错在删除了奖学金最低的人,应该desc降序排列才对,D错在没有限制条件limit。

    7.3.1 DELETE语句中的内连接

    因为相关子查询的效率非常低,所以我们可以利用表连接的方式来改造DELETE语句

    DELETE 表1, ... FROM 表1 JOIN 表2 ON 条件
    [WHERE 条件1, 条件2, ...]
    [ORDER BY ...]
    [LIMIT ...];

    eg1:删除SALES部门该部门的全部员工记录

    DELETE e,d
    FROM t_emp e JOIN t_dept d ON e.deptno=d.deptno
    WHERE d.dname="SALES";

    eg2:删除每个低于部门平均底薪的员工记录

    DELETE e
    FROM t_emp e JOIN
    (SELECT deptno, AVG(sal) avg FROM t_emp GROUP BY deptno) t
    ON e.deptno=t.deptno AND e.sal<t.avg;

    eg3:删除员工KING和他的下属的员工记录,用表连接实现

    DELETE e
    FROM t_emp e JOIN
    (SELECT empno FROM t_emp WHERE ename="KING") t
    ON e.mgr=t.empno OR e.empno=t.empno;

    注意,t 这个临时表是不能删除的,表连接出来的记录就是KING的员工下属和KING本身,删除e即可满足要求。数据表的图示操作就不演示了。

    练习题

    答案选C,即学即用,A错在没按照deptno条件连接,删除了太多无关记录,B错在delete语句中有表连接却没有指定删除的表名,D错在没有分组,查询出来的平均奖学金作为条件没有意义。

    7.3.2 DELETE语句中的外连接

    基本语法

    DELETE 表1, ... FROM 表1 [LEFT | RIGHT] JOIN 表2 ON 条件
    [WHERE 条件1, 条件2, ...]
    [ORDER BY ...]
    [LIMIT ...]

    eg:删除SALES部门的员工,以及没有部门的员工

    这里注意对比上一小节第一个例题,上一小节是删除SALES部门的员工,这里还要删除没有部门的员工,这就是内连接和外连接在这里使用的区别。

    DELETE e
    FROM t_emp e LEFT JOIN t_dept d ON e.deptno=d.deptno
    WHERE d.dname="SALES" OR e.deptno IS NULL;

     练习题

    答案选B,即学即用,就是上面一个知识点。A错在判断为空是 IS NULL不是=NULL,C错在where条件dept.dept-no拼写错误,D错在删除的食品工程系和没有系别的学生应该是并集而不是交集的关系,所以是or而不是and。

    7.3.3 快速删除数据表全部记录

    DELETE语句是在事务机制下删除记录,删除记录之前,先把要删除的记录保存到日志文件里,然后再删除记录。

    TRUNCATE语句在事务机制之外删除记录,速度远超过DELETE语句。

    语法

    TRUNCATE TABLE 表名;

    注意:

    1. drop(drop table 表名)是完全删除表,包括表结构,数据库就查不到这个表了
    2. delete(delete from 表名)是删除表数据,保留表的结构,数据库中该表还存在,如果加where条件,可以只删除一行或者多行,下次插入id不会从1开始,而是从最后一次插入的id+1开始
    3. truncate (truncate table 表名)只能删除全表数据,会保留表结构,数据库中该表还存在,下次插入id从1开始

    如果要永久删除表,应该怎么做?

    只能drop table 表名,用delete和truncate都不行。

    7.4 查询数据

    完整语法

    Select [字段别名]/* from 数据源 [where条件子句] [group by子句] [having子句] [order by子句] [limit 子句];

    7.4.1 Select语句

    最基本的查询语句就是SELECT和FROM关键字组成,SELECT语句屏蔽了物理层的操作,用户不必关系数据的真实存储,交互由数据库高效的查询数据。

    All或者*: 默认保留所有的结果

    Distinct: 去重, 查出来的结果,将重复给去除(所有字段都相同)

    7.4.2 去重查询

    语法格式

    SELECT DISTINCT 字段 FROM 表名;

    假如我们查询员工职业,执行如下语句

    SELECT job FROM t_emp;

    我们发现有很多重复的记录,因为职业是有可能相同的。

    此时我们加上distinct,继续执行

    SELECT DISTINCT job FROM t_emp;

    现在发现查询出来的职业信息就没有重复了。

    注意点:

    1.distinct关键字只能在select子句中使用一次

    SELECT DISTINCT job, DISTINCT ename FROM t_emp;

    写2个distinct直接报错

    2.distinct关键字只能写在select子句的第一个字段前面,否则报错,若有多个字段,则distinct失效。

    SELECT job, DISTINCT ename FROM t_emp; 
    /* distinct写在第二个字段前面 */

     

    distinct没有写在第一个字段前面,结果直接报错

    若有多个字段,即使写在第一个字段前面,distinct也失效。

    SELECT DISTINCT job, ename FROM t_emp;

    job并没有想象中的去重,distinct失效了,因为针对了你的所有字段,只要有一个字段不同就算是不同,所以distinct失效了。

    3.综上1、2所述,distinct只能存在于select子句查询一个字段的情况,否则要么失效,要么语法报错。

    7.4.3 字段别名

    字段别名: 当数据进行查询出来的时候, 有时候名字并不一定就满足需求(多表查询的时候, 会有同名字段). 需要对字段名进行重命名: 别名

    语法

    字段名 [as] 别名;

    再来一个图形化界面的例子

    比如有一个数据表,你想查询员工编号和年收入,你执行结果如下:

    SELECT empno, sal*12 FROM t_emp;

     

    查询的结果集出现了名称为sal*12这一列,语义不明确。添加别名之后

    SELECT empno, sal*12 AS "income" FROM t_emp;

     

    这样就明确多了,这里只是查询的结果集修改了字段,并不会修改底层数据表的字段

    小细节:查询语句的执行顺序是先词法分析与优化,读取SQL语句,然后FROM子句选择数据来源,最后SELECT子句选择输出内容

    7.4.4 数据源

    数据源: 数据的来源, 关系型数据库的来源都是数据表。本质上只要保证数据类似二维表,最终都可以作为数据源。

    数据源分为多种: 单表数据源, 多表数据源, 查询语句

    单表数据源: select * from 表名;

    多表数据源: select* from 表名1,表名2...;

    从一张表中取出一条记录,去另外一张表中匹配所有记录,而且全部保留(记录数和字段数),将这种结果称为笛卡尔积(交叉连接),笛卡尔积没什么用,所以应该尽量避免。只要没有条件,查询多表就会产生笛卡尔积。

    子查询: 数据的来源是一条查询语句(查询语句的结果是二维表)

    Select * from (select 语句) as 表名;

    7.4.5 Where子句

    Where子句: 用来判断数据,筛选数据.

    Where子句返回结果: 0或者1, 0代表false,1代表true.

    语法格式

    SELECT ... FROM ... WHERE 条件 [AND | OR] 条件 ......;

    判断条件:

    比较运算符: >, <, >=, <= ,!= ,<>, =, like, between and, in/not in

    逻辑运算符: &&(and), ||(or), !(not)

    条件查询1: 要求找出学生id为1或者3或者5的学生

    条件查询2: 查出区间落在180,190身高之间的学生:

    Between本身是闭区间。between左边的值必须小于或者等于右边的值

    图形化的例子如下:

    eg1:查询部门编号为10或者20并且收入在2000及以上的记录示例:

    SELECT deptno, empno, ename, sal
    FROM t_emp
    WHERE (deptno=10 OR deptno=20) AND sal >= 2000;

    eg2:查询部门编号为10并且年收入大于15000并且工龄超过20年的职工的一些信息如下

    SELECT deptno, empno, ename, sal, hiredate 
    FROM
    	t_emp 
    WHERE
    	deptno = 10 
    	AND (
    	sal + IFNULL( comm, 0 ))* 12 >= 15000 
    	AND DATEDIFF( NOW(), hiredate )/ 365 >= 20;

     其中IFNULL(comm, 0)表示如果佣金comm为null,则返回0,这里仅仅为了演示IFNULL才加进去的。

    DATEDIFF(NOW(),hiredate)表示当前时间减去入职时间hiredate的天数。

    eg3:查询包含在10,20,30里面的部门编号并且职位不是SALESMAN并且入职日期在1985-01-01以前的员工的一些信息

    SELECT
    empno, ename, sal, deptno, hiredate, job
    FROM t_emp
    WHERE deptno IN(10, 20, 30) AND job != 'SALESMAN'
    AND	hiredate < "1985-01-01";

    例子太多了,下面可以不断变换各种比较运算符去举例,由于篇幅原因,这里不一一举例,只写一点需要注意的地方

    例如判断某个字段是NULL就满足条件,是WHERE comm IS NULL而不是WHERE comm = NULL

    如果不为空则满足条件,是WHERE comm IS NOT NULL而不是WHERE comm != NULL

    比如名字我只记得后面是LACK,第一个字母忘了,WHERE ename like "_LACK"

    我只记得是A开头的, WHERE ename LIKE "A%"

    我只记得名字包含字母A,WHERE ename LIKE "%A%"

    名字大部分人都是英文的,有个中文名但是我不记得了,WHERE ename REGEXP "^[\\u4e00-\\9fa5]{2, 4}$"

    汉族人一般名字是2~4个字,汉字Unicode在\\u4e00-\\9fa5之间,^以...开头,$表示以...结尾。这是正则表达式,很强大,感兴趣的小伙伴可以自行下去搜索一下

    来看几道练习题:

    例子1

    答案选择C,题目没有难度,主要熟悉语法

    例子2

    答案选择C,判断null是不能用等号的,而A是查询已经缴纳宿舍费用的学生姓名。

    例子3

    答案是AD,总学费是tuition和dorm_money两列之和。主要考察IFNULL和BETWEEN的运用。

    例子4

    答案是A,注意NOT IN的使用。

    例子5

    答案为B,可能有同学的疑问点在A和B两个选项中,A项中,只要名字以赵开头,条件就满足,不再往后继续判断,和C语言的短路语句一个道理。

    where语句使用的注意事项:

           WHERE子句中,条件执行的顺序是从左到右的。所以我们应该把索引条件或者筛选掉记录最多的条件写在最左侧。因为索引查询速度快,筛选记录最多的条件更容易触发短路语句的效果,这样就无须执行后续条件就能完成查询。

    小提示:子句的执行顺序是FROM -> WHERE -> SELECT -> ORDER BY -> LIMIT,先选择数据来源,再进行条件筛选,根据筛选完的记录选择输出内容,接着进行排序,最后选择显示的限定条件

    7.4.6 聚合函数

    聚合函数在数据查询分析中,应用十分广泛。聚合函数可以对数据求和、求最大值和最小值、求平均值等等。

    比如SQL提供了如下聚合函数

    Count(): 统计分组后的记录数: 每一组有多少记录

    Max(): 统计每组中非空的最大值

    Min(): 统计非空的最小值

    Avg(): 统计平均值

    Sum(): 统计和

    avg()函数:

    eg:比如求公司员工平均月收入是多少?

    SELECT AVG(sal + IFNULL(comm,0)) AS avg FROM t_emp;
    

    这里sal是月收入,comm是佣金。avg()只用来统计数字,不要去统计别的东西

    max()函数:

    eg1:查询10和20部门中,月收入最高的员工?

    SELECT MAX(sal+IFNULL(comm,0)) FROM t_emp
    WHERE deptno IN(10,20)

    eg2:查询员工名字最长的是几个字符?

    SELECT MAX(LENGTH(ename)) FROM t_emp;

    提示:LENGTH()可以统计字符个数

    min()函数用法和max()一样

    count()函数

    count(*)用于获得包含空值的记录数,count(列名)用于获得包含非空值的记录数

    SELECT COUNT(*), COUNT(comm) FROM t_emp;

    执行结果如上图,表示数据表一共14条数据,而佣金comm不为空的有4条数据

    来个容易混淆的题目

    表结构如下:
    
    CREATE TABLE `score` (
       `id` int(11) NOT NULL AUTO_INCREMENT,
       `sno` int(11) NOT NULL,
       `cno` tinyint(4) NOT NULL,
       `score` tinyint(4) DEFAULT NULL,
       PRIMARY KEY (`id`)
     ) ;
    
    


    以下查询语句结果一定相等的是()
    ①.SELECT sum(score) / count(*) FROM score WHERE cno = 2;

    ②.SELECT sum(score) / count(id) FROM score WHERE cno = 2;

    ③.SELECT sum(score) / count(sno) FROM score WHERE cno = 2;

    ④.SELECT sum(score) / count(score) FROM score WHERE cno = 2;

    ⑤.SELECT sum(score) / count(1) FROM score WHERE cno = 2;

    ⑥.SELECT avg(score) FROM score WHERE cno = 2;


    A:①,⑤,⑥
    B:①,④,⑥
    C:①,②,③,④
    D:④⑥
    E:①,②,⑤,⑥
    F:①,②,③,⑤

    正确答案: D


    几乎所有的聚合函数都会忽略空值(null),除了count(数字)、count(*)。

    count(*)、count(1)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL。

    count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空。

    eg1:查询10和20部门中,底薪超过2000元并且工龄超过15年的员工人数?

    SELECT COUNT(*) FROM t_emp
    WHERE deptno IN(10, 20) AND sal >= 2000
    AND DATEDIFF(NOW(),hiredate)/365 >= 15;

    注意:聚合函数永远不可能出现在where子句里,一定会报错

    练习题:

    答案是D,tuition是学费,dorm_money是宿舍费。都是一些基本语法点的考察。

    7.4.7 Group by子句

    为什么要分组呢?因为默认情况下汇总函数是对全表范围内的数据做统计。

    Group by:主要用来分组查询, 通过一定的规则将一个数据集划分为若干个小的区域,然后针对每个小区域分别进行数据汇总处理。也就是根据某个字段进行分组(相同的放一组,不同的分到不同的组)

    基本语法: group  by 字段名;

    图形化例子:

    eg:根据不同的部门号分组显示平均工资

    SELECT deptno, ROUND(AVG(sal)) FROM t_emp GROUP BY deptno;/*round四舍五入为整数*/

    逐级分组

    数据库支持多列分组条件,执行的时候逐级分组

    eg:查询每个部门里,每种职位的人员数量和平均底薪

    SELECT deptno, job, COUNT(*), AVG(sal)
    FROM t_emp
    GROUP BY deptno, job
    ORDER BY deptno;

    这里千万千万要注意一个硬性要求!

    如果查询语句中含有GROUP BY子句,那么SELECT子句中的内容必须遵守如下约定:

           SELECT子句中可以包含聚合函数或者GROUP BY子句的分组列,其余内容均不可以出现在SELECT子句中。否则查询的结果根本没有任何意义,甚至你自己根本看不懂为什么出现这个结果。任何时候看到GROUP BY 马上检查SELECT子句,若有其他字段,不用往下分析,肯定是混乱的查询。

    假如还是上面的例子

    SELECT deptno, job, COUNT(*), AVG(sal)
    FROM t_emp
    GROUP BY deptno /*相比上面的例子,这里没有job分组,但是select子句却出现了job*/
    ORDER BY deptno;
    /* select子句除了分组列字段deptno和聚合函数,还出现了job!查询结果你都看不懂 */

     

           经过对比,部门为10的里面有3条记录,但是job都为MANAGER??明显不对,看上面一个例子就知道了。

    对分组结果集再次做汇总计算(回溯统计)

           这里就是WITH ROLLUP的使用

    SELECT deptno, AVG(sal), SUM(sal), MAX(sal), MIN(sal), COUNT(*)
    FROM t_emp
    GROUP BY deptno WITH ROLLUP

     

    使用了WITH ROLLUP之后,你发现最底下还有一行,对应列再次做聚合计算,avg列再次做平均值计算,sum列对上面几个部门数据再次进行sum计算...

    GROUP_CONCAT函数

           这个函数可以把分组查询中的某个字段拼接成一个字符串

    eg:查询每个部门内底薪超过2000元的人数和员工姓名

    SELECT deptno, COUNT(*), GROUP_CONCAT(ename)
    FROM t_emp
    WHERE sal >= 2000
    GROUP BY deptno;

    看到ename都是逗号连接的字符串

    练习题

    答案选B,单看聚合函数就排除AC,根据含有GROUP BY子句SELECT子句会有硬性要求的问题,SELECT子句除了聚合函数以外的其他字段必须要出现在GROUP BY子句,所以排除D,答案选择B.

    小提示:语句的执行顺序如下:

    FROM -> WHERE -> GROUP BY -> SELECT -> ORDER BY -> LIMIT

    FROM 选择数据来源,WHERE选择条件,符合条件的记录留下来,然后经过GROUP BY分组,分完组根据SELECT子句里面聚合函数做计算,然后ORDER BY对结果集排序,最后交给LIMIT挑选返回哪些分页的数据显示。

    ====下面几个控制台执行的例子是我之前写的,就不删了,大家也可以对照看一下====

    分组会自动排序: 根据分组字段:默认升序

    Group by 字段 [asc|desc]; -- 对分组的结果然后合并之后的整个结果进行排序

    多字段分组: 先根据一个字段进行分组,然后对分组后的结果再次按照其他字段进行分组

    有一个函数: 可以对分组的结果中的某个字段进行字符串连接(保留该组所有的某个字段): group_concat(字段)

    这对于group by一个字段,而在select语句想查询除了group by字段以外的字段时,非常有用。

    回溯统计: with rollup: 任何一个分组后都会有一个小组, 最后都需要向上级分组进行汇报统计: 根据当前分组的字段. 这就是回溯统计: 回溯统计的时候会将分组字段置空.

    多字段回溯: 考虑第一层分组会有此回溯: 第二次分组要看第一次分组的组数, 组数是多少,回溯就是多少,然后加上第一层回溯即可.

    7.4.8 Having子句

    Having子句与where子句一样是进行条件判断的.

    有同学会问了,和where子句功能一样,那还有什么用,多此一举?

    eg1:查询部门平均底薪超过2000的员工数量,你是不是会这样写?

    SELECT deptno, COUNT(*)
    FROM t_emp
    WHERE AVG(sal) >= 2000
    GROUP BY deptno;

    结果运行出错,我们前面也说了,WHERE子句不允许出现聚合函数。而且WHERE优先级高于GROUP BY,在条件筛选的时候不知道按照什么范围去筛选,是全部数据筛选还是分部门数据筛选呢?

    解决方案来了,那就是HAVING子句,HAVING子句的出现主要是为了WHERE子句不能使用聚合函数的问题,HAVING子句不能独立存在,必须依赖于GROUP BY子句而存在,GROUP BY 执行完成就立即执行HAVING子句

    SELECT deptno, COUNT(*)
    FROM t_emp
    GROUP BY deptno HAVING AVG(sal) >= 2000;

    结果就出来了,20部门底薪超过2000的有5人,10部门底薪超过2000的有3人

    eg2:查询每个部门中,查询每个部门中,1982年以后入职员工超过2个人的部门编号

    SELECT deptno FROM t_emp
    WHERE hiredate>="1982-01-01"
    GROUP BY deptno HAVING COUNT(*)
    ORDER BY deptno;

     

    可以看到满足条件的有2个部门,10部门和20部门还是有老员工的。

    要注意HAVING子句判断只能和具体数值判断大小,不能和字段以及聚合函数判断,比较要有数值。比如查询工资大于平均工资的人的数量就不能写HAVING sal > AVG(sal),子句判断不是和数值在比较,直接报错。表连接能解决这个问题,后面再讲。

    HAVING子句的特殊用法

    如果按照数字1分组,MySQL会按照SELECT子句中的列进行分组,HAVING子句也可以正常使用

    比如按照部门分组,查询各个部门总人数

    SELECT deptno, COUNT(*) FROM t_emp GROUP BY 1;

    HAVING的出现是不是可以完全替换WHERE?

    那肯定是不行的,Where是针对磁盘数据进行判断,进入到内存之后会进行分组操作,分组结果就需要having来处理.

    SELECT deptno, COUNT(*) FROM t_emp
    GROUP BY 1
    HAVING deptno IN(10, 30);/*效率低了*/
    
    SELECT deptno, COUNT(*) FROM t_emp 
    WHERE deptno IN(10, 30)
    GROUP BY 1;

     从功能上来说,上面两种写法没有什么区别,但是WHERE优先级在GROUP BY之前,是先把数据按条件筛选完了再分组好呢,还是分完组再去筛选好呢?肯定是前者。所以WHERE能完成的就用WHERE完成,不要放到HAVING中。大量的数据从磁盘读取到内容代价比较大,先筛选完了,再把符合条件的记录读取到内存中显然更好。

    Having能做where能做的几乎所有事情, 但是where却不能做having能做的很多事情.

     1.分组统计的结果或者说统计函数都只有having能够使用.

    2.Having能够使用字段别名,where不能,where是从磁盘取数据,而名字只可能是字段名,别名是在字段进入到内存后才会产生.

    练习题

    答案选择A,基本语法的运用,看清表是student没有s。

    7.4.9 Order by子句

    Order by: 排序, 根据某个字段进行升序或者降序排序, 依赖校对集.

    使用基本语法

    单字段排序:

    Order by 字段名 [asc|desc]; -- asc是升序(默认的),desc是降序

    我们再图形化举例示范一下:

    执行如下语句

    SELECT empno, ename, sal, deptno FROM t_emp ORDER BY sal DESC;

    按照sal降序就排好了。

    来个练习题:

    很简单,不用多说就知道答案,估摸着有人在BC里面纠结呢,这不一样吗,答案选B,select选择输出字段之间逗号隔开,细节问题。

    多字段排序:

    使用order by 规定首要条件和次要条件排序。数据库会先按照首要条件排序,遇到首要排序内容相同的记录,那么会启用次要条件再次排序。

    使用图形化界面再举一个例子:

    执行如下语句

    SELECT empno, ename, sal, hiredate 
    FROM t_emp ORDER BY sal DESC, hiredate ASC;

    可以看到当首要排序条件sal记录相同时,会按照hiredate进行升序排列

    小提示:

    1.order by 写在 limit前面

    2.子句的执行顺序是FROM -> SELECT -> ORDER BY -> LIMIT,先选择数据来源,再选择输出内容,接着进行排序,最后选择显示的限定条件

    来个练习题:

    A排除,和表不对应,没有name字段,B排除,多字段之间排序用逗号隔开,D排除,升序是ASC或者不写,所以选C。

    7.4.10 Limit子句

    Limit子句是一种限制结果的语句,用来做数据分页的。

    比如我们看朋友圈,只会加载少量的部分信息,不会一次性加载全部朋友圈,那样只会浪费CPU时间、内存、网络带宽。而结果集的记录可能很多,可以使用limit关键字限定结果集数量。

    Limit有两种使用方式

    方案1: 只用来限制长度(数据量): limit 数据量;

    方案2: 限制起始位置,限制数量: limit 起始位置,长度;

    Limit方案2主要用来实现数据的分页: 为用户节省时间,提交服务器的响应效率, 减少资源的浪费.

    对于用户来讲: 可以点击的分页按钮: 1,2,3,4

    对于服务器来讲: 根据用户选择的页码来获取不同的数据: limit offset,length;

    Length: 每页显示的数据量: 基本不变

    Offset: offset = (页码 - 1) * 每页显示量

    小提示:子句的执行顺序 FROM -> SELECT -> LIMIT,先选择数据来源,再选择输出内容,最后选择显示的限定条件

    7.4.11 select语句中各关键字的先后顺序

    (1)from 
    (3) join 
    (2) on 
    (4) where 
    (5)group by(开始使用select中的别名,后面的语句中都可以使用)
    (6) avg,sum.... 
    (7)having 
    (8) select 
    (9) distinct 
    (10) order by
    (11) limit 

    第8章 连接查询

    连接查询: 将多张表(可以大于2张)进行记录的连接(按照某个指定的条件进行数据拼接): 最终结果是: 记录数有可能变化, 字段数一定会增加(至少两张表的合并)

    连接查询的意义: 在用户查看数据的时候,需要显示的数据来自多张表.

    连接查询: join, 使用方式: 左表 join 右表

    左表: 在join关键字左边的表

    右表: 在join关键字右边的表

    8.1 连接查询分类

    SQL中将连接查询分成四类: 内连接,外连接,自然连接和交叉连接

    8.2 交叉连接

    交叉连接: cross join, 从一张表中循环取出每一条记录, 每条记录都去另外一张表的所有记录逐个进行匹配,并保留所有记录,最终形成的结果叫做笛卡尔积.

    基本语法: 左表 [cross] join 右表。其中cross可以省略

    笛卡尔积对于我们的查询没有意义,应该尽量避免(交叉连接没用)

    交叉连接存在的价值: 保证连接这种结构的完整性

    8.3 内连接

    内连接: [inner] join, 从左表中取出每一条记录,去右表中与所有的记录进行匹配,匹配必须是某个条件在左表中与右表中相同最终才会保留结果,否则不保留.

    如下,某个条件左右表相同部分的交集

    基本语法

    SELECT ...... FROM 表1
    [INNER] JOIN 表2 ON 条件
    [INNER] JOIN 表3 ON 条件
    ......

    内连接其实有多种语法形式,想用哪种看个人喜好,效率上没有区别。

    SELECT ... FROM 表1 JOIN 表2 ON 连接条件;
    SELECT ... FROM 表1 JOIN 表2 WHERE 连接条件;
    SELECT ... FROM 表1, 表2 WHERE 连接条件;

     我们来做个例题,首先我们看到前提条件给出了3张表

    1.员工表t_emp

    2.部门表t_dept

    3.薪资等级表t_salgrade

    有人会问了,内连接语法看起来就是交叉连接多了一个ON条件,但是区别可大了,来直观感受一下

    SELECT * FROM t_emp  JOIN t_dept /*交叉连接*/

     

    交叉连接产生笛卡尔积,保留所有结果,导致出现了56条记录

    SELECT * FROM t_emp e JOIN t_dept d ON e.deptno = d.deptno; /*内连接*/

     

    内连接就只针对符合条件的记录去连接,结果集少了很多条记录。

    注意:在查询数据的时候,不同表有同名字段,这个时候需要加上表名才能区分,而表名太长,通常可以使用别名,这里两张表都有deptno,表名也缩短为了一个字母

    再来看看具体例题

    eg1:查询每个员工的工号、姓名、部门名称、底薪、职位、工资等级

    分析:工号empno、姓名ename、底薪sal、职位job是在员工表t_emp,部门名称dname是在部门表t_dept,工资等级grade是在薪资等级表t_salgrade。现在就涉及到了3个表的操作,而员工表t_emp和部门表t_dept都有员工编号deptno字段,这个很容易作为筛选条件, 但是工资等级grade却没有相同字段去对应,那么这个就需要找到逻辑关系的对应,用底薪sal去判断薪资等级中的薪水范围即可

    SELECT e.empno, e.ename, d.dname, e.sal, e.job, s.grade
    FROM t_emp e JOIN t_dept d ON e.deptno = d.deptno
    JOIN t_salgrade s ON e.sal BETWEEN s.losal AND s.hisal;

    eg2:查询与SCOTT相同部门的员工

    分析:还是那3张表,要查和某个人相同部门的员工,有人就开始这么做,上去就是一个sql

    SELECT ename
    FROM t_emp
    WHERE deptno=(SELECT deptno FROM t_emp WHERE ename="SCOTT")
    AND ename!="SCOTT";

    括号中的查询我们称为子表,子表中查询到deptno然后把结果集给父表继续查询,写完感觉自我良好,殊不知自己写了一个领导看到就想把你开除的sql。

    FROM先执行,获取了数据表的每条记录,再去WHERE进行筛选,万一有上万条数据呢?WHERE会逐一判断上万条数据是否满足条件的时候都要去查询一个子表,相当于SELECT deptno FROM t_emp WHERE ename="SCOTT"被你执行了上万次,而子表也是上万条数据,每一次父表的条件判断又会执行上万次子表查询,数据量小的时候看不出差异,数据量大了就很明显了。

    这里用表连接的效率远远高于子查询

    SELECT e2.ename
    FROM t_emp e1 JOIN t_emp e2 ON e1.deptno=e2.deptno
    WHERE e1.ename="SCOTT" AND e2.ename!="SCOTT";

    先内连接减少数据源结果集的数量,然后进行筛选。能达到和子查询一样的效果,效率比子查询要高。

    eg3:查询底薪超过公司平均底薪的员工信息

    SELECT e.empno, e.ename, e.sal
    FROM t_emp e JOIN
    (SELECT AVG(sal) avg FROM t_emp) t 
    ON e.sal >= t.avg;

    把平均底薪查询结果当作一个表再和员工表t_emp连接,返回FROM子句。之前说过,这个问题是WHERE解决不了的,WHERE里面不能出现聚合函数的,直接写WHERE sal >= AVG(sal)肯定报错,而HAVING子句又只能和数值比较,这里e.sal>=t.avg表达式两边都是变量,HAVING子句无法解决。

    eg4:查询RESEARCH部门人数、最高底薪、最低底薪、平均底薪、平均工龄

    SELECT COUNT(*), MAX(e.sal), MIN(e.sal), AVG(e.sal),
    AVG(DATEDIFF(NOW(),e.hiredate)/365)
    FROM t_emp e JOIN t_dept d ON e.deptno=d.deptno
    WHERE d.dname="RESEARCH";

    如果前面的题目都懂了,这题就是语法复习,表连接和聚合函数的使用。

    eg5:查询每种职业的最高工资、最低工资、平均工资、最高工资等级和最低工资等级

    分析:涉及到工资等级,需要薪资等级表t_salgrade,那么就是员工表和薪资等级表的连接,因为同一种职业不同人有不同的收入,所有根据收入等级排工资等级,逻辑要捋清楚。

    SELECT
    e.job,
    MAX(e.sal + IFNULL(e.comm,0)),
    MIN(e.sal + IFNULL(e.comm,0)),
    AVG(e.sal + IFNULL(e.comm,0)),
    MAX(s.grade),
    MIN(s.grade)
    FROM t_emp e JOIN t_salgrade s
    ON (e.sal + IFNULL(e.comm,0)) BETWEEN s.losal AND s.hisal
    GROUP BY e.job;

    eg6:查询每个底薪超过部门平均底薪的员工信息

    SELECT e.empno, e.ename, e.sal
    FROM t_emp e JOIN
    (SELECT deptno, AVG(sal) avg FROM t_emp GROUP BY deptno) t
    ON e.deptno=t.deptno AND e.sal >= t.avg;

    如果只运行子表查询,得到各个部门平均底薪,可以和上图对比一下

    练习一下选择题

    答案选B,都是语法细节,多一个少一个标点符号的问题。A错在别名问题,应该将子表别名写在括号外,C错在没有join,写了个逗号,D错在,select子句少了逗号,这个题目考察眼力哈哈哈。

    答案选择A,考察表连接的另一种写法SELECT ... FROM 表1, 表2 WHERE 连接条件,排除D,因为两个表之间没有逗号,再排除C,因为只从一张表查不出那么多信息,最后排除B,因为NOW()后面没有逗号。

    8.4 外连接

    外连接分为两种:左(外)连接和右(外)连接。

    左外连接就是保留左表所有记录,与右表做连接。如果右表有符合条件的记录就与左表连接。如果右表没有符合条件的记录,就用NULL与左表连接。右连接也是如此。

    基本语法: 左表 left/right join 右表 on 左表.字段 = 右表.字段;

    为什么要有外连接?

           我们还是以内连接中提到的3张数据表为例子。

           如果有一名临时员工,没有固定的部门编号,那么我们查询每名员工和他的部门名称,用内连接就会漏掉临时员工,所以要引入外连接语法才能解决这个问题。外连接与内连接的区别在于,除了符合条件的记录之外,结果集中还会保留不符合条件的记录。

    含有临时员工的员工表t_emp

    部门表t_dept

     

    薪资等级表t_salgrade

    eg1:查询每名员工和他的部门名称

    假设我们使用内连接,我们根本查不到临时员工信息,因为临时员工没有部门编号,如下:

    SELECT e.empno, e.ename, d.dname
    FROM t_emp e JOIN t_dept d
    ON e.deptno=d.deptno;

    当我们使用外连接时,就能够查到临时员工,如下:

    /*左连接*/
    SELECT e.empno, e.ename, d.dname
    FROM t_emp e LEFT JOIN t_dept d
    ON e.deptno=d.deptno;
    
    /*右连接,换一下表的顺序,结果集一样*/
    SELECT e.empno, e.ename, d.dname
    FROM t_dept d RIGHT JOIN t_emp e 
    ON e.deptno=d.deptno;

    左表是员工表,左连接保留所有记录,没有部门编号的临时员工信息也会保留,右表部门编号没有与之匹配,那就用NULL连接。

    eg2:查询每个部门的名称和部门的人数

    有人容易写出下面的错误sql语句

    SELECT d.dname, COUNT(*)
    FROM t_dept d LEFT JOIN t_emp e
    ON d.deptno=e.deptno
    GROUP BY d.deptno;/*按部门分组,所以有group by*/

     

    这题很多细节,很多人会出错,40部门的部门名称为dname为OPERATIONS里没有员工,居然还是有一条记录,因为你在连接的时候左表记录全部保留,在右表中没有员工与OPERATIONS部门匹配,连接的是NULL,这也是一条记录,所以这里才会出现1。

    但是你也不要写成COUNT(d.deptno),因为左边部门表记录全保留,d.deptno有40部门,40部门的dname就是OPERATIONS,右表与之连接的都是NULL,道理和上面一样。

    所以你得按照右边员工表计算,COUNT(e.deptno),记录各个部门非空记录数。40部门没有员工,右表e.deptno没有40,NULL不会被COUNT(e.deptno)计算入内,所以是0,符合预期。

    正确的sql语句如下:

    SELECT d.dname, COUNT(e.deptno)
    FROM t_dept d LEFT JOIN t_emp e
    ON d.deptno=e.deptno
    GROUP BY d.deptno;/*按部门分组,所以有group by*/

    eg3:查询每个部门的名称和部门的人数,如果是没有部门的临时员工,部门名称用NULL代替

    分析:我们上一个例子已经做到了查询部门名称和部门的人数,现在就差一个临时员工和他的部门的问题,临时员工还在等着被你统计呢。临时员工在t_emp表,所以你要保留这个表的所有内容再把eg2例子的查询语句一起联合查询

    (SELECT d.dname, COUNT(e.deptno)
    FROM t_dept d LEFT JOIN t_emp e
    ON d.deptno=e.deptno
    GROUP BY d.deptno) UNION
    (SELECT d.dname, COUNT(*)
    FROM t_dept d RIGHT JOIN t_emp e
    ON d.deptno=e.deptno
    GROUP BY d.deptno);

    这个部门名称dname为NULL的就是那个临时员工。

    eg4:查询每名员工的编号、姓名、部门名称、月薪、工资等级、工龄、上司编号、上司姓名、上司部门(这个题有点综合,没点基础做不出来)

    分析:要查员工的编号、姓名、部门名称、工龄,涉及到员工表t_emp、部门表t_dept,查工资等级涉及到薪资等级表t_salgrade,有的员工是其他员工的上司,所以我们为员工表再做一次查询连接起来当作领导表,连接条件是员工的领导编号和领导的员工编号相等时,这个领导表查出来的员工,就是员工表里对应员工的领导。sql如下,你细品

    SELECT 
    	e.empno, e.ename, d.dname,
    	e.sal + IFNULL(e.comm,0), s.grade,
    	FLOOR(DATEDIFF(NOW(),e.hiredate)/365),
    	t.empno AS mgrno, t.ename AS mname, t.dname AS mdname
    FROM t_emp e LEFT JOIN t_dept d ON e.deptno=d.deptno
    LEFT JOIN t_salgrade s ON e.sal BETWEEN s.losal AND s.hisal
    LEFT JOIN
    (SELECT	e1.empno, e1.ename, d1.dname
    FROM t_emp e1 JOIN t_dept d1
    ON e1.deptno=d1.deptno
    ) t ON e.mgr=t.empno;

    外连接的注意事项:

    内连接只保留符合条件的记录,所以查询条件写在ON子句和WHERE子句中的效果是相同的。但是外连接里,条件写在WHERE子句里,不符合条件的记录是会被过滤掉的,而不是保留下来。

    我们来看看具体差别

    SELECT e.ename, d.dname, d.deptno
    FROM t_emp e
    LEFT JOIN t_dept d ON e.deptno=d.deptno
    AND e.deptno=10; /*这里是and不是where*/

     分析:左连接保留左表全部,按条件连接右表,不仅要部门编号相同,还要部门编号为10,不满足的用NULL连接,所以总记录条数就是左表的COUNT(*)数量

    改为WHERE之后

    SELECT e.ename, d.dname, d.deptno
    FROM t_emp e
    LEFT JOIN t_dept d ON e.deptno=d.deptno
    WHERE e.deptno=10;

    分析:左连接保留左表全部,按照部门号进行对应连接,连接完再进行筛选员工部门号位10的记录,不满足的就过滤。一步步的执行过程如下图

    8.5 自然连接

    自然连接: natural join, 自然连接, 就是自动匹配连接条件: 系统以字段名字作为匹配模式(同名字段就作为条件, 多个同名字段都作为条件).

    自然连接: 可以分为自然内连接和自然外连接.

    自然内连接: 左表 natural join 右表;

    自然外连接: 左表 natural left/right join 右表;

    其实, 内连接和外连接都可以模拟自然连接: 使用同名字段,合并字段

    左表 left/right/inner join 右表 using(字段名); -- 使用同名字段作为连接条件: 自动合并条件

    多表连接: A表 inner join B表 on 条件 left join C表 on条件 ...

    执行顺序: A表内连接B表,得到一个二维表, 左连接C表形成二维表..

    8.6 子查询

    子查询: sub query, 查询是在某个查询结果之上进行的.(一条select查询的sql语句内部包含了另外一条select查询的sql语句).

    8.6.1 子查询分类

    Where子查询: 子查询出现where条件中,where语句里不推荐使用子查询,每执行一次where条件筛选,就会进行一次子查询,效率低下。像这种反复子查询就属于相关子查询,where语句的子查询都属于相关子查询,我们要避免相关子查询的存在。

    比如查询底薪超过公司平均底薪的员工信息

    From子查询: 子查询跟在from之后,通常这种子查询的结果集作为一个临时表,from子查询只会执行一次,不是相关子查询,所以查询效率高。

    SELECT子查询,子查询跟在SELECT之后,SELECT子查询也是相关子查询,不推荐

    8.6.2 单行子查询和多行子查询

    单行子查询的结果集只有一条记录,多行子查询结果集有多行记录

    多行子查询只能出现在WHERE子句和FROM子句中

    eg:如何用子查询查找FORD和MARTIN两个人的同事?

    分析:同一个部门的都算作同事,而且题目限定了用子查询来做,所以不用表连接做。

    SELECT ename FROM t_emp
    WHERE deptno IN
    (SELECT deptno FROM t_emp WHERE ename IN("FORD","MARTIN"))
    AND ename NOT IN("FORD","MARTIN");

    当然这个题目用表连接做时最好的,效率比WHERE里面子查询高的多,只不过这里题目要求用子查询,这里我们还是给出表连接的sql语句供大家参考

    SELECT ename
    FROM t_emp e
    JOIN
    (SELECT deptno FROM t_emp WHERE ename IN("FORD","MARTIN")) d
    ON e.deptno=d.deptno
    AND ename NOT IN("FORD","MARTIN");/*不需要用e.ename因为只有e表有ename*/

    8.6.3 WHERE子句中的多行子查询

    WHERE子句中,可以用IN、ALL、ANY、EXISTS关键字来处理多行表达式结果集的条件判断。

    eg:查询比FORD和MARTIN底薪都高的员工信息?

    SELECT ename FROM t_emp
    WHERE sal > ALL
    (SELECT sal FROM t_emp
    WHERE ename IN("FORD","MARTIN"));
    

    这里是ALL,表示比FORD和MARTIN底薪都高,如果换成ANY,则表示比两者任意一个高就满足条件

    8.6.4 子查询的EXISTS关键字

    EXISTS关键字是把原来在子查询之外的条件判断,写到了子查询的里面。

    SELECT ... FROM 表名 WHERE [NOT] EXISTS (子查询)

    eg:查询工资等级是3级或者4级的员工信息

    SELECT empno, ename, sal
    FROM t_emp
    WHERE EXISTS(
    SELECT *           /*这里选择其他字段也可以,比如grade*/
    FROM t_salgrade
    WHERE sal BETWEEN losal AND hisal
    AND grade IN(3,4)
    )

    只要子查询结果为不为空,那么EXISTS这个条件就是满足的,这条记录就满足条件不会被过滤。

    这里只是演示WHERE多行子查询的EXISTS关键字,解决这个问题用表连接其实好的多。如下:

    SELECT empno, ename, sal
    FROM t_emp
    JOIN t_salgrade
    ON sal BETWEEN losal AND hisal AND grade IN(3,4)

    第9章 视图

    视图: view, 是一种有结构(有行有列)但是没结果(结构中不真实存放数据)的虚拟表, 虚拟表的结构来源不是自己定义, 而是从对应的基表中产生(视图的数据来源).

    9.1 创建视图

    基本语法

    Create view 视图名字 as select语句; -- select语句可以是普通查询;可以是连接查询; 可以是联合查询; 可以是子查询.

    创建单表视图: 基表只有一个

    创建多表视图: 基表来源至少两个

    9.2 查看视图

    查看视图: 查看视图的结构

    视图是一张虚拟表: 表, 表的所有查看方式都适用于视图: show tables [like]/desc 视图名字/show create table 视图名;

    视图比表还是有一个关键字的区别: view. 查看”表(视图)”的创建语句的时候可以使用view关键字

    视图一旦创建: 系统会在视图对应的数据库文件夹下创建一个对应的结构文件: frm文件

    9.3 使用视图

    使用视图主要是为了查询: 将视图当做表一样查询即可.

    视图的执行: 其实本质就是执行封装的select语句.

    9.4 修改视图

    视图本身不可修改, 但是视图的来源是可以修改的.

    修改视图: 修改视图本身的来源语句(select语句)

    Alter view 视图名字 as 新的select语句;

    9.5 删除视图

    Drop view 视图名字;

    9.6 视图意义

    1. 视图可以节省SQL语句: 将一条复杂的查询语句使用视图进行保存: 以后可以直接对视图进行操作
    2. 数据安全: 视图操作是主要针对查询的, 如果对视图结构进行处理(删除), 不会影响基表数据(相对安全).
    3. 视图往往是在大项目中使用, 而且是多系统使用: 可以对外提供有用的数据, 但是隐藏关键(无用)的数据: 数据安全
    4. 视图可以对外提供友好型: 不同的视图提供不同的数据, 对外好像专门设计
    5. 视图可以更好(容易)的进行权限控制
     

    9.7 视图数据操作

    视图是的确可以进行数据写操作的: 但是有很多限制

    将数据直接在视图上进行操作.

    9.7.1 新增数据

    数据新增就是直接对视图进行数据新增.

    1.多表视图不能新增数据

    2.可以向单表视图插入数据: 但是视图中包含的字段必须有基表中所有不能为空(或者没有默认值)字段

    3.视图是可以向基表插入数据的.

    9.7.2 删除数据

    多表视图不能删除数据

    单表视图可以删除数据

    9.7.3 更新数据

    理论上不能单表视图还是多表示视图都可以更新数据.

    更新限制: with check option, 如果对视图在新增的时候,限定了某个字段有限制: 那么在对视图进行数据更新操作时,系统会进行验证: 要保证更新之后,数据依然可以被实体查询出来,否则不让更新.

    9.8 视图算法

    视图算法: 系统对视图以及外部查询视图的Select语句的一种解析方式.

    视图算法分为三种

    Undefined: 未定义(默认的), 这不是一种实际使用算法, 是一种推卸责任的算法: 告诉系统,视图没有定义算法, 系统自己看着办

    Temptable: 临时表算法: 系统应该先执行视图的select语句,后执行外部查询语句

    Merge: 合并算法: 系统应该先将视图对应的select语句与外部查询视图的select语句进行合并,然后执行(效率高: 常态)

    算法指定: 在创建视图的时候

    Create algorithm = 指定算法 view 视图名字 as select语句;

    视图算法选择: 如果视图的select语句中会包含一个查询子句(五子句), 而且很有可能顺序比外部的查询语句要靠后, 一定要使用算法temptable,其他情况可以不用指定(默认即可).

    第10章 数据备份与还原

    备份: 将当前已有的数据或者记录保留

    还原: 将已经保留的数据恢复到对应的表中

    为什么要做备份还原?

    防止数据丢失: 被盗, 误操作
    保护数据记录
     

    数据备份还原的方式有很多种: 数据表备份, 单表数据备份, SQL备份, 增量备份.

    10.1 数据表备份

    不需要通过SQL来备份: 直接进入到数据库文件夹复制对应的表结构以及数据文件, 以后还原的时候,直接将备份的内容放进去即可.

    数据表备份有前提条件: 根据不同的存储引擎有不同的区别.

    存储引擎: mysql进行数据存储的方式: 主要是两种: innodb和myisam(免费)

    对比myisam和innodb: 数据存储方式

    Innodb: 只有表结构,数据全部存储到ibdata1文件中

    Myisam: 表,数据和索引全部单独分开存储

    这种文件备份通常适用于myisam存储引擎: 直接复制三个文件即可, 然后直接放到对应的数据库下即可以使用.

    10.2 单表数据备份

    每次只能备份一张表; 只能备份数据(表结构不能备份)

    如果业务数据非常多,建议只导出表结构,然后用SELECT INTO OUTFILE把数据导出成文本文档,具体操作可以看10.5节图形化操作。

    备份: 从表中选出一部分数据保存到外部的文件中(outfile)

    Select */字段列表 into outfile 文件所在路径 from 数据源; -- 前提: 外部文件不存在

    高级备份: 自己制定字段和行的处理方式

    Select */字段列表 into outfile 文件所在路径 fields 字段处理 lines 行处理 from 数据源;

    Fields: 字段处理

    Enclosed by: 字段使用什么内容包裹, 默认是’’,空字符串

    Terminated by: 字段以什么结束, 默认是”\t”, tab键

    Escaped by: 特殊符号用什么方式处理,默认是’\\’, 使用反斜杠转义

    Lines: 行处理

    Starting by: 每行以什么开始, 默认是’’,空字符串

    Terminated by: 每行以什么结束,默认是”\r\n”,换行符

    数据还原: 将一个在外部保存的数据重新恢复到表中(如果表结构不存在,那么sorry)

    Load data infile 文件所在路径 into table 表名[(字段列表)] fields字段处理 lines 行处理; -- 怎么备份的怎么还原

    10.3 SQL备份与还原

    备份的是SQL语句: 系统会对表结构以及数据进行处理,变成对应的SQL语句, 然后进行备份: 还原的时候只要执行SQL指令即可.(主要就是针对表结构)

    备份: mysql没有提供备份指令: 需要利用mysql提供的软件: mysqldump.exe

    Mysqldump.exe也是一种客户端,需要操作服务器: 必须连接认证

    Mysqldump/mysqldump.exe -hPup 数据库名字 [数据表名字1[ 数据表名字2...]] > 外部文件目录(建议使用.sql)

    mysqldump用来把业务数据导出成SQL文件,其中也包括了表结构

    mysqldump -uroot -p [no-data] 逻辑库 > 路径

    不写no-data表示既包含表结构,又包含数据

    单表备份

    图形化操作如下,选中数据表,点击右键

    整库备份

    Mysqldump/mysqldump.exe -hPup 数据库名字 > 外部文件目录

    对应图形化操作如下,选中数据库选中右键

    SQL还原数据: 两种方式还原

    方案1: 使用mysql.exe客户端还原

    Mysql.exe/mysql -hPup 数据库名字 < 备份文件目录

    方案2: 使用SQL指令还原

    1.use选择数据库; 2.Source 备份文件所在路径;

    对应图形化操作如下

    SQL备份优缺点

    优点: 可以备份结构
    缺点: 会浪费空间(额外的增加SQL指令)

    练习题

    答案选A,语法记忆,注意标点符号。

    10.4 增量备份

    不是针对数据或者SQL指令进行备份: 是针对mysql服务器的日志文件进行备份

    增量备份: 指定时间段开始进行备份., 备份数据不会重复, 而且所有的操作都会备份(大项目都用增量备份)

    练习题

    答案选C,C错在数据导出,导出的纯粹是业务数据。

    10.5 大文件备份和还原(图形化操作,推荐!)

    业务数据比较多的时候,只导出表结构到sql文件,业务数据文件导出到txt文件,这样就跳过了sql词法分析和语法优化,哪怕导入几千万条数据,也可以在1分钟内导入完毕

    1.导出表结构

    2.导出表中业务数据

    3.删除表,为导入做准备

    4.导入表结构

    5.刷新后看到表结构

    6.导入业务数据文件

    7.刷新表即可看到导入成功

    第11章 事务安全

    事务: transaction, 一系列要发生的连续的操作

    事务安全: 一种保护连续操作同时满足(实现)的一种机制

    事务安全的意义: 保证数据操作的完整性

    如果SQL语句直接操作文件是很危险的,比如你要给员工涨工资,正在update操作的时候,系统断电了,你就不知道谁已经涨了谁还没涨。

    我们应该利用日志来间接写入。

    MySQL总共5种日志,其中只有redo日志和undo日志与事务有关

    日志就相当于数据文件的一个副本,SQL语句操作什么样的记录,MySQL就会把这些记录拷贝到undo日志,然后增删改查的操作就会记录到redo日志,最后把redo日志和数据库文件进行同步就行了。即使同步过程中断电了,有了redo日志的存在,重启MySQL数据库之后继续同步数据,同步成功后我们修改的数据就真正同步到数据库里面了,有事务的数据库抵抗风险的能力变强了。

    RDBMS=SQL语句+事务(ACID)

    事务是一个或者多个SQL语句组成的整体,要么全部执行成功,要么全部失败。

    11.1 事务操作

    事务操作分为两种: 自动事务(默认的), 手动事务

    默认情况下,MySQL执行每条SQL语句都会自动开启和提交事务。为了让多条SQL语句纳入到一个事物之下,可以手动管理事务。

    START TRANSACTION;

    SQL语句

    [COMMIT | ROLLBACK];

    START TRANSACTION;
    
    DELETE FROM t_emp;
    DELETE FROM t_dept;
    SELECT * FROM t_emp;
    SELECT * FROM t_dept;

    开启事务: 告诉系统以下所有的操作(写)不要直接写入到数据表, 先存放到redo日志。

    删除员工表t_emp和部门表t_dept之后,SQL语句查询两表的的数据均为空

    但是去看数据表的数据却仍然存在,这是为什么呢?

    因为你开启了事务,你现在的操作还在redo日志里面,并没有同步到数据库文件里面,你只有COMMIT之后才会同步

    继续执行

    COMMIT;

    去数据表查看,2张数据表都被清空了。

    当然你也可以直接回滚,执行ROLLBACK;

    ROLLBACK;

    这样你的redo日志被清空,下次操作的时候重新往redo日志里面进行操作,就不会受到上一次操作的影响。

    11.2 自动事务处理

    在mysql中: 默认的都是自动事务处理, 用户操作完会立即同步到数据表中.

    自动事务: 系统通过autocommit变量控制

    Show variables like ‘autocommit’;

    关闭自动提交: set autocommit = off/0;

    再次直接写操作

    自动关闭之后,需要手动来选择处理: commit提交, rollback回滚

    注意: 通常都会使用自动事务

    11.3 事务原理

    事务操作原理: 事务开启之后, 所有的操作都会临时保存到事务日志, 事务日志只有在得到commit命令才会同步到数据表,其他任何情况都会清空(rollback, 断电, 断开连接)

    11.4 回滚点

    回滚点: 在某个成功的操作完成之后, 后续的操作有可能成功有可能失败, 但是不管成功还是失败,前面操作都已经成功: 可以在当前成功的位置, 设置一个点: 可以供后续失败操作返回到该位置, 而不是返回所有操作, 这个点称之为回滚点.

    设置回滚点语法: savepoint 回滚点名字;

    回到回滚点语法: rollback to 回滚点名字;

    11.5 事务ACID属性

    A: Atomic原子性,一个事物中的所有操作要么全部完成,要么全部失败。事物执行后,不允许停留在中间某个状态。

    C: Consistency一致性,不管在任何给定的时间,并发事务有多少,事务必须保证运行结果的一致性。事务可以并发执行,但是最终MySQL却串行执行。

    怎么保证一致性?

    阻止事务之间相互读取临时数据

    I: Isolation隔离性,每个事务只能看到事务内的相关数据,别的事务的临时数据在当前事务是看不到的。隔离性要求事务不受其他并发事务的影响,在给定时间内,该事务是数据库运行的唯一事务。

    如果事务没有隔离性,按照不受控制的顺序并发读取和修改数据,想像一下会出现哪些问题?

    一、脏读:一个事务读取了第二个事物未提交的数据,当第二个事务回滚了数据之后,第一个事务就读取到了无效的数据。

    如下图,事务1查询course_id=59的平均分score为9.2,而事务2此时将其平均分修改为9.6,当事务1再次读取的时候,平均分就变成了9.6,此时事务2回滚,事务1就是读取的无效数据,简称脏读。

    二、不可重复读:一个事物前后两次读取的同一数据不一致。

    如下图,事务1查询course_id=59的平均分score为9.6,而事务2此时将其平均分修改为9.7,并将修改提交,当事务1再次读取的时候,平均分就变成了9.7,事务1就是读取的错误数据,注意,不可重复读和脏读的区别就是,脏读的数据会回滚,不可重复读会把数据提交,脏读的数据是无效的,而不可重复读因为事务2的提交,数据是有效的。

    三、幻读:指一个事务两次查询的结果集记录数不一致

    如下图,事务1查询到平均分在9.5到9.8之间的记录数是2条,经过事务2对course_id=43的平均分修改,导致事务1第二次查询的记录数为3条,这种情况就叫幻读,幻读的数据最终也是有效的数据。

    innodb的事务隔离性保证了我们事务操作的安全,才让我们实际操作中并没有出现这么多问题。

    怎么保证隔离性?

    综上所述,我们简单总结下

    脏读:事务 A 读取了事务 B 当前更新的数据,但是事务 B 出现了回滚或未提交修改,事务 A 读到的数据就被称为 “脏数据”。通常情况下,使用 “脏数据” 会造成系统数据不一致,出现错误


    不可重复读:事务 A 在执行过程中多次读取同一数据,但是事务 B 在事务 A 的读取过程中对数据做了多次修改并提交,则会导致事务 A 多次读取的数据不一致,进而无法做出准确性判断


    幻读:事务 A 在执行过程中读取了一些数据,但是事务 B 随即插入了一些数据,那么,事务 A 重新读取时,发现多了一些原本不存在的数据,就像是幻觉一样,称之为幻读


    仔细品味,可以发现,不可重复读与幻读从概念上来说,是非常相似的。区分它们只要记住:不可重复读指的是对原来存在的数据做修改,而幻读指的是新增或者删除数据。

    undo和redo日志中的数据都会被标记属于哪个事务的,所以事务执行过程中就只能读到自己的临时数据了。

    D: Durability持久性,事务一旦提交,结果便是永久性的。即便发生宕机,仍然可依靠事务日志完成数据持久化。

    锁机制: innodb默认是行锁, 但是如果在事务操作的过程中, 没有使用到索引,那么系统会自动全表检索数据, 自动升级为表锁

    行锁: 只有当前行被锁住, 别的用户不能操作

    表锁: 整张表被锁住, 别的用户都不能操作

    11.6 事务的隔离级别

    在某些特定场合,我们又想让事务之间读取到一些临时数据,这就需要修改事务的隔离级别

    设置事务隔离级别的语法如下:

    SET [PERSIST|GLOBAL|SESSION]
        TRANSACTION ISOLATION LEVEL
        {
            READ UNCOMMITTED | READ COMMITTED
            | REPEATABLE READ
            | SERIALIZABLE
        }
    
    -- PERSIST:所有连接到mysql服务的新的连接都有效,并且mysql服务器重启后也不会丢失修改
    -- GLOCAL: 所有连接到mysql服务的新的连接都有效,但是mysql服务器重启后会丢失这个修改
    -- SESSION:开发最常用,只会影响到当前连接,当前连接断开,这个隔离级别的修改就会丢失
    
    -- 开发中也可以用show variables like '%iso%'查看当前session的隔离级别
    -- 因为有一个变量参数名为transaction_isolation

    11.6.1 read uncommitted

    场景一:比如买票的场景,逢年过节都需要买票回家,假如A和B都在买同一辆车的车票,此时还剩最后一张票,A点击购买,但是还没付款提交,因为查看不到事务之间的临时数据,所以B查看时,也还剩一张票,于是B点击购买,立即付款提交,结果A就会购买失败。所以理想的情况应该是,当A点击购买去付款时,B应该看得到这个临时数据,显示没有票才对。这种场景会出现脏读、幻读、不可重复读情况,隔离性最低,并发性最高。

    eg1:查看事务之间能否读取未提交的数据

    START TRANSACTION;
    UPDATE t_emp SET sal=1;

    此时开启事务1并进行更新操作,但是没有commit

    再开启一个事务2

    START TRANSACTION;
    SELECT empno, ename, sal FROM t_emp;
    COMMIT;

    注意:这里没有修改数据,仅仅只是select查询数据,redo日志没有改变,所以不会做同步到文件的操作,commit之后会清空对应的undo日志数据。

    结果显示如下,前者在事务1中修改sal为1,事务2中却看不到。

    如果修改事务2隔离级别,如下

    SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; /*代表可以读取其他事务未提交的数据*/
    START TRANSACTION;
    SELECT empno, ename, sal FROM t_emp;
    COMMIT;

    结果立马就变了,事务2能够读取事务1未提交的数据,但是要注意,因为前者并未commit,所以数据库表文件的数据还没有修改

    11.6.2 read committed

    场景二:银行转账的场景,A事务执行往Scott账户转账1000的操作,B事务执行扣除Scott账户100块的操作,如果A能读取到B事务未提交的数据,那么转账后就会修改为5900,而此时因为各种原因需要回滚支出100元的这个操作,此时账户就只有5900块了,凭空消失100块,所以只有A事务读取到B事务提交后的数据才能保证转账的正确性。这种场景就和买票的场景完全不同。这种场景是会出现幻读和不可重复读的。

    还是eg1的例子,此时修改隔离级别的SQL语句即可

    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;/*只能读取其他事务提交的数据*/
    START TRANSACTION;
    SELECT empno, ename, sal FROM t_emp;
    COMMIT;

    其他事物提交的数据都会同步到数据库表文件中,所以这里就是从数据库表文件中读取的数据。

    11.6.3 repeatable read

    场景三:你在淘宝或者京东等电商,点击购买,选好收货地址之类的之后,点击提交订单,就会让你输入支付密码支付,此时显示的价格是undo日志的价格,如果此时卖家涨价,你购买的还是涨价之前的价格,这种场景就是可重复读。可重复读不会出现脏读、不可重复读的情况,因为事务1读取不到事务2对数据的修改。对于幻读,这里只有靠临键锁才能保证不出现幻读的问题。

    新建一个查询,开启事务1

    SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;/*事务在执行中反复读取数据,得到的结果是一致的*/
    START TRANSACTION;
    SELECT empno, ename, sal FROM t_emp;

    这里一定要先执行一次select语句,保证undo日志拷贝过一次数据

    再新建一个查询,开启事务2

    START TRANSACTION;
    UPDATE t_emp SET sal=1;

    此时数据库表文件的数据如下

    此时在事务1执行SELECT empno, ename, sal FROM t_emp;

    虽然数据库表文件的数据已经修改了,但是事务1处的事务隔离级别是可以反复读,每次都从undo日志里面读取,所以这里还是修改前的价格,直到提交commit,commit之后清空对应的undo日志记录,下次会重新从数据库文件里面拷贝数据,那个时候才是sal=1的数据。

    注意:MySQL默认事务隔离级别就是REPEATABLE READ

    11.6.4 serializable

    由于事务并发执行所带来的各种问题,前三种隔离级别只适用于在某种业务场景中,凡事序列化的隔离性,让事务逐一执行,就不会产生上述问题了。但是序列化的隔离级别使用的特别少,它让事务的并发性大大降低。可重复读不会出现幻读、脏读、不可重复读的情况,因为事务1读取不到事务2对数据的修改。隔离性最高,并发性最低,其实就是没有并发,所有事务按照顺序执行。

    开始事务1,sql语句如下

    START TRANSACTION;
    UPDATE t_emp SET sal=2;

    开始事务2,sql语句如下

    SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;/*事务序列化*/
    START TRANSACTION;
    SELECT empno, ename, sal FROM t_emp;

    但是这行sql之后并没有出结果

    直到你的事务1执行commit之后,事务2就会立即执行查询结果。

    针对这4种隔离级别与脏读幻读不可重复读的关系如下:

    第12章 触发器

    触发器: trigger, 事先为某张表绑定好一段代码 ,当表中的某些内容发生改变的时候(增删改)系统会自动触发代码,执行.

    触发器: 事件类型, 触发时间, 触发对象

    事件类型: 增删改, 三种类型insert,delete和update

    触发时间: 前后: before和after

    触发对象: 表中的每一条记录(行)

    一张表中只能拥有一种触发时间的一种类型的触发器: 最多一张表能有6个触发器

    12.1 创建触发器

    在mysql高级结构中: 没有大括号,  都是用对应的字符符号代替

    触发器基本语法

    -- 临时修改语句结束符

    Delimiter 自定义符号: 后续代码中只有碰到自定义符号才算结束

    Create trigger 触发器名字 触发时间 事件类型 on 表名 for each row

    Begin -- 代表左大括号: 开始

    -- 里面就是触发器的内容: 每行内容都必须使用语句结束符: 分号

    End -- 代表右带括号: 结束

    -- 语句结束符

    自定义符号

    -- 将临时修改修正过来

    Delimiter  ;

    12.2 查看触发器

    查看所有触发器或者模糊匹配

    Show triggers [like ‘pattern’];

    \g 的作用是分号和在sql语句中写’;’是等效的

    \G 的作用是将查到的结构旋转90度变成纵向

    可以查看触发器创建语句

    Show create trigger 触发器名字;

    所有的触发器都会保存一张表中: Information_schema.triggers

    12.3 使用触发器

    触发器: 不需要手动调用, 而是当某种情况发生时会自动触发.(订单里面插入记录之后)

    12.4 修改触发器&删除触发器

    触发器不能修改,只能先删除,后新增.

    Drop trigger 触发器名字;

    12.5 触发器记录

    触发器记录: 不管触发器是否触发了,只要当某种操作准备执行, 系统就会将当前要操作的记录的当前状态和即将执行之后新的状态给分别保留下来, 供触发器使用: 其中, 要操作的当前状态保存到old中, 操作之后的可能形态保存给new.

    Old代表的是旧记录,new代表的是新记录

    删除的时候是没有new的; 插入的时候是没有old

    Old和new都是代表记录本身: 任何一条记录除了有数据, 还有字段名字.

    使用方式: old.字段名 / new.字段名(new代表的是假设发生之后的结果)

    查看触发器的效果

    如果触发器内部只有一条要执行的SQL指令, 可以省略大括号(begin和end)

    Create trigger 触发器名字 触发时间 事件类型 on 表名 for each row

    一条SQL指令;

    触发器: 可以很好的协调表内部的数据处理顺序和关系. 但是从JAVA角度出发, 触发器会增加数据库维护的难度, 所以较少使用触发器.

    第13章 函数

    13.1 数字函数

    eg:求四舍五入

    select round(4.6288*100)/100;

    13.2 日期函数

    13.2.1 获取系统时间函数

    NOW()函数能获得系统日期和时间,格式yyyy-MM-dd hh:mm:ss,数据库的最小时间单位是秒s,而不是毫秒ms
    CURDATE()函数能获得当前系统日期,格式yyyy-MM-dd
    CURTIME()函数能获得当前系统时间,格式hh:mm:ss

    SELECT NOW(), CURDATE(), CURTIME();

    13.2.2 日期格式化函数

    DATE_FORMAT(日期,  表达式)

    该函数用于格式化日期,返回用户想要的日期格式

    eg:比如查看员工入职的年份

    SELECT ename, DATE_FORMAT(hiredate,"%Y") AS result FROM t_emp;

    占位符说明

    eg:查询某个日期是星期几

    SELECT DATE_FORMAT("2021-1-1","%w");

    结果是星期5,如果是大写%W,那么就输出英文Friday

    eg:利用日期函数,查询1981年上半年入职的员工有多少个

    SELECT COUNT(*) FROM t_emp
    WHERE DATE_FORMAT(hiredate,"%Y")=1981
    AND DATE_FORMAT(hiredate,"%m")<=6;

    练习题

     

    答案选A,语法基础。

    13.2.3 日期偏移计算

    注意:MySQL数据库里面,两个日期不能直接加减,日期也不能与数字加减
    比如 select hiredate+1 from t_emp;
    其实hiredate是"1980-12-18"变成了19801218,然后+1,结果是19801219

    DATE_ADD(日期, INTERVAL 偏移量  偏移的时间单位)

    该函数可以实现日期的偏移计算,而且时间单位很灵活

    举几个例子

    /*100天之后是什么时间*/
    SELECT DATE_ADD(NOW(), INTERVAL 100 DAY);
    /*300分钟之前是什么时间*/
    SELECT DATE_ADD(NOW(), INTERVAL -300 MINUTE);
    /*6个月零3天之前是什么时间*/
    SELECT DATE_ADD(DATE_ADD(NOW(),INTERVAL -6 MONTH),INTERVAL -3 DAY)

    把日期偏移函数和日期格式化函数混合用一下

    eg:6个月零3天之前是什么时间,保留年月日即可

    SELECT 
    DATE_FORMAT(DATE_ADD(DATE_ADD(NOW(),INTERVAL -6 MONTH), INTERVAL -3 DAY), "%Y/%m/%d");

     

    13.2.4 计算日期之间相隔的天数

    DATEDIFF(日期1, 日期2)

    该函数用来计算两个日期之间相差的天数为日期1-日期2。

    eg:比如计算现在和2019-1-1相差多少天

    SELECT DATEDIFF(NOW(),"2019-1-1");

     

    2019-1-1已经是707天之前了。

    13.3 字符函数

    eg:查询员工表中姓名小写、姓名大写、姓名包含的字符数、底薪末尾添加$,姓名包含有A

    SELECT
    	LOWER(ename), UPPER(ename), LENGTH(ename),
    	CONCAT(sal,"$"),INSTR(ename,"A")
    FROM t_emp;

    这里对于汉字,LOWER和UPPER函数是没有转换作用的,对于LENGTH函数,因为这里的数据库编码是UTF8字符集,所以一个汉字占3个字节,长度为6,INSTR函数会返回首次出现A的位置,从1开始,如果没有包含A,则返回0。

    INSERT例子

    /*插入"先生"并替换从1开始的3个字符*/
    SELECT INSERT("女士早上好", 1, 3, "先生");

     

    REPLACE例子

    SELECT REPLACE("女士早上好","女士","先生");

     

    SUBSTR、SUBSTRING、LPAD、TRIM例子

    SELECT SUBSTR("你好世界", 3, 4), SUBSTRING("你好世界", 3, 2),
    LPAD(SUBSTRING("13312345678", 8, 4),11,"*"),
    TRIM("                Hello World    ");

    说明:SUBSTR("你好世界", 3, 4)表示获取从1开始下标为[3,4]闭区间位置子串,SUBSTRING("13312345678", 8, 4)表示获取从下标8开始后面的4个字符,LPAD(SUBSTRING("13312345678", 8, 4),11,"*")表示子串将由"*"左填充到11个字符的长度,TRIM就是去除首尾空格。

    练习题

    答案选C,A项错在直接把最后4位也用*替代了,B错在substring下标从1开始,D错在是rpad而不是lpad。

    13.4 条件函数

    13.4.1 简单条件判断

    SQL语句可以利用条件函数来实现编程语言里的条件判断

    IFNULL(表达式, 值)

    IF(表达式, 值1, 值2)

    eg:SALES部门发放礼品A,其余部门发放礼品B,打印每名员工获得的礼品

    SELECT
    	e.empno, e.ename, d.dname,
    	IF(d.dname="SALES","礼品A","礼品B")
    FROM t_emp e JOIN t_dept d ON e.deptno=d.deptno;

    练习题

    答案选D,A错在as写成逗号,B错在函数用错,if也是3个参数,C错在入学日期和系号之间没有逗号分隔。

    答案选A,B错在函数错用ifnull,并且ifnull也是2个参数而不是3个,C错在if函数里面的相框参数填写反了,D错在根们没有打印相框类型。

    13.4.2 复杂条件判断

    复杂的条件判断可以用条件语句来实现,比IF语句功能更强大

    CASE
        WHEN 表达式 THEN 值1
        WHEN 表达式 THEN 值2
        ...
        ELSE 值N
    END

     

    eg:公司集体旅游,每个部门目的地不同,SALES部门去P1地点,ACCOUNTING部门去P2地点,RESEARCH部门去P3地点,查询每名员工的旅行地点。

    SELECT
    	e.empno, e.ename,
    	CASE
    		WHEN d.dname="SALES" THEN "p1"
    		WHEN d.dname="ACCOUNTING" THEN "p2"
    		WHEN d.dname="RESEARCH" THEN "P3"
    		END AS place
    FROM t_emp e JOIN t_dept d ON e.deptno=d.deptno;

    eg:公司调整员工基本工资,具体方案如下:

    1.SALES部门中工龄超过20年,涨幅10%
    2.SALES部门中工龄不满20年,涨幅5%
    3.ACCOUNTING部门,涨幅300
    4.RESEARCH部门里低于部门平均底薪,涨幅200
    5.没有部门的员工,涨幅100

    UPDATE t_emp e LEFT JOIN t_dept d ON e.deptno=d.deptno
    LEFT JOIN (SELECT deptno, AVG(sal) avg FROM t_emp GROUP BY deptno) t
    ON e.deptno=d.deptno
    SET sal=(
    	CASE
    		WHEN d.dname="SALES" AND DATEDIFF(NOW(),e.hiredate)/365>=20
    		THEN e.sal*1.1
    		WHEN d.dname="SALES" AND DATEDIFF(NOW(),e.hiredate)/365<20
    		THEN e.sal*1.05
    		WHEN d.dname="ACCOUNTING" THEN e.sal+300
    		WHEN d.dname="RESEARCH" AND e.sal<t.avg THEN e.sal+200
    		WHEN e.deptno IS NULL THEN e.sal+100
    		ELSE e.sal
    	END
    );
    

    13.5 自定义函数

    函数要素: 函数名, 参数列表(形参和实参), 返回值, 函数体(作用域)

    13.5.1 创建函数

    创建语法

    Create function  函数名([形参列表]) returns 数据类型 -- 规定要返回的数据类型

    Begin

    -- 函数体

    -- 返回值: return 类型(指定数据类型);

    End

    定义函数

    自定义函数与系统函数的调用方式是一样: select 函数名([实参列表]);

    13.5.2 查看函数

    查看所有函数: show function status [like ‘pattern’];

    查看函数的创建语句: show create function 函数名;

    13.5.3 修改函数&删除函数

    函数只能先删除后新增,不能修改.

    Drop function 函数名;

    13.5.4 函数参数

    参数分为两种: 定义时的参数叫形参, 调用时的参数叫实参(实参可以是数值也可以是变量)

    形参: 要求必须指定数据类型

    Function 函数名(形参名字 字段类型) returns 数据类型

    在函数内部使用@定义的变量在函数外部也可以访问

    13.5.5 作用域

    Mysql中的作用域与js中的作用域完全一样

    全局变量可以在任何地方使用; 局部变量只能在函数内部使用.

    全局变量: 使用set关键字定义, 使用@符号标志

    局部变量: 使用declare关键字声明, 没有@符号: 所有的局部变量的声明,必须在函数体开始之前

    第14章 存储过程

    存储过程简称过程,procedure, 是一种用来处理数据的方式.

    存储过程是一种没有返回值的函数.

    14.1 创建过程

    Create procedure 过程名字([参数列表])

    Begin

    -- 过程体

    End

    14.2 查看过程

    函数的查看方式完全适用于过程: 关键字换成procedure

    查看所有过程: show procedure status [like ‘pattern’];

    查看过程创建语句: show create procedure 过程名;

    14.3 调用过程

    过程没有返回值: select是不能访问的.

    过程有一个专门的调用关键字: call

    14.4 修改过程&删除过程

    过程只能先删除,后新增

    Drop procedure 过程名;

    /* 这是我某次模拟插入10W条数据的过程代码,仅供参考 */
    
    create PROCEDURE p1()
    BEGIN
    DECLARE i int;
    set i = 1;
    
    WHILE i <= 100000 DO
    
    	INSERT INTO `demo_info`(key1, key2, key3, key_part1, key_part2, key_part3, common_field) VALUES ( 'a', i, 'a', '1可', 'b', 'a', '123是');
    	
    	set i = i + 1;
    
    END WHILE;
    
    END;
    
    DROP PROCEDURE p1;
    
    start TRANSACTION;
    CALL p1();
    COMMIT;

    14.5 过程参数

    函数的参数需要数据类型指定, 过程比函数更严格.

    过程还有自己的类型限定: 三种类型

    In: 数据只是从外部传入给内部使用(值传递): 可以是数值也可以是变量

    Out: 只允许过程内部使用(不用外部数据), 给外部使用的.(引用传递: 外部的数据会被先清空才会进入到内部): 只能是变量

    Inout: 外部可以在内部使用,内部修改也可以给外部使用: 典型的引用传递: 只能传变量

    基本使用

    Create procedure 过程名(in 形参名字 数据类型, out 形参名字 数据类型, inout 形参名字 数据类型)

    调用: out和inout类型的参数必须传入变量,而不能是数值

    正确调用: 传入变量

    存储过程对于变量的操作(返回)是滞后的: 是在存储过程调用结束的时候,才会重新将内部修改的值赋值给外部传入的全局变量.

    测试: 传入数据1,2,3: 说明局部变量与全局变量无关

    最后: 在存储过程调用结束之后, 系统会将局部变量重复返回给全局变量(out和inout)

    关注、留言,我们一起学习。

    ===============Talk is cheap, show me the code================

    展开全文
  • 数据库原理

    2018-05-06 01:45:53
    一提到关系型数据库,我禁不住想:有些东西被忽视了。关系型数据库无处不在,而且种类繁多,从小巧实用的 SQLite 到强大的 Teradata 。但很少有文章讲解数据库是如何工作的。你可以自己谷歌/百度一下『关系型数据库...

    一提到关系型数据库,我禁不住想:有些东西被忽视了。关系型数据库无处不在,而且种类繁多,从小巧实用的 SQLite 到强大的 Teradata 。但很少有文章讲解数据库是如何工作的。你可以自己谷歌/百度一下『关系型数据库原理』,看看结果多么的稀少【译者注:百度为您找到相关结果约1,850,000个…】 ,而且找到的那些文章都很短。现在如果你查找最近时髦的技术(大数据、NoSQL或JavaScript),你能找到更多深入探讨它们如何工作的文章。

    难道关系型数据库已经太古老太无趣,除了大学教材、研究文献和书籍以外,没人愿意讲了吗?

    作为一个开发人员,我不喜欢用我不明白的东西。而且,数据库已经使用了40年之久,一定有理由的。多年以来,我花了成百上千个小时来真正领会这些我每天都在用的、古怪的黑盒子。关系型数据库非常有趣,因为它们是基于实用而且可复用的概念。如果你对了解一个数据库感兴趣,但是从未有时间或意愿来刻苦钻研这个内容广泛的课题,你应该喜欢这篇文章。

    虽然本文标题很明确,但我的目的并不是讲如何使用数据库。因此,你应该已经掌握怎么写一个简单的 join query(联接查询)和CRUD操作(创建读取更新删除),否则你可能无法理解本文。这是唯一需要你了解的,其他的由我来讲解。

    我会从一些计算机科学方面的知识谈起,比如时间复杂度。我知道有些人讨厌这个概念,但是没有它你就不能理解数据库内部的巧妙之处。由于这是个很大的话题,我将集中探讨我认为必要的内容:数据库处理SQL查询的方式。我仅仅介绍数据库背后的基本概念,以便在读完本文后你会对底层到底发生了什么有个很好的了解

    【译者注:关于时间复杂度。计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。如果不了解这个概念建议先看看维基百度百科,对于理解文章下面的内容很有帮助】

    由于本文是个长篇技术文章,涉及到很多算法和数据结构知识,你尽可以慢慢读。有些概念比较难懂,你可以跳过,不影响理解整体内容。

    这篇文章大约分为3个部分:

    • 底层和上层数据库组件概况
    • 查询优化过程概况
    • 事务和缓冲池管理概况

    回到基础

    很久很久以前(在一个遥远而又遥远的星系……),开发者必须确切地知道他们的代码需要多少次运算。他们把算法和数据结构牢记于心,因为他们的计算机运行缓慢,无法承受对CPU和内存的浪费。

    在这一部分,我将提醒大家一些这类的概念,因为它们对理解数据库至关重要。我还会介绍数据库索引的概念。

    O(1) vs O(n^2)

    现今很多开发者不关心时间复杂度……他们是对的。

    但是当你应对大量的数据(我说的可不只是成千上万哈)或者你要争取毫秒级操作,那么理解这个概念就很关键了。而且你猜怎么着,数据库要同时处理这两种情景!我不会占用你太长时间,只要你能明白这一点就够了。这个概念在下文会帮助我们理解什么是基于成本的优化

    概念

    时间复杂度用来检验某个算法处理一定量的数据要花多长时间。为了描述这个复杂度,计算机科学家使用数学上的『简明解释算法中的大O符号』。这个表示法用一个函数来描述算法处理给定的数据需要多少次运算。

    比如,当我说『这个算法是适用 O(某函数())』,我的意思是对于某些数据,这个算法需要 某函数(数据量) 次运算来完成。

    重要的不是数据量,而是当数据量增加时运算如何增加。时间复杂度不会给出确切的运算次数,但是给出的是一种理念。

    图中可以看到不同类型的复杂度的演变过程,我用了对数尺来建这个图。具体点儿说,数据量以很快的速度从1条增长到10亿条。我们可得到如下结论:

    • 绿:O(1)或者叫常数阶复杂度,保持为常数(要不人家就不会叫常数阶复杂度了)。
    • 红:O(log(n))对数阶复杂度,即使在十亿级数据量时也很低。
    • 粉:最糟糕的复杂度是 O(n^2),平方阶复杂度,运算数快速膨胀。
    • 黑和蓝:另外两种复杂度(的运算数也是)快速增长。

    例子

    数据量低时,O(1) 和 O(n^2)的区别可以忽略不计。比如,你有个算法要处理2000条元素。

    • O(1) 算法会消耗 1 次运算
    • O(log(n)) 算法会消耗 7 次运算
    • O(n) 算法会消耗 2000 次运算
    • O(n*log(n)) 算法会消耗 14,000 次运算
    • O(n^2) 算法会消耗 4,000,000 次运算

    O(1) 和 O(n^2) 的区别似乎很大(4百万),但你最多损失 2 毫秒,只是一眨眼的功夫。确实,当今处理器每秒可处理上亿次的运算。这就是为什么性能和优化在很多IT项目中不是问题。

    我说过,面临海量数据的时候,了解这个概念依然很重要。如果这一次算法需要处理 1,000,000 条元素(这对数据库来说也不算大)。

    • O(1) 算法会消耗 1 次运算
    • O(log(n)) 算法会消耗 14 次运算
    • O(n) 算法会消耗 1,000,000 次运算
    • O(n*log(n)) 算法会消耗 14,000,000 次运算
    • O(n^2) 算法会消耗 1,000,000,000,000 次运算

    我没有具体算过,但我要说,用O(n^2) 算法的话你有时间喝杯咖啡(甚至再续一杯!)。如果在数据量后面加个0,那你就可以去睡大觉了。

    继续深入

    为了让你能明白

    • 搜索一个好的哈希表会得到 O(1) 复杂度
      • 搜索一个均衡的树会得到 O(log(n)) 复杂度
      • 搜索一个阵列会得到 O(n) 复杂度
      • 最好的排序算法具有 O(n*log(n)) 复杂度
      • 糟糕的排序算法具有 O(n^2) 复杂度

    注:在接下来的部分,我们将会研究这些算法和数据结构。

    有多种类型的时间复杂度

    • 一般情况场景
    • 最佳情况场景
    • 最差情况场景

    时间复杂度经常处于最差情况场景。

    这里我只探讨时间复杂度,但复杂度还包括:

    • 算法的内存消耗
    • 算法的磁盘 I/O 消耗

    当然还有比 n^2 更糟糕的复杂度,比如:

    • n^4:差劲!我将要提到的一些算法具备这种复杂度。
    • 3^n:更差劲!本文中间部分研究的一些算法中有一个具备这种复杂度(而且在很多数据库中还真的使用了)。
    • 阶乘 n:你永远得不到结果,即便在少量数据的情况下。
    • n^n:如果你发展到这种复杂度了,那你应该问问自己IT是不是你的菜。

    注:我并没有给出『大O表示法』的真正定义,只是利用这个概念。可以看看维基百科上的这篇文章

    合并排序

    当你要对一个集合排序时你怎么做?什么?调用 sort() 函数……好吧,算你对了……但是对于数据库,你需要理解这个 sort() 函数的工作原理。

    优秀的排序算法有好几个,我侧重于最重要的一种:合并排序。你现在可能还不了解数据排序有什么用,但看完查询优化部分后你就会知道了。再者,合并排序有助于我们以后理解数据库常见的联接操作,即合并联接 。

    合并

    与很多有用的算法类似,合并排序基于这样一个技巧:将 2 个大小为 N/2 的已排序序列合并为一个 N 元素已排序序列仅需要 N 次操作。这个方法叫做合并

    我们用个简单的例子来看看这是什么意思:

    通过此图你可以看到,在 2 个 4元素序列里你只需要迭代一次,就能构建最终的8元素已排序序列,因为两个4元素序列已经排好序了:

    • 1) 在两个序列中,比较当前元素(当前=头一次出现的第一个)
    • 2) 然后取出最小的元素放进8元素序列中
    • 3) 找到(两个)序列的下一个元素,(比较后)取出最小的
    • 重复1、2、3步骤,直到其中一个序列中的最后一个元素
    • 然后取出另一个序列剩余的元素放入8元素序列中。

    这个方法之所以有效,是因为两个4元素序列都已经排好序,你不需要再『回到』序列中查找比较。

    【译者注:合并排序详细原理,其中一个动图(原图较长,我做了删减)清晰的演示了上述合并排序的过程,而原文的叙述似乎没有这么清晰,不动戳大。】

    既然我们明白了这个技巧,下面就是我的合并排序伪代码。

    合并排序是把问题拆分为小问题,通过解决小问题来解决最初的问题(注:这种算法叫分治法,即『分而治之、各个击破』)。如果你不懂,不用担心,我第一次接触时也不懂。如果能帮助你理解的话,我认为这个算法是个两步算法:

    • 拆分阶段,将序列分为更小的序列
    • 排序阶段,把小的序列合在一起(使用合并算法)来构成更大的序列

    拆分阶段

    在拆分阶段过程中,使用3个步骤将序列分为一元序列。步骤数量的值是 log(N) (因为 N=8, log(N)=3)。【译者注:底数为2,下文有说明】

    我怎么知道这个的?

    我是天才!一句话:数学。道理是每一步都把原序列的长度除以2,步骤数就是你能把原序列长度除以2的次数。这正好是对数的定义(在底数为2时)。

    排序阶段

    在排序阶段,你从一元序列开始。在每一个步骤中,你应用多次合并操作,成本一共是 N=8 次运算。

    • 第一步,4 次合并,每次成本是 2 次运算。
    • 第二步,2 次合并,每次成本是 4 次运算。
    • 第三步,1 次合并,成本是 8 次运算。

    因为有 log(N) 个步骤,整体成本是 N*log(N) 次运算

    【译者注:这个完整的动图演示了拆分和排序的全过程,不动戳大。】

    合并排序的强大之处

    为什么这个算法如此强大?

    因为:

    • 你可以更改算法,以便于节省内存空间,方法是不创建新的序列而是直接修改输入序列。

    注:这种算法叫『原地算法』(in-place algorithm)

    • 你可以更改算法,以便于同时使用磁盘空间和少量内存而避免巨量磁盘 I/O。方法是只向内存中加载当前处理的部分。在仅仅100MB的内存缓冲区内排序一个几个GB的表时,这是个很重要的技巧。

    注:这种算法叫『外部排序』(external sorting)。

    • 你可以更改算法,以便于在 多处理器/多线程/多服务器 上运行。

    比如,分布式合并排序是Hadoop(那个著名的大数据框架)的关键组件之一。

    • 这个算法可以点石成金(事实如此!)

    这个排序算法在大多数(如果不是全部的话)数据库中使用,但是它并不是唯一算法。如果你想多了解一些,你可以看看 这篇论文,探讨的是数据库中常用排序算法的优势和劣势。

    阵列,树和哈希表

    既然我们已经了解了时间复杂度和排序背后的理念,我必须要向你介绍3种数据结构了。这个很重要,因为它们是现代数据库的支柱。我还会介绍数据库索引的概念。

    阵列

    二维阵列是最简单的数据结构。一个表可以看作是个阵列,比如:

    这个二维阵列是带有行与列的表:

    • 每个行代表一个主体
    • 列用来描述主体的特征
    • 每个列保存某一种类型对数据(整数、字符串、日期……)

    虽然用这个方法保存和视觉化数据很棒,但是当你要查找特定的值它就很糟糕了。 举个例子,如果你要找到所有在 UK 工作的人,你必须查看每一行以判断该行是否属于 UK 。这会造成 N 次运算的成本(N 等于行数),还不赖嘛,但是有没有更快的方法呢?这时候树就可以登场了(或开始起作用了)。

    树和数据库索引

    二叉查找树是带有特殊属性的二叉树,每个节点的关键字必须:

    • 比保存在左子树的任何键值都要大
    • 比保存在右子树的任何键值都要小

    【译者注:binary search tree,二叉查找树/二叉搜索树,或称 Binary Sort Tree 二叉排序树。见百度百科 】

    概念




    这个树有 N=15 个元素。比方说我要找208:

    • 我从键值为 136 的根开始,因为 136<208,我去找节点136的右子树。
    • 398>208,所以我去找节点398的左子树
    • 250>208,所以我去找节点250的左子树
    • 200<208,所以我去找节点200的右子树。但是 200 没有右子树,值不存在(因为如果存在,它会在 200 的右子树)

    现在比方说我要找40

    • 我从键值为136的根开始,因为 136>40,所以我去找节点136的左子树。
    • 80>40,所以我去找节点 80 的左子树
    • 40=40,节点存在。我抽取出节点内部行的ID(图中没有画)再去表中查找对应的 ROW ID。
    • 知道 ROW ID我就知道了数据在表中对精确位置,就可以立即获取数据。

    最后,两次查询的成本就是树内部的层数。如果你仔细阅读了合并排序的部分,你就应该明白一共有 log(N)层。所以这个查询的成本是 log(N),不错啊!

    回到我们的问题

    上文说的很抽象,我们回来看看我们的问题。这次不用傻傻的数字了,想象一下前表中代表某人的国家的字符串。假设你有个树包含表中的列『country』:

    • 如果你想知道谁在 UK 工作
    • 你在树中查找代表 UK 的节点
    • 在『UK 节点』你会找到 UK 员工那些行的位置

    这次搜索只需 log(N) 次运算,而如果你直接使用阵列则需要 N 次运算。你刚刚想象的就是一个数据库索引

    B+树索引

    查找一个特定值这个树挺好用,但是当你需要查找两个值之间的多个元素时,就会有麻烦了。你的成本将是 O(N),因为你必须查找树的每一个节点,以判断它是否处于那 2 个值之间(例如,对树使用中序遍历)。而且这个操作不是磁盘I/O有利的,因为你必须读取整个树。我们需要找到高效的范围查询方法。为了解决这个问题,现代数据库使用了一种修订版的树,叫做B+树。在一个B+树里:

    • 只有最底层的节点(叶子节点)才保存信息(相关表的行位置)
    • 其它节点只是在搜索中用来指引到正确节点的。

    【译者注:参考 B+树 , 二叉树遍历    维基百科


    你可以看到,节点更多了(多了两倍)。确实,你有了额外的节点,它们就是帮助你找到正确节点的『决策节点』(正确节点保存着相关表中行的位置)。但是搜索复杂度还是在 O(log(N))(只多了一层)。一个重要的不同点是,最底层的节点是跟后续节点相连接的。

    用这个 B+树,假设你要找40到100间的值:

    • 你只需要找 40(若40不存在则找40之后最贴近的值),就像你在上一个树中所做的那样。
    • 然后用那些连接来收集40的后续节点,直到找到100。

    比方说你找到了 M 个后续节点,树总共有 N 个节点。对指定节点的搜索成本是 log(N),跟上一个树相同。但是当你找到这个节点,你得通过后续节点的连接得到 M 个后续节点,这需要 M 次运算。那么这次搜索只消耗了 M+log(N) 次运算,区别于上一个树所用的 N 次运算。此外,你不需要读取整个树(仅需要读 M+log(N) 个节点),这意味着更少的磁盘访问。如果 M 很小(比如 200 行)并且 N 很大(1,000,000),那结果就是天壤之别了。

    然而还有新的问题(又来了!)。如果你在数据库中增加或删除一行(从而在相关的 B+树索引里):

    • 你必须在B+树中的节点之间保持顺序,否则节点会变得一团糟,你无法从中找到想要的节点。
    • 你必须尽可能降低B+树的层数,否则 O(log(N)) 复杂度会变成 O(N)。

    换句话说,B+树需要自我整理和自我平衡。谢天谢地,我们有智能删除和插入。但是这样也带来了成本:在B+树中,插入和删除操作是 O(log(N)) 复杂度。所以有些人听到过使用太多索引不是个好主意这类说法。没错,你减慢了快速插入/更新/删除表中的一个行的操作,因为数据库需要以代价高昂的每索引 O(log(N)) 运算来更新表的索引。再者,增加索引意味着给事务管理器带来更多的工作负荷(在本文结尾我们会探讨这个管理器)。

    想了解更多细节,你可以看看 Wikipedia 上这篇关于B+树的文章。如果你想要数据库中实现B+树的例子,看看MySQL核心开发人员写的这篇文章 和 这篇文章。两篇文章都致力于探讨 innoDB(MySQL引擎)如何处理索引。

    哈希表

    我们最后一个重要的数据结构是哈希表。当你想快速查找值时,哈希表是非常有用的。而且,理解哈希表会帮助我们接下来理解一个数据库常见的联接操作,叫做『哈希联接』。这个数据结构也被数据库用来保存一些内部的东西(比如锁表或者缓冲池,我们在下文会研究这两个概念)。

    哈希表这种数据结构可以用关键字来快速找到一个元素。为了构建一个哈希表,你需要定义:

    • 元素的关键字
      • 关键字的哈希函数。关键字计算出来的哈希值给出了元素的位置(叫做哈希桶)。
      • 关键字比较函数。一旦你找到正确的哈希桶,你必须用比较函数在桶内找到你要的元素。
    一个简单的例子

    我们来看一个形象化的例子:

    这个哈希表有10个哈希桶。因为我懒,我只给出5个桶,但是我知道你很聪明,所以我让你想象其它的5个桶。我用的哈希函数是关键字对10取模,也就是我只保留元素关键字的最后一位,用来查找它的哈希桶:

    • 如果元素最后一位是 0,则进入哈希桶0,
    • 如果元素最后一位是 1,则进入哈希桶1,
    • 如果元素最后一位是 2,则进入哈希桶2,
    • …我用的比较函数只是判断两个整数是否相等。

    【译者注:取模运算

    比方说你要找元素 78:

    • 哈希表计算 78 的哈希码,等于 8。
    • 查找哈希桶 8,找到的第一个元素是 78。
    • 返回元素 78。
    • 查询仅耗费了 2 次运算(1次计算哈希值,另一次在哈希桶中查找元素)。

    现在,比方说你要找元素 59:

    • 哈希表计算 59 的哈希码,等于9。
    • 查找哈希桶 9,第一个找到的元素是 99。因为 99 不等于 59, 那么 99 不是正确的元素。
    • 用同样的逻辑,查找第二个元素(9),第三个(79),……,最后一个(29)。
    • 元素不存在。
    • 搜索耗费了 7 次运算
    一个好的哈希函数

    你可以看到,根据你查找的值,成本并不相同。

    如果我把哈希函数改为关键字对 1,000,000 取模(就是说取后6位数字),第二次搜索只消耗一次运算,因为哈希桶 00059 里面没有元素。真正的挑战是找到好的哈希函数,让哈希桶里包含非常少的元素

    在我的例子里,找到一个好的哈希函数很容易,但这是个简单的例子。当关键字是下列形式时,好的哈希函数就更难找了:

    • 1 个字符串(比如一个人的姓)
    • 2 个字符串(比如一个人的姓和名)
    • 2 个字符串和一个日期(比如一个人的姓、名和出生年月日)

    如果有了好的哈希函数,在哈希表里搜索的时间复杂度是 O(1)。

    阵列 vs 哈希表

    为什么不用阵列呢?

    嗯,你问得好。

    • 一个哈希表可以只装载一半到内存,剩下的哈希桶可以留在硬盘上。
    • 用阵列的话,你需要一个连续内存空间。如果你加载一个大表,很难分配足够的连续内存空间
    • 用哈希表的话,你可以选择你要的关键字(比如,一个人的国家和姓氏)。

    想要更详细的信息,你可以阅读我在Java HashMap 上的文章,是关于高效哈希表实现的。你不需要了解Java就能理解文章里的概念。

    全局概览

    我们已经了解了数据库内部的基本组件,现在我们需要回来看看数据库的全貌了。

    数据库是一个易于访问和修改的信息集合。不过简单的一堆文件也能达到这个效果。事实上,像SQLite这样最简单的数据库也只是一堆文件而已,但SQLite是精心设计的一堆文件,因为它允许你:

    • 使用事务来确保数据的安全和一致性
    • 快速处理百万条以上的数据

    数据库一般可以用如下图形来理解:

    撰写这部分之前,我读过很多书/论文,它们都以自己的方式描述数据库。所以,我不会特别关注如何组织数据库或者如何命名各种进程,因为我选择了自己的方式来描述这些概念以适应本文。区别就是不同的组件,总体思路为:数据库是由多种互相交互的组件构成的

    核心组件

    • 进程管理器(process manager:很多数据库具备一个需要妥善管理的进程/线程池。再者,为了实现纳秒级操作,一些现代数据库使用自己的线程而不是操作系统线程。
    • 网络管理器(network manager:网路I/O是个大问题,尤其是对于分布式数据库。所以一些数据库具备自己的网络管理器。
    • 文件系统管理器(File system manager磁盘I/O是数据库的首要瓶颈。具备一个文件系统管理器来完美地处理OS文件系统甚至取代OS文件系统,是非常重要的。
    • 内存管理器(memory manager:为了避免磁盘I/O带来的性能损失,需要大量的内存。但是如果你要处理大容量内存你需要高效的内存管理器,尤其是你有很多查询同时使用内存的时候。
    • 安全管理器(Security Manager:用于对用户的验证和授权。
    • 客户端管理器(Client manager:用于管理客户端连接。
    • ……

    工具

    • 备份管理器(Backup manager:用于保存和恢复数据。
    • 复原管理器(Recovery manager:用于崩溃后重启数据库到一个一致状态
    • 监控管理器(Monitor manager:用于记录数据库活动信息和提供监控数据库的工具。
    • Administration管理器(Administration manager:用于保存元数据(比如表的名称和结构),提供管理数据库、模式、表空间的工具。【译者注:好吧,我真的不知道Administration manager该翻译成什么,有知道的麻烦告知,不胜感激……】
    • ……

    查询管理器

    • 查询解析器(Query parser):用于检查查询是否合法
    • 查询重写器(Query rewriter):用于预优化查询
    • 查询优化器(Query optimizer):用于优化查询
    • 查询执行器(Query executor):用于编译和执行查询

    数据管理器

    • 事务管理器(Transaction manager:用于处理事务
    • 缓存管理器Cache manager:数据被使用之前置于内存,或者数据写入磁盘之前置于内存
    • 数据访问管理器Data access manager:访问磁盘中的数据

    在本文剩余部分,我会集中探讨数据库如何通过如下进程管理SQL查询的:

    • 客户端管理器
    • 查询管理器
    • 数据管理器(含复原管理器)

    客户端管理器

    客户端管理器是处理客户端通信的。客户端可以是一个(网站)服务器或者一个最终用户或最终应用。客户端管理器通过一系列知名的API(JDBC, ODBC, OLE-DB …)提供不同的方式来访问数据库。

    客户端管理器也提供专有的数据库访问API。

    当你连接到数据库时:

    • 管理器首先检查你的验证信息(用户名和密码),然后检查你是否有访问数据库的授权。这些权限由DBA分配。
    • 然后,管理器检查是否有空闲进程(或线程)来处理你对查询。
    • 管理器还会检查数据库是否负载很重。
    • 管理器可能会等待一会儿来获取需要的资源。如果等待时间达到超时时间,它会关闭连接并给出一个可读的错误信息。
    • 然后管理器会把你的查询送给查询管理器来处理。
    • 因为查询处理进程不是『不全则无』的,一旦它从查询管理器得到数据,它会把部分结果保存到一个缓冲区并且开始给你发送
    • 如果遇到问题,管理器关闭连接,向你发送可读的解释信息,然后释放资源。

    查询管理器

    这部分是数据库的威力所在,在这部分里,一个写得糟糕的查询可以转换成一个快速执行的代码,代码执行的结果被送到客户端管理器。这个多步骤操作过程如下:

    • 查询首先被解析并判断是否合法
    • 然后被重写,去除了无用的操作并且加入预优化部分
    • 接着被优化以便提升性能,并被转换为可执行代码和数据访问计划。
    • 然后计划被编译
    • 最后,被执行

    这里我不会过多探讨最后两步,因为它们不太重要。

    看完这部分后,如果你需要更深入的知识,我建议你阅读:

    • 关于成本优化的初步研究论文(1979):关系型数据库系统存取路径选择。这个篇文章只有12页,而且具备计算机一般水平就能理解。
    • 非常好、非常深入的 DB2 9.X 如何优化查询的介绍
    • 非常好的PostgreSQL如何优化查询的介绍。这是一篇最通俗易懂的文档,因为它讲的是『我们来看看在这种情况下,PostgreSQL给出了什么样的查询计划』,而不是『我们来看看PostgreSQL用的什么算法』。
    • 官方SQLite优化文档。『易于』阅读,因为SQLite用的是简单规则。再者,这是唯一真正解释SQLite如何工作的官方文档。
    • 非常好的SQL Server 2005 如何优化查询的介绍
    • Oracle 12c 优化白皮书
    • 2篇查询优化的教程,第一篇 第二篇。教程来自《数据库系统概念》的作者,很好的读物,集中讨论磁盘I/O,但是要求具有很好的计算机科学水平。
    • 另一个原理教程,这篇教程我觉得更易懂,不过它仅关注联接运算符(join operators)和磁盘I/O。

    查询解析器

    每一条SQL语句都要送到解析器来检查语法,如果你的查询有错,解析器将拒绝该查询。比如,如果你写成”SLECT …” 而不是 “SELECT …”,那就没有下文了。

    但这还不算完,解析器还会检查关键字是否使用正确的顺序,比如 WHERE 写在 SELECT 之前会被拒绝。

    然后,解析器要分析查询中的表和字段,使用数据库元数据来检查:

    • 表是否存在
    • 表的字段是否存在
    • 对某类型字段的 运算 是否 可能(比如,你不能将整数和字符串进行比较,你不能对一个整数使用 substring() 函数)

    接着,解析器检查在查询中你是否有权限来读取(或写入)表。再强调一次:这些权限由DBA分配。

    在解析过程中,SQL 查询被转换为内部表示(通常是一个树)。

    如果一切正常,内部表示被送到查询重写器。

    查询重写器

    在这一步,我们已经有了查询的内部表示,重写器的目标是:

    • 预优化查询
    • 避免不必要的运算
    • 帮助优化器找到合理的最佳解决方案

    重写器按照一系列已知的规则对查询执行检测。如果查询匹配一种模式的规则,查询就会按照这条规则来重写。下面是(可选)规则的非详尽的列表:

    • 视图合并:如果你在查询中使用视图,视图就会转换为它的 SQL 代码。
    • 子查询扁平化:子查询是很难优化的,因此重写器会尝试移除子查询

    例如:

    会转换为:

    • 去除不必要的运算符:比如,如果你用了 DISTINCT,而其实你有 UNIQUE 约束(这本身就防止了数据出现重复),那么 DISTINCT 关键字就被去掉了。
    • 排除冗余的联接:如果相同的 JOIN 条件出现两次,比如隐藏在视图中的 JOIN 条件,或者由于传递性产生的无用 JOIN,都会被消除。
    • 常数计算赋值:如果你的查询需要计算,那么在重写过程中计算会执行一次。比如 WHERE AGE > 10+2 会转换为 WHERE AGE > 12 , TODATE(“日期字符串”) 会转换为 datetime 格式的日期值。
    • (高级)分区裁剪(Partition Pruning):如果你用了分区表,重写器能够找到需要使用的分区。
    • (高级)物化视图重写(Materialized view rewrite):如果你有个物化视图匹配查询谓词的一个子集,重写器将检查视图是否最新并修改查询,令查询使用物化视图而不是原始表。
    • (高级)自定义规则:如果你有自定义规则来修改查询(就像 Oracle policy),重写器就会执行这些规则。
    • (高级)OLAP转换:分析/加窗 函数,星形联接,ROLLUP 函数……都会发生转换(但我不确定这是由重写器还是优化器来完成,因为两个进程联系很紧,必须看是什么数据库)。

    【译者注: 物化视图  。谓词,predicate,条件表达式的求值返回真或假的过程】

    重写后的查询接着送到优化器,这时候好玩的就开始了。

    统计

    研究数据库如何优化查询之前我们需要谈谈统计,因为没有统计的数据库是愚蠢的。除非你明确指示,数据库是不会分析自己的数据的。没有分析会导致数据库做出(非常)糟糕的假设。

    但是,数据库需要什么类型的信息呢?

    我必须(简要地)谈谈数据库和操作系统如何保存数据。两者使用的最小单位叫做页或块(默认 4 或 8 KB)。这就是说如果你仅需要 1KB,也会占用一个页。要是页的大小为 8KB,你就浪费了 7KB。

    回来继续讲统计! 当你要求数据库收集统计信息,数据库会计算下列值:

    • 表中行和页的数量
    • 表中每个列中的:
      唯一值
      数据长度(最小,最大,平均)
      数据范围(最小,最大,平均)
    • 表的索引信息

    这些统计信息会帮助优化器估计查询所需的磁盘 I/O、CPU、和内存使用

    对每个列的统计非常重要。
    比如,如果一个表 PERSON 需要联接 2 个列: LAST_NAME, FIRST_NAME。
    根据统计信息,数据库知道FIRST_NAME只有 1,000 个不同的值,LAST_NAME 有 1,000,000 个不同的值。
    因此,数据库就会按照 LAST_NAME, FIRST_NAME 联接。
    因为 LAST_NAME 不大可能重复,多数情况下比较 LAST_NAME 的头 2 、 3 个字符就够了,这将大大减少比较的次数。

    不过,这些只是基本的统计。你可以让数据库做一种高级统计,叫直方图。直方图是列值分布情况的统计信息。例如:

    • 出现最频繁的值
    • 分位数 【译者注:http://baike.baidu.com/view/1323572.htm】

    这些额外的统计会帮助数据库找到更佳的查询计划,尤其是对于等式谓词(例如: WHERE AGE = 18 )或范围谓词(例如: WHERE AGE > 10 and AGE < 40),因为数据库可以更好的了解这些谓词相关的数字类型数据行(注:这个概念的技术名称叫选择率)。

    统计信息保存在数据库元数据内,例如(非分区)表的统计信息位置:

    • Oracle: USER / ALL / DBA_TABLES 和 USER / ALL / DBA_TAB_COLUMNS
    • DB2: SYSCAT.TABLES 和 SYSCAT.COLUMNS

    统计信息必须及时更新。如果一个表有 1,000,000 行而数据库认为它只有 500 行,没有比这更糟糕的了。统计唯一的不利之处是需要时间来计算,这就是为什么数据库大多默认情况下不会自动计算统计信息。数据达到百万级时统计会变得困难,这时候,你可以选择仅做基本统计或者在一个数据库样本上执行统计。

    举个例子,我参与的一个项目需要处理每表上亿条数据的库,我选择只统计10%,结果造成了巨大的时间消耗。本例证明这是个糟糕的决定,因为有时候 Oracle 10G 从特定表的特定列中选出的 10% 跟全部 100% 有很大不同(对于拥有一亿行数据的表,这种情况极少发生)。这次错误的统计导致了一个本应 30 秒完成的查询最后执行了 8 个小时,查找这个现象根源的过程简直是个噩梦。这个例子显示了统计的重要性。

    注:当然了,每个数据库还有其特定的更高级的统计。如果你想了解更多信息,读读数据库的文档。话虽然这么说,我已经尽力理解统计是如何使用的了,而且我找到的最好的官方文档来自PostgreSQL

    查询优化器

    所有的现代数据库都在用基于成本的优化(即CBO)来优化查询。道理是针对每个运算设置一个成本,通过应用成本最低廉的一系列运算,来找到最佳的降低查询成本的方法。

    为了理解成本优化器的原理,我觉得最好用个例子来『感受』一下这个任务背后的复杂性。这里我将给出联接 2 个表的 3 个方法,我们很快就能看到即便一个简单的联接查询对于优化器来说都是个噩梦。之后,我们会了解真正的优化器是怎么做的。

    对于这些联接操作,我会专注于它们的时间复杂度,但是,数据库优化器计算的是它们的 CPU 成本、磁盘 I/O 成本、和内存需求。时间复杂度和 CPU 成本的区别是,时间成本是个近似值(给我这样的懒家伙准备的)。而 CPU 成本,我这里包括了所有的运算,比如:加法、条件判断、乘法、迭代……还有呢:

    • 每一个高级代码运算都要特定数量的低级 CPU 运算。
    • 对于 Intel Core i7、Intel Pentium 4、AMD Opteron…等,(就 CPU 周期而言)CPU 的运算成本是不同的,也就是说它取决于 CPU 的架构。

    使用时间复杂度就容易多了(至少对我来说),用它我也能了解到 CBO 的概念。由于磁盘 I/O 是个重要的概念,我偶尔也会提到它。请牢记,大多数时候瓶颈在于磁盘 I/O 而不是 CPU 使用

    索引

    在研究 B+树的时候我们谈到了索引,要记住一点,索引都是已经排了序的

    仅供参考:还有其他类型的索引,比如位图索引,在 CPU、磁盘I/O、和内存方面与B+树索引的成本并不相同。

    另外,很多现代数据库为了改善执行计划的成本,可以仅为当前查询动态地生成临时索引

    存取路径

    在应用联接运算符(join operators)之前,你首先需要获得数据。以下就是获得数据的方法。

    注:由于所有存取路径的真正问题是磁盘 I/O,我不会过多探讨时间复杂度。

    【译者注:四种类型的Oracle索引扫描介绍  】

    全扫描

    如果你读过执行计划,一定看到过『全扫描』(或只是『扫描』)一词。简单的说全扫描就是数据库完整的读一个表或索引。就磁盘 I/O 而言,很明显全表扫描的成本比索引全扫描要高昂

    范围扫描

    其他类型的扫描有索引范围扫描,比如当你使用谓词 ” WHERE AGE > 20 AND AGE < 40 ” 的时候它就会发生。

    当然,你需要在 AGE 字段上有索引才能用到索引范围扫描。

    在第一部分我们已经知道,范围查询的时间成本大约是 log(N)+M,这里 N 是索引的数据量,M 是范围内估测的行数。多亏有了统计我们才能知道 N 和 M 的值(注: M 是谓词 “ AGE > 20 AND AGE < 40 ” 的选择率)。另外范围扫描时,你不需要读取整个索引,因此在磁盘 I/O 方面没有全扫描那么昂贵

    唯一扫描

    如果你只需要从索引中取一个值你可以用唯一扫描

    根据 ROW ID 存取

    多数情况下,如果数据库使用索引,它就必须查找与索引相关的行,这样就会用到根据 ROW ID 存取的方式。

    例如,假如你运行:

    如果 person 表的 age 列有索引,优化器会使用索引找到所有年龄为 28 的人,然后它会去表中读取相关的行,这是因为索引中只有 age 的信息而你要的是姓和名。

    但是,假如你换个做法:

    PERSON 表的索引会用来联接 TYPE_PERSON 表,但是 PERSON 表不会根据行ID 存取,因为你并没有要求这个表内的信息。

    虽然这个方法在少量存取时表现很好,这个运算的真正问题其实是磁盘 I/O。假如需要大量的根据行ID存取,数据库也许会选择全扫描。

    其它路径

    我没有列举所有的存取路径,如果你感兴趣可以读一读 Oracle文档。其它数据库里也许叫法不同但背后的概念是一样的。

    联接运算符

    那么,我们知道如何获取数据了,那现在就把它们联接起来!

    我要展现的是3个个常用联接运算符:合并联接(Merge join),哈希联接(Hash Join)和嵌套循环联接(Nested Loop Join)。但是在此之前,我需要引入新词汇了:内关系和外关系( inner relation and outer relation) 【译者注: “内关系和外关系” 这个说法来源不明,跟查询的“内联接(INNER JOIN)  、外联接(OUTER JOIN)  ” 不是一个概念 。只查到百度百科词条:关系数据库 里提到“每个表格(有时被称为一个关系)……” 。 其他参考链接 “Merge Join”   “Hash Join”   “Nested Loop Join” 】  。 一个关系可以是:

    • 一个表
    • 一个索引
    • 上一个运算的中间结果(比如上一个联接运算的结果)

    当你联接两个关系时,联接算法对两个关系的处理是不同的。在本文剩余部分,我将假定:

    • 外关系是左侧数据集
    • 内关系是右侧数据集

    比如, A JOIN B 是 A 和 B 的联接,这里 A 是外关系,B 是内关系。

    多数情况下, A JOIN B 的成本跟 B JOIN A 的成本是不同的

    在这一部分,我还将假定外关系有 N 个元素,内关系有 M 个元素。要记住,真实的优化器通过统计知道 N 和 M 的值。

    注:N 和 M 是关系的基数。【译者注: 基数 】

    嵌套循环联接

    嵌套循环联接是最简单的。

    道理如下:

    • 针对外关系的每一行
    • 查看内关系里的所有行来寻找匹配的行

    下面是伪代码:

    由于这是个双迭代,时间复杂度是 O(N*M)。

    在磁盘 I/O 方面, 针对 N 行外关系的每一行,内部循环需要从内关系读取 M 行。这个算法需要从磁盘读取 N+ N*M 行。但是,如果内关系足够小,你可以把它读入内存,那么就只剩下 M + N 次读取。这样修改之后,内关系必须是最小的,因为它有更大机会装入内存。

    在CPU成本方面没有什么区别,但是在磁盘 I/O 方面,最好最好的,是每个关系只读取一次。

    当然,内关系可以由索引代替,对磁盘 I/O 更有利。

    由于这个算法非常简单,下面这个版本在内关系太大无法装入内存时,对磁盘 I/O 更加有利。道理如下:

    • 为了避免逐行读取两个关系,
    • 你可以成簇读取,把(两个关系里读到的)两簇数据行保存在内存里,
    • 比较两簇数据,保留匹配的,
    • 然后从磁盘加载新的数据簇来继续比较
    • 直到加载了所有数据。

    可能的算法如下:

    使用这个版本,时间复杂度没有变化,但是磁盘访问降低了:

    • 用前一个版本,算法需要 N + N*M 次访问(每次访问读取一行)。
    • 用新版本,磁盘访问变为 外关系的数据簇数量 + 外关系的数据簇数量 * 内关系的数据簇数量
    • 增加数据簇的尺寸,可以降低磁盘访问。
    哈希联接

    哈希联接更复杂,不过在很多场合比嵌套循环联接成本低。

    哈希联接的道理是:

    • 1) 读取内关系的所有元素
    • 2) 在内存里建一个哈希表
    • 3) 逐条读取外关系的所有元素
    • 4) (用哈希表的哈希函数)计算每个元素的哈希值,来查找内关系里相关的哈希桶内
    • 5) 是否与外关系的元素匹配。

    在时间复杂度方面我需要做些假设来简化问题:

    • 内关系被划分成 X 个哈希桶
    • 哈希函数几乎均匀地分布每个关系内数据的哈希值,就是说哈希桶大小一致。
    • 外关系的元素与哈希桶内的所有元素的匹配,成本是哈希桶内元素的数量。

    时间复杂度是 (M/X) * N + 创建哈希表的成本(M) + 哈希函数的成本 * N 。
    如果哈希函数创建了足够小规模的哈希桶,那么复杂度就是 O(M+N)

    还有个哈希联接的版本,对内存有利但是对磁盘 I/O 不够有利。 这回是这样的:

    • 1) 计算内关系和外关系双方的哈希表
    • 2) 保存哈希表到磁盘
    • 3) 然后逐个哈希桶比较(其中一个读入内存,另一个逐行读取)。
    合并联接

    合并联接是唯一产生排序的联接算法。

    注:这个简化的合并联接不区分内表或外表;两个表扮演同样的角色。但是真实的实现方式是不同的,比如当处理重复值时。

    1.(可选)排序联接运算:两个输入源都按照联接关键字排序。

    2.合并联接运算:排序后的输入源合并到一起。

    排序

    我们已经谈到过合并排序,在这里合并排序是个很好的算法(但是并非最好的,如果内存足够用的话,还是哈希联接更好)。

    然而有时数据集已经排序了,比如:

    • 如果表内部就是有序的,比如联接条件里一个索引组织表 【译者注: index-organized table 】
    • 如果关系是联接条件里的一个索引
    • 如果联接应用在一个查询中已经排序的中间结果
    合并联接

    这部分与我们研究过的合并排序中的合并运算非常相似。不过这一次呢,我们不是从两个关系里挑选所有元素,而是只挑选相同的元素。道理如下:

    • 1) 在两个关系中,比较当前元素(当前=头一次出现的第一个)
    • 2) 如果相同,就把两个元素都放入结果,再比较两个关系里的下一个元素
    • 3) 如果不同,就去带有最小元素的关系里找下一个元素(因为下一个元素可能会匹配)
    • 4) 重复 1、2、3步骤直到其中一个关系的最后一个元素。

    因为两个关系都是已排序的,你不需要『回头去找』,所以这个方法是有效的。

    该算法是个简化版,因为它没有处理两个序列中相同数据出现多次的情况(即多重匹配)。真实版本『仅仅』针对本例就更加复杂,所以我才选择简化版。

    如果两个关系都已经排序,时间复杂度是 O(N+M)

    如果两个关系需要排序,时间复杂度是对两个关系排序的成本:O(N*Log(N) + M*Log(M))

    对于计算机极客,我给出下面这个可能的算法来处理多重匹配(注:对于这个算法我不保证100%正确):

    哪个算法最好?

    如果有最好的,就没必要弄那么多种类型了。这个问题很难,因为很多因素都要考虑,比如:

    • 空闲内存:没有足够的内存的话就跟强大的哈希联接拜拜吧(至少是完全内存中哈希联接)。
    • 两个数据集的大小。比如,如果一个大表联接一个很小的表,那么嵌套循环联接就比哈希联接快,因为后者有创建哈希的高昂成本;如果两个表都非常大,那么嵌套循环联接CPU成本就很高昂。
    • 是否有索引:有两个 B+树索引的话,聪明的选择似乎是合并联接。
    • 结果是否需要排序:即使你用到的是未排序的数据集,你也可能想用成本较高的合并联接(带排序的),因为最终得到排序的结果后,你可以把它和另一个合并联接串起来(或者也许因为查询用 ORDER BY/GROUP BY/DISTINCT 等操作符隐式或显式地要求一个排序结果)。
    • 关系是否已经排序:这时候合并联接是最好的候选项。
    • 联接的类型:是等值联接(比如 tableA.col1 = tableB.col2 )? 还是内联接外联接笛卡尔乘积?或者自联接?有些联接在特定环境下是无法工作的。
    • 数据的分布:如果联接条件的数据是倾斜的(比如根据姓氏来联接人,但是很多人同姓),用哈希联接将是个灾难,原因是哈希函数将产生分布极不均匀的哈希桶。
    • 如果你希望联接操作使用多线程或多进程

    想要更详细的信息,可以阅读DB2ORACLE 或 SQL Server)的文档。

    简化的例子

    我们已经研究了 3 种类型的联接操作。

    现在,比如说我们要联接 5 个表,来获得一个人的全部信息。一个人可以有:

    • 多个手机号(MOBILES)
    • 多个邮箱(MAILS)
    • 多个地址(ADRESSES)
    • 多个银行账号(BANK_ACCOUNTS)

    换句话说,我们需要用下面的查询快速得到答案:

    作为一个查询优化器,我必须找到处理数据最好的方法。但有 2 个问题:

    • 每个联接使用那种类型?
      我有 3 种可选(哈希、合并、嵌套),同时可能用到 0, 1 或 2 个索引(不必说还有多种类型的索引)。
    • 按什么顺序执行联接?
      比如,下图显示了针对 4 个表仅仅 3 次联接,可能采用的执行计划:

    那么下面就是我可能采取的方法:

    • 1) 采取粗暴的方式
      用数据库统计,计算每种可能的执行计划的成本,保留最佳方案。但是,会有很多可能性。对于一个给定顺序的联接操作,每个联接有三种可能性:哈希、合并、嵌套,那么总共就有 3^4 种可能性。确定联接的顺序是个二叉树的排列问题,会有 (2*4)!/(4+1)! 种可能的顺序。对本例这个相当简化了的问题,我最后会得到 3^4*(2*4)!/(4+1)! 种可能。
      抛开专业术语,那相当于 27,216 种可能性。如果给合并联接加上使用 0,1 或 2 个 B+树索引,可能性就变成了 210,000种。我是不是告诉过你这个查询其实非常简单吗?
    • 2) 我大叫一声辞了这份工作
      很有诱惑力,但是这样一来,你不会的到查询结果,而我需要钱来付账单。
    • 3) 我只尝试几种执行计划,挑一个成本最低的。
      由于不是超人,我不能算出所有计划的成本。相反,我可以武断地从全部可能的计划中选择一个子集,计算它们的成本,把最佳的计划给你。
    • 4) 我用聪明的规则来降低可能性的数量
      有两种规则:
      我可以用『逻辑』规则,它能去除无用的可能性,但是无法过滤大量的可能性。比如: 『嵌套联接的内关系必须是最小的数据集』。
      我接受现实,不去找最佳方案,用更激进的规则来大大降低可能性的数量。比如:『如果一个关系很小,使用嵌套循环联接,绝不使用合并或哈希联接。』

    在这个简单的例子中,我最后得到很多可能性。但现实世界的查询还会有其他关系运算符,像 OUTER JOIN, CROSS JOIN, GROUP BY, ORDER BY, PROJECTION, UNION, INTERSECT, DISTINCT … 这意味着更多的可能性。

    那么,数据库是如何处理的呢?

    动态规划,贪婪算法和启发式算法

    关系型数据库会尝试我刚刚提到的多种方法,优化器真正的工作是在有限时间里找到一个好的解决方案。

    多数时候,优化器找到的不是最佳的方案,而是一个『不错』的

    对于小规模的查询,采取粗暴的方式是有可能的。但是为了让中等规模的查询也能采取粗暴的方式,我们有办法避免不必要的计算,这就是动态规划

    动态规划

    这几个字背后的理念是,很多执行计划是非常相似的。看看下图这几种计划:

    它们都有相同的子树(A JOIN B),所以,不必在每个计划中计算这个子树的成本,计算一次,保存结果,当再遇到这个子树时重用。用更正规的说法,我们面对的是个重叠问题。为了避免对部分结果的重复计算,我们使用记忆法。

    应用这一技术,我们不再有 (2*N)!/(N+1)! 的复杂度,而是“只有” 3^N。在之前 4 个JOIN 的例子里,这意味着将 336 次排序降为 81 次。如果是大一些的查询,比如 8 个 JOIN (其实也不是很大啦),就是将 57,657,600 次降为 6551 次。【译者注:这一小段漏掉了,感谢 nsos指出来。另外感谢 Clark Li 指出Dynamic Programing 应该翻译为动态规划。 】

    对于计算机极客,下面是我在先前给你的教程里找到的一个算法。我不提供解释,所以仅在你已经了解动态规划或者精通算法的情况下阅读(我提醒过你哦):

    针对大规模查询,你也可以用动态规划方法,但是要附加额外的规则(或者称为启发式算法)来减少可能性。

    • 如果我们仅分析一个特定类型的计划(例如左深树 left-deep tree,参考),我们得到 n*2^n 而不是 3^n。

    • 如果我们加上逻辑规则来避免一些模式的计划(像『如果一个表有针对指定谓词的索引,就不要对表尝试合并联接,要对索引』),就会在不给最佳方案造成过多伤害的前提下,减少可能性的数量。【译者注:原文应该是有两处笔误: as=has, to=too】
    • 如果我们在流程里增加规则(像『联接运算先于其他所有的关系运算』),也能减少大量的可能性。
    • ……
    贪婪算法

    但是,优化器面对一个非常大的查询,或者为了尽快找到答案(然而查询速度就快不起来了),会应用另一种算法,叫贪婪算法。

    原理是按照一个规则(或启发)以渐进的方式制定查询计划。在这个规则下,贪婪算法逐步寻找最佳算法,先处理一条JOIN,接着每一步按照同样规则加一条新的JOIN。

    我们来看个简单的例子。比如一个针对5张表(A,B,C,D,E)4次JOIN 的查询,为了简化我们把嵌套JOIN作为可能的联接方式,按照『使用最低成本的联接』规则。

    • 直接从 5 个表里选一个开始(比如 A)
    • 计算每一个与 A 的联接(A 作为内关系或外关系)
    • 发现 “A JOIN B” 成本最低
    • 计算每一个与 “A JOIN B” 的结果联接的成本(“A JOIN B” 作为内关系或外关系)
    • 发现 “(A JOIN B) JOIN C” 成本最低
    • 计算每一个与 “(A JOIN B) JOIN C” 的结果联接的成本 ……
    • 最后确定执行计划 “( ( (A JOIN B) JOIN C) JOIN D ) JOIN E )”

    因为我们是武断地从表 A 开始,我们可以把同样的算法用在 B,然后 C,然后 D, 然后 E。最后保留成本最低的执行计划。

    顺便说一句,这个算法有个名字,叫『最近邻居算法』。

    抛开细节不谈,只需一个良好的模型和一个 N*log(N) 复杂度的排序,问题就轻松解决了。这个算法的复杂度是 O(N*log(N)) ,对比一下完全动态规划的 O(3^N)。如果你有个20个联接的大型查询,这意味着 26 vs 3,486,784,401 ,天壤之别!

    这个算法的问题是,我们做的假设是:找到 2 个表的最佳联接方法,保留这个联接结果,再联接下一个表,就能得到最低的成本。但是:

    • 即使在 A, B, C 之间,A JOIN B 可得最低成本
    • (A JOIN C) JOIN B 也许比 (A JOIN B) JOIN C 更好。

    为了改善这一状况,你可以多次使用基于不同规则的贪婪算法,并保留最佳的执行计划。

    其他算法

    [ 如果你已经受够了算法话题,就直接跳到下一部分。这部分对文章余下的内容不重要。]【译者注:我也很想把这段跳过去 -_- 】

    很多计算机科学研究者热衷于寻找最佳的执行计划,他们经常为特定问题或模式探寻更好的解决方案,比如:

    • 如果查询是星型联接(一种多联接查询),某些数据库使用一种特定的算法。
    • 如果查询是并行的,某些数据库使用一种特定的算法。 ……

    其他算法也在研究之中,就是为了替换在大型查询中的动态规划算法。贪婪算法属于一个叫做启发式算法的大家族,它根据一条规则(或启发),保存上一步找到的方法,『附加』到当前步骤来进一步搜寻解决方法。有些算法根据特定规则,一步步的应用规则但不总是保留上一步找到的最佳方法。它们统称启发式算法。

    比如,基因算法就是一种:

    • 一个方法代表一种可能的完整查询计划
    • 每一步保留了 P 个方法(即计划),而不是一个。
    • 0) P 个计划随机创建
    • 1) 成本最低的计划才会保留
    • 2) 这些最佳计划混合在一起产生 P 个新的计划
    • 3) 一些新的计划被随机改写
    • 4) 1,2,3步重复 T 次
    • 5) 然后在最后一次循环,从 P 个计划里得到最佳计划。

    循环次数越多,计划就越好。

    这是魔术?不,这是自然法则:适者生存!

    PostgreSQL 实现了基因算法,但我并没有发现它是不是默认使用这种算法的。

    数据库中还使用了其它启发式算法,像『模拟退火算法(Simulated Annealing)』、『交互式改良算法(Iterative Improvement)』、『双阶段优化算法(Two-Phase Optimization)』…..不过,我不知道这些算法当前是否在企业级数据库应用了,还是仅仅用在研究型数据库。

    如果想进一步了解,这篇研究文章介绍两个更多可能的算法《数据库查询优化中联接排序问题的算法综述》,你可以去阅读一下。

    真实的优化器

    [ 这段不重要,可以跳过 ]

    然而,所有上述罗里罗嗦的都非常理论化,我是个开发者而不是研究者,我喜欢具体的例子

    我们来看看 SQLite 优化器 是怎么工作的。这是个轻量化数据库,它使用一种简单优化器,基于带有附加规则的贪婪算法,来限制可能性的数量。

    • SQLite 在有 CROSS JOIN 操作符时从不给表重新排序
    • 使用嵌套联接
    • 外联接始终按顺序评估
    • ……
    • 3.8.0之前的版本使用『最近邻居』贪婪算法来搜寻最佳查询计划
      等等……我们见过这个算法!真是巧哈!
    • 从3.8.0版本(发布于2015年)开始,SQLite使用『N最近邻居』贪婪算法来搜寻最佳查询计划

    我们再看看另一个优化器是怎么工作的。IBM DB2 跟所有企业级数据库都类似,我讨论它是因为在切换到大数据之前,它是我最后真正使用的数据库。

    看过官方文档后,我们了解到 DB2 优化器可以让你使用 7 种级别的优化:

    • 对联接使用贪婪算法
    •     0 – 最小优化,使用索引扫描和嵌套循环联接,避免一些查询重写
      •     1 – 低级优化
      •     2 – 完全优化
    • 对联接使用动态规划算法
    •     3 – 中等优化和粗略的近似法
      •     5 – 完全优化,使用带有启发式的所有技术
      •     7 – 完全优化,类似级别5,但不用启发式
      •     9 – 最大优化,完全不顾开销,考虑所有可能的联接顺序,包括笛卡尔乘积

    可以看到 DB2 使用贪婪算法和动态规划算法。当然,他们不会把自己的启发算法分享出来的,因为查询优化器是数据库的看家本领。

    DB2 的默认级别是 5,优化器使用下列特性: 【译者注:以下出现的一些概念我没有做考证,因为[ 这段不重要,可以跳过 ]】

    • 使用所有可用的统计,包括线段树(frequent-value)和分位数统计(quantile statistics)。
    • 使用所有查询重写规则(含物化查询表路由,materialized query table routing),除了在极少情况下适用的计算密集型规则。
    • 使用动态规划模拟联接
    •     有限使用组合内关系(composite inner relation)
    •     对于涉及查找表的星型模式,有限使用笛卡尔乘积
    • 考虑宽泛的访问方式,含列表预取(list prefetch,注:我们将讨论什么是列表预取),index ANDing(注:一种对索引的特殊操作),和物化查询表路由。

    默认的,DB2 对联接排列使用受启发式限制的动态规划算法

    其它情况 (GROUP BY, DISTINCT…) 由简单规则处理。

    查询计划缓存

    由于创建查询计划是耗时的,大多数据库把计划保存在查询计划缓存,来避免重复计算。这个话题比较大,因为数据库需要知道什么时候更新过时的计划。办法是设置一个上限,如果一个表的统计变化超过了上限,关于该表的查询计划就从缓存中清除。

    查询执行器

    在这个阶段,我们有了一个优化的执行计划,再编译为可执行代码。然后,如果有足够资源(内存,CPU),查询执行器就会执行它。计划中的操作符 (JOIN, SORT BY …) 可以顺序或并行执行,这取决于执行器。为了获得和写入数据,查询执行器与数据管理器交互,本文下一部分来讨论数据管理器。

    数据管理器

    在这一步,查询管理器执行了查询,需要从表和索引获取数据,于是向数据管理器提出请求。但是有 2 个问题:

    • 关系型数据库使用事务模型,所以,当其他人在同一时刻使用或修改数据时,你无法得到这部分数据。
    • 数据提取是数据库中速度最慢的操作,所以数据管理器需要足够聪明地获得数据并保存在内存缓冲区内。

    在这一部分,我没看看关系型数据库是如何处理这两个问题的。我不会讲数据管理器是怎么获得数据的,因为这不是最重要的(而且本文已经够长的了!)。

    缓存管理器

    我已经说过,数据库的主要瓶颈是磁盘 I/O。为了提高性能,现代数据库使用缓存管理器。

    查询执行器不会直接从文件系统拿数据,而是向缓存管理器要。缓存管理器有一个内存缓存区,叫做缓冲池从内存读取数据显著地提升数据库性能。对此很难给出一个数量级,因为这取决于你需要的是哪种操作:

    • 顺序访问(比如:全扫描) vs 随机访问(比如:按照row id访问)
    • 读还是写

    以及数据库使用的磁盘类型:

    • 7.2k/10k/15k rpm的硬盘
    • SSD
    • RAID 1/5/…

    要我说,内存比磁盘要快100到10万倍

    然而,这导致了另一个问题(数据库总是这样…),缓存管理器需要在查询执行器使用数据之前得到数据,否则查询管理器不得不等待数据从缓慢的磁盘中读出来。

    预读

    这个问题叫预读。查询执行器知道它将需要什么数据,因为它了解整个查询流,而且通过统计也了解磁盘上的数据。道理是这样的:

    • 当查询执行器处理它的第一批数据时
    • 会告诉缓存管理器预先装载第二批数据
    • 当开始处理第二批数据时
    • 告诉缓存管理器预先装载第三批数据,并且告诉缓存管理器第一批可以从缓存里清掉了。
    • ……

    缓存管理器在缓冲池里保存所有的这些数据。为了确定一条数据是否有用,缓存管理器给缓存的数据添加了额外的信息(叫闩锁)。

    有时查询执行器不知道它需要什么数据,有的数据库也不提供这个功能。相反,它们使用一种推测预读法(比如:如果查询执行器想要数据1、3、5,它不久后很可能会要 7、9、11),或者顺序预读法(这时候缓存管理器只是读取一批数据后简单地从磁盘加载下一批连续数据)。

    为了监控预读的工作状况,现代数据库引入了一个度量叫缓冲/缓存命中率,用来显示请求的数据在缓存中找到而不是从磁盘读取的频率。

    注:糟糕的缓存命中率不总是意味着缓存工作状态不佳。更多信息请阅读Oracle文档

    缓冲只是容量有限的内存空间,因此,为了加载新的数据,它需要移除一些数据。加载和清除缓存需要一些磁盘和网络I/O的成本。如果你有个经常执行的查询,那么每次都把查询结果加载然后清除,效率就太低了。现代数据库用缓冲区置换策略来解决这个问题。

    缓冲区置换策略

    多数现代数据库(至少 SQL Server, MySQL, Oracle 和 DB2)使用 LRU 算法。

    LRU

    LRU代表最近最少使用(Least Recently Used)算法,背后的原理是:在缓存里保留的数据是最近使用的,所以更有可能再次使用。

    图解:

    为了更好的理解,我假设缓冲区里的数据没有被闩锁锁住(就是说是可以被移除的)。在这个简单的例子里,缓冲区可以保存 3 个元素:

    • 1:缓存管理器(简称CM)使用数据1,把它放入空的缓冲区
    • 2:CM使用数据4,把它放入半载的缓冲区
    • 3:CM使用数据3,把它放入半载的缓冲区
    • 4:CM使用数据9,缓冲区满了,所以数据1被清除,因为它是最后一个最近使用的,数据9加入到缓冲区
    • 5:CM使用数据4,数据4已经在缓冲区了,所以它再次成为第一个最近使用的
    • 6:CM使用数据1,缓冲区满了,所以数据9被清除,因为它是最后一个最近使用的,数据1加入到缓冲区
    • ……

    这个算法效果很好,但是有些限制。如果对一个大表执行全表扫描怎么办?换句话说,当表/索引的大小超出缓冲区会发生什么?使用这个算法会清除之前缓存内所有的数据,而且全扫描的数据很可能只使用一次。

    改进

    为了防止这个现象,有些数据库增加了特殊的规则,比如Oracle文档中的描述:

    『对非常大的表来说,数据库通常使用直接路径来读取,即直接加载区块[……],来避免填满缓冲区。对于中等大小的表,数据库可以使用直接读取或缓存读取。如果选择缓存读取,数据库把区块置于LRU的尾部,防止清空当前缓冲区。』

    还有一些可能,比如使用高级版本的LRU,叫做 LRU-K。例如,SQL Server 使用 LRU-2。

    这个算法的原理是把更多的历史记录考虑进来。简单LRU(也就是 LRU-1),只考虑最后一次使用的数据。LRU-K呢:

    • 考虑数据最后第K次使用的情况
    • 数据使用的次数加进了权重
    • 一批新数据加载进入缓存,旧的但是经常使用的数据不会被清除(因为权重更高)
    • 但是这个算法不会保留缓存中不再使用的数据
    • 所以数据如果不再使用,权重值随着时间推移而降低

    计算权重是需要成本的,所以SQL Server只是使用 K=2,这个值性能不错而且额外开销可以接受。

    关于LRU-K更深入的知识,可以阅读早期的研究论文(1993):数据库磁盘缓冲的LRU-K页面置换算法

    其他算法

    当然还有其他管理缓存的算法,比如:

    • 2Q(类LRU-K算法)
    • CLOCK(类LRU-K算法)
    • MRU(最新使用的算法,用LRU同样的逻辑但不同的规则)
    • LRFU(Least Recently and Frequently Used,最近最少使用最近最不常用
    • ……

    写缓冲区

    我只探讨了读缓存 —— 在使用之前预先加载数据。用来保存数据、成批刷入磁盘,而不是逐条写入数据从而造成很多单次磁盘访问。

    要记住,缓冲区保存的是(最小的数据单位)而不是行(逻辑上/人类习惯的观察数据的方式)。缓冲池内的页如果被修改了但还没有写入磁盘,就是脏页。有很多算法来决定写入脏页的最佳时机,但这个问题与事务的概念高度关联,下面我们就谈谈事务。

    事务管理器

    最后但同样重要的,是事务管理器,我们将看到这个进程是如何保证每个查询在自己的事务内执行的。但开始之前,我们需要理解ACID事务的概念。

    “I’m on acid”

    一个ACID事务是一个工作单元,它要保证4个属性:

    • 原子性Atomicity): 事务『要么全部完成,要么全部取消』,即使它持续运行10个小时。如果事务崩溃,状态回到事务之前(事务回滚)。
    • 隔离性Isolation): 如果2个事务 A 和 B 同时运行,事务 A 和 B 最终的结果是相同的,不管 A 是结束于 B 之前/之后/运行期间。
    • 持久性Durability): 一旦事务提交(也就是成功执行),不管发生什么(崩溃或者出错),数据要保存在数据库中。
    • 一致性Consistency): 只有合法的数据(依照关系约束和函数约束)能写入数据库,一致性与原子性和隔离性有关。

    在同一个事务内,你可以运行多个SQL查询来读取、创建、更新和删除数据。当两个事务使用相同的数据,麻烦就来了。经典的例子是从账户A到账户B的汇款。假设有2个事务:

    • 事务1(T1)从账户A取出100美元给账户B
    • 事务2(T2)从账户A取出50美元给账户B

    我们回来看看ACID属性:

    • 原子性确保不管 T1 期间发生什么(服务器崩溃、网络中断…),你不能出现账户A 取走了100美元但没有给账户B 的现象(这就是数据不一致状态)。
    • 隔离性确保如果 T1 和 T2 同时发生,最终A将减少150美元,B将得到150美元,而不是其他结果,比如因为 T2 部分抹除了 T1 的行为,A减少150美元而B只得到50美元(这也是不一致状态)。
    • 持久性确保如果 T1 刚刚提交,数据库就发生崩溃,T1 不会消失得无影无踪。
    • 一致性确保钱不会在系统内生成或灭失。

    [以下部分不重要,可以跳过]

    现代数据库不会使用纯粹的隔离作为默认模式,因为它会带来巨大的性能消耗。SQL一般定义4个隔离级别:

    • 串行化(Serializable,SQLite默认模式):最高级别的隔离。两个同时发生的事务100%隔离,每个事务有自己的『世界』。
    • 可重复读(Repeatable read,MySQL默认模式):每个事务有自己的『世界』,除了一种情况。如果一个事务成功执行并且添加了新数据,这些数据对其他正在执行的事务是可见的。但是如果事务成功修改了一条数据,修改结果对正在运行的事务不可见。所以,事务之间只是在新数据方面突破了隔离,对已存在的数据仍旧隔离。
      举个例子,如果事务A运行”SELECT count(1) from TABLE_X” ,然后事务B在 TABLE_X 加入一条新数据并提交,当事务A再运行一次 count(1)结果不会是一样的。
      这叫幻读(phantom read)。
    • 读取已提交(Read committed,Oracle、PostgreSQL、SQL Server默认模式):可重复读+新的隔离突破。如果事务A读取了数据D,然后数据D被事务B修改(或删除)并提交,事务A再次读取数据D时数据的变化(或删除)是可见的。
      这叫不可重复读(non-repeatable read)。
    • 读取未提交(Read uncommitted):最低级别的隔离,是读取已提交+新的隔离突破。如果事务A读取了数据D,然后数据D被事务B修改(但并未提交,事务B仍在运行中),事务A再次读取数据D时,数据修改是可见的。如果事务B回滚,那么事务A第二次读取的数据D是无意义的,因为那是事务B所做的从未发生的修改(已经回滚了嘛)。
      这叫脏读(dirty read)。

    多数数据库添加了自定义的隔离级别(比如 PostgreSQL、Oracle、SQL Server的快照隔离),而且并没有实现SQL规范里的所有级别(尤其是读取未提交级别)。

    默认的隔离级别可以由用户/开发者在建立连接时覆盖(只需要增加很简单的一行代码)。

    并发控制

    确保隔离性、一致性和原子性的真正问题是对相同数据的写操作(增、更、删):

    • 如果所有事务只是读取数据,它们可以同时工作,不会更改另一个事务的行为。
    • 如果(至少)有一个事务在修改其他事务读取的数据,数据库需要找个办法对其它事务隐藏这种修改。而且,它还需要确保这个修改操作不会被另一个看不到这些数据修改的事务擦除。

    这个问题叫并发控制

    最简单的解决办法是依次执行每个事务(即顺序执行),但这样就完全没有伸缩性了,在一个多处理器/多核服务器上只有一个核心在工作,效率很低。

    理想的办法是,每次一个事务创建或取消时:

    • 监控所有事务的所有操作
    • 检查是否2个(或更多)事务的部分操作因为读取/修改相同的数据而存在冲突
    • 重新编排冲突事务中的操作来减少冲突的部分
    • 按照一定的顺序执行冲突的部分(同时非冲突事务仍然在并发运行)
    • 考虑事务有可能被取消

    用更正规的说法,这是对冲突的调度问题。更具体点儿说,这是个非常困难而且CPU开销很大的优化问题。企业级数据库无法承担等待几个小时,来寻找每个新事务活动最好的调度,因此就使用不那么理想的方式以避免更多的时间浪费在解决冲突上。

    锁管理器

    为了解决这个问题,多数数据库使用和/或数据版本控制。这是个很大的话题,我会集中探讨锁,和一点点数据版本控制。

    悲观锁

    原理是:

    • 如果一个事务需要一条数据
    • 它就把数据锁住
    • 如果另一个事务也需要这条数据
    • 它就必须要等第一个事务释放这条数据
      这个锁叫排他锁

    但是对一个仅仅读取数据的事务使用排他锁非常昂贵,因为这会迫使其它只需要读取相同数据的事务等待。因此就有了另一种锁,共享锁

    共享锁是这样的:

    • 如果一个事务只需要读取数据A
    • 它会给数据A加上『共享锁』并读取
    • 如果第二个事务也需要仅仅读取数据A
    • 它会给数据A加上『共享锁』并读取
    • 如果第三个事务需要修改数据A
    • 它会给数据A加上『排他锁』,但是必须等待另外两个事务释放它们的共享锁。

    同样的,如果一块数据被加上排他锁,一个只需要读取该数据的事务必须等待排他锁释放才能给该数据加上共享锁。

    锁管理器是添加和释放锁的进程,在内部用一个哈希表保存锁信息(关键字是被锁的数据),并且了解每一块数据是:

    • 被哪个事务加的锁
    • 哪个事务在等待数据解锁
    死锁

    但是使用锁会导致一种情况,2个事务永远在等待一块数据:

    在本图中:

    • 事务A 给 数据1 加上排他锁并且等待获取数据2
    • 事务B 给 数据2 加上排他锁并且等待获取数据1

    这叫死锁

    在死锁发生时,锁管理器要选择取消(回滚)一个事务,以便消除死锁。这可是个艰难的决定:

    • 杀死数据修改量最少的事务(这样能减少回滚的成本)?
    • 杀死持续时间最短的事务,因为其它事务的用户等的时间更长?
    • 杀死能用更少时间结束的事务(避免可能的资源饥荒)?
    • 一旦发生回滚,有多少事务会受到回滚的影响?

    在作出选择之前,锁管理器需要检查是否有死锁存在。

    哈希表可以看作是个图表(见上文图),图中出现循环就说明有死锁。由于检查循环是昂贵的(所有锁组成的图表是很庞大的),经常会通过简单的途径解决:使用超时设定。如果一个锁在超时时间内没有加上,那事务就进入死锁状态。

    锁管理器也可以在加锁之前检查该锁会不会变成死锁,但是想要完美的做到这一点还是很昂贵的。因此这些预检经常设置一些基本规则。

    两段锁

    实现纯粹的隔离最简单的方法是:事务开始时获取锁,结束时释放锁。就是说,事务开始前必须等待确保自己能加上所有的锁,当事务结束时释放自己持有的锁。这是行得通的,但是为了等待所有的锁,大量的时间被浪费了

    更快的方法是两段锁协议(Two-Phase Locking Protocol由 DB2 和 SQL Server使用),在这里,事务分为两个阶段:

    • 成长阶段:事务可以获得锁,但不能释放锁。
    • 收缩阶段:事务可以释放锁(对于已经处理完而且不会再次处理的数据),但不能获得新锁。

    这两条简单规则背后的原理是:

    • 释放不再使用的锁,来降低其它事务的等待时间
    • 防止发生这类情况:事务最初获得的数据,在事务开始后被修改,当事务重新读取该数据时发生不一致。

    这个规则可以很好地工作,但有个例外:如果修改了一条数据、释放了关联的锁后,事务被取消(回滚),而另一个事务读到了修改后的值,但最后这个值却被回滚。为了避免这个问题,所有独占锁必须在事务结束时释放

    多说几句

    当然了,真实的数据库使用更复杂的系统,涉及到更多类型的锁(比如意向锁,intention locks)和更多的粒度(行级锁、页级锁、分区锁、表锁、表空间锁),但是道理是相同的。

    我只探讨纯粹基于锁的方法,数据版本控制是解决这个问题的另一个方法

    版本控制是这样的:

    • 每个事务可以在相同时刻修改相同的数据
    • 每个事务有自己的数据拷贝(或者叫版本)
    • 如果2个事务修改相同的数据,只接受一个修改,另一个将被拒绝,相关的事务回滚(或重新运行)

    这将提高性能,因为:

    • 读事务不会阻塞写事务
    • 写事务不会阻塞读
    • 没有『臃肿缓慢』的锁管理器带来的额外开销

    除了两个事务写相同数据的时候,数据版本控制各个方面都比锁表现得更好。只不过,你很快就会发现磁盘空间消耗巨大。

    数据版本控制和锁机制是两种不同的见解:乐观锁和悲观锁。两者各有利弊,完全取决于使用场景(读多还是写多)。关于数据版本控制,我推荐这篇非常优秀的文章,讲的是PostgreSQL如何实现多版本并发控制的。

    一些数据库,比如DB2(直到版本 9.7)和 SQL Server(不含快照隔离)仅使用锁机制。其他的像PostgreSQL, MySQL 和 Oracle 使用锁和鼠标版本控制混合机制。我不知道是否有仅用版本控制的数据库(如果你知道请告诉我)。

    [2015-08-20更新]一名读者告诉我:

    Firebird 和 Interbase 用不带锁的版本控制。

    版本控制对索引的影响挺有趣的:有时唯一索引会出现重复,索引的条目会多于表行数,等等。

    如果你读过不同级别的隔离那部分内容,你会知道,提高隔离级别就会增加锁的数量和事务等待加锁的时间。这就是为什么多数数据库默认不会使用最高级别的隔离(即串行化)。

    当然,你总是可以自己去主流数据库(像MySQLPostgreSQL 或 Oracle)的文档里查一下。

    日志管理器

    我们已经知道,为了提升性能,数据库把数据保存在内存缓冲区内。但如果当事务提交时服务器崩溃,崩溃时还在内存里的数据会丢失,这破坏了事务的持久性

    你可以把所有数据都写在磁盘上,但是如果服务器崩溃,最终数据可能只有部分写入磁盘,这破坏了事务的原子性

    事务作出的任何修改必须是或者撤销,或者完成。

    有 2 个办法解决这个问题:

    • 影子副本/页(Shadow copies/pages):每个事务创建自己的数据库副本(或部分数据库的副本),并基于这个副本来工作。一旦出错,这个副本就被移除;一旦成功,数据库立即使用文件系统的一个把戏,把副本替换到数据中,然后删掉『旧』数据。
    • 事务日志(Transaction log):事务日志是一个存储空间,在每次写盘之前,数据库在事务日志中写入一些信息,这样当事务崩溃或回滚,数据库知道如何移除或完成尚未完成的事务。
    WAL(预写式日志)

    影子副本/页在运行较多事务的大型数据库时制造了大量磁盘开销,所以现代数据库使用事务日志。事务日志必须保存在稳定的存储上,我不会深挖存储技术,但至少RAID磁盘是必须的,以防磁盘故障。

    多数数据库(至少是Oracle, SQL ServerDB2PostgreSQL, MySQL 和 SQLite) 使用预写日志协议(Write-Ahead Logging protocol ,WAL)来处理事务日志。WAL协议有 3 个规则:

    • 1) 每个对数据库的修改都产生一条日志记录,在数据写入磁盘之前日志记录必须写入事务日志。
    • 2) 日志记录必须按顺序写入;记录 A 发生在记录 B 之前,则 A 必须写在 B 之前。
    • 3) 当一个事务提交时,在事务成功之前,提交顺序必须写入到事务日志。

    这个工作由日志管理器完成。简单的理解就是,日志管理器处于缓存管理器(cache manager)和数据访问管理器(data access manager,负责把数据写入磁盘)之间,每个 update / delete / create / commit / rollback 操作在写入磁盘之前先写入事务日志。简单,对吧?

    回答错误! 我们研究了这么多内容,现在你应该知道与数据库相关的每一件事都带着『数据库效应』的诅咒。好吧,我们说正经的,问题在于,如何找到写日志的同时保持良好的性能的方法。如果事务日志写得太慢,整体都会慢下来。

    ARIES

    1992年,IBM 研究人员『发明』了WAL的增强版,叫 ARIES。ARIES 或多或少地在现代数据库中使用,逻辑未必相同,但AIRES背后的概念无处不在。我给发明加了引号是因为,按照MIT这门课的说法,IBM 的研究人员『仅仅是写了事务恢复的最佳实践方法』。AIRES 论文发表的时候我才 5 岁,我不关心那些酸溜溜的科研人员老掉牙的闲言碎语。事实上,我提及这个典故,是在开始探讨最后一个技术点前让你轻松一下。我阅读过这篇 ARIES 论文 的大量篇幅,发现它很有趣。在这一部分我只是简要的谈一下 ARIES,不过我强烈建议,如果你想了解真正的知识,就去读那篇论文。

    ARIES 代表『数据库恢复原型算法』(Algorithms for Recovery and Isolation Exploiting Semantics)。

    这个技术要达到一个双重目标:

    • 1) 写日志的同时保持良好性能
    • 2) 快速和可靠的数据恢复

    有多个原因让数据库不得不回滚事务:

    • 因为用户取消
    • 因为服务器或网络故障
    • 因为事务破坏了数据库完整性(比如一个列有唯一性约束而事务添加了重复值)
    • 因为死锁

    有时候(比如网络出现故障),数据库可以恢复事务。

    这怎么可能呢?为了回答这个问题,我们需要了解日志里保存的信息。

    日志

    事务的每一个操作(增/删/改)产生一条日志,由如下内容组成:

    • LSN:一个唯一的日志序列号(Log Sequence Number)。LSN是按时间顺序分配的 * ,这意味着如果操作 A 先于操作 B,log A 的 LSN 要比 log B 的 LSN 小。
    • TransID:产生操作的事务ID。
    • PageID:被修改的数据在磁盘上的位置。磁盘数据的最小单位是页,所以数据的位置就是它所处页的位置。
    • PrevLSN:同一个事务产生的上一条日志记录的链接。
    • UNDO:取消本次操作的方法。
      比如,如果操作是一次更新,UNDO将或者保存元素更新前的值/状态(物理UNDO),或者回到原来状态的反向操作(逻辑UNDO) **。
    • REDO:重复本次操作的方法。 同样的,有 2 种方法:或者保存操作后的元素值/状态,或者保存操作本身以便重复。
    • …:(供您参考,一个 ARIES 日志还有 2 个字段:UndoNxtLSN 和 Type)。

    进一步说,磁盘上每个页(保存数据的,不是保存日志的)都记录着最后一个修改该数据操作的LSN。

    *LSN的分配其实更复杂,因为它关系到日志存储的方式。但道理是相同的。

    ** ARIES 只使用逻辑UNDO,因为处理物理UNDO太过混乱了。

    注:据我所知,只有 PostgreSQL 没有使用UNDO,而是用一个垃圾回收服务来删除旧版本的数据。这个跟 PostgreSQL 对数据版本控制的实现有关。

    为了更好的说明这一点,这有一个简单的日志记录演示图,是由查询 “UPDATE FROM PERSON SET AGE = 18;” 产生的,我们假设这个查询是事务18执行的。【译者注: SQL 语句原文如此,应该是作者笔误 】

    每条日志都有一个唯一的LSN,链接在一起的日志属于同一个事务。日志按照时间顺序链接(链接列表的最后一条日志是最后一个操作产生的)。

    日志缓冲区

    为了防止写日志成为主要的瓶颈,数据库使用了日志缓冲区

    当查询执行器要求做一次修改:

    • 1) 缓存管理器将修改存入自己的缓冲区;
    • 2) 日志管理器将相关的日志存入自己的缓冲区;
    • 3) 到了这一步,查询执行器认为操作完成了(因此可以请求做另一次修改);
    • 4) 接着(不久以后)日志管理器把日志写入事务日志,什么时候写日志由某算法来决定。
    • 5) 接着(不久以后)缓存管理器把修改写入磁盘,什么时候写盘由某算法来决定。

    当事务提交,意味着事务每一个操作的 1 2 3 4 5 步骤都完成了。写事务日志是很快的,因为它只是『在事务日志某处增加一条日志』;而数据写盘就更复杂了,因为要用『能够快速读取的方式写入数据』。

    STEAL 和 FORCE 策略

    出于性能方面的原因,第 5 步有可能在提交之后完成,因为一旦发生崩溃,还有可能用REDO日志恢复事务。这叫做 NO-FORCE策略

    数据库可以选择FORCE策略(比如第 5 步在提交之前必须完成)来降低恢复时的负载。

    另一个问题是,要选择数据是一步步的写入(STEAL策略),还是缓冲管理器需要等待提交命令来一次性全部写入(NO-STEAL策略)。选择STEAL还是NO-STEAL取决于你想要什么:快速写入但是从 UNDO 日志恢复缓慢,还是快速恢复。

    总结一下这些策略对恢复的影响:

    • STEAL/NO-FORCE 需要 UNDO 和 REDO: 性能高,但是日志和恢复过程更复杂 (比如 ARIES)。多数数据库选择这个策略。 注:这是我从多个学术论文和教程里看到的,但并没有看到官方文档里显式说明这一点。
    • STEAL/ FORCE 只需要 UNDO.
    • NO-STEAL/NO-FORCE 只需要 REDO.
    • NO-STEAL/FORCE 什么也不需要: 性能最差,而且需要巨大的内存。
    关于恢复

    Ok,有了不错的日志,我们来用用它们!

    假设新来的实习生让数据库崩溃了(首要规矩:永远是实习生的错。),你重启了数据库,恢复过程开始了。

    ARIES从崩溃中恢复有三个阶段:

    • 1) 分析阶段:恢复进程读取全部事务日志,来重建崩溃过程中所发生事情的时间线,决定哪个事务要回滚(所有未提交的事务都要回滚)、崩溃时哪些数据需要写盘。
    • 2) Redo阶段:这一关从分析中选中的一条日志记录开始,使用 REDO 来将数据库恢复到崩溃之前的状态。

    在REDO阶段,REDO日志按照时间顺序处理(使用LSN)。

    对每一条日志,恢复进程需要读取包含数据的磁盘页LSN。

    如果LSN(磁盘页)>= LSN(日志记录),说明数据已经在崩溃前写到磁盘(但是值已经被日志之后、崩溃之前的某个操作覆盖),所以不需要做什么。

    如果LSN(磁盘页)< LSN(日志记录),那么磁盘上的页将被更新。

    即使将被回滚的事务,REDO也是要做的,因为这样简化了恢复过程(但是我相信现代数据库不会这么做的)。

    • 3) Undo阶段:这一阶段回滚所有崩溃时未完成的事务。回滚从每个事务的最后一条日志开始,并且按照时间倒序处理UNDO日志(使用日志记录的PrevLSN)。

     

    恢复过程中,事务日志必须留意恢复过程的操作,以便写入磁盘的数据与事务日志相一致。一个解决办法是移除被取消的事务产生的日志记录,但是这个太困难了。相反,ARIES在事务日志中记录补偿日志,来逻辑上删除被取消的事务的日志记录。

    当事务被『手工』取消,或者被锁管理器取消(为了消除死锁),或仅仅因为网络故障而取消,那么分析阶段就不需要了。对于哪些需要 REDO 哪些需要 UNDO 的信息在 2 个内存表中:

    • 事务表(保存当前所有事务的状态)
    • 脏页表(保存哪些数据需要写入磁盘)

    当新的事务产生时,这两个表由缓存管理器和事务管理器更新。因为是在内存中,当数据库崩溃时它们也被破坏掉了。

    分析阶段的任务就是在崩溃之后,用事务日志中的信息重建上述的两个表。为了加快分析阶段,ARIES提出了一个概念:检查点(check point),就是不时地把事务表和脏页表的内容,还有此时最后一条LSN写入磁盘。那么在分析阶段当中,只需要分析这个LSN之后的日志即可。

    结语

    写这篇文章之前,我知道这个题目有多大,也知道写这样一篇深入的文章会相当耗时。最后证明我过于乐观了,实际上花了两倍于预期的时间,但是我学到了很多。

    如果你想很好地了解数据库,我推荐这篇研究论文:《数据库系统架构》,对数据库有很好的介绍(共110页),而且非计算机专业人士也能读懂。这篇论文出色的帮助我制定了本文的写作计划,它没有像本文那样专注于数据结构和算法,更多的讲了架构方面的概念。

    如果你仔细阅读了本文,你现在应该了解一个数据库是多么的强大了。鉴于文章很长,让我来提醒你我们都学到了什么:

    • B+树索引概述
    • 数据库的全局概述
    • 基于成本的优化概述,特别专注了联接运算
    • 缓冲池管理概述
    • 事务管理概述

    但是,数据库包含了更多的聪明巧技。比如,我并没有谈到下面这些棘手的问题:

    • 如何管理数据库集群和全局事务
    • 如何在数据库运行的时候产生快照
    • 如何高效地存储(和压缩)数据
    • 如何管理内存

    所以,当你不得不在问题多多的 NoSQL数据库和坚如磐石的关系型数据库之间抉择的时候,要三思而行。不要误会,某些 NoSQL数据库是很棒的,但是它们毕竟还年轻,只是解决了少量应用关注的一些特定问题。

    最后说一句,如果有人问你数据库的原理是什么,你不用逃之夭夭,现在你可以回答:

    或者,就让他/她来看本文吧。


    ----------------------------------------------------------------------------------------------------------------------------------------

    原文地址:http://blog.jobbole.com/100349/

    英文出处:http://coding-geek.com/how-databases-work/

    展开全文
  • 数据库基础与SQL基础知识整理

    万次阅读 2016-09-16 12:48:31
    PS:对数据库进行一些危险操作,一定要先备份 一.数据库简介 1. DBMS(DataBaseManagement System,数据库管理系统)和数据库数据库Schema有两种含义,一种是概念上的Schema,指的是一组DDL语句集,该语句集完整地...

    PS:对数据库进行一些危险操作,一定要先备份

    一.数据库简介

    1. DBMS(DataBaseManagement System,数据库管理系统)和数据库数据库Schema有两种含义,一种是概念上的Schema,指的是一组DDL语句集,该语句集完整地描述了数据库的结构。还有一种是物理上的Schema,指的是数据库中的一个名字空间,它包含一组表、视图和存储过程等命名对象 MSSQLServer、Oracle等某种DBMS;存放一堆数据表的一个分类( Catalog )

    2.品牌:

     不同品牌的DBMS有自己的不同的特点:

    MYSQL(速度很快,适合对数据要求并不是十分严谨的地方,去掉了很多中小型企业中不常用的功能,php里面用),

    MSSQLServer(与.net结合很好,只能运行在windows平台下,大数据量、大交易量表现并不十分好)

    DB2(大型数据库)、Oracle(大型数据库),

    Access(文件)、 SQLite(极其轻量级数据库)、Sybase等。对于开发人员来讲,大同小异

    SQL(语言)<SQLServer()<MSSQLServer。

    SQL是英文StructuredQuery Language的缩写,意思为结构化查询语言

    除了Access、SQLServerCE、SQLite等文件型数据库之外,大部分数据库都需要数据库服务器才能运行。学习、开发时是连接本机的数据库,上线运行时是数据库运行在单独的服务器

    3. 名词解释:

     数据库DataBase,不同类的数据应该放到不同的数据库中

    1.便于对各个数据类别的进行个性化管理

    2.避免命名冲突

    3.安全性更高 

    Table(表):关系数据库中的关系指的就是表。

    不同的货物要放到各自的货物架,将这种区域叫做“表”(Table)。不同的表根据放的数据不同进行空间的优化,找起来也方便。

    列(Column)、字段(Field)、行(Row)元组

    4.杂谈

       1.数据库里面的数据--关系型的数据。

       2.关系转对象,将数据库中的数据关系转为对象进行处理(面向对象编程)

       3.“-- ”为注释标识符,相当于java中的“//”

    二.主键与外键

       1.英文名:Primary key(主键)

       2.主键简介:主键就是数据行的唯一标识。不会重复的列才能当主键。一个表可以没有主键,但是会非常难以处理,因此没有特殊理由表都要设定主键

       3.主键的方法:业务主键和逻辑主键和组合主键

    • 业务主键是使用有业务意义的字段做主键,比如身份证号、银行账号等;
    • 逻辑主键是使用没有任何业务意义的字段做主键,完全给程序看的,业务人员不会看的数据。因为很难保证业务主键不会重复(身份证号重复)、不会变化(帐号升位),因此推荐用逻辑主键。
    • 组合主键,有多个数据组合而成的,几乎不用。

       4.英文名: Foreign Key(外键)

       5.外键简介:A主键表里面有一列内容为B主键表的主键。那么该A主键表的改行内容就叫做外键,A表就叫做外键表。

    三.DataBase包含的文件

    1. 必须包含

    •   1个主数据文件(.mdf)必须。--后缀名必须记住
    •   1个事务日志文件(.ldf)必须。

    2. 可以包含:

    • 任意多个次要数据文件(.ndf)
    • 多个事务日志文件

    四.DataBase的数据类型(使用时机)

    1. bit数据类型在写程序的时候只能用1(true)或0(false)来表示,但是在设计器中只能用true或false单词来表示。设计器中bit写true和false,在sql代码中用1和0

    2.创建表的数据类型:

    • Char(若为10,则只能存10个,不够的话空格补齐)
    • Nchar
    • Varchar(可以0-10个英文,不会空格补齐(动态分配),不能10个汉字)
    • Nvarchar(可以10个汉字可以10个英文)
    • Varchar(max)
    • Nvarchar(max)
    • Text
    • Ntext
    • int(将该列设定为主键后,可以在属性中设置自动增长)

    3.一些技巧

    • 带var表示可变
    • 不带var表四固定长度
    • 带N为unicode字符
    • 不带N为非unicode字符,
    • 如果列数据项的大小一致,则使用 char。
    • 如果列数据项的大小差异相当大,则使用 varchar。
    • 如果列数据项大小相差很大,而且大小可能超过 8,000 字节,请使用 varchar(max)
    • ps:1. Char(10),10表示10个字符。能存储10个ASCII字符和5个Unicode字符。长度为1-8000
    • 存储10个ASCII字符,占10个字节
    • 存储5个Unicode字符占10个字节
    •    2.Nchar(10),能存储10个ASCII字符或10个Unicode字符。长度为1-4000
    • 存储10个ASCII字符占20个字节
    • 存储10个Unicode字符也占20个字节。

    五.SQL语句(脚本或命令)

    1. 简介:

    •   SQL 全名是结构化查询语言(Structured Query Language),是关系数据库管理系统的标准语言,所有的数据库都可以使用
    • SQL语句是和DBMS“交谈”专用的语句,不同DBMS都认SQL语法
    • Sql语句中字符串用单引号,没有字符,都是字符串,单等号(=),转意(’’’’)
    • Sql语句中关键字大小写不敏感,字符串还是敏感的
    • Sql语句中创建库,表,删除,几乎大多数功能都可以通过代码来完成

    2.主要成分:

          DDL(数据定义语言,建表、建库等语句。)、DML(数据操作语言)和DCL (数据库控制语言)

    • DDL:(Create Table、DropTable、Alter Table等)//对表或是数据库进行操作
    • DML:Select、Insert、Update、Delete//对数据进行操作
    • DCL:GRANT 授权、REVOKE取消授权

    3.分离后加载数据库后出错的原因:

    • 1,版本不一样(导出sql脚本。数据还原与备份,数据导入导出。)鼠标右键任务生成脚本-----生成低版本的脚本,后面高级设置中版本和数据类型更改
    • 2.拒绝访问修改权限--—235对本电脑上的SQL程序右击属性权限全部选定为对号即可
    • 3.不能改名修改权限
    • 4.系统错误。-------kanqingkuang
    • 5.只读
    • 6. 表创建完成后进行修改时可能会报错------工具选项designers 阻止保存创建要求重新勾 去掉

     4.生成脚本

              数据库→右键→任务→生成脚本

    • 可选择生成什么样的脚本
    • 选择生成的数据库版本
    • 是否包含某些脚本等。
    • 是否生成带数据的脚本(2005、2008都有该功能express没有。)

             将脚本发送到对方,对方复制粘贴到编辑框运行即可

     4.通过代码创建数据库

    drop database MySchool—删除数据库
    
    ---------------------------------------------------------------
    
    create database MySchool---创建数据库
    
    on primary—创建主文件
    
    (
    
              --用“,”分隔语句最后一句不用写
    
             --括号一定是圆括号
    
             name='MySchool_data',--数据库名称—最好加上_data以便分别日志文件与主文件
    
             fileName='d:\MySchool_data.mdf',--物理文件名
    
             size=5mb,--初始大小
    
             maxSize=10mb,--最大大小
    
            fileGrowth=15% --主文件增长率
    
    )
    
    log on—创建日志文件
    
    (
    
    name='MySchool_log',--日志文件名
    
    fileName='d:\MySchool_log.ldf',--日志物理文件名
    
    maxSize=4mb,--最大大小
    
    size=2mb,
    
    fileGrowth=1mb
    
    )

    5.创建表(除了creat,select,insert into,也可以创建表,在表备份里面)

    useMyDBTest   -- 声明在哪个数据库中生成,一定要写或者在上方选择
    
    create table Tstudent -- 创建一个Tstudent表
    
    (
    
    stuId int identity(1,1) primary key, -- 列名类型 自增 主键
    
    stuName nvarchar(10), -- 列明 类型
    
    stuGender bit not null   -- 列明 类型 非空
    
    )

     6.向表中插入数据(bit类型代码添加时 只能输入0或1)

    • 1. Insert into 表名(列名1,列名2)values(值1,值2)
    • 2. Insert into 表名values(值1,值2)-- 若向所有的列插入数据,则可以省略列名
    • 3. Insert into 表(列1) values(值1)—向指定列中添加数据,但须保证其余列可以为NULL才可。
    • 4.插入多行数据
       insert into Score1(studentId,english,math)
    
    select 1,80,100 UNION all
    
    select 1,80,100 union all
    
    select 3,50,59 union ALL
    
    select 4,66,89 UNION all
    
    select 5,59,100
    
    ----全部为union all连接的语句进行添加,才不会删除重复项,若有一个不是union all ,则就会删除重复项
    
       Insert into 表(列)----当多行添加时只写一行改行代码即可
    
    select 值1,值2 union—---写列值
    
    select 值1,值2—最后一--行不必加union
    
     -----union连接的语句进行添加数据会将重复添加的数据删除

    7.更新表中数据

        1.更新一部分数据关键字where,数据间的‘,’别忘了(可以更新一些数据)

    UPDATE food SET  fdname='zhazhahza', fddsec ='可以' WHERE fdname='炸鸡'
    update 表名 set 列1=值1,列2=值2  where 列名=值

        2.更新一整列的数据

    UPDATEfood SET  fdname='zhazhahza', fddsec ='可以'
    update 表名set 列1=值1,列2=值2

    8.更新数据的注意与补充

         1.用where语句表示只更新列名是值的行

          注意SQL中等于判断用单个=,而不是==

         2.Where中可以使用的其他逻辑运算符:(||)or、(&&)and、(!)not、<、>、>=、<=、 <>(或!=)等,,,例如: 

    update Student set sAge = sAge + 1—所有人的年龄加1
    
    update Student set sAge=30 where sName='华佗' orsAge<25
    
    update TbStudent set tSGender=0;
    
    not and or—优先级
    
    用小括号可以改变优先级.
    
    update TblStudent set tSAge=tSAge+1
    
    Null代表不知道
    
    UPDATE TblStudent set tsname=tsname+'^-^'where tsage<20 and tsgender='男'     UPDATE food SET fdname='jjj'+'-' WHEREfdid>5

    9.数据的删除操作

            1. delete FROM student--删除表中所有数据 表还有(不常用)

    drop TABLE student --删除该表格
    
    delete FROM Department where deptid=1--根据条件删除某一条数据
    
    truncate TABLE Department--删除数据表还有,但会重置id值(主键)再写入数据时会重排列主键—不能加where

    2. truncate语句删除数据的优点

     

    truncate语句非常高效。由于truncate操作采用按最小方式来记录日志,所以效率非常高。对于数百万条数据使用truncate删除只要几秒钟,而使用delete则可能耗费几小时。
    
    truncate语句会把表中的自动编号重置为默认值。
    
    truncate语句不触发delete触发器(触发器后期会讲)。

    10,注意点

        1.GO的作用

            为分段的作用,点击执行后执行到Go的地方便会停下,再点击执行执行下一段

       2.N的作用(必须大写)

         插入数据时,在单引号钱加一个大写的N使插入的数据不会乱码

       3.【】的作用

         想用关键词作为普通此使用,加上【】即可,尽量不适用、

    11.约束

        1.简介

    •  数据库约束是为了保证数据的完整性(正确性)而实现的一套机制
    • 见文件Employee.sql
    • 非空约束 不让改的时候就工具选项,干掉勾
    • 主键约束(PK) primary key constraint 唯一且不为空
    • 唯一约束 (UQ)unique constraint 唯一,允许为空,但只能出现一次
    • 默认约束 (DF)default constraint 默认值
    • 检查约束 (CK)check constraint 范围以及格式限制
    • 外键约束 (FK)foreign key constraint 表关系

     2.鼠标点点约束(最后要进行Ctrl+S保存才可以)

    • 1.主键约束(PK),选中表单击设计设为主键
    • 2.非空约束,将null不选中即可
    •  3.检查约束(CK),选定一行右击选择CHECK约束,添加一个,修改名字,写入一个表达式类len(DeptName)>3即该内容必须大于三个字
    • 4.默认约束(DF),在设计页面中选定一行,在下方的属性列表里有一个默认值,输入即可。   //改名时名字要规范
    • 5,唯一约束(UQ),选中一行,右击选择“索引\键”,添加,是否唯一,改名字,选定需要约束哪一列。
    • 6.外键约束(FK),(前提是必须有两个表,一个为外键表,一个为主键表。)选定其中一个表,右击,关系,添加,改名,表和列规范,选定主键表,选定主键行和外键行,关闭,保存。。约束后必须使外键和主键的值相同
    • 7,视图中点开表,下一级就是。

    第十五天

    Ps:像in or and等词,几乎可以在任何地方使用,多注意

    一.SQL语句之代码对列(非数据)的操作(关键词alter)

    1.删除外键表和主键表时应先删除外检表再删除主键表才可。或者进行“级联“—打开其中一张表的设计—右击—关系—表设计器—INSERT与UPDATE规范—改为级联--即可

    2.手动删除一列(删除Employees里面的empage列)

           ALTERTABLE Employees DROP COLUMN empage

       --关键词为alter(改变)和drop和column(列)

    3.手动增加列

          ALTER TABLE Employees ADD EmpAdr NVARCHAR(50),EmpAgeINT

        --不加column关键词,增加多列用逗号隔开

    4.手动修改列的数据类型

          ALTER TABLE employees ALTER COLUMN empemail NVARCHAR(1000)

       --需要column和两个alter关键词

    二,SQL语句之通过代码添加约束

        1.添加主键约束

            ALTER TABLE employees ADD CONSTRAINTPK_EmpId PRIMARY KEY (EmpId)

           --constraint关键词,primary key关键词(填写需要添加约束的列名)

    2.添加非空约束

            ALTER TABLEemployees ALTER COLUMN empname VARCHAR(50) NOT NULL

           --alter column注意 修改的列后面必须加上数据类型

    3.添加唯一约束

           ALTER TABLE employees ADD CONSTRAINTUQ_EmpName UNIQUE (empname)

       --和主键约束差不多,只需让unique替换下来即可

    4.添加默认约束

    ALTER TABLE employees ADD CONSTRAINTDF_EmpGender DEFAULT ('男')FOR empgender

        --关键词default(默认的意思),(默认的数据)for 所要操作的列

    5.添加检查约束

            ALTERTABLE employees ADD CONSTRAINT CK_EmpAge CHECK (empage>0 AND empage<120)

           --括号内写所需要遵从的表达式

    6.添加外键约束

      ALTERTABLE employees ADD CONSTRAINT FK_employees_department FOREIGN KEY(deptid) REFERENCESdepartment (depid)

          --第一个()内写外键表的指定列名,第二个括号内写主键表的指定列名

    三.SQL语句之准确查询语句的补充

    PS:对于查询语句from后面跟一个结果集(要起一个临时表名),不是非要一个单一的表名

    1.查询一个表中的所有数据

             SELECT* FROM employees    

            --*代表所有的列,为了简化而已。

    2.查询特定列的数据

             SELECTEmpName,EmpAge FROM employees

            --将*替换下来即可,中间用逗号隔开

    3.添加别名查询某一列(为了让别人看的更方便)

             SELECT EmpName AS 姓名,EmpAgeAS 年龄 FROM employees

    --关键词as 后面的为别名,但不会修改表自身的列名

     SELECT姓名=EmpName,年龄=EmpAge FROMemployees

    --另一种方法

    SELECT EmpName+EmpAdr AS 信息 FROM employees

    --将几列数据合为一列,以别名的方式显示出来,加号注意同类型才可加

    4. 查询具有某种条件的方法

             SELECT姓名=EmpName,年龄=EmpAge FROMemployees WHERE deptid=1

         --关键词where   查询depid为一的人

    5.between and语句的使用,在什么之间

             SELECT *FROM tblstudent WHERE tsage BETWEEN20 AND 30

    6.in的作用  or的意思

           SELECT*FROM tblstudent WHERE tsid IN (10,11,12)

        --查询tsid为10或12或11的数据

    **SQL语句之模糊查询

         1. 模糊查询关键字 like 

         2.通配符 _    、   %   、  []   、  ^ 

            select * from TblStudent where tSName like '张%^'

            select * from TblStudent where tSName like '张%' andLEN(tSName)=3

               --^只有MSSQL Server支持,其他DBMS用not like。

            select * from TblStudent where tSName like '张%'

    --通配符 %多字符匹配的通配符,它匹配任意次数(零或多个)出现的任意字符

    select * from TblStudent where tSName like '张__'

    --通配符_ 单字符匹配,它匹配单个出现的字符,查询三个字的张姓人物

             select * from TblStudent where tSName like '张[a-z]_'

    select * from TblStudent where tSName like '张[0-9]_'

    select * from TblStudent where tSName like '张[^0-9]_'

    --[] 只匹配一个字符  并且这个字符必须是[]范围内的    [0-9]  [a-z]

    not与like一起使用:not like ….

         3. 要通配_、%、[、^这些字符怎么办?[_]、[%]、[ [ ]、^(不需要放到中括号里,因为^只有放到中括号中才认为是通配符) 

    四.select补充

    1.可直接计算

        select 45*3     --执行后会显示结果

    2.显示当前时间

             SELECT getdate ()

    3.print的用法 和select这两个用法相同,但select是以表格的形式来显示,而print是以消息的方式来显示

    4.    SELECT '姓名',EmpName FROMemployees

           --“,“可以添加临时列,显示结果为两列一行

    SELECT EmpName AS '姓名' FROM employees

    --as可以添加临时行,显示为一列两行

    --“+”可以连接内容,不增加列与行—总结为三个小符号的不同

    五.折行(当语句过长时)从关键字的地方折行

    六.TOP,order by,Distinct,percent关键词的作用(查询)

    1.TOP

        查询前几条数据

              SELECTTOP 3*FROM employees   --查询前三行 

    2.order by

         排序,默认为从小到大,ASC为从小到大,不必添加

               SELECT TOP 3*FROM employees ORDER BY deptid ASC–一般和TOP配合使用

    3.percent

         百分比,小数就进一,不是四舍五入

            SELECTTOP 3 PERCENT *FROM employees ORDER BY deptid DESC

         --在TOP后面,,DESC为从大到小

    4.distinct

        去除重复的数据(可以是指定列的数据)

            SELECT DISTINCT EmpName FROM employees

             --去除EmpName列的重复数据

            SELECT DISTINCT *FROM employees

            DISTINCT是对查询出的整个结果集进行数据重复处理的,而不是针对某一个列。

    七.SQL聚合函数(需要有后缀括号)

       1.注意:聚合使用后一定要弄清楚是否有其他列,不然会产生不伦不类的表

       2.MAX(最大值)、MIN(最小值)、AVG (平均值)、SUM (和)、COUNT(数量:记录的条数。)

    聚合函数对null值不计算。

    如果一行的数据都是null,count(*)包含对空值行、重复行的统计

    3.代码:

     SELECT COUNT(empName) FROM employees—名字的数目

    SELECT COUNT(empName),empage FROMemployees—会报错,因为会产生畸形表

    SELECT MAX(empage),MIN(empage)FROMemployees—最大最小值

    八.SQL语句之空值(null)的处理

    1. 说明:数据库中,一个列如果没有指定值,那么值就为null,数据库中的null表示“不知道”,而不是表示没有。因此select null+1结果是null,因为“不知道”加1的结果还是“不知道”。

     2.错误举例

       select * from score where english = null ;

    select * from score where english != null ;

    都没有任何返回结果,因为数据库也“不知道”

         3.SQL中使用is null、is not null来进行空值判断:

     select * from score where english is null ;

     select * from score where english is not null

        4.将null值替换为指定字符,,在下面有介绍搜“isnull”即可

    九.SQL之数据排序(order by)

         1.关键词(ORDER BY)子句位于SELECT语句的末尾,它允许指定按照一个列或者多个列进行排序,还可以指定排序方式是升序(从小到大排列,ASC)还是降序(从大到小排列,DESC)

         2. Order by 语句一般要放到所有语句的后面,就是先让其他语句进行筛选,全部筛选完成后,最后排序一下。

    3.表中数据是集合,集合是没有顺序的。Orderby 返回的数据是有顺序的,故此我们把order by 以后返回的数据集合叫“游标”

    4.例如:

    按照年龄升序排序所有学生信息的列表:

    SELECT * FROM  Student ORDER BY sAge ASC

    按照英语成绩从大到小排序,如果英语成绩相同则按照数学成绩从大到小排序 :

    SELECT * FROM  Score ORDER BY english DESC,math DESC

    ORDER BY子句要放到WHERE子句之后:  

     SELECT * FROM Score where english>=60 and math>=60 ORDER BY english DESC,mathDESC

         按总分查询

            SELECT 总分=tenglish+tmathFROM tblscore ORDER BY 总分 DESC

    十.SQL之数据分组(group by,where,having)

          1. 在使用select查询的时候,有时需要对数据进行分组汇总(即:将现有的数据按照某列来汇总统计),这时就需要用到group by语句。select 语句中可以使用group by 子句将行划分成较小的组,然后,使用聚组函数返回每一个组的汇总信息。//分组一般都和聚合函数连用。

    2.group by 语句作用的一列就相当于聚合函数的作用。

             SELECT 班级ID=tsclassid,总人数=COUNT(*)FROM tblstudent GROUPBY tsclassid

            --count(*)因为前面执行的语句,显示的为符合前面语句的数量,tsclassid相当于被聚合

    SELECT 班级ID=tsclassid,总人数=COUNT(*)

    FROM tblstudent

    WHERE tsgender='男'

    GROUP BY tsclassid

           --上述为一个语句,添加了一个条件为男

    3. GROUP BY子句必须放到WHERE语句的之后 ,Group By与Order By都是对筛选后的数据进行处理,而Where是用来筛选数据的

      没有出现在GROUP BY子句中的列是不能放到SELECT语句后的列名列表中的 (聚合函数中除外)

    错误: select sClassId,count(sName),sAge fromstudent group by sClassId

    正确: select sClassId,count(sName),avg(sAge)from student group by sClassId

    4.分组之后再进行筛选(having)

       对表中的数据分组后,会得到一个分组后的结果集,对该结果集进行删选用having

            SELECT 班级ID=tsclassid,总人数=COUNT(*)

    FROM tblstudent

           GROUP BY tsclassid

           HAVING COUNT(*)>3—having后需写聚合表达式

    5.where和having的区别

     注意:Having中不能使用未参与分组的列,Having不能替代where。作用不一样,Having是对组进行过滤。

    Having 是Group By的条件对分组后的数据进行筛选(与Where类似,都是筛选,只不过having是用来筛选分组后的组的。)

    在Where中不能使用聚合函数,必须使用Having,Having要位于GroupBy之后。

    Having的使用几乎是与where一样的,也可以用in。

    Having count(*) in (5,8,10)

    十一. SQL语句的执行顺序

    1>…From 表

    2>…Where 条件

    3>…Group by 列

    4>…Having 筛选条件

    5>…Select 5-1>选择列,5-2>distinct,5-3>top(应用top选项最后计算)

    6>…Order by 列

    十二.SQL之类型转换函数

        1.简介:CAST ( expressionAS data_type)//顺序和convert相反

    CONVERT ( data_type, expression,[style])

    Select ‘您的班级编号’+1  错误这里+是数学运算符

        2.代码:

           SELECT '英语成绩为:'+CAST(tenglishAS VARCHAR(20)) FROM tblscore

    SELECT '英语成绩为:'+CONVERT(VARCHAR(10),tenglish) FROM tblscore

         3.对日期的转换

          SELECT CONVERT(VARCHAR(50),GETDATE())—显示当前时间

           SELECT CONVERT(VARCHAR(50),GETDATE(),120)

         --第三个参数可以填特定的数字,将显示的时间格式转换

    4.isnull()函数对null进行操作

        --当为null时,执行这个函数

    SELECT ISNULL(CAST(tenglish AS VARCHAR(50)),'缺考')FROM tblscore

    --将前面的参数替换为后面的参数,必要时可以进行数据类型的转换

    十三.联合结果集

        1.上下联合之union(效率低,因为要去除重复)

            1.简介:

                集合运算符是对两个集合操作的,两个集合必须具有相同的列数,列具有相同的数据类型(至少能隐式转换的),最终输出的集合的列名由第一个集合的列名来确定。(可以用来连接多个结果)

            2.基本原则:

              每个结果集必须有相同的列数;每个结果集的列必须类型相容。   

            3.   SELECT tsage,tsphone FROM  tblstudent

    UNION

    SELECT tsid,CAST(tmath AS VARCHAR(20)) FROMtblscore

            4. 将多个结果集合并成一个结果集。

    union(默认去除重复,相当于默认应用了distinct)

    2.上下联合之union all(效率高,不去出重复)

         1.和uniom几乎相同

        2.不同点

           select tName,tSex from teacher union

    select sName,sSex from student

    --UNION合并两个查询结果集,并且将其中完全重复的数据行合并为一条

    select tName,tSex from teacher union all

    select sName,sSex from student

    --Union因为要进行重复值扫描,所以效率低,因此如果不是确定要合并重复行,那么就用UNION ALL

    十四.备份表格

         1.无条件复制 并且表没事先建好    

    SELECT *INTO newTblscore FROM tblscore

    --将tblscore全部复制到通过该语句新创建的表newTblscore里面

    --通过这种方式复制,只能复制表中的数据,以及列的名字和数据类型。对于约束,不会复制过来。下同

     2.有条件复制并且表没事先建好

              SELECT*INTO newTblscore FROM tblscore WHERE 4>3

    --若where后面的表达式成立,则复制全部的表

    --若where后面的表不成立,则只复制表结构 

    --这样做可以只复制表结构,但效率并不高

    SELECT TOP 0 *INTO new1tblscore FROMtblscore

    --只复制表的结构的话,这个方法效率高,建议使用

    3.表要事先建好

             insertinto backupStudent select * from students

         --backupstudent要事先建好

    十五.字符串函数(不改变表的内容,只改变显示的内容)
          SELECT LEN('你好哈,我不好')

    --len计算字符串的长度

    --计算时不包含字符右边的(再往右无字符)空格

    SELECT DATALENGTH('你好a')

    --datalength计算字符串的字节

    --计算时包含右面的空格

    SELECT LOWER('AFASdf')

    --lower大写转小写,小写不变

    SELECT UPPER('asfWE')

    --upper小写转大写,大写不变

    PRINT LTRIM('  哈哈  ')

    SELECT LTRIM('  哈哈  ')

    --ltrim字符串左侧的空格去掉

    --print以消息的形式显示

    SELECT RTRIM('  哈哈 ')

    --rtrim字符串右侧的空格去掉

    --配合使用和ltrim

    PRINT LEFT ('你好哈',2)

    --left从左侧开始留下两个字符,其余的切掉

    SELECT RIGHT ('你好哈',2)

    --right从右侧开始留下两个字符,其余的切掉

    SELECT SUBSTRING(umsg,2,4) FROM t6

    --substring显示从第二个字符开始到第三个字符   

    十六.日期函数(可精确到秒 操作)

        1.非日期函数,添加的内容之对日期的转换

             SELECT CONVERT(VARCHAR(50),GETDATE())—显示当前时间

            SELECT CONVERT(VARCHAR(50),GETDATE(),120)

            --第三个参数可以填特定的数字,将显示的时间格式转换

            判断是否为本月的方法

            WHERE datediff(month,[StartDateTime],'2010-07-1')= 0

        2. SELECT GETDATE()

    --获得当前时间

    SELECT DATEADD (YEAR,10,GETDATE())

    SELECT DATEADD (MONTH,-10,GETDATE())

    SELECT DATEADD (hour ,10,GETDATE())

    --dateadd将当前时间增加第一个参数指导的变量10

    --会自动进位(按照时间规范)

    --可为负

    PRINT DATEDIFF (YEAR,'2013-12-03',GETDATE())

    SELECT DATEDIFF (DAY,'2013-12-03',GETDATE())

    --datediff计算第二个参数到第三个参数的时间差,可以用天,月,年,小时等形式表示

    SELECT DATEPART(YEAR,GETDATE())

    PRINT DATEPART(MONTH,GETDATE())

    --datepart分开显示当前时间

    SELECT DATEDIFF(YEAR,tsbirthday,GETDATE())FROMtblstudent

    --例子。计算年龄  

    第十八天(最后部分)

    一:Case函数(case,when,then,end)

         1.

    SELECT tsid AS 学号,

    tEnglish AS 英语,--所要显示的列名,逗号不要忘了

    等级=--新增临时列的名称

    (   --括号可以不加

    CASE

                WHEN tenglish>90 THEN 'A'--中间部分为条件,后面部分为满足条件后显示的内容

                WHEN tenglish>80 THEN 'B'—then后面的数据类型要一致

                WHEN tenglish>70 THEN 'C'—没有逗号分隔

                WHEN tenglish IS NULL THEN '缺考'

               ELSE 'D'

    END --以此结束,case语句

    )

    FROM TblScore

         2.

    select studentId,成绩=

    (     

                case

                      when english between 90 and 100 then 'A'—and表示区间

                      when english between 80 and 89 then 'B'

                      when english between 70 and 79 then 'C'

                      when english between 60 and 69 then 'D'

                      when english < 60 then 'E'

                      else '缺考'

               end

    )

    from score

    3.

    SELECT

    A=CASE

    WHEN A>B THEN A ELSE B-----A的值大于B的值显示A的值,否则显示B的值

    END ,--配套的case开头,end结束

    B=CASE

    WHEN B>C THEN B ELSE C-----case在一个Select语句中可以多次使用

    END

    FROM t8

    4.

    SELECT 商品名称,销售总额=sum(销售数量*销售价格),

    等级=

    CASE

              WHEN  sum(销售数量*销售价格)>10000 THEN '金牌'

              WHEN sum(销售数量*销售价格) BETWEEN7000 AND 10000 THEN '银牌'

           ELSE '铜牌'

    END

    FROM Myorders

    GROUP BY 商品名称

    ORDER BY 销售总额 DESC

    第十九天

    一:子查询

       1.简介:把一个查询的结果在另一个查询中使用就叫子查询。(将一个查询语句做为一个结果集供其他SQL语句使用) 就像使用普通的表一样,被当作结果集的查询语句被称为子查询。所有可以使用表的地方几乎都可以使用子查询来代替。

       2.分类:

        独立子查询(子查询可以独立运行)

           SELECT

          总人数=(SELECTCOUNT(*)FROM tblstudent),

          男同学=(SELECTCOUNT(*) FROM  tblstudent WHERE tsgender='男'),

       平均成绩=(SELECT AVG(tmath)FROM tblscore)—这个没有使用from

    相关子查询(子查询中引用了父查询中的结果)

    SELECT tsname FROM (SELECT tsname,tsid FROMtblstudent) AS tbl

    SELECT * FROM (SELECT * FROM tblstudent WHEREtsgender='男') AS tbl WHERE tbl.tsage=29;

    SELECT * FROM tblstudent

    WHERE tsclassid

    IN (SELECT tclassid FROM tblclass

    WHERE tclassname='高一一班' OR  tclassName='高二一班')

     

    3.注意点:

         所使用的结果集需要起别名(as xxx)

         可以多个表一起使用

         如果子查询是多行单列的子查询,这样的子查询的结果集其实是一个集合。可以使用in关键字代替=号

    二:分页

       1.原始版分页

          SELECT TOP 6 * FROM customers--这是选择的地三页,一页6个数据

          WHERE customerid NOT IN --一定要not in才可

         (SELECT TOP (6*2) customerid FROM customers)--去除前两页的数据

         ORDER BY customerid--以id来排列的

      2.现代版分页

          SELECT * FROM --不需要top6了,因为下方的between语句

          (SELECT *,编号=ROW_NUMBER()OVER(ORDER BY customerid)

          FROM customers) AS newcus--核心代码

         WHERE newcus.编号 BETWEEN(3-1)*6+1 AND 3*6--第三页的数据,将3替换为一个参数即可

     3.关键词:row_number(),over(),orderby(),as,.

    三:over与rank语句和partition语句()

       1.over简介:

    Over()就是传说中的”开窗函数”,本身聚合函数只会计算一次,开窗以后就可以为每条记录都计算一次聚合了。

    Over子句可以为每一行计算表达式而不是只为一行,并且over可以单独定义窗口中的排序方式,而不影响最终结果集。例如:select*,row_number() over(order by id asc) as hanghao from callrecords order by iddesc

    在应用具体的聚合函数、排名函数前对行集的分区和排序。over子句,用于支持窗口的计算,所以一般与排名开窗函数、聚合开窗函数一起使用。

    窗口是用户指定的一组行。开窗函数计算从窗口派生的结果集中各行的值。

    以前使用聚合函数必须分组,即便没有group by 子句,也是默认将表中所有的数据分成了1组,来聚合。通过使用over子句可以在不分组的情况下实现聚合运算,在查询结果集中既包含基础行的数据也包含聚合函数的值。(可以把over()子句理解成是“后台运行的数据”,只是为了让聚合函数或者是排名函数“用一下”,并不影响实际显示的数据。在后台提供数据。)

    2.over两种使用方法

      (1).over子句与排名开窗函数(row_number())一起用,语法:over([partition by 列1] order by 列2)。必须有order by 子句

               SELECT *FROM

    (SELECT *,编号=ROW_NUMBER()OVER(ORDERBY customerid)

    FROM customers )AS newcus----添加编号的方法

    WHERE newcus.编号 BETWEEN(3-1)*5+1 AND 3*5

    (2).over子句与聚合开窗函数(rank())一起用,语法:over([partition by 列1])不能使用order by子句了。

         不能用order语句是说在整个语句的最后面,不是在括号里,因为在最后面使用时会将原本因为聚合开窗函数局部聚合的数据打乱。

    SELECT *,排名=RANK()OVER(ORDERBY tmath)

    FROM tblscore

    //排名形式1,2,2,4,4,4,7,7,9

    ------------------------求百分比

            SELECT *,销售价格=(销售数量*销售价格),

    百分比=(销售价格*销售数量)*1.0/(SUM(销售价格*销售数量)OVER(partition BY 销售员))*100 FROM myorders

    //灰色为一个整体执行,,红色字体为一部分执行

    3.partition语句(聚合开窗函数)

              selectid,商品名称,行号=ROW_NUMBER()

    OVER(partition by 商品名称 order by id asc) from MyOrders

    --通过商品名称将所有相同的商品先分为一组显示,然后再进行排序

    四:表连接

       1.关键:两个表需要有类似于主键与外键的连接,on为筛选器

       2. 内连接(inner join),多表内连接。(只将on后面等于的数据,相等才会显示,比如id两个表的id值必须相同才会将其连接 并显示)

    无论几张表连接,每次执行都是两张表进行连接

            SELECTtblscore.tSId,tsname,tmath,tenglish FROM tblstudent—可以查询组合表里的任意东西

    INNER JOIN --关键词

    tblscore ON tblstudent.tSId=tblscore.tSId

    WHERE tsage>20

    --on为关键词 外键=主键

       3. 外连接

    左外联(left outer join)

    SELECT * FROM tblstudent

    LEFT JOIN

    tblscore ON tblstudent.tsid=tblscore.tSId

    右外联(right outer join)

                 SELECT * FROM tblstudent

    RIGHT JOIN

    tblscore ON tblstudent.tsid=tblscore.tSId

    //不同点:左(右)外连是以左(右)表为基础,左(右)表有多少行,右(左)表就要有多少行,不够就显示null-----连接关键词左边的就是左表,右边为右表

    4.例题:

    (1)   查询所有学生(参加和未参加考试)的学生姓名、年龄、成绩,如果没有参加考试显示缺考,如果小于english&math60分显示不及格

        SELECTtsname,

    tsage,

    tmath=(CASE WHEN tmath IS NULL THEN '缺考' ELSE CONVERT(VARCHAR(10),tmath) END ),--case语句要保持then后面的类型一致,所以需要转换

    tenglish=(CASE WHEN tEnglish IS NULL THEN '缺考' ELSE CONVERT(VARCHAR(10),tEnglish) END ),

    是否及格=(CASE WHEN tmath>60 AND tenglish>60THEN '及格'ELSE '不及格' END )

    FROM tblstudent

    LEFT JOIN tblscore ON tblstudent.tsid=tblscore.tsId--所有的人根据题意要左连

        (2) 对一个表进行操作,将表中对应的省和市写三列对应起来

      SELECT--将一个表添加两个别名进行操作

    tb1.areaid,

    tb1.areaname,

    tb2.areaname

    FROM tblarea AS tb1

    INNER JOIN tblarea AS tb2

    ON tb2.AreaId=tb1.AreaPId--将不同的id对应起来

    五:视图(方便查询)

    1.简介:

    视图是一张虚拟表,它表示一张表的部分数据或多张表的综合数据,其结构和数据是建立在对表的查询基础上,以表为基础建立视图,建立后便会存在数据库里面

    视图在操作上和数据表没有什么区别,但两者的差异是其本质是不同:数据表是实际存储记录的地方,然而视图并不保存任何记录。

    相同的数据表,根据不同用户的不同需求,可以创建不同的视图(不同的查询语句)

    视图的目的是方便查询,所以一般情况下不能对视图进行增删改

    表里面 的内容改变,相对应的视图数据就会改变

    多次使用复杂代码时,就创建一个视图,谨记!!!!

    不建议修改视图,若需求改变,则最好重新建立一个视图

    优点:

    筛选表中的行\降低数据库的复杂程度

    防止未经许可的用户访问敏感数据

    2.  简单SQL操作:(什么时候用什么时候添加即可)

             --以tblstudent表创建视图,内部只包含名字和年龄,关键词as

    CREATE VIEW vw_cwStudent

    AS

    SELECT tsname,tsage FROM tblstudent

    --查询视图

    SELECT * FROM vw_cwstudent

    --使用视图数量

    SELECT * FROM vw_cwStudent WHERE tsage>30—好处在于将代码简化

    --删除视图,用于更改视图,先删除再更改

    DROP VIEW vw_cwStudent

    3.注意点:

        创建视图时,不能使用order by排序,除非有指定的top语句

        所有查询的列,必须有列名,且列名必须唯一

        createview vw_name as 后不能跟begin end

    六:变量

        1.变量分类

          局部变量:

    局部变量必须以标记@作为前缀 ,如@Ageint

    局部变量:先声明,再赋值

    全局变量(系统变量):

    全局变量必须以标记@@作为前缀,如@@version

    全局变量由系统定义和维护,我们只能读取,不能修改全局变量的值

    2.局部变量(可以修改和定义)

         (1).关键词declare(声明),@,set

              声明时,先写变量名,再写类型,与C#不太一样

         (2).简单SQL语句:

            --变量的声明

    DECLARE @name VARCHAR(10) ,@id INT –声明多个 逗号分隔

    --给变量赋初值

    SET @name='洋样' –只能单个赋值

    SELECT @name=tsname FROM tblstudent WHERE tsid=2--可以给多个变量赋值,逗号分隔

    --查询变量

    SELECT(print也可) @name—select能同时输出多个,print只能输出一个

         --以上三种语句需要一起运行才可以

      SELECT以表格的方式输出,可以同时输出多个变量

    PRINT 以文本的方式输出,一次只能输出一个变量的值

      //

           DECLARE @num int =10--声明加赋值

           SELECT(set也可) @name='李阳'--重新给变量赋值

          //

           declare @age int

    set @age=10

    print @age--输出10

    select @age+1--输出11

    print @age--输出10

    select @age=@age+1--输出10

    print @age--输出11

    /

    declare @num int

    select @num=tMath from TblScore--将最后一个值存放在num变量中

    --SET @num=(select tMath from TblScore)--严禁set只能一个,而select查询出许多

    select @num

    3.全局变量

        (1).只能读

       (2).简单全局变量

             --记录上一个执行的语句的错误信息,若无错(有错,但一行受影响其值也为0),则为0

    PRINT @@error

            --常用全局变量

    @@ERROR

    最后一个T-SQL错误的错误号

    @@IDENTITY

    最后一次插入的标识值

    @@LANGUAGE

    当前使用的语言的名称

    @@MAX_CONNECTIONS

    可以创建的同时连接的最大数目

    @@ROWCOUNT 

    受上一个SQL语句影响的行数

    @@SERVERNAME

    本地服务器的名称

    @@TRANSCOUNT      

    当前连接打开的事务数

    @@VERSION

    SQL Server的版本信息

     

    七:if else语句和while循环(与begin和end配套)

    1.i公式:

         IF(条件表达式)

         BEGIN --相当于C#里的{

           语句1
      ……

      END--相当于C#里的}

    ELSE

     BEGIN

          语句1

          ……

      END

    WHILE(条件表达式)

     BEGIN --相当于C#里的{

        语句

       ……

           continue

       BREAK

      END--相当于C#里的}

    2.简单例题:

    --计算平均分数并输出,如果平均分数超过60分输出成绩最高的三个学生的成绩,否则输出后三名的学生(if  else)

        DECLARE@avgEnglish INT--声明局部变量用以存储平均值

    --SELECT @avgEnglish=AVG(tenglish) FROMtblscore--用select进行赋值

    SET @avgEnglish=(SELECT AVG(tenglish)FROMtblscore)--set的用法

    IF(@avgEnglish>60)

    BEGIN

         SELECTTOP 3 * FROM tblscore ORDER BY tenglish DESC--select可以显示数据!! 

    END

    ELSE

    BEGIN

      SELECTTOP 3 * FROM tblscore ORDER BY tenglish 

    END

    --计算1-100数字的和(while)

      DECLARE@sum INT=0,@i INT=0

    WHILE(@i<=100)

    BEGIN

          SET @sum=@sum+@i--要用set赋值才可

          SET @i=@i+1

    END

    SELECT @sum

    ---求1-100奇数和

    DECLARE @sum INT=0,@i INT =0

    WHILE(@i<=100)

    BEGIN

         IF(@i%2!=0)

             BEGIN

                 SET @sum=@sum+@i

             END

             SET @i=@i+1

    END

    SELECT @sum

     

    第二十天

    一:事务

       1.总:同生共死,将几个语句联系起来,一个未成功,则后面所有执行了的语句都回到执行前的状态。

       2.简介:

     指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)--也就是由多个sql语句组成,必须作为一个整体执行

    这些sql语句作为一个整体一起向系统提交,要么都执行、要么都不执行

       3.步骤:

          开始事务:BEGIN TRANSACTION

    事务提交:COMMIT TRANSACTION—都没错时,执行此语句

    事务回滚:ROLLBACK TRANSACTION—有一个错误就会执行此语句,将执行后的改变全部复原

         //判断是否出错的方法

              全局变量@@ERROR;

    @@ERROR只能判断当前一条T-SQL语句执行是否有错,为了判断事务中所有T-SQL语句是否有错,我们需要对错误进行累计;

    4.四大属性:

       原子性

    事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。

    一致性

    事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如B 树索引或双向链表)都必须是正确的。

    隔离

    由并发事务所作的修改必须与任何其他并发事务所作的修改隔离。事务识别数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是第二个事务修改它之后的状态,事务不会识别中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。

    持久性

    事务完成之后,它对于系统的影响是永久性的。该修改即使出现系统故障也将一直保持。

    5.简单代码:

          BEGIN TRANSACTION --开始事务的标志

    DECLARE @sumerror INT =0--定义一个变量存储错误信息

    UPDATE bank SET balance=balance-1000 WHEREcid=0001

    SET @sumerror=@sumerror+@@error--存储错误信息

    UPDATE bank SET balance=balance+1000 WHEREcid=0002

    SET @sumerror=@sumerror+@@error

    IF(@sumerror>0)--判断

    BEGIN

             ROLLBACK TRANSACTION --事务回滚roll为滚动的意思

    END

    ELSE

    BEGIN

            COMMIT TRANSACTION --事务提交commit承诺保证的意思

    END

    6.补充:系统存在隐形事务,了解

    二:存储过程(SQL语句里面的函数,调用,简化代码)

        1.步骤:

    • 创建-声明变量-as-begin-SQL语句-end
    • 就像数据库中运行方法(类似于函数)
    • 和C#里的方法一样,由存储过程名/存储过程参数组成/可以有返回结果。
    • 前面学的if else/while/变量/insert/select 等,都可以在存储过程中使用

    2.优点;

    •  执行速度更快 – 在数据库中保存的存储过程语句都是编译过的
    • 允许模块化程序设计 – 类似方法的复用
    • 提高系统安全性 – 防止SQL注入
    • 减少网络流通量 – 只要传输 存储过程的名称

        3.两种:

           系统存储过程

    由系统定义,存放在master数据库中

    名称以“sp_”开头或”xp_”开头,自定义的存储过程可以以usp_开头。

     上放的表格

     

    说明

    sp_databases

    列出服务器上的所有数据库。

    sp_helpdb

    报告有关指定数据库或所有数据库的信息

    sp_renamedb

    更改数据库的名称

    sp_tables

    返回当前环境下可查询的对象的列表

    sp_columns

    回某个表列的信息

    sp_help

    查看某个表的所有信息

    sp_helpconstraint

    查看某个表的约束

    sp_helpindex

    查看某个表的索引

    sp_stored_procedures

    列出当前环境中的所有存储过程。

    sp_password

    添加或修改登录帐户的密码。

    sp_helptext

    显示默认值、未加密的存储过程、用户定义的存储过程、触发器或视图的实际文本。显示原始代码

     

    自定义存储过程

    由用户在自己的数据库中创建的存储过程usp,自己定义最好加上usp前缀

         4.调用参数的存储过程

           (1)无参数的存储过程调用:

    Exec usp_upGrade

    (2)有参数的存储过程两种调用法:

    EXEC usp_upGrade2 60,55 ---按次序

    EXEC usp_upGrade2 @english=55,@math=60 --参数名

    (3)参数有默认值时:

    EXEC usp_upGrade2 --都用默认值

    EXEC usp_upGrade2 1  --第一个用默认值

    EXEC usp_upGrade2 1,5   --不用默认值

      5.简单代码:

            (1)简单操作

    exec sp_databases--查看系统所有数据库

    exec sp_helptext sp_databases--查看存储过程的代码

    exec sp_helptext usp_计算和--查看自己定义存储过程的代码

    exec usp_计算和 11,12--调用存储过程

    调用的时候exec ..

    (2)定义存储过程

              --创建存储过程

    create proc usp_ChengFaa   --proc关键词

    @num1 int=0,--多个变量必须用逗号隔开

    @num2 int=0

    as   --as关键词

    begin

               select @num1*@num2

    end

    -----调用该存储过程

    exec usp_ChengFaa 3,4

     --存储过程之模糊查询

    create proc usp_personCount1

    @age int,--逗号

    @name nvarchar(50),

    @countSum int output--返回值,在下方用一个变量接收

    as--关键词

    begin

                   set @countSum=(select COUNT(*) fromTblStudent where tsname like @name+'%' and tsage=@age)--用and连接不同形式语句

               select * from TblStudent where tsname like@name+'%' and tsage=@age

    end

    ------调用

    declare @cs int--因为使用赋值的形式,所以定义该变量为output修饰的赋值

    exec usp_personCount @age=19,@name='张',@countSum=@cs output--调用时可以给变量赋值,但都要用=号。也可以直接写,像上方。

    select @cs--output修饰的变量将值付给@cs变量

    --存储过程之提高分数

    create proc usp_UpScore

    @passLine int,--逗号,及格线

    @upScore int,--提升的分数

    @pCount int output--提升的次数

    as

    begin

               set @pCount=0

               declare @numNow int=(select count(*)from tblscore)--班级总人数

                  declare @notNow int=(select count(*) fromtblscore where tenglish<@passline)--不及格总人数

                  while(@notNow>@numNow/2)--while语句判断

                  begin

                     update tblscore set tenglish=tenglish+@upScore--加分就是更新数据

                     set @notNow=(select count(*) fromtblscore where tenglish<@passline)--重新计算不及格人数

                        set @pCount=@pCount+1

                        update tblscore set tenglish=200 wheretenglish>200--避免超过总分

                  end

    end

    --调用,非赋值方法

    declare @newCount int=0

    exec usp_UpScore 150,2,@newCount output

    select @newCount

    --存储过程之分页

    create proc usp_paging

    @page int=0,

    @count int=0,

    @sumPage int=0 output

    as

    begin

               set @sumPage=ceiling((select count(*)fromtblstudent)*1.0/@count)--ceiling向高进位关键词,1.0001进为2

               select * from

               (select *,编号=row_number()over(order by tsname )from tblstudent)as tbl

               where tbl.编号 between (@page-1)*@count+1and @page*@count

    end

    --调用

    declare @newPage int

    exec usp_paging 3,7,@newPage output

    select @newPage

    三:索引

       1.简介:

         (1)与之对应的为全表扫描,对数据进行检索(select)效率最差的是全表扫描,就是一条条的找。

          (2) 如果没有目录,查汉语字典就要一页页的翻,而有了目录只要查询目录即可。为了提高检索的速度,可以为经常进行检索的列添加索引,相当于创建目录。

              创建索引的方式,在表设计器中点击右键,选择“索引/键”→添加→在列中选择索引包含的列。

              使用索引能提高查询效率,但是索引也是占据空间的,而且添加、更新、删除数据的时候也需要同步更新索引,因此会降低Insert、Update、Delete的速度。只在经常检索的字段上(Where)创建索引。

    (*)即使创建了索引,仍然有可能全表扫描,比如like、函数、类型转换等。年龄索引,以姓名查看。

         (3)形式:

           索引

                   相当于字典中的目录

                   加快查询速度

                   在执行增删改的时候降低了速度

          聚集索引

                   相当于字典中拼音目录

                   拼音目录的顺序和数据的顺序是一致的

                   一个表中只能有一个聚集索引。

                   索引的排序顺序与表中数据的物理存储位置是一致的,一般新建主键列后回自动生成一个聚集索引。

         非聚集索引(逻辑上的排序。 )

                   一个表中可以有多个非聚集索引。

                   相当于字典中笔画目录

                   笔画目录的顺序和数据是无关的 

    四:触发器

       1.简介:

    触发器的作用:

    自动化操作,减少了手动操作以及出错的几率。

    触发器是一种特殊类型的存储过程,它不同于前面介绍过的一般的存储过程。【在SQL内部把触发器看做是存储过程但是不能传递参数】

    一般的存储过程通过存储过程名称被直接调用,而触发器主要是通过事件进行触发而被执行。

    触发器是一个功能强大的工具,在表中数据发生变化时自动强制执行。触发器可以用于SQL Server约束、默认值和规则的完整性检查,还可以完成难以用普通约束实现的复杂功能。

    那究竟何为触发器?在SQL Server里面也就是对某一个表的一定的操作,触发某种条件,从而执行的一段程序。触发器是一个特殊的存储过程。

    2.总:作用就是备份和特殊的存储过程

    3.触发器使用的表inserted表和deleted表

          (1)deleted表存放由于执行delete或update语句而要从表中删除的所有行。

    在执行delete或update操作时,被删除的行从激活触发器的表中被移动(move)到deleted

    表,这两个表不会有共同的行。 ----删除时会将删除的数据放在deleted表中,写触发器时,将数据从该表中提取出来,放入新表中。。。inserted表同


    (2)inserted表存放由于执行insert或update语句而要向表中插入的所有行。

    在执行insert或update事物时,新的行同时添加到激活触发器的表中和inserted表中,

    inserted表的内容是激活触发器的表中新行的拷贝。

    说明:update事务可以看作是先执行一个delete操作,再执行一个insert操作,旧的行首先被移动到deleted表,让后新行同时添加到激活触发器的表中和inserted表中。

    4.种类:

    After触发器:

    在语句执行完毕之后触发

    按语句触发,而不是所影响的行数,无论所影响为多少行,只触发一次。

    只能建立在常规表上,不能建立在视图和临时表上。(*)

    可以递归触发,最高可达32级。

    update(列),在update语句触发时,判断某列是否被更新,返回布尔值。

    介绍如何编写after触发器。

    instead of触发器

    用来替换原本的操作

    不会递归触发

    可以在约束被检查之前触发

    可以建在表和视图上(*)

    介绍如何编写instead of 触发器

    5.简单语句:

           --删除的触发器

    select top 0 * into newtblscore fromtblscore--创建一个表结构

    ----

    create trigger tr_deleteScore on tblscore--依据tblscore表创建一个触发器,关键词on

    for delete --声明删除触发器

    as--关键词

    begin

          insert into newtblscore select * from deleted--从deleted表中将数据插入到新建表中

    end

    --使用

    delete from tblscore where tsid=7--删除一条数据

    select * from newtblscore--查看是否成功

      --插入的触发器

    create trigger tr_Records

    on Records

    for insert

    as

           declare@type int,@money money,@id char(4)

           select@type = rType,@money=rMoney,@id=userId from inserted

          

            update bank set balance = balance +@money*@type

            where cId = @id

    --当插入数据的时候就会引发触发器

    insert into Records values(-1,10,'0002')

    6.建议:

        不要太耗时

        避免复杂操作

        注意对多行触发时的处理(游标)

    五:游标(了解,效率低,不常用)

       1.用处:SQL语句是把结果集作为一个整体(整个作为一个单元)来操作的,但有时候我们需要针对结果集中的每条记录(或某部分记录)进行特殊操作,这时就需要用到游标。

            对于及其复杂的子查询,则相对游标性能可能会更高

       2.基本语法

    1.delcare 游标名 cursor[local | global][游标选项] for 查询语句

    2.open 游标名

    3.fetch [next] from 游标名 into @v1,@v2…

    4.判断@@fetch_status状态,继续fetch

    5.close 游标名 –关闭游标

    6.deallocate 游标名 –删除游标中数据库中保存的数据结构(彻底释放资源)

      3.基本原理(了解)

        Local

    局部游标(当前存储过程中或批处理等,类似于c#的局部变量)

    Global

    全局游标(相对于当前连接)

    游标选项:

    FAST_FORWARD:指定启用了性能优化的 FORWARD_ONLY、READ_ONLY 游标。只能使用fetch next

    FORWARD_ONLY :只能向前检索数据。默认选项。

    READ_ONLY:只能读取数据,不能修改。禁止通过该游标进行更新。在 UPDATE 或 DELETE 语句的 WHERECURRENT OF 子句中不能引用游标。

    SCROLL:指定所有的提取选项(FIRST、LAST、PRIOR、NEXT、RELATIVE、ABSOLUTE)均可用。

    STATIC:定义一个游标,以创建将由该游标使用的数据的临时复本。对游标的所有请求都从tempdb 中的这一临时表中得到应答;因此,在对该游标进行提取操作时返回的数据中不反映对基表所做的修改,并且该游标不允许修改。

    KEYSET:指定当游标打开时,游标中行的成员身份和顺序已经固定。对行进行唯一标识的键集内置在tempdb 内一个称为 keyset 的表中。

    DYNAMIC:定义一个游标,以反映在滚动游标时对结果集内的各行所做的所有数据更改。行的数据值、顺序和成员身份在每次提取时都会更改。动态游标不支持ABSOLUTE 提取选项。

    SCROLL_LOCKS:指定通过游标进行的定位更新或删除保证会成功。将行读取到游标中以确保它们对随后的修改可用时,Microsoft SQL Server 将锁定这些行。如果还指定了 FAST_FORWARD,则不能指定SCROLL_LOCKS。

    OPTIMISTIC:指定如果行自从被读入游标以来已得到更新,则通过游标进行的定位更新或定位删除不会成功。当将行读入游标时SQL Server 不会锁定行。相反,SQL Server 使用timestamp 列值的比较,或者如果表没有 timestamp 列,则使用校验和值,以确定将行读入游标后是否已修改该行。如果已修改该行,则尝试进行的定位更新或删除将失败。如果还指定了FAST_FORWARD,则不能指定 OPTIMISTIC。

    TYPE_WARNING:指定如果游标从所请求的类型隐式转换为另一种类型,则向客户端发送警告消息。

    ===============================================================================================================================

    Fetch的选项:

    NEXT :返回下一条记录。紧跟当前行返回结果行,并且当前行递增为返回行。如果FETCH NEXT 为对游标的第一次提取操作,则返回结果集中的第一行。NEXT为默认的游标提取选项。

    PRIOR:返回前一条记录。返回紧邻当前行前面的结果行,并且当前行递减为返回行。如果FETCH PRIOR 为对游标的第一次提取操作,则没有行返回并且游标置于第一行之前。

    FIRST:返回第一条记录。

    LAST:返回最后一条记录。

    ABSOLUTE n :如果n为正数,则返回从第一行开始的行,如果为负数,则返回从最后一行开始的行。

    RELATIVE n:相对于当前行移动,如果n为正数则向前移动,如果n为负数则向后移动。

    ===============

    @@CURSOR_ROWS :返回最后一个游标中的数据的行。动态游标返回为-1

    4.语句:

          --声明游标 cursorfast_forward for后面为要做的事

    declare cur_tblStudent cursor fast_forward forselect * from tblStudent

    --打开游标

    open cur_tblStudent

    --向下移动foreach缩写

    fetch next from cur_tblStudent

    while (@@fetch_status=0)

    begin

                 fetch next from cur_tblStudent

    end

    --关闭游标

    close cur_tblStudent

    --释放缓存

    deallocate cur_tblStudent

         --查询TblTeacher表中的数据,为每个老师增加工资。每个老师的工资增长数额不同,具体增加额度在TblTeacherSalary表中

         declare cur_TblTeacherSalary cursor forward_only

    for

    select tTId,reward from TblTeacherSalary

    declare @teachId int

    declare @treward money

    open cur_TblTeacherSalary

    fetch next from cur_TblTeacherSalary into@teachId,@treward

    while @@fetch_status=0

    begin--将查询的ttid,reward赋值给两个变量

                update TblTeacher set tTSalary=tTSalary+@trewardwhere ttid=@teachId

                fetch next from cur_TblTeacherSalary into@teachId,@treward

    end

    close cur_TblTeacherSalary

    deallocate cur_TblTeacherSalary

    六:拾遗

       1.动态SQL

    String sql=“select * from @tbName”与string sql=“execute(‘select * from ’+@tbName)”;

    尽量避免使用,会有安全问题(SQL注入),当遇到一些非常复杂的问题时自然会考虑到用动态sql,所以一般不用去主动考虑。

    sp_executesql动态执行sql。(其实还是带参数的sql语句),exec(‘sql’)才是真正的动态sql。

       2.跨数据库访问表

         select * from [LYY\SQLEXPRESS].Person.dbo.Employees

          --select * from 【服务器名称】.数据库名称.表名

    七:数据库的设计

    1. 设计中的一些术语

    实体,任何一个对象,表中的一行。

    实体集,对象的集合,一张表。

    属性,指实体的某个属性,即表的列。

    联系(关系),实体与实体间的关系(主外键关系)

    联系集(关系集),多个实体间的关系的集合,同类联系的集合。(存储关系的表,联想QQ好友关系表)

    E-R图(实体-关系图),用图来描述表间关系的图。(用画图的方式来表示表和表之间的关系。)

    映射基数:一个实体通过联系集能同时与多少个其他实体关联。假设有实体集A,与实体集B。

    一对一,A中的一个实体最多能与B中的一个实体对应,反之一样。

    一对多,A中的一个实体能与B中的任意多个实体对应,B中的一个实体则最多只能与A中的一个实体对应。

    多对一,A中一个实体至多能与B中的一个实体对应,B中的一个实体则能与A中的任意多个实体对应。

    多对多,A中的一个实体能与B中的任意实体对应,反之一样。

    码(主键,属性集),唯一标示一个实体。

     2.设计中注意事项

     数据库设计过程,一般对于小型应用,只要一个人对应用需求非常了解则可以设计数据库,包括表、属性、关系、约束等。但对于比较复杂的应用,则很难由一个人了解所有的数据需求,这时就要通过不断了解需求,通过E-R图与客户反复确定需求并最终确定数据库设计。

    数据库设计阶段一般只关心如何描述数据及其相互关系,不关心数据存储细节。

    数据库设计中要避免的问题:

    数据冗余,浪费存储空间等问题。

    数据库设计不完整,比如,两种信息存储在了一个表中,没有关系表,会导致后续无法正常添加数据等各种问题。

    3.设计过程一般包括

    (1)需求分析  分析客户的业务需求.

    (2)概要设计  根据需求分析阶段的数据画E-R图,通过E-R图和用户沟通,如此反复最终确定实际需求。

    (3)详细设计  将E-R图转换为表,并使用三大范式审核。

    (4)物理设计  选择合适的数据库, 进行物理实现:建库、建表、加约束等。

    (5)实施与维护

    4.技巧;

        1对多时候,需要在N方加入外键。在学生表中增加“班级”中的关键字“班号”作为外部关键字 

        当多对多关系时,需要把关系也创建成一个表

    5.数据库设计(审核)三大范式(3nf)

     (1)第一大范式

              第一范式的目标是确保每列的原子性(不可再分性)

    如果每列都是不可再分的最小数据单元(也称为最小的原子单元),则满足第一范式(1NF)

    数据冗余2nf(数据是否有冗余)

    列没依赖主键

    例如,如果关于员工的关系中有一个工资属性,而工资又由更基本的两个数据项基本工资和岗位工资组成,则这个员工的关系模式就不满足1NF。员工表(员工号,姓名,工资)进行分解,使其满足1NF条件。

    员工表(员工号,姓名,基本工资,岗位工资)

    (2)第二大范式

    如果一个关系满足1NF,并且除了主键以外的其他列,都依赖与该主键,则满足第二范式(2NF),一个表只描述一件事情。

    第二范式要求每个表只描述一件事情

    (3)第三大范式

    如果一个关系满足2NF,并且除了主键以外的其他列都不传递依赖于主键列,则满足第三范式(3NF)

    第三范式要求其它列必须直接依赖于主键

    (4)作用:

    使用三大范式减少了数据冗余,但是牺牲了查询性能

    所以有时为了性能,需要做适当折中,适当牺牲规范化的要求,来提高数据库的性能。

    数据库设计看似简单,甚至同时会有多种设计可供选择,但当有大量复杂实体时(比如,银行,电信,电厂等业务),并非一件简单的事情(同时要考虑数据存储以及之间的各种关系及扩展问题),所以还需要大家认真对待。

    八:SQL面试题

    --1.列出EMPLOYEES表中各部门的:部门编号,最高工资,最低工资

    --2.列出EMPLOYEES表中各部门的:部门编号、部门名称、最高工资、最低工资

    --3.列出EMPLOYEES表中各部门中'职员'(Employee_job为'职员')的:最低工资,最高工资和部门Id

    --4.对于EMPLOYEES中最低工资小于1000的部门,列出EMPLOYEE_JOB为'职员'的:部门编号,最低工资,最高工资

    --5.根据部门编号由高到低,工资由低到高,列出每个员工的姓名,部门号,工资

    --6.列出'吴用'所在部门中每个员工的姓名与部门号

    --7.列出每个员工的姓名,头衔,部门号,部门名

    --8.列出EMPLOYEES中头衔为'职员'的员工的姓名,工作,部门号,部门名

    --9.对于DEPARTMENTS表中,列出所有:部门名称,部门编号,以及该部门的:员工姓名与头衔

    --10.列出工资高于本部门工资平均水平的员工的部门编号,姓名,工资,并且按部门编号排序。

    --11.对于EMPLOYEES,列出各个部门中工资高于本部门平均水平的员工数和部门号,按部门号排序

    --12.请找出部门中具有两人以上,员工工资大于所在部门平均工资的:部门的id与这些人的人数。

    --分解:

    --1>.部门中有人的工资比部门的平均工资还高

    --2>并且这些人在人以上

    --3>查询出这些部门Id,与工资高于部门平均工资的人的人数。

    --13.对于EMPLOYEES中低于自己工资至少5人的员工,列出其部门号,姓名,工资,以及工资少于自己的人数

    -----------------------------答案------------------------------

    表结构:

    create database MyCompany

    go

    use MyCompany

    go

    create table Departments

    (

           Department_IDint identity(1,1) primary key,

           Department_Namenvarchar(50),

    )

    go

    create table Employees

    (

           Employee_Idint identity(1,1) primary key,

           Employee_Namenvarchar(50),

           Employee_Jobnvarchar(50),

           Salarymoney,

           Department_Idint foreign key references Departments(Department_ID)

    )

    Go

    --------------------------------------------插入数据----------------------------------------------------------------------------------

    ----------------------------------部门表-------------------------------------------------------------------

    SET IDENTITY_INSERT departments ON

    insert departments(Department_ID,Department_Name)values( 1             ,    N'财务部'                                                                                                                                                                                                                                                          )

    insert departments(Department_ID,Department_Name)values( 2             ,    N'行政部'                                                                                                                                                                                                                                                          )

    insert departments(Department_ID,Department_Name)values( 3             ,    N'开发部'                                                                                                                                                                                                                                                          )

    insert departments(Department_ID,Department_Name)values( 4             ,    N'市场部'                                                                                                                                                                                                                                                          )

    SET IDENTITY_INSERT departments OFF

    ------------------------=============================员工表

    SET IDENTITY_INSERT employees ON                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               insert employees(Employee_Id,Employee_Name,Employee_Job,Salary,Department_Id)values( 1           ,    N'曹操'   ,    N'组长'    ,    20000.00    ,    1       )

    insert employees(Employee_Id,Employee_Name,Employee_Job,Salary,Department_Id)values( 2  ,    N'刘备'    ,    N'经'  ,    30000.00   ,    3     )

    insert employees(Employee_Id,Employee_Name,Employee_Job,Salary,Department_Id)values( 3 ,N'诸葛'  , N'CEO' , 10000.00    ,    2    )

    SET IDENTITY_INSERT employees OFF

    ------------------------参考答案:-------------------------------------------------------

    select * from departments

    select * from Employees

    --参考答案:

    --1.列出EMPLOYEES表中各部门的:部门编号,最高工资,最低工资

    select

           department_id,

           max(salary)最高工资,

           min(salary)最低工资

    from employees

    group by department_id

    --2.列出EMPLOYEES表中各部门的:部门编号、部门名称、最高工资、最低工资

    select

           emp.department_id,

           dept.department_name,

           max(salary)最高工资,

           min(salary)最低工资

    from employees as emp

    inner join departments as dept on emp.department_id=dept.department_id

    group by emp.department_id,dept.department_name

    --3.列出EMPLOYEES表中各部门中'职员'(Employee_job为'职员')的:最低工资,最高工资和部门Id

    select

           department_id,

           最高工资=max(salary),

           最低工资=min(salary)

    from Employees

    where employee_job='职员'

    group by department_id

    --4.对于EMPLOYEES中最低工资小于的部门,列出EMPLOYEE_JOB为'职员'的:部门编号,最低工资,最高工资

    --方案:

    select

           department_id,

           min(salary)最低工资,

           max(salary)最高工资

    from employees

    where employee_job='职员'

    group by department_id

    having min(salary)<1000

    --方案:

    select Department_Id,max(salary) 最高工资,min(salary) 最低工资 from dbo.Employees where Department_Idin

    (select Department_Id from dbo.Employees groupby Department_Id having min(Salary)<1000)

    group by Department_Id

    --方案:

    select

           department_id,

           max(salary)最高工资,

           min(salary)最低工资

    from dbo.Employees emp

    where (select min(salary) from employees asemp_sub where emp_sub.department_id=emp.department_id)<1000

    group by emp.department_id

    --5.根据部门编号由高到低,工资由低到高,列出每个员工的姓名,部门号,工资

    select employee_name,department_id,salary fromemployees order by department_id desc,salary asc

    --6.列出'吴用'所在部门中每个员工的姓名与部门号

    select employee_name,department_id from employees

    where department_id in (select department_idfrom employees where employee_name='吴用')

    --7.列出每个员工的姓名,头衔,部门号,部门名

    select

           emp.employee_name,

           emp.employee_job,

           emp.department_id,

           dept.department_name

    from employees as emp

    inner join departments as dept on emp.department_id=dept.department_id

    --8.列出EMPLOYEES中头衔为'职员'的员工的姓名,工作,部门号,部门名

    select

           emp.employee_name,

           emp.employee_job,

           emp.department_id,

           dept.department_name

    from employees as emp

    inner join departments as dept on emp.department_id=dept.department_id

    where emp.employee_job='职员'

    --9.对于DEPARTMENTS表中,列出所有:部门名称,部门编号,以及该部门的:员工姓名与头衔

    select

           dept.department_name,

           dept.department_id,

           emp.employee_name,

           emp.employee_job

    from departments as dept

    left join employees as emp on emp.department_id=dept.department_id

    --10.列出工资高于本部门工资平均水平的员工的部门编号,姓名,工资,并且按部门编号排序。

    --方案:

    select * from employees as emp

    inner join (select department_id,avg(salary)as avg_salary from employees group by department_id) as tmpdata

    on emp.department_id=tmpdata.department_id

    where emp.salary>tmpdata.avg_salary

    --方案:(相关子查询)

    select * from employees as emp

    where

    exists(

    select department_id,avg(salary) as avg_salaryfrom employees as emp_sub group by department_id

    having emp_sub.department_id=emp.department_idand emp.salary>avg(salary)

    )

    --方案:(相关子查询)

    select

           a.employee_id,

           a.DEPARTMENT_IDas 部门号,

           a.EMPLOYEE_NAMEas 姓名,

           a.SALARYas 工资

    from EMPLOYEES as a

    where

    a.SALARY >(select avg(SALARY) from EMPLOYEESas b where a.DEPARTMENT_ID = b.DEPARTMENT_ID)

    order by a.DEPARTMENT_ID

    --11.对于EMPLOYEES,列出各个部门中工资高于本部门平均水平的员工数和部门号,按部门号排序

    select

                  emp.department_idas 部门编号,

                  count(*)as 员工数

    from Employees as emp

    where emp.salary > (select avg(salary) fromemployees emp_sub where emp_sub.department_id=emp.department_id)

    group by emp.department_id

    order by emp.department_id

    --12.请找出部门中具有两人以上,员工工资大于所在部门平均工资的:部门的id与这些人的人数。

    --分解:

    --1>.部门中有人的工资比部门的平均工资还高

    --2>并且这些人在人以上

    --3>查询出这些部门Id,与工资高于部门平均工资的人的人数。

    select

                  emp.department_idas 部门编号,

                  count(*)as 员工数

    from Employees as emp

    where emp.salary > (select avg(salary) fromemployees emp_sub where emp_sub.department_id=emp.department_id)

    group by emp.department_id

    having count(*) >2

    order by emp.department_id

    --13.对于EMPLOYEES中低于自己工资至少人的员工,列出其部门号,姓名,工资,以及工资少于自己的人数

    --步骤:计算小于自己工资的人数的总和。

    select

                  employee_name姓名,

                  salary工资,

                  小于自己工资的人数=(select count(*) from employees as emp_sub where emp_sub.salary<emp.salary)

    from employees as emp

    --步骤:筛选出工资比自己低的人数中小于的

    select

                  employee_name姓名,

                  salary工资,

                  小于自己工资的人数=(select count(*) from employees as emp_sub where emp_sub.salary<emp.salary)

    from employees as emp

    where (select count(*) from employees as emp_subwhere emp_sub.salary<emp.salary)<5

    展开全文
  • 文章目录前言一、数据库连接池——DBCP1.DBCP简绍2.DBCP的使用步骤2.1 添加jar2.2 在src下创建DBCP的配置文件dbcpconfig.properties2.3 编写操作DBCP的工具类DBCPUtil2.4 在数据库中建立学生表并插入数据2.5 创建...

    前言

    在JDBC编程中,每次创建和断开Connection对象都会消耗一定的时间和IO资源。 这是因为在Java程序与数据库之间建立连接时,数据库端要验证用户名和密码并为该连接分配资源,而程序则要把代表连接Connection对象等加载到内存中,所以建立数据库连接的开销很大。尤其是在大量的并发访问时,频繁地创建、断开数据库 连接势必会影响数据库的访问效率,甚至导致数据库崩溃。
    为了解决该类问题的发生诞生了数据库连接池技术。数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用现有的数据库连接,而不是每次都重新建立连接,在本次博客中将简绍两种最常见的数据库连接池——DBCP和C3P0

    一、数据库连接池——DBCP

    1.DBCP简绍

    DBCP(DataBase Connection Pool)由Apache研发,而且Tomcat的连接池也正是采用DBCP实现的,该数据库连接池既可与应用服务器整合使用,也可由应用程序独立使用

    2.DBCP的使用步骤

    2.1 添加jar包

    commons-dbcp.jar
    commons-pool.jar
    mysql-connector-java-5.0.8-bin.jar

    导包位置如下:
    在这里插入图片描述

    2.2 在src下创建DBCP的配置文件dbcpconfig.properties

    如下图所示dbcpconfig.properties配置的级别与src同级,文件名建议直接复制
    在这里插入图片描述
    dbcpconfig.properties里面的内容如下(只需要修改当前使用的数据库及数据库的use和password)

    #<!-- 连接设置 -->
    driverClassName=com.mysql.jdbc.Driver
    url=jdbc:mysql://localhost:3306/数据库名字
    username=root
    password=你的密码
    
    #<!-- 初始化连接 -->
    initialSize=10
    
    #<!-- 最大连接数量 -->
    maxActive=50
    
    #<!-- 最大空闲连接 -->
    maxIdle=20
    
    #<!-- 最小空闲连接 -->
    minIdle=5
    
    #<!-- 超时等待时间(单位毫秒) -->
    maxWait=50000
    
    #<!-- 编码方式 -->
    connectionProperties=useUnicode=true;characterEncoding=utf8
    
    ##<!-- 指定由连接池所创建的连接自动提交 -->
    defaultAutoCommit=true
    
    #<!-- 指定由连接池所创建的连接的事务级别 -->
    defaultTransactionIsolation=REPEATABLE_READ
    

    2.3 编写操作DBCP的工具类DBCPUtil

    代码如下(DBCPUtil工具类):

    import java.sql.Connection;
    import java.sql.ResultSet;
    import java.sql.Statement;
    import java.util.Properties;
    
    import javax.sql.DataSource;
    
    import org.apache.commons.dbcp.BasicDataSourceFactory;
    
    public class DBCPUtil {
        private static DataSource dataSource = null;
        //创建数据库连接池
        static{
            Properties properties = new Properties();
            try {
                ClassLoader classLoader=DBCPUtil.class.getClassLoader();
                properties.load(classLoader.getResourceAsStream("dbcpconfig.properties"));
                dataSource = BasicDataSourceFactory.createDataSource(properties);
            } catch (Exception e) {
                throw new ExceptionInInitializerError("DBCP始化错误,请检查配置文件");
            }
        }
        //创建连接
        public static Connection getConnection(){
            try {
                return dataSource.getConnection();
            } catch (Exception e) {
                throw new RuntimeException("数据库连接错误");
            }
        }
        //释放连接
        public static void release(Connection connection, Statement statement, ResultSet resultSet) {
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                resultSet = null;
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                statement = null;
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                connection = null;
            }
        }
    }
    

    2.4 在数据库中建立学生表并插入数据

    代码如下(创建学生表的MySQL命令):

    -- 创建表
    DROP TABLE IF EXISTS student;
    CREATE TABLE student (
      studentid INT,
      studentname VARCHAR(50)
    );
    
    -- 插入数据
    INSERT INTO student (studentid,studentname) VALUES (1,"张三");
    INSERT INTO student (studentid,studentname) VALUES (2,"李四");
    INSERT INTO student (studentid,studentname) VALUES (3,"王五");
    INSERT INTO student (studentid,studentname) VALUES (4,"赵六");
    INSERT INTO student (studentid,studentname) VALUES (5,"小明");
    

    2.5 创建一个学生类(里面的元素与学生表的字段信息类型相同)

    代码如下(学生类):

    public class Student {
    	private int studentID;// 根据学生表创建学生ID
    	private String studentName; // 根据学生表创建学生姓名
    
    	public Student() {
    
    	}
    
    	public Student(int studentID, String studentName) {
    		this.studentID = studentID;
    		this.studentName = studentName;
    	}
    
    	public int getStudentID() {
    		return studentID;
    	}
    
    	public void setStudentID(int studentID) {
    		this.studentID = studentID;
    	}
    
    	public String getStudentName() {
    		return studentName;
    	}
    
    	public void setStudentName(String studentName) {
    		this.studentName = studentName;
    	}
    	// 重写toString方法方便后续测试结果直观展示
    	@Override
    	public String toString() {
    		return "Student [studentID=" + studentID + ", studentName=" + studentName + "]";
    	}
    
    }
    

    2.6 使用DBCP测试

    代码如下(DBCP测试):

    public class Test01 {
    
    	public static void main(String[] args) {
    		Connection connection = null; 
    		PreparedStatement preparedStatement = null;
    		ResultSet resultSet = null;
    		try {
    			connection = DBCPUtil.getConnection();// 调用工具类DBCPUtil创建连接
    			preparedStatement = connection.prepareStatement("select * from student");// 创建preparedStatement及sql预编译
    			resultSet = preparedStatement.executeQuery(); 
    			while(resultSet.next()) {
    				Student student = new Student();
    				int id = resultSet.getInt("studentid");
    				String name = resultSet.getString("studentname");
    				student.setStudentID(id);
    				student.setStudentName(name);
    				System.out.println(student);
    			}
    			
    		} catch (Exception e) {
    		}finally {
    			DBCPUtil.release(connection, preparedStatement, resultSet);// 调用工具类DBCPUtil里面的方法释放资源
    		}
    	}
    }
    
    

    代码如下(DBCP测试输出):

    Student [studentID=1, studentName=张三]
    Student [studentID=2, studentName=李四]
    Student [studentID=3, studentName=王五]
    Student [studentID=4, studentName=赵六]
    Student [studentID=5, studentName=小明]
    

    二、数据库连接池——C3P0

    1.C3P0简绍

    C3P0是目前最流行的开源数据库连接池之一,它支持 JDBC2和JDBC3的标准规范,易于扩展并且性能优越,著名的开源框架Hibernate和 Spring使用的数据源正是C3P0。

    2.C3P0的使用步骤

    2.1 添加jar包

    c3p0-0.9.1.2.jar
    mysql-connector-java-5.0.8-bin.jar

    导包位置如下:
    在这里插入图片描述

    2.2 在src下创建C3P0的配置文件c3p0-config.xml

    如下图所示c3p0-config.xml配置的级别与src同级,文件名建议直接复制
    在这里插入图片描述

    c3p0-config.xml里面的内容如下(只需要修改当前使用的数据库及数据库的use和password)

    <?xml version="1.0" encoding="UTF-8"?>
    <c3p0-config>
      <default-config>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/你当前使用的数据库名</property>
        <property name="user">使用者名字</property>
        <property name="password">密码</property>
        <property name="initialPoolSize">15</property>
        <property name="maxIdleTime">40</property>
        <property name="maxPoolSize">150</property>
        <property name="minPoolSize">20</property>
      </default-config>
    </c3p0-config>
    

    2.3 编写操作C3P0的工具类C3P0Util

    代码如下(C3P0Util工具类):

    import java.sql.Connection;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.sql.Statement;
    import javax.sql.DataSource;
    import com.mchange.v2.c3p0.ComboPooledDataSource;
    
    public class C3P0Util {
    
        //创建数据库连接池
        private static DataSource dataSource = new ComboPooledDataSource();
    
        //创建连接
        public static Connection getConnection(){
            try {
                return dataSource.getConnection();
            } catch (SQLException e) {
                throw new RuntimeException("获取数据库连接失败");
            }
        }
    
        //释放连接
        public static void release(Connection connection, Statement statement, ResultSet resultSet) {
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                resultSet = null;
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                statement = null;
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                connection = null;
            }
        }
    }
    

    2.4 在数据库中建立学生表并插入数据

    代码如下(创建学生表的MySQL命令):

    -- 创建表
    DROP TABLE IF EXISTS student;
    CREATE TABLE student (
      studentid INT,
      studentname VARCHAR(50)
    );
    
    -- 插入数据
    INSERT INTO student (studentid,studentname) VALUES (1,"张三");
    INSERT INTO student (studentid,studentname) VALUES (2,"李四");
    INSERT INTO student (studentid,studentname) VALUES (3,"王五");
    INSERT INTO student (studentid,studentname) VALUES (4,"赵六");
    INSERT INTO student (studentid,studentname) VALUES (5,"小明");
    

    2.5 创建一个学生类(里面的元素与学生表的字段信息类型相同)

    代码如下(学生类):

    public class Student {
    	private int studentID;// 根据学生表创建学生ID
    	private String studentName; // 根据学生表创建学生姓名
    
    	public Student() {
    
    	}
    
    	public Student(int studentID, String studentName) {
    		this.studentID = studentID;
    		this.studentName = studentName;
    	}
    
    	public int getStudentID() {
    		return studentID;
    	}
    
    	public void setStudentID(int studentID) {
    		this.studentID = studentID;
    	}
    
    	public String getStudentName() {
    		return studentName;
    	}
    
    	public void setStudentName(String studentName) {
    		this.studentName = studentName;
    	}
    	// 重写toString方法方便后续测试结果直观展示
    	@Override
    	public String toString() {
    		return "Student [studentID=" + studentID + ", studentName=" + studentName + "]";
    	}
    
    }
    

    2.6 使用C3P0进行测试

    代码如下(C3P0测试):

    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    
    public class Test01 {
    
    	public static void main(String[] args) {
    		Connection connection = null;
    		PreparedStatement preparedStatement = null;
    		ResultSet resultSet = null;
    		try {
    			connection = C3P0Util.getConnection();
    			preparedStatement = connection.prepareStatement("select * from student");
    			resultSet = preparedStatement.executeQuery();
    			while(resultSet.next()) {
    				Student student = new Student();
    				int id = resultSet.getInt("studentid");
    				String name = resultSet.getString("studentname");
    				student.setStudentID(id);
    				student.setStudentName(name);
    				System.out.println(student);
    			}
    			
    		} catch (Exception e) {
    		}finally {
    			C3P0Util.release(connection, preparedStatement, resultSet);
    		}
    	}
    }
    
    

    代码如下(C3P0测试输出):

    三月 08, 2021 7:40:23 下午 com.mchange.v2.log.MLog <clinit>
    信息: MLog clients using java 1.4+ standard logging.
    三月 08, 2021 7:40:24 下午 com.mchange.v2.c3p0.C3P0Registry banner
    信息: Initializing c3p0-0.9.1.2 [built 21-May-2007 15:04:56; debug? true; trace: 10]
    三月 08, 2021 7:40:24 下午 com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource getPoolManager
    信息: Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> 1hge2veag4w6lqb1ochemm|4fca772d, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> 1hge2veag4w6lqb1ochemm|4fca772d, idleConnectionTestPeriod -> 0, initialPoolSize -> 15, jdbcUrl -> jdbc:mysql://localhost:3306/db5, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 40, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 150, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 20, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ]
    Student [studentID=1, studentName=张三]
    Student [studentID=2, studentName=李四]
    Student [studentID=3, studentName=王五]
    Student [studentID=4, studentName=赵六]
    Student [studentID=5, studentName=小明]
    

    总结

    其实不难看出这两种数据库连接池无论是使用过程、步骤甚至连导包都非常相识,基本只要掌握一个另一个也基本就懂了

    展开全文
  • 数据库-简介

    2018-05-28 20:00:55
    MySQL数据库 数据库 就是一个文件系统,访问数据的时候需要通过标准的SQL语言来完成。 关系型的数据库 保存的是实体(用户)与实体(商品,订单等)的关系。 ER模型图 常见的数据库 Oracle ...
  • 数据库底层原理

    千次阅读 2019-04-24 21:44:47
    看到一篇很不错的数据库文章,拿过来分享一下: 一提到关系型数据库,我禁不住想:有些东西被忽视了。关系型数据库无处不在,而且种类繁多,从小巧实用的 SQLite 到强大的 Teradata 。但很少有文章讲解数据库是如何...
  • 数据库的访问实现过程

    千次阅读 2016-03-29 21:53:29
    数据库,现在社会中大家都知道是一个非常重要的数据管理软件系统,在这里我们进行一次Access数据库的访问操作,在正式介绍之前,我们先了接一下数据库访问的过程,在数据库访问中,根据最普遍的想法,需知道的构件...
  • 数据库技术-数据库命名与设计规范

    千次阅读 2014-01-18 17:42:15
    数据库技术-数据库命名与设计规范  数据库开发历史上一直使用一个有点神秘的系统命名数据库表和字段。最初的数据库管理系统(DBMS ) ,这些命名方案的限制的结果已成为惯例和传统。然而,随着数据库应用程序变得...
  • 数据库原理剖析

    2016-08-23 16:23:33
    一提到关系型数据库,我禁不住想:有些东西被忽视了。关系型数据库无处不在,而且种类繁多,从小巧实用的 SQLite 到强大的 Teradata 。但很少有文章讲解数据库是如何工作的。你可以自己谷歌/百度一下『关系型数据库...
  • 关系数据库如何工作

    千次阅读 2021-05-15 10:54:02
    当涉及到关系数据库时,我不禁会以为有些东西丢失了。它们无处不在。有许多不同的数据库:从小型且有用的SQLite到功能强大的Teradata。但是,只有少数几篇文章解释了数据库的工作方式。您可以自己在Google上搜索...
  • Oracle数据库使用教程

    千次阅读 2020-04-14 15:27:39
    数据库(Database,简称DB):存放数据的仓库 数据库管理系统(Database Management system,简称DBMS),比如Oracle,SQL,Server,DB2等 数据库管理员(Database Administrator,简称DBA),一种职业,专门从事数据库管理,维护...
  • 数据库的原理

    2017-02-28 14:02:53
    数据库的原理,你最需要了解的基础知识 目录视图 摘要视图 订阅 CSDN日报20170226——《你离心想事成只差一个计划》 程序员2月书讯  " target="_blank">【招募】Pytho
  • MySQL数据库详解

    2018-10-28 22:06:25
    安装mysql 可以前往oracle官网下载,装后就可以启动服务mysqld了 2. mysql安装后初始密码在/var/log/mysqld.log,可以用下面命令获取密码(必须是启动服务后才会有) awk '/temporary password/{print $11}' /...
  • 数据库——事务

    2016-07-13 09:32:17
    数据库事务(Database Transaction)  是指作为单个逻辑工作单元执行的一系列数据操作,要么完全地执行,要么完全地不执行。事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的...
  • 一文看懂数据库原理

    万次阅读 多人点赞 2018-06-25 16:16:02
    一提到关系型数据库,我禁不住想:有些东西被忽视了。关系型数据库无处不在,而且种类繁多,从小巧实用的 SQLite 到强大的 Teradata 。但很少有文章讲解数据库是如何工作的。你可以自己谷歌/百度一下『关系型数据库...
  • 数据库对象命名参考

    千次阅读 2015-09-06 16:06:17
    数据库对象命名参考 本文是一个参考,不是一个规范,更不是一个标准。它仅代表了我个人的观点和建议,并只考虑了通常条件下的规则,你可以根据实际情况随意修改它。 引言 编码规范是一个优秀程序员的必备素质,...
  • spark连接数据库

    2019-10-15 13:56:27
    使用用户名和密码远程连接MongoDB数据库,用Java和Scala连接其实原理相同,都是JDBC,用MongoDB的连接驱动,只是语法上稍有区别而已,而在类、方法的调用上一模一样。 在此,分享一下Scala连接Mon...
  • 对于一个大项目来讲,数据库的设计命名规范是很重要的一个环节,好的表设计,让人看得很舒服,一看就明白是什么意思了,下面看到一篇很不错的数据库对象命名参考文档,所以整理分享给大家。 引言 编码规范是一个...
  • oracle数据库升级

    千次阅读 2009-12-11 09:23:00
    Oracle 9i补丁集9.2.0.6安装指南声明:以下内容是根据p3948480_9206_WINNT.zip里的README.html里的内容整理而得的,仅供参考。本文没有包括p4269928_9206_WINNT.zip的内容一、下载补丁集我是在...在数据库服务器上以Adm