Oracle to MySQL数据库迁移方案

 

Oracle to MySQL数据库迁移方案

 

1 信息收集

1.1 迁移指标

1.1.1 是否允许停机

1.1.2 是否考虑回退

1.2 Oracle数据库基本信息收集

2.1.1 数据库基本信息收集

    针对待迁移的数据库进行基本信息采集,了解当前数据库的运行状态、规模以及架构等,可对Oracle数据库有一个基础的判断,是数据迁移工作的首要步骤。

 

  • 数据库版本号

n  了解当前数据库的版本信息,以便针对版本进行数据迁移工具选型及与MySQL兼容性对比

  • 数据库字符集

n  了解当前数据库层字符集设定,以便在数据迁移过程中适配相应字符集防止数据乱码及保持MySQL端字符集兼容

  • 数据库操作系统平台

n  了解当前数据库操作系统平台,以便针对不同平台进行迁移软件选型评估

n  部分迁移软件对操作系统类型、版本有相应限制

  • 数据库名称

n  了解待迁移的database或是schema名称,以此与MySQL端一一对应

n  需要注意的是Oracle端一个用户或schema通常对应MySQL端一个数据库

  • 数据库归档模式

n  了解数据库是否开启归档,以便确定在做迁移时是否会涉及数据库重启操作

n  如需要基于REDO LOG/ARCHIVE LOG进行数据迁移则必须开启数据库归档

  • 数据库架构

n  了解当前数据库高可用架构,如是否为RAC架构,是否为DataGuard复制架构

n  基于不同架构可灵活调整数据迁移方案以及是否涉及到数据库停机时间

n  如存在DataGuard架构则数据迁移可基于DataGuard节点进行数据抽取及同步

  • 数据库大小

n  了解当前数据库总大小以及待迁移的数据库大小

n  用于评估迁移的复杂度、时间以及空间占用

  • 数据库中大表最大记录数及TOP10大表

n  了解当前数据库中待迁移表是否存在大表以及大表的数据量规模

n  用来评估是否需要针对大表进行额外处理

n  如数据库总大小为1T,其中一个访问日志流水表占用300G,经过评估分析可不进行迁移或通过部分迁移的方式抽取数据。

2.1.2 业务涉及的数据库对象复杂性

    针对待迁移的数据库内部对象,需要进行统计分析,了解当前数据库迁移复杂度,数据库对象是否与业务逻辑存在耦合关系,该部分的复杂及耦合程度的高低是评估迁移难易程度的重要依据。

 

存储过程数量及复杂度

n  统计当前数据库中存储过程的数量(包括统计出失效存储过程的数量)、调用频率以及复杂程度

n  用来评估存储过程是否涉及业务逻辑、是否可拆分或使用业务代码实现

n  MySQL端不建议使用包含业务逻辑的存储过程,增加业务与数据库的耦合性

触发器数量及复杂度

n  统计当前数据库中触发器的数量(包括失效的数量)、调用频率及复杂程度

n  用来评估触发器是否涉及业务逻辑、是否可拆分或使用业务代码实现

n  MySQL端不建议使用包含业务逻辑的触发器,增加业务与数据库的耦合性

视图数量及复杂度

n  统计当前数据库视图数量及应用场景

n  用来评估是否可进行视图拆分及用业务代码实现

n  MySQL端不建议使用包含业务逻辑的视图,增加业务与数据库的耦合性

物化视图数量及复杂度

n  统计当前数据库中物化视图的数量及应用场景

n  用来评估是否需要针对物化视图进行改造或使用业务代码实现

n  MySQL端不支持物化视图,对于这类场景数据可采用缓存形式进行改造

自定义序列数量及使用逻辑

n  统计当前数据库中自定义序列的使用场景及数量

n  用来评估是否可基于MySQL自增序列进行改造或基于业务代码层生成序列值

DBLINK数量

n  统计当前数据库是否存在DBLINK及DBLINK的数量

n  用来评估是否存在多系统相互依赖或不同数据库相关迁移表之间的依赖关系

n  如业务涉及DBLINK至其他数据库中库表,则需要一并进行迁移并进行业务改造

分析类SQL语句数量及执行效率

n  统计是否存在分析类业务(OLAP)以及该类业务的执行频率、数量及效率等

n  MySQL对OLAP类业务支持力度较弱,针对该类场景需要进行评估是否在MySQL端处理或将该功能切分至数据分析平台,如hadoop等。

2.2.3 Oracle与MySQL的兼容性

    针对待迁移的Oracle数据库基本信息,结合MySQL的一些特性及与Oracle的差异性,对Oracle中不满足MySQL相关设计规范的数据库对象进行筛选统计,可在数据迁移前期有效的规避大部分兼容性问题。对于MySQL的库表设计规范,可参考第6节中6.3小节《MySQL库表设计规范》文档附录。

 

是否存在字段函数默认值

n  统计数据库中各字段是否存在函数默认值及具体逻辑

n  MySQL默认仅支持对日期类型字段设定函数默认值,如Oracle中sys_uuid、check约束等,针对该类属性需进行业务改造,将该部分逻辑交由业务代码实现或进行逻辑判断

是否存在大小写敏感/特殊字符的表名、字段名

n  Oracle支持对表及字段名使用双引号来创建非标准的表名(大写字母+数字+下划线)

n  MySQL设计规范建议对表及字段名称忽略大小写且建议表及字段不包含特殊字符

n  针对Oracle端存在大小写敏感及有特殊字符的表,建议在数据迁移过程中进行转换并结合业务代码一并改造使之符合MySQL库表设计规范

是否存在超过MySQL限制的的表名、字段名

n  默认Oracle仅支持30个字符的表名长度,MySQL最高支持64个字符

n  考虑数据回退场景,建议MySQL端在创建表名及字段名称时也不超过30个字符

是否存在长度超过MySQL限制的索引

n  MySQL在不同字符集下对单个索引有不同的长度限制

u  UTF8 = 3072/3 = 1024个字符

u  UTF8MB4 = 3072/4 = 768个字符'

n  对于超过长度限制的索引,在MySQL端需考虑使用前缀索引或缩短数据类型

是否存在无主键或唯一键的表

n  Oracle默认为堆表结构,索引与数据分离,MySQL为索引组织表,索引即数据

n  MySQL设计规范建议各表均采用自增ID作为主键,如Oracle端无主键或唯一键在数据迁移过程中存在性能问题以及数据不一致风险。

