2022. 11. 26. 15:45ㆍ수업/데이터베이스
SQL?
1974년, IBM연구소에서 System R이라는 관계형 DBMS를 연구할 때 관계형 대수와 관계형 해석을 기반으로 개발 된 데이터 언어
처음엔 SEQUEL이라 불렀지만 이후 SQL이라는 이름으로
--> 관계형 데이터베이스의 표준언어로써
--> 상용 DBMS인 IBM DB2, MS-SQL/Server, ORACLE, MySQL, PostgreSQL등과 같은 모든 주요 제품에서 채택되고 있다
SQL의 특징
--> 데이터 정의어(DDL), 데이터 조작어(DML), 데이터 제어어(DCL)등 모든 기능 제공
--> 터미널을 통한 대화형 질의어와 Java, Visual Basic, C, C++등과 같은 고급 프로그래밍 언어 내에 SQL이 삽입된 형태로 사용이 가능
테이블을 생성하고 정의하거나 새로운 애트리뷰트를 추가/제거할 수 있다. 또한 뷰 생성/제거, 인덱스 생성/제거를 할 수 있다.
CREATE TABLE 예)
CREATE TABLE 수강
( 학번 INTEGER NOT NULL,
과목코드 CHAR(5) NOT NULL,
정수 INTEGER,
성적 CHAR(2),
PRIMARY KEY(학번, 과목코드),
FOREIGN KEY(학번) REFERENCES 학생(학번)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY(과목코드) REFERENCES 과목(과목코드)
ON DELETE CASCADE
ON UPDATE CASCADE,
CHECK(성적 >= 0 AND 성적 <= 100));
학번, 과목코드, 점수, 성적 4개의 애트리뷰트로 이루어져있다
--> 각각의 데이터 타입은 INTEGER, CHAR(5), INTEGER, CHAR(2)
--> 기본키와 같이 어떤 애트리뷰트의 값으로 NULL을 허용하지 않으려면 NOT NULL을 명시
--> PRIMARY KEY는 기본키를 명세하는 것으로써 하나의 튜블을 유일하게 식별할 수 있도록 중복된 값이 존재할 수 없음
--> FOREIGN KEY는 외래키로서 참조 무결성을 유지하기 위한 것인데 참조하고 있는 행이 삭제(ON DELETE)되거나 변경(ON UPDATE)될 떄 취해야 할 동작을 명시
--> 이 동작에는 CASCADE, SET NULL, SET DEFAULT, NO ACTION의 4가지 옵션을 지정할 수 있음
--> CHECK 절에서는 한 애트리뷰트가 가질 수 있는 값들의 범위를 지정
데이터 검색(SELECT)
--> SQL 검색문의 기본적인 구조는
SELECT 열_리스트 FROM 테이블_리스트 WHERE 조건;
SELECT 학번, 성명 FROM 학생 WHERE 학과 = '컴퓨터';
명확성을 위해 그 소속 테이블 이름과 열 이름이 구두점(.)으로 연결된 형태를 취하는 것이 원칙이다
SELECT 학생.학번, 학생.성명 FROM 학생 WHERE 학과 = '컴퓨터';
데이터 검색
--> SQL과 이론적 관계형 데이터 모델과의 차이점
--> SQL은 질의 결과에서 똑같은 튜플들을 자동으로 제거하지 않는다(정렬 연산은 시간이 길 수 있으므로)
--> 그러므로, SELECT 결과 테이블은 이론상의 튜플 집합은 아니다. 이론적인 관계형 모델은 튜플의 유일성으로 인해 두 개의 똑같은 튜플들을 허용하지 않는다
--> 따라서 사용자가 SELECT 문에 DISTINCT를 명시적으로 작성할 때만 튜플의 중복을 제거한다(deafult는 ALL)
일반적인 형식
SELECT [ALL|DISTINCT] 열_리스트 FROM 테이블_리스트
[WHERE조건][GROUP BY 열_리스트[HAVING조건]] [ORDER BY 열_리스트 [ASC|DESC]];
데이터 열 전부검색?
SELECT * FROM 학생;
테이블 전체를 검색할 때는 열 이름 일일이 전부 나열필요 없이 *로
DISTINCT를 이용한 검색
: 학생 테이블에 어떤 학과들이 있는지 검색하라
SELECT DISTINCT 학과 FROM 학생;
조건검색
: 과목 테이블에서 학점이 3이고 학년이 4인 과목코드와 과목명을 검색하라
SELECT 과목코드, 과목명 FROM 과목 WHERE 학점=3 AND 학년=4;
WHERE절에 나오는 조건식에는 비교연산자 =, ≠, ≤, ≥등과 불리언 연산자 AND, OR, NOT을 사용할 수 있따
LIKE를 이용하는 검색
: 과목코드가 'CO'로 시작하는 과목의 과목코드와 과목명을 검색하라
SELECT 과목코드, 과목명 FROM 과목 WHERE 과목코드 LIKE 'CO%';
* 여기서 '%'는 패턴에 일치하는 모든 문자, '_'는 패턴에 일치하는 하나의 문자
NULL을 사용한 검색
: 강의 테이블에서 강의실이 NULL인 교수번호와 과목코드를 검색하라
SELECT 교수번호, 과목코드 FROM 강의 WHERE IS NULL;
범위검색
: 수강 테이블에서 점수가 80점부터 90점 사이의 열들을 검색하라
SELECT * FROM 수강 WHERE 점수 BETWEEN 80 AND 90;
ORDER BY를 이용한 검색
--> 사용자가 정렬된 형태로 결과를 보기 원할 경우 ORDER BY 키워드를 이용, 오름차순(ASC)이나 내림차순(DESC)으로 결과 레코드 정렬
: 수강 테이블에서 점수가 80점 이상인 학생의 학번과 성적을 검색하되, 학번은 오름차순으로, 같은 학번에 대해서 성적은 내림차순으로 검색하라
SELECT 학번, 성적 FROM 수강 WHERE 점수 >= 80 ORDER BY 학번 ASC, 성적 DESC;
여기서 첫 번째 속성 학번은 주(1차) 정렬이고, 두 번째 나오는 성적은 부(2차) 정렬 키이다.
계산된 열 검색(이름 변경)
--> 간단한 계싼식을 이용하여 데이터를 가져오는 경우 가끔 열 이름이 보기에 불편하거나 이름이 없는 경우가 있다. 이런 경우 별칭을 나타내는 AS를 사용할 수 있다
: 수강 테이블에서 과목코드 ‘CO123’에 수강한 학생의 학번과 점수에 2점을 더한 값을 검색하되, 학번의 열 이름은 “DB수강학번”, 점수의 열 이름은 “수정점수”, 그리고 그 사이에는 “최종점수 =”라는 텍스트 내용을 “내용”이라는 열 이름으로 디스플레이하라.
SELECT 학번 AS DB수강학번, '최종점수 =' AS 내용, 점수+2 AS 수정점수 FROM 수강 WHERE 과목코드='CO123';
조인검색
--> 두 개 이상의 테이블로부터 데이터를 조인하는 복수 테이블 질의
: 과목코드 CO423에 수강한 학생의 성명, 학과, 성적을 검색하라
SELECT 성명, 학과, 성적 FROM 학생, 수강 WHERE 학생.학번 = 수강.학번 AND 과목코드='CO423';
열 이름이 애매하게 되면 테이블 이름ㅇ르 붙인 한정된 열 이름ㅇ르 써야한다(ex. 학생.학번, 수강.학번)
--> 자연조인은 FROM절에 관련 테이블을 여러개 명세하게 된다
--> WHERE절에 어떤 조건으로 조인하느냐를 표현하는 학생.학번 = 수강.학번을 꼭 작성해야하는데, 이를 조인조건 또는 조인 프레디킷이라 한다
--> 조인 질의에서 열 이름을 단순화하기 위해 FROM 절에 별칭을 사용할 수 있다
--> 이는 셀프조인에서 명확한 열 참조를 가능케 한다
: 위 질의에 대한 별칭의 사용 예
SELECT 성명, 학과, 성적 FROM 학생 S, 수강 E WHERE S.학번 = E.학번 AND 과목코드 = 'CO423';
자기 자신의 테이블에 조인하는 검색(셀프조인)
: 같은 학과 학생들의 학번을 쌍으로 검색하라. 단 첫 번째 학번은 오름차순으로 정렬하라
SELECT S1.학번, S2.학번 FROM 학생 S1, 학생 S2 WHERE S1.학과 = S2.학과 ORDER BY S1.학번;
// ORDER BY에서 ASC는 DEFAULT임
집계 함수를 이용한 검색
: 학생 테이블에서 학생 수를 구하여라
SELECT COUNT(*) AS 학생수 FROM 학생;
: 과목 'CO123'에 대한 점수의 평균을 구하여라
SELECT AVG(점수) AS 평균 FROM 수강 WHERE 과목코드 = 'CO123';
GROUP BY를 이용한 검색
: 과목별 점수의 평균을 구하라
SELECT 과목코드, AVG(점수) AS 평균 FROM 수강 GROUP BY 과목코드;
HAVING을 사용한 검색
: 2명 이상 수강한 과목에 대한 점수의 평균을 구하라
SELECT 과목코드, AVG(점수) AS 평균 FROM 수강 GROUP BY 과목코드 HAVING COUNT(*) >= 2;
중첩 질의문을 사용한 검색
--> 어떤 값들을 검색한 후에 이를 다시 비교조건에서 사용하는 질의는 중첩 질의문을 사용하면 편리하게 표현할 수 있다
--> SELECT의 WHERE 절 안에 "SELECT - FROM - WHERE" 형태를 취한다
--> 부속 질의문이라고도 하고, 중첩 질의문은 소괄호 안에 표기한다
--> 중첩 질의문의 결과로는
--> 한 개의 단일 값이 반환되는 경우 / 한 개의 애트리뷰트로 이루어진 테이블이 반환되는 경우 / 여러 애트리뷰트들로 이루어진 테이블ㄹ이 반환되는 경우
----------------------------------------------------------------------------------------------------------------------
중첩 질의문에서 한 개의 단일 값이 반환되는 경우?
: 학생 김철수와 같은 학과에 속하는 학생의 성명을 검색하라
SELECT 성명 FROM 학생 WHERE 학과 =(SELECT 학과 FROM 학생 WHERE 성명='김철수');
--> 반환되는 결과는 '컴퓨터' 단일 값이다
--> 이 값과 같은 학생의 성명은 김철수, 이영자, 박용철이 최종검색된다
----------------------------------------------------------------------------------------------------------------------
중첩 질의문에서 한 개의 애트리뷰트로 이루어진 테이블이 반환되는 경우?
--> 이런 경우 외부 질의문의 WHERE 절에는 IN, ANY, ALL, EXISTS와 같은 연ㅅ나자가 사용된다
: 과목코드 'CO123' 을 수강한 학생의 성명을 검색하라
SELECT 성명 FROM 학생 WHERE 학번 IN (SELECT 학번 FORM 수강 WHERE 과목코드='CO123');
--> 반환되는 결과는 20181234, 20182587, 20184652이다
--> 한 애트리뷰트가 중첩 질의문 결과 값들의 집합에 속하는가를 테스트함
:과목코드 'CO123'에 수강하지 않은 학생의 성명을 검색하라
SELECT 성명 FROM 학생 WHERE 학번 NOT IN (SELECT 학번 FROM 수강 WHERE 과목코드 = 'CO123');
ALL, ANY, SOME과 같은 키워드를 WHERE 조건식에 사용될 수 있다
: 수강 테이블에서 학번 20181234의 점수보다 좋은 점수를 받은 학생의 학번과 과목코드를 검색하라
SELECT 학번, 과목코드 FROM 수강 WHERE 점수 > ALL(SELECT 점수 FROM 수강 WHERE 학번 = 20181234);
--> ALL은 왼쪽의 애트리뷰트 값이 오른쪽 값을 모두 만족해야 되는 AND의 의미이고, ANY는 하나 이상 만족해도 되는 OR의 의미로 생각할 수 있따
--> ALL과 ANY는 비교연산자(=, <>, <, <=, >, >=)와 결합하여 사용되는데, =ANY는 IN과 같은 의미이고, <>ALL은 NOT IN과 같은 의미이다
----------------------------------------------------------------------------------------------------------------------
중첩 질의문에서 여러 애트리뷰트들로 이루어진 테이블이 반환되는 경우?
--> 이런 경우 EXISTS 연산자를 사용하여 중첩 질의문의 결과가 빈 테이블인지를 검사한다
--> 중첩 질의문의 결과가 적어도 하나의 레코드가 들어 있으면 참이 되고 그렇지 않으면 거짓이 된다
: 과목'CO123'에 수강한 학생의 성명을 검색하라
SELECT 성명 FROM 학생 WHERE EXISTS (SELECT * FROM 수강 WHERE 수강.학번 = 학생.학번 AND 과목코드 = 'CO123');
--> "학생 테이블에서 성명을 검색하는데, 어떤 학생이냐 하면 과목 CO123을 수강하여 그 튜플이 존재하는 그런 학생이다" 라는 뜻
UNION이 관련된 검색
--> 두 SELECT문의 결과 테이블이 합병 가능해야 함
: '컴퓨터'과 학생이거나 'EE123'에 수강한 학생의 학번을 검색하라
SELECT 학번 FROM 학생 WHERE 학과='컴퓨터' UNION SELECT 학번 FROM 수강 WHERE 과목코드 = 'EE123';
--> 합집합과 같으므로 결과에서 중복되는 튜플들은 자동으로 제거되
----------------------------------------------------------------------------------------------------------------------
삽입(INSERT)
: 학번이 20188991, 성명이 '오영주', 학과 '컴퓨터', 전화번호 '010-1234-5678'인 학생을 삽입하라
INSERT INTO 학생(학번, 성명, 학과, 전화번호) VALUES(20188991, '오영주', '컴퓨터', '010-1234-5678');
또는
INSERT INTO 학생 VALUES(20188991, '오영주', '컴퓨터', '010-1234-5678');
한번에 여러 개의 튜플들을 삽입
: 학생 테이블에서 '컴퓨터'과 학생의 학번, 성명, 전화번호를 검색하여 컴퓨터 테이블에 삽입하라
INSERT INTO 컴퓨터 VALUES(학번, 성명, 전화번호) SELECT 학번, 성명, 전화번호 FROM 학생 WHERE 학과='컴퓨터';
삭제(DELETE)
: 학번 201811234인 학생을 삭제하라
DELETE FROM 학생 WHERE 학번=20181234;
복수 레코드 삭제
: 학생 테이블의 모든 행을 삭제하라
DELETE FROM 학생;
중첩 질의문을 이용한 삭제
: 과목 CO123의 점수가 85점 이상인 '컴퓨터' 과 학생을 수강 테이블에서 삭제하라
DELETE FROM 수강 WHERE 과목코드='CO123' AND 점수 >= 85 AND 수강.학번 IN (SELECT 학번 FROM 학생 WHERE 학과='컴퓨터');
갱신(UPDATE)
단일 레코드 변경
: 성명이 ' 유진호'인 학생의 학과를 '컴퓨터'로 변경하라
UPDATE 학생 SET 학과='컴퓨터' WHERE 성명='유진호';
복수 레코드 변경
: 과목 'CO123'의 점수를 2점씩 증가시켜라
UPDATE 수강 SET 점수=점수+2 WHERE 과목코드='CO123';
중첩 질의문을 이용한 변경
: '컴퓨터'과 학생의 점수를 2점씩 증가시켜라
UPDATE 수강 SET 점수=점수+2 WHERE 학번 IN (SELECT 학번 FROM 학생 WHERE 학과='컴퓨터');
SQL 뷰(View)
하나 또는 둘 이상의 기본 테이블로부터 유도되어 만들어지는 가상 테이블
--> 뷰의 생성
: 학생 테이블의 '컴퓨터'과 학생들로 구성된 학생뷰를 생성하라
CREATE VIEW 학생뷰(학번, 성명, 전화번호) AS SELECT 학번, 성명, 전화번호 FROM 학생 WHERE 학과='컴퓨터' WITH CHECK OPTION;
--> 뷰 생성은 "CREATE VIEW 뷰이름 AS SELECT 문" 형태로 만들어진다
--> WITH CHECK OPTION 절은 이 뷰에 대한 갱신이나 삽입 연산 시 뷰 정의 조건인 학과='컴퓨터'를 위반하면 실행이 거절된다는 것을 기술하는 것임
뷰의 생성
집계함수 사용 예
CREATE VIEW 학과별통계(학과, 학생수) AS SELECT 학과, COUNT(*) FROM 학생 GROUP BY 학과;
--> 두 번째 열은 집계함수로부터 유도되기 때문에 열의 이름을 상속받을 수 없음
두 개 이상의 테이블을 조인해서 뷰 정의
CREATE VIEW 우수학생(성명, 학과, 점수) AS SELECT 학생.성명, 학생.학과, 수강.점수
FROM 학생, 수강 WHERE 학생.학번 = 수강.학번 AND 수강.점수 >= 90;
--> 수강테이블에서 수강과목 점수가 90점 이상인 학생의 학번을 가지고 조인해서 우수학생을 보여주는 VIEW 생성
뷰의 제거
일반 형식?
DROP VIEW 뷰_이름 { RESTRICT | CASCADE };
RESTRICT
--> 이 뷰에 종속적인 뷰가 정의되지 않았을 때만 뷰를 삭제하도록 지시
CASCADE
--> 이 뷰 뿐만 아니라 이 뷰에 종속적인 다른 모든 뷰나 제약조건이 함께 제거
예)
DROP VIEW 학과별통계 CASCADE;
뷰의 장점
- 관련된 데이터만 이용가능
: 중요하고 적합한 데이터만으로 구성가능, 민감한 데이터에 대한 접근 금지(보안)가 가능
- 데이터베이스 복잡성 해소
: 복잡한 데이터베이스 구조를 숨길 수 있음, 복잡한 질의를 단순화 할 수 있음
- 권한 부여를 단순화 함
뷰의 단점
- 정의를 변경할 수 없음
- 삽입, 삭제, 갱신 연산에 제한이 많음
삽입 SQL
삽입 SQL은 이중모드의 특성을 가지고 있다
--> 터미널에서 대화식으로 사용할 수 있는 모든 SQL문은 응용 프로그램에서도 그대로 사용할 수 있는 특성을 뜻한다
삽입 SQL을 포함하는 으용프로그램의 특징
--> 명령문 앞에 EXEC SQL을 붙여 다른 명령문과 쉽게 구별 가능
--> 호스트 변수는 BEGIN DECLARE와 END DECLARE SECTION문을 사용하여 선언된다
--> SQL문에서 사용되는 호스트 변수는 콜른(;)을 앞에 붙임
--> 호스트 변수와 DB필드의 이름은 같아도 됨(호스트 변수는 콜른을 붙이기 때문)
--> 호스트 변수와 대응하는 SQL열의 데이터 타입은 일치해야
--> SQLSTATE라는 호스트 변수를 포함, SQL문이 실행되면 실행상태(성공, 실패, 오류)가 이 변수에 전달
응용 프로그램에 SQL 삽입 예
--> 호스트 변수는 사용하기 전에 반드시 SQL 선언부인 BEGIN DECLARE SECTION 속에서 선언되어야 함
EXEC SQL BEGIN DECLARE SECTION;
int sno;
char sname[21];
char dept[7];
char SQLSTATE[6];
EXEC SQL END DECLARE SECTION;
sno=20181234;
EXEC SQL SELECT 학번, 성명, 학과
INTO :sno, :sname, :sdept
FROM 학생
WHERE 학번 = sno;
IF(SQLSTATE='00000')
....;
ELSE ....;
단일 레코드 검색(snigleton SELECT) 예
#include <stdio.h>
int main(){
EXEC SQL BEGIN DECLARE SECTION;
int sno;
char sname[10];
char sdept[20];
EXEC SQL END DECLARE SECTION;
sno = 20181234;
EXEC SQL CONNECT TO academicDB USER kim;
EXEC SQL SELECT sno, sname, sdept
INTO :sno, :sname, :sdept
FROM student
WHERE sno = :sno;
printf("Student no: %s, name: %s, dept: %s", sno, sname, sdept);
return (0);
}
--> WHERE 절을 만족하는 레코드가 오직 하나만 있을 때 그 레코드와 학번, 성명, 학과 값이 호스트 변수 sno, sname, sdept에 저장된다
EXEC SQL SELECT 학번, 성명, 학과
INTO :sno, :sname, :sdept
FROM 학생
WHERE 학번=:sno;
단일 레코드 갱신
: 학번 20183654 학생이 수강한 과목의 점수를 호스트 변수 x값만큼 증가시켜라
EXEC SQL UPDATE 수강
SET 점수=점수+:x
WHERE 학번=20183654;
복수 레코드 검색
여러 개의 레코드가 검색되는 삽입 SQL문
--> 집합 속에 있는 레코드를 하나씩 접근하기 위해 커서(cursor)를 사용
--> DECLARE CURSOR문은 뒤에 나오는 SELECT문을 연결
--> SELECt 문은 CURSOR가 OPEN될 때 실행
--> 커서는 각 투플을 가리키는 포인터로 생각할 수 있다
--> CURSOR가 OPEN될 때 SELECT 문이 실행됨, 결과의 첫 번째 튜플 이전을 커서가 가리키게 된다
--> FETCH문은 커서를 다음 튜플로 이동하게 함
--> DO ... END문 내에서 루프를 수행
--> 더 이상 레코드가 없으면 루프가 종료되고, CLOSE 문에서 커서를 닫는다
EXEC SQL DECLARE cur CURSOR FOR /* 커서의 정의 */
SELECT 학번, 성명, 학과
FROM 학생
WHERE 학과 = :sdept;
EXEC SQL OPEN cur; /* SLEECT문 실행 */
DO
EXEC SQL FETCH cur INTO :sno, :sname, :sdept;
... /* 다음 레코드 채취 */
END;
EXEC SQL CLOSE cur; /* 커서 종료 */
커서를 이용하는 데이터 조작
--> 커서가 가리키는 특정 레코드 CURRENT OF cur을 변경시키거나 삭제하는 예
변경?
EXEC SQL UPDATE 학생
SET 학과 = :sdept
WHERE CURRENT OF cur;
삭제?
EXEC SQL DELETE
FROM 학생
WHERE CURRENT OF cur;
동적 SQL
온라인 애플리케이션의 수행과정은 다음과 같다
--> 터미널로부터 명령문을 접수
--> 입력된 명령문을 분석
--> 데이터베이스에 대한 적절한 SQL문으로 지시
--> 터미널에 메시지나 결과를 반환
이때, 입력 명령문이 상황에 따라 다른 경우 SQL문을 미리 만들어 두는 것이 불가능하게 된다
따라서 SQL문을 동적으로 작성할 수 있도록 지원하는 기능을 동적 SQL이라 한다
--> 가장 중요한 기본적인 명령은 PREPARE와 EXECUTE
--> PREPARE?
--> 주어진 SQL문을 예비 컴파일하여 목적코드를 생성하고 stSQL에 저장
--> EXECUTE?
--> 저장되어 있는 stSQL의 SQL목적 코드를 실행
varchar staticSQL[256];
staticSQL = "SELECT * FROM 수강 WHERE 과목코드 = 'CO123' AND 점수 >= 90";
EXEC PREPARE stSQL FROM :staticSQL;
EXEC SQL EXECUTE stSQL;
스트링으로 표현되는 staticSQL문에는 호스트 변수를 사용 못함. 그러나 물음표로 표현되는 매개변수는 사용 가능
Varchar dynamicSQL[256];
dynamicSQL = "SELECT * FROM 수강 WHERE 과목코드=? AND 점수=?";
...
EXEC SQL PREPARE dySQL FROM :dynamicSQL;
...
code='CO123',
score=90;
EXEC SQL EXECUTE dySQL USING :code, :score;
--> 위의 dynamicSQL에는 2개의 물음표 매개변수가 포함되어 있다
--> 물음표를 포함한 SQL문 실행을 위해 USING 절을 사용
--> 호스트 변수 :code와 :score의 값이 SELECT문의 물음표 순서에 대응
public static void main (String[] args) {
Connection conn = null;
String url = "jdbc:mysql://localhost/academicDB"; //academicDB 사용
String id = "root";
String pass = "root";
Statement stmt = null;
ResultSet rs = null;
String query = "select * from student";
try {
conn = DriverManager.getConnection(url, id, pass);
stmt = conn.createStatement();
rs = stmt.executeQuery(query);
while(rs.next()) {
System.out.println(rs.getInt(1) + ", " + rs.getString(2) );
}
rs.close();
stmt.close();
conn.close();
}
}
'수업 > 데이터베이스' 카테고리의 다른 글
데이터베이스 - 무결성, 보안, 회복 (0) | 2022.11.27 |
---|---|
데이터베이스 - 데이터베이스 설계 (1) | 2022.11.27 |
데이터베이스 - 데이터베이스 정규화 (1) | 2022.11.27 |
데이터베이스 - 데이터와 정보, 데이터베이스, DBMS (0) | 2022.11.24 |