Skip to content

Latest commit

 

History

History
512 lines (383 loc) · 23.9 KB

0912-WPSN.md

File metadata and controls

512 lines (383 loc) · 23.9 KB

2017.09.12(화)

1. JOIN

이제까지 배운 것들은 전부 하나의 테이블에서 데이터를 가져오는 방법이었습니다.

예를 들어, 사원의 연봉 변동 기록 테이블의 각각의 레코드에 사원의 이름을 붙여서 출력해보려고 합니다. 그런데 문제는 사원의 연봉 변동 기록사원의 이름이 각각 다른 테이블에 저장되어 있다는 것입니다. 이 두 테이블에 저장되어 있는 레코드들을 이어주는 역할을 하는 컬럼이 있는데, 바로 사원 번호(emp_no) 입니다. 이 사원 번호를 이용해 두 테이블을 합쳐서 출력할 수 있습니다.

SQL에서의 조인(join) 은 여러 테이블의 데이터를 가져와서 하나의 테이블로 합치는 것을 말합니다. MySQL에서는 여러 가지 방법으로 조인을 할 수 있습니다.

FROM, WHERE 구문을 통한 조인

FROM 뒤에 여러 개의 테이블 이름을 적어서, 테이블을 합칠 수 있습니다.

SELECT salaries.salary, salaries.from_date, employees.first_name
FROM salaries, employees
WHERE salaries.emp_no = employees.emp_no;

테이블을 합칠 때에는 어떻게 합칠 지를 직접 명시해주어야 합니다. 이렇게 JOIN 구문을 사용하지 않고 FROMWHERE만을 사용해 조인하는 것을 암시적 조인(implicit join)이라고 합니다.

JOIN 구문

위의 예제처럼 테이블을 합칠 수도 있지만, 보통의 경우 JOIN 구문을 이용해서 조인을 하는 것이 관례입니다. (조인을 하고 있다는 것을 명확히 알 수 있고, 읽기도 쉽기 때문입니다.) 이렇게 직접 JOIN 구문을 사용해서 조인을 하는 것을 명시적 조인(explicit join)이라고 합니다. 아래의 예제는 바로 위의 암시적 조인과 완전히 같은 동작을 합니다.

SELECT salaries.salary, salaries.from_date, employees.emp_no, employees.first_name
FROM salaries
JOIN employees ON employees.emp_no = salaries.emp_no;

FROM에 적었던 두 번째 테이블의 이름을 JOIN 바로 뒤에 적어주었습니다. 이렇게 FROM 구문으로 지정한 하나의 테이블에 JOIN 구문을 사용해서 다른 테이블을 합칠 수 있습니다. 이전에 WHERE를 이용해 적어주었던 기준은 ON 구문을 이용해 지정해줍니다.

SELECT 구문 뒤에 오는 컬럼 이름 앞에는 테이블 이름을 table_name.column_name과 같은 형식으로 붙여줍니다. 만약 컬럼 이름이 여러 테이블에 걸쳐서 유일하다면 테이블 이름을 아래와 같이 생략할 수 있습니다.

-- `emp_no`는 두 테이블 모두 가지고 있기 때문에, 앞에 붙이는 테이블 이름을 생략할 수 없습니다.
SELECT first_name, salary, from_date, employees.emp_no
FROM salaries
JOIN employees ON employees.emp_no = salaries.emp_no;

INNER JOIN & OUTER JOIN

조인의 기준을 충족시키지 못하는 레코드들, 다시 말해서 일치하는 emp_no가 없거나, 어느 한 쪽의 emp_no 컬럼에 NULL이 저장되어 있는 경우는 어떻게 될까요? 위에서 사용한 SQL 명령을 그대로 쓰면, 조인의 기준을 충족시키지 못하는 레코드들은 아예 출력 결과에 포함이 되지 않습니다.

이와 같은 조인 방식을 INNER JOIN이라고 합니다. 사실 위 SQL 명령은 다음과 완전히 같은 의미입니다.

SELECT first_name, salary, from_date, emp.emp_no
FROM salaries AS sal
INNER JOIN employees AS emp ON emp.emp_no = sal.emp_no;
-- `INNER JOIN`과 `JOIN`은 같은 의미입니다.

