数据库(SQL注入问题、视图、触发器、事务、存储过程、内置函数、流程控制、索引)

发布时间 2023-07-16 15:57:33作者: Maverick-Lucky

SQL注入问题

SQL注入的原因:由于特殊符号的组合会产生特殊的效果

 实际生活中,尤其是在注册用户名的时候会非常明显的提示你很多特殊符号不能用,会产生特殊的效果。

结论:涉及到敏感数据部分,不要自己拼接,交给现成的方法拼接即可。

import pymysql
# 链接MySQL服务端
conn = pymysql.connect(
    host = '127.0.0.1',
    port = 3306,
    user = 'root',
    password = '123',
    database = 'db6',
    charset = 'utf8',
    autocommit = True # 针对 增 改 删自动二次确认
)
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
username = input('>>>:').strip()
password = input('>>>:').strip()
sql = "select * from userinfo where username='%s' and password='%s'" % (username, password)
# 不要手动拼接数据 先用%s占位 之后将需要拼接的数据直接交给execute方法即可
res = cursor.execute(sql) # 自动识别sql里面的%s用后面元组里面的数据替换
if res:
    print("登录成功")
    print(cursor.fetchall())
else:
    print("用户名或密码错误")

 

注入问题: 1.只需要用户名也可以登录

  username:>>>   kevin ' -- ddasfdfsdfdsfsdfsdfsdfdsfsdfsd 会将 ' -- 后面的内容当成mysql中的注释

  username:>>>   xxx ' or 1=1  ,条件一直为真

  2.不需要用户名和密码也可以登录

结果如图所示:

 解决注入问题:

import pymysql
# 链接MySQL服务端
conn = pymysql.connect(
    host = '127.0.0.1',
    port = 3306,
    user = 'root',
    password = '123',
    database = 'db6',
    charset = 'utf8',
    autocommit = True # 针对 增 改 删自动二次确认
)

# 产生一个游标对象,字典形式
cursor = conn.cursor(cursor = pymysql.cursors.DictCursor)

username = input('username:').strip()
password = input('password:').strip()

# 编写SQL语句
# 因为拼接问题,产生注入问题
# sql = "select * from userinfo where username='%s' and password='%s'" % (username,password)

# 解决注入问题,execute内部会自动对username,password进行拼接,不需要手动拼接
sql = "select * from userinfo where username=%s and password=%s"
cursor.execute(sql,(username,password))
# 取出表中的数据
data = cursor.fetchall()

if data:
    print(data)
    print('登录成功')
else:
    print('用户名或密码错误')

正确的结果:

pymysql补充

import pymysql

conn = pymysql.connect(
    host='127.0.0.1',
    port=3306,
    user='root',
    password='123',
    db='db6',
    charset='utf8',
    autocommit=True#  默认是autocommit=False 自动提交,后面执行SQL语句时不需要再commit

)

cursor = conn.cursor(pymysql.cursors.DictCursor)
# 向user表里增加数据
sql = "insert into userinfo(username,password) values (%s,%s)"
rows = cursor.execute(sql,('kkk',123))
print(rows)

# # 删
sql = "delete from userinfo where id=2"
rows = cursor.execute(sql)
print(rows)

#
sql = "update userinfo set username='jackjack' where id=3"
rows = cursor.execute(sql)
print(rows)

#
sql = "select username from userinfo where id=3"
cursor.execute(sql)
print(cursor.fetchall())  # [{'username': 'jackjack'}]

 

一次性插入多条数据

  # 向user表里增加数据
sql = "insert into user(name,password) values (%s,%s)"rows = cursor.execute(sql,('kkk',123))print(rows)

# 可以改成 # 向user表里增加数据

sql = "insert into user(name,password) values (%s,%s)"# rows = cursor.execute(sql,('kkk',123))rows = cursor.executemany(sql,[('qjwnb',123),('qjwhaonb',123)])print(rows)

 

 视图(了解)

视图:就是通过查询得到一张虚拟表,然后保存下来,下次直接使用即可。

使用原因:

  通过SQL语句去查询数据比较慢,使用一张虚拟表,可以不用重复查询

固定语法:

  create view 表名/视图名 as 虚拟表的查询sql语句

具体使用,比如:

