写法一:
SELECT a.student_name,a.student_age,b.class_name
FROM t_student,t_class
WHERE a.id = b.id;
写法二:
SELECT a.student_name,a.student_age,b.class_name
FROM t_student a JOIN t_class b
ON WHERE a.id = b.id; (这种写法也叫内连接)
写法一:
SELECT a.employee_name,a.employee_salary,b.salary_level
FROM t_employee a,t_job_grade b
WHERE a.employee_salary between b.lowest_sal and b.highest_salary
写法二:还是使用ON的方式,比较简单我就不写了,其实这里的SQL并不重要,重要的只是让你们了解这几个连接的含义;
写法一:
SELECT emp.employee_id,emp.employee_name,mgr.employee_id,mgr.employee_name
FROM t_employee emp,t_employee mgr
WHERE emp.manager_id = mgr.employee_id
写法二:通过ON进连接,比较简单不写了
在SQL中,连接方式总共有7种:
写法一:
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp,t_dept dept
WHERE emp.dept_id = dept.id
写法二:
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp INNER JOIN t_dept dept
ON emp.dept_id = dept.id
inner 可以省略。
在讲解外连接之前,我需要先讲一下SQL语法的协议,SQL是一门语法,关于这门语法市面上主流的有两个版本的协议:SQL92与SQL99,SQL92定义了关于SQL的语法,如何实现联表查询,等一切跟SQL有关系的东西,都是由SQL92进行定义的,SQL92语法简单但是不利于人眼理解,SQL99语法复杂但是更便于程序员理解,SQL99是对SQL92的进一步优化,在这两个版本中,关于外连接的语法实现有区别,所以请看这两个不同的版本分别来实现外连接查询
现在我有一个员工表,有117条记录,其中有1个员工没有部门,此时我用正常的连表查询,是无法将没有部门的这个员工查出来的,如下就是正常的SQL语法,也就是内连接语法:
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp,t_dept dept
WHERE emp.dept_id = dept.id
图解:
下图员工表多了一个员工,相当于比部门表多出来一部分,要想将多的那个员工也查出来,SQL92的设计者们就想办法把部门表补起来,两边不就一样长了吗,一样长再查询时,就可以把左边多的员工查出来了呀,同时多查出来的这个员工,正好它的部门也会是null,因为我补起来的这个部门,根本就没有;
那如何补呢?在缺少的那个表处添加一个(+)即可,如下:
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp,t_dept dept
WHERE emp.dept_id = dept.id(+)
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp LEFT JOIN t_dept dept
ON emp.dept_id = dept.id
注意:mysql不支持SQL92语法,只支持SQL99,但是Oracle两种语法都支持
现在我有一个员工表,有117条记录,其中有1个员工没有部门,但是还有一个部门没有员工,此时我用正常的连接查询,是无法将员工的这个部门查出来的:
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp,t_dept dept
WHERE emp.dept_id(+) = dept.id
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp RIGHT JOIN t_dept dept
ON emp.dept_id = dept.id
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp FULL JOIN t_dept dept
ON emp.dept_id = dept.id
但是一旦你将这个写法拿到mysql上执行,就会提示语法错误,这是因为Mysql虽然支持SQL99语法,但是很蛋疼的是,它不支持满连接的写法,Oracle却支持,此时就要学习Mysql中实现满连接的方式了:
注意看下图:UNION的意思是左表和右表的数据我全都有,同时中间重叠的部分,我只要一份;
这个UNION底层会做什么呢:首先它会将左表中的数据全查出来,右表中的数据也会查出来,然后将两个表相叠加,中间产生的重叠部分会进行一次去重操作,只保留一份重复的数据:
UNION ALL操作符:
看下图,注意对比跟上图的区别,下图想表达的意思是:UNION ALL将左表跟右表的数据都查出来后,直接叠加放在一起,重复的部分就让它重复,反正我UNION ALL就是不对中间重复的这部分去重,这样的好处是啥?当然是减少了一次去重操作,效率更高了;
注意:在实际的开发过程中:由于UNION ALL所需要的资源更少,如果你明确知道两表合并后的数据并不会出现重叠的部分,或者你的业务需求不需要取出重复的部分,那么我们建议你使用UNION ALL以提高查询效率,同时日常开发中,确实也是UNION ALL用的更多;
注意:刚才我们不是说到使用UNION ALL会出现重叠的数据吗,那我们如何操作呢?
如果你直接将A的数据跟B的数据进行UNION ALL,确实会多出重复数据,就如下图:
但你可以另辟蹊径,我让下图这哥俩UNION ALL,不就可以拼凑出一个满外连接吗:
所以,写法二来了:
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp LEFT JOIN t_dept dept
ON emp.dept_id = dept.id
UNION ALL
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp RIGHT JOIN t_dept dept
ON emp.dept_id = dept.id
WHERE emp.dept_id != dept.id;
注意1:你别看使用UNION ALL的代码量一大串,但他就是比你直接使用UNION的效率更高;
注意2:看下图,UNION ALL上下查询语句的字段名必须相同,字段类型也必须相同,字段个数也必须相同,
因为这相当于两个结果集合并,要进行合并,一定得保证结果集中的字段个数,字段类型,字段名完全相同;
这个图相当于是先左外连接,再将中间部分去掉,你使用WHERE条件过滤一下就好了,所以SQL如下:
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp LEFT JOIN t_dept dept
ON emp.dept_id = dept.id
WHERE emp.dept_id != dept.id;
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp RIGHT JOIN t_dept dept
ON emp.dept_id = dept.id
WHERE emp.dept_id != dept.id;
思路:使用下面两个进行UNION ALL就可以了
写法:
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp RIGHT JOIN t_dept dept
ON emp.dept_id = dept.id
WHERE emp.dept_id != dept.id;
UNION ALL
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp LEFT JOIN t_dept dept
ON emp.dept_id = dept.id
WHERE emp.dept_id != dept.id;
拓展:下面这种写法也是可以的
自然的单词是NATURAL,连接的单词是JOIN,所以自然连接就叫NATURAL JOIN
在之前的学习中,我们的连接条件都是一个,比如下图:
ON后面跟着的就是连接条件,这里的连接条件只有一个,那多个连接条件如何写呢?
我在ON后面又用AND多加了一个连接条件,这就是多连接条件,只有满足
emp.dept_id = dept.id AND emp.manager_id = dept.manager_id 的数据才两表之间的重叠部分;
SELECT emp.employee_id,emp.employee_name,dept.name
FROM t_employee emp NATURAL JOIN t_dept dept
我们会发现,自然连接的语法,跟我们刚才写的多连接查询的结果是一样的,为什么呢?
这是因为SQL99的自然连接会帮你自动找到两张表中所有相同的自然,然后进行等值连接,
可以看到上图:我们的员工表和部门表中有两个完全相同的字段,manager_id和department_id,
所以你使用自然连接,跟你直接使用多连接查询时一样的等价的(注意,我这个图中是dept_id,这是我写错了,应该是department_id,但是我懒,我不想改了,你自己能懂意思就好)
自然连接的缺点:不灵活,你了解自然连接就行,还是建议自己写连接条件;
USING是用来替换连接条件ON的,在什么情况下能替换呢?看下图:
当你的连接条件,左表的连接字段,跟右边的连接字段完全一致时(名称相同,类型相同),就可以用USING替换ON emp.dept_id=dept.dept_id。
写成:
①超过三个表禁止用JOIN
②在进行多表连接时,需要JOIN的字段需要保持数据类型的绝对一致
③多表关联查询时,必须保证被关联的字段有索引;
MYSQL的内置函数可以分为单行函数跟多行函数,本章专门讲解单行函数
单行函数:表示对一个数据做处理,返回一个结果的函数,都叫单行函数,
我输入一个参数,你返回一个参数,只对一行进行变换,参数可以是一个字段或者一个值
比如我输入一个1.1,你将其取整返回给我1,这就是单行函数。
多行函数:又叫聚合函数,聚集函数,分组函数,表示对一组数据做处理,返回一个结果的函数,都叫多行函数,
比如我输入一堆数字,你返回这些数字的和,这就是多行函数。注意:这里的分组函数不是GROUP BY,不要学混了;
这里需要注意一点的就是:
举例:CEIL(42.33)的结果就等于43,42.33的天花板,往上就是43,所以结果为43,注意这里的天花板并不是值无穷大,而是仅仅只大1个数;
举例:FLOOR(42.33)的结果就等于42,42.33的地板就是向下取整,所以结果为42;
FLOOR(-42.33)的结果是-43;
RAND()函数是返回0~1的随机数。
RAND(x)中的x表示随机因子,假如你现在有两个RAND(10),RAND(10),
SELECT RAND(10),RAND(10) FROM daul 此时你会发现,这两个RAND(10)每次随机出来的值都是一样的;
所以随机因子只要一样,那么两个Rand函数返回的值就是一样的;
平时我们直接使用select语句,后面必须要跟着一个张表,否则这个select语法就是错的,但是有时候,比如我只是单纯的想实现一个简单功能,并不想从某个表中查数据,比如我SELECT concat('许海','666'),我只是想单纯的将字符串'许海'跟'666'拼接,完全没有必要FROM哪个表,没有daul表之前,你就完不成这个操作,daul表就是专门用来解决这个问题的,此时你就可以SELECT concat('许海','666') FROM daul;
举例:
SELECT ROUND(123.456,1) FROM DAUL :意思是保留一位小数,且四舍五入
保留一位小数,就是保留到123.4,同时又因为4后面是5,再四舍五入,结果就是123.5;
SELECT ROUND(123.556,0) FROM DAUL :意思是保留零位小数,且四舍五入
保留零位小数,就是保留123,有由于3后面是5,四舍五入就是124;
SELECT ROUND(123.556) FROM DAUL 结果就是124,它跟ROUND(123.556,0) 是等价的
TRUNCATE(123.456,0)的意思是:将123.456截断成0位小数,翻译过来就是保留0位小数,这个函数是不会四舍五入的;
结果就是123;
TRUNCATE(123.456,1)结果就是123.4
举例:
SELECT TRUNCATE(ROUND(123.456,2),0)
这就是嵌套,结果是123
值得注意的是:
注意:在java中,字符串的索引是从0开始,但是在SQL中,索引是从1开始
所以 INSERT('helloworld',2,3,'aaaaa')的结果就为'haaaaaoworld'
注意:通过最下面两个函数获取的世界标准时间,跟我们的北京时间是有八小时的差值的。
举例:
结果:
这里不太好理解,讲一下:
返回日期类型中的年/月/日,比如YEAR('2023-03-21 00:33:00')返回的就是2023,也是我当前写这篇博客的时间。。。
其他的不讲了,好理解了。
举例:
结果:获取到了当前时间的秒数
举例:
结果:
3.5这部分用的很少,甚至我都不该写;
举例1:
注意:INTERVAL是关键字,必须写,1和DATE_ADD共同表示加1,这里的type取的是YEAR,表示单位是年,所以是把当前时间往后加1年。
结果:
举例2:
我把+1,改成-1,就表示往前1年,
结果就是:
举例3:往当前时间后面加1分钟01秒,type就得用MINUTE_SECOND,1分钟01秒用字符串'1_1'表示,中间有个下划线,因为1_1不再是一个数值了,所以要加单引号。
SELECT DATE_ADD(NOW(),INTERVAL '1_1' MINUTE_SECOND) FROM dual;
举例:
ADDTIME(now(),1),表示对当前时间往后加1秒,这里1是数字,所以不用加单引号
ADDTIME(now(),'1:1:3'),'1:1:3'表示时分秒,整句的意思是对当前时间往后加1小时1分01秒
举例:
SUBTIME(now(),1),表示对当前时间往前减1秒
SUBTIME(now(),'1:1:3'),表示对当前时间往前减1小时1分01秒
举例:
DATEDIFF(now(),'2023-02-11')):意思就是计算当前时间跟'2023-02-11'之间的天数间隔
DATEDIFF(now(),'2023-02-11 23:22:12'):你传一个完整的包含时分秒的时间进去也是一样的
返回的也是天数
TIMEDIFF(now(),'2023-02-11 23:22:12')):意思就是计算当前时间跟'2023-02-11 23:22:12'之间的时间间隔
返回的结果就是以时分秒的形式返回,如下图,就表示两个时间差了838个小时59分59秒:
翻译过来就是:返回给定年份year中的第n天的日期
fmt格式:
DATE_FORMAT(date,fmt)与TIME_FORMAT(time,fmt)就是将字符串格式化成日期格式
举例:
结果:
STR_TO_DATE(str,fmt)
举例:
结果:
注意:你在进行逆格式化时,你选定的格式一定要对了,否则是解析不出来的;
SELECT name,salary,CASE WHEN salary >= 1500 THEN '高富帅'
WHEN salary >= 8000 THEN '潜力股'
WHEN slaary >= 3000 THEN '大学生'
ELSE '草根' END as 别名
FROM t_people;
SELECT name,salary,dept_no,CASE dept_no WHEN 10 THEN salary * 1.1
WHEN 20 THEN salary * 1.2
WHEN 30 THEN salary * 1.3
ELSE salary * 1 END as 别名
FROM t_employee;
SELECT name,salary,dept_no,CASE dept_no WHEN 10 THEN salary * 1.1
WHEN 20 THEN salary * 1.2
WHEN 30 THEN salary * 1.3
END as 别名
FROM t_employee
WHERE dept_no in (10,20,30);
你用where加一个限制条件就好了,就排除掉了其他部门,同时因为你排除掉了其他部门,case when这个条件里面就不可能出现dept_no等于其他部门的情况,所以你就可以把ELSE去掉了
这几个太简单了,不多说,
唯一值得注意的是:这几个函数都不会计算NULL值
学到这里,下面的面试题就很好解答了:
面试题:COUNT(1)和COUNT(*),COUNT(字段)谁的速度更快?
答:显然是COUNT(1)最快,COUNT(字段)次之,COUNT(*)最慢
举例:我现在有一个员工表,表中有字段department_id,job_id(工种id),salary工资,
我要统计每个部门下,不同工种的平均工资,我像下面这样写可以吗?
SELECT department_id,job_id,AVG(salary) ( AVG(salary)这种就叫组函数字段 )
FROM employee
GROUP BY department_id;
这里很容易犯错误,这种写法是错的,我只是GROUP BY department_id,相当于只是对部门进行分组,有多少个部门,结果就有多少条记录,AVG(salary)也是计算的每个部门的平均工资,
你查出的结果就是这样:
但是此时你SELECT 后面明明就还有job_id字段,但是这个job_id字段到底取谁呢?一个部门中有多个工种,我到底取哪一个?显然这里的逻辑就出现错误了,此时执行应该报错;但很神奇的来了,mysql这样查是不会报错的,且还能查出来结果,结果如下,但我们可以肯定这个结果是错的,
这就是mysql跟Oracle的差距了,我们在oracle中执行这样的sql,会直接提示你的GROUP BY表达式错误:
HAVING的使用范围更广,由于在查询语句结构中,WHERE是在GROUP BY之前执行,所以WHERE无法对分组结果进行筛选,HAVING是在GROUP BY之后,所以HAVING可以对GROUP BY之后的分组结果进行筛选,这个功能是WHERE无法完成的
如果过滤条件中没有聚合函数时,这种情况下WHERE的执行效率要高于HAVING。
第一步:先FROM :表示你要查哪一张表,如果这里是多张表,此时查出来的就是包含笛卡尔积的结果
第二步:再执行ON:这一步是消灭上面的笛卡尔积,因为ON后面指定了连接条件,可以消灭笛卡尔积
第三步:再执行LEFT/RIGHT JOIN:上一步是相当于是查询出了内连接的结果,这一步会判断你到底是左连接还是右连接,或者是内连接,如果是左连接,就会继续多查出左表的数据,反之同理,内连接同理;
第四步:再执行WHERE:再对上面的结果进行条件过滤
第五步:再执行GROUP BY:再执行分组
第六步:再执行HAVING,将分组后的数据再过滤一遍
第七步:再执行SELECT,一张表的字段有很多,有可能你并不想查询出全部的字段,所以此时执行SELECT,就可以筛选出那些字段是要展示的;
第八步:再执行DISTINCT,执行去重
第九步:再执行ORDER BY
第十步:再执行LIMIT
问题1:为什么WHERE的执行效率要高于HAVING?
问题2:为什么WHERE中不能使用聚合函数作为过滤条件?
问题3:为什么在SELECT对某个字段取的别名,在WHERE中不能用,在GROUP BY中也不能用,在HAVING中也不能用,但是在ORDER BY中能用呢?
在上面的执行过程中,每执行一步,都会生成对应的虚拟表,往后执行,就是对虚拟表的修改过程,等执行到最后,得到的就是我们能看到的表数据。
需求:查询出员工表中谁的工资比Abel高:
思路:先查出Abel的工资,再查出其他人的工资跟Abel的工资做对比
方式1:分两步查询,这种方式的特点是效率低,因为要跟mysql server做两次tcp连接,两次tcp连接总比一次tcp连接慢。
SELECT salary
FROM employee
WHERE last_name = 'Abel'; 结果为11000
SELECT last_name,salary
FROM employee
WHERE salary > 11000;
方式2:自连接
SELECT last_name,salary
FROM employee e1,employee e2
WHERE e1.salary > e2.salary
AND e2.last_name = 'Abel';
方式3:子查询
SELECT last_name,salary
FROM employee
WHERE salary > (SELECT salary
FROM employee
WHERE last_name = 'Abel');
举例:查询学生中分数高于小明的学生信息
SELECT name,age FROM t_student
WHERE score > (SELECT score FROM t_student WHERE name = '小明');
里面的SELECT score FROM t_student WHERE name = '小明'就叫内查询,
外面的SELECT就叫外查询;
比如:上面的例子,查询学生中分数高于小明的学生信息,内查询时查询出小明的分数,小明的分数只可能有一个,所以内查询的结果只有一个,这就是单行内查询。
由于多行子查询中内查询返回的结果有多个,所以此时你再用'='等于符号就不太合适了,所以就有了多行比较操作符
查询平均工资最低的部门id:
方式一:
//先查出每个部门的平均工资
SELECT AVG(salary) FROM t_department GROUP BY department_id
//然后我再AVG聚合函数外面再套一层MIN函数不就可以了吗?
SELECT MIN(AVG(salary)) FROM t_department GROUP BY department_id
照这个思路应该是可以的,但是执行,就直接报错,如下,这是因为MYSQL不支持聚合函数外面再套聚合函数(也叫多行函数,分组函数),但是Oracle中支持;
但是MYSQL中支持单行函数外面套单行函数。
方式二:在FROM中写子查询
将SELECT AVG(salary) FROM t_department GROUP BY department_id的结果看成一张表
在关系型数据中,查询语句并不是一次性就得到结果,而是经过一定的SQL执行过程,这个中间数据是呈现在一个虚拟表中的,随着SQL的执行,虚拟表不断变化,最终呈现出我们看到的样子,比如我们之前学的查询SQL执行过程,它会先执行FROM找到要从哪张表或者哪几张表查,这个时候虚拟表就是一个带有笛卡尔积的很大的表,然后再执行ON消除笛卡尔积等等。
所以你在写解这道题目时也可以将SELECT AVG(salary) FROM t_department GROUP BY department_id的结果看成一张临时的表,你给AVG(salary)取一个别名,外查询就可以用了,所以写法如下:
不过要注意的是,这种将子查询看做临时表的方式,一定要给这临时表取别名,否则会报错。
上面还写没完,只不过是找到了最低的平均工资,你还需要找到部门中平均工资等于这个最低工资的部门,这就很简单了;
SELECT department_id,department_name FROM t_department
GROUP BY department_id HAVING AVG(salary) = (
SELECT MIN(avgsalary) FROM (
SELECT AVG(salary) as avgsalary FROM t_department GROUP BY department_id
) faketablename;
)
方式三:使用多行操作符ALL
SELECT department_id,department_name FROM t_department
GROUP BY department_id HAVING AVG(salary) <= ALL (
SELECT AVG(salary) as avgsalary FROM t_department GROUP BY department_id
)
查询员工表中大于公司平均工资的员工
SELECT * FROM t_employee WHERE salary > (SELECT AVG(salary) FROM t_employee )
这个例子就是不相关子查询:对于外层查询来说,每进行一行比较,内查询的结果就是一样的,所以是不相关子查询;
题目1:查询员工表中工资大于本部门平均工资的员工姓名,工资,和部门id
方式一:
SELECT name,salary,department_id
FROM employee e1
WHERE salary > (
SELECT AVG(salary)
FROM employee e2
GROUP BY department_id
WHERE department_id = e1.department_id
)
在内查询中使用到了外查询的表,这就是相关子查询;
方式二:在FROM中写相关子查询
题目2:在ORDER BY中使用相关子查询
现在有一个员工表,部门表,请查询员工的id,salary,最后按照department_name进行排序(部门名称是在部门表中)
SELECT e.id,e.salary
FROM department e
ORDER BY (
SELECT department_name
FROM department d
WHERE e.department_id = d.id
) ASC;
写这道题目的目的是:ORDER BY后面不仅仅能跟字段名,还可以跟子查询的结果,上面的这个例子中子查询的结果就是真正的部门名称
在SELECT,FROM,WHERE,HAVING,ORDER BY中能写,
在GROUP BY和lmit中不能写,
也就是除了GROUP BY和lmit中不能写,其他地方都能写
题目1:查询公司管理者的员工id,姓名,岗位id,部门id
方式一:自连接
方式二:子查询
方式三:EXISTS
题目2:
SQL的分类是基础,请记住。
当你在对数据库,字段,表名等命名时,建议不要使用保留字命名,比如你将某个表命名成order,而order是保留字,你再执行sql时就会报错,
如果你非要使用,那么在执行sql时,可以将order加上着符,如:`order` ,注意:不是单引号,而是键盘上波浪键上的那个符号;
double(10,2):表示的意思是double类型,总共运行10位数字,小数点后允许有2位小数,那么小数点前面就最多允许有8位数字
主键id的类型,需要为bigint unsigned,unsigned表示无符号,也就是不能为负数,将bigint设置为无符号可以有效的提高bigint能表示的范围,因为主键不可能有负数。
不仅仅是主键,只要其他字段是数字类型,都要设置为unsigned。
create database 库名;
create database 库名 character set '字符集名称' collate '比较规则名称';
如果你不指定,默认就是utf8mb4与utf8mb4_0900_ai_ci
案例:
create database 库名 character set 'utf8mb4' collate 'utf8mb4_0900_ai_ci';
create database if not exists 库名 character set '字符集名称' collate '比较规则名称';
show databases
use 库名
select database();
show tables;
查看指定库下的所有表:
show tables from 库名;
show create database 库名
或
show create database 库名\G
alter database 库名 character set '新的字符集名称';
方式一:
drop database 库名;
方式二:
drop database if exists 库名 (推荐)
create table [if not exists] 表名(
字段1 数据类型 [约束条件] [默认值] [备注],
字段2 数据类型 [约束条件] [默认值] [备注]
)engine = InnoDB,characterset = 'utf8mb4'
注意1:上面的语法中,中括号【】括起来的表示可选项,并不是必填项;
注意2:在创建表时,如果不指定存储引擎,默认就是innodb,不指定字符集,默认跟库使用的字符集保持一致;
约束条件:如unique,primary key,not null,分别就是唯一约束,主键约束,非空约束,
还有一个约束条件为unsigned | signed,unsigned表示非负数,这个比较实用,它可以显著提高数值类型能表示的范围;
默认值:default 值
备注:comment '备注内容'
create table 表名 as select * from 旧表名;
将旧表完全复制一遍,包括数据与数据结构。
注意:你也可以针对虚拟表创建表,你使用select语句时,会得到虚拟表,你通过create table as 虚拟表就能创建出虚拟表的实体表;
假如我只想复制某张表的所有列,不想要数据,怎么实现呢?
只需要在后面加上where过滤条件即可,只要给它一个不可能达到的条件,那么查出来就没有数据,只剩下数据结果,
比如下面的department_id > 10000000就是我举的例子,部门id不可能达到10000000;
create table 表名 as select * from 旧表名
where department_id > 10000000;
或者
create table 表名 as select * from 旧表名
where 1 > 2;
DESC 表名;
结果如下:这句话的作用就等同于navicat的ctrl+D编辑表页面。
alter table 表名 add [column] 字段名 数据类型 [约束条件] [默认值] [备注] [first | after 某字段名];
注意1:最后的first表示你本次添加的字段会在表中字段的第一个位置,
after 某字段名,表示你本次添加的字段会在这个字段后面;
注意2:column关键字可要可不要,但是我建议你还是写上吧。
修改字段的数据类型
alter table 表名 add [column] 字段名 数据类型 [约束条件] [默认值] [备注] [first | after 某字段名];
修改字段的长度
alter table 表名 modify [column] 字段名 varchar(20); 将字段的长度修改成了20
修改字段名称
alter table 表名 change [column] 旧字段名 新字段名 字段的类型;
注意1:最后你必须把字段类型跟上,否则会报错,也就是说你只是改个字段名,就算数据类型前后不变,你在最后也必须把数据类型带上;
注意2:change除了能修改字段名,还可以修改字段的类型,长度,约束条件,等等都能修改,但是modify不能修改字段名
alter table 表名 drop column 列名;
drop table if exists 表名;
truncate table 表名;
rename table 旧表名 to 新表名;
alter table 旧表名 rename [to] 新表名;
表示提交,一旦提交后,数据就会被永久存储,提交后无法回滚,你要`回滚`只能删除;
执行ROLLBACK语句,则可以进行数据回滚,但无法保证回滚一定成功。
回滚到什么程度呢?最后一次commit之后的操作你都能回滚,但是之前的你回滚不了,你非要`回滚`只能删除;
truncate:一旦执行,无法使用rolllback,及时你使用rollback语句也无法恢复数据。它属于DDL。
delete from:执行delete语句后,默认是无法回滚,但如果你在删除之前SET autocommit = false,就可以回滚。它属于DML。
注意:
DDL的操作一旦执行,就不可回滚
DML的操作执行后,默认情况下,也是不可回滚的,但是我们可以修改某参数,在DML语句执行前,修改这个参数SET autocommit = false就可以让DML支持回滚,默认情况下autocommit这个参数是true。
但是DDL操作,及时我们修改autocommit = false,DDL操作也还是无法回滚的。
原因有2:
DDL操作之后会有一次自动的COMMIT操作,它的COMMIT操作是不受aotocommit这个参数的控制的,我们无法改变
DML在执行时,mysql底层会有事务,mysql server会一遍操作,一边备份数据(记录事务日志),以便后续进行回滚,但是DDL在执行操作时,根本就没有考虑事务的事,所以DDL执行的效率的更高,因为它不占用事务日志资源。
案例演示:回滚操作
SET autocommit = false; //先关闭自动提交
DELETE FROM employees; //再删除全表
ROLLBACK; //再执行回滚,就能撤销刚才的删除操作
案例演示:提交操作
SET autocommit = false; //先关闭自动提交
DELETE FROM employees; //再删除全表
COMMIT; //再手动提交一次
ROLLBACK; //再执行回滚,你就会发现不管用了,因为你前面的删除操作已经commit了
回答这个问题,请将我们第3点讲的内容都回答出来,不要漏。
在我们刚学习的知识中,DDL是不支持事务的,但是在MYSQL8中InnoDB已经支持对DDL的事务了,也就是说你对MYSQL8的InnoDB进行DDL操作是可以回滚的。
以下我分别在mysql5.7,mysql8中举例子:
mysql5.7:
//创建test库,设置字符集为utf8mb3,mb3的意思是1~3个bit表示一个字符,
//如果是英文,数字,就是用1个bit表示,如果是中文就用3个bit表示;
//utf8mb4中mb4的意思是1~4个bit表示一个字符,mb4能比mb3表示更多字符,如冷门字,生僻字,表情符号等;
create database test character set 'utf8mb3' collate 'utf8mb3_0900_ai_ci';
use test;
//再创建一个名为table1的表
create table table1(
id bigint unsigned primary key,
age tinyint unsigned
)
//我们再执行这条删除语句,很显然,table2这张表是不存在的,
//当这句话执行到table1,它就会把table1删除,但是table2不存在
//这条DDL语句就会报错。
drop table table1,table2;
//当我们再查看当前库中的表时,我们会发现table1这张表被删掉了,且我们无法回滚。
//及时drop table table1,table2执行出现了异常也没有回滚。
show tables from test;
但是到了mysql8.0中就不一样了,完全一样的流程,当drop table table1,table2执行出错时,它会把已经删除的table1给恢复回来。
增删改属于DML操作。
insert into 表名(字段1,字段2,字段3) values (值1,值2,值3);
insert into 表名 values (值1,值2,值3);
insert into 表名(字段1,字段2,字段3) values (值1,值2,值3),(值1,值2,值3),(值1,值2,值3);
insert into 表名 values (值1,值2,值3),(值1,值2,值3),(值1,值2,值3);
insert into 表1(字段1,字段2) select 字段1,字段2 from 表2;
注意1:这种方式不需要在select前面加values;
注意2:被插入表的字段名,字段顺序,要跟select查出来的字段名,字段顺序保持一致;
注意3:在插入时表1时,要注意字段长度的问题,假如字段1在表2中是varchar(20),但是在表1中是varchar15,那么很有可能表2的数据是插不进去表1的,所以你一定要注意字段长度的问题。
某一个列的值是通过其他列的计算得到的,比如a列值为1,b列值为2,c列不需要我们手动插入,定义a+b的结果为c的中,那么c就是计算列;
create table 表名(
a bigint unsigned,
b bigint unsigned,
c bigint generated always as (a + b) virtual //注意:virtual这个词一定要加
)
c列就作为了计算列,它的值始终等于a+b的值
在上一章,我提到过mysql数据类型,但是没有细致讲解,细致对mysql的所有数据类型进行完整的讲解
解释一下几个类型:
ENUM:枚举类型,存进来其实也算`字符串`,只不过枚举是只能从几个`字符串`中选一个,比如我设定了一个叫季节的字段,那么这个字段的值就只能从春夏秋冬四个值中任选一个了,不能写成其他的值;
二进制字符串类型:虽然这里写的是`字符串`,但他并不是字符串,而是二进制的数据,假如你要在mysql存储音频,视频等文件,你就可以选择二进制字符串类型来存储,通常会用其中的BLOB类型。
JSON类型:
空间数据类型:跟地图相关的数据可能会用到空间数据类型。
在上面,我们学习过,创建数据库时能指定字符集,创建表时也能指定字符集,其实创建字段时也能指定字符集,
但我们通常只会在创建数据库时指定一个字符集,然后下面的所有表都使用同一个字符集。
create table tableName(
id bigint unsigned primary key auto_increment,
name varchar(20) character set 'gbk'
)
DOUBLE:
FLOAT:
注意:在mysql中对DOUBLE和FLOAT使用unsigned修饰没有任何意义,它不会改变float,double的表示范围,这跟整数类型不一样,你死记即可;
修改mysql的时区:
set time_zone = '+8:00';
char是固定长度,长度范围是:0~255,比如你设置字段类型为char(20),就算存进来的值没有达到20的长度,也会占用20的长度;
varchar是可变长度,长度范围是0~65535,varchar字段占用的空间是实际长度+1个字节,因为会多用1个字节来记录到底使用了多少长度;
存储过程:
①因为SQL语句已经提前编译好了(也就是被提前解析成了二进制文件),你不用再像以前那样每次都新写一个SQL,然后把SQL字符串通过网络IO传递给mysql server,你只需要传递一个命令给mysql server,减少了IO通信传递的消息数量,同时也不需要server再对sql进行解析,优化,编译,因为mysql server早就已经提前做好了,所以能提高我们的效率
②提高了sql语句的重复利用性,你可以把它想象成在java中写了一个方法,你要使用某个特定功能时,你只需要调用一下即可;
③减少了SQL语句暴露在网络中的风险,提高了系统的安全性,因为上面第一点才提到,你调用mysql server根本就不是传递sql过去了,而是传指令;
CREATE PROCEDURE 存储过程名称(IN | OUT | INOUT 参数名 参数类型 ...)
将SELECT * FROM employees这句SQL封装成一个存储过程,以便我们以后使用
按照存储过程的语法,那么应该这么写:
CREATE PROCEDURE my_first_procedure() --这个存储过程不需要传参数,所以括号里我们什么都不写
BEGIN
SELECT * FROM employees;
END
我们这么写语法上是没错的,但是我们一执行,立马就会报错,为什么呢?
因为你看这里的分号,表示SQL语句的结束,当myssql server解析到分号时它就会认为你这句sql结束了,但是此时你的END关键字根本没有被读进去,
就会导致mysql server收到一个不完整的存储过程语法,所以会报错;
这里正确的做法是:使用DELIMITER关键字。
语法:DELIMITER $
DELIMITER的作用是:将SQL中的结束符替换成后面跟着的字符,比如DELIMITER $的意思,就是将SQL结束符---分号' ; '改成用美元符号' $ '表示结束,
但我们一般是不会替换的,目前能用到的场景就是在创建存储过程时用到,不过你在使用DELIMITER时,一定要先在用完之后,将结束符改回分号;
我们再接着完善上面的例子,正确的写法如下:
例2:创建一个无参数无返回值的存储过程,返回所有员工的平均工资 (简单)
delimiter $
create procedure avg_salary()
begin
select avg(salary) from employees;
end$
delimiter ;
调用我们创建好的存储过程:CALL 存储过程名称;
call avg_salary();
例3:创建一个无参有返回值的存储过程:获取员工表的最低工资,并将该最低工资通过out返回
调用无参有返回值的存储过程:CALL 存储过程名(@变量名)
说明:
>>> 变量名这里填你在存储过程中out后面定义的变量名,
>>> 然后在变量名中加@,加了@的变量,表示是用户自定义变量,mysql中还有会话变量,全局变量等,下一章再讲。
所以我们上面写的无参有返回值的存储过程,就应该这么调用
这句话的意思,就是调用我们刚才写的存储过程,又由于我们上面的存储过程中写了into minsalary,所以最后存储过程的结果就到了变量minsalary中,而不是直接像select语句那样在控制显示给你,
所以你要查看这个结果的话,必须再调用一下这个变量,就可以查看到最小的工资了,如下:
select minsalary;
例4:创建有参数无返回值的存储过程:查询员工表中某个员工的工资
调用有参无返回值的存储过程:CALL 存储过程名(变量值)
调用方式一:
call salary('许海');
我们调用后,就能在控制台看到结果:
这里要注意一下了:为什么我们例2中,调用存储过程在控制台看不到结果呢?
因为有返回值的存储过程使用了into将结果封装到了变量中,这个变量被存到了mysql server中,而不是直接输出到控制台,所以你要查看结果,
只能再次调用一次这个变量。
调用方式二:
先用一个变量把'许海'装起来,然后再将变量传到自定义存储过程中
set @empname = '许海'; -- 也可以写成 set @empname := '许海'; := 是mysql中明确表示赋值的符号;
call salary(@empname);
结果同样是11000。
注意:一个@表示用户自定义的变量,两个@@表示系统变量
例5:创建有参有返回值的存储过程:查看员工表中某员工的工资,用in接收参数,并用out将结果封装到变量中
delimiter $
create procedure salary(in empname varchar(20),out empsalary double)
begin
select salary into empsalary from employees where name = empname;
end$
delimiter ;
调用有参有返回值的存储过程:
set @empname := '许海';
call salary(@empname,@empsalary); 也可以写成call salary('许海',@empsalary);
select @empsalary
例6:带有inout的存储过程:查询某个员工领导的姓名,并用intou参数输入员工姓名,输出领导姓名
delimiter $
create procedure show_manager_name(inout empname varchar(20))
begin
select name into empname
from employee
where employee_id = (
select manager_id from employee
where name = empname;
)
end$
delimiter ;
从inout的用法可以看出:inout是用同一个变量来接收参数,返回参数,入参跟出参类型都可能是不一样的,所以我不喜欢用inout
还是in,out比较明确;
调用带inout的存储过程:
set @empname := '许海';
call show_manager_name(@empname);
select @empname;
我们之前学习的单行函数,多行函数(聚合函数,聚集函数,分组函数)都是mysql为我们定义好的函数,我们直接拿来使用即可,
存储函数其实就是自定义函数;
create function 函数名(参数名 参数类型...)
returns 返回值类型
[ characteristics ...]
BEGIN
函数体
return 结果; --函数体中肯定有return 语句,返回的内容可以用select查询;
END
说明:
参数列表:存储过程中指定参数为in,out,或者inout都是可以的,但是在存储函数中,只能指定参数为in参数,如果你不指定,那么它默认也是in参数,反正就是不能改,并且由于存储函数中只能是in类型,所以创建存储函数时可以不写in;
returns 返回值类型:必须写,这句是用来说明本函数返回值的类型的,
它是跟函数体中的 return 结果;配合的,return 结果只是表示具体返回的结果是多少,但是不知道返回的类型,所以"returns 返回值类型"就是用来说明返回值类型是什么的;
【 characteristics 】是用来表示对本函数的约束的:
错误的写法:当我们这些写时,会发现会报错,因为在创建存储函数时,它要求我们必须加上对函数的约束characteristics,
但是在创建存储过程时就不需要加,这是它们两者的不同点之一;
delimiter $
create function get_email_by_name()
returns varchar()
begin
return (select email from employee where name = '许海');
end$
delimiter ;
正确的写法1:加上characteristics
delimiter $
create function get_email_by_name()
returns varchar(20)
DETERMINISTIC
CONTAINS SQL
READS SQL DATA
begin
return (select email from employee where name = '许海');
end$
delimiter ;
正确的写法2:
先设置set GLOBAL_log_bin_trust_function_creators = 1,这个全局变量的意思是:让mysql server信任我们创建的存储函数,
mysql server默认是不信任的,所以要求我们加上characteristics,修改成1后,就不需要加了;
set GLOBAL_log_bin_trust_function_creators = 1;
delimiter $
create function get_email_by_name()
returns varchar(20)
begin
return (select email from employee where name = '许海');
end$
delimiter ;
select 函数名();
set GLOBAL_log_bin_trust_function_creators = 1;
delimiter $
create function get_email_by_name(name1 varchar(20))
returns varchar(20)
begin
return (select email from employee where name = name1);
end$
delimiter ;
drop function | procedure if exists 函数名|存储过程名; //注意:函数名|存储过程名后面不用加()
优点:
缺点:
在mysql的存储过程与存储函数中,可以用变量来存储查询或计算的中间结果数据,
在Mysql中,变量分为系统变量和用户自定义变量。
查看所有系统变量
show global variables;
查看指定系统变量
show global variables like '%名称%';
select @@global.变量名;
查看所有会话变量
show session variables;
show variables; //如果不指明serssion,默认就是查看会话变量;
查看指定会话变量
show session variables like '%名称%';
或
select @@session.变量名;
或
select @@变量名; //注意,没指定session或者global的话,系统会先去会话变量中找,找不到就再去系统变量中找;
方式一:
//修改全局变量
set @@global.变量名 = 值;
或
set global 变量名 = 值;
//修改会话变量
set @@session.变量名 = 值;
或set @@变量名 = 值;
或set session 变量名 = 值;
方式二:还可以修改mysql的配置文件my.ini。
注意:通过命令行set的方式,只针对于当前正在运行的mysql实例有效,重启后就失效了,换句话说:mysql服务重启后,它还是会去读my.ini里面的配置;
会话用户变量:只对当前连接会话有效,连接断开,则该会话的会话用户变量失效
局部变量:只在BEGIN,END语句块中有效,局部变量只能在存储过程和函数中使用
注意:前面我们说过一个@表示用户变量,其实准确的说,应该是会话用户变量,并不代表局部变量,因为局部变量只在存储过程,存储函数中有效,根本用不上@符号;
方式一:
set @a := 1;
方式二:使用':='或into关键字,将sql语句的结果赋给用户变量,
这种方式必须使用select,不能用set,因为要先将sql的结果查出来,才能设置,所以要用select;
//意思是将员工数量赋值给会话用户变量@a
select @a := count(1) from employees;
或
select count(1) into @a from employees;
在存储函数,存储过程中,可以使用DECLARE语句定义一个局部变量,DECLARE仅仅在BEGIN...END代码块中有效,并且只能放在BEGIN...END代码块中的第一句;
局部变量声明格式:
局部变量的赋值:
赋值方式一:
set 变量名 = 值 或者 set 变量名 := 值
赋值方式二:使用into关键字赋值
select 字段名或表达式 into 变量名 from 表;
案例:
set GLOBAL_log_bin_trust_function_creators = 1;
delimiter $
create function get_email_by_name()
returns varchar(20)
begin
declare a int default 0;
declare b int;
declare emp_name varchar(20);
//如果两个变量类型相同,也可以写成
//declare a,b int default 0;
select last_name into emp_name from emoloyee where emp_id = 1;
return emp_name;
end$
delimiter ;
在各种语言中都有针对异常的处理机制,SQL语言中也有此类机制。
定义条件就是:我们实现定义好SQL程序执行过程中可能会遇到的问题。
处理程序就是:我们针对程序执行过程中可能遇到的问题所专门的处理方式。
delimiter $
create procedure update_data_no_condition()
begin
set @x = 1;
update employee set email = null where name = 'able';
set @x = 2;
update employee set email = 'sdsd' where name = 'able';
set @x = 3;
end$
delimiter ;
背景:员工表的email表有非空约束,所以当执行到update employee set email = null where name = 'able'这句时一定会报错,报错如下:
①先定义条件:
定义条件的语法:
declare 错误名称 condition for 错误码(或错误操作)
错误码说明: 错误码有两种,
MySQL_error_code:表示数值类型错误代码
sqlstate_value:表示长度为5的字符类型错误代码
你在错误码这里填1418或者HY000都可以表示这个错误类型,不同的错误,填不同的值;
举例:对Mysql的"ERROR 1148(42000)"定义条件:
declare Field_Not_be_NULL condition for 1048;
或
declare Field_Not_be_NULL condition for sqlstate ‘42000';//因为'42000'可能被隐式转换为整数类型,为了避免歧义,所以在前面加上一个sqlstate避免歧义;
②再定义处理程序
declare 处理方式 handler for 错误类型 处理语句
delimiter $
create procedure update_data_no_condition()
begin
//意思是:如果出现1048的异常,就continue继续执行,并声明一个变量prc_value = -1;
//这是没有用定义条件的用法;
//我感觉定义条件的方式很鸡肋,我就不举例了,不定义条件,直接用错误码更好;
declare continue handler for 1048 set @prc_value = -1;
set @x = 1;
update employee set email = null where name = 'able';
set @x = 2;
update employee set email = 'sdsd' where name = 'able';
set @x = 3;
end$
delimiter ;
举例:
delimiter $
create procedure test(age int)
begin
if age > 40
then select '中老年';
elseif age > 18
then select '靑年';
else
select '少年';
end if;
end$
delimiter ;
loop语法:
举例1:
delimiter $
create procedure test()
begin
declare num int default 1;
loop_name:loop
set num = num + 1;
if num >= 10
then leave loop_name; //leave的意思就是跳出循环结构
end loop loop_name;
end$
delimiter ;
delimiter $
create procedure update_salary_loop(inout num int)
begin
set num = 0; //这里将num改成0的原因是:万一外面传了一个num不等于0,那么计算次数就不是从0开始计数,计数就错了
loop_name:loop
update employee set salary = salary * 1.1;
select avg(salary) into avg_salary from employee;
if avg_salary >= 12000
then leave loop_name;
end if;
set num = num + 1;
end loop loop_name;
end$
delimiter ;
leave表示跳出循环;
iterate可以看做java中continue的意思,跳过本次循环直接进入下一次循环;
leave语法:
leave 循环名称;
iterate语法:
iterate 循环名称;
这里的循环名称就是我们在循环结构中给当前循环取的名称,取名的目的是就是为了给leave,iterate,end loop使用;
现在有一个语句:我将员工表中工资大于15000的员工查出来了,这是一个结果集。
结果集如下:
假如我现在想修改结果集中的第10行数据,我该怎么做呢?
由现有的知识并不好做,我们只能用肉眼看,找到第10的数据,再找到它的id,再使用where id = ?的方式进行修改,
显然这个过程很low。
这个时候就需要游标了;
declare 游标名称 cursor for 查询语句;
注意1:这里的查询语句就是我们上面说的结果集。
注意2:在存储过程、存储函数中,游标的声明需要放在变量的声明后面,变量的声明需要放在begin...end代码块的最前面。
open 游标名称;
当我们定义好游标后,如果要使用它,必须先打开游标,打开游标胡,select语句的查询结果集就会被送到游标工作区,为后面游标逐条读取结果集中的记录做准备。
fetch 游标名称 into 字段1,字段2,字段3;
这里需要注意:假如你在创建游标时,查询语句的结果集中有字段1,字段2,字段3,那么你在fetch 游标名称 into 后面也必须跟字段1,字段2,字段3,必须 一 一对应,否则会报错。
触发器是由事件驱动的,这些事件包括insert,update,delete等事件。
说明:
INSERT | UPDATE | DELETE :表示你要监听表上哪种事件。
ON 表名:触发器是一定要跟表进行绑定的,因为触发器就是用来监控表的,所以要有一个ON 表名。
BEFORE | AFTER:表示你到底要在INSERT | UPDATE | DELETE 事件之前还是之后执行。
FOR EACH ROW:表示监控表中的每一行,任意一行发生INSERT | UPDATE | DELETE 时,就会触发这个触发器。
触发器要执行的语句块:触发器被触发后,要执行的逻辑,可以是一句简单的SQL,如果逻辑复杂,可以用BEGIN...END代码块表示。
delimiter $
create trigger before_insert_test_trigger
before insert on test
for each row
begin
insert into test_log(operation,time) values ('向test表插入了一条语句','2023-03-27 00:00:00');
end$
delimiter ;
这里要注意几点:
执行insert语句时,如何表示新插入进来的那个对象?在insert事件中,用new关键字代表新插入进来的对象。
当批量插入时,new关键字也是表示新插入的一个对象,因为前面有for each row语法,你可以将其理解为一个循环,new关键字代表这个循环中的当次循环中的元素。
在删除、更新事件中,我们用old关键字表示旧的那一行记录;
在mysql中如何像java一样手动抛出异常?
signal sqlstate '异常代码名称' set message_text = '异常代码提示' ,按照这个语法就能手动抛出异常,这是固定语法,只有'异常代码名称'和'异常代码提示'我们能改,其他的都不能改。
方式1:
show triggers;
方式2:从系统库information_schema的TRIGGERS表中查询触发器的信息;
select * from information_schema.TRIGGERS;
drop trigger if exists 触发器名称;
优点:
通过对表事件的监控,保证数据的完整性,同时也可以实现一些很有用的功能,比如监控某张重要表的记录,将其操作记录通过触发器写到一张单独的日志表中。
触发器还可以在操作数据前,对数据进行合法性的检查。
缺点: