1. ERD 설계 및 테이블 생성하기

SNS 어플리케이션인 instagram의 정확한 DB 관계도를 표현한 것이 아니다.

하지만, 실제로 DB가 어떤 식으로 이어지는지 그 흐름과 실질적인 데이터 추출을 연습하고자 따라가는 과정이다.

 

https://kmooc.udemy.com/course/best-mysql-k/learn/lecture/35998016#questions/20983746

실습에 필요한 데이터는 혹시 모를 누군가를 위해, 구글 드라이브 링크를 걸어둔다.

위 데이터는 Colt Steele이라는 사람의 데이터를 인용한 것이다. 그의 강의유튜브를 참고삼아 공부하는 것도 좋다.

역시 저 사람의 문제를 따라가면서 실습을 해보았는데, 의문인 점이 있어서 질문을 직접 보내놨다.

이 부분은 문제를 따라가면서 밑에서 설명한다.

 

1차적으로 우선 데이터베이스 틀을 작성해야 한다.

ERD를 보고서 직접 작성해보는 것이 제일 바람직하다고 본다.

 

CREATE TABLE users (
    id			INT AUTO_INCREMENT PRIMARY KEY,
    username	VARCHAR(255) UNIQUE NOT NULL,
    created_at	TIMESTAMP DEFAULT NOW()
);

유저 정보에 대한 데이터를 저장하는 테이블이다.

핵심 부분만 간단하게 작성하는 테이블이기에 이름과 생성일자만 담아둔다.

유저 이름은 중복을 허용하지 않아 UNIQUE로 선언하였다.

 

MySQL 기준으로 실제 테이블을 생성하여 서식을 조회해본다면 위와 같다.

 

CREATE TABLE photos (
    id			INT AUTO_INCREMENT PRIMARY KEY,
    image_url	VARCHAR(255) NOT NULL,
    user_id		INT NOT NULL,
    created_at	TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY(user_id) REFERENCES users(id)
);

유저가 올리는 사진에 대해서 저장하는 테이블이다.

이미지 정보는 url 문자로 받아들이고, 없어서는 안 되기에 NOT NULL로 선언한다.

이미지만 있고 유저가 없는 경우도 말이 안 된다. 따라서 유저 아이디도 NOT NULL로 선언한다.

또한 유저 아이디는 users 테이블의 외래키로 사용한다.

 

MySQL 기준으로 실제 테이블을 생성하여 서식을 조회해본다면 위와 같다.

 

CREATE TABLE comments(
    id	INT AUTO_INCREMENT PRIMARY KEY,
    comment_text	VARCHAR(255) NOT NULL,
    user_id			INT NOT NULL,
    photo_id		INT NOT NULL,
    created_at		TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY(user_id) REFERENCES users(id),
    FOREIGN KEY(photo_id) REFERENCES photos(id)
);

계정이 올린 메시지를 관리하는 테이블이다.

내용과 메시지를 작성한 유저가 반드시 있어야 하기에 NOT NULL로 선언한다.

사진은 선택 여부로 두어도 되지만 여기서는 NOT NULL로 선언했다.

유저 정보와 사진 정보는 외래키로 사용한다.

 

MySQL 기준으로 실제 테이블을 생성하여 서식을 조회해본다면 위와 같다.

 

CREATE TABLE likes (
    user_id		INT NOT NULL,
    photo_id	INT NOT NULL,
    created_at	TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY(user_id) REFERENCES users(id),
    FOREIGN KEY(photo_id) REFERENCES photos(id),
    PRIMARY KEY(user_id, photo_id)
);

좋아요를 누를 수 있는 정보를 담는 테이블이다.

이때 유저가 특정 사진에 좋아요를 한 번만 누를 수 있어야 하기에, 두 column의 조합을 기본키로 둔다.

 

MySQL 기준으로 실제 테이블을 생성하여 서식을 조회해본다면 위와 같다.

 

CREATE TABLE follows (
    follower_id	INT NOT NULL,
    followee_id	INT NOT NULL,
    created_at	TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY(follower_id) REFERENCES users(id),
    FOREIGN KEY(followee_id) REFERENCES users(id),
    PRIMARY KEY(follower_id, followee_id)
);

팔로워와 팔로잉을 저장하는 테이블이다.

likes와 마찬가지로 한 유저가 다른 유저를 한 번만 팔로우할 수 있다.

두 column의 조합을 기본키로 설정한다.

 

MySQL 기준으로 실제 테이블을 생성하여 서식을 조회해본다면 위와 같다.

 

CREATE TABLE tags (
    id			INT AUTO_INCREMENT PRIMARY KEY,
    tag_name	VARCHAR(255) UNIQUE,
    created_at	TIMESTAMP DEFAULT NOW()
);

해시태그를 저장하는 테이블이다.

태그 이름은 중복으로 생성하면 안 되기 때문에 UNIQUE로 생성한다.

태그를 생성하는 방법은 다양한 방법이 있기에 검색하는 것을 추천한다.

 

MySQL 기준으로 실제 테이블을 생성하여 서식을 조회해본다면 위와 같다.

 

CREATE TABLE photo_tags (
    photo_id	INT NOT NULL,
    tag_id		INT NOT NULL,
    FOREIGN KEY(photo_id) REFERENCES photos(id),
    FOREIGN KEY(tag_id) REFERENCES tags(id),
    PRIMARY KEY(photo_id, tag_id)
);

사진에 작성한 태그를 저장하는 테이블이다.

한 사진에 다양한 태그를 넣을 수 있지만, 한 태그는 한 번밖에 넣지 못한다.

따라서 두 column의 조합을 기본키로 선언한다.

 

MySQL 기준으로 실제 테이블을 생성하여 서식을 조회해본다면 위와 같다.

 

2. 간단한 예제 풀어보기

실생활에서 실제로 있을 수 있는 질문 7가지가 준비되어 있다.

다음 질문에 맞는 쿼리를 작성해보자.

 

[ Question 1 ]
We want to reward our users who have been around the longest.
Find the 5 oldest users.

[ Question 2 ]
What day of the week do most users register on?
We need to figure out when to schedule an ad campaign.

[ Question 3 ]
We want to target our inactive users with an email comapaign.
Find the users who have never posted a photo.

[ Question 4 ]
We're running a new contest to see who can get the most likes on a single photo.

[ Question 5 ]
Our investors want to know that
How many times does the average user post?

[ Question 6 ]
A brand wants to know which hashtags to use in a post
What are the top 5 most commmonly used hashtags?


[ Question 7 ]
We have a small problem with bots on our site.
Find users who have liked every single photo on the site

3. SQL 작성하기

-- Question 1
-- We want to reward our users who have been around the longest.
-- Find the 5 oldest users.

SELECT * FROM users ORDER BY created_at ASC LIMIT 5;

SNS를 오래 사용한 유저에게 감사의 의미로 기념품을 제공하려고 한다.

이때 가장 오래 사용한 5명의 유저를 추출하라는 문제였다.

users 테이블에서 created_at을 오름차순으로 정렬한 뒤 LIMIT 5를 하는 간단한 문제다.

 

-- Question 2
-- What day of the week do most users register on?
-- We need to figure out when to schedule an ad campaign.

SELECT
    DAYNAME(created_at) AS register_on,
    COUNT(*) AS how_many
FROM users
GROUP BY register_on
ORDER BY how_many DESC;

광고를 띄우려고 하는데 어떤 요일에 띄우는 것이 제일 좋은지 알고 싶다.

따라서 사람들이 가장 많이 회원 가입을 한 요일을 찾으라는 문제였다.

register_on으로 GROUP BY 한 다음, DAYNAME으로 요일을 추출한다.

그 다음 COUNT()로 개수를 센 다음 내림차순으로 정렬하면 된다.

 

-- Question 3
-- We want to target our inactive users with an email comapaign.
-- Find the users who have never posted a photo.

SELECT username
FROM users
LEFT JOIN photos ON users.id = photos.user_id
WHERE photos.image_url IS NULL;

비활성 유저들 때문에 골머리를 썩고 있다. 활동 촉진을 하든, 계정 삭제를 하든 해야 한다.

그들에게 메일을 보내야 해서, 포스팅 이력이 없는 유저를 찾는 문제였다.

photos 테이블과 users 테이블을 JOIN하여 포스팅 이력을 확인한다.

이때 유저의 어떤 정보로든 NULL을 확인하면 된다.

 

-- Question 4
-- We're running a new contest to see who can get the most likes on a single photo.

SELECT
    users.username,
    photos.id,
    photos.image_url,
    COUNT(*) AS how_many_likes
FROM photos
JOIN likes ON likes.photo_id = photos.id
JOIN users ON users.id = photos.user_id
GROUP BY photos.id
ORDER BY how_many_likes DESC
LIMIT 1;

가장 많은 좋아요를 받은 사진을 포스팅한 유저에게 기념품을 주려고 한다.

해당 유저 정보를 찾는 문제였다.

JOIN을 2번해야 하는 조금 복잡한 query였다.

 

photos와 likes, users를 id를 기준으로 INNER JOIN한다.

그리고 photo를 기준으로 GROUP BY하여 개수를 COUNT()한다.

COUNT()를 기준으로 내림차순하고 LIMIT 1하면 된다.

 

-- Question 5
-- Our investors want to know that
-- How many times does the average user post?

SELECT (SELECT COUNT(*) FROM photos) / (SELECT COUNT(*) FROM users) AS avg;

투자자가 SNS를 인수하려고 한다. 그래서 상품 가치를 판단하려고 한다.

그래서 유저당 평균 포스팅 개수는 얼마인지 구하는 문제였다.

Colt steele은 위와 같은 형태로 query를 작성했다.

단순하게 photos의 개수를 users의 개수로 나눈 것이다.

 

SELECT AVG(user_post) AS avg_user_post
FROM (
    SELECT COUNT(*) AS user_post
    FROM comments
    GROUP BY user_id
) AS subquery;

하지만 의문이 들었다. 저건 단순히 유저-게시글의 비율이지 않은가?

포스팅을 한 유저에 한해서 평균을 내야 한다고 생각하여 작성한 query다.

subquery를 이용하여 문제를 해결했다.

user_id를 기준으로 comments를 GROUP BY하여 유저별 포스팅 개수를 알아낸다.

그리고 해당 테이블에서 전체 평균을 구한다.

 

-- Question 6
-- A brand wants to know which hashtags to use in a post
-- What are the top 5 most commmonly used hashtags?

SELECT
    tags.tag_name,
    COUNT(*) as how_many_use
FROM photo_tags
LEFT JOIN tags ON tags.id = photo_tags.tag_id
GROUP BY photo_tags.tag_id
ORDER BY how_many_use DESC
LIMIT 5;

회사에서 SNS 현황을 조사하려고 한다. 가장 많이 사용한 TOP 5 해시태그를 조사하는 문제였다.

photo_tags와 tags를 LEFT JOIN하여, tag별로 GROUP BY하고 개수를 센 뒤, DESC하는 간단한 문제였다.

 

WITH RankedTags AS (
    SELECT
        tags.tag_name,
        COUNT(*) AS how_many_use,
        DENSE_RANK() OVER(ORDER BY COUNT(*) DESC) AS ranking
    FROM photo_tags
    LEFT JOIN tags ON tags.id = photo_tags.tag_id
    GROUP BY tags.tag_name
)
SELECT *
FROM RankedTags
WHERE ranking < 6;

그런데 LIMIT 5를 하면 문제가 있다고 생각했다.

이전 window function에서 RANK()와 DENSE_RANK()를 배웠다.

TOP 5의 정의를 조금 더 명확하게 해주어야 했다고 생각했다.

여기서는 5순위(동순위)까지 포함하는 태그를 찾도록 query를 작성했다.

마찬가지로 subquery를 이용했다.

Quesiton 5에서 사용한 subquery 형태와 조금 다른데, subquery 연습을 위해서 조금 다르게 구현해보았다.

 

우선 photo_tags와 tags를 LEFT JOIN하여 GROUP BY 해준다.0

그 다음 DESNE_RANK()를 이용하여 동순위를 포함한 순위를 계산한다.

이때 window function은 GROUP BY와 HAVING, WHERE 조건이 전부 끝난 뒤 마지막에 실행한다.

따라서 해당 query로 나온 결과를 RankedTags라는 테이블로 두고 다시 조회를 해야 한다.

RankedTags에서 ranking이 6보다 작으면 전체 정보를 출력한다.

 

-- Question 7
-- We have a small problem with bots on our site.
-- Find users who have liked every single photo on the site

SELECT
    username,
    COUNT(*) AS total_likes
FROM likes
JOIN users ON users.id = likes.user_id
GROUP BY user_id
HAVING total_likes = (SELECT COUNT(*) FROM photos);

 

 

SNS에 봇이 너무 많아서 문제가 생기고 있다.

모든 사진에 좋아요를 누른 유저가 몇 명인지 찾는 문제였다.

오히려 5번 6번 문제보다는 쉬운 문제라고 생각한다.

likes와 users 테이블을 LEFT JOIN하고 user_id로 GROUP BY한다.

그리고 subquery를 사용하여 likes 개수가 사진 개수와 같은 유저를 찾아낸다.

'Language > SQL' 카테고리의 다른 글

SQL Window functions  (0) 2023.12.10
SQL View, Mode  (0) 2023.10.11
SQL Many-to-Many Relationship  (0) 2023.10.08
SQL One-to-Many Relationship  (0) 2023.10.02
SQL CONSTRAINT, ALTER  (0) 2023.09.26
CREATE TABLE employees (
    emp_no INT PRIMARY KEY AUTO_INCREMENT,
    department VARCHAR(20),
    salary INT
);
 
INSERT INTO employees (department, salary) VALUES
('engineering', 80000),
('engineering', 69000),
('engineering', 70000),
('engineering', 103000),
('engineering', 67000),
('engineering', 89000),
('engineering', 91000),
('sales', 59000),
('sales', 70000),
('sales', 159000),
('sales', 72000),
('sales', 60000),
('sales', 61000),
('sales', 61000),
('customer service', 38000),
('customer service', 45000),
('customer service', 61000),
('customer service', 40000),
('customer service', 31000),
('customer service', 56000),
('customer service', 55000);