create view teacher2course asselect * from teacher inner join course on teacher.tid = course.teacher_id;

注意点:

  1. 创建视图在硬盘上只会有表结构 没有表数据(数据还是来自于之前的表)

  2. 视图一般只用来查询 里面的数据不要继续修改 可能会影响真正的表

视图到底使用频率高不高呢?

""" 不高,当你创建了很多视图之后,会造成表的不好维护  """

视图了解即可 基本不用

触发器(了解)tigger

触发器:在满足对表数据进行增、删、改的情况下,会自动触发的功能。使用触发器可以帮助我们实现监控、日志...

c触发器可以在六种情况下自动触发:增前 、增后、删前、删后、改前、改后

基本语法结构:

create trigger 触发器的名字 
before/after insert/update/delete on 
表名
for each row
begin    
        sql语句
end

具体使用:

针对触发器的名字,我们通常需要做到见名知意

针对增:

create trigger tri_before_insert_t1  
before insert on t1
for each row
begin
    sql语句
end
create trigger tri_after_insert_t1
after insert on t1
for each row
begin
    sql语句
end
"""针对删除和修改 书写格式一致"""

delimiter:修改MySQL默认的语句结束符,只作用于当前窗口

需要注意:在书写sql代码的时候结束符是; 而整个触发器的结束也需要分号;

delimiter $$     将默认的结束符号由;改为$$
delimiter ;

案例

1. 创建表cmd

CREATE TABLE cmd (
    id INT PRIMARY KEY auto_increment,
    USER CHAR (32),
    priv CHAR (10),
    cmd CHAR (64),
    sub_time datetime, #提交时间
    success enum ('yes', 'no') #0代表执行失败
);

2. 创建表errlog

CREATE TABLE errlog (
    id INT PRIMARY KEY auto_increment,
    err_cmd CHAR (64),
    err_time datetime
);

需求:

"""

当cmd表中的记录succes字段是no那么就触发触发器的执行去errlog表中插入数据

NEW指代的就是一条条数据对象

"""

3.写触发器

delimiter $$
create trigger tri_after_insert_cmd 
after insert on cmd 
for each row
begin
    if NEW.success = 'no' then
        insert into 
errlog(err_cmd,err_time) 
values(NEW.cmd,NEW.sub_time);
    end if;
end $$
delimiter ;

4. 在cmd表插入数据

INSERT INTO cmd (
    USER,
    priv,
    cmd,
    sub_time,
    success
)
VALUES
    ('jason','0755','ls -l /etc',NOW(),'yes'),
    ('jason','0755','cat /etc/passwd',NOW(),'no'),            
    ('jason','0755','useradd xxx',NOW(),'no'),
    ('jason','0755','ps aux',NOW(),'yes');

5. 删除触发器

drop trigger tri_after_insert_cmd;

 

事务(掌握)

事务:开启一个事务可以包含多条sql语句,这些sql语句要么同时成功,要么就一个都别想成功,称之为事务的原子性。

作用:

    保证了对数据操作的'数据安全性'

例如:

egon 用银行卡给我的支付宝转账1000

  1. 将egon银行卡账户的数据减1000块

  2. 将jason支付宝账户的数据加1000块

事务具有的四个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。

原子性:

  一个事物是一个不可分割的工位,事务中包括的诸多操作要么都做,要么都不做。

一致性:

  事务必须是使数据库从一个一致性状态变到另外一个一致性状态。一致性与原子性是密切相关的。

隔离性:

  一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

持久性:

  持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

事务的使用

事务相关的关键

1. 开启事务

start transaction;

2. 回滚,回到事务执行之前的状态

rollback;

3.确认,确认之后就无法回滚了

commit;

 

模拟转账功能

先建表(cmd里执行)

create table user(
    id int primary key auto_increment, 
    name char(16), 
    balance int
);

插入数据

insert into user(name,balance) values
('jason',1000),
('egon',1000),
('tank',1000);

修改数据

# 修改数据之前先开启事务操作
start transaction;

# 修改操作
update user set balance=900 where name='jason'; # 买支付100元
update user set balance=1010 where name='egon'; # 中介拿走10元
update user set balance=1090 where name='tank'; # 卖家拿到90元

# 回滚到上一个状态
rollback;