是否存在VARCHAR2长度超过MySQL限制的表

n  MySQL在不同字符集下对单表VARCHAR字段长度有所限制

u  UTF8 = 65535/3 ≈ 21800

u  UTF8MB4 = 65535/4 ≈ 16000

n  针对Oracle端超过该限制的表,需对部分字段进行长度缩减或修改字段类型

是否存在MySQL不支持或不完全兼容的数据类型

n  Oracle端存在部分特殊数据类型及部分数据类型精度超过MySQL限制

u  TIMESTAMP(7)、TIMESTAMP(8)

u  RAW

u  UROWID

u  BLOB

u  BFILE

n  针对特殊的数据类型需结合业务实际情况进行调整成MySQL兼容的数据类型,部分类型如BFILE由于保存大文件至数据库,不适用于MySQL数据库,则还需涉及业务逻辑改造。

2.2.4 数据迁移依赖统计

    数据迁移通常采用以下2种逻辑进行数据抽取及同步

  1. 基于数据库日志解析进行全量及增量数据同步
  2. 基于数据库中某字段判断数据是否更新以便进行数据增量同步

    基于场景2,需要数据库各待迁移表包含modify_date时间类型字段,该字段表示当表中任意一条记录被修改时,该时间字段均会自动更新,从而在全量抽取完成后可基于该字段进行增量数据的区分及抽取过滤。

2.工具选型

2.1 表结构转换

    在数据迁移流程中,表结构转换作为数据同步的前提,其转换的效率及准确性直接影响着数据同步是否能正常进行及数据是否能成功迁移,以下针对2种不同的表结构转换逻辑进行对比,并最终选择最优的转换方案。

    Oracle与MySQL的数据类型不尽相同,对于各类不同的数据类型的转换,我们总结了一份Oracle与MySQL之间的数据类型转换mapping关系表,各类型对应关系可查看第6章中6.1小节《数据类型mapping关系表》文档附录。

2.1.1 基于mapping关系人工转换

    在大部分数据迁移示例场景中,表结构转换均以单个表作为转换示例,介绍如何进行表字段数据类型的匹配及转换,而针对大批量的转换场景,人工转换则可能效率低下。以下是人工基于mapping进行表结构转换的优缺点

  • 对于表数量较少时(通常为100张以下)转换较为便捷
  • 转换方式灵活,可自行增加附加字段
  • 在mapping关系正确的情况下,人工转换表结构准确性高
  • 对于表数据较多时(通常为100张以上)转换较为耗时

2.1.2 基于sqlines工具批量转换

    sqlines是专门针对异构数据库之间表结构转换、数据迁移及数据库对象转换的开源工具,工具部署方便,操作简单。该方案仅针对其支持的异构数据库之间表结构转换特性进行对比。

  • 可批量对指定数据库表进行转换,转换效率高
  • 支持通配符和正则表达式进行表过来
  • 支持自定义mapping数据类型映射关系
  • 支持自定义表名及字段名转换
  • 对数据类型的转换存在放大问题

n  如Oracle的number不指定精度则转换为MySQL中double类型

n  该问题可自定义数据类型mapping关系进行规避

  • 字段默认值可能丢失导致表结构默认属性不一致

2.1.3 表结构转换工具选型

    综合2种不同的表结构转换方式,可以发现其各有优缺点,通过实际验证后最终采用以下方案

  1. 首先采用sqlines对需要迁移的表进行批量转换
  2. 对sqlines转换完成的表结构进行人工微调校验

n  确保数据类型转换正确

n  确保字段默认值属性正确

2.2 数据迁移

    数据迁移工具或方式的选型决定了数据迁移的难易程度,选择一个正确的数据迁移工具能有效的避免迁移过程中的各种异常情况,以下针对4种不同的数据迁移方式或工具进行特性分析,以及结合实际数据迁移的时效性、业务连续性等要求最终确定数据迁移逻辑。

2.2.1 文本文件导入导出

    绝大多数数据库均支持对数据进行逻辑的文本文件导出和导入,如Oracle数据库的sqlldr工具,如MySQL的mysqlexport及mysqlimport工具,以下是文本文件导入导出的优缺点

  • 导入导出效率极高,无需关心表之间的关联逻辑
  • 从Oracle数据库中导出数据较为繁琐,需要对各表进行字段拼接处理
  • 导入导出文件中数据之间的分隔符和界定符需确保不与数据冲突

n  分隔符过于简单容易与数据冲突导致导入数据时字段匹配出错

n  分隔符过于复杂则会导致导出的数据文件显著增大

n  导出时不支持BLOB等大对象数据类型

n  对增量数据处理上不友好,如源端对数据进行删除,则无法识别出

2.2.2 MySQL Migration Toolkit工具

 

    MySQL Migration Toolkit工具是Oracle官方早期提供的异构数据库表结构转换及数据迁移工具,该工具通过流程化的操作,逐步完成数据的迁移配置,对用户友好,支持多种类型数据库之间数据交换并生成转换语句,以下为该工具的优缺点

  • 图形化界面操作,操作简单且对用户友好
  • 仅支持Windows平台,兼容性较差
  • 对大批量数据的迁移会导致软件崩溃
  • 该软件由10多年前开发,多年前停止维护,目前已用workbench取代部分功能
  • 表结构转换的准确性偏低且大部分数据类型均会进行数据类型定义放大的问题
  • 对增量数据处理上不友好,如源端对数据进行删除,则无法识别出
  • 对BLOB、CLOB类型数据支持较差

2.2.3 kettle开源ETL工具

 

    Kettle是基于java开发的免费且开源的ETL工具,不仅支持多种不同数据库直接数据转换,还支持大数据、文本、日志、自定义函数、自定义变量等各类非数据库数据处理,具有强大的数据处理、清洗、转换功能,以下为该工具的优缺点

  • 支持Windows、Linux、Unix多平台
  • 图形化操作管理,无需编写代码,仅通过界面拖拽即可
  • 支持定时触发迁移、批量job迁移
  • 支持自定义表结构mapping关系
  • 对增量数据处理上不友好,依赖modify_date字段对源端变更数据做识别,如源端对数据进行删除,则无法识别出

