一.索引
1.1什么是索引
索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容
搜索引导, 所以是一种单独的,物理的 有序的 存储结构,用于加速查询
例如: 字典 书的目录 车票上的车厢号
1.2为什么需要索引
一个项目正常运行后,对数据库的操作中,哪些操作是最频繁的?
数据的读操作会更加频繁,比例在10:1左右,也就是说对数据库的查询操作是非常频繁的
随着时间的推移,表中的记录会越来越多,此时如果查询速度太慢的话对用户体验是非常不利
因为项目运行中,查询操作非常频繁,为了提高用户体验,要提高查询的速度,
简单的说索引的就是用帮我们加快查询速度的
1.3索引带来的影响
1.加速查询
2.降低写入(增加 删除 修改)速度
3.会额外占用磁盘空间
1.4索引数据结构剖析
索引最终的目的是要尽可能降低io次数,减少查找的次数,以最少的io找到需要的数据,此时B+树闪亮登场
光有数据结构还不行,还需要有对应的算法做支持,就是二分查找法
有了B+数据结构后查找数据的方式就不再是逐个的对比了,而是通过二分查找法来查找
1.5聚集索引
1.聚集索引 就是主键索引
行中的完整记录存在聚集索引的叶子节点上
叶子节点保存的就是完整的一行记录,如果设置了主键,主键就作为聚集索引,
如果没有主键,则找第一个NOT NULL 且QUNIQUE的列作为聚集索引,
1.6辅助索引 除了主键的索引
除了聚集索引之外的索引都称之为辅助索引或第二索引,包括 foreign key
与 unique
叶子节点 存储索引字段的值 还有 主键的值
使用辅助索引时 会产生两种现象
1.回表 要查的数据就不在辅助索引中 需要到聚集索引中查找
(获得所在行的主键值回到聚集索引再取查找其他字段称为回表)
2.覆盖索引 要查的数据就在辅助索引中
查询速度对比:
聚集索引 > 覆盖索引 > 非覆盖索引(回表)
1.7正确使用索引
案例:
首先准备一张表数据量在百万级别
create table usr(id int,name char(10),gender char(3),email char(30));
#准备数据
delimiter //
create procedure addData(in num int)
begin
declare i int default 0;
while i < num do
insert into usr values(i,"jack","m",concat("xxxx",i,"@qq.com"));
set i = i + 1;
end while;
end//
delimiter ;
#执行查询语句 观察查询时间
select count(*) from usr where id = 1;
#1 row in set (3.85 sec)
#时间在秒级别 比较慢
1.
#添加主键
alter table usr add primary key(id);
#再次查询
select count(*) from usr where id = 1;
#1 row in set (0.00 sec)
#基本在毫秒级就能完成 提升非常大
2.
#当条件为范围查询时(范围查询不能优化必须把所有表查询一边)
select count(*) from usr where id > 1;
#速度依然很慢 对于这种查询没有办法可以优化因为需要的数据就是那么多
#缩小查询范围 速度立马就快了
select count(*) from usr where id > 1 and id < 10;
#当查询语句中匹配字段没有索引时 效率测试
select count(*) from usr where name = "jack";
#1 row in set (2.85 sec)
# 速度慢
3.
# 为name字段添加索引
create index name_index on usr(name);
# 再次查询
select count(*) from usr where name = "jack";
#1 row in set (3.89 sec)
# 速度反而降低了 为什么?(name字段里所有的记录都是jack所以相同的太多添加反而增加I0操作所以速度反而低了)
#由于name字段的区分度非常低 完全无法区分 ,因为值都相同 这样一来B+树会没有任何的子节点,像一根竹竿每一都匹配相当于,有几条记录就有几次io ,所有要注意 区分度低的字段不应该建立索引,不能加速查询反而降低写入效率,
#同理 性别字段也不应该建立索引,email字段更加适合建立索引
# 修改查询语句为
select count(*) from usr where name = "aaaaaaaaa";
#1 row in set (0.00 sec) 速度非常快因为在 树根位置就已经判断出树中没有这个数据 全部跳过了
# 模糊匹配时
select count(*) from usr where name like "xxx"; #快(树根位置就判断出没有这个数据)
select count(*) from usr where name like "xxx%"; #快(树根位置就判断出没有这个数据)
select count(*) from usr where name like "%xxx"; #慢(所有都匹配了一边)
#由于索引是比较大小 会从左边开始匹配 很明显所有字符都能匹配% 所以全都匹配了一遍
4.索引字段不能参加运算
select count(*) from usr where id * 12 = 120;
#速度非常慢原因在于 mysql需要取出所有列的id 进行运算之后才能判断是否成立
#解决方案
select count(*) from usr where id = 120/12;
#速度提升了 因为在读取数据时 条件就一定固定了 相当于
select count(*) from usr where id = 10;
#速度自然快了
5.有多个匹配条件时 索引的执行顺序 and 和 or
#先看and
#先删除所有的索引
alter table usr drop primary key;
drop index name_index on usr;
#测试
select count(*) from usr where name = "jack" and gender = "m" and id = 1 and email = "xxxx2@qq.com";
#1 row in set (1.34 sec) 时间在秒级
#为name字段添加索引
create index name_index on usr(name);
#测试
select count(*) from usr where name = "jack" and gender = "m" and id = 1 and email = "xxxx2@qq.com";
#1 row in set (17.82 sec) 反而时间更长了(记录内容相同太多区分度低)
#为gender字段添加索引
create index gender_index on usr(gender);
#测试
select count(*) from usr where name = "jack" and gender = "m" and id = 1 and email = "xxxx2@qq.com";
#1 row in set (16.83 sec) gender字段任然不具备区分度(记录内容相同太多区分度低)
#为id加上索引
alter table usr add primary key(id);
#测试
select count(*) from usr where name = "jack" and gender = "m" and id = 1 and email = "xxxx1@qq.com";
#1 row in set (0.00 sec) id子弹区分度高 速度提升
#虽然三个字段都有索引 mysql并不是从左往右傻傻的去查 而是找出一个区分度高的字段优先匹配
#改为范围匹配
select count(*) from usr where name = "jack" and gender = "m" and id > 1 and email = "xxxx1@qq.com";
#速度变慢了
#删除id索引 为email建立索引
alter table usr drop primary key;
create index email_index on usr(email);
#测试
select count(*) from usr where name = "jack" and gender = "m" and id = 1 and email = "xxxx2@qq.com";
#1 row in set (0.00 sec) 速度非常快(记录内容区分度高)
#对于or条件 都是从左往右匹配
select count(*) from usr where name = "jackxxxx" or email = "xxxx0@qq.com";
#注意 必须or两边都有索引才会使用索引
使用or的时候 如果两边都有索引 会使用索引,但是注意 or两边都要执行
顺序依然从左往右. 只有一边有索引会不会使用索引? 不会使用 无法加速查询
6.多字段联合索引
为什么需要联合索引
案例:
select count(*) from usr where name = "jack" and gender = "m" and id > 3 and email = "xxxx2@qq.com";
假设所有字段都是区分度非常高的字段,那么除看id为谁添加索引都能够提升速度,但是如果sql语句中没有出现索引字段,那就无法加速查询,最简单的办法是为每个字段都加上索引,但是索引也是一种数据,会占用内存空间,并且降低写入效率
此处就可以使用联合索引,
联合索引最重要的是顺序
按照最左匹配原则
应该将区分度高的放在左边
区分度低的放到右边
#删除其他索引
drop index name_index on usr;
drop index email_index on usr;
drop index gender_index on usr;
#联合索引
create index mul_index on usr(email,name,gender,id);
# 查询测试
select count(*) from usr where name = "xx" and id = 1 and email = "xx";
只要语句中出现了最左侧的索引(email) 无论在前在后都能提升效率
drop index mul_index on usr;
索引总结:
索引的正确使用姿势
命中索引 ,条件中有索引字段匹配上
0优先使用聚集索引
1.无论索引如何设计 无法降低范围查询的查询速度
select count(*) from usr where id > 1;
即使命中索引也无法提高效率
2.索引不应该加在值重复度很高的字段上 应该加在重复度低的字段,索引的字段数据量应该尽可能小
3. 使用and时 当 条件中出现多个索引命中时 会自定找一个区分度最高的索引来使用
4.使用or的时候 如果两边都有索引 会使用索引,但是注意 or两边都要执行 顺序依然从左往右,只有一边有索引会不会使用索引? 不会使用 无法加速查询
5.优化查询 不仅仅要加索引,sql语句也需要优化 使其能命中索引
你的条件中应该使用区别度高的索引
6.联合索引
为是什么使用它
降低资源的占用 , 降低增删改的时间 会比单个字段的索引快
建立联合索引时 应该把区分度高放最左边 区分度低的依次往右放
按照区分度的高低 从左往右 依次排列
查询中 把区分度高放左边 查询时 尽可能使用最左边的索引
使用and时 无所谓书写顺序 会自动找区分度最高的
注意联合索引在查询时 如果压根没用到最左侧索引 不能加速查询
缺点
索引越多越好?
索引能够提升效率
同时降低了写入速度
增加额外的磁盘占用
正常开发时*******
优先使用聚集索引
再次 使用联合索引 如果你的条件不包含最左侧索引 不能加速查询 这时候就应该使用单个字段索引
二. 元类
class Dog:
def __init__(self):
print("狗初始化了")
color = "red"
def talk(self):
print("狗在叫!")
d1 = Dog()
#print(Dog.__dict__)
#d1.talk()
# 查看对象时哪个类实例化出来的
print(d1.__class__)# <class '__main__.Dog'>
print(Dog.__class__)<class 'type'>
print(type(d1)) # <class '__main__.Dog'>
print(type(Dog)) <class 'type'>
可以推导出===>产生DOG的过程一定发生了:Teacher=type(...)
用于实例化产生类的类称之为元类就是此时的type类;所有类产生时都被tpye实例化
DOG是通过type实例化得到的,既然如此,是不是可以自己调用type来实例化一个calss呢?
2.创建类的流程分析
class关键字在帮我们创建类时,必然帮我们调用了元类Teacher=type(...),那调用type时传入的参数是什么呢?必然是类的关键组成部分,一个类有三大组成部分,分别是
1、 类名class_name='Teacher
2、 基类class_bases=(object,)
3、 类的名称空间class_dic,类的名称空间是执行类体代码而得到的
class_name = "pig"
bases = (object,)
pic_dict = {}
class_body = """
def __init__(self):
print("猪初始化了")
color = "red"
def talk(self):
print("猪在叫!")
"""
# 执行一堆字符串代码 将生产的内容放到pic_dict中
exec(class_body,{},pic_dict)#
# print(pic_dict,type(pic_dict))
# 调用type产生一个类
c = type(class_name,bases,pic_dict)
print(c)
print(Dog)#和上面自己定义一个类输出的结果是一样的
"""
默认情况下 所有的类都是通过type这个元类示例化的
我们完全可以自己来实例化
元类的作用?
用于创建类的类 称为元类
"""
3.通过元类来控制类的创建过程
class MyMetaclass(type):
# 什么时候执行? MyMetaclass 定义一个类是 系统会自动去调用元类
def __init__(cls, class_name, bases, namespace):
# 既然创建类时 会自动执行 该方法 那完全编写一些逻辑在init中 来控制类的创建
# 首字母必须大写否则不让创建
if not class_name.istitle():
raise TypeError("类名首字母必须大写!")
if object not in bases:
raise TypeError("必须显式的继承object")
print(cls)
print(class_name)
print(bases)#object写在定义类里
print(namespace)
# 当求解释器执行到这行diamante时 自动调用了MyMetaclass
class Foo(metaclass=MyMetaclass): # 等同于 Foo = MyMetaclass()
attr = 123
pass
class foo(metaclass=MyMetaclass):#首字母小写报错
attr = 123
pass
class Foo1(object,metaclass=MyMetaclass): # 等同于 Foo = MyMetaclass()
attr = 123
pass
4自定义元类控制类的调用
控制类的调用过程 关键在于call函数, 类也是对象,调用类必然也会执行call函数
Class Bar:
def __call__(self, *args, **kwargs):
print("run call")
# 调用类时 还是 调用对象时执行
b1 = Bar()
b1()
print(b1)
# 在调用对象时 自动触发__call__的执行
# 推导 b1是Bar的实例 调用b1 会触发Bar中的__call__
# Bar 是type的实例 调用Bar 应当触发type中的__call__
# 类中的__init__也被拦截了
class MyMetaClass(type):
def __call__(self, *args, **kwargs):
print("MyMetaClass __call__ run!")
print(self)
#需求 判断实例化时的参数必须是字符串类型
#isinstance(args[0],str)
if type(args[0]) != str:
raise TypeError("姓名必须是字符串类型!")
# 这是自定义元类时 必须要有的模板 以保证可以正常实例化产生对象
obj = object.__new__(self)
obj.__init__(*args,**kwargs)#被拦截
return obj
class Foo(metaclass=MyMetaClass):
def __init__(self,name):
self.name = name
pass
# 调用类本质上就是在调用__call__ 其返回值表示实例化得到的对象
res = Foo("a")
print(res)
# 调用一个类 创建出一个空对象,调用__init__来完成对象的初始化,返回该对象
# 控制类的调用 也就是实例化过程 核心函数 元类中的__call__
# 需要注意的是,在__call__中应当先完成基础的逻辑 1.创建空对象,2.执行__init__ 3.返回新对象
# 在此基础上添加额外的业务逻辑
三 单例
什么是单例,
单例是指的是单个实例,指的是 一个类有且仅有一个实例 就叫单例为什么要用单例
实现单例 就通过判断是否已经创建过对象
为什么要用单例
当一个类的实例中的数据不会变化时使用单例,数据是不变的
例如开发一个音乐播放器程序,音乐播放器可以封装为一个对象,那你考虑一下,当你切歌的时候,是重新创建一个播放器,还是使用已有的播放器?
因为播放器中的数据和业务逻辑都是相同的没有必要创建新的,所以最好使用单例模式,以节省资源,
class User:
def __init__(self,name,age,sex):
self.name = name
self.age = age
self.sex = sex
instance = None
# 通过指定的方法来获取实例 而不是调用类来创建新对象
@classmethod
def get_instance(cls,name,age,sex):
if not cls.instance:
cls.instance = cls(name,age,sex)
return cls.instance
@staticmethod
def get_instance2(name,age,sex):
if not User.instance:
User.instance = User(name,age,sex)
return User.instance
# u1 = User("张三",20,"man")
# u2 = User("张三",20,"man")
u1 = User.get_instance("张三",20,"man")
print(u1)# <__main__.User object at 0x0038CFF0>
#通过方法可以保证u1和u2对象的一致,但不能阻止使用者直接用类来生成新的对象.
u2 = User.get_instance2("张三",20,"man")
print(u2)# <__main__.User object at 0x0038CFF0>
# 通过classMethod 可以完成单例 但是还是可以通过直接调用;类产生新对象 此时就需要用到元类
u3 = User("张三",20,"man")#<__main__.User object at 0x003A3030>
#u3和上面u1.u2不一致使用者还是可以通过直接调用类产生新对象
该方法无法避免使用者直接调用类来实例化,这样就不是单例了
使用元类创造单利
class MyMetaClass(type):
instance = None
def __call__(cls, *args, **kwargs):
if not MyMetaClass.instance:
# 创建空对象
MyMetaClass.instance = object.__new__(cls)
print("创建新的播放器对象!")
#初始化对象
MyMetaClass.instance.__init__(*args,**kwargs)
#arg收到的参数是下面init,属性里定义的参数一致,
# 返回对象
return MyMetaClass.instance
# 只能有一个播放器实例
class CDPlayer(metaclass=MyMetaClass):
def play(self,music):
print("切换音乐",music)
def __init__(self,music_name):
self.music_name = music_name
p1 = CDPlayer("你发如雪!")
p2 =CDPlayer("夜曲")
print(p1)# <__main__.CDPlayer object at 0x008B90B0>
print(p2)# <__main__.CDPlayer object at 0x008B90B0>
p1.play("菊花台")
p1.play("菊花台2")
p1.play("菊花台3")
# 元类实现单例 就是拦截了元类中的__call__的正常执行 使得创建对象都必须经过自己判断逻辑(这样就不能通过类(CDPlay)创建新的对象)
# 类中的__init__也被拦截了