# 开启事务之后,只要没有执行commit操作,数据其实都没有真正刷新到硬盘
commit;
"""开启事务检测操作是否完整,不完整主动回滚到上一个状态,如果完整就应该执行commit操作"""

总结:

"""

当你想让多条sql语句保持一致性

要么同时成功要么同时失败 

你就应该考虑使用事务

"""

 事务要尽可能少的开,不要写一个SQL语句都写到事务里面去

存储过程(了解)

存储过程:

  类似于python中的自定义函数,它的内部包含了一系列可以执行的sql语句,存储过程存放于MySQL服务端正,可以直接通过调用存储过程触发内部sql语句的执行。

基本使用(语法结构)procedure

create procedure 存储过程的名字(形参1,形参2,...)
begin
    sql代码
end
# 调用
call 存储过程的名字();

三种开发模型

第一种:

"""
    应用程序:程序员写代码开发
    MySQL:提前编写好存储过程,供应用程序员调用
    
    好处:开发效率提升了,执行效率也提升了
    缺点:考虑到人为因素、跨部门沟通的问题后续的存储过程的扩展性差
"""

第二种:

"""
    应用程序:程序员写代码开发之外,涉及到数据库操作也自己动手写
    优点:扩展性很高
    缺点:开发效率降低
    编写sql语句太过繁琐,而且后续还需要考虑到sql优化的问题

"""

第三种:

"""
    应用程序:只写程序代码,不写sql语句,基于别人写好的操作MySQL的python框架(ORM框架)直接调用操作
    优点:开发效率比上面两种情况都要高 
    缺点:语句的扩展性差,可能会出现执行效率低下的问题
"""

第一种基本不用。一般都是第三种,出现效率问题再动手写sql

创建存储过程

对于存储过程,可以接收参数,其参数有三类:

#in 仅用于传入参数用
#out 仅用于返回值用
#inout 既可以传入又可以当作返回值

delimiter $$ # 把结束符号;改为$$
create procedure p1(
    in m int,  # 只进不出  m不能返回出去
    in n int,  # 只进不出 n不能返回出去
    out res int  # 该形参可以返回出去
)
begin
    select tname from teacher where tid>m and tid<n;
    set res=666;  # 将res变量修改 用来标识当前的存储过程代码确实执行了
end $$
delimiter ; # 把结束符号$$改为;

# 针对形参res 不能直接传数据 应该传一个变量名
# 定义变量
set @ret = 10;
# 查看变量对应的值
select @ret; 

 

存储过程具体演示

大前提:存储过程在哪个库下面创建的只能在对应的库下面使用!!!

# 1. 直接在mysql中调用
set @res=10   # res的值是用来判断存储过程是否被执行成功的依据,所以需要先定义一个变量@res存储10
call p1(2,4,@res);

# 查看结果
select @res;  # 执行成功。@res变量值发生了改变

# 2. 在python程序中调用
pymysq连接mysql
# 产生的游表
cursor.callproc('p1',(2,4,10))  # 内部原理:@_p1_0=2,@_p1_1=4,@_p1_2=10;
cursor.excute('select @_p1_2;')

pymysql模块中调用存储过程

import pymysql

conn = pymysql.connect(
    host = '127.0.0.1',
    port = 3306,
    user = 'root',
    passwd = '123456',
    db = 'day48',
    charset = 'utf8',
    autocommit = True
)
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 调用存储过程 传参
cursor.callproc('p1',(1,5,10))
"""
@_p1_0=1
@_p1_1=5
@_p1_2=10
"""
print(cursor.fetchall())
# cursor.execute('select @_p1_2;')
print(cursor.fetchall())

函数(了解)

跟存储过程是有区别的,存储过程是自定义函数,函数就是类似于内置函数。

mysql内置的函数只能在sql语句中使用!

 
('jason','0755','ls -l /etc',NOW(),'yes') # NOW()就是函数

CREATE TABLE blog (
    id INT PRIMARY KEY auto_increment,
    NAME CHAR (32),
    sub_time datetime
);