2.2.4 Oracle GoldenGate

 

    OGG全称为Oracle GoldenGate,是由Oracle官方提供的用于解决异构数据环境中数据复制的一个商业工具。相比于其它迁移工具OGG的优势在于可以直接解析源端Oracle的redo log,因此能够实现在不需要对原表结构做太多调整的前提下完成数据增量部分的迁移。以下为该工具的优缺点及特性

  • Oracle原生商业异构数据同步工具
  • 有完善的产品文档及故障处理逻辑
  • 针对多种不同场景的数据迁移有与之相应的解决方案参考

n  多个库汇聚为一个库

n  一个库分散至库多个

n  一个库同步至一个库

n  一个库分发至多个不同类型数据库

  • 软件支持多种平台
  • 异构场景下对全量数据同步效率较差

n  可通过开启多个并发同步进程提升效率

  • 支持自定义表结构及字段mapping关系
  • 支持实时监控各同步进程状态,便于故障诊断
  • 完整支持全量及增量迁移、断点续传
  • 不支持图形化界面配置,文本方式配置较为繁琐
  • 可Oracle其他软件产品结合使用,如Dataguard

2.2.5 数据迁移工具选型

    综合以上4种数据迁移工具及方式,可以了解到使用文本文件导入导出以及MySQL Migration Toolkit均有较大局限性且兼容性和可维护性均较差,基本可以被排除在外。综合考虑具体的迁移要求及业务场景,以及大部分情况下Oracle数据库均未配置modify_date字段用以区分变更后的数据,使用kettle无法有效的识别增量数据,最终采用以下方式进行数据迁移

  1. 使用Oracle GoldenGate作为数据全量和增量的主要同步工具

n  全量数据也可采用kettle方式进行同步

n  增量数据必须采用GoldenGate解析redo log的方式进行同步

  1. Oracle端数据采用从DataGuard的方式抽取,减少对主库的影响
  2. 同步期间支持对数据进行静态校验校验(结合Oracle DataGuard架构)

3.数据校验

3.1 基于业务逻辑校验

    基于业务逻辑的数据校验分为以下2部分,具体校验逻辑及维度需业务人员提供具体业务场景,

  1. 基于业务维度对数据进行汇总统计,如

a)   当天/当周/当月交易金额

b)   会员数量/等级

c)   各表总记录数

  1. 创建小批测试用户进行业务逻辑的验证,确保Oracle与MySQL端产生的结果一致,如

a)   测试会员注册

b)   测试会员交易成功/失败

c)   测试会员等级变更

3.2 基于CRC32一致性校验

    通过将表中记录的关键字段值拼接成字符串,然后计算该字符串CRC32校验码,再将所有记录的校验码通过逻辑异或进行汇聚,生成该表所有记录关键字段的校验码,从而确保数据迁移的数据完整性和一致性。下面是对一张表的数据校验SQL示例

  • MySQL:

SELECT BIT_XOR(CRC32(CONCAT_WS('',`字段1`,`字段2`,`字段3`,`字段...`))) FROM TabName;

  • Oracle:

SELECT BIT_XOR(CRC32(字段1||字段2||字段3||字段...))) FROM TabName;

  • Oracle需要通过用户自定义聚合函数及配置外部动态库方式实现BIT_XOR和CRC32算法。
  • 结果示例

 

3.3 基于Oracle GoldenGate统计信息校验

Oracle GoldenGate

  • 全量数据验证

n  Oracle GoldenGate全量抽取、回放线程执行完成后会生成一个report文件,可用该文件来判断全量抽取及全量同步是否存在异常,当输出文件中discards记录为0表示没有异常数据生成,数据正常同步成功

n  通过对比全量抽取和全量回放日志中各表的insert字段来判断抽取的记录数、回放的记录数以及抽取和回放过程中是否存在数据异常

  • 增量数据验证

n  Oracle GoldenGate增量抽取、回放过程同样会生成处理的统计信息,通过对统计信息进行过滤,可以了解到数据同步的整体状态、单表同步状态以及是否存在同步异常等,精确追踪和诊断数据同步问题。

3.4 数据校验逻辑选型

    该方案采用Oracle GoldenGate结合Oracle DataGuard进行数据抽取同步,决定采用以下方式进行数据校验

  • 全量数据校验
  1. 待全量数据采用GoldenGate同步完成后,停止DataGuard与Oracle主库的同步
  2. 采用统计GoldenGate全量同步report文件的方式判断是否存在同步失败的数据
  3. 采用crc32的方式对Oracle和MySQL端表全字段进行一致性校验
  • 增量数据校验
  1. 在T-1日期间停止停止DataGuard与Oracle主库的同步
  2. 采用GoldenGate增量同步统计信息对增量数据判断是否存在同步失败的数据
  3. 采用crc32的方式对Oracle和MySQL端表全字段进行一致性校验
  4. 基于业务逻辑及测试用户进行数据及业务代码验证
  5. 开启DataGuard数据同步
  6. 在T日数据同步割接后再次重复以上操作,完成最终的数据一致性校验

4.数据回退

    在绝大部分数据库迁移项目中,数据回退仅作为最终不得已的手段,对已迁移的业务及业务数据回退至老系统中,回退的代价较大,对于数据库而言则需要识别出哪些数据是迁移后产生的并重新回放至源端老系统中,以下分别针对3种数据回退方式进行对比。

4.1 基于MODIFY_DATE字段获取新增数据

    待源端Oracle数据迁移至目标端数据库时,为符合MySQL开发设计规范,将在MySQL数据库各表字段中新增CREATE_DATE、MODIFY_DATE字段,其中MODIFY_DATE则用于当该表任意一行记录做修改时,自动更新该行记录的MODIFY_DATE时间,则基于该字段可识别出至数据迁移完成后,存在哪些表的哪些数据做过更新,将这部分数据识别出来导出并重新导入至Oracle数据库中完成数据回退。该方式有以下特点

  • 需对每张表进行查询匹配MODIFY_DATE时间
  • 每张表更新的数据均需要进行处理并编写Oracle端导入语句
  • 对BLOB或特殊字符数据支持力度不够

 

4.2 基于binlog2sql解析SQL语句

    MySQL开源工具binlog2sql可基于binlog日志进行解析,将执行过的DML语句解析成标准的SQL语句,基于该工具将Oracle涉及的表的所有DML更新均实时解析并生成一份完整的回退SQL文件,最终将该SQL文件导入至Oracle数据库中,该方式有以下特点

  • 需对binlog做库表过滤解析
  • 对源端和目标端字段不一致时生成的SQL语句兼容性较差
  • 解析生成的SQL语句需进行额外特殊注释信息替换方可支持Oracle语法

 

