SUBJECT: index와 constraints를 이해하고 사용해보자
select, insert, update, delete등의 SQL을 사용하는 것과 별개로,SQL의 사용에 영향을 주는 다른 기능들을 알아보자.
References
web
text book
- Learning sql 3rd edition/Alan Beaulieu/9781492057611
- SQL level up/Mic/9788968482519
Indexes
SELECT first_name, last_name FROM customer WHERE last_name LIKE 'Y%'; +------------+-----------+ | first_name | last_name | +------------+-----------+ | LUIS | YANEZ | | MARVIN | YEE | | CYNTHIA | YOUNG | +------------+-----------+ 3 rows in set (0.00 sec)매우 많은 데이터를 대상으로 table scan을 진행할 때,서버는 추가적인 도움없이 합리적인 시간 내에 결과를 생성할 수 없을 것이다.이때에 필요한 도움은 테이블 내의 하나 혹은 다수의 indexes의 형태로 받을 수 있다.
- index
- 출판물의 앞 혹은 뒤에 있는 목차, 용어 목록과 다르지 않게db서버는 indexes를 사용하여 테이블의 특정 행을 가리킨다.indexes는 일반적인 테이블과 다르게 특정한 순서가 유지되는 테이블이다.그들은 데이터 entity를 전부 포함하는 대신, 특정 행을 가리키기 위한 데이터를 포함하며, 거기에는 특정하는 행의 물리적인 위치정보도 포함된다.그러므로, indexes의 역할은 테이블의 데이터 subset을 추적하는 시설을 갖추어모든 테이블을 탐색하지 않도록 하는 것이다.
INDEX 생성
# Create Index on MYSQL ALTER TABLE customer ADD INDEX idx_email (email); ## new since MYSQL5 Internally alter table # CREATE INDEX idx_email # ON customer (email); Query OK, 0 rows affected (0.10 sec) Records: 0 Duplicates: 0 Warnings: 0 # REMOVE Index on MYSQL ALTER TABLE customer DROP INDEX idx_email; ## new since MYSQL5 Internally alter table # DROP INDEX idx_email # on customer; Query OK, 0 rows affected (0.10 sec) Records: 0 Duplicates: 0 Warnings: 0
EXPLAIN SELECT * FROM customer WHERE email LIKE 'a%'; # with Index +----+-------------+----------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+----------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | customer | NULL | range | idx_email | idx_email | 203 | NULL | 44 | 100.00 | Using index condition | +----+-------------+----------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------+ # without Index +----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | customer | NULL | ALL | NULL | NULL | NULL | NULL | 599 | 11.11 | Using where | +----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+ SHOW index FROM customer WHERE Key_name = 'idx_email' \G; *************************** 1. row *************************** Table: customer Non_unique: 1 Key_name: idx_email Seq_in_index: 1 Column_name: email Collation: A Cardinality: 599 Sub_part: NULL Packed: NULL Null: YES Index_type: BTREE Comment: Index_comment: Visible: YES Expression: NULL 1 rows in set (0.01 sec)
CREATE INDEX idx_email ON customer (email);
Multicolumn indexes
CREATE INDEX idx_full_name ON customer (last_name, first_name); EXPLAIN SELECT * FROM customer WHERE last_name LIKE 'k%' AND FIRST_NAME LIKE 'k%'; +----+-------------+----------+------------+-------+-----------------------------+---------------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+----------+------------+-------+-----------------------------+---------------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | customer | NULL | range | idx_full_name,idx_last_name | idx_full_name | 364 | NULL | 14 | 11.11 | Using index condition | +----+-------------+----------+------------+-------+-----------------------------+---------------+---------+------+------+----------+-----------------------+ 1 row in set, 1 warning (0.00 sec)
이때 idx_full_name은 last_name 을 기준으로 first_name까지 연결된 것을 Index로 삼고 있기 때문에,
- last_name과 뒤로 first_name이 연결된 것을 검색할 때
- last_name을 기준으로 검색할 때
Tip
multicolumn index는 구성 요소가 같아도 순서가 다르다면(순서를 기준으로 구성되는 것이라) 생성 가능하다.
UNIQUE (unique index)
- INDEX로서의 기능(B-tree)
- 데이터 항목이 중복된 내용으로 수정, 생성이 되는 것을 거부하는 기능
# UNIQUE INDEX creation (ALTER)
CREATE UNIQUE INDEX idx_email
ON customer (email);
# duplicate insertion error
ERROR 1062 (23000): Duplicate entry 'dup-email@dupemail.org' for key 'customer.idx_email'
Types of Indexes
B-tree index
- branch node: A-Z, 0-9 등으로 traverse 방향을 가리키기 위한 navigating node
- leaf node(no child node): actual value와 physical 데이터 위치값을 가진 actual node
Bitmap index
두 개 범위의 값을 가지고 있는 경우(true, false)두 종류의 비트맵을 생성한다.만약 false인 데이터를 찾는다면, DB서버는 0 비트맵을 읽고 빠르게 찾으려는 데이터를 조회한다.
# Oracle feature
CREATE BITMAP INDEX idx_active
ON customer (active);
Text index
Note
- MySQL의 경우에는 full text indexes라는 도구를 지원하며,
- Oracle의 경우에는 Oracle Text라는 도구를 지원한다.
Using Index
각 Database Server 는 query optimizer가 어떻게 SQL을 처리하는지 확인시켜주는 도구를 포함한다.
EXPLAIN SELECT customer_id, first_name, last_name FROM customer WHERE first_name LIKE 'S%' AND last_anme LIKE 'P%' \G; id: 1 select_type: SIMPLE table: customer partitions: NULL type: range possible_keys: idx_last_name key: idx_last_name key_len: 182 ref: NULL rows: 28 filtered: 11.11 Extra: Using index condition; Using where 1 row in set, 1 warning (0.02 sec)
Tip
테이블 내에 Index를 생성하는 것이 적합한지는 값의 다양성 분포(cardinality)가 높은지에 달려 있다.
SQL구문의 Index활용 효율성은 데이터 선택 압축율이 10%보다 작은 것을 기준으로 일반적으로 평가된다.
압축율이 높은 범위의 Index scan을 하도록 제한하는 방법을 사용한다.
SELECT * FROM post WHERE created BETWEEN '2023-08-01' AND '2023-08-07'
찾으려는 데이터 항목을 Covering Index로 생성하고 이를 사용한다.
이는 테이블 접근 없이, multicolumn index object 내에서 검색과 추출이 종료됨을 의미한다.
# multicolumn index 없이 두 항목을 필터 추출 EXPLAIN FORMAT=JSON SELECT last_name, address_id FROM customer WHERE address_id BETWEEN 200 AND 300 AND (last_name LIKE 'H%' OR last_name LIKE 'J%')\G;
EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "28.86" }, "table": { "table_name": "customer", "access_type": "range", "possible_keys": [ "idx_fk_address_id", "idx_last_name" ], "key": "idx_last_name", "used_key_parts": [ "last_name" ], "key_length": "182", "rows_examined_per_scan": 63, "rows_produced_per_join": 10, "filtered": "16.69", "index_condition": "((`sakila`.`customer`.`last_name` like 'H%') or (`sakila`.`customer`.`last_name` like 'J%'))", "cost_info": { "read_cost": "27.81", "eval_cost": "1.05", "prefix_cost": "28.86", "data_read_per_join": "5K" }, "used_columns": [ "last_name", "address_id" ], "attached_condition": "(`sakila`.`customer`.`address_id` between 200 and 300)" } } }
# multicolumn index에 대해서만 필터 추출 ALTER TABLE customer ADD INDEX idx_lname_addr (address_id, last_name); EXPLAIN FORMAT=JSON SELECT last_name, address_id FROM customer WHERE address_id BETWEEN 200 AND 300 AND (last_name LIKE 'H%' OR last_name LIKE 'J%')\G;
EXPLAIN: { "query_block": { "select_id": 1, "cost_info": { "query_cost": "20.60" }, "table": { "table_name": "customer", "access_type": "range", "possible_keys": [ "idx_fk_address_id", "idx_last_name", "idx_lname_addr" ], "key": "idx_lname_addr", "used_key_parts": [ "address_id" ], "key_length": "184", "rows_examined_per_scan": 99, "rows_produced_per_join": 10, "filtered": "10.52", "using_index": true, // <- covering index 사용여부 "cost_info": { "read_cost": "19.56", "eval_cost": "1.04", "prefix_cost": "20.60", "data_read_per_join": "5K" }, "used_columns": [ "last_name", "address_id" ], "attached_condition": "((`sakila`.`customer`.`address_id` between 200 and 300) and ((`sakila`.`customer`.`last_name` like 'H%') or (`sakila`.`customer`.`last_name` like 'J%')))" } } }
캐싱 데이터를 활용한다.
IO 크기를 줄이는 summary table (접근 테이블을 분할해서 복사한 테이블)
- 원본 테이블과의 sync가 fresh(신선)해야한다.
# 검색 데이터 항목과 primary key만 가지고 있는 subset. SELECT * FROM post_date_mart WHERE created BETWEEN '2023-08-01' AND '2023-08-07';
데이터 수를 줄이는 cache database (최근 사용중인 데이터만 저장하는 서버)
- 백업 DB로의 주기적인 업로드 batch job을 포함한다.
Downside of Index
- 많은 공간을 차지하며,
- 테이블에 대한 수정은 연관된 index table의 조정으로 이어진다.
따라서 Index를 많이 생성할수록 서버는 모든 Schem objects(index)이어서 관리해야하고 이는 속도를 느리게 한다.
월별로 실시하는 백업이 있다면, 필요한 만큼 index를 생성하고 업무가 끝난 뒤에 삭제하고,
데이터센터의 경우라면 로그가 쌓이는 낮에 index의 수정이 취약하고, 데이터를 백업하는 저녁에는 덜 필요할 것이다.
- 백업전에 index를 삭제하고, 데이터 백업 후 index를 새로 생성하는 것이 일반적이다.
Primary key
Foreign key, CASCADE
frequently used to retrieve data
Tip
- date column
- short string(2 ~ 50) column
위 경우 이외에는 피하는 것이 좋다.
Constraints
Primary key: | 데이터의 구성요소가 테이블 내에서 유일함을 보장함
|
---|---|
Unique key: | 데이터의 구성요소가 테이블 내에서 유일함을 보장함 |
Foreign key: | 데이터의 구성요소가 다른 테이블의 primary key 항목만을 포함하도록 보장함
|
Check key: | 데이터의 구성요소가 특정한 값들만 가질 수 있도록 제한함 |
constrints를 제외하면 database의 지속가능성은 의심받을 수 있는 것이다.
예를 들어, 만약 서버가 foreign key로 사용중인 customer의 ID를 변경하면서 연관된 정보를 수정하지 않는다면,해당 ID를 참조하는 다른 테이블의 정보는 유효하지 않은 것이 된다.이러한 데이터를 orphaned rows라고 부른다.반면 primary key, foreign key가 존재한다면,
DB서버는 제약사항이 무너지는지 확인되는 순간 에러를 발생시키거나,규칙을 유지하기 위해 다른 테이블에 이 변화를 전파할 것이다.
Creation
테이블 생성시 제약 조건 생성
CREATE TABLE customer ( customer_id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT, store_id TINYINT UNSIGNED NOT NULL, address_id SMALLINT UNSIGNED NOT NULL, last_name VARCHAR(45) NOT NULL, # Constraints ## PK PRIMARY KEY (customer_id), ## INDEX KEY idx_last_name (last_name), ## FK KEY idx_fk_store_id (store_id), KEY idx_fk_address_id (address_id), ### customer.address_id->address.address_id CONSTRAINT fk_customer_address FOREIGN KEY (address_id) REFERENCES address (address_id) ON DELETE RESTRICT ON UPDATE CASCADE, ### customer.store_id->store.store_id CONSTRAINT fk_customer_store FOREIGN KEY (store_id) REFERENCES store (store_id) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
테이블 생성 후 제약 조건 생성
ALTER TABLE customer ADD CONSTRAINT fk_customer_address FOREIGN KEY (address_id) REFERENCES address (address_id) ON DELETE RESTRICT ON UPDATE CASCADE;
- RESTRICT: 참조 항목 변경, 삭제 시도를 에러처리 한다.
- orphaned row가 발생하지 않도록 제한한다.
- CASCADE: 참조 항목 변경 시 데이터 항목은 이를 반영하며, 삭제시 항목을 포함하는 데이터도 삭제한다.
- orphaned row가 될 경우 삭제하도록 한다.
- SET <VALUE>: 참조 항목 변경, 삭제시 데이터 항목을 <VALUE>값으로 설정한다.