INSERT INTO blog (NAME, sub_time)
VALUES
    ('第1篇','2015-03-01 11:31:21'),
    ('第2篇','2015-03-11 16:31:21'),
    ('第3篇','2016-07-01 10:21:31'),
    ('第4篇','2016-07-22 09:23:21'),
    ('第5篇','2016-07-23 10:11:11'),
    ('第6篇','2016-07-25 11:21:31'),
    ('第7篇','2017-03-01 15:33:21'),
    ('第8篇','2017-03-01 17:32:21'),
    ('第9篇','2017-03-01 18:31:21');

select date_format(sub_time,'%Y-%m'),count(id) from blog group by date_format(sub_time,'%Y-%m');

流程控制(了解)

 # if判断
delimiter //   # // 结束符号
CREATE PROCEDURE proc_if ()
BEGIN
    declare i int default 0;
    if i = 1 THEN
        SELECT 1;
    ELSEIF i = 2 THEN
        SELECT 2; 
   ELSE
        SELECT 7;
    END IF;
END //
delimiter ;

# while循环
delimiter //
CREATE PROCEDURE proc_while ()
BEGIN
    DECLARE num INT ;
    SET num = 0 ;
    WHILE num < 10 DO
        SELECT
            num ;
        SET num = num + 1 ;
    END WHILE ;

索引(了解概念知识)

数据都是存在于硬盘上的,查询数据不可避免的需要进行IO操作

索引:

  就是一种数据结构,类似于书的目录。意味着以后在查询数据的时候应该先找目录再找数据,而不是一页一页的翻书,从来提升查询速度降低IO操作。

  索引在MySQL中也叫“键”,是存储引擎用于快速查找记录的一种数据结构

有:

  primary key、unique key、index key

注意:

  foreign key不是用来加速查询用的,不在我们的研究范围值之内。

  上面的三种key,掐面两者除了可以增加查询速度之外各自还具有约束条件,而最后一种 index key没有任何的约束条件,只是用来帮助快速查询数据的。

本质:

  通过不断的缩小想要的数据范围筛选出最终的结果,同时将随机事件(一页一页的翻)变成顺序事件(先找目录,再找数据),也就是说有了索引机制,我们可以总是用一种固定的方式查找数据

一张表中可以有多个索引(多个目录),索引虽然能够帮助加快查询速度但是也有缺点

  """

    1 当表中有大量数据存在的前提下 创建索引速度会很慢

    2 在索引创建完毕之后 对表的查询性能会大幅度的提升 但是写的性能也会大幅度的降低

  """

索引不要随意的创建!!!以后加索引的时候,尽量给字段中存的是数字的列加,我们使用主键查询速度很快。

b+树

  在b+树中只有叶子结点存放真实数据,根和树枝节点存的仅仅是虚拟数据,查询次数是由树的层级决定,层级越低次数越少。

  一个磁盘块儿的大小是一定的 ,那也就意味着能存的数据量是一定。

  如何保证树的层级最低呢:一个磁盘块存放占用空间比较小的数据项。

将主键id字段建立索引能够降低树的层级高度

聚集索引(primary key)

聚集索引其实指的就是表的主键,innodb引擎规定一张表中必须要有主键。

  myisam在建表的时候产生三个文件到硬盘,innodb在建表的时候产生两个文件到硬盘,innodb的索引和数据是都放在了idb表数据文件中

特点:叶子结点放的一条条完整的记录

辅助索引(unique、index)

辅助索引:

  查询数据的时候不可能都是用id作筛选条件的,也可能会用name,password等字段信息,那么这个时候就无法利用到聚集索引的加速查询效果,就需要给其他字段建立索引,这些索引就叫辅助索引。

特点:

  叶子结点存放的是辅助索引字段对应的那条记录的主键的值(比如按照name字段创建索引,那么叶子节点存放的是:{name对应的值:name所在的那条记录的主键值})

  select name from user where name='jack';

上述语句叫非覆盖索引,虽然查询的时候命中了索引字段name,但是要查的是age字段,所以还需要利用主键去查找。

测试索引

准备

#1. 准备表
create table s1(
    id int,
    name varchar(20),
    gender char(6), 
   email varchar(50
));
select id,name from t1;
#2. 创建存储过程,实现批量插入记录
delimiter $$ #声明存储过程的结束符号为$$
create procedure auto_insert1()
BEGIN
    declare i int default 1;
    while(i<100000)do
        insert into s1 values(i,'jason','male',concat('jason',i,'@oldboy'));
        set i=i+1;
    end while;