밑에서 다루는 window function들은 전부 위의 table을 바탕으로 조회한 결과다.

 

-- Window Functions
-- https://dev.mysql.com/doc/refman/8.0/en/window-functions.html

-- Explain)
-- Window functions perform aggregate operations on groups of rows,
-- but they produce a result FOR EACH ROW


SELECT department, AVG(salary) FROM employees GROUP BY department;
SELECT AVG(salary) FROM employees;

Window function은 각 행에 대해서 집계를 해주는 함수다.

쉽게 말하면 집계 함수(aggregate function)을 묶지 않고, 각 행마다 적용하는 것이다.

 

집계 함수를 예를 들어보면 위와 같은 결과를 보인다.

department를 기준을 GROUP BY를 하여 AVG를 구한 결과, 그리고 전체를 AVG한 결과다.

이런 식으로 '집계 함수(agg func)'는 특정 집단을 묶어서 결과를 뱉는다.

 

-- OVER
-- The OVER() clause constructs a window.
-- When it's empty, the window will include all records.
SELECT emp_no, department, salary, AVG(salary) OVER() FROM employees;

 

Window function 중 OVER() 함수를 사용하면, 각 행에 대해서 연산을 수행한다.

'window'가 붙은 이유는 각 묶음을 window라고 부르기 때문이다.

OVER() 괄호 안에 아무런 조건을 넣지 않으면, 모든 행을 범위로 계산한다.

 

AVG(salary) OVER()

가장 우측에 보이는 열은 AVG(salary) OVER()를 수행한 결과다.

OVER()에 아무런 조건을 넣지 않아, 모든 행을 범위로 평균을 구하고, 모든 행에 결과를 적용한다.

 

-- PARTITION BY
-- Inside of the OVER(), use PARTITION BY to form rows into groups of row
SELECT
    emp_no,
    department,
    salary,
    AVG(salary) OVER(PARTITION BY department) AS dept_avg
FROM employees;

OVER() 내에 window 범위를 지정해주는 PARTITION BY 함수다.

PARTITION BY 'col_name' 형태로 적으면 col_name에 따라서 window로 묶어서 계산한다.

 

AVG(salary) OVER(PARTITION BY department) AS dept_avg

PARTITION BY department를 조건으로 넣어, 같은 department끼리 salary의 평균을 구한다.

customer sevice끼리, engineering끼리, sales끼리의 salary 평균을 구하여 각 행에 적용한다.

 

SELECT
    emp_no,
    department,
    salary,
    AVG(salary) OVER(PARTITION BY department) AS dept_avg,
    AVG(salary) OVER() AS company_avg
FROM employees;

SELECT
    emp_no,
    department,
    salary,
    MIN(salary) OVER(PARTITION BY department) AS dept_min,
    MAX(salary) OVER(PARTITION BY department) AS dept_max
FROM employees;

SELECT
    emp_no,
    department,
    salary,
    AVG(salary) OVER(PARTITION BY department) AS dept_avg,
    COUNT(department) OVER(PARTITION BY department) AS team
FROM employees;

AVG(), MIN(), MAX(), COUNT() 같은 집계 함수를, OVER(), PARTITION BY로 작성한 예제 구문들이다.

첫 번째 구문은 department를 window로 salary 평균을 구하고, 전체 salary의 평균을 구한다.

두 번째 구문은 department를 window로 가장 작은 salary와 가장 큰 salary를 구한다. 

세 번째 구문은 department를 window로 salary 평균을 구하고, department를 window로 개수를 구한다.

 

-- ORDER BY
-- Use ORDER BY inside of the OVER() clause to re-order rows within each window.
SELECT
    emp_no,
    department,
    salary,
    SUM(salary) OVER(PARTITION BY department ORDER BY salary) AS rolling_dept_salary,
    SUM(salary) OVER(PARTITION BY department) AS total_dept_salary
FROM employees;

ORDER BY는 window를 정렬한다. DESC와 ASC 모두 가능하다.

 

SUM(salary) OVER(PARTITION BY department ORDER BY salary) AS rolling_dept_salary 구문이다.

자세히보면 SUM(salary) 부분이 특이하게 동작한다.

ORDER BY에 의해 정렬한 다음, 각 행이 늘어날 때마다 추가적으로 더해주는 모습이다.

첫 번째 행은 31000, 두 번째 행은 69000, 세 번째 행은 109000처럼 말이다.

 

-- Only window functions
-- https://dev.mysql.com/doc/refman/8.0/en/window-function-descriptions.html

-- RANK()
SELECT
    emp_no,
    department,
    salary,
    RANK() OVER(PARTITION BY department ORDER BY salary DESC) AS dept_salary_rank,
    RANK() OVER(ORDER BY salary DESC) AS overall_salary_rank
FROM employees
ORDER BY department;

ORDER BY()는 굳이 window function이 아니더라도, 사용할 수 있는 함수다.

RANK()는 오직 window function으로만 사용할 수 있는 함수 중 하나이다.

이런 함수들은 https://dev.mysql.com/doc/refman/8.0/en/window-function-descriptions.html에서 확인할 수 있다.

RANK()는 해당 값이 몇 위인지 알려준다. 이때 동순위가 있으면 다음 순위는 그만큼 건너뛴다.

 

RANK() OVER(PARTITION BY department ORDER BY salary DESC) as dept_salary_rank

왼쪽에서부터 4번째 열은 RANK() OVER(PARTITION BY department ORDER BY salary DESC) AS dept_salary_rank다.

department끼리 window로 묶어서 salary를 기준으로 순위를 매겼다.

이때 sales의 순위를 보면, 1 2 3 4 4 6 7순으로 붙은 것을 확인할 수 있다.

 

-- ROW_NUMBER()
SELECT
    emp_no,
    department,
    salary,
    ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) as 'row_number',
    RANK() OVER(PARTITION BY department ORDER BY salary DESC) as dept_salary_rank,
    RANK() OVER(ORDER BY salary DESC) as overall_salary_rank
FROM employees
ORDER BY department;

ROW_NUMBER() 또한 오직 window function으로만 동작한다.

함수명 그대로 몇 번째 행인지 알려준다.

 

row_number로 이름이 붙은 열을 보면, department를 window로 묶어 위에서부터 행을 적은 것을 볼 수 있다.

 

-- DENSE_RANK()
SELECT
    salary,
    ROW_NUMBER() OVER s AS 'row-number',
    RANK()       OVER s AS 'rank',
    DENSE_RANK() OVER s AS 'dense_rank'
FROM employees
WINDOW s AS (ORDER BY salary);

DESNE_RANK()는 RANK()처럼 순위를 반환해주지만, 동순위가 있어도 다음 순위는 그대로 이어서 알려준다.

참고로 window function은 위와 같은 형태로도 작성할 수 있다.

 

ROW_NUMBER(), RANK(), DENSE_RANK()를 비교하기 위해 작성한 코드 예제다.

ROW_NUMBER()는 행 자체 번호를 반환한다.

RANK()는 순위를 반환하되, 동순위가 있다면 다음 순위를 그만큼 건너뛰고 반환한다.

DENSE_RANK()는 순위를 반환하되, 동순위가 있어도 다음을 바로 이어서 반환한다.

 

-- NTILE()
SELECT
    emp_no,
    department,
    salary,
    NTILE(4) OVER(ORDER BY salary DESC) AS salary_quantile
FROM employees;

SELECT
    emp_no,
    department,
    salary,
    NTILE(4) OVER(PARTITION BY department ORDER BY salary DESC) AS dept_salary_quantile
FROM employees;

NTILE()은 전체를 N개로 나누고 어떤 분위에 속하는지 확인하는 함수다.

N이 4라면 상위 25%, 상위 50%, 상위 75%, 상위 100%로 나누어, 어디에 속하는지 알려준다는 말이다.

또 다른 예로 N을 100으로 설정하면 백분율을 알 수 있다.

 

왼쪽은 첫 번째 구문으로, 전체를 NTILE(4)한 다음 salary를 기준으로 DESC 정렬을 한 결과다.

오른쪽은 두 번째 구문으로, department로 window를 나누고 NTILE(4)를 한 결과다.

 

-- FIRST_VALUE() LAST_VALUE(), Nth_VALUE()
SELECT
    emp_no,
    department,
    salary,
    FIRST_VALUE(emp_no) OVER(PARTITION BY department ORDER BY salary DESC) AS highest_dept_salary,
    LAST_VALUE(emp_no) OVER(PARTITION BY department ORDER BY salary DESC) AS lowest_dept_salary,
    Nth_VALUE(emp_no, 2) OVER(PARTITION BY department ORDER BY salary DESC) AS '2th_dept_salary'
FROM employees;

FIRST_VALUE()는 특정 window에서 첫 번째 나온 값이 무엇인지 알려준다.

LAST_VALUE()는 특정 window에서 마지막에 나온 값이 무엇인지 알려준다.

Nth_VALUE()는 특정 window에서 N번째로 나온 값이 무엇인지 알려준다.

 

department를 기준으로 window를 나누고, salary의 FIRST_VALUE(), LAST_VALUE(), 2th_VALUE()를 확인한다.

FIRST_VALUE()는 가장 처음에 나온 값을 반환하기에, 숫자가 고정인 것을 알 수 있다.

LAST_VALUE()는 가장 마지막에 나온 값을 반환하기에, 새로운 행을 window에 포함할수록 바뀌는 것을 알 수 있다.

2th_VALUE()는 2번째 값을 반환하기에, 가장 첫 행에서는 NULL을 반환하는 것을 알 수 있다.

 

-- LAG(bef), LEAD(aft)
SELECT
    emp_no,
    department,
    salary,
    salary - LEAD(salary) OVER(ORDER BY salary DESC) AS salary_diff_by_aft,
    LAG(salary) OVER(ORDER BY salary DESC) AS bef_salary
FROM employees;

SELECT
    emp_no,
    department,
    salary,
    salary - LAG(salary) OVER(PARTITION BY department ORDER BY salary DESC) AS dept_salary_diff_bef,
    salary - LEAD(salary) OVER(PARTITION BY department ORDER BY salary DESC) AS dept_salary_diff_aft
FROM employees;

SELECT
    emp_no,
    department,
    salary,
    LAG(salary, 2) OVER(ORDER BY salary DESC) AS bef_salary
FROM employees;

LAG() 함수는 바로 직전의 값이 무엇이었는지 알려준다.

LEAD() 함수는 바로 직후의 값이 무엇일지 알려준다.

LAG(), LEAD() 함수 모두 2번째 매개변수로 N을 넘겨주면, N번째 앞 혹은 뒤의 값을 알려준다.

 

위의 결과는 3개의 구문 중 첫 번째 구문을 실행한 결과다.

왼쪽에서부터 4번째 행은 salary - LEAD(salary) OVER(ORDER BY salary DESC) AS salary_diff_by_aft 구문이다.

salary를 기준으로 DESC 한 다음, 전체를 window로 보고, 현재 값에서 바로 다음 값을 뺸 값을 보여준다.

가장 오른쪽 끝 행은 LAG(salary) OVER(ORDER BY salary DESC) AS bef_salary 구문이다.

salary를 기준으로 DESC 한 다음, 전체를 window로 보고 바로 이전 값을 보여준다.

이처럼 정렬 기준을 여러 번 선언할 수 있다. 이때는 가장 마지막에 선언한 것을 기준으로 정렬한다.

'Language > SQL' 카테고리의 다른 글

ERD와 예제 문제 살펴보기: instagram  (0) 2023.12.11
SQL View, Mode  (0) 2023.10.11
SQL Many-to-Many Relationship  (0) 2023.10.08
SQL One-to-Many Relationship  (0) 2023.10.02
SQL CONSTRAINT, ALTER  (0) 2023.09.26

 

https://ko.wikipedia.org/wiki/VIEW_(SQL)

SQL에서 View(뷰)의 정의는 위키피디아에 따르면 위와 같다.

"하나 이상의 테이블에서 원하는 모든 데이터를 선택하여 그들을 사용자 정의하여 나타낸 것."

또한, "뷰는 기본 테이블과 같이 행과 열로 구성되지만, 다른 테이블에 있는 데이터를 보여줄 뿐이다."

장황하게 적혀있지만, 간단하게 설명하자면 '가상의 테이블'을 만드는 것이다.

 

-- View exercise
SELECT title, released_year, genre, rating, first_name, last_name
FROM reviews
JOIN series ON series.id = reviews.series_id
JOIN reviewers ON reviewers.id = reviews.reviewer_id;

저번 글에서 사용한 series, reviews, reviewers의 테이블 3가지를 사용한 코드다.

마지막 예제에서 살펴보았듯, 두 번의 JOIN을 통해 3개의 테이블을 연결한 상태다.

이때 위에서 얻은 테이블을 따로 사용하고 싶다면? 아니면 위의 테이블을 자주 조회해야 한다면?

4줄에 걸친 저 긴 코드를 계속해서 사용해야 할 것이다.

이런 불편함과 번거로움을 줄이기 위해 가상의 테이블을 하나 만드는 것이다. 이것이 View(뷰)다.

마치 프로그래밍 언어에서 함수 선언과 유사하다.

 

-- 기본 구문
CREATE VIEW view_name AS table_expression_query

-- 예제
CREATE VIEW full_reviews AS
SELECT title, released_year, genre, rating, first_name, last_name
FROM reviews
JOIN series ON series.id = reviews.series_id
JOIN reviewers ON reviewers.id = reviews.reviewer_id;

기본 구문은 CREATE VIEW 다음에 원하는 이름을 적고, AS 뒤에 실행할 구문을 적는다.

위에서 적은 JOIN 구문을 예로 들면, 그 자체를 하나의 새로운 가상 테이블로 취급하는 것이다. 

이제 4줄의 긴 query 없이 full_reivews라는 하나의 뷰에서 데이터 접근이 가능하다.

 

 

SELECT * FROM full_reviews;

SELECT *
FROM full_reviews
WHERE genre = 'Animation';

SELECT
    genre,
    AVG(rating) AS avg_rating
FROM full_reviews
GROUP BY genre;

full_reviews라는 뷰를 하나의 테이블처럼 사용 가능하다.

첫 번째 예시는 full_reviews의 모든 column을 조회하는 query다.

