儲存引擎系列(四):不同型別的查詢語句如何設定索引(上)—— 資料表初始化
B+ 索引樹回顧
上篇教程學院君給大家介紹了不同型別的資料庫索引對應的 B+ 樹是如何維護的,這其實是對資料庫表記錄進行更新時底層所做的(插入、修改、刪除)事情,我們來簡單回顧下 B+ 索引樹:
- 每個索引都對應一棵 B+ 樹,這棵 B+ 樹最下面一層葉子節點存放的是儲存使用者記錄的資料頁,其他層存放的是儲存資料頁目錄項(這裡的資料頁可能是葉子節點、也可能是非葉子節點)的資料頁;
- 對於 InnoDB 儲存引擎而言,主鍵索引也叫簇擁索引,葉子節點儲存的使用者記錄包含了對應表記錄的完整資料集,如果一張表沒有指定主鍵,則系統會自動為其建立一個隱式主鍵;
- 對於二級索引(唯一索引、普通索引、聯合索引),葉子節點儲存的使用者記錄由索引列和主鍵值組成(如果對應資料表沒有指定主鍵,則使用系統自動生成的隱式主鍵),因此想通過二級索引獲取完整資料記錄,需要經歷兩次查詢:先通過二級索引獲取對應記錄主鍵值,再通過主鍵值到簇擁索引獲取完整資料記錄(這一步操作叫做回表);
- B+ 樹的每一層節點以及節點內的記錄都是按照索引值從小到大排列的,這樣一來,當我們進行 SQL 查詢時,就可以從 B+ 樹的根節點開始,先通過二分查詢在資料頁目錄中快速定位到記錄所在的資料頁,再在儲存使用者記錄的資料頁中通過二分查詢找到對應的資料記錄,由於二分查詢效率非常高,所以命中索引的 SQL 查詢效率也非常高。
注:上篇教程是在資料表有主鍵的基礎上介紹 B+ 索引樹的維護,如果一張資料表沒有指定主鍵,則 MySQL 會自動為其建立隱式主鍵,這樣一來,就依然會有完整的簇擁索引和二級索引,只是這個隱式索引欄位是虛擬的,不可能通過顯式的 SQL 查詢條件命中,但是如果命中了二級索引,回表的時候依然不會出現全表掃描,而是通過隱式主鍵去簇擁索引中拿到完整資料記錄。
SQL 查詢語句分為多種型別,包括等值查詢、範圍查詢、模糊匹配、連線查詢,以及排序、分組、限定等更復雜的過濾條件,在各種不同的查詢場景下,又是如何命中索引對指定 B+ 樹進行搜尋的呢?這將是我們今天所要探討的問題。
初始化資料庫
通過儲存過程
對於一些非常簡單的資料表示例資料填充,可以通過 MySQL 自帶的儲存過程來實現,比如我們建立一個名為 demo
的資料表:
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;
為了方便對比測試,我們先不設定任何索引欄位,然後我們通過儲存過程對這張資料表進行填充:
1delimiter ;;
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
表明使用了全表掃描:
而如果我們為欄位設定索引的話:
1CREATE 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
:
composer create-project --prefer-dist laravel-zero/laravel-zero db-test -vvv
進入 db-test
專案目錄,安裝資料庫依賴:
php application app:install database
以及支援通過 .env
配置環境變數:
php application app:install dotenv
在 .env
中完成資料庫配置:
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
方法如下:
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 語句去建立:
1CREATE 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
目錄下),編寫模型工廠程式碼如下:
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
模型類程式碼如下:
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
資料表:
php application make:command SeedUsersTable
生成的命令類位於 app/Commands
目錄下,編寫命令類程式碼如下:
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
表為例演示對於不同型別的查詢語句,如何合理設定索引欄位可以有效提升查詢效能,而又不用帶來過多的索引代價。