跳转至

数据字典

Conventions

  • 每一个代码块的顶部都有它所属的文件(相对)路径,如果代码块属于某个函数(方法),那么顶部会有函数(方法)的声明。
  • 代码块中的对象,如果很重要,会在行尾补充该对象的声明。
  • 我使用的MySQL 版本是 8.0.41。你看到这篇时,可能有了更新的版本,比如 8.0.42,区别不大的。

数据字典是什么?

数据字典就是存放元数据的多张表集合。这里的元数据指的是数据库、表、索引、视图等数据库对象的信息,比如用户创建了一个数据库 test,那么要把这个数据库的名字记下来,还有这个数据库使用什么字符集记下来,创建表也有很多信息,表的名字、字符集、使用的存储引擎、行格式等等,这些信息也是存进一张表里,和我们开发把业务数据写进表里没什么分别,区别在于数据字典表是 MySQL 负责创建和维护的。

数据字典都有哪些表

源码 sql/dd/impl/tables 目录下的每一个头文件都代表着一张表,所有表的集合就是数据字典。

sql/dd/impl/tables/schemata.h 为例,它代表存放数据库元数据的表 schemata,接下来我们看看这张表有哪些字段。

sql/dd/impl/tables/schemata.h
  enum enum_fields {
    FIELD_ID,
    FIELD_CATALOG_ID,
    FIELD_NAME,
    FIELD_DEFAULT_COLLATION_ID,
    FIELD_CREATED,
    FIELD_LAST_ALTERED,
    FIELD_OPTIONS,
    FIELD_DEFAULT_ENCRYPTION,
    FIELD_SE_PRIVATE_DATA,
    NUMBER_OF_FIELDS  // Always keep this entry at the end of the enum
  };

  enum enum_indexes {
    INDEX_PK_ID = static_cast<uint>(Common_index::PK_ID),
    INDEX_UK_CATALOG_ID_NAME = static_cast<uint>(Common_index::UK_NAME),
    INDEX_K_DEFAULT_COLLATION_ID
  };

  enum enum_foreign_keys { FK_CATALOG_ID, FK_DEFAULT_COLLATION_ID };

schemata 这张表存在于 mysql 系统数据库中,但是当我们打开 mysql 数据库时,却发现找不到 schemata,因为 MySQL 开发将它隐藏起来了。我们只能通过源码查看 schemata 的表结构。换做是你,你也不会把数据字典的表暴露出来,因为这些表里存放的是元数据,是一切的基石,一旦被人改了,MySQL 就无法正常工作了。

我们看贴出来的源码。源码第 50 行到第 69 行定义了 schemata 的字段、索引和外键,以枚举的形式。这么看,好像还是差了一点意思。

sql/dd/impl/tables/schemata.cc
Schemata::Schemata() {
  m_target_def.set_table_name("schemata");

  m_target_def.add_field(FIELD_ID, "FIELD_ID",
                         "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT");
  m_target_def.add_field(FIELD_CATALOG_ID, "FIELD_CATALOG_ID",
                         "catalog_id BIGINT UNSIGNED NOT NULL");
  m_target_def.add_field(FIELD_NAME, "FIELD_NAME",
                         "name VARCHAR(64) NOT NULL COLLATE " +
                             String_type(name_collation()->m_coll_name));
  m_target_def.add_field(FIELD_DEFAULT_COLLATION_ID,
                         "FIELD_DEFAULT_COLLATION_ID",
                         "default_collation_id BIGINT UNSIGNED NOT NULL");
  m_target_def.add_field(FIELD_CREATED, "FIELD_CREATED",
                         "created TIMESTAMP NOT NULL");
  m_target_def.add_field(FIELD_LAST_ALTERED, "FIELD_LAST_ALTERED",
                         "last_altered TIMESTAMP NOT NULL");
  m_target_def.add_field(FIELD_OPTIONS, "FIELD_OPTIONS", "options MEDIUMTEXT");
  m_target_def.add_field(FIELD_DEFAULT_ENCRYPTION, "FIELD_DEFAULT_ENCRYPTION",
                         "default_encryption ENUM('NO', 'YES') NOT NULL");
  m_target_def.add_field(FIELD_SE_PRIVATE_DATA, "FIELD_SE_PRIVATE_DATA",
                         "se_private_data MEDIUMTEXT");

  m_target_def.add_index(INDEX_PK_ID, "INDEX_PK_ID", "PRIMARY KEY (id)");
  m_target_def.add_index(INDEX_UK_CATALOG_ID_NAME, "INDEX_UK_CATALOG_ID_NAME",
                         "UNIQUE KEY (catalog_id, name)");
  m_target_def.add_index(INDEX_K_DEFAULT_COLLATION_ID,
                         "INDEX_K_DEFAULT_COLLATION_ID",
                         "KEY (default_collation_id)");

  m_target_def.add_foreign_key(FK_CATALOG_ID, "FK_CATALOG_ID",
                               "FOREIGN KEY (catalog_id) REFERENCES \
                                catalogs(id)");
  m_target_def.add_foreign_key(FK_DEFAULT_COLLATION_ID,
                               "FK_DEFAULT_COLLATION_ID",
                               "FOREIGN KEY (default_collation_id) \
                                REFERENCES collations(id)");

  m_target_def.add_populate_statement(
      "INSERT INTO schemata (catalog_id, name, default_collation_id, created, "
      "last_altered, options, default_encryption, se_private_data) VALUES "
      "(1,'information_schema',33, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, "
      "NULL, 'NO', NULL)");
}