두 번째 예시는 full_reviews에서 genre가 Animation인 column만 조회하는 qeury다.

세 번째 예시는 full_reviews에서 genre별로 묶어, genre와 평균 rating을 조회하는 qeury다.

 

-- Can not do
DELETE FROM full_reviews
WHERE released_year = 2010;

View가 가상 테이블이라고 하는 것은, 실제로 선언되어있지는 않다는 말이다.

하지만, SHOW TABLES로 테이블을 조회하면 목록에 보이기는 한다.

이처럼 오묘한 View가 행동할 수 있는 경계가 있다.

SQL DOCS를 확인하면 정확하게 알 수 있다. https://dev.mysql.com/doc/refman/8.0/en/view-updatability.html

대표적으로는 위처럼 DELETE는 안 된다.

정확하게는 JOIN이나 GROUP BY로 묶은 View에 한해서 DELETE 할 수 없다.

 

-- Can do
CREATE VIEW ordered_series AS
SELECT *
FROM series
ORDER BY released_year;

INSERT INTO ordered_series(title, released_year, genre)
VALUES ('The Great', 2020, 'Comedy');

DELETE FROM ordered_series
WHERE title = 'The Great';

ORDER BY는 JOIN이나 GROUP BY와 다르게 제약 조건이 없다.

그래서 released_year를 기준으로 정렬한 series 테이블을 ordered_series라는 View로 정의했다고 해보자.

그럼 ordered_series에는 데이터 추가, 삭제가 모두 가능하다.

 

 

-- Redefine view query
CREATE OR REPLACE VIEW ordered_series AS
SELECT * FROM series ORDER BY released_year DESC;

ALTER VIEW ordered_series AS
SELECT * FROM series ORDER BY released_year DESC;

View를 바꿀 일이 있다면, 위의 query 중 하나로 변경가능하다. 

 

DROP VIEW ordered_series;

삭제하는 것도 물론 가능하다.

 

-- GROUP BY: HAVING
-- https://dev.mysql.com/doc/refman/8.0/en/select.html
SELECT
    title,
    AVG(rating) AS avg_rating,
    COUNT(rating) AS review_count
FROM full_reviews
GROUP BY title
HAVING COUNT(rating) > 1;

GROUP BY에 대한 조건절을 살펴보자.

WHERE로 조건을 만족하는 행(row)을 필터링하여 선택할 수 있다.

하지만, GROUP BY로 묶는다면 WHERE 조건절을 사용할 수 없다.

title을 기준으로 묶었다고 한다면, WHERE로는 특정 행을 거를 수 없다. 묶였기 때문이다.

따라서 GROUP BY로 필터링을 하고 싶다면 두 가지 중 하나를 선택해야 한다.

 

1. WHERE 조건으로 필터링 한 다음 GROUP BY로 묶는다.

2. GROUP BY로 묶은 다음 HAVING절로 필터링한다.

GROUP BY로 묶었기 때문에 HAVING 조건에는 당연히 집계 함수를 사용해야 한다. 

 

-- GROUP BY: ROLL UP
-- https://dev.mysql.com/doc/refman/8.0/en/group-by-modifiers.html
SELECT
    title,
    AVG(rating) AS avg_rating
FROM full_reviews
GROUP BY title
WITH ROLLUP;

SELECT
    title,
    COUNT(rating) AS rating_count
FROM full_reviews
GROUP BY title
WITH ROLLUP;

ROLLUP은 GROUP BY로 묶은 테이블의 전체 결과를 반환해준다.

첫 번째 예시처럼 AVG(rating)을 하고 ROLLUP을 한다면, 각 그룹의 평균을 보여주고 마지막에 전체 평균을 보여준다.

두 번째 예시처럼 COUNT(rating)을 하고 ROLLUP을 한다면, 각 그룹의 개수를 보여주고 마지막에 전체 개수를 보여준다.

사용 구문은 WITH ROLLUP이다. 

 

SELECT
    released_year,
    genre,
    AVG(rating) AS avg_rating
FROM full_reviews
GROUP BY released_year, genre
WITH ROLLUP;

두 개 이상의 그룹으로 묶어주었을 때도 ROLLUP을 사용할 수 있다.

그럼 각 그룹으로 묶은 소결과에 대해 보여주고, 마지막에 전체 결과를 한 번 더 보여주는 방식으로 진행한다.

 

-- SQL modes
-- https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html
SELECT @@GLOBAL.sql_mode;
SELECT @@SESSION.sql_mode;

SQL에는 mode라는 것이 존재한다. 쉽게 말하면 기본 설정이다.

'0으로 나눌 수 없다'든지, '맞지 않는 자료형은 데이터를 추가할 수 없다'든지 말이다.

mode에는 2가지 종류가 있다. GLOBAL과 SESSION.

GLOBAL과 SESSION의 차이는 마치 컴퓨터 환경 변수의 시스템 설정과 사용자 설정의 차이와 같다.

GLOBAL은 현 시스템의 전체 설정을 변경하는 것이고, SESSION은 말그대로 이번 SESSION 설정을 변경하는 것이다.

 

SET GLOBAL sql_mode = 'modes';
SET SESSION sql_mode = 'modes';

SELECT 3/0;
SHOW WARNINGS;

모드를 바꾸는 것은 가장 첫 두 줄과 같다.

만약 전후를 비교하고 싶다면 하단의 두 줄로 비교해보자.

되도록이면 SESSION 설정을 바꾸는 것을 권장하고, 일반적이라면 그대로 두는 것을 권장한다.

 

STRICT_TRANS_TABLES
-- INSERT 또는 UPDATE 같은 데이터 변경문에서
-- MySQL이 유효하지 않거나 누락된 값을 처리하는 방식을 제어한다
-- string 컬럼을 INT 컬럼으로 바꾸는 등

FULL_GROUP_BY
ONLY_FULL_GROUP_BY
-- GROUP BY할 때 선택할 수 있는 컬럼을 제어한다.
-- SELECT 목록이 집계되지 않은 컬럼을 참조하는 쿼리를 거부한다.

NO_ZERO_IN_DATE
-- 날짜에 0이 있으면 안 됨

mode에는 다양한 mode가 존재한다.

SQL 구문에 대한 설정 사항이기 때문에 엄청나게 많은 mode가 존재할 수밖에 없다.

위에 3종류의 mode는 기본적으로 설정된 mode로 알아두는 것이 좋은 mode다.

첫 번째는 테이블의 데이터를 변경할 때, 자료형이 맞지 않는 경우를 대비하는 mode다.

두 번째는 그룹으로 묶고, 특정 행 데이터에 접근하는 것을 대비하는 mode다.

세 번째는 날짜 데이터에 0을 넣지 못하게끔 대비하는 mode다.

궁금한 사항이 있거나 필요한 경우 직접 docs에서 확인하자.

 └ https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html

'Language > SQL' 카테고리의 다른 글

ERD와 예제 문제 살펴보기: instagram  (0) 2023.12.11
SQL Window functions  (0) 2023.12.10
SQL Many-to-Many Relationship  (0) 2023.10.08
SQL One-to-Many Relationship  (0) 2023.10.02
SQL CONSTRAINT, ALTER  (0) 2023.09.26

One-to-Many Relationship(이하 일대다)에 이은 Many-to-Many Relationship(이하 다대다)이다.

일대다가 다대다보다 훨씬 많이 사용하는 방법이지만, 다대다를 사용하지 않는 것은 아니다.

ㆍ 책의 저자가 여러 명일 수 있고, 각 저자들은 한 권 이상의 책을 집필했을 수 있다.

ㆍ 유튜브 계정은 여러 개일 수 있고, 계정마다 작성한 댓글이 한 개 이상일 수 있다.

이처럼 '다대다'라는 관계도 일대다 못지 않게 많이 사용한다.

 

이번 글에서는 위의 사진에 있는 3개의 테이블을 통한 예제 풀이에 가깝다.

여러 가지 예시와 구문을 이용하여 SQL의 Many-to-Many Relationship을 익혀보자.

 

CREATE TABLE reviewers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL
);

INSERT INTO reviewers (first_name, last_name) VALUES
    ('Thomas', 'Stoneman'),
    ('Wyatt', 'Skaggs'),
    ('Kimbra', 'Masters'),
    ('Domingo', 'Cortes'),
    ('Colt', 'Steele'),
    ('Pinkie', 'Petit'),
    ('Marlon', 'Crafford');

Reviewers 테이블 구조와 삽입할 데이터들이다. 

 

CREATE TABLE reviews (
    id INT PRIMARY KEY AUTO_INCREMENT,
    rating DECIMAL(2, 1),
    series_id INT, 
    reviewer_id INT,
    FOREIGN KEY (series_id) REFERENCES series(id),
    FOREIGN KEY (reviewer_id) REFERENCES reviewers(id)
);

INSERT INTO reviews(series_id, reviewer_id, rating) VALUES
    (1,1,8.0),(1,2,7.5),(1,3,8.5),(1,4,7.7),(1,5,8.9),
    (2,1,8.1),(2,4,6.0),(2,3,8.0),(2,6,8.4),(2,5,9.9),
    (3,1,7.0),(3,6,7.5),(3,4,8.0),(3,3,7.1),(3,5,8.0),
    (4,1,7.5),(4,3,7.8),(4,4,8.3),(4,2,7.6),(4,5,8.5),
    (5,1,9.5),(5,3,9.0),(5,4,9.1),(5,2,9.3),(5,5,9.9),
    (6,2,6.5),(6,3,7.8),(6,4,8.8),(6,2,8.4),(6,5,9.1),
    (7,2,9.1),(7,5,9.7),
    (8,4,8.5),(8,2,7.8),(8,6,8.8),(8,5,9.3),
    (9,2,5.5),(9,3,6.8),(9,4,5.8),(9,6,4.3),(9,5,4.5),
    (10,5,9.9),
    (13,3,8.0),(13,4,7.2),
    (14,2,8.5),(14,3,8.9),(14,4,8.9);

Reviews 테이블 구조와 삽입할 데이터들이다. 

 

series_id와 reviewer_id는 각각 series 테이블과 reviewers 테이블의 외래키다.

 

CREATE TABLE series (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(100),
    released_year YEAR,
    genre VARCHAR(100)
);

INSERT INTO series (title, released_year, genre) VALUES
    ('Archer', 2009, 'Animation'),
    ('Arrested Development', 2003, 'Comedy'),
    ("Bob's Burgers", 2011, 'Animation'),
    ('Bojack Horseman', 2014, 'Animation'),
    ("Breaking Bad", 2008, 'Drama'),
    ('Curb Your Enthusiasm', 2000, 'Comedy'),
    ("Fargo", 2014, 'Drama'),
    ('Freaks and Geeks', 1999, 'Comedy'),
    ('General Hospital', 1963, 'Drama'),
    ('Halt and Catch Fire', 2014, 'Drama'),
    ('Malcolm In The Middle', 2000, 'Comedy'),
    ('Pushing Daisies', 2007, 'Comedy'),
    ('Seinfeld', 1989, 'Comedy'),
    ('Stranger Things', 2016, 'Drama');

Series 테이블 구조와 삽입할 데이터들이다. 

 

 

첫 번째 예제는 위와 같다.

첫 번째 예제를 포함한 밑에 존재하는 총 7개의 예제 중 일부는, 전문을 포함하지 않는다.

다대다 관계 특성상 JOIN 구문을 이용하여 결과를 확인하면 return 값이 길어지는 경우가 존재한다.

따라서 전체 결과를 첨부하기에는 무리가 있다고 판단해 적당한 길이에서 값을 잘라서 첨부한다.

일부분을 보고서 특정 패턴을 파악하고 분석할 줄 아는 것도 중요하다고 생각한다. 

 

위의 예제는 테이블을 적당히 JOIN해서 title과 rating만을 추출한다.

Archer가 하나 raintg이 여러 개인 일대다 관계가 아니다.

rating은 각 사용자의 평점이 담긴 column으로써 다대다 관계를 만족한다.

물론, 위와 같은 경우 어떤 사용자가 얼마의 평점을 달았는지 알 수 없어 경우에 따라 쓰임이 바뀐다고 생각한다.

하지만 어디까지나 다대다 관계의 SQL에 익숙해지기 위한 예제임을 감안하자. 

 

 

-- Exercise
SELECT title, rating
FROM series
JOIN reviews ON series.id = reviews.series_id;

reviews 테이블의 series_id는 외래키로, seires의 id 값을 참조한다.

따라서 두 키를 사용하여 INNER JOIN을 하고, title과 rating을 반환하면 된다.

 

두 번째 예제는 위와 같다.

특정 테이블 값을 적절히 이용하여 title과 평균 rating을 반환한다.

 

-- Exercise
SELECT
    title,
    AVG(rating) AS avg_rating
FROM series
JOIN reviews ON series.id = reviews.series_id
GROUP BY title
ORDER BY avg_rating;

title을 담고 있는 테이블은 series 테이블이고, rating 정보는 reviews 테이블이 담고 있다.

따라서 외래키를 이용하여 두 테이블을 INNER JOIN한다.

각 title에 대해서 많은 rating 정보를 담고 있으니 title을 기준으로 GROUP BY한다.

그리고 title과 AVG(rating)으로 평균값을 반환한다.

이때 두 번째 예제를 잘 보면, avg_rating의 컬럼명을 설정해주었고, 오름차순으로 정렬된 상태다.

따라서 AS를 이용해 컬럼명을 지정하고, ORDER BY로 정렬까지 해주어야 한다.

작은 요소도 꼼꼼이 보는 게 진짜 실력이다.

 

세 번째 예제는 위와 같다.

첫 번째 예제와 유사하게 first_name, last_name에 따른 rating 정보를 반환한다.

 

-- Exercise
SELECT first_name, last_name, rating
FROM reviewers
JOIN reviews ON reviewers.id = reviews.reviewer_id;

first_name과 last_name은 reviewers 테이블에 담겨있다.

reviews 테이블의 reviewers 외래키인 id 값을 참조하여 INNER JOIN으로 두 테이블을 연결한다.

그리고 first_name, last_name, rating을 반환하면 된다.

 

네 번째 예제는 위와 같다.

리뷰가 하나도 없는 series의 title을 찾는 예제다.

 

