CMU CS DB Andy Pavlo 교수님의 "Intro. to Database Systems" 강의를 필기한 내용입니다.
본 글의 그림들은 별도의 명시가 없는 한 (1) 해당 강의 자료에서 가져오거나 (2) 주인장이 직접 만들었습니다.
다소 잘못된 내용과 구어적 표현 이 포함되어 있을 수 있습니다.
수업에 사용된 SQL
History
- 저번 시간에도 말한것 처럼 Relational Model 은 어떠한 Programming language 도 제시하지 않았고, 그냥 수학적인 모델일 뿐이었다.
- 그러다가 IBM 이 1972 년도에 이 Relational Model 을 도입한 Square 라는 언어를 개발했는데,
- 근데 이놈은 문법이 너무 기괴해서 사망띠
- 그리고 그 이후에 IBM 은 Structured English Query Language 를 줄여 SEQUEL 을 제시했고, 이것이 나중에 SQL 로 발전하게 된다.
- 추가적으로 UC Berkely 에서 Ingres 라는 DBMS 와 SQL 을 제시했는데, 이것이 발전한게 그 유명한 Postgres 이다. (Post-ingres)
- 뭐 그리고 IBM 에서는 이후로 SQL 에 기반한 System-38, SQL-DL, DB2 같은 여러 상업 DBMS 를 출시했다고 한다.
- SQL 은 1986년에 미국 표준 (ANSI) 로 등록되고, 1987년에는 국제 표준 (ISO) 으로 등록되었으며, 2023년까지도 지속적으로 버전업데이트를 하고 있다고 한다.
- 근데 이건 표준일 뿐이고 각 DBMS 에서 이 표준의 특정 기능을 지원하는지의 여부는 각기 다르다.
- 거의 모든 DBMS 에 구현된 “최소한의” 기능은 SQL-92 표준이라고 한다.
SQL
- 대략 네 파트로 나누어 볼 수 있다고 한다.
- Data Management Language (DML): 데이터 다루기 (Insert, select 등등)
- Data Definition Language (DDL): Scheme 정의 (Create table, view 등)
- Data Control Language (DCL): 얘는 ACL 같은 거다 - 뭐 접근제어 (보안) 등
- 그리고 그 외에 Transaction 과 같은 내용들..
- 그리고 앤디씨가 여기서 짚고 넘어간 점은 SQL 은 Bag 를 다루고 있고, 따라서 중복이 가능하다는 점이다.
- 반면에 Relational algebra 에서는 안된다. (Set 을 다룸)
- 여담으로 SQL 이라는 표준이 있지만 각 DBMS 에서는 이것을 잘 따르지 않고 지맘대로 한다고 한다.
- 특히 (최근에는 좀 나아졌지만) MySQL 은 진짜 청개구리라네
- SQL 은 그 자체로 최대한 많은 것을 제공해 주는 식으로 발전해 왔다고 한다.
- 이게 뭔소리냐면, 가령 어떤 domain 을 변경하는 것은
SELECT
로 데이터를 가져와서 변경한 뒤 다시INSERT
로 넣는 방식도 가능하다. - 하지만 Application code 로 이런 것을 하게 두기 보다는 이런 작업을 SQL 표준으로 만들어서 그냥
UPDATE
keyword 만 사용하면 되게끔 - 즉, DBMS 에서 최대한 많은 것을 해주려는 일종의 맥시멀리즘의 성향을 띈다고 한다.
- 이게 뭔소리냐면, 가령 어떤 domain 을 변경하는 것은
- 그럼 이제 SQL 표준의 여러 기능들을 살펴보자.
Aggregation function
- Aggregation function 은 Bag of tuple 를 input 으로 받아, 하나의 "값" 을 output 으로 주는 함수이다.
- 말만 들으면 복잡한데 아래 예시 보면 별거 아니다.
AVG(col)
: 해당 column 에 대한 평균MIN(col)
: 해당 column 에 대한 최소값MAX(col)
: 해당 column 에 대한 최대값SUM(col)
: 해당 column 에 대한 합계COUNT(anything...)
: tuple 들의 개수anything...
이라고 적어놓은 것은 column 은 아무래도 상관 없기 때문이다.
- 말만 들으면 복잡한데 아래 예시 보면 별거 아니다.
- 이놈은
SELECT
절에 들어가 결과를 합쳐준다. - 가령
COUNT
로 예를 들어 보면
PostgreSQL
SELECT COUNT(*) FROM student WHERE login LIKE '%@cs';
- 여러개의 Aggregation 을 사용하는 것도 가능하다
PostgreSQL
SELECT AVG(gpa), COUNT(*) FROM student WHERE login LIKE '%@cs';
GROUP BY
- Aggregation 을 사용할 때,
SELECT
에 Aggregation 되지 않은 것을 그냥 쓸 수는 없다. - 가령 다음 구문은 에러가 난다.
PostgreSQL
SELECT AVG(s.gpa), e.cid
FROM enrolled AS e JOIN student AS s
ON e.sid = s.sid;
- 왜냐면
AVG(s.gpa)
의 경우에는 aggregate 되어 하나의 결과로 나오지만,e.course_id
의 경우에는 하나로 합쳐지지 않기 때문. - 그래서 이때는
GROUP BY
를 사용하면 된다.
PostgreSQL
SELECT AVG(s.gpa), e.cid
FROM enrolled AS e JOIN student AS s
ON e.sid = s.sid
GROUP BY e.cid;
- 이렇게 하면 같은
e.course_id
를 가지는 row 들이 그루핑되어 해당 그룹에 대한AVG(s.gpa)
가 계산된다. - 즉,
GROUP BY
는 지정해준 column 을 기준으로 subbag 을 만든 뒤, 해당 subbag 에 대해 aggregation 을 할 수 있도록 해주는 키워드이고, SELECT
절에 있는 모든 non-aggregated value 들은 반드시GROUP BY
로 그루핑되어야 한다.- 물론 근데 DBMS 마다 조금씩은 차이가 있다.
- 대부분은 에러가 나지만
- MySQL 을
"traditional"
mode 로 바꾼 경우나 - SQLite 의 경우에는 결과가 나오긴 한다.
- 다만 이때는 non-aggregated value 의 경우 그냥 임의로 하나의 값을 고르는 듯 하다; 어떤 값이 나오든 간에 잘못된 행동이고, 에러가 나는게 SQL 표준에 맞는 것이다.
- 물론 근데 DBMS 마다 조금씩은 차이가 있다.
HAVING
- 만약에 aggregation 의 결과에 대해 조건을 걸기 위해
WHERE
절에 넣으면 어떻게 될까?
PostgreSQL
SELECT AVG(s.gpa) AS avg_gpa, e.cid
FROM enrolled AS e, student AS s
WHERE e.sid = s.sid
AND avg_gpa > 3.9
GROUP BY e.cid;
- 보다시피 에러가 난다.
- 이것은 Aggregation 이 마지막에 수행되기 때문에,
WHERE
절에서는avg_gpa
가 뭔지 모르기 때문.
- 이것은 Aggregation 이 마지막에 수행되기 때문에,
- 이때
HAVING
을 사용하면 된다.
PostgreSQL
SELECT AVG(s.gpa) AS avg_gpa, e.cid
FROM enrolled AS e, student AS s
WHERE e.sid = s.sid
GROUP BY e.cid
HAVING AVG(s.gpa) > 3.9;
- 다만 이때는 주의할 점이 있다.
HAVING
절은GROUP BY
다음에 와야 한다. 이전에 오면 에러남HAVING
절에는AS
로 지정된 alias 를 사용할 수는 없다. 이것도 “column 을 찾을 수 없다” 는 에러가 난다.- 참고로 내부 optimizer 에 의해 알아서 최적화 되기 때문에, 이렇게 한다고 해서
AVG
가 두번 연산되지는 않는다.
- 참고로 내부 optimizer 에 의해 알아서 최적화 되기 때문에, 이렇게 한다고 해서
- 즉,
HAVING
은 Aggregation 에 대해 필터링을 하고 싶을 때 사용하는 문법이다.
String
Constant
- 문자열에 관해 SQL-92 에 명시된 표준은 Single quote (
''
) 에 Case-sensitive 이다. - 하지만 (당연하게도?) 모든 DBMS 에서 이것을 따르지는 않는다.
NAME | CASE | QUOTE |
---|---|---|
SQL-92 | Sensitive | Single quote |
Postgres/MSSQL/Oracle | Sensitive | Single quote |
MySQL | Insensitive | Single/Double quote |
SQLite | Sensitive | Single/Double quote |
- 저 괘씸한 놈을 한번 확인해 보자.
- 먼저 PostgreSQL 은:
PostgreSQL
SELECT "good";
- SQL-92 를 잘 따르고 있는 것을 확인할 수 있다.
- 하지만 MySQL 은?
MySQL
SELECT "good";
""
도 사용 가능하다.- 또한 대소문자도 확인해 보자.
- 우선 PostgreSQL 은:
PostgreSQL
SELECT * FROM student WHERE name = 'RzA';
- 역시나 결과가 나오지 않는다. (
'RZA'
가 올바른 값이다.) - 하지만 MySQL 은:
MySQL
SELECT * FROM student WHERE name = 'RzA';
- 역시나 자기 맘대로 하는 것을 볼 수 있다.
- MySQL 에서는 대소문자를 구별하려면 다음처럼 하면 된다.
MySQL
SELECT * FROM student WHERE CAST(name AS BINARY) = 'RzA';
LIKE
- String matching 기능. 뭐 별거 없다.
LIKE
뒤에 다음의 것들을 이용해 pattern 을 적어주면 된다.%
는 임의 길이의 substring 과 매칭되고,_
는 subchar 하나와 매칭된다.
- 예를 들어:
PostgreSQL
SELECT * FROM student WHERE login LIKE 's%c_';
Functions
SUBSTRING()
: 말 그대로 substring 추출하는 것.UPPER()
,LOWER()
: 말 그대로 대소문자 변경하는 것.
PostgreSQL
SELECT name, SUBSTRING(name, 1, 3) AS substr,
UPPER(name) AS up,
LOWER(name) AS low
FROM student WHERE sid = 53655;
Concat
- String concat 은 SQL-92 표준으로는
||
이다. 하지만,- MSSQL 은
+
를 사용하고, - MySQL 은 이런 기능이 없어
CONCAT()
함수를 사용한다.
- MSSQL 은
- MySQL 로 해보자.
MySQL
SELECT name FROM student WHERE login = name || '@cs';
- 보다시피 에러가 난다.
- 하지만
CONCAT()
을 사용하면,
MySQL
SELECT name FROM student WHERE login = CONCAT(LOWER(name), '@cs');
Date & Time
- 이것 또한 각 DBMS 마다 지멋대로인 것 중 하나이다.
현재 날짜, 시간
- 일단 현재 날짜와 시간을 구하는 것부터 큰일이다.
NOW()
와CURRENT_TIMESTAMP
는 키워드도 있고 함수 (CURRENT_TIMESTAMP()
) 도 있다.
DBMS | NOW() | CURRENT_TIMESTAMP | CURRENT_TIMESTAMP() |
---|---|---|---|
DuckDB | O | O | X |
MySQL | O | O | O |
Oracle | X | O | X |
PostgreSQL | O | O | X |
SQLite | X | O | X |
String -> Date 변환
VARCHAR
로 표현된 날짜를 Date 객체로 바꾸는 것은,DATE()
함수나CAST(... AS DATE)
를 사용하면 된다.
MySQL
SELECT DATE('2024-07-05') AS func, CAST('2024-07-05' AS DATE) AS cast;
- 그런데 PostgreSQL (+ 그리고 이놈을 이어받은 DuckDB 같은 애들) 은
::date
라는 또 다른 문법적 설탕을 제공한다.- 근데 이게 왜 설탕인지는 모르겠다; 장점이 없는디
PostgreSQL
SELECT DATE('2024-07-05') AS func, '2024-07-05'::date AS sugar;
날짜 차이
- 두 날짜 간의 차이를 구하는 것을 해보자.
- PostgreSQL 에서는 그냥
DATE
간에 뺼셈을 해주면 된다.
PostgreSQL
SELECT DATE('2024-07-05') - DATE('2024-01-01');
- 근데 MySQL 에서는 이렇게 하면 안된다.
MySQL
SELECT DATE('2024-07-05') - DATE('2024-01-01');
- 저
604
라는 값은 7월 과 1월의 차이 (6
) 과 5일과 1일의 차이 (04
) 로 도출된 이상한 값이다. - MySQL 에서는 UNIX time 으로 바꿔서 계산하거나
MySQL
SELECT ROUND((UNIX_TIMESTAMP('2024-07-05') - UNIX_TIMESTAMP('2024-01-01')) / (60*60*24));
- 아니면
DATEDIFF
를 사용하면 된다.
MySQL
SELECT DATEDIFF(DATE('2024-07-05'), DATE('2024-01-01'));
- 물론 근데 이건 MySQL 에만 있는 기능인 것 같다.
- SQLite 에서는
julianday()
함수로 율리우스력으로 바꿔서 계산할 수 있다고 한다.
EXTRACT
DATE
객체에서 특정 값을 뽑아내는 함수이다.
PostgreSQL
SELECT EXTRACT(DAY FROM DATE('2024-07-05'));
왜 이렇게 문법이 지멋대로일까
- 일단 너무나 많은 DBMS 들이 있고 그 중에서 선두주자가 없기 때문이다.
- 이전에는 IBM 이라는 큰 기업이 있었고 여기에서 만든 SQL 이 표준으로 채택되긴 했지만
- 지금은 DBMS 에서는 그렇다 할 리딩DB 가 없기 때문
- 또한 큰 기업이라 할지라도 공통의 표준을 만드는 것은 어렵기 때문이다.
- 가령 구글이 ZetaSQL 라는 자기들만의 SQL 문법을 제시헀지만
- 망한 것을 보면 알 수 있다.
- 지금의 SQL 표준을 정할 때는 표준을 정한 뒤 나머지 DBMS 들이 채택하는 것과는 반대로 진행된다.
- 즉, 이미 DBMS 들에서는 나름대로 다 구현해 놓고,
- 그 기능에 대해 DBMS 들 간에 경쟁 (?) 을 통해 자신들의 구현본을 표준에 집어넣으려고 한다고 한다.
- 가령 최근의 SQL-2023 에 등록된 Graph query 의 경우에는 Oracle 이 제안해서 추가된 기능이다.
Window function
- Aggregation 은 여러개의 tuple 을 모아 하나의 결과를 내는 것이었다면
- Window 는 여러개의 tuple 에 대해 무언가를 해 각각의 tuple 에 대해 결과를 내는 것이다.
- 잘 이해가 안된다면 대표적인 예시인
RANK()
를 생각하면 된다.- 여러 tuple 들에 대해 순위를 매겨 각 tuple 별로 결과가 나오기 때문.
- 기본적인 문법은
함수이름() OVER()
형식이다.- 저기
OVER()
를 통해 고려할 tuple 들의 범위를 지정하거나, 추가적인 정보를 주게 된다.
- 저기
- 더 자세한 것은 아래 각 함수들을 이용해 살펴보자.
ROW_NUMBER()
- 말 그대로 줄 번호를 출력하는 것이다.
PostgreSQL
SELECT *, ROW_NUMBER() OVER() FROM enrolled;
- 당연한 얘기지만 이 값을 PK 로 사용할 수는 없다.
- 단순하게 화면에 출력되는 순서에 관한 것이고, 순서가 바뀌면 각 tuple 에 대한
ROW_NUMBER()
도 달라질 수 있기 때문.
- 단순하게 화면에 출력되는 순서에 관한 것이고, 순서가 바뀌면 각 tuple 에 대한
OVER()
, PARTITION BY
- Window function 을 사용할 때
OVER()
를 통해 범위를 지정해 줄 수 있고, PARTITION BY
는 aggregation 에서의GROUP BY
와 유사한 기능을 해준다.- 즉, 이것을 통해 window function 을 적용할 그룹을 지어줄 수 있는 것.
- 예시를 보자.
PostgreSQL
SELECT *, ROW_NUMBER() OVER(PARTITION BY cid) FROM enrolled;
- 보다시피
cid
별로 그룹지어져ROW_NUMBER()
가 계산된 것을 알 수 있다.
ORDER BY
기본 사용법
- 이건 Window function 에 국한된 기능은 아니긴 한데
- 어쨋든 말 그대로 결과를 정렬하는 기능이다.
PostgreSQL
SELECT * FROM enrolled ORDER BY cid;
정렬 옵션
- 기본적으로는 보다시피 오름차순으로 정렬하고, 이것은
ASC
옵션을 사용한 것과 같다.
PostgreSQL
SELECT * FROM enrolled ORDER BY cid ASC;
- 내림차순으로 정렬하기 위해서는
DESC
옵션을 사용하면 된다.
PostgreSQL
SELECT * FROM enrolled ORDER BY cid DESC;
OVER()
연동
ORDER BY
는 아래처럼OVER()
안에 넣어 정렬 후 Window function 을 굴릴 수도 있다.
PostgreSQL
SELECT *, ROW_NUMBER() OVER(ORDER BY cid) FROM enrolled;
- 결과만 보면
ORDER BY
를OVER()
안에 넣는 것이나 빼는 것이나 다를 바가 없어 보이지만, 차이점은 언제 정렬을 하냐에 달려 있다. - 가령 다음의 문장은
cid
로 정렬해서ROW_NUMER()
를 계산한 뒤, 그 결과를grade
에 따라 정렬해서 출력하게 된다.
PostgreSQL
SELECT *, ROW_NUMBER() OVER(ORDER BY cid) FROM enrolled ORDER BY grade;
RANK()
- 근데 어떤 것의 순위를 매길 때는
RANK()
함수를 더 많이 사용한다.
PostgreSQL
SELECT *, RANK() OVER(ORDER BY grade) FROM enrolled;
- 근데 보면 어차피
ORDER BY
를 사용해야 하는 것은 매한가지인데 이때ROW_NUMBER()
를 사용하는 것과RANK()
를 사용하는 것 간에 어떤 차이점이 있을 까?- 이건 주인장도 모르겠다. 딱히 문법적 설탕도 아닌 거시 요상혀
복잡한 용례
- Course 별 2번째로 성적이 높은 학생들만을 출력한다고 해보자.
- 그럼 일단 Course 별로 성적을 정렬한다.
PostgreSQL
SELECT *, RANK() OVER(PARTITION BY cid ORDER BY grade ASC) AS rank FROM enrolled;
- 그 다음에 이 결과에서
rank
가2
인 결과들을 필터링하면 될 것이야.
PostgreSQL
SELECT * FROM (
SELECT *, RANK() OVER(PARTITION BY cid ORDER BY grade ASC) AS rank FROM enrolled
) AS ranking WHERE ranking.rank = 2;
Nested (Sub) query
- Nested query (Subsuery) 는 말 그대로 하나의 query 안에 또 다른 query 가 들어가는 것이다.
- 이것은 query 내의 어디든 들어갈 수 있다.
FROM
절에도 가능하고WHERE
에도 가능하는 등
- Nested query 에서 바깥에 있는 query 를 Outer query 라고 하고, 안에 들어가 있는 query 를 Inner query 라고 한다.
- 가령 다음의 예시에서
SELECT name ...
이 Outer query 이고, SELECT sid ...
이 Inner query 이다.
- 가령 다음의 예시에서
PostgreSQL
SELECT name FROM student WHERE sid IN (
SELECT sid FROM enrolled WHERE cid = '15-445'
);
- 위 문장에서는
sid
가 두번 등장하는데, 첫번째sid
는student
테이블을 가리키고 두번째는enrolled
테이블을 가리킨다. - 이렇듯 각 DBMS 의 SQL 파서는 “문맥” 을 파악해 어떤 attribute 가 어떤 relation 의 것을 가리키는지 파악한다고 한다.
- 만일 파악에 실패한 경우라면, 좀 더 구체적으로 작성해 달라는 에러를 출력하게 된다.
- 이 Nested query 는 강력한 기능이긴 하지만, DBMS 로 하여금 성능을 최적화 하기에는 매우 어려운 부분이라고 한다.
- 그리고 위 문장은 기본적으로 두 테이블을
JOIN
하는 것이나 마찬가지이다.- 어떤 게 더 좋을지는 모르겠음; 알아서 최적화되어있으려나
Operators
- Inner query 를 Outer query 에서 사용할 때의 연산자가 몇개 있다.
ANY
, IN
=ANY()
: 이것은 왼쪽의 값이 오른쪽의 Inner query 의 결과에 포함되어 있으면 참으로 판단한다.- 다음의 쿼리를 보자.
PostgreSQL
SELECT name FROM student WHERE sid = ANY (
SELECT sid FROM enrolled WHERE cid = '15-445' OR cid = '15-721'
);
- 이 쿼리에서의 Inner query 인 다음의 문장의 결과를 다음처럼 나오는데
PostgreSQL
SELECT sid FROM enrolled WHERE cid = '15-445' OR cid = '15-721';
- 이때
student
테이블의 tuple 을 하나하나 검사해 sid 가 위의 결과에 포함되어 있으면 참으로 판단해 결과에 포함시키는 것이다. - 결과적으로 다음과 같이 출력된다.
IN()
은=ANY()
의 문법적 설탕이다.- 위의 예시를
IN
으로 고쳐도 결과는 같다.
- 위의 예시를
PostgreSQL
SELECT name FROM student WHERE sid IN (
SELECT sid FROM enrolled WHERE cid = '15-445' OR cid = '15-721'
);
ALL
- Subquery 의 모든 출력 결과에 부합해야 한다.
- 예를 들면 다음과 같은 query 가 가능하다.
PostgreSQL
SELECT * FROM student WHERE sid = ALL (
SELECT sid FROM enrolled WHERE enrollment_id = 1
);
- 하지만
enrollment_id > 1
가 되면 여러sid
가 조회되고, 이들과 부합하는 학생은 없기 때문에 아무런 결과도 나오지 않는다.
PostgreSQL
SELECT * FROM student WHERE sid = ALL (
SELECT sid FROM enrolled WHERE enrollment_id > 1
);
EXISTS
- Subquery 의 결과가 존재해야 한다.
- 가령 (별로 좋은 예시는 아닌 것 같지만) 다음의 예시가 가능하다.
- 여기서는 subquery 가 false 이기 때문에, 아무런 tuple 도 출력되지 않는다.
PostgreSQL
SELECT * FROM student WHERE EXISTS (
SELECT sid FROM enrolled WHERE enrollment_id < 1
);
EXPLAIN
- 이 키워드는 어떤 query 를 실행할 때의 query plan 을 출력하는 기능이다.
- 즉, 요청한 query 가 optimizer 를 거쳐 실제로 어떻게 처리되는지를 보여준다.
- 예를 들어 PostgreSQL 에서는:
PostgreSQL
EXPLAIN SELECT name FROM student WHERE sid IN (
SELECT sid FROM enrolled WHERE cid = '15-445' OR cid = '15-721'
);
- 그리고 MySQL 의 경우에는:
MySQL
EXPLAIN SELECT name FROM student WHERE sid IN (
SELECT sid FROM enrolled WHERE cid = '15-445' OR cid = '15-721'
);
SELECT cid, COUNT(*) FROM enrolled GROUP BY cid;
LATERAL
LATERAL
은 Join 의 한 종류인데 외부의 alias 를 가져다 쓸 수 있게 해주는 Join 이다.- 가령 다음의 문장은 에러가 난다.
PostgreSQL
SELECT * FROM (SELECT 1 AS x) AS t1, (SELECT t1.x + 1 AS y) AS t2;
- 이때에는 저기 에러문구에도 친절하게 적혀있듯이,
LATERAL
을 사용하면 된다.
PostgreSQL
SELECT * FROM
(SELECT 1 AS x) AS t1,
LATERAL (SELECT t1.x + 1 AS y) AS t2;
예제1
- 모든 course 정보를 출력하고, 각 course 에 대해 등록한 student 들의 명수와 student 들의 평균 GPA 를 출력하라
정답
PostgreSQLSELECT * FROM course AS c, LATERAL (SELECT COUNT(*) AS cnt FROM enrolled AS e WHERE e.cid = c.cid) AS t1, LATERAL (SELECT AVG(gpa) FROM student AS s JOIN enrolled AS e ON s.sid = e.sid WHERE e.cid = c.cid) AS t2;
Common Table Expression (CTE)
- 기존의 Nested query 에서와 유사하게 query 안에 query 를 담는 것이긴 한데
- Nested query 에서는 한 inner query 는 한 outer query 에 종속되어 여기에서만 사용 가능했다면
- Common Table Expression (CTE) 는 query 외부에 저장해 다른 query 에서도 사용할 수 있게 해놓은 것이다.
- 비유하자면 Nested query 는 local variable, CTE 는 global variable 과 같은 느낌이다.
- 이놈은
WITH ... AS ()
문법을 사용한다.
PostgreSQL
WITH maxGPA AS (
SELECT MAX(gpa) FROM student
)
SELECT * FROM maxGPA;
- Column 이름을 붙이는 것도 가능하다.
PostgreSQL
WITH maxGPA (mx, mn) AS (
SELECT MAX(gpa), MIN(gpa) FROM student
)
SELECT * FROM maxGPA;