Skip to main content

Database Index

前言​

此文章的目的並非讓大家對於 Btree 有深度了解,而是使用簡單的方式解釋 Database Index 的基本原理,使得在設計 Index 的時候有正確的方向。

介紹​

當我們發現資料庫 Query 速度越來越慢,且已經漸漸地影響到使用者體驗的時候,那就是優化資料庫的最佳時機,而最典型的方法就是加 index,但是在設計上如果不理解 index 的原理而隨便亂加的話,反而會造成整體資料庫效率降低!!

目前多數的 RDBMS 的 index 都是由 Btree 的方法實作,所以講 Database index 之前,必須先簡單了解一下 Btree 的基本特性。

Btree​

這是 Btree 的基本樣貌: Btree

Btree 共有幾種特性:

  • 有別於一般二元樹,每個 node 都能擁有多個 Key
  • 一個 node 可以擁有 K+1 個支線(K = Key 的數量)
  • Key 數值必在兩個 Parent 的 Key 範圍內(如圖上的 56/74 都在 47 及 99 內)
  • Btree 為平衡樹且每個葉節點深度必定相同

接著我們來看一下 Search 速度上的差異,圖上的例子共有 23/44/47 等八個數字,若我們用 For-loop 的方式尋找,最差情況是 O(n)

若我們從 Btree 的方式搜尋,則最差情況是最深階層數,故為 O(log n)

Database index​

其實在使用資料庫的同時,我們已經不知不覺中使用了大量的 index,而這個 index 就是每個 Table 的 Primary Key,接下來我們就用 PK 來解釋 Database 的搜尋方式

首先我們先建立簡單的 Table

create table test(
id integer primary key,
num integer,
content varchar(40)
);

找出目前 test table 內的 index,可以看出 Primary Key 本身就會被建立成 index

eric_db=# select * from pg_indexes where tablename = 'test';
schemaname | tablename | indexname | tablespace | indexdef

public | test | test_pkey | | CREATE UNIQUE INDEX test_pkey ON public.test USING btree (id)

接著我們來討論 index 與 Btree 之間的關係

Btree-index

圖中所有的 Key 都是 test 中的 id(PK),而在葉節點的部分才會帶該筆 id 的資料內容。

Query​

select * from test where id = 29;

Steps

當我們下 Query 時,Database 找出 id 為 29 的方式如圖上所示,且與 Btree 的走訪方法相同,這邊我就不重複解釋。比較不同的是資料僅存在於葉節點,走訪的過程即便是遇到 Key 相同只要不在葉節點就無法取得資料。

那假設我們是找 num = 29 的時候也是照著 Btree 的方法?​

答案是否定的,只要沒設 index,Database 就會用 full scan 的方式去搜尋。

原理​

每當我們建立 index 時,Database 都會幫我們建立一個 index table 去維護這個 Btree,所以 index 越多則 index table 當然也越來越多,所需要的空間需求也會隨之增加。

當我們建立 Primary Key 以外的 index 時,基本上原理都是相同的,差別只在於非 PK 的 Btree 葉節點的資料內容為 PK。

接著上面的範例,我們設了 num 為 index,然後找出 num = 29,Database 動作如下

  • 透過 num 的 btree 找出 id
  • 再由 id 的 btree 找出整個 row 的內容

我的硬碟就是大,那我每個欄位都設 index 來加速好了​

上面有提到 Database 會幫我們維護所有的 index btree,若 index 太多會造成新增或刪除資料的時候負擔太大,同時維護多個 Btree(包括這些 index table 的讀寫動作)所造成的負擔都會使你的系統效能變差。

實驗​

隨機建立 100 萬份資料

create table test(
id integer primary key,
num integer,
content varchar(40)
);

create index test_num_index on test(num);

insert into test SELECT generate_series(1,1000000) as id, (random()*(10^3))::integer, (random()*(10^3))::VARCHAR(40);

若沒有 postgres 的環境的話,可以用 docker-compose 來玩,這邊是我實驗的環境

version: "3.3"
services:
postgres:
image: postgres:latest
container_name: eric_db
ports:
- 5432:5432
environment:
POSTGRES_DB: eric_db
POSTGRES_USER: eric
POSTGRES_PASSWORD: eric
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql

explain 是 postgres 提供出來查詢 sql command 的成本,我們就用此方法來實驗

eric_db=# explain select * from test where content = '29';
QUERY PLAN
Gather (cost=1000.00..13561.43 rows=1 width=26)
Workers Planned: 2
-> Parallel Seq Scan on test (cost=0.00..12561.33 rows=1 width=26)
Filter: ((content)::text = '29'::text)
(4 rows)
eric_db=# explain select * from test where id = 29;
QUERY PLAN

Index Scan using test_pkey on test (cost=0.42..8.44 rows=1 width=26)
Index Cond: (id = 29)
(2 rows)

若未使用 index 的情況下成本為 13561.43,這邊 Postgres 已經幫我們使用了並行處裡,否則成本會更高。反觀使用 index 的總成本只需要 8.44

這邊我就不解釋此成本的計算方式,簡單理解成數值越高成本越大即可,有興趣者可前往 postgres 的 document。

結論​

index 是非常典型的以空間換取時間的例子,但也不是 index 越多越好,而是必須透過實際情況來決定哪些 column 該設定 index,都沒用到的 column 設定 index 也是很浪費的。