MySQL数据库(5)- pymysql的使用、索引
一、pymysql模块的使用
1、pymysql的下载和使用
之前我们都是通过MySQL自带的命令行客户端工具mysql来操作数据库,那如何在python程序中操作数据库呢?这就需要用到pymysql模块,该模块本质就是一个套接字客户端软件,使用前需要事先安装。
1)pymysql模块的下载
pip3 install pymysql
2)pymysql模块的使用
现有数据库mydb,其中有一个userinfo表,表中数据如下:
mysql> select * from userinfo; +----+------+-----+ | id | name | pwd | +----+------+-----+ | 1 | wll | 123 | | 2 | ssx | 456 | +----+------+-----+
示例:使用Python实现用户登录,如果用户存在则登录成功,否则登录失败
import pymysql username = input('请输入用户名:') pwd = input('请输入密码:') # 1、连接 conn = pymysql.connect( host = '127.0.0.1', port = 3306, user = 'root', password = '123', db = 'mydb', charset = 'utf8' ) # 2、创建游标 cur = conn.cursor() sql = "select * from userinfo where name='%s' and pwd='%s'" %(username,pwd) # 3、执行sql语句 result = cur.execute(sql) print(result) # result为sql语句执行后生效的行数 # 4、关闭:游标和连接都要关闭 cur.close() conn.close() if result: print('登录成功') else: print('登录失败')
2、execute()之sql注入问题
sql语句的注释:-- 这是注释
一条sql语句如果是select * from userinfo where name='wll' -- haha' and pwd=''
那么-- 之后的内容就被注释掉了(注意:--后面还有一个空格)。
所以,上例中当用户输入如下内容就会出现问题
# sql注入之:用户存在,绕过密码
wll' -- 任意字符
# sql注入之:用户不存在,绕过用户名和密码
xxx' or 1=1 -- 任意字符
原因是我们对sql语句进行字符串拼接时,为%s加了引号,解决方法如下:
# 用execute()帮我们做字符串拼接,无需且一定不能再为%s加引号(因为pymysql会自动加上) sql = "select * from userinfo where name=%s and pwd=%s" result = cur.execute(sql,[username,pwd]) # 第二个参数可以是列表 result = cur.execute(sql,(username,pwd)) # 也可以是元组
# 当execute()的第二个参数是字典时,sql中应该加上key,如下 sql = "select * from userinfo where name=%(key1)s and pwd=%(key2)s" result = cur.execute(sql,{'key1':username,'key2':pwd})
3、pymysql中对数据库增、删、改:conn.commit()
commit()方法:通过pymysql对数据库进行增、删、改时,必须用commit()方法提交,否则无效。
示例:
import pymysql username = input('请输入用户名:') pwd = input('请输入密码:') # 1、连接 conn = pymysql.connect( host = '127.0.0.1', port = 3306, user = 'root', password = '123', db = 'mydb', charset = 'utf8' ) # 2、创建游标对象 cur = conn.cursor() # 3、执行sql语句 # 增 sql = "insert into userinfo(name,pwd) values (%s,%s)" result = cur.execute(sql,[username,pwd]) print(result) # 输出 1 # 同时插入多条数据 effect_row = cur.executemany(sql,[('张三','110'),('李四','119')]) print(effect_row) # 输出 2 # 删 sql = "delete from userinfo where id=1" effect_row = cur.execute(sql) print(effect_row) # 1 # 改 sql = "update userinfo set name=%s where id=2" effect_row = cur.execute(sql,username) print(effect_row) # 1 # 4、增、删、改之后一定要commit conn.commit() # 5、关闭:游标和连接都要关闭 cur.close() conn.close()
4、pymysql中对数据库查询:fetchone()、fetchall()、fetchmany(n)
有如下表内容:
mysql> select * from userinfo; +----+--------+-----+ | id | name | pwd | +----+--------+-----+ | 1 | wll | 123 | | 2 | ssx | 456 | | 3 | 张三 | 123 | | 4 | 李四 | 456 | +----+--------+-----+
示例一:fetchone() – 获取一行数据,第一次为首行
import pymysql conn = pymysql.connect(host='localhost', port=3306, user='root', password='123', db='mydb', charset='utf8') # 此处不能写 utf-8 ,否则报错 cur = conn.cursor() sql = "select * from userinfo" effct_row = cur.execute(sql) print(effct_row) # 4 row = cur.fetchone() # 查询第一行的数据 print(row) # (1, 'wll', '123') row = cur.fetchone() # 从上次位置继续,即查询第二行数据 print(row) # (2, 'ssx', '456') cur.close() conn.close()
示例二:fetchall() - 获取所有行数据
import pymysql conn = pymysql.connect(host='localhost', port=3306, user='root', password='123', db='mydb', charset='utf8') cur = conn.cursor() sql = "select * from userinfo" effct_row = cur.execute(sql) print(effct_row) # 4 rows = cur.fetchall() # 查询所有行的数据 print(rows) # 结果为: # ((1, 'wll', '123'), (2, 'ssx', '456'), (3, '张三', '123'), (4, '李四', '456')) cur.close() conn.close()
总结:从上例中输出结果可以看出,我们获取到的返回值是一个元组,每一行数据也是一个元组,所以我们无法知道数据对应的字段是什么,这个时候,可以通过如下方式将每一行的数据变为一个字典,字典的key就是字段名,value就是对应的值,如下:
# 在实例化游标对象的时候,将属性cursor设置为 pymysql.cursors.DictCursor cur = conn.cursor(cursor=pymysql.cursors.DictCursor) # 结果为: # [ # {'id': 1, 'name': 'wll', 'pwd': '123'}, # {'id': 2, 'name': 'ssx', 'pwd': '456'}, # {'id': 3, 'name': '张三', 'pwd': '123'}, # {'id': 4, 'name': '李四', 'pwd': '456'} # ]
示例三:移动指针位置
fetchone示例中,在获取行数据的时候,可以理解为,刚开始,有一个行指针指着第一行的上方,获取一行,它就向下移动一行,所以当行指针移到最后一行的时候,就不能再获取到内容,所以我们可以使用如下方法来移动行指针:
cur.scroll(1,mode='relative') # 相对当前位置移动 cur.scroll(1,mode='absolute') # 相对首行位置移动
参数解释:
第一个值为移动的行数,正数为向下移动,负数为向上移动;mode指定了是相对当前位置移动,还是相对于首行移动。
代码:
import pymysql conn = pymysql.connect(host='localhost', port=3306, user='root', password='123', db='mydb', charset='utf8') cur = conn.cursor(cursor=pymysql.cursors.DictCursor) sql = 'select * from userinfo' effct_row = cur.execute(sql) row = cur.fetchone() # 查询第一行的数据 print(row) # {'id': 1, 'name': 'wll', 'pwd': '123'} row = cur.fetchone() # 查询第二行数据 print(row) # {'id': 2, 'name': 'ssx', 'pwd': '456'} cur.scroll(-1,mode='relative') row = cur.fetchone() print(row) # {'id': 2, 'name': 'ssx', 'pwd': '456'} cur.scroll(0,mode='absolute') row = cur.fetchone() print(row) # {'id': 1, 'name': 'wll', 'pwd': '123'} cur.close() conn.close()
示例四:fetchmany(n) - 获取n行数据
import pymysql conn = pymysql.connect(host='localhost', port=3306, user='root', password='123', db='mydb', charset='utf8') cur = conn.cursor(cursor=pymysql.cursors.DictCursor) sql = 'select * from userinfo' effct_row = cur.execute(sql) rows = cur.fetchmany(2) # 获取2 条数据 print(rows) # 结果为: # [ # {'id': 1, 'name': 'wll', 'pwd': '123'}, # {'id': 2, 'name': 'ssx', 'pwd': '456'} # ] cur.close() conn.close()
二、索引
1、索引的介绍
数据库中专门用于帮助用户快速查找数据的一种数据结构,类似于字典中的目录,查找字典内容时可以根据目录查找到数据的存放位置,然后直接获取。
2、索引的作用
约束和加速查找。
3、常见的几种索引
1)普通索引:加速查找作用
示例一:创建表的时候设置普通索引
create table userinfo( id int not null auto_increment primary key, name varchar(32) not null, email varchar(64) not null, index ix_name(name) # 设置普通索引 );
示例二:已经创建完表之后单独创建普通索引(慢)
create index 索引的名字 on 表名(列名);
示例三:删除普通索引(快)
drop index 索引名 on 表名;
示例四:查看索引
show index from 表名;
2)唯一索引:加速查找和约束唯一作用(可以为空)
示例一:创建表的时候设置唯一索引
create table userinfo( id int not null auto_increment primary key, name varchar(32) not null, email varchar(64) not null, unique index ix_name(name) # 设置唯一索引(name就有了唯一的约束) );
示例二:单独设置唯一索引
create unique index 索引名 on 表名(列名);
示例三:删除唯一索引
drop index 索引名 on 表名;
3)主键索引:加速查找和约束唯一作用(不可以为空)
示例一:创建表的时候设置主键索引
create table userinfo( id int not null auto_increment primary key, # 设置主键就是主键索引 name varchar(32) not null, email varchar(64) not null, ); 或者 create table userinfo( id int not null auto_increment, name varchar(32) not null, email varchar(64) not null, primary key(id) # 设置主键,就创建主键索引 );
示例二:单独创建主键索引
alter table 表名 add primary key(列名);
示例三:删除主键索引
alter table 表名 drop primary key;
alter table 表名 modify 列名 int, drop primary key;
4)联合索引(多列)
又分为:联合主键索引、联合唯一索引、联合普通索引。
应用场景:频繁的同时使用n列来进行查询,
如:where name = ‘alex’and email = ‘alex@qq.com’;
示例一:创建联合普通索引
create index 索引名 on 表名(列名1,列名2);
4、覆盖现象和合并现象
示例一:查找字段和索引字段相同,则直接在索引文件中获取数据
select name from userinfo where name = 'alex50000'; # 直接索引文件中获取 select * from userinfo where name = 'alex50000'; # 先查索引文件,再查物理表
示例二:多个单列索引同时作为条件时,索引则合并使用
select * from userinfo where name = 'alex13131' and id = 13131;
5、如何正确使用索引
数据库表中添加索引后确实会让查询速度起飞,但前提必须是正确的使用索引来查询,如果以错误的方式使用,则即使建立索引也会不奏效。
使用索引,我们必须遵循以下几点:
1)创建索引;
2)命中索引;
3)正确使用索引;
准备一个含有300w数据的表:
# 1. 准备表 create table userinfo( id int, name varchar(20), gender char(6), email varchar(50) ); # 2. 创建存储过程,实现批量插入记录 delimiter $$ # 声明存储过程的结束符号为$$ create procedure auto_insert1() BEGIN declare i int default 1; while(i<3000000)do insert into userinfo values(i,concat('alex',i),'male',concat('egon',i,'@oldboy')); set i=i+1; end while; END$$ # $$结束 delimiter ; # 重新声明分号为结束符号 # 3. 查看存储过程 show create procedure auto_insert1\G; # 4. 调用存储过程 call auto_insert1();
测试如下查询语句,体会以下不正确使用索引的情况,理解如何正确使用索引:
# 示例一:like '%xx' select * from userinfo where name like '%al'; # 示例二:使用函数 select * from userinfo where reverse(name) = 'alex333'; # 示例三:or select * from userinfo where id = 1 or email = 'alex122@oldbody'; # 注意:当or条件中有未建立索引的列才失效,以下两种会走索引: select * from userinfo where id = 1 or name = 'alex1222'; select * from userinfo where id = 1 or email = 'alex122@oldbody' and name = 'alex112' # 示例四:类型不一致 select * from userinfo where name = 999; # 表中name字段是字符串 # 解释:若某字段是字符串类型,则查询条件中必须带引号,否则即使该字段有索引,速度也很慢 # 示例五:!= select count(*) from userinfo where name != 'alex'; # 注意:如果是主键,则还是会走索引 # 示例六:> select * from userinfo where name > 'alex'; # 注意:如果是主键或者字段是整数类型,则还是会走索引,如下: select * from userinfo where id > 123 select * from userinfo where num > 123 # 示例七:order by select email from userinfo order by name desc; # 注意:当根据索引排序的时候,选择的映射如果不是索引,则不走索引 # 示例八:联合索引最左前缀匹配
PS:什么是最左前缀匹配?
create index ix_name_email on userinfo(name,email); # 创建联合索引,name在左
select * from userinfo where name = 'alex'; # 查找速度快
select * from userinfo where name = 'alex' and email='alex@oldBody'; # 快
select * from userinfo where email='alex@oldBody'; # 慢
分析:如果创建了联合索引,如上边代码,创建name和email联合索引,那么查询
(1)name和email时 -- 使用索引,速度快
(2)name -- 使用索引,速度快
(3)email -- 不使用索引,速度慢
注意:对于同时搜索n个条件时,组合索引的性能 > 多个单列索引合并的性能。
6、使用索引的注意事项
1)避免使用select *;
2)count(1)或count(列) 代替count(*);
3)创建表时尽量使用char代替varchar;
4)表的字段顺序固定长度的字段优先;
5)组合索引代替多个单列索引(经常使用多个条件查询时);
6)尽量使用短索引(create index ix_title on tb(title(16));仅限特殊的数据类型text);
7)使用连接(join)来代替子查询;
8)连表时注意条件类型需一致;
9)索引散列(有重复且种类少)不适用于建索引,例如:性别不合适;
7、执行计划
explain + 查询SQL :用于显示SQL执行信息参数,根据参考信息可以进行SQL优化。如下示例:
mysql> explain select * from userinfo; +----+-------------+----------+------+---------------+------+---------+------+---------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+---------+-------+ | 1 | SIMPLE | userinfo | ALL | NULL | NULL | NULL | NULL | 2973016 | NULL | +----+-------------+----------+------+---------------+------+---------+------+---------+-------+
mysql> explain select * from (select id,name from userinfo where id <20) as A; +----+-------------+------------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+-------+---------------+---------+---------+------+------+-------------+ | 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 19 | NULL | | 2 | DERIVED | userinfo | range | PRIMARY | PRIMARY | 4 | NULL | 19 | Using where | +----+-------------+------------+-------+---------------+---------+---------+------+------+-------------+
参数说明:
select_type(查询类型): SIMPLE --- 简单查询 PRIMARY --- 最外层查询 SUBQUERY --- 映射为子查询 DERIVED --- 子查询 UNION --- 联合 UNION RESULT --- 使用联合的结果 table(正在访问的表名) type(查询时的访问方式): 性能:all < index < range < index_merge < ref_or_null < ref < eq_ref < system/const all --- 全表扫描,对于数据表从头到尾找一遍(如果有limit限制,则找到之后不再向下找); index --- 全索引扫描,对索引从头到尾找一遍; range --- 对索引列进行范围查找; index_merge --- 合并索引,使用多个单列索引搜索; ref --- 根据索引查找一个或多个值; eq_ref --- 连接时使用primary key或unique类型; system --- 系统,表仅有一行(=系统表),这是const连接类型的一个特例; const --- 常量,表最多有一个匹配行,因为仅有一行,在这行的列值可被优化器剩余部分认为是常数,const表很快,因为它们只读取一次; possible_keys(可能使用的索引) key:真实使用的 key_len(MySQL中使用索引字节长度): rows(MySQL估计为了找到所需的行而要读取的行数,只是预估值): extra(该列包含MySQL解决查询的详细信息): Using index --- 此值表示mysql将使用覆盖索引,以避免访问表。不要把覆盖索引和index访问类型弄混了; Using where --- 这意味着mysql服务器将在存储引擎检索行后再进行过滤,许多where条件里涉及索引中的列,当(并且如果)它读取索引时,就能被存储引擎检验,因此不是所有带where子句的查询都会显示“Using where”。有时“Using where”的出现就是一个暗示:查询可受益于不同的索引; Using temporary --- 这意味着mysql在对查询结果排序时会使用一个临时表; Using filesort --- 这意味着mysql会对结果使用一个外部索引排序,而不是按索引次序从表里读取行。mysql有两种文件排序算法,这两种排序方式都可以在内存或者磁盘上完成,explain不会告诉你mysql将使用哪一种文件排序,也不会告诉你排序会在内存里还是磁盘上完成; Range checked for each record(index map: N) --- 这个意味着没有好用的索引,新的索引将在联接的每一行上重新估算,N是显示在possible_keys列中索引的位图,并且是冗余的;
8、慢日志记录
开启慢查询日志,可以让MySQL记录下查询超过指定时间的语句,通过定位分析性能的瓶颈,才能更好的优化数据库系统的性能。
1)进入MySQL查询是否开启了慢查询日志
show variables like 'slow_query%';
参数解释:
slow_query_log:慢查询开启状态(OFF未开启,ON为开启);
slow_query_log_file:慢查询日志存放的位置;
2)查看慢查询超时时间(默认10秒)
show variables like 'long%';
3)开启慢日志方式一:
set global slow_query_log=1; # 1表示开启,0表示关闭
注意:设置关要退出重新进入才生效。
4)开启慢日志方式二(推荐):
修改my.ini配置文件(mac中为my.cnf文件),找到[mysqld],在下面添加:
slow_query_log = 1 slow_query_log_file=C:\mysql-5.6.40-winx64\data\localhost-slow.log long_query_time = 1
参数解释:
slow_query_log:慢查询开启状态,1为开启
slow_query_log_file:慢查询日志存放的位置
long_query_time:查询超过多少秒才记录,默认10秒,修改为1秒
9、分页性能相关方案
先回顾一下,如何取当前表中的前10条记录,每十条取一次,依次得到每页数据,如下:
# 第1页: select * from userinfo limit 0,10; # 第2页: select * from userinfo limit 10,10; # 第3页: select * from userinfo limit 20,10; # 第4页: select * from userinfo limit 30,10; ...... # 第200001页 select * from userinfo limit 2000000,10;
PS:我们会发现,越往后查询,需要的时间约长,此方法要进行全文扫描查询,越往后查,扫描查询的数据越多。
解决方案:
1)只有上一页和下一页的情况
前提:做一个记录,记录当前页的第一条数据min_id或者最后一条数据max_id
# 下一页 select * from userinfo where id>max_id limit 10; # 上一页 select * from userinfo where id<min_id order by id desc limit 10;
2)中间有页码的情况
select * from userinfo where id in( select id from (select * from userinfo where id > pre_max_id limit (cur_max_id-pre_max_id)*10) as A order by A.id desc limit 10 );