END$$ #$$结束
delimiter ; #重新声明分号为结束符号

#3. 查看存储过程
show create procedure auto_insert1\G 

#4. 调用存储过程
call auto_insert1();

 

# 表没有任何索引的情况下
select * from s1 where id=1000;
# 避免打印带来的时间损耗
select count(id) from s1 where id = 1000000;
select count(id) from s1 where id = 1;

# 给id做一个主键
alter table s1 add primary key(id); # 速度很慢


select count(id) from s1 where id = 1;  # 速度相较于未建索引之前两者差着数量级
select count(id) from s1 where name = 'jason'  # 速度仍然很慢
"""范围问题"""# 并不是加了索引,以后查询的时候按照这个字段速度就一定快
select count(id) from s1 where id > 1; # 速度相较于id = 1慢了很多、
select count(id) from s1 where id >1 and id < 3;
select count(id) from s1 where id > 1 and id < 10000;
select count(id) from s1 where id != 3;alter table s1 drop primary key; # 删除主键 单独再来研究name字段
select count(id) from s1 where name = 'jason'; # 又慢了create index idx_name on s1(name); # 给s1表的name字段创建索引
select count(id) from s1 where name = 'jason' # 仍然很慢!!!"""再来看b+树的原理,数据需要区分度比较高,而我们这张表全是jason,根本无法区分那这个树其实就建成了“一根棍子”"""
select count(id) from s1 where name = 'xxx';  # 这个会很快,我就是一根棍,第一个不匹配直接不需要再往下走了select count(id) from s1 where name like 'xxx';select count(id) from s1 where name like 'xxx%';
select count(id) from s1 where name like '%xxx'; # 慢 最左匹配特性# 区分度低的字段不能建索引drop index idx_name on s1;# 给id字段建普通的索引create index idx_id on s1(id);select count(id) from s1 where id = 3; # 快了
select count(id) from s1 where id*12 = 3; # 慢了 索引的字段一定不要参与计算drop index idx_id on s1;select count(id) from s1 where name='jason' and gender = 'male' and id = 3 and email = 'xxx';# 针对上面这种连续多个and的操作,mysql会从左到右先找区分度比较高的索引字段,先将整体范围降下来再去比较其他条件
create index idx_name on s1(name);select count(id) from s1 where name='jason' and gender = 'male' and id = 3 and email = 'xxx'; # 并没有加速drop index idx_name on s1;# 给name,gender这种区分度不高的字段加上索引并不难加快查询速度create index idx_id on s1(id);select count(id) from s1 where name='jason' and gender = 'male' and id = 3 and email = 'xxx'; # 快了 先通过id已经讲数据快速锁定成了一条了
select count(id) from s1 where name='jason' and gender = 'male' and id > 3 and email = 'xxx'; # 慢了 基于id查出来的数据仍然很多,然后还要去比较其他字段drop index idx_id on s1create index idx_email on s1(email);select count(id) from s1 where name='jason' and gender = 'male' and id > 3 and email = 'xxx'; # 快 通过email字段一剑封喉 ```
#### 联合索引

```mysql
select count(id) from s1 where name='jason' and gender = 'male' and id > 3 and email = 'xxx';  
# 如果上述四个字段区分度都很高,那给谁建都能加速查询
# 给email加然而不用email字段
select count(id) from s1 where name='jason' and gender = 'male' and id > 3; 
# 给name加然而不用name字段
select count(id) from s1 where gender = 'male' and id > 3; 
# 给gender加然而不用gender字段
select count(id) from s1 where id > 3; 

# 带来的问题是所有的字段都建了索引然而都没有用到,还需要花费四次建立的时间
create index idx_all on s1(email,name,gender,id);  # 最左匹配原则,区分度高的往左放
select count(id) from s1 where name='jason' and gender = 'male' and id > 3 and email = 'xxx';  # 速度变快

总结:上面这些操作,你感兴趣可以敲一敲,不感兴趣你就可以不用敲了,权当看个乐呵。理论掌握了就行了
慢查询日志
设定一个时间检测所有超出该时间的sql语句,然后针对性的进行优化!