-- Exercise
SELECT title AS unreviewed_series
FROM series
LEFT JOIN reviews ON series.id = reviews.series_id
WHERE rating IS NULL;

INNER JOIN을 하면 양 테이블에 모두 들어있는, 교집합을 찾는 JOIN 방법이다.

NULL 값을 알기 위해서는 LEFT JOIN이나 RIGHT JOIN을 해야 한다.

series 테이블을 reviews 테이블을 LEFT JOIN(혹은 RIGHT JOIN)으로 이어준다.

그리고 rating이 NULL인 경우에만 title을 선택한다.

물론 AS를 이용하여 컬럼명을 변경하는 것을 잊지 말자..

 

리뷰가 없다는 이야기는 reviews 테이블에 정보가 없다는 말과 같다.

reviews 테이블은 총 4가지, id, rating, series_id, reviewer_id가 있다.

즉 위의 4가지 colulmn 정보가 하나도 없다는 말과 같다.

따라서 WHERE rating IS NULL외에 나머지 3개의 열에 대해서 IS NULL을 해도 결과는 동일하다.

 

다섯 번째 예제는 위와 같다.

장르별로 평균 rating을 계산하여 반환한다.

 

-- Exercise
SELECT
    genre,
    AVG(rating) AS avg_rating
FROM series
JOIN reviews ON series.id = reviews.series_id
GROUP BY genre;

series와 reviews를 외래키로 참조한 다음, genre를 GROUP BY로 묶는다.

그리고 장르별로 AVG 평균을 계산한다.

확실하게 해주길 원한다면 genre를 기준으로 ORDER BY를 해도 된다.

 

 

여섯 번째 예제는 위와 같다.

리뷰를 작성한 사용자 데이터를 다양한 평점을 기준으로 반환한다.

리뷰 개수, 최솟값, 최댓값, 평균값, 그리고 ACTICE(작성) 여부를 포함하는 결과다.

 

-- Exercise
SELECT
    first_name,
    last_name,
    COUNT(rating) AS COUNT,
    IFNULL(MIN(rating), 0) AS MIN,
    IFNULL(MAX(rating), 0) AS MAX,
    IFNULL(AVG(rating), 0) AS AVG,
    IF(COUNT(rating) > 0, 'ACTIVE', 'INACTIVE') AS STATUS
FROM reviewers
LEFT JOIN reviews ON reviewers.id = reviews.reviewer_id
GROUP BY first_name, last_name;

first_name과 last_name이 있는 reviewers 테이블과 rating이 있는 reviews 테이블을 연결한다.

NULL 값도 포함해야 하기 때문에 LEFT JOIN 혹은 RIGHT JOIN을 해준다.

rating을 COUNT(), MIN(), MAX(), AVG() 함수로 씌워준다. 이때 NULL 값을 0으로 출력하기 위해 IFNULL을 사용한다.

CASE WHEN THEN ELSE END를 이용하여  ACTIVE와 INACTIVE를 나타내도 된다.

하지만 True or False만을 나누는 것은 IF만을 사용해도 괜찮다.

 

일곱 번째 예제는 위와 같다.

모든 테이블을 연결하여 title, rating, reviewer를 반환하는 결과다.

 

-- Exercise
SELECT
    title,
    rating,
    CONCAT(first_name, ' ', last_name) AS reviewer
FROM reviews
JOIN series on series.id = reviews.series_id
JOIN reviewers ON reviewers.id = reviews.reviewer_id
ORDER BY title;

INNER JOIN과 외래키를 사용하여 세 테이블을 모두 연결한다.

JOIN을 하나의 테이블에서 2개 이상 선택하여 연결하는 것이 가능하다.

CONCAT으로 문자열을 이을 때, 공백도 있음에 유의하자. 

'Language > SQL' 카테고리의 다른 글

SQL Window functions  (0) 2023.12.10
SQL View, Mode  (0) 2023.10.11
SQL One-to-Many Relationship  (0) 2023.10.02
SQL CONSTRAINT, ALTER  (0) 2023.09.26
SQL Logical Operator  (0) 2023.09.25

Relations 종류

관계형 데이터베이스(RDB)에는 여러 종류의 관계가 있다.

이때 관계는 '테이블' 사이의 관계 혹은, 테이블의 '데이터' 사이의 관계를 기준으로 형성하는 관계를 말한다.

하나와 하나의 값끼리만 연관있는 관계가 One-to-One Relations,

예를 들면 '한 명의 고객'은 '하나의 평점'밖에 남기지 못하는 상황과 같다.

일반적으로 가장 많이 사용하는 One-to-Many Relations,

예를 들면 '하나의 도서'에는 '여러 개의 리뷰'가 달려있는 상황과 같다.

일대다 다음으로 널리 사용하는 Many-to-Many Relations,

예를 들면 '다양한 도서 종류'에 따른 '다양한 저자'가 있는 상황과 같다.

하나의 책에 한 명의 저자만 있으란 법도 없고, 한 저자가 하나의 책만 출판하라는 법도 없다.

이번 글에서는 One-to-Many를 기준으로 살펴본다.

 

-- One to Many Relations: CUSTOMERS & ORDERS
CREATE TABLE customers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(50)
);

CREATE TABLE orders(
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_date DATE,
    amount DECIMAL(8, 2),
    customer_id INT,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

INSERT INTO customers (first_name, last_name, email) 
VALUES ('Boy', 'George', 'george@gmail.com'),
       ('George', 'Michael', 'gm@gmail.com'),
       ('David', 'Bowie', 'david@gmail.com'),
       ('Blue', 'Steele', 'blue@gmail.com'),
       ('Bette', 'Davis', 'bette@aol.com');
       
INSERT INTO orders (order_date, amount, customer_id)
VALUES ('2016-02-10', 99.99, 1),
       ('2017-11-11', 35.50, 1),
       ('2014-12-12', 800.67, 2),
       ('2015-01-03', 12.50, 2),
       ('1999-04-11', 450.25, 5);

One-to-Many Relations 예제를 확인하기 위해 생성한 테이블과 데이터다.

한 도서 사이트가 있다고 했을 때, 사용자 정보를 저장하는 customers 테이블이 있다.

사용자가 도서를 구매할 때, 날짜와 가격 그리고 수량을 저장하는 orders 테이블이 있다.

만약 이 두 값을 독립한 2개의 테이블이 아니라 1개의 테이블로 만들었다고 생각해보자.

 

1개의 테이블 column은 다음과 같이 구성할 것이다.

id, first_name, last_name, email, order_date, amount.

물론 이렇게 관리해도 문제는 없다. 하지만 중복된 데이터가 너무 많아진다.

예를 들어서 Blue Steele씨가 매일 한 권의 도서를 구매한다고 해보자.

그럼 테이블은 온톤 Blue Steele이라는 데이터로 도배가 될 것이다.

이를 방지하기 위해 테이블을 쪼갠 것이고, 이를 전문 용어로 '정규화(Normalization)'라고 한다.

 

FOREIGN KEY (customer_id) REFERENCES customers(id)

CREATE로 테이블을 생성하고, INSERT로 데이터를 추가하는 과정에서 핵심은 위의 한 줄이다.

위처럼 2개의 테이블로 정규화를 진행할 때, 두 테이블은 연결고리가 있어야 한다.

이 연결고리, 즉 한 테이블에서 다른 테이블을 참조하는 값을 '외래키(Foreign Key)'라고 한다.

정리하자면 FOREIGN KEY는 '두 테이블 간 연결을 참조하는 데이터 column'이다.

실제로 orders 테이블의 customer_id를 FOREIGN KEY로 선언하였다.

이러면 참조하는 custormers 테이블의 id 값에 존재하지 않는 데이터는 추가할 수 없다.

 

두 개의 테이블을 벤 다이어그램으로 나타내면 위와 같다.

물론 정확하게 따지자면 'id' 값을 교집합으로 하는 벤 다이어그램을 그려야 한다.

하지만 편의상 두 개의 테이블이 있다는 것을 보여주기 위해 따로 그렸다.

두 개가 교집합을 만들고, JOIN 종류에 따라 어느 값을 가져오는지는 밑에서 따라가면서 그린다.

 

SELECT id FROM customers WHERE last_name = 'George';
SELECT * FROM orders WHERE customer_id = (search_id);

SELECT *
FROM  orders
WHERE customer_id = (
        SELECT id
        FROM customers
        WHERE last_name = 'George'
);

JOIN에 대해 들어가기 전, 간단한 코드다.

가장 상단의 코드 2줄은 George가 주문한 책의 전체 정보를 확인하기 위한 query를 작성했다.

첫 번째 예시에서 customers 테이블에서 이름이 George인 id를 찾는다.

그리고 두 번째 예시에서 찾은 id를 기준으로 orders 테이블의 모든 column을 반환한다.

이것을 Sub query를 사용하여 세 번째 예시처럼 작성할 수도 있다.

그리 추후에 설명할 JOIN으로 간단하게 해결할 수 있다.

 

-- Cross Join (Cartesian Join)
SELECT * FROM customers, orders;

FROM으로 호출하는 테이블을 쉼표로 여러 개 작성할 수 있다.

이러면 모든 테이블의 데이터를, 모든 경우의 수로 조합하여 반환한다.

예를 들면 Customers가 5개의 행, Orders가 5개의 행이 있으니, 결과는 25개의 행을 반환한다.

이런 방법을 Cross Join 또는 Cartesian Join이라고 한다.

 

 

 

위의 벤 다이어그램은 Customers 테이블과 Orders 테이블의 실제 관계를 그렸다.

위처럼 교집합이 있을 것이고, 이를 이용한 Join을 Inner Join이라고 한다.

MySQL에서 JOIN 키워드를 사용할 때 default가 INNER JOIN이다.

따라서 INNER를 따로 명시할 필요는 없고, JOIN 적어도 상관없다.

 

-- Inner Join
-- JOIN keyword default: INNER JOIN
SELECT *
FROM orders
JOIN customers ON customers.id = orders.customer_id;

SELECT *
FROM customers
JOIN orders ON customers.id = orders.customer_id;

JOIN 구문은 'FROM {특정_테이블} JOIN {다른_테이블} ON {교집합_조건}' 형태로 작성한다.

이때 두 가지 구문을 작성했다.

첫 번째로 orders를 기준으로 customers의 id를 비교하는 구문과,

두 번째로 customers를 기준으로 orders의 id를 비교하는 구문이다.

당연하게도 교집합을 찾는 구문이기에 두 query의 결과는 동일하다.

 

-- Inner Join GROUP BY
SELECT first_name, last_name, SUM(amount) AS 'total($)'
FROM customers
JOIN orders ON orders.customer_id = customers.id
GROUP BY first_name, last_name;

JOIN으로 추출한 교집합에도 GROUP BY로 묶는 것이 가능하다.

 

LEFT JOIN(왼쪽), RIGHT JOIN(오른쪽)

JOIN에는 크게 세 가지가 있다. INNER, LEFT, RIGHT.

LEFT JOIN은 우선 왼쪽에 해당하는 테이블의 데이터를 전부 작성한다.

그리고 JOIN(교집합)에 해당하는 데이터가 있으면 채워넣는 방식이다.

RIGHT JOIN 반대로 오른쪽에 해당하는 테이블의 데이터를 전부 작성하고, JOIN(교집합)을 채워넣는다.

 

그럼 Customers를 왼쪽에 두고 LEFT JOIN을 하는 거나,

Customers를 오른쪽에 두고 RIGHT JOIN을 하는 거나 똑같지 않은가 하는 의문이 들 수 있다.

맞다. 둘은 같은 JOIN 구문으로, 같은 결과를 반환한다.

 

-- Left Join
-- 비활성 데이터를 찾을 떄 유용
SELECT first_name, last_name, order_date, amount
FROM customers
LEFT JOIN orders ON orders.customer_id = customers.id;

SELECT first_name, last_name, order_date, amount
FROM orders
LEFT JOIN customers ON orders.customer_id = customers.id;

첫 번째 예시는 customers를 왼쪽에 두고, orders 테이블과 id를 기준으로 LEFT JOIN을 하는 구문이다.

두 번째 예시는 orders를 왼쪽에 두고, customers 테이블과 id를 기준으로 LEFT JOIN을 하는 구문이다.

비슷해 보이지만, 결과의 행 개수가 다르다.

 

-- LEft Join GROUP BY
SELECT first_name, last_name, IFNULL(SUM(amount), 0) as total_spent
FROM customers
LEFT JOIN orders ON orders.customer_id = customers.id
GROUP BY first_name, last_name;

SELECT first_name, last_name,
    CASE
        WHEN SUM(amount) IS NULL THEN 0.00
        ELSE SUM(amount)
    END as total_spent
FROM customers
LEFT JOIN orders ON orders.customer_id = customers.id
GROUP BY first_name, last_name;

첫 번째 예시와 두 번째 예시 모두 LEFT JOIN과 GROUP BY를 섞은 구문 예제다.

IFNULL(value, instead) 함수는 value가 NULL이라면 instead를 반환하고, 아니라면 value를 반환한다.

LEFT JOIN을 했을 때, 교집합이 없는 데이터도 있을 수 있어, NULL 값을 처리하는 방법이다.

첫 번째 예시와 두 번째 에시 모두 결과는 같고, 구현하는 방법이 다른 예제다.

 

-- Right Join
SELECT first_name, last_name, order_date, amount
FROM customers
RIGHT JOIN orders ON customers.id = orders.customer_id;

RIGHT JOIN은 위에서 언급했듯, LEFT JOIN과 위치만 바뀐 JOIN 구문이다.

위의 예시는 FROM orders LEFT JOIN customers와 사실상 같은 구문이다.

 

DELETE FROM customers WHERE last_name = 'George';

만약 다른 테이블이 참조하고 있는, 특정 테이블의 데이터를 삭제한다고 해보자.

위의 구문처럼 last_name이 George인 모든 행을 삭제하려고 한다.

그러면 Error Code 1451처럼 다른 테이블이 외래키로 참조 중이라 삭제할 수 없다는 에러를 띄운다.

이를 방지하기 위해서 CASCADE라는 키워드를 사용하면 된다.

 

-- CASCADE preventation
CREATE TABLE orders(
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_date DATE,
    amount DECIMAL(8, 2),
    customer_id INT,
    FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE
);

테이블을 선언할 때, FOREIGN KEY의 하위 옵션으로 ON DELETE CASCADE를 적는다.

이러면 참조하고 있는 데이터와 연관된 모든 행을 제거한다.

 

-- EXAMPLE: Make table
CREATE TABLE students (
    id INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50)
);

