一、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中对数据库增、删、改:connmit()

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_insert1G;

    # 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 | | 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中为myf文件),找到[mysqld],在下面添加:

slow_query_log = 1
slow_query_log_file=C:mysql-5.6.40-winx64datalocalhost-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
);

更多推荐

【后端】pymysql详细使用说明