4.3 基于Oracle GoldenGate反向同步

    Oracle GoldenGate不仅支持Oracle数据库实时同步数据至MySQL,也支持抓取MySQL日志解析同步至Oracle数据库中,通过相同的逻辑可将割接后的MySQL的增量数据实时抓取保存,如涉及数据回退操作,则开启MySQL至Oracle端的同步进程,将数据反向同步至Oracle端即可,且同步逻辑和直接参考数据迁移流程。

4.4 数据回退逻辑选型

    基于数据迁移工具选型用Oracle GoldenGate,数据回退仍旧可以用的Oracle GoldenGate反向从MySQL同步数据到Oracle的逻辑进行处理,以及在数据割接期间同时记录割接时间,便于后续如需做基于时间点的数据回退时可仍旧采用MODIFY_DATE字段或binlog2sql工具,有效的将三种不同的回退逻辑结合。

5.常见问题

5.1 timestamp字段精度过长

       Oracle timestamp类型精度可精确到秒后面7位(timestamp(7)),MySQL datetime数据类型仅支持到6位,转换时报错Too-big precision 7 specified for 'READ_TIME'. Maximum is 6.

修复方式: 
       编辑sqlines的 sqlines_cmap.txt文件增加字段数据类型转换细节,只保留6位,后续数据同步时也需对数据进行截断处理 
test.table1,table1,datetime(6)

 

5.2 表及字段中存在特殊字符

Oracle可以通过创建表时增加双引号来创建有特殊字符的表,具体表现为

  • 有中划线的表,如:GY-tbl-basic_org
  • 有区分大小的写表如:GY-tbl_ib_basic
  • ·         有特殊字符的表,如:EIMP_$DAB

修复方式: 

     1.在使用sqlines时可在tf文件中也同时对需要获取的Oracle表增加双引号,但在转换为MySQL结构时,由于MySQL采用的是反引号作为特殊字符表的创建方式,所以无法创建成功,需要将存在特殊字符的表获取处理手工转换。

     2.对于存在特殊字符或存在大小的表,建议修改为符合MySQL设计规范的结构,如小写字母+下划线+数字的组合形式。

 

5.3 索引字符长度过长

### PARENT_IDS 为varchar(2000)

CREATE INDEX SYS_OFFICE_PARENT_IDS ON upi_asset.SYS_OFFICE (`PARENT_IDS`);

ERROR 1071 (42000): Specified key was too long; max key length is 3072 bytes

 

       MySQL针对varchar类型进行索引有长度限制,对于UTF8时,一个字符占用3个直接,UTF8MB4占用4个字节,MySQL限定索引最大长度为3072个字节,所以对应字符集的最大可索引的varchar长度为:

  • UTF8 varchar(1024) 1024*3=3072
  • UTF8MB4 varchar(768) 768*4=3072

 

3072字节的来源:

     innodb的page默认大小为16k,由于采用b+tree结构,为了不退化为链表,要求子节点一个page最好包含两条记录,所以一个记录不能超过8k。又由于聚簇索引,在二级索引中包含主键索引,所以单个索引不能超过4k,去除预留和辅助空间,取值3072。

 

 

 

6.文档附录

6.1 数据类型mapping关系表

 

Oracle

MySQL

1

BFILE

Pointer to binary file, ⇐ 4G

VARCHAR(255)

2

BINARY_FLOAT

32-bit floating-point number

FLOAT

3

BINARY_DOUBLE

64-bit floating-point number

DOUBLE

4

BLOB

Binary large object, ⇐ 4G

LONGBLOB

5

CHAR(n), CHARACTER(n)

Fixed-length string, 1 ⇐ n ⇐ 255

CHAR(n), CHARACTER(n)

6

CHAR(n), CHARACTER(n)

Fixed-length string, 256 ⇐ n ⇐ 2000

VARCHAR(n)

7

CLOB

Character large object, ⇐ 4G

LONGTEXT

8

DATE

Date and time

DATETIME

9

DECIMAL(p,s), DEC(p,s)

Fixed-point number

DECIMAL(p,s), DEC(p,s)

10

DOUBLE PRECISION

Floating-point number

DOUBLE PRECISION

11

FLOAT(p)

Floating-point number

DOUBLE

12

INTEGER, INT

38 digits integer

INT

DECIMAL(38)

13

INTERVAL YEAR(p) TO MONTH

Date interval

VARCHAR(30)

14

INTERVAL DAY(p) TO SECOND(s)

Day and time interval

VARCHAR(30)

15

LONG

Character data, ⇐ 2G

LONGTEXT

16

LONG RAW

Binary data, ⇐ 2G

LONGBLOB

17

NCHAR(n)

Fixed-length UTF-8 string, 1 ⇐ n ⇐ 255

NCHAR(n)

18

NCHAR(n)

Fixed-length UTF-8 string, 256 ⇐ n ⇐ 2000

NVARCHAR(n)

19

NCHAR VARYING(n)

Varying-length UTF-8 string, 1 ⇐ n ⇐ 4000

NCHAR VARYING(n)

20

NCLOB

Variable-length Unicode string, ⇐ 4G

NVARCHAR(max)

21

NUMBER(p,0), NUMBER(p)

8-bit integer, 1 <= p < 3

TINYINT

(0 to 255)

16-bit integer, 3 <= p < 5

SMALLINT

32-bit integer, 5 <= p < 9

INT

64-bit integer, 9 <= p < 19

BIGINT

Fixed-point number, 19 <= p <= 38

DECIMAL(p)

22

NUMBER(p,s)

Fixed-point number, s > 0

DECIMAL(p,s)

23

NUMBER, NUMBER(*)

Floating-point number

DOUBLE

24

NUMERIC(p,s)

Fixed-point number

NUMERIC(p,s)

25

NVARCHAR2(n)

Variable-length UTF-8 string, 1 ⇐ n ⇐ 4000

NVARCHAR(n)

26

RAW(n)

Variable-length binary string, 1 ⇐ n ⇐ 255

BINARY(n)

27

RAW(n)

Variable-length binary string, 256 ⇐ n ⇐ 2000

VARBINARY(n)

28

REAL

Floating-point number

DOUBLE

29

ROWID

Physical row address

CHAR(10)

30

SMALLINT

38 digits integer

DECIMAL(38)

31

TIMESTAMP(p)

Date and time with fraction