CREATE TABLE papers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(50),
    grade TINYINT,
    student_id INT,
    FOREIGN KEY (student_id) REFERENCES students(id)
);

INSERT INTO students (first_name)
VALUES ('Caleb'), ('Samantha'), ('Raj'), ('Carlos'), ('Lisa');

INSERT INTO papers (student_id, title, grade)
VALUES
(1, 'My First Book Report', 60),
(1, 'My Second Book Report', 75),
(2, 'Russian Lit Through The Ages', 94),
(2, 'De Montaigne and The Art of The Essay', 98),
(4, 'Borges and Magical Realism', 89);

다양한 예제를 가지고 JOIN을 사용해보자.

위의 코드는 Customers와 Orders 테이블 외에 사용할 또 다른 예제 테이블이다.

학생들(studetns)에게 과제(papers)를 내주고, 어떤 이름으로 어느 점수를 받았는지에 대한 테이블이다.

 

-- EXAMPLE: Question
SELECT first_name, title, grade
FROM students
JOIN papers ON students.id = papers.student_id
ORDER BY grade DESC;

SELECT first_name, title, grade
FROM students
LEFT JOIN papers ON students.id = papers.student_id;

SELECT
    first_name,
    IFNULL(title, 'MISSING'),
    IFNULL(grade, 0)
FROM students
LEFT JOIN papers ON students.id = papers.student_id;

SELECT
    first_name,
    IFNULL(AVG(grade), '0') as average
FROM students
LEFT JOIN papers ON students.id = papers.student_id
GROUP BY first_name
ORDER BY average DESC;

SELECT
    first_name,
    IFNULL(AVG(grade), '0') AS average,
    CASE
        WHEN IFNULL(AVG(grade), 0) >= 75 THEN 'PASSING'
        ELSE 'FAILING'
    END AS passing_status
FROM students
LEFT JOIN papers ON students.id = papers.student_id
GROUP BY first_name
ORDER BY average DESC;

일반적인 JOIN, IFNULL을 사용한 데이터 표시, GROUP BY와 ORDER BY와 혼합 사용 예제다.

또한, CASE-THEN으로 조건에 따른 데이터 표시 활용도 포함하고 있다.

'Language > SQL' 카테고리의 다른 글

SQL View, Mode  (0) 2023.10.11
SQL Many-to-Many Relationship  (0) 2023.10.08
SQL CONSTRAINT, ALTER  (0) 2023.09.26
SQL Logical Operator  (0) 2023.09.25
SQL DATA TYPE  (0) 2023.09.19

CONSTRAINT는 '제약'이라는 뜻으로, MySQL에서 테이블을 생성할 때 걸 수 있는 제약 조건이다.

또한 ALTER는 테이블을 수정하는 SQL 구문으로, 위의 두 구문 모두 테이블 관련 구문이다.

따라서 이번 글에서는 테이블을 생성하는 코드를 주로 나아간다.

혹시라도 언제 Tab을 사용하고, 몇 칸의 spacebar를 띄워야할지 헷갈린다면 단축키를 사용하자.

Ctrl + B를 누르면 MySQL에서 사용하는 형태로 정렬해준다.

 

-- UNIQUE
CREATE TABLE uni (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL UNIQUE
);

UNIQUE 키워드다.

'해당 값이 유일하게 존재해야 한다'를 명시하는 키워드로 PK(기본키)와 쓰임이 비슷하다.

 

하지만 PK는 NULL을 허용하지 않고, UNIQUE는 NULL을 허용한다.

이런 관점에서 보았을 때, UNIQUE가 PK보다 상위 개념이다.

하지만 그렇다고 UNIQUE에 NOT NULL을 사용할 수 없다는 말은 아니다.

 

-- UNIQUE
CREATE TABLE companies (
    supplier_id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    phone VARCHAR(15) NOT NULL UNIQUE,
    address VARCHAR(255) NOT NULL
);

PK가 없다고 해서 UNIQUE를 선언할 수 없는 것은 아니다. 차이를 위해 두 값 모두 선언했다. 

DESC companies를 사용해 companies의 테이블 구조를 살펴보면 정확하게 알 수 있다.

PK로 선언한 column은 Key 값에 PRI가 들어가고, UNIQUE로 선언한 column은 Key 값에 UNI가 들어간다.

 

-- CHECK
CREATE TABLE users (
    name VARCHAR(50),
    age INT CHECK (age > 18)
);

CHECK를 이용하여 테이블을 생성할 때 제약을 걸 수 있다.

첫 번째 예시처럼 CHECK (age > 18)하면 age가 19일 때부터 입력이 가능하다. 

 

실제로 18 이하의 값을 users 테이블에 입력하면 위와 같은 에러가 뜬다.

에러 코드를 살펴보면 'users_chk_1'으로 나온다.

users 테이블에서 처음으로 뜬 에러 코드라는 이야기이다.

이때 에러 코드를 직접 정의할 수 있다면, 어디서 에러가 떴는지 훨씬 보기 쉬울 것이다.

그럴 때 사용하는 키워드가 CONSTRAINT이다.

 

-- CHECK ERROR NAME SETUP
CREATE TABLE users (
    username VARCHAR(50) NOT NULL,
    age INT,
    CONSTRAINT age_over_18 CHECK (age > 18)
);

CONSTRAINT {error_name} CHECK {condition}의 형태로 작성한다.

condition에 부합하지 않는 값이 테이블에 들어오면 error_name을 기반으로 에러를 반환한다.

 

 

users_chk_1으로 뜨던 에러 문구가 age_over_18로 뜨는 것을 볼 수 있다.

 

-- CHECK
CREATE TABLE palindromes (
    word VARCHAR(100) CHECK (REVERSE(word) = word)
);

-- CHECK ERROR NAME SETUP
CREATE TABLE palindromes (
    word VARCHAR(100),
    CONSTRAINT is_palindromes CHECK (REVERSE(word) = word)
);

CHECK를 활용한 팰린드롬 예제다.

입력받는 word가 뒤집어도 같은지 아닌지 CHECK와 CONSTRAINT로 제약을 건 코드다.

 

-- DOUBLE COLUMN CONSTRAINT
CREATE TABLE companies (
    name VARCHAR(255) NOT NULL,
    address VARCHAR(255) NOT NULL,
    CONSTRAINT name_address UNIQUE (name, address)
);

CONSTRAINT 키워드를 사용하여 위에서 말한 UNIQUE를 선언하는 것도 가능하다.

CHECK처럼 반드시 이 조건을 지켜야 한다는 의미보다는, 중복 제거에 가깝다.

위 예시처럼 UNIQUE(name, address)하면 (name, address) 조합이 유일해야 한다는 의미다.

 

만약 (Steve, LA)라는 조합으로 두 번 데이터를 넣었다고 해보자.

그럼 위와 같은 에러 문구를 반환한다.

 

-- DOUBLE COLUMN CONSTRAINT
CREATE TABLE stock_market (
    purchase_price INT NOT NULL,
    sale_price INT NOT NULL,
    CONSTRAINT pprice_lt_sale_price CHECK (purchase_price <= sale_price)
);

CONSTRAINT를 사용할 때, 단순 수치를 비교하는 것 외에 열끼리 비교하는 것도 가능하다.

위의 주식 거래(stock_market) 테이블을 만들고, 구매가(purchase_price)와 판매가(sale_price) 열을 만들었다.

이 주식 거래소는 고객들이 손해를 보는 것을 눈뜨고는 볼 수 없어 특단의 조치를 내렸다.

자신의 구매가보다 판매가가 낮다면, 팔 수 없게 하는 것이다.

CHECK (purchase_price <= sale_price)를 통해 구현 가능하다.

 

-- ALTER: ADD COLUMN
ALTER TABLE companies
ADD COLUMN phone VARCHAR(15);

ALTER TABLE companies
ADD COLUMN employee_count INT NOT NULL DEFAULT 1;

ALTER는 테이블의 데이터를 변경할 수 있는 키워드다.

테이블 이름, column 타입, column 이름 등 다양한 것들을 삭제, 추가, 수정할 수 있다.

ALTER TABLE {table_name} ADD *COLUMN {column_name} {column_type}이 기본 구문이다.

COLUMN 뒤에는 실제로 추가할 이름과 자료형을 적어주면 된다.

NOT NULL, DEFAULT, PRIMARY KEY 등 일반적으로 생성할 때와 동일하다.

이때 ADD 뒤에 COLUMN을 적지 않아도 상관없다.

 

-- ALTER: DROP COLUMN
ALTER TABLE companies DROP COLUMN phone;

ALTER TABLE {table_name} DROP *COLUMN {column_name}이 기본 구문이다.

삭제하려는 테이블 이름에서 삭제하려는 column 이름을 적으면 된다. 

이때 DROP 뒤에 COLUMN을 적지 않아도 상관없다.

 

-- ALTER: RENAME TABLE
RENAME TABLE companies TO suppliers;
ALTER TABLE suppliers RENAME TO companies;

-- ALTER: RENAME COLUMN
ALTER TABLE companies
RENAME COLUMN name TO username;

RENAME TO로 기존에 존재하는 column 이름을 바꿀 수 있다.

첫 번째 예시와 두 번째 예시는 테이블의 이름을 바꾸는 예제다.

RENAME TABLE ~ TO ~의 형식으로 쓰나, ALTER TABLE ~ RENAME TO ~로 쓰나 결과는 동일하다. 

세 번째 예시는 column의 이름을 바꾸는 예제다.

companies라는 테이블에 존재하는 name을 username으로 바꾼다. 

이때 ADD, DROP과 달리 COLUMN을 적지 않으면 SyntaxError가 발생한다.

 

-- ALTER: MODIFY: 타입 변환만 가능
ALTER TABLE companies MODIFY username VARCHAR(100) DEFAULT 'unknown';

-- ALTER: CHANGE: 이름 변경까지 가능
ALTER TABLE companies CHANGE username name VARCHAR(255);

ALTER로 수정을 할 때 MODIFY와 CHANGE를 사용하여 자료형을 바꿀 수 있다.

첫 번째 예시는 특정 column의 자료형(타입)만을 변경하는 예시다.

MODIFY를 사용하면 바꾸고자하는 자료형만 명시하면 된다.

두 번째 예시는 특정 column의 자료형(타입)뿐만 아니라 이름까지 변경하는 예시다.

CHANGE를 사용하면 바꾸기 전의 이름, 바꾼 후의 이름, 자료형을 명시해야 한다.

 

-- ALTER 제약 조건
ALTER TABLE companies ADD CONSTRAINT NOT_HOME CHECK (address != 'house');
ALTER TABLE companies DROP CONSTRAINT NOT_HOME;

ALTER를 통한 ADD, DROP 등의 다양한 작업에도 CONSTRAINT를 통한 제약 역시 가능하다.

생각해보면 당연한 구문이다.

CONSTRAINT는 테이블을 생성할 때 column에 제약을 거는 기능이다.

ALTER는 테이블에 column을 추가하거나 삭제하는 기능이니, 당연히 제약을 걸 수 있다.

'Language > SQL' 카테고리의 다른 글

SQL Many-to-Many Relationship  (0) 2023.10.08
SQL One-to-Many Relationship  (0) 2023.10.02
SQL Logical Operator  (0) 2023.09.25
SQL DATA TYPE  (0) 2023.09.19
SQL 집계 함수  (0) 2023.09.17
-- New Table
CREATE TABLE people (
    name VARCHAR(100),
    birthdate DATE,
    birthtime TIME,
    birthdt DATETIME
);
 
INSERT INTO people (name, birthdate, birthtime, birthdt)
VALUES
('Elton', '2000-12-25', '11:00:00', '2000-12-25 11:00:00'),
('Lulu', '1985-04-11', '9:45:10', '1985-04-11 9:45:10'),
('Juan', '2020-08-15', '23:59:00', '2020-08-15 23:59:00'),
('Hazel', '2022-10-05', '14:58:29', '2022-10-05 14:58:29');

밑에서 실행할 예제는 이전 글에서 사용한 테이블과 데이터를 사용한다.

또한, 추가적으로 사용하는 테이블과 데이터가 있어서 위의 코드를 참고하자.

Colt Steele의 SQL 데이터를 참고했음을 알린다.

 

NOT

부정을 의미하는 NOT 예약어이다.

어떤 행동의 반대되는 결과를 반환하거나 수행한다.

 

-- NOT EQUAL
SELECT title FROM books WHERE released_year != 2017;
SELECT title FROM books WHERE NOT released_year = 2017;
SELECT title, author_lname FROM books WHERE author_lname != 'Gaiman';
SELECT title, author_lname FROM books WHERE NOT author_lname = 'Gaiman';

같지 않다의 의미로 사용할 수 있다. NOT을 사용해도 되고, != 연산자를 사용해도 된다.

첫 번째와 두 번째 예시가 동일하고, 세 번째와 네 번째 에시가 동일하다.

첫 번째 예시는 realeased_year가 2017이 아닌 것만 골라서 title을 반환한다.

세 번째 예시는 author_lname이 Gaiman이 아닌 경우에만 골라서 title과 author_lname을 반환한다.

 

-- NOT LIKE
SELECT title FROM books WHERE title NOT LIKE '% %';
SELECT title FROM books WHERE title NOT LIKE '%e%';
SELECT author_fname FROM books WHERE author_fname NOT LIKE 'da%';

와일드카드를 사용하는 LIKE에도 사용할 수 있다.

첫 번째 예시는 title에서 공백을 포함하지 않는 경우에만 title을 반환한다.

두 번째 예시는 title에서 e를 포함하지 않는 경우에만 title을 반환한다.

세 번째 예시는 author_fname에서 da로 시작하지 않는 경우에만 author_fname을 반환한다.

LIKE는 부정을 뜻하는 ! 연산을 사용할 수 없다. !LIKE가 아닌 NOT LIKE만 사용 가능하다.

 

