zoukankan      html  css  js  c++  java
  • mysql高级、索引

    一.mysql高级

    1.视图

    # 引子
    select * from emp left join dep on emp.dep_id = dep.id
    union
    select * from emp right join dep on emp.dep_id = dep.id;
    
    create view temp(emp_id,emp_name,salary,dep_id,dep_id2,dep_name,work) as 
    select * from emp left join dep on emp.dep_id = dep.id
    union
    select * from emp right join dep on emp.dep_id = dep.id;
    
    # 全连接形成一张新表, 现有查询是基于全连接这张新表, 如何来操作
    # 解决: 将新表其别名full_table, 将需求转换为 select 需求字段 from full_table 条件

    什么是视图?

    视图是由一张表或者多张表的查询结果构成的一张表,这张虚拟的表的作用就是为了查询。

    为什么用视图?

    为了将复杂常用的查询结果保留下来重复使用或者将一张大表拆分成多张小表,就是将复杂问题简单化,提升查询的效率。

    '''
    what: 视图是由一张表或多张表的查询结果构成的一张虚拟表
    why: 将复杂常用的查询结果保留下来重复使用 | 将一张大表拆分成多张小表
    
    语法:
    create [or replace] view 视图名[(查询字段别名们)] as 查询语句
    create view new_emp as (select * from emp);
    
    注:
    1.查询字段别名们 要与 查询语句的查询字段对应
    2.create or replace: 操作视图没有则创建、有则替换
    create or replace view new_emp(id,姓名,工资) as (select id,name,salary from emp where dep_id = 2);
    
    视图的修改:alter 等价于 create or replace, 且语法一致
    alter view new_emp(id,姓名,工资) as (select id,name,salary from emp where dep_id = 1);
    
    视图中字段的操作:不允许alter操作字段
    alter table new_emp rename new_emp1;
    alter view new_emp modify id tinyint;
    
    视图中记录的操作:等价于普通表,完成增删改查
    update new_emp set 姓名='san' where id = 3;
    delete from new_emp where id = 3;
    insert into new_emp(id, 姓名, 工资) values (10, "Bob", 10000); # 操作的是实体表, 虚拟表要重新创建才能拿到最新数据
    
    视图的删除:
    drop view 视图名;
    
    总结: 虚拟表作用 -- 查询
    '''

    2.触发器

    什么是触发器?

    在表发生数据更新时(update,delete,insert),会自动触发的功能称之为触发器。

    为什么用触发器?

    当一个表在发生数据更新时,需要去完成一些额外的操作,可以为具体数据更新的方式添加触发器。

    '''
    what:在表发生数据更新时,会自动触发的功能称之为触发器
    why:当一个表在发生数据更新时,需要去完成一些操作,可以为具体数据更新的方式添加触发器
    
    语法:
    delimiter //
    create trigger 触发器名 before|after insert|update|delete on 表名 for each row
    begin 
        需要触发执行的sql代码们
    end //
    delimiter ;
    
    # 触发器名: t1_before_insert_tri
    
    注:delimiter是用来修改sql的语句结束标识符
    
    删除触发器:drop trigger 触发器名;
    '''
    # cmd表
    create table cmd (
        id int primary key auto_increment,
        user char(32),
        priv char(10),
        cmd char (64),
        sub_time datetime, # 提交时间
        success enum ('yes', 'no') # 0代表执行失败
    );
    # 错误日志表
    create table errlog (
        id int primary key auto_increment,
        err_cmd char(64),
        err_time datetime
    );
    # 创建触发器
    delimiter //
    create trigger trigger1 after insert on cmd for each row
    begin
    # new就是cmd当前插入的那条记录(对象)
    if new.success = "no" then
        insert into errlog values(null, new.cmd, new.sub_time);
    end if;
    end //
    delimiter ;
    
    # 往表cmd中插入记录,触发触发器,根据IF的条件决定是否插入错误日志
    insert into cmd(user, priv, cmd, sub_time, success) values
        ('egon', '0765', 'ls -l /etc', now(), 'yes'),
        ('jerry', '0852', 'cat /etc/passwd', now(), 'no'),
        ('kevin', '0867', 'useradd xxx', now(), 'no'),
        ('owen', '0912', 'ps aux', now(), 'yes');
    # 查看cmd数据信息
    select * from cmd;
    # 查看错误日志表中的记录是否有自动插入
    select * from errlog;

    3.事务

    什么是事务?

    事务是逻辑上的一组操作,要么都成功,要么都失败。

    为什么有事务?

    很多时候一个数据操作,不是一个sql语句就完成的,可能有很多个sql语句,如果部分sql执行成功而部分sql执行失败将导致数据错乱

    比如:转账 => 转入转出都成功,才能认为操作成功

    '''
    what:事务是逻辑上的一组操作,要么都成功,要么都失败
    why:很多时候一个数据操作,不是一个sql语句就完成的,可能有很多个sql语句,如果部分sql执行成功而部分sql执行失败将导致数据错乱
    eg:转账 => 转入转出均成功,才能认为操作成功
    
    事务的使用:
    start transaction; --开启事物,在这条语句之后的sql将处在同一事务,并不会立即修改数据库
    commit;--提交事务,让这个事物中的sql立即执行数据的操作,
    rollback;--回滚事务,取消这个事物,这个事物不会对数据库中的数据产生任何影响
    
    事务的四大特性:
    1.原子性:事务是一组不可分割的单位,要么同时成功,要么同时不成功
    2.一致性:事物前后的数据完整性应该保持一致(数据库的完整性:如果数据库在某一时间点下,所有的数据都符合所有的约束,则称数据库为完整性的状态)
    3.隔离性:事物的隔离性是指多个用户并发访问数据时,一个用户的事物不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离
    4.持久性:持久性是指一个事物一旦被提交,它对数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
    
    事务的用户隔离级别:
    数据库使用者可以控制数据库工作在哪个级别下,就可与防止不同的隔离性问题
    read uncommitted --不做任何隔离,可能脏读,幻读
    read committed --可以防止脏读,不能防止不可重复读,和幻读, 
    Repeatable read --可以防止脏读,不可重复读,不能防止幻读
    Serializable --数据库运行在串行化实现,所有问题都没有,就是性能低
    
    修改隔离级别:
    select @@tx_isolation;--查询当前级别 
    set[session|global] transaction isolation level ....;修改级别
    实例:
    set global transaction isolation level Repeatable read;
    注:修改后重新连接服务器生效
    '''
    #准备数据
    create table account(
        id int primary key auto_increment,
        name varchar(20),
        money double
    );
    insert into account values
        (1,'owen',10000),
        (2,'egon',1000),
        (3,'jerry',1000),
        (4,'kevin',1000);
    
    # egon向owen借1000块钱
    # 未使用事务
    update account set money = money - 1000 where id = 1;
    update account set moneys = money + 1000 where id = 2; # money打错了导致执行失败
    
    # 在python中使用事务处理
    from pymysql.err import InternalError
    sql = 'update account set money = money - 1000 where id = 1;'
    sql2 = 'update account set moneys = money + 1000 where id = 2;' # money打错了导致执行失败
    try:
        cursor.execute(sql)
        cursor.execute(sql2)
        conn.commit()
    except InternalError:
        print("转账失败")
        conn.rollback()

    4.存储过程

    什么是存储过程?

    用于完成指定功能的sql语句,类似于python中的函数

    为什么要存储过程?

    将指定功能的sql语句块建立成存储过程,不仅将sql语句逻辑化了,更是功能化了,那我们要完成相同的事,只需要重复使用建立的存储过程,就不需要再重复书写sql语句了。

    总结:存储过程可以让sql语句具有复用性,从而提高开发效率。

    语法:
    delimiter //
    create procedure 存储过程名(
        输入输出类型1 参数名1 参数类型1(宽度), 
        ... ,
        输入输出类型n 参数名n 参数类型n(宽度)
    )
    begin
    sql语句块
    end //
    delimiter ;
    注:
    1.输入输出类型:in | out | inout
    2.call 存储过程名(实参们)来调用存储过程
    
    案例:
    set @res = null; # 定义空值变量, 用来接收存储过程的执行结果
    delimiter //
    create procedure user_info(in b int, in l int, out res char(20))
    begin
    select * from emp limit b, l;
    set res = 'success';
    end //
    delimiter ;
    call user_info(2, 3, @res); # 调用存储过程, 传入相应的实参
    select @res; # 查看存储过程的执行结果
    
    变量的使用:
    1.赋值变量:set @变量名 = 变量值
    2.使用变量:@变量名 | select @变量名
    3.删除变量:set @变量名 = null
    
    三种开发方式:
    1. 业务逻辑 + 存储过程:高执行与开发效率,低耦合 | 不易移植,人员成本高
    2. 业务逻辑 + 原生sql:人员成本低 | 开发难度大
    3. 业务逻辑 + ORM:高开发效率,对象化操作数据库,可移植 | 性能消耗加大,多表联查、复杂条件会复制化ORM
    
    存储过程的操作:
    1.查看
    select routine_name, routine_type from information_schema.routines where routine_schema='数据库名';
    
    eg: select routine_name, routine_type from information_schema.routines where routine_schema='db2';
    
    2.删除
    drop procedure [if exists] 数据库名.存储过程名

    5.流程控制

    1.if 语句的使用

    第一种 if:
    """
    if 条件 then
    语句;
    end if;
    """
    第二种 if elseif
    """
    if 条件  then
    语句1;
    elseif 条件 then
    语句2;
    else 语句3;
    end if;
    """
    # 案例:编写过程 实现 输入一个整数type  范围 1 - 2 输出  type=1 or type=2 or type=other;
    delimiter //
    create procedure showType(in type int,out result char(20))
    begin
    if type = 1 then 
    set result = "type = 1";
    elseif type = 2 then 
    set result = "type = 2";
    else 
    set result = "type = other";
    end if;
    end //
    delimiter ;
    
    set @res=null;
    call showType(100, @res);
    select @res;

    2.CASE语句

    大体意思与Swtich一样的 你给我一个值 我对它进行选择 然后执行匹配上的语句 语法:

    create procedure caseTest(in type int)
    begin
    CASE type 
    when 1  then select "type = 1";
    when 2  then select "type = 2";
    else select "type = other";
    end case;
    end

    3.定义变量

    declare 变量名 类型 default 值; 例如: declare i int default 0;

    4.WHILE循环

    循环输出10次hello mysql
    create procedure showHello()
    begin 
    declare i int default 0;
    while  i < 10 do
    select "hello mysql";
    set i  = i + 1;
    end while;
    end

    5.LOOP循环

    没有条件,需要自己定义结束语句

    语法:

    输出十次hello mysql;
    create procedure showloop()
    begin 
    declare i int default 0;
    aloop: LOOP
    select "hello loop";
    set i = i + 1;
    if i > 9 then leave aloop;
    end if;
    end LOOP aloop;
    end

    6.REPEAT循环

    #类似do while
    #输出10次hello repeat
    create procedure showRepeat()
    begin
    declare i int default 0;
    repeat
    select "hello repeat";
    set i = i + 1;
    until i > 9
    end repeat;
    end
    
    #输出0-100之间的奇数
    create procedure showjishu()
    begin
    declare i int default 0;
    aloop: loop
    set i = i + 1;
    if i >= 101 then leave aloop; end if;
    if i % 2 = 0 then iterate aloop; end if;
    select i;
    end loop aloop;
    end

    二.mysql索引

    1.什么是索引?

    在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构; 也称之为key

    索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。

    2.为什么需要索引?

    思考:一个项目正常运行后,对数据库的操作中,哪些操作是最频繁的? 对数据库的写操作(增加 删除 修改)频繁吗? 对数据库的读操作(查询)频繁吗?

    相比较下,对数据的读操作会更加频繁,比例在10:1左右,也就是说对数据库的查询操作是非常频繁的,随着时间的推移,表中的记录会越来越多,此时如果查询速度太慢的话对用户体验是非常不利的,索引是提升查询效率最有效的手段。简单的说索引的就是用帮我们加快查询速度的

    需要注意的是:在数据库中插入数据会引发索引的重建

    ​3.索引的实现原理

    如何能实现加快查询的效果呢?

    来看一个例子:

    第一版的新华字典共800页,那时没有检字表,每个字的详细信息,随机的罗列在书中,一同学买回来查了一次,在也没用过,因为没有任何的数据结构,查字只能一页一页往后翻,反了两小时没翻着,只能放弃了!

    后来出版社发现了这个问题,他们将书中所有字按照拼音音节顺序进行了排序,拼音首字母为a的排在最前,首字母为z的排在最后:

    如此一来再不再需要一页一页的去查字了,而是先查看索引,找出字的拼音首字母到索引中进行对照,例如:找字其拼音首字母为d,所以直接找到D对应的索引目录,很快就能定位到要找的字在79页,查询速度得到数量级的提升! 需要注意的是,原来内容为800页现在因为多了索引数据,整体页数必然增加了

    数据库中的索引,实现思路与字典是一致的,需要一个独立的存储结构,专门存储索引数据

    本质上索引是通过不断的缩小查询范围来提高查询效率

    磁盘IO问题(了解)

    数据库的数据最终存储到了硬盘上

    机械硬盘由于设计原理,导致查找数据时需要有一个寻道时间与平均延迟时间,常规硬盘寻道为5ms,平均延迟按照每分钟7200转来计算,7200/60 = 120 ; 1000/120/2 = 4ms 总共为9ms,那么9毫秒对于cpu而言已经非常非常的长了,足够做很多运算操作,目前最新的处理器每秒能处理数万亿次运算,拿一个非常垃圾的处理器来举例子,假设处理器每秒处理5亿次计算,每毫秒是50万次运算,9ms可以进行450万次运算,数据库中成千上万的数据,每条数据9ms显然慢到不行!

    操作系统预读取(了解)

    考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不仅获取当前磁盘地址的数据,而且把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。

    索引数据结构剖析

    在字典的例子中我们知道了,索引是独立于真实数据的一个存储结构,这个结构到底是什么样的?

    索引最终的目的是要尽可能降低io次数,减少查找的次数,以最少的io找到需要的数据,此时B+树闪亮登场

    光有数据结构还不行,还需要有对应的算法做支持,就是二分查找法

    有了B+数据结构后查找数据的方式就不再是逐个的对比了,而是通过二分查找法来查找(流程演示)

    另外,其实大多数文件系统都是使用B+是来完成的!

    应该尽可能的将数据量小的字段作为索引

    通过分析可以发现在上面的树中,查找一个任何一个数据都是3次IO操作, 但是这个3次并不是固定的,它取决于数结构的高度,目前是三层,如果要存储新的数据比99还大的数据时,发现叶子节点已经不够了必须在上面加一个子节点,由于树根只能有一个,所以整个数的高度会增加,一旦高度增加则 查找是IO次数也会增加,所以:应该尽可能的将数据量小的字段作为索引,这样一个叶子节点能存储的数据就更多,从而降低树的高度;例如:nameid,应当将id设置为索引而不是name

    最左匹配原则*

    当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候(多字段联合索引),b+树会按照从左到右的顺序来建立搜索树,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。

    聚集索引*

    mysql官方文档原文: 插入了解 或折叠

    MySQL为表把它的数据词典信息以.frm文件的形式存在数据库目录里,这对所有MySQL存储引擎都是真的。但 是每个InnoDB表在表空间内的InnoDB内部数据词典里有它自己的条目。当MySQL移除表或数据库,它不得不 删除.frm文件和InnoDB数据词典内的相应条目。这就是为什么你不能在数据库之间简单地移动.frm文件来移 动InnoDB表。

    每个InnoDB表有专门索引,被称为clustered index,对行的数据被存于其中。如果你对你的表定义一 个PRIMARY KEY, 主键的索引是集束索引。

    如果你没有为表定义PRIMARY KEY,MySQL拾取第一个仅有NOT NULL列的UNIQUE索引作为主键,并 且InnoDB把它当作集束索引来用。如果表中没有这样一个索引,InnoDB内部产生一个集束索引,其中 用InnoDB在这样一个表内指定给行的行ID来排序行。行ID是一个6字节的域,它在新行被插入的时候简单地增加。因此被行ID排序的行是物理地按照插入顺序排的。

    通过集束索引访问一个行是较快的,因为行数据是在索引搜索引导的同一页面。如果表是巨大的,当对比于传 统解决方案,集束索引构架经常节约磁盘I/O。(在许多数据库,数据传统地被存在与索引记录不同的页)。

    在InnoDB中,非集束索引里的记录(也称为第二索引)包含对行的主键值。InnoDB用这个 主键值来从集束索 引中搜索行。注意,如果主键是长的,第二索引使用更多空间。

    聚焦索引的特点:

      叶子节点保存的就是完整的一行记录,如果设置了主键,主键就作为聚集索引,

      如果没有主键,则找第一个NOT NULL 且QUNIQUE的列作为聚集索引,

      如果也没有这样的列,innoDB会在表内自动产生一个聚集索引,它是自增的

    辅助索引*

    除了聚集索引之外的索引都称之为辅助索引或第二索引,包括 foreign keyunique

    辅助索引的特点:

    其叶子节点保存的是索引数据与所在行的主键值,InnoDB用这个 主键值来从聚集索引中搜查找数据

    覆盖索引

    覆盖索引指的是需要的数据仅在辅助索引中就能找到:

    #假设stu表的name字段是一个辅助索引
    select name from stu where name = "jack";

    这样的话则不需要在查找聚集索引数据已经找到

    回表

    如果要查找的数据在辅助索引中不存在,则需要回到聚集索引中查找,这种现象称之为回表

    # name字段是一个辅助索引 而sex字段不是索引 
    select sex from stu where name = "jack";

    需要从辅助索引中获取主键的值,在拿着主键值到聚集索引中找到sex的值

    查询速度对比:

    聚集索引 > 覆盖索引 > 非覆盖索引

    4.正确使用索引

    # 首先准备一张表数据量在百万级别
    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字段的区分度非常低 完全无法区分 ,因为值都相同 这样一来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.有多个匹配条件时 索引的执行顺序  andor
    #先看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两边都有索引才会使用索引 
    
    
    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;
    #联合索引
    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;
    案例
  • 相关阅读:
    POJ 1451
    LightOJ 1224
    POJ 2001
    HDU 2072
    POJ 3764
    CH 1602
    CH 1601
    Gym 101873K
    CH 1201
    Gym 101873C
  • 原文地址:https://www.cnblogs.com/wangke0917/p/10260243.html
Copyright © 2011-2022 走看看