그런데 가끔, 조인의 기준을 충족시키지 못하는 레코드들도 결과에 포함시켜야 하는 경우가 있습니다. 이러한 조인 방식을 OUTER JOIN이라고 합니다. 이 문서에서는 OUTER JOIN을 다루지 않으나, 가끔 필요한 경우가 있으니 아래의 문서를 참고해주세요.

세 개 이상의 테이블 조인하기

아래와 같이 JOIN 구문을 여러 번 써서, 세 개 이상의 테이블을 합치는 것도 가능합니다.

-- departments 테이블에는 부서 번호와 부서 이름이 저장되어 있습니다.
-- dept_emp 테이블에는 사원의 전보 내역이 기록되어 있습니다.
SELECT employees.first_name, dept_emp.from_date, departments.dept_name
FROM employees
JOIN dept_emp ON employees.emp_no = dept_emp.emp_no
JOIN departments ON departments.dept_no = dept_emp.dept_no;

2. 서브쿼리

SQL에는 쿼리 안에 쿼리를 중첩시켜서 중첩된 쿼리의 결과를 바깥쪽 쿼리에서 활용할 수 있는 기능이 있는데, 이렇게 내부에 중첩된 쿼리를 서브쿼리라고 부릅니다.

서브쿼리의 용법에는 여러가지가 있습니다.

단일 행 서브쿼리

단일 행 서브쿼리는 하나의 행을 반환하는 서브쿼리입니다. 단일 행 서브쿼리는 바깥쪽 쿼리의 WHERE 구문에서 비교를 위해 사용될 수 있습니다.

아래는 '1997년도 이전에 최고 연봉을 받은 사람의 이름'을 구하기 위한 쿼리입니다.

SELECT emp_no, first_name FROM employees
WHERE emp_no = (
  SELECT emp_no FROM salaries
  WHERE YEAR(from_date) <= '1997'
  ORDER BY salary DESC
  LIMIT 1
);

아래와 같이 튜플을 사용해서 여러 컬럼을 동시에 비교할 수도 있습니다.

-- 튜플을 사용하는 서브쿼리의 예를 보여드리기 위한 쿼리이며, 별 의미는 없습니다.
SELECT emp_no, first_name FROM employees
WHERE (emp_no, first_name) = (
  SELECT emp_no, first_name FROM employees
  LIMIT 1
);

다중 행 서브쿼리

다중 행 서브쿼리는 여러 개의 행을 반환하는 서브쿼리입니다. 다중 행 서브쿼리는 바깥쪽 쿼리의 WHERE 구문에서 IN 연산자와 함께 사용됩니다.

아래는 최고 연봉 15만 달러를 달성한 적이 있는 사람들의 이름을 가져오기 위한 쿼리입니다.

SELECT first_name FROM employees
WHERE emp_no IN (
  SELECT emp_no FROM salaries
  WHERE salary >= 150000
);

서브쿼리와 조인

서브쿼리로 짠 쿼리를 조인을 이용해서 짤 수 있는 경우도 있습니다. 위의 첫 번째 예제를 조인을 이용해 다시 작성하면 다음과 같이 됩니다.

-- '1997년도 이전에 최고 연봉을 받은 사람의 이름'을 구하기 위한 쿼리
SELECT first_name FROM employees
JOIN salaries ON salaries.emp_no = employees.emp_no
WHERE YEAR(salaries.from_date) <= '1997'
ORDER BY salary DESC
LIMIT 1;

다만 모든 서브쿼리를 조인으로 대체할 수 있는 것은 아닙니다. 다음과 같이 서브쿼리를 써야만 가능한 쿼리도 있습니다.

-- '1997년도 이전의 최고 연봉보다 더 많은 연봉을 받은 사람들의 이름'을 구하기 위한 쿼리
SELECT DISTINCT first_name FROM employees
JOIN salaries ON salaries.emp_no = employees.emp_no
WHERE salary > (
  SELECT salary FROM salaries
  WHERE YEAR(salaries.from_date) <= '1997'
  ORDER BY salary DESC
  LIMIT 1
);

같은 작업을 하는 쿼리를 조인으로 짤 수도 있고, 서브쿼리로도 짤 수 있다면 둘 중에 어떤 것을 써야 할까요?

IN 연산자에 들어가는 서브쿼리의 결과가 크면 연산 속도가 조인에 비해 느려집니다.