-- COMPARE OPERATORS --
-- GREATER THAN
SELECT title, released_year FROM books WHERE released_year > 2000;
SELECT title, pages FROM books WHERE pages > 500;
SELECT 99 > 1; -- boolean: 1
SELECT 1 > NULL; -- NULL

비교 연산자 >, <, >=, <= 네 가지다.

등호(=)를 붙일 때는 반드시 오른편에 써야한다. =>는 인식하지 못하고 >=로 작성해야 한다.

첫 번째 예시는 realeased_year가 2000보다 큰 경우에만 title과 released_year를 반환한다.

세 번째 예시는 단순한 수식이다. 비교 연산만을 SELECT로 연산하면 boolean을 반환한다.

즉, 99가 1보다 큰 것은 사실이므로, 1을 반환한다.

네 번째 예시에서 NULL은 비교 대상이 될 수 없다. 따라서 NULL을 반환한다.

 

-- LESS THAN
SELECT title, released_year FROM books WHERE released_year < 2000 ORDER BY released_year;
SELECT title, pages FROM books WHERE pages < 200;

-- GREATER THAN or EQUAL TO
SELECT title, released_year FROM books WHERE released_year >= 2010 ORDER BY released_year;

-- LESS THAN or EQUAL TO
SELECT title, released_year FROM books WHERE released_year <= 2010 ORDER BY released_year DESC;

위에서 사용한 GREATER THAN과 방향이 반대이다.

>나 <를 사용할 경우 비교 대상을 포함하지 않는다. 이때 =를 사용하면 비교 대상을 포함하여 연산한다.

많은 프로그래밍 언어에서 사용하고 있는 규칙과 동일하다.

 

-- LOGICAL OPERATORS --
-- AND
SELECT title, author_lname, released_year
FROM books
WHERE author_lname = 'Eggers'
    AND released_year > 2010;
    
SELECT title, author_lname, released_year
FROM books
WHERE author_lname = 'Eggers'
    AND released_year >= 2010
    AND title LIKE '%novel%';

SELECT title, pages
FROM books
WHERE CHAR_LENGTH(title) > 30
    AND pages > 500;

논리 연산자인 AND, OR다. 부울대수에 나오는 개념으로 진리값(boolean)을 활용한 논리 판별 연산자이다.

쉽게 말하면 AND는 두 값이 모두 참일 때만 참이고, OR는 두 값 중 하나라도 참일 때 참을 반환한다.

첫 번째 예시는 author_lname이 Eggers이면서 released_year가 2010보다 큰 경우에만 3개의 열을 반환한다.

두 번째 예시는 WHERE에서 AND를 두 번 이상 사용하였다. 이처럼 논리 연산자를 여러 개 사용하는 것도 가능하다.

세 번째 예시는 title의 길이가 30보다 길면서 pages가 500보다 큰 경우에만 title과 pages를 반환한다.

 

SELECT title, released_year FROM books
WHERE released_year % 2 = 1 AND released_year >= 2000;

다른 프로그래밍 언어와 마찬가지로 SQL에서도 % 연산자를 사용할 수 있다.

%는 나머지 연산자(modular)로 A % B라면, A를 B로 나눈 나머지를 반환한다.

따라서 위의 예시는 realeased_year가 홀수이면서 2000보다 큰 경우에만 title과 released_year를 반환한다.

 

-- OR
SELECT title, author_lname, released_year
FROM books
WHERE author_lname = 'Eggers'
    OR released_year > 2010;

SELECT title, pages
FROM books
WHERE pages < 200
    OR title LIKE '%stories%';

AND는 모든 조건을 만족해야하지만, OR는 조건을 하나라도 만족하면 된다.

AND에서는 author_lname이 Eggers면서 released_year가 2010보다 큰 경우만 데이터를 뽑아냈다.

첫 번째 예시는 OR에서는 Eggers이거나 2010보다 큰 경우, 하나라도 만족할 경우 데이터를 뽑아낸다.

두 번째 예시는 와일드카드를 사용하여 OR 조건으로 데이터를 뽑아냈다.

 

-- BETWEEN
SELECT title, released_year FROM books WHERE 2004 <= released_year AND released_year <= 2014;
SELECT title, released_year FROM books WHERE released_year BETWEEN 2004 AND 2014;

SELECT title, released_year FROM books WHERE 2004 > released_year OR released_year > 2014;
SELECT title, released_year FROM books WHERE released_year NOT BETWEEN 2004 AND 2014;

첫 번째 예시처럼 released_year가 2004보다 크거나 같고 2014보다 작거나 같은 경우를 뽑을 수 있다.

이를 두 번째 예시처럼 간단하게 BETWEEN을 사용하여 표현할 수 있다. 이때 두 경곗값을 포함한다.

BETWEEN도 처음에 사용한 NOT을 사용할 수 있다.

 

-- A note about comparing date
SELECT * FROM people WHERE birthdate < '2005-01-01';
SELECT * FROM people WHERE YEAR(birthdate) < 2005;
SELECT * FROM people WHERE birthtime > '09:00:00';
SELECT * FROM people WHERE HOUR(birthtime) > 9;

비교 연산자(>, < 등)를 사용하여 날짜를 비교하는 것도 가능하다.

첫 번째와 세 번째 예시처럼 SQL에서는 기본적으로 문자열과 날짜를 비교하는 것을 지원한다.

두 번째와 네 번째 예시처럼 날짜 데이터와 숫자를 비교하는 것 또한 가능하다.  

 

-- 기본 구문
CAST(sample_date AS want_type)

-- 예시
SELECT CAST('09:00:00' AS TIME);

CAST 함수를 사용하여 데이터의 자료형을 변경할 수 있다.

예를 들어 '09:00:00'은 문자열 자료형이다. 이를 TIME 자료형으로 변경할 수 있다.

 

SELECT * FROM people WHERE birthtime BETWEEN '12:00:00' AND '16:00:00';

SELECT * FROM people WHERE birthtime
BETWEEN CAST('12:00:00' AS TIME) AND CAST('16:00:00' AS TIME);

SELECT * FROM people WHERE HOUR(birthtime)
BETWEEN 12 AND 16;

BETWEEN과 CAST를 활용한 날짜 데이터 비교다.

실제로 SQL 공식 docs에서도 비교 연산자보다는 BETWEEN을 통한 비교를 권장하고 있다.

위에서 언급했듯이, SQL에서 날짜 데이터는 문자열 데이터와 비교 가능하다.

첫 번째 예시는 12시부터 16시 사이에 해당하는 birthtime만을 골라 모든 열을 반환한다.

두 번째 예시는 첫 번째 예시와 동일한 결과를 반환하지만, CAST로 비교하는 자료형을 TIME으로 바꾸었다.

세 번째 예시는 birthtime를 HOUR 함수로 시간만을 추출하여, BETWEEN으로 비교한다. 

 

-- IN
SELECT title, author_lname FROM books
WHERE author_lname = 'Carver' OR author_lname = 'Lahiri' OR author_lname = 'Smith';

SELECT title, author_lname FROM books
WHERE author_lname IN ('Carver', 'Lahiri', 'Smith');

SELECT title, author_lname FROM books
WHERE author_lname NOT IN ('Carver', 'Lahiri', 'Smith');

IN 연산자는 존재하는 여러 개의 데이터에 값이 있는지 확인하는 연산이다. Python의 in과 동일하다.

첫 번째 예시와 두 번째 예시는 같은 결과를 반환한다.

첫 번째 예시는 OR 연산자를 사용하여 author_lname를 3개 중에서 있는지 확인하는 코드다.

두 번째 예시는 IN 연산자를 사용하여 간단하게 3개의 유무를 확인한다.

세 번째 예시처럼 마찬가지로 NOT 연산자를 사용할 수 있다.

 

-- CASE STATEMENTS
SELECT title, released_year,
    CASE
        WHEN released_year >= 2000 THEN 'Modern Lit'
        ELSE '20th Centiry Lit'
    END AS GENRE
FROM books;

SELECT title, stock_quantity,
    CASE
        WHEN stock_quantity BETWEEN 0 AND 50 THEN '*'
        WHEN stock_quantity BETWEEN 51 AND 100 THEN '**'
        ELSE '***'
    END AS STOCK
FROM books;

SELECT title, stock_quantity,
    CASE
        WHEN stock_quantity <= 50 THEN '*'
        WHEN stock_quantity <= 100 THEN '**'
        ELSE '***'
    END AS STOCK
FROM books;

다른 프로그래밍 언어의 if-else if-else와 같은 조건문이 SQL에도 존재한다.

대신 다른 키워드를 사용하여 CASE-WHEN-THEN-ELSE-END 구문으로 돌아간다.

CASE로 조건문을 시작함을 알리고, END로 조건문이 끝남을 알린다.

WHEN으로 조건문을 작성하고, 이에 해당하는 결과를 THEN에 작성한다.

마지막에 ELSE는 조건문 없이 나머지에 해당하는 결과만을 작성한다.

 

-- IS NULL
SELECT * FROM books WHERE author_lname IS NULL;
SELECT * FROM books WHERE author_lname IS NOT NULL;

위에서 NULL을 비교하는 것은 불가능하다고 했다.

따라서 SQL에서 NULL인지 아닌지 확인하기 위해서는 IS NULL을 사용한다.

마찬가지로 NOT을 사용하여 IS NOT NULL을 사용하여 NULL이 아닌지 확인할 수 있다.

 

 

-- 실습 Q1
SELECT title, released_year FROM books WHERE released_year < 1980;

-- 실습 Q2
SELECT title, author_lname FROM books WHERE author_lname IN ('Eggers', 'Chabon');

-- 실습 Q3
SELECT title, author_lname, released_year FROM books
WHERE author_lname = 'Lahiri' AND released_year > 2000;

-- 실습 Q4
SELECT title, pages FROM books WHERE pages BETWEEN 100 AND 200;

-- 실습 Q5
SELECT title, author_lname FROM books
WHERE author_lname LIKE 'C%' OR author_lname LIKE 'S%';
SELECT title, author_lname FROM books
WHERE SUBSTR(author_lname, 1, 1) IN ('C', 'S');

-- 실습 Q6
SELECT title, author_lname,
    CASE
        WHEN title LIKE '%stories%' THEN 'Short Stories'
        WHEN title LIKE '%kids%' or title LIKE '%heartbreaking%' THEN 'Memoir'
        ELSE 'Novel'
	END AS TYPE
FROM books;

-- 실습 Q7
SELECT author_fname, author_lname,
    CASE
        WHEN COUNT(*) = 1 THEN CONCAT('1 book')
        ELSE CONCAT(COUNT(*), ' books')
	END AS COUNT
FROM books
GROUP BY author_fname, author_lname;

여러 가지 다양한 실습들이다. 직접 테스트하면서 연산자에 익숙해지자.

'Language > SQL' 카테고리의 다른 글

SQL One-to-Many Relationship  (0) 2023.10.02
SQL CONSTRAINT, ALTER  (0) 2023.09.26
SQL DATA TYPE  (0) 2023.09.19
SQL 집계 함수  (0) 2023.09.17
SQL 부분 조회(필터링)  (0) 2023.09.15
-- CHAR and VARCHAR
CHAR(n) > Only N characters 'Allowed'
CHAR is faster for fixed length text
	: State_Abbreviations - CA, NY
    : YES_or_NO Flags - Y, N
    : Zip_codes - 59715, 94924
Otherwise - use VARCHAR

ㅇㅇ

 

-- INT and etcINT
1B - TINYINT
2B - SMALLINT
3B - MEDIUMINT
4B - INT
8B - BIGINT
CREATE TABLE tableName (columnName DATATYPE UNSIGNED);

ㅇㅇ

 

-- Decimal
DECIMAL(total_numer_of_digits, digits_after_decimal)
	: 0.1 - possible
	: 1450.2 - impossible
    : 0.123 - warning - SHOW WARNINGs

ㅇㅇ

 

-- FLOAT and DOUBLE
store larger numbers using less space
but, it comes at the cost of precision
FLOAT - memory_needed(4B) - precision_issues(~7digits)
DOUBLE - memory_needed(8B) - precision_issues(~15digits)
docs says: you shold try to use the most precise type in all cases
         : double - if accuracy is not too important or if speed is the highest priority
         : BIGINT - for high precision you can always convert to a fixed-point type

ㅇㅇ

 

-- Dates and Times
DATE: 'YYYY-MM-DD' - values with a date but no time
TIME: 'HH:MM:SS' - values with a time but no date
DATETIME: 'YYYY-MM-DD HH:MM:SS' - values with a date and time

ㅇㅇ

 

-- CURRENT
CURDATE(CURRENT_DATE)
CURTIME(CURRENT_TIME)
NOW(CURRENT_TIMESTAMP)

ㅇㅇ

 

-- Useful data functions
DAY() : DAYOFMONTH랑 같음, 며칠인지 return
DAYOFWEEK(): 무슨 요일인지 숫자로 return 일(1)
DATOFYEAR(): 00월 00일이 0000년에 몇 번째 날인지 return
MONTHNAME(): 월 이름 return, such as october
WEEK(): 그 해의 몇 번째 주인지 return
https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html

ㅇㅇ

 

-- Useful time functions
MINUTE()
HOUR()
SECOND()
https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html

ㅇㅇ

 

-- Formatting DATES
DATE_FORMAT(column_field, formatting)
	: %b 월이름 약어
	: $c 월 숫자
    : %M 월이름 그대로
    : %a 요일 약어
    : %w 주 약어
	: %e 패딩없는 일(day)
    
    : 시분초 지정자도 있음
    :https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-format

ㅇㅇ

 

-- DATE MATH
DATEDIFF(a, b): a, b의 날짜 차이를 숫자(일수)로 계산
DATE_ADD(date, interval, expr): DATE_ADD(CURDATE(), INTERVAL 1 YEAR) 1년 뒤 계산
DATE_SUB(): DATA_ADD와 유사한 동작
TIMEDIFF(), ADDTIME(), SUBTIME()
https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html

ㅇㅇ

 

