스칼라 서브쿼리, 인라인 뷰 서브쿼리, 중첩 서브쿼리가 있는데 사용 위치에 따라서 부르는 이름이 다른 것뿐이다.
1. 스칼라 서브쿼리는 SELECT 절에 위치한다. SELECT 절은 원래 출력할 컬럼을 선택하기 위해서 존재한다.
정확히 하나의 값. 단일 컬럼의 단일행을 반환한다. 만약 서브쿼리가 여러 행을 반환하면, 어떤 값을 선택할지 불확실해지기 때문에 오류가 발생함.
2. 인라인뷰 서브쿼리는 FROM 절에 위치한다.
FROM절은 원래 어떤 테이블에서 데이터를 가져온다는 것을 명시하기 위해서 존재한다. 그래서 FROM 절에 오는 서브쿼리는 반드시 하나의 테이블로 리턴되어야 한다. VIEW와 사용하는 방식은 동일하다. VIEW는 데이터베이스에 정의되는 반면 서브쿼리는 서브쿼리가 실행될 때 정의된다는 차이점이 있다.
3. 중첩(NESTED) 서브쿼리는 WHERE이나 HAVING 절에 위치한다.
WHERE절은 원래 결과 집합을 특정한 조건을 가진 행으로 한정하기 위해서 사용한다. HAVING은 GROUP BY의 WHERE... 그래서 WHRER 절 뒤에 오는 중첩 서브쿼리는 메인 쿼리 컬럼을 참조한다. 다중컬럼 또는 다중행을 반환할 수 있다.
#내부 조인은 일반 조인 혹은 동등 조인이라고 하기도 한다. 칼럼명을 기준으로, 결합 조건을 만족하는 데이터만 출력한다.
1. WHERE 절에서 JOIN하기
SELECT~FROM TABLE1, TABLE2
WHERE TABLE1.속성 = TABLE2.속성
(세 개의 다른 테이블)
SELECT TABLE1.속성1, TABEL2. 속성2FROM TABLE1, TABLE2, TABLE3
WHERE TABLE1.속성1= TABLE2.속성1AND TABLE2.속성2= TABLE3. 속성2
2. FROM 절에서 JOIN하기
SELCT ~FROM TABLE1 JOIN TABLE2
ON TABLE1.속성 = TABLE2.속성;
(세 개의 다른 테이블)
FROM TABLE1
JOIN TABLE2
ON TABLE1.속성 = TABLE2.속성
JOIN TABLE3
ON TABLE2.속성 = TABLE3.속성;
이렇게 명시적으로 조인을 하겠다고 표시하고,
ON 뒤로 어떤 속성을 결합 조건으로 할건지 정하면 된다.
JOIN이라고만 쓰면 내부 조인으로 자연스럽게 인식한다.
OUTER JOIN
#외부 조인은 한쪽 테이블에 데이터가 없어도, 기준이 되는 테이블에 데이터가 있다면 모두 출력한다.
LEFT JOIN, RIGHT JOIN, FULL OUTETR JOIN이 있다.
MySQL에서는 full outer join을 구현하는 명령어가 없기 때문에 LEFT JOIN과 RIGHT JOIN의 합집합(UNION)을 이용해서 구하면 된다.
LEFT JOIN은 왼쪽 테이블의 데이터를 모두 출력하고, 오른쪽 테이블의 데이터는 있으면 출력, 없으면 NULL로 표시한다.
RIGHT JOIN은 반대로 오른쪽 테이블의 데이터를 모두 출력하고, 왼쪽은 있으면 출력, 없으면 NULL로 표시한다.
<FULL OUTER JOIN 예시>
SELECT residents.name AS resident_name, animals.name AS animal_name
FROM residents LEFTJOIN animals
ON residents.id = animals.owner_id
UNIONSELECT residents.name AS resident_name, animals.name AS animal_name
FROM residents RIGHTJOIN animals
on residents.pet_id = animals.id;
결과는 이렇게 나온다.
1. 거주자 ID랑 동물 주인 ID가 같을 때 거주자 ID만 있으면 거주자 이름이랑 동물 이름을 출력한 것. 만약 residents.id가 2인데, animals.owner_id가 2인 행이 없다면? 동물 이름 대신 NULL이 반환된다.여기서 기준은 residents 테이블이기 때문이다.
2. 거주자가 소유한 동물 ID랑 동물 ID가 같을 때 거주자의 이름과 동물의 이름을 출력한 것. 여기서 기준은 animals 테이블이므로, residents 테이블에서 residents.pet_id 중 일치하는 게 없으면 거주자 이름은 거주자 이름은 NULL로 나온다.
3. 두 개를 UNION으로 합친다.
UNION을 사용하기 위해서는 모든 컬럼의 개수와 순서, 데이터 형식이 동일해야 한다. UNION은 중복값을 제거하고, UNION ALL은 중복값을 그대로 보여준다. 중복 처리하는 과정이 없으므로 UNION ALL이 속도는 더 빠르다.
결국 거주자 이름과 동물 이름이 모두 있는 경우, 거주자 이름만 있는 경우, 동물 이름만 있는 경우를 모두 출력할 수 있게 된다.
SELECT BOOK_ID, DATE_FORMAT(PUBLISHED_DATE, '%y-%m-%d') AS PUBLISHED_DATE
FROM BOOK
WHERE CATEGORY = '인문' AND YEAR(PUBLISHED_DATE) = 2021
ORDER BY PUBLISHED_DATE;
SELECT USER_ID, PRODUCT_ID
FROM ONLINE_SALE
GROUP BY 1, 2
HAVING COUNT(PRODUCT_ID) > 1
ORDER BY USER_ID ASC, PRODUCT_ID DESC;
#HAVING 절은 WHERE 절과 비슷하지만
그룹 전체에만(즉, 그룹을 나타내는 결과 집합의 행에) 적용되는 반면
WHERE 절은 개별 행에 적용됩니다.
데이터 모델링 : 현실세계(사물/사건에 관한 양상)를 추상화, 단순화, 명확화하기 위해 일정한 표기법에 의해 표현하는 기법. (현실세계를 추상화, 단순화, 명확화한 것이=데이터 모델)
-추상화: 현실세계를 일정한 형식에 맞추어 표현 -단순화: 현실세계를 약속된 규약에 의해 제한된 표기법이나 언어로 표현 -명확화: 누구나 이해하기 쉽도록 애매모호함이 없고 정확하게 기술
# 모델링의 세 가지 관점
-데이터 관점: 업무가 어떤 데이터와 관련이 있는지? -프로세스 관점: 업무가 무엇을 해야하는지? -상관관점: 일의 방법에 따라서 데이터가 어떻게 영향을 받는지?
# 데이터 모델이 제공하는 기능:
-시스템을 현재 또는 원하는 모습으로 가시화 -시스템의 구조와 행동을 명세화 -시스템을 구축하는 구조화된 틀 제공 -시스템을 구축하는 과정에서 결정한 것을 문서화 -다양한 영역에 집중하기 위해 다른 영역의 세부 사항은 숨기는 다양한 관점 제공 -특정 목표에 따라 구체화된 상세 수준의 표현 방법을 제공
# 데이터 모델링의 중요성
-파급효과 : 기초적인 작업으로 파급 효과가 크기 때문에 시스템 구축 작업 중에서 다른 어떤 설계 과정보다 데이터 설계가 중요.
-복잡한 정보 요구사항의 간결한 표현: 말과 글보다 훨씬 간결
-데이터 품질: 데이터 중복(고객 정보 등의 정보), 비유연성, 비일관성이 발생할 수 있다.
# 모델링의 3단계
현실세계에서 개념적 구조로 만드는 개념데이터 모델링 :
추상화 수준 높음(상세하지 않음), 업무 중심적이고 가장 중요하고 포괄적인 수준의 모델링 진행.
(전사적 데이터 모델링, EA수립 시 많이 사용)
개념적 구조에서 논리적 구조로 만드는 논리데이터 모델링:
구축하고자 하는 업무의 키, 속성, 관계등을 정확하게 표현한다. 한번 구축해놓으면 재사용성이 높음
논리에서 물리로 물리데이터 모델링:
실제로 데이터베이스에 이식할 수 있도록 성능, 저장 등 물리적 성격 고려하여 설계한다.
# 프로젝트 생명 주기는 정보전략계획- 분석- 설계- 개발- 테스트- 전환/이행 단계이다.
이 중 개념적 데이터 모델링: 정보전략계획, 분석단계에서 일어나고 논리적 데이터 모델링: 분석단계, 물리적 데이터 모델링: 설계 단계에서 일어난다.
# 데이터 독립의 필요성
오늘 날씨와 내일 날씨가 다르니까, 이 세상의 데이터는 끊임없이 증가한다. --> 데이터 중복성 증가--> 데이터 복잡도 증가
사용자 요구사항도 끊임없이 발생함. - 유지보수 비용 증가 - 요구사항 대응 저하
데이터 독립성 개념은 사용자가 보는 화면과 데이터베이스 간의 독립성을 유지하기 위해서 출현하였다.
# 데이터 독립성을 유지하기 위한 3단계 구조
외부 단계와 개념적 단계의 독립성은 논리적 데이터 독립성으로 유지된다. 내부적단계와 개념적 단계/외부 단계의 독립성은 물리적 데이터 독립성으로 유지된다.
외부 스키마(사용자 관점) 개념 스키마(연결고리 , 통합관점) 내부 스키마(물리적 관점)
논리적 독립성: 개념스키마가 변경되어도 내부스키마는 그대로임. 물리적 독립성:내부스키마가 변경되어도 개념, 외부 스키마는 그대로임.
#
데이터베이스 3단계 구조에서의 사상(매핑)
외부적, 개념적 사상: 논리적 사상. 외부적 뷰와 개념적 뷰의 상호 호환성을 정의함. 개념적, 내부적 사상: 물리적 사상. 개념적 뷰와 저장된 데이터베이스의 상호 관련성을 정의함.
*** 중간에 매핑 과정이 있기 때문에 만약 내부 데이터베이스가 바뀌어도 사상만 바꾸면 개념 스키마가 그대로 남아있는 것. 반대로 사용자가 접근하는 형식에 따라 다른 타입의 필드를 가질 수 있는데, 사상만 바꾸면 개념 스키마 즉 개념적 뷰의 필드 타입에는 변화가 없음.
#
데이터 모델링 용어
* 어떤 것? 복수/집합 개념은 엔터티, 개별/단수 개념은 인스턴스 ex)고객 = 엔터티, 홍길동= 인스턴스
*어떤 것 간의 연관 고객과 고객이 주문한 상품이 페어링 되는 관계..
* 어떤 것의 성격 : 속성- ex)주문금액, 주문 일시, 배송지.. : 속성값- ex) 2만 9천원, 3월 1일...
# 데이터 모델링 작업 순서
1. 엔터티 그리기 2. 엔터티 배치 3. 엔터티 간 관계 설정 4. 관계명 기술 5. 관계의 참여도 기술 6. 관계의 필수 여부 기술
IE 표기법과 BARKER 표기법이 있음.
#
데이터 모델링의 이해관계자 프로젝트 개발자(제일 중요), 현업업무 전문가, 전문 모델러, DBA 관계자 모두가 적어도 모델을 정확하게 해석할 수 있을 정도의 데이터 모델링 지식을 갖춰야 함.
# 좋은 데이터 모델?
완전성
중복 배제
업무 규칙-데이터 모델 분석만으로도 비즈니스 로직이 이해가 되어야 함
데이터 재사용
의사소통
통합성-동일한 데이터는 유일하게 정의해서 다른 영역에서 참조해야 함(한 정보는 한 곳에서만 관리함)
USE market_db; SELECT * FROM member; SHOW INDEX FROM member; SHOW TABLE STATUS LIKE 'member'; (인덱스 하나도 없는 상태 확인.)
<인덱스 생성>
1) 인덱스 생성
CREATE INDEX idx_member_addr ON member (addr);
SHOW INDEX FROM member;
SHOW TABLE STATUS LIKE 'member'; 인덱스 만들고 다시 실행했는데 인덱스 정보가 안 보임.
----> ANALYZE TABLE member; 이걸 해야 인덱스가 적용된다.
SHOW TABLE STATUS LIKE 'member'; 인덱스 생긴 거 확인 가능.
2) 유니크 인덱스 생성
CREATE UNIQUE INDEX idx_member_mem_name ON member (mem_name); 유니크 인덱스는 중복값이 없어야 한다.
SHOW INDEX FROM member;
INSERT INTO member VALUES('MOO', '마마무', 2, '태국', '001', '12341234', 155, '2020.10.10'); ANALYZE TABLE member; 멤버 이름이 같은 데이터_ 유니크 인덱스가 있어서 삽입 불가능.
3) 인덱스 사용해서 검색
SELECT mem_id, mem_name, addr FROM member WHERE mem_name = '에이핑크'; 인덱스 사용해서 검색했음. 인덱스는 where절, 열 이름이 나와야 쓸 수 있음.
4) 인덱스를 사용하려면, WHERE 절에 뒤에 쓰는 열에 아무 가공을 하면 안된다.
CREATE INDEX idx_member_mem_number ON member (mem_number); ANALYZE TABLE member;
SELECT mem_name, mem_number FROM member WHERE mem_number >= 7; 인덱스가 있든 없든 결과는 동일하게 나옴. execution plan 에 들어가서 결과 나오는 과정을 보면 index range scan --> query block #1 이런 식의 과정이 확인 됨.
SELECT mem_name, mem_number FROM member WHERE mem_number >= 1; 결과값으로 테이블의 모든 행이 출력됨. 이 경우 execution plan 보면 인덱스 사용하지 않음. 어떤 방식이 더 효과적인지 자동으로 계산을 하는 것.
SELECT mem_name, mem_number FROM member WHERE mem_number*2 >= 14; 멤버 수가 7 이상인 거랑 똑같은 결과가 나오지만, 인덱스를 사용하지 않음.
SELECT mem_name, mem_number FROM member WHERE mem_number >= 14/2; 이렇게 2를 뒤로 넘기면 인덱스 사용한다. --> WHERE절에 가공을 하지 않으면 더 효율적으로 정보를 불러올 수 있다.
5) 인덱스 제거
DROP INDEX idx_member_mem_name ON member; DROP INDEX idx_member_addr ON member; DROP INDEX idx_member_mem_number ON member;
ALTER TABLE member DROP PRIMARY KEY; 이건 PK, FK로 지금 buy 테이블과 연결되어 있기 때문에 안 지워짐.
SELECT table_name, constraint_name FROM information_schema.referential_constraints WHERE constraint_schema = 'market_db' ; market_db의 외래키 이름 확인하는 구문.
USE market_db; CREATE TABLE cluster ( mem_id CHAR(8), mem_name VARCHAR(10) ); INSERT INTO cluster VALUES('TWC', '트와이스'); INSERT INTO cluster VALUES('BLK', '블랙핑크'); INSERT INTO cluster VALUES('WMN', '여자친구'); INSERT INTO cluster VALUES('OMY', '오마이걸'); INSERT INTO cluster VALUES('GRL', '소녀시대'); INSERT INTO cluster VALUES('ITZ', '잇지'); INSERT INTO cluster VALUES('RED', '레드벨벳'); INSERT INTO cluster VALUES('APN', '에이핑크'); INSERT INTO cluster VALUES('SPC', '우주소녀'); INSERT INTO cluster VALUES('MMU', '마마무');
SELECT * FROM cluster; 입력한 순서대로, 인덱스 없는 상태.
ALTER TABLE cluster ADD CONSTRAINT PRIMARY KEY (mem_id);
SELECT *FROM cluster; 아이디를 기본키로 지정---> 클러스터형 인덱스가 만들어져, 아이디의 알파벳 순으로 정렬 됨.
<보조 인덱스 구성하기 연습>
CREATE TABLE second ( mem_id CHAR(8), mem_name VARCHAR(10) );
INSERT INTO second VALUES('TWC', '트와이스'); INSERT INTO second VALUES('BLK', '블랙핑크'); INSERT INTO second VALUES('WMN', '여자친구'); INSERT INTO second VALUES('OMY', '오마이걸'); INSERT INTO second VALUES('GRL', '소녀시대'); INSERT INTO second VALUES('ITZ', '잇지'); INSERT INTO second VALUES('RED', '레드벨벳'); INSERT INTO second VALUES('APN', '에이핑크'); INSERT INTO second VALUES('SPC', '우주소녀'); INSERT INTO second VALUES('MMU', '마마무'); SELECT * FROM second;
인덱스 없이 테이블만 만든 것. 입력 순서대로 정렬
ALTER TABLE second ADD CONSTRAINT UNIQUE (mem_id);
SELECT * FROM second;
유니크 키 지정으로 보조 인덱스를 생성해도, 데이터의 순서 바뀌지 않음. 내부적으로 인덱스(찾아보기 기능)이 생겼고, 페이지 번호와 위치가 저장된 것이다.
CREATE TABLE table1 ( col1 INT PRIMARY KEY, col2 INT, col3 INT ); SHOW INDEX FROM table1;
키네임에 프라이머리 있으면 자동으로 클러스터형 인덱스가 만들어진다. 그 열에 입력된 값 기준으로 사전식 정렬.
CREATE TABLE table2 ( col1 INT PRIMARY KEY, col2 INT UNIQUE, col3 INT UNIQUE ); SHOW INDEX FROM table2;
여기서 프라이머리 키인 컬럼 1은 클러스터 인덱스, 컬럼2, 컬럼3는 보조 인덱스로 봄.
둘 다 자동으로 생성되는 인덱스이다. 유니크 조건이 여러 열에 붙을 수 있듯, 보조 키도 여러개 만들 수 있음. 프라이머리키와 클러스터형 인덱스는 테이블 당 하나만 만들 수 있다.
USE market_db; DROP TABLE IF EXISTS buy, member; CREATE TABLE member ( mem_id CHAR(8), mem_name VARCHAR(10), mem_number INT, addr CHAR(2) );
INSERT INTO member VALUES('TWC', '트와이스', 9, '서울'); INSERT INTO member VALUES('BLK', '블랙핑크', 4, '경남'); INSERT INTO member VALUES('WMN', '여자친구', 6, '경기'); INSERT INTO member VALUES('OMY', '오마이걸', 7, '서울');
SELECT * FROM member;
새로운 멤버 테이블 만들었다. 인덱스 없이(PK 지정 안 하고) 무작위로 입력하면, 입력한 순서 그대로 테이블에서 보인다.
ALTER TABLE member ADD CONSTRAINT PRIMARY KEY(mem_id); SELECT *FROM member;
프라이머리 키 다시 지정한 후 불러오면 멤버 아이디가 알파벳 순으로 정렬되는 걸 확인할 수 있다. 클러스터형 인덱스가 자동으로 생성된 것.
ALTER TABLE member DROP PRIMARY KEY ; 다시 기본 키를 제거하고 ALTER TABLE member ADD CONSTRAINT PRIMARY KEY(mem_name); 새로운 기본 키를 멤버 이름 열에 부여하면 SELECT * FROM member; 멤버 이름 순으로 다시 정렬되는 걸 확인할 수 있다.
INSERT INTO member VALUES ('GRL', '소녀시대', 8, '서울'); 이 상태에서 소녀시대 행을 새로 입력하면 SELECT * FROM member; 블핑, 여자친구 사이에 삽입이 되었다. ㄱㄴㄷ 순으로.
보조인덱스는 UNIQUE, 고유 키로 지정하면 생성된다. 클러스터 형 인덱스를 사전 만들기에 비유한다면, 보조 인덱스는 용어 사전이 아니라 줄글로 된 책의 인덱스를 만드는 것과 비슷하다. 책의 내용을 다시 정렬하는 것이 아니라, 맨 뒤에 <찾아보기>를 추가하는 방식이다.
USE market_db; DROP TABLE IF EXISTS member; CREATE TABLE member ( mem_id CHAR(8), mem_name VARCHAR(10), mem_number INT, addr CHAR(2) );
INSERT INTO member VALUES('TWC', '트와이스', 9, '서울'); INSERT INTO member VALUES('BLK', '블랙핑크', 4, '경남'); INSERT INTO member VALUES('WMN', '여자친구', 6, '경기'); INSERT INTO member VALUES('OMY', '오마이걸', 7, '서울');
SELECT * FROM member;
인덱스 없이 새로운 테이블 만들기.
ALTER TABLE member ADD CONSTRAINT UNIQUE (mem_id); SELECT * FROM member;
유니크 키를 추가한 뒤 멤버 테이블을 조회해도, 내용, 순서가 바뀌지 않았다. 보조 인덱스가 생성된 것이다.
ALTER TABLE member ADD CONSTRAINT UNIQUE (mem_name); SELECT * FROM member;
두번째 유니크 키를 지정함. 책 뒤에 찾아보기는 여러 버전으로 만들 수 있으므로.
INSERT INTO member VALUES ('GRL', '소녀시대' ,8, '서울'); SELECT * FROM member;
USE market_db; SELECT mem_id, mem_name, addr FROM member;
이렇게 써서 테이블을 조회하나
USE market_db; CREATE VIEW v_member AS SELECT mem_id, mem_name, addr FROM member;
SELECT * FROM v_member;
이렇게 써서 뷰를 조회하나 똑같은 결과로 보인다.
SELECT mem_name, addr FROM v_member WHERE addr IN ('서울', '경기');
SELECT B. mem_id, M.mem_name, B.prod_name, M.addr
CREATE VIEW v_memberbuy AS SELECT B.mem_id, M.mem_name, B.prod_name, M.addr, CONCAT(M.phone1, M.phone2) '연락처' FROM buy B INNER JOIN member M ON B.mem_id = M.mem_id;
복잡한 조건의 쿼리문을 뷰로 생성해 놓으면 다음에 불러오기 편하다.
SELECT * FROM v_memberbuy WHERE mem_name = '블랙핑크';
블랙핑크에 관한 특정 정보(아이디, 이름, 구매물품, 주소, 전화번호)를 보고 싶을 때, 보안에도 유용.
v_memberbuy라는 뷰에서 불러오면 블핑의 멤버 수, 평균 키 같은 정보는 나오지 않는다.
USE market_db; CREATE VIEW v_viewtest1 AS SELECT B.mem_id 'Member ID', M.mem_name AS 'Member Name', B.prod_name "product Name", CONCAT(M.phone1, M.phone2) AS "Office Phone" FROM buy B INNER JOIN member M ON B.mem_id = M.mem_id;
새로운 열 이름으로 정보를 불러오고 싶을 때는 기존의 작은 따옴표가 아니라 키보드 1 버튼 왼쪽에 있는 거?로 이름을 묶어주면 된다.
ALTER VIEW v_viewtest1 AS SELECT B.mem_id '회원 아이디', M.mem_name AS '회원 이름', B.prod_name "제품 이름", CONCAT(M.phone1, M.phone2) AS "연락처" FROM buy B INNER JOIN member M ON B.mem_id = M.mem_id;
SELECT DISTINCT `회원 아이디`, `회원 이름` FROM v_viewtest1;
ALTER VIEW 기능을 이용하여 이렇게 새로운 이름을 한글로 지정할 수도 있다.
DROP VIEW v_viewtest1;
USE market_db; CREATE OR REPLACE VIEW v_viewtest2 AS SELECT mem_id, mem_name, addr FROM member;
DESCRIBE v_viewtest2; DESCRIBE member;
describe ~ 누르면 각 열의 이름, 타입, 널 값, 키 값 등 확인 가능함.
SHOW CREATE VIEW v_viewtest2;
뷰를 만들었던 쿼리문 확인하고 싶을 때 이렇게 입력하면 된다.
UPDATE v_member SET addr = '부산' WHERE mem_id= 'BLK' ; SELECT * FROM v_member;
뷰를 통한 데이터 수정이 잘 되었다.
INSERT INTO v_member(mem_id, mem_name, addr) VALUES('BTS', '방탄소년단', '경기') ;
이 경우에는 수정이 되지 않았는데, 기존 테이블에서 not null값인 멤버 수 등의 정보가 빠졌기 때문에 얘네만 입력할 수는 없는 것. 뷰를 통해서 데이터는 입력이 될 수도 있고 안 될 수도 있고, 이렇게 인서트 하는 것은 바람직하지 않음.
CREATE VIEW v_height167 AS SELECT * FROM member WHERE height >= 167 ;
SELECT * FROM v_height167;
키가 167 이상인 멤버만 보여주는 뷰
ALTER VIEW v_height167 AS SELECT * FROM member WHERE height >= 167 WITH CHECK OPTION ;
CREATE TABLE buy ( num INT AUTO_INCREMENT NOT NULL PRIMARY KEY, mem_id CHAR(8) NOT NULL, prod_name CHAR(6) NOT NULL, FOREIGN KEY(mem_id) REFERENCES member(mem_id) );
DROP TABLE IF EXISTS buy; CREATE TABLE buy ( num INT AUTO_INCREMENT NOT NULL PRIMARY KEY, mem_id CHAR(8) NOT NULL, prod_name CHAR(6) NOT NULL ); ALTER TABLE buy ADD CONSTRAINT FOREIGN KEY(mem_id) REFERENCES member(mem_id);