zoukankan      html  css  js  c++  java
  • 存储引擎系列(四):不同类型的查询语句如何设置索引(上)—— 数据表初始化

    B+ 索引树回顾

    上篇教程学院君给大家介绍了不同类型的数据库索引对应的 B+ 树是如何维护的,这其实是对数据库表记录进行更新时底层所做的(插入、修改、删除)事情,我们来简单回顾下 B+ 索引树:

    • 每个索引都对应一棵 B+ 树,这棵 B+ 树最下面一层叶子节点存放的是存储用户记录的数据页,其他层存放的是存储数据页目录项(这里的数据页可能是叶子节点、也可能是非叶子节点)的数据页;
    • 对于 InnoDB 存储引擎而言,主键索引也叫簇拥索引,叶子节点存储的用户记录包含了对应表记录的完整数据集,如果一张表没有指定主键,则系统会自动为其创建一个隐式主键;
    • 对于二级索引(唯一索引、普通索引、联合索引),叶子节点存储的用户记录由索引列和主键值组成(如果对应数据表没有指定主键,则使用系统自动生成的隐式主键),因此想通过二级索引获取完整数据记录,需要经历两次查询:先通过二级索引获取对应记录主键值,再通过主键值到簇拥索引获取完整数据记录(这一步操作叫做回表);
    • B+ 树的每一层节点以及节点内的记录都是按照索引值从小到大排列的,这样一来,当我们进行 SQL 查询时,就可以从 B+ 树的根节点开始,先通过二分查找在数据页目录中快速定位到记录所在的数据页,再在存储用户记录的数据页中通过二分查找找到对应的数据记录,由于二分查找效率非常高,所以命中索引的 SQL 查询效率也非常高。

    注:上篇教程是在数据表有主键的基础上介绍 B+ 索引树的维护,如果一张数据表没有指定主键,则 MySQL 会自动为其创建隐式主键,这样一来,就依然会有完整的簇拥索引和二级索引,只是这个隐式索引字段是虚拟的,不可能通过显式的 SQL 查询条件命中,但是如果命中了二级索引,回表的时候依然不会出现全表扫描,而是通过隐式主键去簇拥索引中拿到完整数据记录。

    SQL 查询语句分为多种类型,包括等值查询、范围查询、模糊匹配、连接查询,以及排序、分组、限定等更复杂的过滤条件,在各种不同的查询场景下,又是如何命中索引对指定 B+ 树进行搜索的呢?这将是我们今天所要探讨的问题。

    初始化数据库

    通过存储过程

    对于一些非常简单的数据表示例数据填充,可以通过 MySQL 自带的存储过程来实现,比如我们创建一个名为 demo 的数据表:

     
     
    1
    CREATE TABLE `demo` (
    2
      `a` int(10) unsigned NOT NULL,
    3
      `b` int(10) unsigned NOT NULL,
    4
      `c` int(10) unsigned NOT NULL
    5
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

    为了方便对比测试,我们先不设置任何索引字段,然后我们通过存储过程对这张数据表进行填充:

     
     
    1
    delimiter ;;
    2
    create procedure insertdata()
    3
    begin
    4
      declare i int;  
    5
      set i=1;  
    6
      while(i<=1000000)do
    7
          insert into demo values(i, i, i);    
    8
          set i=i+1;  
    9
      end while;
    10
    end;;
    11
    delimiter ;
    12
    call insertdata();

    -w571

    这里我们向 demo 表插入了 1000000 条记录,由于没有设置任何索引,所以查询耗时很长:

    -w592

    如果使用 explain 查看执行计划的话,通过 type 字段为 ALL 表明使用了全表扫描:

    -w1258

    而如果我们为字段设置索引的话:

     
     
    1
    CREATE TABLE `demo` (
    2
      `a` int(10) unsigned NOT NULL,
    3
      `b` int(10) unsigned NOT NULL,
    4
      `c` int(10) unsigned NOT NULL,
    5
      PRIMARY KEY (`a`),
    6
      KEY `b_c` (`b`, `c`)
    7
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

    则查询效率会明显改善:

    -w1200

    通过执行计划可以看到第二次查询命中了主键索引,使得查询效率提升了 30 倍。

    通过编写代码

    如果表结构比较复杂,要填充数据表,可以通过编码的方式实现,只不过使用这种方式性能远不及存储过程高效。

    这里我们借助一个 PHP 命令行应用微框架 Laravel Zero 演示数据表结构的初始化和测试数据的填充。

    数据库和项目初始化

    开始之前,我们先来创建一个测试数据库 test_db

    -w833

    然后运行如下命令通过 Composer 初始化一个命令行应用 db-test

     
     
    1
    composer create-project --prefer-dist laravel-zero/laravel-zero db-test -vvv

    进入 db-test 项目目录,安装数据库依赖:

     
     
    1
    php application app:install database

    以及支持通过 .env 配置环境变量:

     
     
    1
    php application app:install dotenv

    .env 中完成数据库配置:

     
     
    1
    DB_CONNECTION=mysql
    2
    DB_HOST=localhost
    3
    DB_PORT=3306
    4
    DB_DATABASE=test_db
    5
    DB_USERNAME=root
    6
    DB_PASSWORD=root

    创建演示数据表

    创建一个数据库模型类和对应的迁移文件

    -w639

    在数据表迁移文件 2020_09_07_094403_create_users_table.php 中,编写创建数据表的 up 方法如下:

     
     
    1
    public function up()
    2
    {
    3
        Schema::create('users', function (Blueprint $table) {
    4
            $table->id();
    5
            $table->string('name', 50);
    6
            $table->string('id_number', 60);
    7
            $table->boolean('gender');
    8
            $table->string('address', 100);
    9
            $table->date('birthday');
    10
        });
    11
    }

    然后运行 php application migrate 创建 users 表:

    -w674

    这样就可以看到数据库 test_db 中已经存在这个数据表了:

    -w510

    或者你也可以执行如下 SQL 语句去创建:

     
     
    1
    CREATE TABLE `users` (
    2
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    3
      `name` varchar(50) NOT NULL,
    4
      `id_number` varchar(60) NOT NULL,
    5
      `gender` tinyint(1) NOT NULL,
    6
      `address` varchar(100) NOT NULL,
    7
      `birthday` date NOT NULL,
    8
      PRIMARY KEY (`id`)
    9
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

    为了对比索引存在与否对查询性能的影响,除了主键 ID 之外,并没有添加其他的索引字段。

    通过模型工厂模拟数据插入

    接下来,我们通过模型工厂来编写填充示例数据的代码:

    -w687

    打开刚刚生成的模型工厂文件 UserFactory.php(位于 database/factories 目录下),编写模型工厂代码如下:

     
     
    1
    <?php
    2
    3
    /** @var IlluminateDatabaseEloquentFactory $factory */
    4
    5
    use AppUser;
    6
    use FakerGenerator as Faker;
    7
    8
    $factory->define(User::class, function (Faker $faker) {
    9
        return [
    10
            'name' => $faker->name,
    11
            'id_number' => $faker->uuid,
    12
            'gender' => $faker->boolean,
    13
            'address' => $faker->address,
    14
            'birthday' => $faker->date(),
    15
        ];
    16
    });

    修改 User 模型类代码如下:

     
     
    1
    <?php
    2
    3
    namespace App;
    4
    5
    use IlluminateDatabaseEloquentModel;
    6
    7
    class User extends Model
    8
    {
    9
        const FEMALE = 0;
    10
        const MALE = 1;
    11
    12
        public $timestamps = false;
    13
    }

    编写填充用户数据命令

    最后我们编写一个命令调用模型工厂填充 users 数据表:

     
     
    1
    php application make:command SeedUsersTable

    生成的命令类位于 app/Commands 目录下,编写命令类代码如下:

     
     
    1
    <?php
    2
    3
    namespace AppCommands;
    4
    5
    use AppUser;
    6
    use IlluminateConsoleSchedulingSchedule;
    7
    use LaravelZeroFrameworkCommandsCommand;
    8
    9
    class SeedUsersTable extends Command
    10
    {
    11
        /**
    12
         * The signature of the command.
    13
         *
    14
         * @var string
    15
         */
    16
        protected $signature = 'seed:users';
    17
    18
        /**
    19
         * The description of the command.
    20
         *
    21
         * @var string
    22
         */
    23
        protected $description = 'Seed Users Table';
    24
    25
        /**
    26
         * Execute the console command.
    27
         *
    28
         * @return mixed
    29
         */
    30
        public function handle()
    31
        {
    32
            $this->info('Start seeding users table...');
    33
            $startTime = time();
    34
            // 插入 100000 条记录
    35
            $amount = 100000;
    36
            // 通过进度条显式进度
    37
            $this->output->progressStart($amount);
    38
            // 调用模型工厂插入用户记录,每次插入 1000 条
    39
            for ($i = 0; $i < $amount; $i += 1000) {
    40
                factory(User::class, 1000)->create();
    41
                $this->output->progressAdvance(1000);
    42
            }
    43
            $this->output->progressFinish();
    44
            $endTime = time();
    45
            $execTime = $endTime - $startTime;
    46
            $this->info('Finished.(Time spent: ' . $execTime . 's)');
    47
        }
    48
    49
        /**
    50
         * Define the command's schedule.
    51
         *
    52
         * @param  IlluminateConsoleSchedulingSchedule $schedule
    53
         * @return void
    54
         */
    55
        public function schedule(Schedule $schedule): void
    56
        {
    57
            // $schedule->command(static::class)->everyMinute();
    58
        }
    59
    }

    重点关注 handle 方法,这是我们运行 seed:users 命令时底层所执行的代码:这里我们插入了 100000 条记录,每次调用模型工厂插入 1000 条记录,并且通过输出进度条显示插入进度,所有记录插入成功后输出提示文本和耗时。

    在终端 db_test 项目根目录下执行 php application seed:users,由于插入记录多,所以会比较耗时(这个时候可以泡杯咖啡,慢慢等待☕️,或者去干点别的):

    -w640

    Tips:可以看到,使用这种方式插入 100000 条记录也远不及使用存储过程插入 1000000 条记录来的快。

    如果你想要插入更多记录,可以打开新的终端窗口并行执行上述命令,比如你想要插入 1000000 条记录,则同时打开 10 个终端窗口执行上述命令即可。

    你可以通过 select count(*) from users 查看总记录数是否是 100000:

    -w619

    下篇教程,我们将以 users 表为例演示对于不同类型的查询语句,如何合理设置索引字段可以有效提升查询性能,而又不用带来过多的索引代价

  • 相关阅读:
    html5的跨域处理
    file表单提交异步模拟
    Bigpipe :bigpipe的了解之2
    javascript获取指定父元素
    javascript淡入淡出的效果轮换转播
    IE下,动态创建的iframe在异步提交时会跳转的问题的解决
    javascriptIE不支持table的innerHTML解决方案
    javascript删除元素所引起的 对于NodeList的理解
    javascript淡入淡出的效果轮换转播后续
    数据加载的bigpipe
  • 原文地址:https://www.cnblogs.com/aipiaoborensheng/p/13650817.html
Copyright © 2011-2022 走看看