데이터 모델링
엔티티(Entity)
속성(attribute)
식별자
관계
요구 분석 단계
Non-Identifying Relationship
Identifying Relationship
외래키를 제공하는 쪽이 부모 엔티티, 참조하는 쪽이 자식 엔티티에 해당됨.
도메인
논리적 모델링
이상
제 1 정규화
제 2 정규화
제 3 정규화
반 정규화
물리적 모델링
| 논리적 DB 설계 | 물리적 DB 설계 |
|---|---|
| 엔티티(Entity) | 테이블(Table) |
| 속성(Attribute) | 컬럼(Column) |
| 주식별자(Primary Identifier) | 기본키(Primary Key) |
| 외래식별자(Foreign Identifier) | 외래키(Foreign Key) |
| 뷰(View) | |
| 인덱스(Index) |
테이블(Table)
CREATE ALTER, DROPCREATE TABLE 테이블명-- 테이블 생성 실습
CREATE TABLE tb_member (
mem_no INT,
mem_id VARCHAR(20),
mem_pass VARCHAR(20),
mem_name VARCHAR(15),
enroll_date DATE DEFAULT CURDATE()
);
-- 테스트 테이블에 샘플 데이터 추가
INSERT INTO tb_member VALUES(1, 'USER1', '1234', '홍길동','2024-12-08');
INSERT INTO tb_member VALUES(2, 'USER2', '2345', '이몽룡', CURDATE());
INSERT INTO tb_member VALUES(3, 'USER3', '1234', '성춘향', DEFAULT);
-- default 값이 삽입될 것.
INSERT INTO tb_member (mem_no, mem_id) VALUES (4, 'USER4');
-- 값을 넣지 않는 곳은 전부 default 값으로 삽입되게 됨. 제약 조건
CREATE TABLE tb_member(
mem_no INT NOT NULL,
mem_id VARCHAR(20) NOT NULL,
mem_pass VARCHAR(20) NOT NULL,
mem_name VARCHAR(15) NOT NULL,
enroll_date DATE DEFAULT CURDATE()
);
UPDATE tb_member
SET mem_id = NULL
WHERE mem_name = '홍길동';
-- NOT NULL로 인해 데이터 삽입/갱신이 불가능하다.
-- 제약 조건 실습
-- 기본키 Primary Key / Unique
-- UNIQUE 조건은 중복된 값이 들어오면 안돼! 임. NULL 값이 들어올 수 있음ㅇㅇ
INSERT INTO tb_member VALUES(1, 'USER1', '1234', '홍길동', CURDATE());
-- 이 경우 동일한 값을 갖는 여러 행이 존재하게 됨.
CREATE TABLE tb_member(
mem_no INT PRIMARY KEY, -- primary key는 노란색 열쇠로 나타나게 됨.
mem_id VARCHAR(20) NOT NULL UNIQUE, -- unique는 빨간색 열쇠로 나타남.
mem_pass VARCHAR(20) NOT NULL,
mem_name VARCHAR(15) NOT NULL,
enroll_date DATE DEFAULT CURDATE()
);
-- mem_no, mem_id는 기능적으로 동일하지만 의미가 다름.
INSERT INTO tb_member VALUES (1, 'USER1', '1234', '홍길동', '2024-12-08');
INSERT INTO tb_member VALUES (2, 'USER2', '1234', '이몽룡', CURDATE());
-- INSERT INTO tb_member VALUES (1, 'USER3', '1234', '성춘향', DEFAULT);
-- 에러가 나게 됨. 이미 1이라는 기본키 값이 존재하기 때문에
-- INSERT INTO tb_member VALUES (NULL, 'USER3', '1234', '성춘향', DEFAULT);
-- NULL 값도 허용하지 않기 때문에 에러가 나게 됨.
CREATE TABLE tb_member(
mem_no INT AUTO_INCREMENT PRIMARY KEY, -- column 영역에서 제약조건 설정
mem_id VARCHAR(20) NOT NULL UNIQUE,
mem_pass VARCHAR(20) NOT NULL,
mem_name VARCHAR(15) NOT NULL,
enroll_date DATE DEFAULT CURDATE()
);
-- autoincrement : 해당 열의 값을 자동으로 1씩 증가시켜 입력
INSERT INTO tb_member(mem_id, mem_pass, mem_name) VALUES ('USER1', '1234', '홍길동');
INSERT INTO tb_member(mem_id, mem_pass, mem_name) VALUES ('USER2', '1234', '이몽룡');
-- auto increment -> 대리키(GUI)
SELECT UUID();
-- 고유성이 보장된 숫자.
-- auto_increment 로 단순하게 증가하는 숫자를 쓰기 싫다면-사용하도록
-- 열 정의 후, 제약 조건을 별도로 지정하는 방법 -> 테이블 영역에서 제약조건 설정
DROP TABLE tb_member;
CREATE TABLE tb_member(
mem_no INT AUTO_INCREMENT, -- column 영역에서 제약조건 설정
mem_id VARCHAR(20) NOT NULL,
mem_pass VARCHAR(20) NOT NULL,
mem_name VARCHAR(15) NOT NULL,
enroll_date DATE DEFAULT CURDATE(),
-- primary key 제약 조건의 경우 여러개의 열을 묶어서 하나의 기본키 생성이 가능함.
PRIMARY KEY(mem_no),
-- UNIQUE(mem_id, mem_id) -- unique 제약 조건도 여러 개의 열을 묶어서 제약 조건으로 생성할 수 있음.
CONSTRAINT uq_tb_member_mem_id UNIQUE(mem_id) -- UNIQUE로 만들어진 애들은 이름이 바뀌어 있음
);
INSERT INTO tb_member(mem_id, mem_pass, mem_name) VALUES ('USER1', '1234', '홍길동');
INSERT INTO tb_member(mem_id, mem_pass, mem_name) VALUES ('USER2', '1234', '이몽룡');
-- FOREIGN KEY 제약조건
-- 부모 테이블 생성
CREATE TABLE tb_member_grade(
grade_code VARCHAR(10) PRIMARY KEY,
grade_name VARCHAR(10) NOT NULL
);
INSERT INTO tb_member_grade VALUES('vip', 'VIP 회원');
INSERT INTO tb_member_grade VALUES('gold', 'gold 회원');
INSERT INTO tb_member_grade VALUES('silver', 'silver 회원');
CREATE TABLE tb_member(
mem_no INT AUTO_INCREMENT PRIMARY KEY,
mem_id VARCHAR(20) NOT NULL UNIQUE,
mem_pass VARCHAR(20) NOT NULL,
mem_name VARCHAR(15) NOT NULL,
grade_code VARCHAR(10) REFERENCES tb_member_grade(grade_code), -- tb_member_grade의 grade_code를 참조하겠다-
enroll_date DATE DEFAULT CURDATE()
);
-- 외래키는 녹색 열쇠 형태로 만들어짐
INSERT INTO tb_member(mem_id, mem_pass, mem_name, grade_code) VALUES ('USER1', '1234', '홍길동', 'vip' );
INSERT INTO tb_member(mem_id, mem_pass, mem_name, grade_code) VALUES ('USER2', '1234', '이몽룡', NULL );
-- 두 번째 꺼는 Insert가 안될거임 -> 외래키 제약조건은 외래키 열에서 잘못된 값이 들어오는 걸 막아줌
-- 외래키 열에는 아무 값이나 들어올 수 있는 것이 아닌, 참조하고 있는 테이블의 참조 열의 값만 가져올 수 있음
-- tb_member_grade 테이블에 grade_code 열에 'bronze'라는 값이 없어서 외래키 제약 조건에 위배되어 에러가 나타나게 된다.
SELECT m.mem_no,
m.mem_id,
m.mem_name,
mg.grade_name
FROM tb_member m
LEFT OUTER JOIN tb_member_grade mg ON m.grade_code = mg.grade_code;외래키 제약 조건(FOREIGN KEY)
ON UPDATE CASCADE : 외래 키 값을 업데이트할 경우 행 전체가 업데이트 됨ON UPDATE SET NULL : 부모 테이블 값을 업데이트 할 경우, 자식 테이블은 똑같이 업데이트 되지 않고 NULL 값을 갖게 된다.ON DELETE CASCADE : 외래 키 값을 삭제할 때 참조하는 쪽의 값 전체가 사라짐 → vip 회원을 지운다고 했을 때, vip 회원 전부가 다 사라지게 되는 셈. → 한 번에 데이터가 전부 사라지기 때문에 이런게 있다- 정도로만 참고ON DELETE SET NULL : 부모 키의 값을 지울 경우 참조하는 테이블의 값이 NULL로 바뀌게 됨.CHECK 제약 조건
-- CHECK 제약 조건
CREATE TABLE tb_member(
mem_no INT AUTO_INCREMENT PRIMARY KEY,
mem_id VARCHAR(20) NOT NULL UNIQUE,
mem_pass VARCHAR(20) NOT NULL,
mem_name VARCHAR(15) NOT NULL,
gender CHAR(2),
age TINYINT,
grade_code VARCHAR(10) REFERENCES tb_member_grade(grade_code),
enroll_date DATE DEFAULT CURDATE()
);
INSERT INTO tb_member(mem_id, mem_pass, mem_name, gender, age, grade_code)
VALUES ('USER1', '1234', '홍길동', '남남', 36, 'vip' );
INSERT INTO tb_member(mem_id, mem_pass, mem_name, gender, age, grade_code)
VALUES ('USER2', '1234', '이몽룡', '남자', 30, NULL );
INSERT INTO tb_member(mem_id, mem_pass, mem_name, gender, age, grade_code)
VALUES ('USER3', '1234', '성춘향', '남남', -28, 'silver' );
SELECT *
FROM tb_member;
-- 위의 쿼리는 아무 문제가 없지만, 성별은 (남자, 여자) 중 하나로, 나이는 0~150 사이의 값을 갖도록 설정해 두고 싶다.
DROP TABLE tb_member;
CREATE TABLE tb_member(
mem_no INT AUTO_INCREMENT PRIMARY KEY,
mem_id VARCHAR(20) NOT NULL UNIQUE,
mem_pass VARCHAR(20) NOT NULL,
mem_name VARCHAR(15) NOT NULL,
gender CHAR(2) CHECK(gender IN ('남자', '여자')),
age TINYINT,
grade_code VARCHAR(10) REFERENCES tb_member_grade(grade_code),
enroll_date DATE DEFAULT CURDATE(),
-- 열을 지정한 뒤에 제약 조건을 지정하는 경우
CONSTRAINT ck_th_member_age CHECK(age BETWEEN 0 AND 150) -- 제약 조건 이름은 ck_th_member_age가 됨
);
INSERT INTO tb_member(mem_id, mem_pass, mem_name, gender, age, grade_code)
VALUES ('USER1', '1234', '홍길동', '남남', 36, 'vip' );
-- 위와 동일한 insert문을 사용하게 되면 gender에 걸려있는 제약조건에 의해 에러가 났다는 의미
INSERT INTO tb_member(mem_id, mem_pass, mem_name, gender, age, grade_code)
VALUES ('USER2', '1234', '이몽룡', '남자', 30, NULL );
-- 얘는 문제 없이 insert 됨
INSERT INTO tb_member(mem_id, mem_pass, mem_name, gender, age, grade_code)
VALUES ('USER3', '1234', '성춘향', '여자', -28, 'silver' );
-- 얘는 제약 조건 위배 -> ck_tb_member_age 의 제약 조건에 위배되었기 때문에 오류가 발생했다
-- 성별, 나이에 유효하지 않은 값들은 삽입이 불가능함 .
뷰(View)
뷰 생성 : CREATE VIEW AS SELECT ~
인덱스(Index)
클러스터형 인덱스
보조 인덱스
→ 직접 인덱스를 생성하고자 한다면 ALTER TABLE 이나 CREATE INDEX 등의 구문을 통해 생성함. → 이런 방식으로 생성된 인덱스는 다 보조 인덱스라고 생각하면 됨.
→ 여러 개의 열을 묶어서 하나의 인덱스로 만들 수도 있음.
인덱스는 고유 인덱스 vs 비고유 인덱스로 나눌 수 있음
※ 주의 사항 ※
→ 터미널에서 sql문 일괄 실행 → mariadb -u root -p 0000 → source employee.sql → 일괄 실행
-- 인덱스 실습
-- 검색 속도가 빨라지는 것을 확인
-- 사이트에서 제공하는 용량이 큰 데이터들을 다뤄본다.
SELECT *
FROM employees;
EXPLAIN SELECT * FROM employees ;
-- EXPLAIN : 해당 쿼리문을 실행할 때의 실행 계획을 확인할 수 있음
-- type에 ALL 이라고 되어 있음 -> 이는 인덱스를 사용하지 않고 전체 데이터를 검색했다는 것을 의미함. -> full table scan
SELECT * FROM employees WHERE emp_no = 48730;
EXPLAIN SELECT * FROM employees WHERE emp_no = 48730;
-- 이 경우 type은 const, possible key는 primary가 됨. -> const : cluster 형 인덱스인 프라이머리라는 데이터를 찾아옴
-- 인덱스를 이용해 검색헀다-> 전체 데이터 조회 보다 속도가 훨씬 빠르게 조회됨.
SELECT *
FROM employees
WHERE first_name = 'moon';
EXPLAIN SELECT * FROM employees WHERE first_name = 'moon';
-- type은 all로 나오게 됨. -> 해당 행을 다 확인할 때까지 rows : 298803개의 데이터를 검색했다고 생각하면 됨.
-- 데이터가 많으면 많을 수록 더 많은 데이터를 검색하게 됨.
-- first_name 은 제약 조건이 정해져 있는 애가 아니기 때문에 index가 자동으로 완성된 애가 아님.
-- 실습 : first_name 을 통해 인덱스를 생성해볼 것. -> first_name에는 중복된 값이 존재하기 때문에 비고유 인덱스로 생성할거임
-- 인덱스 생성
CREATE INDEX idx_employees_fisrt_name
ON employees(first_name);
ANALYZE TABLE employees; -- Msg_text : OK
-- first_name에 해당되는 인덱스는 녹색으로 나타나게 됨.
SELECT * FROM employees WHERE first_name = 'Moon';
EXPLAIN SELECT * FROM employees WHERE first_name = 'Moon';
-- type이 ref형으로 나오게 됨. -> 보조 인덱스를 싱행시켰다-
-- 사용 가능한 인덱스(possible_keys)에는 idx_employees_first_name이 나오게 됨.
-- rows에는 243개가 나오게 됨. -> 실제 검색에는 243번 밖에는 안썼다는 의미임.
-- 데이터가 압도적으로 많지는 않기 때문에 속도에서 큰 차이가 명확히 나지는 않지만,
-- rows 개수로 차이가 명확히 드러남
EXPLAIN SELECT * FROM employees WHERE first_name = 'moon' AND last_name = 'Yetto';
-- last_name이 Yetto인 경우 -> moon을 조회했을 때와 마찬가지로 243개를 조회함
CREATE INDEX idx_employees_firstname_lastname
ON employees(first_name, last_name);
-- 인덱스가 저장되어 있는 페이지도 존재하다 보니 데이터베이스의 크기가 조금 늘어나게 됨.
EXPLAIN SELECT * FROM employees WHERE first_name = 'moon' AND last_name = 'Yetto';
-- 사용 가능한 인덱스가 idx_employees_fisrt_name,idx_employees_firstname_lastname로 2개 있었고,
-- rows는 243개에서 1개로 줄게 됨. -> 단 한 번의 탐색으로 데이터를 찾아왔다는 의미.
-- 열 두개를 묶어서 하나의 인덱스로 생성해주면 되겠지
-- 테이블에 지정된 인덱스 확인
SHOW INDEX FROM employees; -- employees 테이블에 저장된 인덱스 종류 확인
-- Non_unique : 고유 인덱스(0)인지 비고유 인덱스(1)인지?
-- Seq_in_index : 인덱스 저장 순번이라고 보면 됨(column_name -> first_name -> last_name)
-- Cardinality : 데이터 수 정도로 생각
-- index_typ : BTREE라는 알고리즘으로 저장되어 있음.
-- 인덱스 삭제
DROP INDEX idx_employees_firstname_lastname ON employees;
DROP INDEX idx_employees_fisrt_name ON employees;
-- 인덱스가 있는데도 사용하지 않는 경우
-- 1) 전체 데이터를 Select 할 때
SELECT * FROM employees;
EXPLAIN SELECT * FROM employees;
-- 이 경우 인덱스가 있어도 사용하지 않음
SELECT * FROM employees WHERE emp_no < 250000;
EXPLAIN SELECT * FROM employees WHERE emp_no < 250000;
-- 전체 데이터 개수는 약 30만개/ -> 조건으로는 25만개 밑으로 조회할 경우
-- 사용 가능한 키로 primary key가 있는데도 인덱스를 사용하지 않음
SELECT * FROM employees WHERE emp_no < 100000;
EXPLAIN SELECT * FROM employees WHERE emp_no < 100000;
-- 이 경우에서는 primary key를 이용해서 조회함. -> type은 range
-- 범위를 지정해서 선택할 때 -> 전체 데이터의 30% 이상을 조회할거면 차라리 전체 데이터를 조회하는게 빠르겠다고 판단,
-- 이 경우 인덱스를 사용하지 않음.
SELECT * FROM employees WHERE emp_no = 100000;
EXPLAIN SELECT * FROM employees WHERE emp_no = 100000;
-- 위의 경우에서는 인덱스로 기본키를 사용, 딱 한 개만 조회하게 됨.
SELECT * FROM employees WHERE emp_no * 1 = 100000;
EXPLAIN SELECT * FROM employees WHERE emp_no * 1 = 100000;
-- 인덱스의 기준이 되는 열에 함수나 산술 연산을 하게 될 경우 인덱스를 사용하지 않음.
-- 중복되는 데이터가 많은 열을 인덱스 기준으로 두는 것도 의미 없음
-- 인덱스 추가
ALTER TABLE employees ADD INDEX idx_employees_gender (gender);
ANALYZE TABLE employees;
SELECT * FROM employees WHERE gender = 'M'; -- 조회는 전체 데이터의 절반 이상
EXPLAIN SELECT * FROM employees WHERE gender = 'M';
-- possible key는 있지만 type은 all로 나타나 됨 -> 중복 데이터가 많아서 인덱스를 생성해서 검색해도 사용하지는 않음.
ALTER TABLE employees DROP INDEX idx_employees_gender;