본 글의 그림들은 별도의 명시가 없는 한 (1) 해당 강의 자료에서 가져오거나 (2) 주인장이 직접 만들었습니다.

다소 잘못된 내용과 구어적 표현 이 포함되어 있을 수 있습니다.

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 에서 최대한 많은 것을 해주려는 일종의 맥시멀리즘의 성향을 띈다고 한다.
  • 그럼 이제 SQL 표준의 여러 기능들을 살펴보자.

Aggregation function

  • Aggregation functionBag 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 표준에 맞는 것이다.

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 가 뭔지 모르기 때문.
  • 이때 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 가 두번 연산되지는 않는다.
  • 즉, HAVINGAggregation 에 대해 필터링을 하고 싶을 때 사용하는 문법이다.

String

Constant

  • 문자열에 관해 SQL-92 에 명시된 표준은 Single quote ('') 에 Case-sensitive 이다.
  • 하지만 (당연하게도?) 모든 DBMS 에서 이것을 따르지는 않는다.
NAMECASEQUOTE
SQL-92SensitiveSingle quote
Postgres/MSSQL/OracleSensitiveSingle quote
MySQLInsensitiveSingle/Double quote
SQLiteSensitiveSingle/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() 함수를 사용한다.
  • 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()) 도 있다.
DBMSNOW()CURRENT_TIMESTAMPCURRENT_TIMESTAMP()
DuckDBOOX
MySQLOOO
OracleXOX
PostgreSQLOOX
SQLiteXOX

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() 도 달라질 수 있기 때문.

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 BYOVER() 안에 넣는 것이나 빼는 것이나 다를 바가 없어 보이지만, 차이점은 언제 정렬을 하냐에 달려 있다.
  • 가령 다음의 문장은 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;

  • 그 다음에 이 결과에서 rank2 인 결과들을 필터링하면 될 것이야.
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 가 두번 등장하는데, 첫번째 sidstudent 테이블을 가리키고 두번째는 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 를 출력하라

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;