-- 0.0021 sec
SELECT first_name FROM employees
WHERE emp_no IN (
  SELECT emp_no FROM salaries
)
LIMIT 1000;
-- 0.00075 sec
SELECT first_name FROM employees
JOIN salaries ON salaries.emp_no = employees.emp_no
LIMIT 1000;

대개의 경우, IN 연산자에 들어가는 서브쿼리의 길이가 굉장히 긴 (데이터가 수십 만 행 이상) 경우에는 조인을, 짧은 경우에는 서브쿼리를 사용하는 것이 빠릅니다. 이것을 외우기 귀찮다면, 조인을 쓸 수 있는 상황에서는 조인을 쓰고, 아니라면 서브쿼리를 쓴다고 외워 두어도 무방합니다.

3. 인덱스

단어의 뜻을 찾고 싶을 때, 단어의 철자만 알고 있으면 영어 단어가 '사전순'으로 나열되어 있다는 사실을 이용해 단어를 빠르게 찾을 수 있었습니다. 그런데 만약에 영어 사전에 있는 단어들의 순서가 기준이 없이 뒤죽박죽 나열되어 있다면, 단어 하나의 뜻을 찾기 위해서 온 사전을 다 뒤져봐야 할 것입니다.

데이터베이스도 이와 비슷합니다. 검색을 효율적으로 하기 위해 특정 컬럼에 저장된 데이터를 미리 정렬시켜 놓은 것을 가지고 인덱스라고 부릅니다. 쿼리가 사용할 수 있는 인덱스가 없으면 쿼리는 전체 테이블을 순회하며 검색을 하게 되어 결과를 받을 때까지 시간이 오래 걸리지만, 쿼리가 활용할 수 있는 인덱스가 미리 만들어져있는 상태라면 쿼리 속도가 비약적으로 상승하게 됩니다.

인덱스 생성하기

검색을 빠르게 하기 위해 만드는 인덱스. CREATE INDEX 구문을 사용해서 인덱스를 생성할 수 있습니다.

CREATE INDEX ix_from_date ON salaries(from_date);

다중 컬럼 인덱스를 생성할 수도 있습니다. 같이 인덱스를 생성한다. 위의 인덱스 생성을 두 번 한 것과는 다른 결과를 초래합니다.

CREATE INDEX ix_from_date_to_date ON salaries(from_date, to_date);

인덱스 확인하기

앞에서 생성된 인덱스를 SHOW INDEX 구문으로 확인할 수 있습니다.

SHOW INDEX FROM salaries;

Primary Key, Foreign Key

기본 키와 외래 키에 대해서는 자동으로 인덱스가 생성되기 때문에, 별도의 인덱스를 만들어줄 필요가 없습니다.

Unique 인덱스

UNIQUE 제약조건을 걸면 자동적으로 인덱스가 생성된다.
특정 컬럼(혹은 다중 컬럼)의 값을 유일하게 만드는 제약 조건을 걸고 싶을 때 UNIQUE INDEX를 사용합니다.

CREATE UNIQUE INDEX ix_uniq_column ON table_name(col_name);

인덱스 제거하기

DROP INDEX 구문을 이용해 인덱스를 제거할 수 있습니다.

DROP INDEX index_name ON table_name;

인덱스의 설계

인덱스는 테이블과는 별도로 저장됩니다. 즉, 인덱스도 디스크의 용량을 차지합니다. 또한 미리 정렬을 시켜둬야 하는 인덱스의 성질때문에, 인덱스를 생성한 뒤에는 데이터의 추가나 수정이 느려집니다. 다시 말해서, 인덱스는 읽기 효율을 높이는 대신 쓰기 효율을 희생시킵니다.

테이블에 쓰기가 극단적으로 많이 일어나는 경우에는 인덱스 사용을 재고해보는 것이 좋습니다. 그렇지 않은 경우라면, WHERE 혹은 ORDER BY 구문에서 자주 사용되는 컬럼에 대해서는 인덱스를 걸어두는 것이 좋습니다. 또한 AND 연산으로 엮여서 자주 검색되는 컬럼들에 대해서는 다중 컬럼 인덱스를 생성하는 것이 좋습니다.

실행 계획

작성한 쿼리가 인덱스를 잘 활용하고 있는지 확인하려면 EXPLAIN 구문을 사용해서 실행 계획을 확인할 수 있습니다.

EXPLAIN 구문에 대한 자세한 설명은 공식 문서를 참고하세요.

4. 트랜잭션

