1. 程式人生 > 實用技巧 >儲存引擎系列(四):不同型別的查詢語句如何設定索引(上)—— 資料表初始化

儲存引擎系列(四):不同型別的查詢語句如何設定索引(上)—— 資料表初始化

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();

這裡我們向 demo 表插入了 1000000 條記錄,由於沒有設定任何索引,所以查詢耗時很長:

如果使用 explain 檢視執行計劃的話,通過 type 欄位為 ALL 表明使用了全表掃描:

而如果我們為欄位設定索引的話:

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;

則查詢效率會明顯改善:

通過執行計劃可以看到第二次查詢命中了主鍵索引,使得查詢效率提升了 30 倍。

通過編寫程式碼

如果表結構比較複雜,要填充資料表,可以通過編碼的方式實現,只不過使用這種方式效能遠不及儲存過程高效。

這裡我們藉助一個 PHP 命令列應用微框架 Laravel Zero 演示資料表結構的初始化和測試資料的填充。

資料庫和專案初始化

開始之前,我們先來建立一個測試資料庫 test_db

然後執行如下命令通過 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

建立演示資料表

建立一個數據庫模型類和對應的遷移檔案

在資料表遷移檔案 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 表:

這樣就可以看到資料庫 test_db 中已經存在這個資料表了:

或者你也可以執行如下 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 之外,並沒有新增其他的索引欄位。

通過模型工廠模擬資料插入

接下來,我們通過模型工廠來編寫填充示例資料的程式碼:

開啟剛剛生成的模型工廠檔案 UserFactory.php(位於 database/factories 目錄下),編寫模型工廠程式碼如下:

1
<?php
2
3
/** @var \Illuminate\Database\Eloquent\Factory $factory */
4
5
use App\User;
6
use Faker\Generator 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 Illuminate\Database\Eloquent\Model;
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 App\Commands;
4
5
use App\User;
6
use Illuminate\Console\Scheduling\Schedule;
7
use LaravelZero\Framework\Commands\Command;
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  \Illuminate\Console\Scheduling\Schedule $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,由於插入記錄多,所以會比較耗時(這個時候可以泡杯咖啡,慢慢等待☕️,或者去幹點別的):

Tips:可以看到,使用這種方式插入 100000 條記錄也遠不及使用儲存過程插入 1000000 條記錄來的快。

如果你想要插入更多記錄,可以開啟新的終端視窗並行執行上述命令,比如你想要插入 1000000 條記錄,則同時開啟 10 個終端視窗執行上述命令即可。

你可以通過 select count(*) from users 檢視總記錄數是否是 100000:

下篇教程,我們將以 users 表為例演示對於不同型別的查詢語句,如何合理設定索引欄位可以有效提升查詢效能,而又不用帶來過多的索引代價