쿼리에 간접적으로 영향을 줄 수 있는 인덱스와 제약조건constraint 기능에 대해 알아보자.
비개발자라도 인덱스는 다들 들어봤을 것이다. 색인 같은 것을 생각했다면 그게 맞다.
MySQL은 기본적으로 데이터를 검색할 때 첫 번째 필드부터 차례대로 훑으면서 내려간다. 즉, 내가 쿼리를 쓰면 그 쿼리의 결과값을 보여주기 위해 1개 이상의 테이블을 처음부터 끝까지 다 훑는다는거다. 내가 지금까지 썼던 쿼리들은 테이블의 행이 적기 때문에 금방금방 돌아갔는데, 테이블에 3,000,000개의 행이 있다고 생각해보면... 🤦🏾♀️ 괴로워진다.
인덱스의 사용목적은 테이블 내의 데이터를 빠르게 찾기 위해서다. 사람이 인덱스를 이용해서 빠르게 책의 원하는 곳으로 가는것처럼, 데이터베이스 서버는 인덱스를 사용해 빠르게 테이블의 행을 찾는다.
책 + 구글링해보니 2가지 방법이 있는 것으로 보인다.
CREATE INDEX 인덱스이름
ON 테이블이름 (열이름) ;
혹은,
ALTER TABLE 테이블이름
ADD INDEX 인덱스이름 (열이름) ;
SHOW
명령어로 인덱스를 조회할 수도 있다.
SHOW INDEX FROM 테이블이름 ;
기본적으로 서버는
PRIMARY KEY
의 인덱스는 자동 생성하고,PRIMARY
라는 이름을 부여한다.
데이터베이스를 설계할 때는 중복 데이터를 포함할 수 있는 열과 그렇지 않은 열을 고려하는게 중요하다.
두 열을 사용해서 인덱스를 만들 수 있다. 예를 들면 회원이 많을 경우에는 이름만 아는 것보다, 이름과 생년월일을 동시에 검색하게 하면 더 신속하게 원하는 결과를 찾을 수 있을 것이다.
단, 다중 열 인덱스는 단일 열 인덱스보다 더 비효율적으로
INSERT
,UPDATE
,DELETE
를 수행하기 때문에 신중하게 사용하자. 가급적 업데이트가 안되는 값을 사용하는게 좋다.
MySQL, 오라클, SQL 서버는 디폴트로 B-Tree Index를 사용한다. B-Tree 인덱스는 잎 노드leaf node와 하나 이상의 가지 노드branch node 레벨이 있는 트리tree로 구성된다.
예를 들어 customer.last_name
열에 생성된 B-Tree 인덱스는 위 그림과 같을 것이다. 만약 성이 G
로 시작하는 모든 고객을 검색하는 쿼리를 실행한다면 서버는,
Hawthorne
)
Binary Search Index의 작동방식에 대해 잘 설명한 그림이다. (출처: https://shrtm.nu/F5Qi) B-Tree Index는 Binary Search Index의 한계를 극복한 자료구조다.
B-Tree 인덱스가 균형 트리 인덱스balanced-tree index로도 불리는 이유는 서버가 테이블에 행이 삽입, 업데이트, 삭제 됨에 따라 값을 균등하게 재분배해 트리의 균형을 유지하려 하기 때문이다.
비트맵 인덱스는 MySQL에서는 지원하지 않는다.
만약 활성화고객은 1, 비활성화고객은 0으로 나타낸 인덱스가 있다고 하자. 정상적인 서비스라면 활성화고객 수가 훨씬 많을 것이기 때문에 0보다 1의 갯수가 훨씬 많아질꺼고, 균형을 유지하기 어렵다.
따라서 많은 양의 데이터가 상대적으로 적은 값을 포함하는 열의 경우 다른 종류의 인덱스가 필요하다. 이때 사용하는게 비트맵 인덱스이다. (예: 영업 분기, 지역, 제품코드 등)
DB에 문서가 저장된 경우, 해당 문서에서 특정 단어나 구문을 검색하도록 지원해야 할 수 있다. MySQL에서는 풀텍스트 인덱스full-text index라는게 있다.
MySQL 서버 옵티마이저가 쿼리 실행을 결정하는 방법을 EXPLAIN
명령어로 확인해보자.
SELECT
customer_id
, first_name
, last_name
FROM customer
WHERE first_name LIKE 'S%'
AND last_name LIKE 'P%' ;
위의 쿼리의 경우 서버는 다음 전략 중 1가지를 사용할 수 있다.
last_name
열의 인덱스를 사용해 성이 P로 시작하는 모든 고객을 찾는다. → Customer 테이블의 각 행을 읽어서 이름이 S로 시작하는 행만 찾는다. last_name
과 first_name
열의 인덱스를 사용해 성이 P로 시작하고, 이름이 S로 시작하는 모든 고객을 찾는다.3번이 가장 합당해보인다. 실제로 MySQL 서버 옵티마이저의 쿼리 실행 계획을 확인해보자.
key
열에서 last_name
인덱스를 통해 쿼리가 실행될 것임을 알 수 있다. 또 type
열을 보면 range
라고 나와있다. 범위 스캔이 사용될 것이라는 뜻이다. 즉, 데이터베이스 서버는 하나의 행을 검색하는 대신 인덱스에서 값의 범위를 찾는다.
더 많은 인덱스가 반드시 좋은 것은 아니다. 모든 인덱스는 결국 특수한 유형의 테이블이기 때문이다. 즉, 테이블에서 행을 추가하거나 삭제할 때마다 해당 테이블의 모든 인덱스도 수정해야 한다. 따라서 인덱스가 많을수록 서버가 모든 스키마를 최신 상태로 유지하기 위해 더 많은 작업을 수행해야 하므로 속도가 느려진다.
제약이란 테이블의 하나 이상의 열에 적용되는 제한사항이다.
제약조건은 데이터베이스가 일관성을 가질 수 있게 해준다. 예를 들어 rental 테이블에서 동일한 고객ID를 변경하지 않고, customer 테이블에서만 고객ID를 변경할 수 있도록 허용한다면(=제약조건이 없다면), 유효하지 않은 고객ID의 대여 데이터가 생길꺼다. 즉, 고아 행orphan row이 생긴다.
제약조건은 보통 CREATE TABLE
문을 통해 테이블과 동시에 생성된다.
시리즈 거의 첫 부분에서 Table 만들기 를 했었는데 이때 테이블을 만드는 과정은 이랬다.
아래와 같은 스키마를 생성한다고 가정했을 때, 제약조건은 3가지가 포함된다.
CREATE TABLE customer (
customer_id SMALLINT UNSIGNED AUTO_INCREMENT NOT NULL
, store_id TINYINT UNSIGNED NOT NULL
, first_name VARCHAR(45) NOT NULL
, last_name VARCHAR(45) NOT NULL
, email VARCHAR(50) DEFAULT NULL
, address_id SMALLINT UNSIGNED NOT NULL
, active BOOLEAN NOT NULL DEFAULT TRUE
, create_date DATETIME NOT NULL
, last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
, PRIMARY KEY (customer_id)
, KEY idx_fk_store_id (store_id)
, KEY idx_fk_address_id (address_id)
, KEY idx_fk_last_name (last_name)
, CONSTRAINT fk_customer_address FOREIGN KEY (address_id)
REFERENCES address (address_id) ON DELETE RESTRICT ON UPDATE CASCADE
, CONSTRAINT fk_customer_store FOREIGN KEY (store_id)
REFERENCES store (store_id) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
FK 제약조건의 경우, CREATE TABLE
문으로도 설정할 수 있고, 일단 테이블 생성 후 ALTER TABLE
문으로 추가할수도 있다.
ALTER TABLE customer
ADD CONSTRAINT fk_customer_address FOREIGN KEY (address_id)
REFERENCES address (address_id) ON DELETE RESTRICT ON UPDATE CASCADE ;
ALTER TABLE customer
ADD CONSTRAINT fk_customer_store FOREIGN KEY (store_id)
REFERENCES store (store_id) ON DELETE RESTRICT ON UPDATE CASCADE ;
이 때 여러개의 ON
절이 포함되어 있는데 의미는 이렇다.
위의 쿼리예제에서는, 자식테이블은 customer가 되고 부모테이블은 address 또는 store가 된다.
ON DELETE RESTRICT
절은 부모테이블에서 행이 삭제될 때 고아 행이 발생하는 것을 막아준다. ON UPDATE CASCADE
절은 부모테이블에서 PK값이 수정될 때, 자동으로 자식테이블을 업데이트함으로써 고아 행이 발생하는 것을 막아준다. 외래키가 포함된 테이블을 자식 테이블이라고 하고 외래키 값을 제공하는 테이블을 부모 테이블이라한다.
ON DELETE RESTRICT
ON DELETE CASCADE
ON DELETE SET NULL
ON UPDATE RESTRICT
ON UPDATE CASCADE
ON UPDATE SET NULL
선택사항이며 ON DELETE
에서 0개 또는 1개 + ON UPDATE
에서 0개 또는 1개를 각각 선택할 수 있다.