DATETIME(p)

32

TIMESTAMP(p) WITH TIME ZONE

Date and time with fraction and time zone

DATETIME(p) 

33

UROWID(n)

Logical row addresses, 1 ⇐ n ⇐ 4000

VARCHAR(n)

34

VARCHAR(n)

Variable-length string, 1 ⇐ n ⇐ 4000

VARCHAR(n)

35

VARCHAR2(n)

Variable-length string, 1 ⇐ n ⇐ 4000

VARCHAR(n)

36

XMLTYPE

XML data

LONGTEXT

 

 

6.2 常用函数mapping关系表

 

Oracle

MySQL

1

ABS(num)

Get the absolute value

ABS(num)

2

ACOS(num)

Get the arc cosine

ACOS(num)

3

ADD_MONTHS(date, num)

Add num months to date

TIMESTAMPADD(MONTH, num, date)

4

ASCII(str)

Get ASCII code of left-most char

ASCII(str)

5

ASCIISTR(string)

Get ASCII code version of string

 

6

ASIN(num)

Get the arcsine

ASIN(num)

7

ATAN(num)

Get the arc tangent

ATAN(num)

8

ATAN2(x, y)

Get the arc tangent of x and y

ATAN2(x, y)

9

BIN_TO_NUM(bit1, bit2, …)

Convert bit vector to number

 

10

BITAND(exp1, exp2)

Perform bitwise AND

(exp1 & exp2)

11

CEIL(num)

Get the smallest following integer

CEIL(num)

12

CHR(num)

Get character from ASCII code

CHAR(num USING ASCII)

13

COALESCE(exp1, exp2, …)

Return first non-NULL expression

COALESCE(exp1, exp2, …)

14

CONCAT(char1, char2)

String concatenation

CONCAT(char1, char2)

15

CONVERT(string, charset)

Convert string to charset

CONVERT(string USING charset)

16

COS(num)

Get the cosine

COS(num)

17

COSH(num)

Get hyperbolic cosine

(EXP(num) + EXP(-num)) / 2

18

CURRENT_DATE

Get the current date

NOW()

19

CURRENT_TIMESTAMP

Get the current date and time

NOW()

20

DECODE(exp, when, then, …)

Evaluate conditions

CASE expression

21

EXP(n)

Raise e to the nth power

EXP(n)

22

EXTRACT(YEAR FROM date)

Extract year from date

YEAR(date)

23

EXTRACT(MONTH FROM date)

Extract month from date

MONTH(date)

24

EXTRACT(DAY FROM date)

Extract day from date

DAY(date)

25

EXTRACT(HOUR FROM time)

Extract hour from time

HOUR(time)

26

EXTRACT(MINUTE FROM time)

Extract minute from time

MINUTE(time)

27

EXTRACT(SECOND FROM time)

Extract second from time

SECOND(time)

28

FLOOR(num)

Get the largest preceding integer

FLOOR(num)

29

GREATEST(exp, exp2, …)

Get the maximum value in a set

GREATEST(exp, exp2, …)

30

INITCAP(string)

Capitalize words

User-defined function

31

INSTR(str, substr)

Get position of substring

INSTR(str, substr)

INSTR(str, substr, pos)

LOCATE(str, substr, pos)

INSTR(str, substr, pos, num)

User-defined function

32

LAST_DAY(date)

Get last day of the month

LAST_DAY(date)

33

LEAST(exp, exp2, …)

Get the minimum value in a set

LEAST(exp, exp2, …)

34

LENGTH(string)

Get length of string in chars

CHAR_LENGTH(string)

35

LENGTHB(string)

Get length of string in bytes

