MySQL数据库(5)- pymysql的使用、索引

时间:2022-11-04 08:33:00

一、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
    );