트랜잭션의 필요성

다 성공시키거나 다 실패시키거나. 동시에 성공해야하는 쿼리들이 있을때 사용한다.

데이터베이스에서는 여러 쿼리에 걸쳐서 데이터를 안전하게 다루기 위해 트랜잭션이라는 기능을 사용합니다. 트랜잭션의 필요성을 말씀드리기 위해 간단히 예를 들어보겠습니다.

한 은행에서 계좌 정보를 관리하기 위해 데이터베이스를 사용하고 있습니다. 한 계좌에서 다른 계좌로 1000원을 옮기기 위해서는, 아래와 같이 데이터베이스에 두 개의 쿼리를 날려야 합니다.

-- 첫 번째 쿼리는 'from_account' 계좌에서 1000원을 인출합니다.
UPDATE account
SET balance = balance - 1000
WHERE account_id = 'from_account';
-- 두 번째 쿼리는 'to_account' 계좌에 1000원을 입금합니다.
UPDATE account
SET balance = balance + 1000
WHERE account_id = 'to_account'

두 쿼리가 모두 성공적으로 수행된다면, 우리가 처음에 의도했던 작업이 잘 완료됐다고 볼 수 있습니다. 하지만 계좌에 잔액이 부족하거나, 네트워크 오류 혹은 정전 등의 이유로 하나의 쿼리만 실행되고 나머지 하나는 실행이 되지 않는 상황이 발생할 수 있습니다. 즉, 인출은 됐는데 입금이 되지 않은 (혹은 그 반대의) 결과가 나옵니다.

언제 생길 지 알 수 없는 오류 때문에 데이터를 신뢰할 수 없다면, 은행과 같은 기업의 입장에서는 데이터베이스를 사용할 수 없을 것입니다.

이렇게 데이터베이스를 다루다 보면 "여러 쿼리에 걸친 데이터 조작의 신뢰성"을 확보할 필요성이 있습니다. 이를 위해 트랜잭션이라는 기능을 사용합니다. 트랜잭션을 사용하면, 여러 개의 쿼리 중 하나라도 실패했을 때 데이터베이스의 상태를 원상복귀 시킬 수 있습니다.

트랜잭션의 사용

START TRANSACTION 구문을 사용하면 현재 커넥션에 대해 트랜잭션이 시작됩니다.

START TRANSACTION;

트랜잭션을 시작한 다음에는 평소처럼 쿼리를 실행할 수 있습니다.

INSERT INTO employees (emp_no, birth_date, first_name, last_name, gender, hire_date)
VALUES (876543, '1980-03-05', 'Georgi', 'Jackson', 'M', '2017-09-11');

여러 번의 쿼리를 통해 필요한 작업을 완료한 뒤에는, COMMIT 구문으로 현재까지의 변경사항을 데이터베이스에 반영해주어야 합니다. COMMIT전에는 다른 커넥션에서 추가한 요소를 확인할 수 없습니다.

COMMIT;

만약, 쿼리를 하는 도중에 이제까지 트랜잭션 안에서 했던 모든 작업을 취소하고 싶은 경우에는 ROLLBACK 구문을 사용하면 됩니다.

ROLLBACK;

트랜잭션의 격리

커밋하기 전에는 트랜잭션 내에서 변경된 사항을 다른 커넥션에서 볼 수 없습니다. (단, READ COMMITTED 이상의 격리 수준인 경우에 한정) 이런 성질을 트랜잭션의 격리(isolation) 라고 부릅니다. MySQL은 다양한 격리 수준(isolation level)을 지원합니다. 자세한 사항은 공식문서이 글을 참고해주세요.

-- 위의 INSERT 쿼리를 실행하고 COMMIT을 하지 않은 경우에, 아무 결과도 안 뜹니다.
SELECT * FROM emplyees
WHERE emp_no = 876543;

5. 데이터 모델링

데이터 모델링

데이터 모델링은 현실 세계의 개념을 데이터베이스의 세계에서 다룰 수 있도록 재해석하는 작업을 말합니다.

데이터 모델링을 하기 위해서는 먼저 모델링의 대상이 되는 현실 세계를 잘 이해할 필요가 있습니다. 이를 위해 데이터 요구사항을 정리하고, 관계자와 인터뷰를 하는 등의 활동을 통해 현실 세계에서의 작업이 어떤 절차를 통해 이루어지는지를 자세히 파악해야 합니다.