我把 Schemata 这个类的构造函数贴了出来,这样是不是看得更明白了。以第 58 行和 59 行代码为例,m_target_def.add_field(FIELD_ID, "FIELD_ID", "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT"); 是在添加字段,继续看 add_field 定义。

sql/dd/impl/types/object_table_definition_impl.h
  void add_field(int field_number, const String_type &field_name,
                 const String_type field_definition) override {
    add_element(field_number, field_name, field_definition, &m_field_numbers,
                &m_field_definitions);
  }

add_field 函数的第一个形参是字段号,对应枚举中的 FIELD_ID,也就是将枚举当作整型来用,表示的是这个字段在表中的顺序,所以枚举中的排序是按照字段顺序来排列的。

第二个形参是字段名,对应字符串 "FIELD_ID",第三个形参是字段定义,对应字符串 "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT",看到这里我想大家有数了吧,这和我们平时的建表语句没什么区别。

MySQL 开发将数据字典表 schemata 的定义用上面这种方式定义好了,是面向对象编程的体现。如果是我们,可能直接就是写死 SQL。第 93 行,死 SQL 来了,原来是这张表的初始化语句,新增了一条记录,可以看到新增记录是库 information_schema 的元数据。开玩笑的,虽说初始化语句写死的,但是是通过 m_target_def 对象的函数 add_populate_statement 来维护在对象内的。

接下来,我们看看 m_target_def 对象是什么。

sql/dd/impl/types/object_table_impl.h
Object_table_definition_impl m_target_def;

m_target_def 对象的类型是 Object_table_definition_impl,那就看它。

sql/dd/impl/types/object_table_definition_impl.h
 public: //line 43
  typedef std::map<String_type, int> Element_numbers; // line 44
  typedef std::map<int, String_type> Element_definitions; // line 45

  Element_numbers m_field_numbers; // line 93
  Element_definitions m_field_definitions; // line 94

  void add_element(int element_number, const String_type &element_name, // line 107
                   const String_type &element_definition,
                   Element_numbers *element_numbers,
                   Element_definitions *element_definitions) {
    assert(element_numbers != nullptr && element_definitions != nullptr &&
           element_numbers->find(element_name) == element_numbers->end() &&
           element_definitions->find(element_number) ==
               element_definitions->end());

    (*element_numbers)[element_name] = element_number;
    (*element_definitions)[element_number] = element_definition;
  }

  void add_field(int field_number, const String_type &field_name, // line 216
                 const String_type field_definition) override {
    add_element(field_number, field_name, field_definition, &m_field_numbers,
                &m_field_definitions);
  }

Object_table_definition_impl 中关于 add_field 函数的部分都在上面。先看第 44、45 行,定义了两个别名,Element_numbers 是 key 为字符串,value 为整型的散列表,Element_definitions 和 Element_numbers 正好 key 和 value 反过来。这是为了方便,既可以通过字符串查找下标,又可以通过下标查找字符串。

第 93、94 行用别名声明了两个变量,m_field_numbers 和 m_field_definitions。

第 216 行的函数声明上面我们看过它们的形式参数,现在看函数块,很简单,调用了 add_element 函数,将字段序号 field_number、字段名 field_name、字段定义 field_definition 原封不动传给了 add_element,还有 93、94 行声明的两个散列表变量 &m_field_numbers 和 &m_field_definitions。

add_element 函数在第 107 行,assert 断言不用管,因为它是做校验的,我们不关心。那这函数就剩两行了,原来两个散列表变量是分别用来存字段名和字段序号、字段序号和字段定义的。m_field_numbers 的 key 存字段名,value 存字段序号。m_field_definitions 的 key 存字段序号,value 存字段定义。

刚开始我搞错了,以为 m_field_numbers 和 m_field_definitions 都是存字段名和字段序号,只是 key 和 value 对调,现在看不是的,变量命名就已经说明了它们存放的东西。

  Element_numbers m_index_numbers;
  Element_definitions m_index_definitions;

  Element_numbers m_foreign_key_numbers;
  Element_definitions m_foreign_key_definitions;

    void add_index(int index_number, const String_type &index_name,
                 const String_type &index_definition) override {
    add_element(index_number, index_name, index_definition, &m_index_numbers,
                &m_index_definitions);
  }

  virtual void add_foreign_key(int foreign_key_number,
                               const String_type &foreign_key_name,
                               const String_type &foreign_key_definition) {
    add_element(foreign_key_number, foreign_key_name, foreign_key_definition,
                &m_foreign_key_numbers, &m_foreign_key_definitions);
  }

add_index、add_foreign_key,和 add_field 是差不多的,都是调用 add_element 方法,存放的散列表不同而已。索引放在 m_index_numbers 和 m_index_definitions 中,外键放在 m_foreign_key_numbers 和 m_foreign_key_definitions 中。

好了,看完了 schemata 这张数据字典表的定义,再去看 sql/dd/impl/tables 目录下其他数据字典表的定义就很轻松了,都差不多的。

小结

数据字典是 MySQL 用来存放数据库对象(库、表、索引、约束、触发器等)元数据的表集合,具体有多少张表,表的定义又是如何的,要去 sql/dd/impl/tables 目录下看,通过查询语句是查看不到的,因为被 MySQL 开发隐藏起来了。好心的 MySQL 开发还是给用户留了查看的地方的,在 information_schema 数据库中的视图,数据都是来自数据字典表,不信你看 information_schema.SCHEMATA 视图的定义。