-- TIMESTAMP
DATETIME보다 메모리를 덜 차지함
1970 01 01 00:00:01 ~ 2038 01 19 03:14:07
DATETIME은 1000 01 01 00:00:00 ~ 9999 12 31 23:59:59
TIMESTAMPADD()
TIMESTAMPDIFF()
CREATE TABLE captions (
    text VARCHAR(150),
    created_at TIMESTAMP default CURRENT_TIMESTAMP
);
CREATE TABLE captions(
    text VARCHAR(150),
    created_at TIMESTAMP default CURRENT_TIMESTAMP,
    updated_at TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html

ㅇㅇ

 

--
Q1. What's a good use case for CHAR? > 고정 데이터 연도나 우편번호 등
Q2. Fill in the Blanks
CREATE TABLE inventory (
    item_name __________,	VARCHAR(150)
    price ______________,	DECIMAL(7, 2)
    quantity ___________	INT
);
Q3. What's the difference between DATETIME and TIMESTAMP? > 메모리 차이, 날짜 범위 등
Q4. Print Out The Current Time > SELECT CURTIME();
Q5. Print out The Current Date(but not time) > SELECT CURDATE();
Q6. Print out the current day of the week > SELECT DAYOFWEEK(CURDATE()); / SELECT DATE_FORMAT(NOW(), '%w');
Q7. print out the current day and time using this format mm/dd/yyyy
> SELECT DATE_FORMAT(CURDATE(), '%m/%d/%Y')
> SELECT DATE_FORMAT(NOW(), '%M %D at %k:%i')
Q8. create a tweets table that stores
	the tweet content
    a username
    time it was created
>
CREATE TABLE tweets(
    content VARCHAR(180),
    username VARCHAR(20),
    created_at TIMESTAMP DEFAULT NOW() 또는 CURRENT_TIMESTAMP();
);

ㅇㅇ

'Language > SQL' 카테고리의 다른 글

SQL CONSTRAINT, ALTER  (0) 2023.09.26
SQL Logical Operator  (0) 2023.09.25
SQL 집계 함수  (0) 2023.09.17
SQL 부분 조회(필터링)  (0) 2023.09.15
SQL 문자열 함수  (0) 2023.09.14
-- Aggregate Funtions: 집계 함수
-- https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html

집계 함수(Aggregate function)란 입력이 여러 개의 행(row)이고, 출력이 하나인 결과를 말한다.

대표적인 집계 함수에는 합계(SUM), 최솟값(MIN), 최댓값(MAX) 등이 있다.

여러 개의 행(row)이 있을 때, 특정 조건에  만족하는 결과가 하나 나온다.

다른 MySQL의 집계 함수에 대해서 궁금하면 사이트를 더 참고하자. 

 └ https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html

 

CREATE DATABASE book_shop;

CREATE TABLE books 
	(
		book_id INT AUTO_INCREMENT,
		title VARCHAR(100),
		author_fname VARCHAR(100),
		author_lname VARCHAR(100),
		released_year INT,
		stock_quantity INT,
		pages INT,
		PRIMARY KEY(book_id)
	);
 
INSERT INTO books (title, author_fname, author_lname, released_year, stock_quantity, pages)
VALUES
('The Namesake', 'Jhumpa', 'Lahiri', 2003, 32, 291),
('Norse Mythology', 'Neil', 'Gaiman',2016, 43, 304),
('American Gods', 'Neil', 'Gaiman', 2001, 12, 465),
('Interpreter of Maladies', 'Jhumpa', 'Lahiri', 1996, 97, 198),
('A Hologram for the King: A Novel', 'Dave', 'Eggers', 2012, 154, 352),
('The Circle', 'Dave', 'Eggers', 2013, 26, 504),
('The Amazing Adventures of Kavalier & Clay', 'Michael', 'Chabon', 2000, 68, 634),
('Just Kids', 'Patti', 'Smith', 2010, 55, 304),
('A Heartbreaking Work of Staggering Genius', 'Dave', 'Eggers', 2001, 104, 437),
('Coraline', 'Neil', 'Gaiman', 2003, 100, 208),
('What We Talk About When We Talk About Love: Stories', 'Raymond', 'Carver', 1981, 23, 176),
("Where I'm Calling From: Selected Stories", 'Raymond', 'Carver', 1989, 12, 526),
('White Noise', 'Don', 'DeLillo', 1985, 49, 320),
('Cannery Row', 'John', 'Steinbeck', 1945, 95, 181),
('Oblivion: Stories', 'David', 'Foster Wallace', 2004, 172, 329),
('Consider the Lobster', 'David', 'Foster Wallace', 2005, 92, 343),
('10% Happier', 'Dan', 'Harris', 2014, 29, 256),
('fake_book', 'Freida', 'Harris', 2001, 287, 428),
('Lincoln In The Bardo', 'George', 'Saunders', 2017, 1000, 367);

이번 글에서 사용하는 테이블은 이전 글에서 사용한 테이블과 동일하다.

만약 데이터베이스나 테이블 데이터가 없다면 위의 코드를 사용하자.

 

SELECT COUNT(column_field) FROM table_name;

조건에 맞는 개수를 세는 COUNT 함수다.

집계 함수는 '여러 개의 행'이 입력이고, '출력이 하나'라고 했다.

아무런 조건 없이 기본 테이블에서 COUNT를 쓴다면 언제나 결과가 같다.

왜냐하면 column1이든 column2든 행 개수는 동일하기 때문이다.

 

-- Using COUNT
SELECT COUNT(*) FROM books;
SELECT title, COUNT(*) FROM books; -- ERROR

SELECT COUNT(author_fname) FROM books;
SELECT COUNT(DISTINCT author_fname) FROM books;

SELECT title FROM books WHERE title LIKE '%the%';
SELECT COUNT(title) FROM books WHERE title LIKE '%the%';

첫 번째 예시는 books의 열(row) 개수를 구한다.

두 번째 예시는 title과 COUNT(*)을 출력하려고 하면 error가 발생한다.

왜냐하면 COUNT(*)에서 이미 모든 데이터를 하나로 묶었다. 그렇기에 title로 열의 개수를 세려고 하면 오류가 생긴다.

세 번째 예시는 author_fname의 열 개수를 구한다. 이는 첫 번째 예시와 값이 같다.

author_fname에는 같은 이름이 여러 개 있을 수 있다. 네 번째 예시처럼 DISTINCT를 이용하면 중복을 없앨 수 있다.

여섯 번째 예시처럼 COUNT에서 와일드카드를 이용하여 개수를 셀 수도 있다.

 

SELECT (groupby_column or aggregate_function) FROM table_name GROUP BY column_field;

여러 개의 행을 하나로 묶는 GROUP BY 함수다.

다른 일반적인 함수와는 쓰임이 조금 달라, 자세하게 살펴보려고 한다. 아래의 표 예시를 보자.

 

title author_name pages
Le petit prince Saint-Exupery 200
The old man and the sea Ernest Hemingway 200
Demain Hermann Hesse 400
The great gatsby F. Scott Fitzgerald 400
Unterm rad Hermann Hesse 300

위와 같은 테이블이 있다고 가정해보자.

그럼 title로 묶든, author_name으로 묶든, pages로 묶든 할 수 있다. 순서대로 묶어보자.

 

Le petit prince Saint-Exupery 200
The old man and the sea Ernest Hemingway 200
Demain Hermann Hesse 400
The great gatsby F. Scott Fitzgerald 400
Unterm rad Hermann Hesse 300

GROUP BY title을 했을 때의 테이블 상황이다.

title은 중복된 데이터가 없기 때문에, 5개의 행이 그대로 유지된다.

 

Saint-Exupery Le petit prince 200
Ernest Hemingway The old man and the sea 200
Hermann Hesse Demain
Unterm rad
400
300
F. Scott Fitzgerald The great gatsby 400

GROUP BY author_name을 했을 때의 테이블 상황이다. 

author_name는 헤르만 헤세가 두 권의 책을 적어 행이 4개가 됐다.

 

200 Le petit prince
The old man and the sea
Saint-Exupery
Ernest Hemingway
400 Demain
The great gatsby
Hermann Hesse
F. Scott Fitzgerald
300 Unterm rad Hermann Hesse

GROUP BY pages를 했을 때의 테이블 상황이다.

200 페이지 책과 400 페이지 책은 각각 2권씩 있어, 행이 3개가 됐다.

 

이렇게 GROUP BY으로 행을 묶어주면, SELECT로 선택할 때 제약이 생긴다.

GROUP BY에서 사용한 column이나 집계 함수만 사용이 가능하다.

예를 들어 pages로 묶은 테이블에서 title이나 author_name에 접근한다고 해보자.

300 페이지는 상관없지만, 200 페이지나 400 페이지에는 여러 데이터가 있다.

즉 어떤 title이나 어떤 author_name에 접근할지 DBSM 입장에서는 모른다는 것이다.

 

-- Using GROUP BY
-- Defination: GROUP BY summarizes or aggregates idnetical data into single rows
SELECT author_lname FROM books GROUP BY author_lname;
SELECT author_lname, COUNT(*) FROM books GROUP BY author_lname;
SELECT author_lname, COUNT(*) AS count FROM books GROUP BY author_lname;
SELECT author_lname, COUNT(*) AS count FROM books GROUP BY author_lname ORDER BY count;

SELECT released_year, COUNT(*) FROM books GROUP BY released_year;

첫 번째 예시는 author_lname으로 테이블을 묶고, author_lname 목록을 반환한다.

이 코드는 사실상 author_lname을 선택한 것과 다를 바가 없다. 하지만 테이블 내부에서는 그룹으로 나뉜 상태다.

두 번째 예시는 author_lname으로 테이블을 묶고, author_lname과 각 행 개수를 센다.

세 번째 예시는 두 번째 예시와 같지만, count의 field를 AS로 지정해준다.

네 번째 예시는 세 번째 예시에서 count 개수 오름차순으로 정렬한 뒤 반환한다.

다섯 번째 예시는 released_year로 테이블을 묶고, released_year와 그에 해당하는 각 행 개수를 센다.

 

-- Using multiple GROUP BY
SELECT author_fname, author_lname, COUNT(*)
FROM books
GROUP BY author_lname, author_fname;

SELECT CONCAT(author_fname, ' ', author_lname) AS author, COUNT(*)
FROM books
GROUP BY author;

GROUP BY에는 한 가지 이상의 값으로 묶는 것이 가능하다.

일차적으로 묶은 데이터 내부에서, 따로 데이터를 더 묶는 것이다.

ORDER BY처럼 AS로 선언한 별칭에 대해서 GROUP BY하는 것도 가능하다.

 

SELECT MIN(column_field) FROM table_name;
SELECT MAX(column_field) FROM table_name;

column_field에서 가장 작은 값(MIN) 혹은 가장 큰 값(MAX)을 반환하는 함수다.

 

-- Using MIN, MAX
SELECT MIN(released_year) FROM books;
SELECT MAX(pages) FROM books;
SELECT MIN(author_lname) FROM books;
SELECT MAX(author_lname) FROM books;

숫자 데이터는 당연히 대소 비교를 한 뒤, 값을 반환한다.

문자 데이터라면 글자 순으로 값을 반환한다. a에 가까울수록 작은 값이고, z에 가까울수록 큰 값이다.

 

-- Using MIN/MAX with GROUP BY
SELECT released_year, MAX(pages)
FROM books
GROUP BY released_year;

SELECT author_fname, author_lname, MIN(released_year)
FROM books
GROUP BY author_fname, author_lname;

MIN / MAX와 GROUP BY를 같이 사용할 수 있다.

COUNT 대신 MIN이나 MAX 함수를 사용하는 방법과 같다.

 

SELECT column_field
FROM table_name
WHERE condition = (SELECT column_field FROM table_name);

'서브 쿼리(sub query)'라는 기능이 있다.

쿼리(query) 안에서 쿼리(query)를 수행하는 기능으로, WHERE 값으로 또 다른 쿼리를 작성한다.

 

예를 들어서, condition에서 pages가 가장 큰 값을 찾고 싶어한다.

이때 pages의 최댓값을 모른다면? SELECT pages FROM books ORDER BY DESC로 값을 찾아야만 한다.

이때 WHERE 값으로 SELECT MAX(pages) FROM books를 넣어준다면, 바로 찾을 수 있다.

 

-- Using Sub Query
SELECT title, pages FROM books ORDER BY pages DESC LIMIT 1;
SELECT * FROM books
WHERE pages = (SELECT MAX(pages) FROM books);

SELECT title, released_year FROM books
WHERE released_year = (SELECT MIN(released_year) FROM books);

첫 번째 예시와 두 번째 예시는 모두 실행 결과가 같다.

첫 번째 예시는 pages를 역순 정렬한 다음, 위에서 한 행(row)만 잘라내어 title과 pages를 반환한다.

두 번째 예시는 pages에서 가장 큰 값을 찾아, 해당 값과 일치하는 pages 값만 반환한다.

세 번째 예시는 released_year에서 최솟값을 찾아 해당 값과 일치하는 title과 released_year를 반환한다.

 

SELECT SUM(column_field) FROM table_name;

MIN, MAX, COUNT 함수와 쓰임은 동일하다.

SUM은 합계를 구하는 함수다.

 

-- Using SUM
SELECT SUM(pages) FROM books;

SELECT author_fname, author_lname, SUM(pages)
FROM books
GROUP BY author_fname, author_lname;

첫 번째 예시는 books 테이블에서 pages 값을 모두 더하여 반환한다.

두 번째 예시는 author_fname, author_lname을 기준으로 묶어, 저자마다 pages 합계를 반환한다.

 

SELECT AVG(column_field) FROM table_name;

평균(average)의 약어인 AVG로, 주어진 데이터들의 평균을 구하는 함수다.

숫자 데이터의 평균은 구할 수 있지만, 문자 데이터의 평균은 구할 수 없다.

 

-- Using AVG
SELECT AVG(released_year) FROM books;
SELECT AVG(pages) FROM books;

SELECT released_year, AVG(stock_quantity), COUNT(*)
FROM books
GROUP BY released_year
ORDER BY released_year;

첫 번째 예시는 released_year의 평균을 반환한다.

두 번째 예시는 pages의 평균을 반환한다.

세 번째 예시는 released_year를 기준으로 데이터를 묶고, 오름차순 정렬을 한다.

그리고 released_year, 재고(stock_quantoty)의 평균, 행 개수를 반환한다.

 

-- Example
-- Q1
SELECT COUNT(*) FROM books;

-- Q2
SELECT released_year, COUNT(*) FROM books GROUP BY released_year;

-- Q3
SELECT SUM(stock_quantity) FROM books;

-- Q4
SELECT CONCAT(author_fname, ' ', author_lname) AS author, AVG(released_year)
FROM books GROUP BY author;

-- Q5
SELECT author_fname, author_lname, pages
FROM books
WHERE pages = (SELECT MAX(pages) FROM books);

-- Q6
SELECT
    released_year AS 'year',
    COUNT(*) AS '# books',
    AVG(pages) AS 'avg pages'
FROM books
GROUP BY released_year
ORDER BY released_year;

집계 함수를 사용한 다양한 활용 예제이다.

참고하여 집계 함수를 익숙하게 사용해보자.

'Language > SQL' 카테고리의 다른 글

SQL Logical Operator  (0) 2023.09.25
SQL DATA TYPE  (0) 2023.09.19
SQL 부분 조회(필터링)  (0) 2023.09.15
SQL 문자열 함수  (0) 2023.09.14
CRUD  (0) 2023.09.11
INSERT INTO books
(title, author_fname, author_lname, released_year, stock_quantity, pages)
VALUES
('10% Happier', 'Dan', 'Harris', 2014, 29, 256),
('fake_book', 'Freida', 'Harris', 2001, 287, 428),
('Lincoln In The Bardo', 'George', 'Saunders', 2017, 1000, 367);

저번 SQL 문자열 함수에서 사용한 테이블 값을 이어서 사용한다.

이미 테이블이 있고 값을 추가해놨다면, 위의 INSERT 구문으로 3개의 행만 추가하면 된다.

이번 코드와 테이블 예제들도  Colt Steele의 SQL 데이터를 참고했음을 알린다. 

 

이번 글에서 다룰 것은 부분 조회에 대한 함수와 사용법이다.

SELECT로 검색할 때, 원하는 조건에 따라 일부만 추출한다는 뜻에서 '부분 조회'라는 단어를 사용했다.

 

SELECT DISTINCT column_field FROM table_name;

분명하다는 뜻을 가진 DISTINCT를 사용하면 중복하지 않은 데이터를 반환한다.

값이 분명하다는 것은 다른 것과 헷갈리지 않는다는 말과 같고, 곧 유일하다는 뜻이다.

 

-- Using DISTINCT
SELECT author_lname FROM books;
SELECT DISTINCT author_lname FROM books;

SELECT released_year FROM books;
SELECT DISTINCT released_year FROM books;

DISTINCT 없이 author_lname이나 released_year를 추출하면 모든 데이터를 반환한다.

하지만 DISTINCT를 SELECT 이후에 적어주면, 해당 column_field에서 중복을 모두 제거하여 반환한다.

즉 반환한 모든 데이터는 유일하다는 거다. 

 

SELECT DISTINCT author_fname FROM books;
SELECT DISTINCT author_lname FROM books;

-- Combination
SELECT DISTINCT author_fname, author_lname FROM books;

한 저자가 여러 권의 책을 출간했을 수 있으니, 모든 행이 아니라 저자의 이름을 중복 없이 뽑아내고자 한다.

그럼 첫 번째 줄과 두 번째 줄처럼 author_fname과 author_lname에서 각각 DISTINCT를 하면 어떨까?

각각 DISTINCT를 하면 성 따로 이름 따로 중복을 제거해버린다.

예를 들어 Alax James와 Casandra James가 있다면 이름은 2개지만, 성은 1개가 되는 일이 발생한다.

 

이때 combination(조합)의 개념을 사용하면 된다.

세 번째 예시처럼 하나의 DISTINCT에서 fname과 lname을 같이 두는 것이다.

그럼 DISTINCT는 fname과 lname의 묶음을 하나의 값으로 간주해, 중복을 찾아낸다.

(Alax, Casandra)를 비교하고, (James, James)를 비교하여 중복을 찾는 게 아니라,

(Alax James, Casandra James)를 비교하여 중복을 찾아내는 것이다.

 

SELECT column_field FROM table_name ORDER BY criteria_field *OPTION;

순서를 정렬해주는 ORDER BY 함수다.

ORDER BY 함수는 2개의 값을 필요로 한다. 이때 OPTION에 해당하는 값은 선택이다.

WHERE처럼 무엇은 기준으로 '정렬'할지 선택하는 column_field를 적어준다.

그리고 OPTION에서는 오름차순(ASC)할지, 내림차순(DESC)할지 정한다. Default는 ASC다.

 

-- Using ORDER BY
SELECT title, pages FROM books ORDER BY pages;
SELECT title, pages FROM books ORDER BY pages ASC;

SELECT author_lname FROM books ORDER BY pages DESC;

첫 번째 예시와 두 번째 예시는 같은 결과를 반환한다.

books 테이블에서 title과 pages를 반환하는데, pages의 오름차순을 정렬로 반환한다.

Default가 ASC(Ascending)이기에 같은 결과가 나온다.

 

세 번째 예시는 books 테이블에서 author_lname을 pages 내림차순으로 반환한다.

WHERE처럼 꼭 SELECT에 해당하는 field가 아니라, 다른 field를 ORDER BY 기준으로 사용할 수 있다.

하지만 무엇을 기준으로 정렬했는지 알 수 없기에, 보통 SELECT에 있는 값으로 정렬한다.

 

-- Another using ORDER BY
SELECT book_id, author_fname, author_lname, pages
FROM books
ORDER BY pages;

SELECT book_id, author_fname, author_lname, pages
FROM books
ORDER BY 4;

ORDER BY에는 index로 접근하는 편리한 기능이 하나있다. 

첫 번째 예시에서는 books 테이블에서 4개의 column을 pages 오름차순 기준으로 반환한다.

이때 pages는 4개의 column 중에서 4번째에 존재하는 column이다.

이렇게 정렬 기준이 되는 column이 SELECT 구문에 존재한다면 index로 적는 것이 가능하다.

두 번째 예시처럼 ORDER BY pages 대신 ORDER BY 4로 작성한다.

 

-- Another using ORDER BY
SELECT CONCAT
(
    author_fname, ' ', author_lname
) AS author
FROM books
ORDER BY author;

ORDER BY에서 한 가지 더 신기한 기능이 있다.

기존 books 테이블에는 author라는 column_field는 존재하지 않는다.

하지만 위의 코드에서는 AS로 정의한 author를 기준으로 정렬하는 모습을 볼 수 있다.

이처럼 AS에서 새로 정의한 field_name을 기준으로 정렬하는 것 또한 가능하다. 

 

-- Multiple ORDER BY
SELECT author_lname, released_year
FROM books
ORDER BY author_lname, released_year;

SELECT author_lname, released_year
FROM books
ORDER BY author_lname, released_year DESC;

ORDER BY는 두 가지 이상의 기준으로 정렬하는 것도 가능하다.

Python에서 sort()를 진행할 때 key = lambda를 활용하여, 2가지 이상의 기준으로 정렬하는 것과 동일하다.

앞에 적은 기준으로 우선 정렬하고 그 기준은 유지한 채로, 뒤에 적은 기준으로 정렬한다.

 

첫 번째 예시로 books 테이블에서 author_lname과 released_year를 정렬하려고 한다.

author_lname으로 먼저 오름차순 정렬을 하고, 그 다음 released_year로 오름차순 정렬을 한다.

물론 이때도 두 번째 예시처럼 ASC와 DESC 사용이 가능하다.

 

SELECT column_field FROM table_name LIMIT *start_index, how_many;

열(column)에 제한을 두는 것이 아닌 행(row)에 제한을 두는 부분 조회 방법이다.

LIMIT을 사용하면 상위 몇 개의 값을 추출하여 반환한다.

LIMIT은 두 개의 값을 필요로 하는데, 첫 번째 값은 optional이다.

 

첫 번째 값은 시작하는 index 위치를 지정한다. Default 값은 0이다.

두 번째 값은 얼마나 많은 행을 추출할 것인지 지정한다.

주의할 점은 문자열의 첫 index 값은 1이지만, 튜플(tuple)의 첫 index 값은 0이다. 

 

-- Using LIMIT
SELECT book_id, title, released_year FROM books;
SELECT book_id, title, released_year FROM books LIMIT 5;
SELECT book_id, title, released_year FROM books LIMIT 0, 5;
SELECT book_id, title, released_year FROM books LIMIT 3, 7;
SELECT book_id, title, released_year FROM books ORDER BY released_year LIMIT 5;

첫 번째 예시는 books 테이블에서 3개의 column을 지정하여 모든 행을 뽑아낸다.

두 번째 예시는 books 테이블에서 3개의 column을 지정한 뒤, 위에서부터 5개의 행만을 뽑아낸다.

세 번째 예시는 books 테이블에서 3개의 column을 지정한 뒤, 0번째(처음)부터 5개의 행만을 뽑아낸다. 

네 번째 예시는 books 테이블에서 3개의 column을 지정한 뒤, 3번째 index(위에서 4번째)부터 7개의 행만을 뽑아낸다.

다섯 번째 예시처럼 ORDER BY로 정렬한 뒤에 LIMIT으로 제한하는 것도 가능하다.

 

SELECT column_field FROM table_name WHERE criteria_field LIKE condition;

유사하다는 뜻의 LIKE는 말그대로 유사한 데이터를 뽑아내는 함수다.

기존의 WHERE 조건으로는 데이터 값이 정확하게 일치하는 경우만 찾을 수 있었다.

LIKE로 유사한 값들을 전부 찾을 수 있다.

 

-- Using LIKE
SELECT title, author_fname, author_lname
FROM books
WHERE author_fname = 'David';

SELECT title, author_fname, author_lname
FROM books
WHERE author_fname LIKE 'David';

기본 사용 방법은 위와 같다.

첫 번째 예시는 author_fname이 David인 title, author_fname, author_lname을 반환한다.

두 번째 예시는 author_fname이 David인 title, author_fname, author_lname을 반환한다.

어라? 똑같다. 그래서 와일드카드(wildcard)라는 개념이 필요하다.

 

% : 0 or more than 0
_ : exactly _ count

와일드카드는 WHERE만 있을 때는 사용이 불가능하고, LIKE 구문이 있을 때 사용할 수 있다.

카드 게임에서 와일드카드는 여러 의미가 있는데 '다른 카드의 대용이 가능한 특수한 카드'라는 뜻이 있다.

즉, MySQL에서 와일드카드는 '임의의 문자(열)에 대응하기 위해 사용하는 문자'다.

 

와일드카드는 %와 _ 두 가지가 있다.

%는 0개 이상의 문자(열)에 대응하는 특수문자다. 

_는 정확하게 단 1개의 문자에 대응하는 특수문자다.

 

-- Wildcard example
LIKE '%a'
LIKE 'a%'

LIKE '_a'
LIKE 'a____'

첫 번째 예시는 마지막 글자가 a로 끝나는 데이터를 찾는다. 이때 문자 길이는 상관없다.

두 번째 예시는 첫 번째 글자가 a로 시작하는 데이터를 찾는다. 이때 문자 길이는 상관없다.

세 번째 예시는 마지막 글자가 a로 끝나는 데이터를 찾는다. 이때 a를 포함하여 딱 두 글자여야만 한다.

네 번째 예시는 첫 번째 글자가 a로 시작하는 데이터를 찾는다. 이때 a를 포함하여 딱 다섯글자여야만 한다.

 

SELECT title, author_fname, author_lname
FROM books
WHERE author_fname LIKE '%av%'; -- % wildcard: 0 and more than char

이를 응용하면 위와 같은 구문을 작성할 수 있다.

books 테이블에서 title, author_fname, author_lname의 column을 찾는다.

단, author_fname에서 av가 들어간 행만을 찾는다.

av가 들어간 행을 찾는 이유는 av 앞뒤로 %가 둘 다 붙었기 때문이다.

즉, av 앞뒤로 몇 글자든 상관이 없으니 있다면 조건에 해당하는 것이다.

'%AV%'나 '%av%'나 상관이 없다. MySQL은 대소문자 구분을 하지 않는다.

 

-- Using wildcard
SELECT * FROM books WHERE title LIKE '%:%';

SELECT * FROM books WHERE author_fname LIKE '____';
SELECT * FROM books WHERE author_fname LIKE '____%';

SELECT * FROM books WHERE author_fname LIKE '_a_';
SELECT * FROM books WHERE author_fname LIKE '%a%';

SELECT * FROM books WHERE author_fname LIKE '%n';

첫 번째 예시는 title에 콜론(:)이 들어간 행만을 모든 열을 반환한다.

두 번째 예시는 author_fname이 정확하게 4글자인 경우만 모든 열을 반환한다.

세 번째 예시는 author_fname이 4글자 이상인 경우만 모든 열을 반환한다.

네 번째 예시는 author_fname이 3글자이면서 가운데가 a인 경우만 모든 열을 반환한다.

다섯 번째 예시는 author_fname의 글자 수에 상관없이 a가 있다면 모든 열을 반환한다.

마지막 예시는 author_fname의 글자 수에 상관없이 마지막 글자가 n인 경우만 모든 열을 반환한다.

 

-- Wildcard escape
SELECT * FROM books WHERE title LIKE '%\%%';
SELECT * FROM books WHERE title LIKE '%\_%';

만약 찾으려는 문자열에 와일드카드 특수문자(%, _)가 있다면 어떡할까?

다른 프로그래밍 언어에서 역슬래시(\)를 쓰는 방법과 동일하다.

%라는 데이터 자체를 찾으려고 한다면, \%로 적어주면 된다.

 

첫 번째 예시에서 LIKE '%\%%'는 %가 들어간 모든 문자열을 찾는 구문이다.

마찬가지로 두 번째 예씨는 _가 들어간 모든 문자열을 찾는 구문이다.

'Language > SQL' 카테고리의 다른 글

SQL DATA TYPE  (0) 2023.09.19
SQL 집계 함수  (0) 2023.09.17
SQL 문자열 함수  (0) 2023.09.14
CRUD  (0) 2023.09.11
SQL 기본 구문  (0) 2023.09.10

+ Recent posts