LENGTH(string

36

LN(num)

Get natural logarithm of num

LN(num)

37

LOCALTIMESTAMP

Get the current date and time

LOCALTIMESTAMP

LOCALTIMESTAMP([prec])

LOCALTIMESTAMP()

38

LOG(num1, num2)

Get logarithm, base num1, of num2

LOG(num1, num2)

39

LOWER(string)

Lowercase string

LOWER(string)

40

LPAD(string, len)

Pad the left-side of string

LPAD(string, len, ' ')

LPAD(string, len, pad)

LPAD(string, len, pad)

41

LTRIM(string)

Remove leading spaces

LTRIM(string)

LTRIM(string, set)

Remove leading chars

TRIM(LEADING set FROM string)

42

MONTHS_BETWEEN(date1, date2)

Get number of months between 
date1 and date2

User-defined function

43

MOD(dividend, divisor)

Get remainder

MOD(dividend, divisor)

44

NEXT_DAY

Get the next date by day name

NEXT_DAY user-defined function

45

NULLIF(exp1, exp2)

Return NULL if exp1 = exp2

NULLIF(exp1, exp2)

46

NVL(expreplacement)

Replace NULL with the specified value

IFNULL(expreplacement)

47

NVL2(exp1, exp2, exp3)

Return exp2 if exp1 is not NULL, 
otherwise exp3

CASE expression

48

POWER(value, n)

Raise value to the nth power

POWER(value, n)

49

REMAINDER(n1, n2)

Get remainder

(n1 - n2*ROUND(n1/n2))

50

REPLACE(str, search)

Remove search-string

REPLACE(str, search, '')

REPLACE(str, search, replace)

Replace search-string

REPLACE(str, search, replace)

51

ROUND(num, integer)

Get rounded value

ROUND(num, integer)

52

RPAD(string, len)

Pad the right-side of string

RPAD(string, len, ' ')

RPAD(string, len, pad)

RPAD(string, len, pad)

53

RTRIM(string)

Remove trailing spaces

RTRIM(string)

RTRIM(string, set)

Remove trailing chars

TRIM(TRAILING set FROM string)

54

SIGN(exp)

Get sign of exp

SIGN(exp)

55

SIN(num)

Get sine

SIN(num)

56

SINH(num)

Get hyperbolic sine

(EXP(num) - EXP(-num)) / 2

57

SOUNDEX(string)

Get 4-character sound code

SOUNDEX(string)

58

SQRT(num)

Get square root

SQRT(num)

59

SUBSTR(string, pos, len)

Get a substring of string

SUBSTR(string, pos, len)

60

SYS_GUID()

Get GUID, 32 characters without dashes

REPLACE(UUID(), '-', '')

61

SYSDATE

Get current date and time

SYSDATE()

62

SYSTIMESTAMP

Get current timestamp

CURRENT_TIMESTAMP

63

TAN(num)

Get tangent

TAN(num)

64

TANH(num)

Get hyperbolic tangent

(EXP(2*num) - 1)/(EXP(2*num) + 1)

65

TO_CHAR(datetime, format)

Convert datetime to string

DATE_FORMAT(datetime, format) 

TO_CHAR(number, format)

Convert number to string

FORMAT(number, decimal_digits) 

66

TO_DATE(string, format)

Convert string to datetime

STR_TO_DATE(string, format) 

67

TO_LOB(exp)

Convert to LOB

 

68

TO_NCHAR(exp)

Convert to NCHAR

 

69

TO_NCLOB(exp)

Convert to NCLOB

 

70

TO_NUMBER(exp)

Convert to NUMBER

 

71

TO_SINGLE_BYTE(exp)

Convert to single-byte character

 

72

TO_TIMESTAMP(exp)

Convert to TIMESTAMP

 

73

TRANSLATE(string, from, to)

Replace characters

User-defined function

74

TRIM([type trim FROM] string)

Remove characters

TRIM([type trim FROM] string)

75

TRUNC(num)

Truncate num

TRUNCATE(num, 0)

TRUNC(num, num2)

TRUNCATE(num, num2)

76

TRUNC(datetime)

Truncate datetime

DATE(datetime), DATE_FORMAT

77

UNISTR(string)

Convert Unicode code points to chars

CHAR(string USING UCS2) 

78

UPPER(string)

Uppercase string

UPPER(string)

79

USER

Get the current user

USER()

80

USERENV('parameter')

Get the current session information

 

81

VSIZE(exp)

Get the size of exp in bytes

 

82

XMLAGG(exp)

Get a aggregated XML document

 

83

XMLCAST(exp AS datatype)

Convert exp to datatype

 

84

XMLCDATA(exp)

Generate a CDATA section

 

85

XMLCOMMENT(exp)

Generate an XML comment

 

86

XMLCONCAT(exp, exp2, …)

Concatenate XML expressions

 

87

XMLDIFF(doc, doc2)

Compare two XML documents

 

88

XMLELEMENT(NAME element)

Get an XQuery element node

 

89

XMLFOREST(exp, exp2, …)

Get a forest of XML expressions

 

90

XMLISVALID(exp)

Check XML exp

 

91

XMLPARSE(DOCUMENT exp)

Parse XML document

 

92

XMLPATCH(doc, doc2)

Patch XML document

 

93

XMLPI(NAME identifier)

Get XML processing instruction

 

94

XMLROOT(exp, VERSION exp2)

Create a new XML value

 

95

XMLSEQUENCE(exp)

Get a varray of the top-level nodes

 

96

XMLSERIALIZE(CONTENT exp 
AS datatype)

Get a serialized XML value

 

97

XMLTRANSFORM(instance, exp)

Transform XML document



 

6.3 MySQL库表设计规范

6.3.1 MySQL 总体设计规范

l  本规范主要针对OLTP类型业务

l  MySQL表、字段的设计使用统一、简洁、明确的原则设计

l  反范式设计逻辑

n  不需要强制满足数据库设计中的第三范式、尽量不使用外键约束

n  关联表的数据完整性可在业务端实现

n  表字段适度的冗余,减少多表JOIN的查询语句

l  保持MySQL数据库仅作为一个纯粹的数据存储系统,业务逻辑均在代码层实现

n  尽量避免使用自定义函数、存储过程、触发器、视图以便减少与业务耦合性

l  数据库的账户权限授予确保精确细化

n  严格遵循最小权限原则

n  应用用户避免授予DDL权限

n  不使用简单密码

6.3.2 MySQL 表结构设计规范

l  所有表名及字段名全部使用小写字母、数字与下划线组合

l  表名用有实际含义的英语单词,除约定俗成的英文缩写外,尽量避免使用缩写。

l  所有表及字段需要明确标注中文注释

l  所有表均使用InnoDB存储引擎且在建表语句中明确指定

n  完整的ACID事务支持

n  崩溃自动恢复能力

n  行级别锁,更高并发

l  所有表均使用UTF8或UTF8MB4字符集

l  所有表均需要有主键且建议用与业务无关的自增ID(保证ID列不会被修改)

l  所有表名及字段名尽量不超过32个字符 (保持与其他数据库兼容性)

l  所有表名及字段名不要使用MySQL的保留字和关键字(附录A)

l  不在数据库中存图片、二进制大文件

l  提前规划好表的容量、大表做好数据归档策略

 

6.3.3 MySQL 字段设计规范

l  字段尽量添加NOT NULL属性并设置默认值

l  字段存储非负数值时增加UNSIGNED属性

l  字段数据包含小数时尽量使用decimal类型而不使用浮点型

l  字段长度遵循最合适、最完整、最简短原则

n  最合适:选择最合适的数据类型存储不同数据,避免都使用VARCHAR

n  最完整:确保定义的数据类型长度可以存储完整的业务数据不会截断

n  最简短:确保能用占用字节更少的数据类型存储完整数据

l  不同表之间关联字段的类型、长度需保持一致

l  单表字段VARCHAR类型不超过16000(UTF8MB4)、21000(UTF8)

l  对于BLOB、TEXT等大字段字段,建议单独拆分一个表,只存放主表ID及BLOB或TEXT字段。

l  禁止使用BIT数据类型

l  不使用字段存储明文密码,密码必须加密存储

l  每张表包含CREATE_DATE、MODIFY_DATE字段

n  CREATE_TIME用NOT NULL DEFAULT CURRENT_TIMESTAMP属性

MODIFY_DATE用NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP属性

6.3.4 MySQL 索引设计规范

l  要创建索引的字段需要选择性(Cardinality)较强

n  字段值需要唯一性较高,如主键的唯一性为100%

n  如:性别列不适合创建单列索引

索引长度不可超过768个字符(包括组合索引,utf8数据类型不可超过1024)

l  单表的索引在满足业务需求下尽可能控制在5个以内

l  对长度大于50的VARCHAR字段建立索引时,建立一定长度的前缀索引

l  合理的创建联合索引,联合索引(a,b,c) 等于 (a) 、(a,b) 、(a,b,c)三个索引

6.3.5 MySQL保留字和关键字

ACCESSIBLE (R)

ACTION

ADD (R)

AFTER

AGAINST

AGGREGATE

ALGORITHM

ALL (R)

ALTER (R)

ANALYSE[a]

ANALYZE (R)

AND (R)

ANY

AS (R)

ASC (R)

ASCII

ASENSITIVE (R)

AT

AUTHORS[b]

AUTOEXTEND_SIZE

AUTO_INCREMENT

AVG

AVG_ROW_LENGTH

BACKUP

BEFORE (R)

BEGIN

BETWEEN (R)

BIGINT (R)

BINARY (R)

BINLOG

BIT

BLOB (R)

BLOCK

BOOL

BOOLEAN

BOTH (R)

BTREE

BY (R)

BYTE

CACHE

CALL (R)

CASCADE (R)

CASCADED

CASE (R)

CATALOG_NAME

CHAIN

CHANGE (R)

CHANGED

CHAR (R)

CHARACTER (R)

CHARSET

CHECK (R)

CHECKSUM

CIPHER

CLASS_ORIGIN

CLIENT

CLOSE

COALESCE

CODE

COLLATE (R)

COLLATION

COLUMN (R)

COLUMNS

COLUMN_FORMAT[c]

COLUMN_NAME

COMMENT

COMMIT

COMMITTED

COMPACT

COMPLETION

COMPRESSED

CONCURRENT

CONDITION (R)

CONNECTION

CONSISTENT

CONSTRAINT (R)

CONSTRAINT_CATALOG

CONSTRAINT_NAME

CONSTRAINT_SCHEMA

CONTAINS

CONTEXT

CONTINUE (R)

CONTRIBUTORS[d]

CONVERT (R)

CPU

CREATE (R)

CROSS (R)

CUBE

CURRENT[e]

CURRENT_DATE (R)

CURRENT_TIME (R)

CURRENT_TIMESTAMP (R)

CURRENT_USER (R)

CURSOR (R)

CURSOR_NAME

DATA

DATABASE (R)

DATABASES (R)

DATAFILE

DATE

DATETIME

DAY

DAY_HOUR (R)

DAY_MICROSECOND (R)

DAY_MINUTE (R)

DAY_SECOND (R)

DEALLOCATE

DEC (R)

DECIMAL (R)

DECLARE (R)

DEFAULT (R)

DEFAULT_AUTH[f]

DEFINER

DELAYED (R)

DELAY_KEY_WRITE

DELETE (R)

DESC (R)

DESCRIBE (R)

DES_KEY_FILE

DETERMINISTIC (R)

DIAGNOSTICS[g]

DIRECTORY

DISABLE

DISCARD

DISK

DISTINCT (R)

DISTINCTROW (R)

DIV (R)

DO

DOUBLE (R)

DROP (R)

DUAL (R)

DUMPFILE

DUPLICATE

DYNAMIC

EACH (R)

ELSE (R)

ELSEIF (R)

ENABLE

ENCLOSED (R)

END

ENDS

ENGINE

ENGINES

ENUM

ERROR

ERRORS

ESCAPE

ESCAPED (R)

EVENT

EVENTS

EVERY

EXCHANGE

EXECUTE

EXISTS (R)

EXIT (R)

EXPANSION

EXPIRE[h]

EXPLAIN (R)

EXPORT[i]

EXTENDED

EXTENT_SIZE

FALSE (R)

FAST

FAULTS

FETCH (R)

FIELDS

FILE

FIRST

FIXED

FLOAT (R)

FLOAT4 (R)

FLOAT8 (R)

FLUSH

FOR (R)

FORCE (R)

FOREIGN (R)

FORMAT[j]

FOUND

FROM (R)

FULL

FULLTEXT (R)

FUNCTION

GENERAL[k]

GEOMETRY

GEOMETRYCOLLECTION

GET[l] (R)

GET_FORMAT

GLOBAL

GRANT (R)

GRANTS

GROUP (R)

HANDLER

HASH

HAVING (R)

HELP

HIGH_PRIORITY (R)

HOST

HOSTS

HOUR

HOUR_MICROSECOND (R)

HOUR_MINUTE (R)

HOUR_SECOND (R)

IDENTIFIED

IF (R)

IGNORE (R)

IGNORE_SERVER_IDS[m]

IMPORT

IN (R)

INDEX (R)

INDEXES

INFILE (R)

INITIAL_SIZE

INNER (R)

INOUT (R)

INSENSITIVE (R)

INSERT (R)

INSERT_METHOD

INSTALL

INT (R)

INT1 (R)

INT2 (R)

INT3 (R)

INT4 (R)

INT8 (R)

INTEGER (R)

INTERVAL (R)

INTO (R)

INVOKER

IO

IO_AFTER_GTIDS[n] (R)

IO_BEFORE_GTIDS[o] (R)

IO_THREAD

IPC

IS (R)

ISOLATION

ISSUER

ITERATE (R)

JOIN (R)

KEY (R)

KEYS (R)

KEY_BLOCK_SIZE

KILL (R)

LANGUAGE

LAST

LEADING (R)

LEAVE (R)

LEAVES

LEFT (R)

LESS

LEVEL

LIKE (R)

LIMIT (R)

LINEAR (R)

LINES (R)

LINESTRING

LIST

LOAD (R)

LOCAL

LOCALTIME (R)

LOCALTIMESTAMP (R)

LOCK (R)

LOCKS

LOGFILE

LOGS

LONG (R)

LONGBLOB (R)

LONGTEXT (R)

LOOP (R)

LOW_PRIORITY (R)

MASTER

MASTER_AUTO_POSITION[p]

MASTER_BIND[q] (R)

MASTER_CONNECT_RETRY

MASTER_DELAY

MASTER_HEARTBEAT_PERIOD[r]

MASTER_HOST

MASTER_LOG_FILE

MASTER_LOG_POS

MASTER_PASSWORD

MASTER_PORT

MASTER_RETRY_COUNT[s]

MASTER_SERVER_ID

MASTER_SSL

MASTER_SSL_CA

MASTER_SSL_CAPATH

MASTER_SSL_CERT

MASTER_SSL_CIPHER

MASTER_SSL_CRL[t]

MASTER_SSL_CRLPATH[u]

MASTER_SSL_KEY

MASTER_SSL_VERIFY_SERVER_CERT (R)

MASTER_USER

MATCH (R)

MAXVALUE (R)

MAX_CONNECTIONS_PER_HOUR

MAX_QUERIES_PER_HOUR

MAX_ROWS

MAX_SIZE

MAX_UPDATES_PER_HOUR

MAX_USER_CONNECTIONS

MEDIUM

MEDIUMBLOB (R)

MEDIUMINT (R)

MEDIUMTEXT (R)

MEMORY

MERGE

MESSAGE_TEXT

MICROSECOND

MIDDLEINT (R)

MIGRATE

MINUTE

MINUTE_MICROSECOND (R)

MINUTE_SECOND (R)

MIN_ROWS

MOD (R)

MODE

MODIFIES (R)

MODIFY

MONTH

MULTILINESTRING

MULTIPOINT

MULTIPOLYGON

MUTEX

MYSQL_ERRNO

NAME

NAMES

NATIONAL

NATURAL (R)

NCHAR

NDB

NDBCLUSTER

NEW

NEXT

NO

NODEGROUP

NONE

NOT (R)

NO_WAIT

NO_WRITE_TO_BINLOG (R)

NULL (R)

NUMBER[v]

NUMERIC (R)

NVARCHAR

OFFSET

OLD_PASSWORD

ON (R)

ONE

ONE_SHOT[w]

ONLY[x]

OPEN

OPTIMIZE (R)

OPTION (R)

OPTIONALLY (R)

OPTIONS

OR (R)

ORDER (R)

OUT (R)

OUTER (R)

OUTFILE (R)

OWNER

PACK_KEYS

PAGE

PARSER

PARTIAL

PARTITION[y] (R)

PARTITIONING

PARTITIONS

PASSWORD

PHASE

PLUGIN

PLUGINS

PLUGIN_DIR[z]

POINT

POLYGON

PORT

PRECISION (R)

PREPARE

PRESERVE

PREV

PRIMARY (R)

PRIVILEGES

PROCEDURE (R)

PROCESSLIST

PROFILE

PROFILES

PROXY[aa]

PURGE (R)

QUARTER

QUERY

QUICK

RANGE (R)

READ (R)

READS (R)

READ_ONLY

READ_WRITE (R)

REAL (R)

REBUILD

RECOVER

REDOFILE

REDO_BUFFER_SIZE

REDUNDANT

REFERENCES (R)

REGEXP (R)

RELAY

RELAYLOG

RELAY_LOG_FILE

RELAY_LOG_POS

RELAY_THREAD

RELEASE (R)

RELOAD

REMOVE

RENAME (R)

REORGANIZE

REPAIR

REPEAT (R)

REPEATABLE

REPLACE (R)

REPLICATION

REQUIRE (R)

RESET

RESIGNAL (R)

RESTORE

RESTRICT (R)

RESUME

RETURN (R)

RETURNED_SQLSTATE[ab]

RETURNS

REVERSE

REVOKE (R)

RIGHT (R)

RLIKE (R)

ROLLBACK

ROLLUP

ROUTINE

ROW

ROWS

ROW_COUNT[ac]

ROW_FORMAT

RTREE

SAVEPOINT

SCHEDULE

SCHEMA (R)

SCHEMAS (R)

SCHEMA_NAME

SECOND

SECOND_MICROSECOND (R)

SECURITY

SELECT (R)

SENSITIVE (R)

SEPARATOR (R)

SERIAL

SERIALIZABLE

SERVER

SESSION

SET (R)

SHARE

SHOW (R)

SHUTDOWN

SIGNAL (R)

SIGNED

SIMPLE

SLAVE

SLOW[ad]

SMALLINT (R)

SNAPSHOT

SOCKET

SOME

SONAME

SOUNDS

SOURCE

SPATIAL (R)

SPECIFIC (R)

SQL (R)

SQLEXCEPTION (R)

SQLSTATE (R)

SQLWARNING (R)

SQL_AFTER_GTIDS[ae]

SQL_AFTER_MTS_GAPS[af]

SQL_BEFORE_GTIDS[ag]

SQL_BIG_RESULT (R)

SQL_BUFFER_RESULT

SQL_CACHE

SQL_CALC_FOUND_ROWS (R)

SQL_NO_CACHE

SQL_SMALL_RESULT (R)

SQL_THREAD

SQL_TSI_DAY

SQL_TSI_HOUR

SQL_TSI_MINUTE

SQL_TSI_MONTH

SQL_TSI_QUARTER

SQL_TSI_SECOND

SQL_TSI_WEEK

SQL_TSI_YEAR

SSL (R)

START

STARTING (R)

STARTS

STATS_AUTO_RECALC[ah]

STATS_PERSISTENT[ai]

STATS_SAMPLE_PAGES[aj]

STATUS

STOP

STORAGE

STRAIGHT_JOIN (R)

STRING

SUBCLASS_ORIGIN

SUBJECT

SUBPARTITION

SUBPARTITIONS

SUPER

SUSPEND

SWAPS

SWITCHES

TABLE (R)

TABLES

TABLESPACE

TABLE_CHECKSUM

TABLE_NAME

TEMPORARY

TEMPTABLE

TERMINATED (R)

TEXT

THAN

THEN (R)

TIME

TIMESTAMP

TIMESTAMPADD

TIMESTAMPDIFF

TINYBLOB (R)

TINYINT (R)

TINYTEXT (R)

TO (R)

TRAILING (R)

TRANSACTION

TRIGGER (R)

TRIGGERS

TRUE (R)

TRUNCATE

TYPE

TYPES

UNCOMMITTED

UNDEFINED

UNDO (R)

UNDOFILE

UNDO_BUFFER_SIZE

UNICODE

UNINSTALL

UNION (R)

UNIQUE (R)

UNKNOWN

UNLOCK (R)

UNSIGNED (R)

UNTIL

UPDATE (R)

UPGRADE

USAGE (R)

USE (R)

USER

USER_RESOURCES

USE_FRM

USING (R)

UTC_DATE (R)

UTC_TIME (R)

UTC_TIMESTAMP (R)

VALUE

VALUES (R)

VARBINARY (R)

VARCHAR (R)

VARCHARACTER (R)

VARIABLES

VARYING (R)

VIEW

WAIT

WARNINGS

WEEK

WEIGHT_STRING

WHEN (R)

WHERE (R)

WHILE (R)

WITH (R)

WORK

WRAPPER

WRITE (R)

X509

XA

XML

XOR (R)

YEAR

YEAR_MONTH (R)

ZEROFILL (R)

 

 

 

posted @ 2020-09-09 17:29  屠魔的少年  阅读(70)  评论(0)    收藏  举报