이 문서에서는 널리 사용되는 모델링 방법인 개체-관계 모델을 다룰 것입니다. 개체-관계 모델을 통해 모델링을 할 때에는, 위에서 파악한 절차로부터 아래의 요소들을 이끌어 내게 됩니다.

  • 개체 (entity) : 절차에 관여하는 어떤 것(thing). 식별 가능한 사람, 장소, 사물, 사건 등
  • 속성 (attribute) : 개체가 가지는 성질
  • 관계 (relationship) : 개체 간에 가지는 관계

데이터베이스에는 각각 아래와 같은 형태로 저장될 수 있습니다.

  • 개체 - 테이블
  • 속성 - 개체를 나타내는 테이블의 컬럼
  • 관계 - 외래 키, 혹은 관계 테이블

관계의 차수(Cardinality)

두 개체 간 관계에서 각 개체의 참여자 수를 차수(cardinality)라고 부릅니다. 차수는 일반적으로 1:1, 1:N, M:N의 세 가지 형태로 분류합니다.

1:1 관계

데이터베이스에서는 1:1 관계에 있는 두 엔티티를 하나의 테이블에 저장하거나, 두 엔티티를 각각 다른 테이블에 저장하고 한 테이블의 기본 키를 다른 테이블에 대한 외래 키로 지정하는 등의 방법을 사용해서 1:1 관계를 표현합니다.

1:N 관계

한 사원의 연봉변동 기록과 같은 것 데이터베이스에서는 1:N 관계에 있는 두 엔티티를 각각 다른 테이블에 저장하고, N 측의 테이블에 다른 테이블에 대한 외래 키를 둬서 1:N 관계를 표현합니다.

M:N 관계

데이터베이스에서는 외래 키 하나만 가지고는 M:N 관계를 표현할 수 없으므로, 별도의 관계 테이블을 두고 관계 테이블에 두 개의 외래 키를 두는 방법으로 M:N 관계를 표현합니다.(외래 키에서는 화살표가 하나만 나가기 때문입니다.)

정규화 (Normalization)

중복이 많으면 실수가 잦아진다. 정규화란 데이터의 중복을 최소화할 수 있도록 데이터를 구조화하는 작업을 말합니다. 일반적으로, 정규화가 잘 된 데이터베이스는 작고 잘 조직된 여러 개의 테이블로 나누어집니다. 예를 들어서, 어떤 학원의 성적 관리 데이터베이스에서는 성적 데이터를 아래와 같이 저장할 수 있을 것입니다.

이름 과목 점수 전화번호
김이린 국어 88 010-1234-5678
김이린 영어 76 010-1234-5678
김이린 수학 65 010-1234-5678
이영빈 국어 60 010-8765-4321
이영빈 영어 91 010-8765-4321
이영빈 수학 85 010-8765-4321
구지용 국어 43 010-5555-5555
구지용 영어 74 010-5555-5555
구지용 수학 56 010-5555-5555

여기서 이름, 과목, 전화번호 등의 정보는 여러 행에 걸쳐 중복되어 있습니다. 이를 여러 테이블로 나누어서 아래와 같이 저장할 수도 있을 것입니다.

학생 테이블

학생 ID 이름 전화번호
1 김이린 010-1234-5678
2 이영빈 010-8765-4321
3 구지용 010-5555-5555

과목 테이블

과목 ID 과목
1 국어
2 영어
3 수학

점수 테이블

학생 ID 과목 ID 점수
1 1 88
1 2 76
1 3 65
2 1 60
2 2 91
2 3 85
3 1 43
3 2 74
3 3 56

정규화를 거치고 나서 테이블이 나뉘어져서 조금 더 복잡해졌지만, 중복되는 데이터가 없어져서 데이터를 관리하기도 쉬워지고 또 데이터가 차지하는 용량도 줄어들었습니다.

보통 데이터 모델링을 할 때에는 정규화 과정을 통해 중복을 없애주는 것이 좋습니다. 다만 성능상의 이유로 일부러 데이터를 중복시켜 저장하는 경우도 있는데, 이를 반정규화라고 합니다.

Entity Relationship Diagram

ERD(Entity Relationship Diagram)은 개체, 속성, 관계를 도표로 나타내는 방법으로, 데이터 모델링의 결과를 시각화하기 위해 널리 사용됩니다. MySQL Workbench을 통해 ERD를 그려볼 수 있습니다.

