2018.11.27数据库索引,元类,单例

一.索引

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__也被拦截了