데이터 모델링 시작

서비스 동작에 관하여 마음대로 적어본 다음 사용자, 기능별로 분류한 후에 객체, 속성, 관계를 이끌어 낼 수 있다. 객체는 보통 명사인 요소인 사람, 사건 등이다.

예시 도서관

객체(테이블) : 회원, 도서, 회원카드, 대출내역, 반납내역, 예약내역, 연장내역, 사용기기내역, 로그인, 십진분류법
속성(컬럼)

  • 회원 : 아이디, 패스워드, 실명, 전화번호, 주소, 성별, 이메일, 생년월일, 사서여부, 회원번호 , 대출가능권수, 비밀번호 틀린 횟수
  • 도서 : 이름, 출판사, 출간일, 저자, ISBN, 번역자 , 분류(외래키), 도서식별자ID, 정가
  • 대출내역 : 대출일자, 회원ID, 도서식별자ID (회원ID와 도서ID는 대출을 중심으로 한 M대N 관계, 중간에 관계테이블이 하나 더 필요하다), 연장여부, 예약여부, 반납여부
  • 십진분류법 : 분류코드
  • 사용기기내역 : 기기코드, 접속일시
  • 로그인 : 일시, 회원아이디, 사용기기내역ID, 로그인 당시의 IP
    관계(외래 키, 혹은 관계 테이블)
  • 회원과 대출내역: 1대N
  • 도서와 대출내역 : 1대N
  • 분류법과 도서 : 1대N
  • 회원과 로그인 : 1대N
  • 사용기기내역과 로그인: 1대N

데이터 모델링 적용

-- MySQL Workbench Synchronization
-- Generated: 2017-09-12 16:46
-- Model: New Model
-- Version: 1.0
-- Project: Name of the project
-- Author: none

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';

CREATE SCHEMA IF NOT EXISTS `library` DEFAULT CHARACTER SET utf8 ;

CREATE TABLE IF NOT EXISTS `library`.`user` (
  `id` VARCHAR(20) NOT NULL,
  `password` VARCHAR(45) NOT NULL,
  `real_name` VARCHAR(45) NOT NULL,
  `phone_number` VARCHAR(45) NOT NULL,
  `address` VARCHAR(255) NOT NULL,
  `gender` ENUM('M', 'F') NOT NULL,
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

CREATE TABLE IF NOT EXISTS `library`.`book` (
  `id` INT(11) NOT NULL,
  `name` VARCHAR(255) NOT NULL,
  `company` VARCHAR(255) NOT NULL,
  `release_date` DATE NULL DEFAULT NULL,
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

CREATE TABLE IF NOT EXISTS `library`.`user_has_book` (
  `user_id` VARCHAR(20) NOT NULL,
  `book_id` INT(11) NOT NULL,
  `rent_date` DATE NOT NULL,
  `reserved` TINYINT(4) NOT NULL,
  `returned` TINYINT(4) NOT NULL,
  PRIMARY KEY (`user_id`, `book_id`),
  INDEX `fk_user_has_book_book1_idx` (`book_id` ASC),
  INDEX `fk_user_has_book_user_idx` (`user_id` ASC),
  CONSTRAINT `fk_user_has_book_user`
    FOREIGN KEY (`user_id`)
    REFERENCES `library`.`user` (`id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_user_has_book_book1`
    FOREIGN KEY (`book_id`)
    REFERENCES `library`.`book` (`id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

CREATE TABLE IF NOT EXISTS `library`.`login` (
  `id` INT(11) NOT NULL,
  `created_at` DATETIME NOT NULL,
  `user_id` VARCHAR(20) NOT NULL,
  PRIMARY KEY (`id`, `user_id`),
  INDEX `fk_login_user1_idx` (`user_id` ASC),
  CONSTRAINT `fk_login_user1`
    FOREIGN KEY (`user_id`)
    REFERENCES `library`.`user` (`id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;


SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

데이터 모델링 업데이트

-- MySQL Workbench Synchronization
-- Generated: 2017-09-12 16:51
-- Model: New Model
-- Version: 1.0
-- Project: Name of the project
-- Author: noe

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';

ALTER TABLE `library`.`user` 
ADD COLUMN `email` VARCHAR(255) NOT NULL AFTER `gender`;


SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;