Date : 2023-08-04

Topic : SQL 함수, 코딩테스트 실습, 집합연산, SQL VIEW


Note

SQL 함수

문자열 함수

SELECT title, LENGTH(title) AS title_leng
FROM film LIMIT 10;

SELECT title, LOWER(title)
FROM film LIMIT 10;

SELECT CONCAT(first_name, ' ', last_name)
FROM actor LIMIT 10;

SELECT CONCAT(first_name, last_name, 'name', 'dave') -- comma로 연결 가능
FROM actor LIMIT 10;

SELECT SUBSTRING(description, 3, 10) -- 3번째부터 10글자씩 '부분 문자열' 추출
FROM film LIMIT 10;

날짜/시간 함수

SELECT NOW() AS current_date_time; -- 현재 날짜와 시간 반환

SELECT CURDATE() AS 'current_date'; -- 현재 날짜 반환
SELECT CURTIME() AS 'current_time'; -- 현재 시간 반환

# 날짜에 간격을 추가 (시간 단위 변경 가능)
SELECT
    rental_date,
    DATE_ADD(rental_date, INTERVAL 7 DAY) -- rental_date에서 7일이 추가된
FROM rental LIMIT 10;

# 날짜에서 간격을 뺀 (시간 단위 변경 가능)
SELECT
    rental_date,
    DATE_SUB(rental_date, INTERVAL 7 MINUTE) -- rental_date에서 7분 뺀
FROM rental LIMIT 10;

숫자 함수

SELECT ABS(-amount) FROM payment LIMIT 10; -- 절대값 예시 위해 (-) 붙임

SELECT amount, CEIL(amount) FROM payment LIMIT 10; -- 크거나 같은 가장 작은 정수값 (반대는 floor)

SELECT amount, ROUND(amount, 1) FROM payment LIMIT 10; -- 소수점 n자리까지 반올림

SELECT rental_duration, SQRT(rental_duration) FROM film LIMIT 10; -- 제곱근

기본 연습문제

# film 테이블에서 영화 제목( title )의 길이가 15자인 영화들을 찾아주세요.
SELECT title, LENGTH(title) FROM film
WHERE LENGTH(title) = 15;

# actor 테이블에서 첫 번째 이름( first_name )이 소문자로 'john'인 배우들의 전체 이름을 대문자로 출력해주세요.
SELECT UPPER(CONCAT(first_name, ' ',last_name)) AS name -- 함수는 중첩 가능
FROM actor
WHERE lower(first_name) = 'john';

# film 테이블에서 description 의 3-6번째 'Action'인 영화의 제목을 찾아주세요.
SELECT title, SUBSTRING(description, 3, 6) AS desc_action
FROM film
WHERE SUBSTRING(description, 3, 6)='Action';

/* rental 테이블에서 대여 시작 날짜( rental_date )가 2023년 1월 1일 이후인 모든 대여에 대해,
예상 반납 날짜를 대여 날짜로부터 5일 뒤로 설정해주세요. */
SELECT
	rental_date, -- 대여일
	DATE_ADD(rental_date, INTERVAL 5 DAY) AS return_date5 -- 5일 뒤 반납
FROM rental
WHERE rental_date >= '2006-01-01';

# 현재 날짜가 2006년 2월 15일일 때, rental 테이블에서 7일 이내에 반납해야 하는 모든 대여 정보를 찾아주세요.
SELECT *
FROM rental 
WHERE DATE_SUB('2006-02-15', INTERVAL 7 DAY) <= rental_date; -- 헷갈리니까 다시 풀고 해설 달기

# payment 테이블에서 결제 금액( amount )이 -10 이하인 모든 결제에 대해, 절대값을 계산하여 출력해주세요.
SELECT ABS(amount) AS abs_amount FROM payment
WHERE abs_amount <= -10;

# film 테이블에서 영화 길이( length )가 120분 이상인 모든 영화에 대해, 영화 길이의 제곱근을 계산해주세요.
SELECT SQRT(length) FROM film -- 제곱근 SQRT (square root)
WHERE length >= 120;

# payment 테이블에서 결제 금액( amount )을 소수점 첫 번째 자리에서 반올림하여 출력해주세요.
SELECT ROUND(amount, 0) FROM payment; -- n자리에서 반올림하면 n+1까지 구해야함

SubQuery 중급

예제

# 평균 결제 금액보다 많은 결제를 한 고객을 찾아봅시다
SELECT first_name, last_name	-- 조건에 맞는 고객 이름 출력
FROM customer
WHERE customer_id IN (
	SELECT customer_id	-- 보다 많은 고객(ID)
	FROM payment
	WHERE amount > (SELECT AVG(amount) FROM payment) -- 고객 평균 결제액 (4.201356)
	);

# 평균 결제 횟수보다 많은 결제를 한 고객을 찾아봅시다
SELECT first_name, last_name FROM customer -- 4. 이름 출력
WHERE customer_id IN (
	SELECT customer_id FROM payment -- 3. 평균 결제 횟수보다 보다 많은 고객
	GROUP BY customer_id
	HAVING COUNT(*) > (
		SELECT AVG(cnt_payment) -- 2. 고객별 결제 횟수의 평균
		FROM (
			SELECT COUNT(*) AS cnt_payment -- 1. 고객별 결제 횟수
			FROM payment
			GROUP BY customer_id
			) a -- 파생된 테이블은 반드시 별칭을 가져야함
		)
	);

# 가장 많은 결제를 한 고객을 찾아봅시다
SELECT first_name, last_name -- 가장 많이 주문한 고객 이름
FROM customer
WHERE customer_id = (
	SELECT customer_id -- 4.최다 주문 고객ID
	FROM (
		SELECT customer_id, COUNT(*) AS cnt_payment -- 1.고객별 고객ID와 카운트
		FROM payment
		GROUP BY customer_id
		) AS payment_counts
	ORDER BY cnt_payment DESC -- 2.많이 주문한 고객순 정렬
	LIMIT 1 -- 3.가장 많이 주문한 고객
	)

/* 각 고객에 대해 자신이 결제한 평균 금액보다 큰 결제를 한 경우의 결제 정보를 찾아봅시다
상관 서브쿼리(Correlated Subquery) : 서브쿼리가 외부 쿼리의 변수를 참조 */
SELECT P.customer_id, P.amount, P.payment_date
FROM payment P
WHERE P.amount > (	-- 2.보다 큰 금액 결제한 경우
	SELECT AVG(amount)	-- 1.평균 결제액
	FROM payment
	WHERE customer_id = P.customer_id
	);

기본 연습문제

# film 테이블에서 평균 영화 길이( length )보다 상영시간이 긴 영화들의 제목( title )을 찾아주세요.
SELECT title FROM film
WHERE length > (SELECT AVG(length) FROM film);

# rental 테이블에서 평균 대여 횟수보다 많은 대여를 한 고객들의 이름( first_name , last_name )을 찾아주세요.
SELECT first_name, last_name FROM customer
WHERE customer_id IN (
	SELECT customer_id FROM rental -- 4.고객ID
	GROUP BY customer_id
	HAVING COUNT(*) > ( -- 3.평균보다 많은 대여 횟수를 가진
		SELECT AVG(rental_count) -- 2.렌트 횟수 평균
		FROM (
			SELECT COUNT(*) AS rental_count -- 1. 고객별 렌트 횟수
			FROM rental 
			GROUP BY customer_id
			) AS rental_counts
		)
	);

# 가장 많은 영화를 대여한 고객의 이름(first_name, last_name)을 찾아주세요.
SELECT c.first_name, c.last_name -- 5.고객 이름 출력
FROM customer c
JOIN (
    SELECT customer_id, COUNT(*) AS rent_cnt -- 1.고객별 렌트 횟수 서브쿼리
    FROM rental
    GROUP BY customer_id
    ) r
ON c.customer_id = r.customer_id -- 2.파생 테이블 r과 customer 테이블 조인
ORDER BY r.rent_cnt DESC -- 3.렌트 횟수 내림차순
LIMIT 1; -- 4. 가장 렌트를 많이 한 고객

# 각 고객에 대해 자신이 대여한 평균 영화 길이( length )보다 긴 영화들의 제목( title )을 찾아주세요.
SELECT C.first_name, C.last_name, F.title -- 4.영화 제목 출력
FROM customer C 
JOIN rental R ON R.customer_id = C.customer_id
JOIN inventory I ON I.inventory_id = R.inventory_id
JOIN film F ON F.film_id = I.film_id
WHERE F.length > (	-- 3.보다 긴 영화
    SELECT AVG(F.length) -- 2.평균 영화 길이
    FROM film F
    JOIN inventory I ON I.film_id = F.film_id
    JOIN rental R ON R.inventory_id = I.inventory_id
    WHERE R.customer_id = C.customer_id -- 1.렌트한 고객과 고객ID가 같을 때
    )

복합 연습 문제

/* rental 과 inventory 테이블을 JOIN하고, film 테이블에 있는 replacement_cost 가 $20 이상인 영화를
대여한 고객의 이름을 찾으세요. 고객 이름은 소문자로 출력해주세요.*/
SELECT DISTINCT LOWER(CONCAT(first_name, ' ', last_name)) AS customer_name -- DISTINCT로 고객명 중복 제거
FROM rental r
JOIN inventory i ON i.inventory_id = r.inventory_id
JOIN film f ON f.film_id = i.film_id
JOIN customer c ON c.customer_id = r.customer_id
WHERE replacement_cost >= 20

집합 연산

: 두 개 이상의 SELECT문의 결과를 결합하거나 비교 [UNION, UNION ALL, INTERSECT, EXCEPT]

  • 집합 연산을 사용할 때는 SELECT 문이 동일한 수의 열을 선택
  • 열은 동일한 순서로 있어야 함
  • 해당 열은 호환 가능한 데이터 유형을 가져야 함
  • 모든 데이터베이스 시스템이 INTERSECT 와 EXCEPT 를 지원하는 것은 아님

 

UNION: 두 개 이상 SELECT문 결합, ⭐중복행 제거, 열 순서가 같아야 함

SELECT film_id FROM film
UNION
SELECT film_id FROM film_category;
# film 또는 film_category 테이블에 있는 중복행을 제거하고 고유한 film_id 리스트를 반환

UNION ALL: UNION과 같은 기능이지만 ⭐중복된 행을 포함

SELECT film_id FROM film
UNION ALL
SELECT film_id FROM film_category;
# film 및 film_category 테이블의 모든 film_id 리스트를 반환하며, 중복 값도 포함

INTERSECT: 두 개 이상 SELECT문의 교집합을 반환(=모든 SELECT문의 공통적으로 있는 행)

SELECT film_id FROM film
INTERSECT
SELECT film_id FROM film_category;
# film 및 film_category 테이블 모두에 있는 film_id를 반환

EXCEPT: 두 개 이상 SELECT문의 차집합을 반환(=첫 번째 결과 집합엔 있지만 두 번째 결과 집합엔 없는 행)

SELECT film_id FROM film
EXCEPT
SELECT film_id FROM film_category;
# film에는 있지만 film_category에는 없는 film_id를 반환

SQL VIEW

: 실제 테이블을 기반으로 한 가상 테이블. 뷰를 사용하면 복잡한 쿼리를 단순화하고 데이터 특정 부분에만 접근 가능

 

VIEW 생성

CREATE VIEW view_name AS
    SELECT column1, column2, ...
    FROM table_name
    WHERE condition;

VIEW 수정

CREATE OR REPLACE VIEW view_name AS -- OR REPLACE(뷰를 생성하거나 이미 존재하면 교체)
    SELECT column1, column2, ...
    FROM table_name
    WHERE condition;

VIEW 삭제

DROP VIEW view_name;

예제

/* 1. ActorInfo라는 VIEW를 만드세요.
(actor 테이블에서 first_name 과 last_name 컬럼을 포함해야합니다. actor_id 가 50 미만인 배우만 포함) */
CREATE VIEW ActorInfo AS
	SELECT first_name, last_name FROM actor
	WHERE actor_id < 50;

/* 2. film 테이블에서 렌탈 비용이 $2.00보다 높은 영화에 대한 VIEW를 만들어 봅니다.
이 VIEW의 이름은 ExpensiveFilms 이고, title 과 rental_rate 컬럼만을 포함해야 합니다. */
CREATE VIEW ExpensiveFilms AS
	SELECT title, rental_rate FROM film
	WHERE rental_rate > 2.00;

/* 3. 이미 만든 VIEW인 ActorInfo 를 수정하여 actor_id 가 100 미만인 배우만 포함하도록 바꾸려면 어떻게 해야 할까요? */
CREATE OR REPLACE VIEW ActorInfo AS
	SELECT first_name, last_name FROM actor
	WHERE actor_id < 100;

/* 4. ExpensiveFilms VIEW를 삭제하려면 어떤 SQL 명령을 사용해야 할까요? */
DROP VIEW ExpensiveFilms;

문자열, 숫자 함수는 자주 쓰이는 게 특정되는데 날짜 함수는 파생 문법도 많고 형태가 헷갈리고 종류가 너무 다양해서 익숙해지려면 많이 보고 자주 사용해봐야 겠다. (사실 현업에서 DATETYPE 컬럼은 자주 사용되지 않는다는 말씀을 하셨지만 그래도 사용해야 할 경우가 언제 생길지 모르니 항상 숙지하고 있을 것!) TIMESTAMPDIFF()는 모든 시간 형태를 사용할 수 있으니 꼭 기억해두기⭐ 그동안 JOIN 공부한다고 잠깐 서브쿼리 문제는 손에서 놨더니 조금만 난이도가 올라가면 금방 헷갈려진다. 서브쿼리 구조 자체는 이해할 수 있는데 코딩테스트처럼 문제를 보고 쿼리 작성하는 시간이 너무 오래 걸리니 연습 문제 꾸준히 복습하기✍️

Date : 2023-08-03

Topic : MySQL index, FOREIGN KEY, INNER JOIN, SubQuery


Note

INDEX

  • 테이블 동작 속도를 높여주는 자료 구조
  • 인덱스: 키-필드 구조 (나머지 세부 테이블 컬럼 정보는 가지고 있지 않음)
    • 클러스터형 인덱스: 영어 사전 같은 형태(데이터순 정렬) : 테이블에서 PK로 정의한 컬럼이 있을 경우 자동 생성
    • 보조 인덱스: 일반 책 뒤에 있는 <찾아보기> 같은 형태

 

- 실습: userTbl 테이블 생성

CREATE TABLE userTbl (
userID CHAR(8) NOT NULL PRIMARY KEY,
name VARCHAR(10) NOT NULL,
birthYear INT NOT NULL,
addr CHAR(2) NOT NULL,
mobile1 CHAR(3),
mobile2 CHAR(8),
height SMALLINT,
mDate DATE
) DEFAULT CHARSET=utf8 COLLATE=utf8_bin; -- 생략 가능
  • DEFAULT CHARSET=utf8 COLLATE=utf8_bin
    : 문자셋 인코딩(한글 데이터 정확한 처리)
    • Character set: 문자가 저장될 때 어떤 코드로 저장될지에 대한 규칙의 집합
    • Collation: 문자들을 서로 비교하기 위해 사용하는 규칙 집합(문자 비교 검색, 정렬 등)

인덱스 확인

SHOW INDEX FROM userTbl

index check

  • Key_name이 P.K로 된 것은 클러스터형 인덱스를 의미
참고
- Table: The name of the table.
- Non_unique: 0 if the index cannot contain duplicates, 1 if it can.
- Key_name: The name of the index. If the index is the primary key, the name is always PRIMARY.
- Seq_in_index: The column sequence number in the index, starting with 1.
- Column_name: The column name.
- Collation: How the column is sorted in the index. This can have values A (ascending) or NULL (notsorted).
- Cardinality: An estimate of the number of unique values in the index.
- Index_type: The index method used (BTREE, FULLTEXT, HASH, RTREE).

 

  • buyTbl 구조
CREATE TABLE buyTbl (
num INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
userID CHAR(8) NOT NULL,
prodName CHAR(4),
groupName CHAR(4),
price INT NOT NULL,
amount SMALLINT NOT NULL,
FOREIGN KEY (userID) REFERENCES userTbl(userID)
);
  • Key_name 이 PRIMARY 가 아닌 것은 보조 인덱스를 의미
  • foreign key로 설정된 컬럼이 인덱스가 없다면, 인덱스를 자동 생성

- 참고: 테이블 변경

ALTER TABLE userTbl ADD CONSTRAINT TESTDate UNIQUE(mDate);
# 특정 컬럼에 duplicate값이 나오지 않도록 unique 제약조건 추가

UNIQUE 제약을 넣으면, 보조 테이블이 만들어짐

- 인덱스 생성 및 삭제 (필요에 따라)

CREATE INDEX idx_name ON userTbl (name); -- CREATE INDEX를 이용한 인덱스 추가
ALTER TABLE userTbl ADD INDEX idx_addr (addr); -- ALTER TABLE을 이용한 인덱스 추가

똑같은 추가 기능

- 테이블 생성 시 인덱스 추가

UNIQUE INDEX idx_userTbl_name (name), -- 항상 유일해야 함
INDEX idx_userTbl_addr (addr)
  • UNIQUE INDEX idx_uerTbl_name (name) : name 컬럼에 대해 idx_userTbl_name 이름으로 인덱스 생성, name 은 UNIQUE 제약조건 필요
  • INDEX idx_userTbl_addr (addr) : addr 컬럼에 대해 idx_userTbl_addr 이름으로 인덱스 생성

 

- 인덱스 삭제

ALTER TABLE userTbl DROP INDEX idx_userTbl_name -- idx_userTbl_name 인덱스 삭제

외래키(FOREIGN KEY)

buyTbl.userID 컬럼을, userTbl.userID에서 참조할 때

INSERT INTO buyTbl (userID, prodName, groupName, price, amount) VALUES('STJ', '운동화', '의류', 30, 2);

userTbluserID가 STJ인 데이터가 없을 경우 입력이 안된다.

👉데이터 무결성 (두 테이블간, 데이터의 정확성을 보장하는 제약 조건)

(현업에서는 비즈니스 로직이 다양하므로 제약을 걸어놓을 경우 예외적인 비즈니스 로직 처리가 어렵기 때문에, 꼭 필요한 경우만 사용한다.)


HAVING

: 집계함수를 가지고 조건비교, GROUP BY절과 함께 사용

SELECT provider, COUNT(*)
FROM items
WHERE provider != '스마일배송'	-- 스마일배송은 제외
GROUP BY provider		-- 판매처별로 그룹
HAVING COUNT(*) >= 100		-- 베스트상품이 100개 이상 등록된 경우만 검색
ORDER BY COUNT(*) DESC;		-- 베스트상품 등록갯수 순으로 검색

JOIN

: 두 개 이상 테이블로부터 필요한 데이터를 연결해 하나의 포괄적인 구조로 결합

SELECT * FROM items
INNER JOIN ranking
ON ranking.item_code = items.item_code
WHERE ranking.main_category = "ALL";

/* [Alias]
SELECT * FROM items a
INNER JOIN ranking b
ON a.item_code = b.item_code
WHERE b.main_category = "ALL";
*/

- 실습 INNER JOIN

# 전체 베스트상품(ALL 메인 카테고리)에서 판매자별 베스트상품 개수 출력
SELECT provider, COUNT(provider) cnt_pvd
FROM items i
INNER JOIN ranking r
ON i.item_code = r.item_code
WHERE r.main_category = "ALL"
GROUP BY provider;

/* 메인 카테고리가 패션의류인 서브 카테고리 포함, 패션의류 전체 베스트상품에서
판매자별 베스트 상품 갯수가 5이상인 판매자와 베스트상품 개수 출력 */
SELECT provider, COUNT(*) cnt_pvd
FROM items i
INNER JOIN ranking r
ON i.item_code = r.item_code
WHERE main_category='패션의류'
GROUP BY provider
HAVING cnt_pvd >= 5;

/* 메인 카테고리가 신발/잡화인 서브 카테고리 포함, 전체 베스트상품에서 
판매자별 베스트상품 갯수가 10이상인 판매자와 베스트상품 갯수를 베스트상품 개수 순으로 출력 */
SELECT provider, COUNT(*) cnt_pvd
FROM items i
INNER JOIN ranking r
ON i.item_code = r.item_code
WHERE main_category='신발/잡화'
GROUP BY provider
HAVING cnt_pvd >= 10
ORDER BY cnt_pvd;

/* 메인 카테고리가 화장품/헤어인 서브 카테고리 포함, 전체 베스트상품의 평균, 최대, 최소 할인 가격 출력 */
SELECT avg(dis_price) avg_pvd,
	   max(dis_price) max_pvd,
	   min(dis_price) min_pvd
FROM items i
INNER JOIN ranking r
ON i.item_code = r.item_code
WHERE main_category='화장품/헤어'; -- GROUP BY 할 필요 없음

- 실습 OUTER JOIN

  • OUTER JOIN은 ON절 조건 중 한쪽 데이터를 모두 가져옴.
  • LEFT, RIGHT으로 기준이 되는 데이터 설정
/* sakila 데이터베이스에서 address_id가 address 테이블에는 있지만,
customer 테이블에는 없는 데이터의 갯수 출력 */

USE sakila; -- 사용할 데이터베이스 변경해야 하니까 이전에 사용하던 다른 데이터가 있다면 새로 작성 후 실행

SELECT COUNT(*) AS cust_addr_null
FROM address addr
LEFT OUTER JOIN customer cust
ON addr.address_id = cust.address_id
WHERE cust.address_id IS NULL; -- customer 테이블엔 없어야 하니까 WHERE절에 is null 조건

SubQuery

: SQL문 안에 () 괄호를 사용해 포함된 SQL문. 테이블과 테이블간 검색 시,
(테이블 중 필요한 부분만 먼저 가져오도록) 검색 범위를 좁히는 기능에 주로 사용

  • JOIN은 출력 결과에 여러 테이블의 열이 필요한 경우 유용
  • 대부분 서브쿼리는 JOIN문으로 처리 가능

(✅강사님피셜 : 현업에서는 JOIN을 쓰는 경우가 조금 더 많다)

 

EX1) 서브 카테고리가 '여성신발'인 상품 타이틀만 가져오기

- JOIN | SubQuery 비교

# JOIN 사용
SELECT title
FROM items
INNER JOIN ranking ON items.item_code = ranking.item_code
WHERE ranking.sub_category = '여성신발';

# 서브쿼리 사용
SELECT title
FROM items
WHERE item_code IN
	(SELECT item_code FROM ranking WHERE sub_category = '여성신발')

 

- 실습

1. 메인 카테고리별로 할인 가격이 10만원 이상인 상품이 몇개 있는지를 출력해보기

# JOIN 사용
SELECT main_category, 
       COUNT(*) AS tot_dis10
FROM items i
INNER JOIN ranking r
ON i.item_code = r.item_code
WHERE dis_price >= 100000
GROUP BY main_category;

# 서브쿼리 사용
SELECT main_category, 
       COUNT(*) AS tot_dis10
FROM ranking
WHERE item_code IN (
	SELECT item_code FROM items
	WHERE dis_price >= 100000
	)
GROUP BY main_category;

2. 'items' 테이블에서 'dis_price'가 200000 이상인 아이템들 중, 각 'sub_category'별 아이템 수 출력

# JOIN 사용
SELECT sub_category,
	COUNT(*) AS cnt_sub
FROM items i
INNER JOIN ranking r 
ON i.item_code=r.item_code
WHERE dis_price >= 200000
GROUP BY sub_category;

# 서브쿼리 사용
SELECT sub_category,
	COUNT(*) AS cnt_sub
FROM ranking
WHERE item_code IN (
	SELECT item_code 
	FROM items i
	WHERE dis_price >= 200000
	)
GROUP BY sub_category;

3. 메인 카테고리, 서브 카테고리에 대해, 평균할인가격과 평균할인율을 출력

SELECT main_category, 
	sub_category, 
	AVG(dis_price) AS avg_dis,
	AVG(discount_percent) AS avg_dis_per
FROM ranking r
INNER JOIN items i
ON r.item_code=i.item_code
GROUP BY main_category, sub_category; -- 여러 개 그룹핑 가능(,콤마 구분)

4. 판매자별, 베스트상품 갯수, 평균할인가격, 평균할인율을 베스트상품 갯수가 높은 순으로 출력

SELECT provider,
	COUNT(*) AS tot_cnt,
	AVG(dis_price) AS avg_dis,
	AVG(discount_percent) AS avg_dis_per
FROM items
GROUP BY provider
ORDER BY tot_cnt DESC;

SQL Query Test

 

Date : 2023-08-01

Topic : sakila, world data SQL 실습


Note

1. Sakila 데이터 실습 (DVD 가게 데이터!)

실제 현업에서 사용하는 데이터베이스에는 다양한 테이블이 있지만,
각 테이블에 대한 매우 친절하고, 상세한 설명이 없는 경우도 많습니다. 데이터를 보며 유추하기도 합니다.

👉테이블과 데이터 사이 관계를 보며 추측해보는 습관!

 

  • 사용한 SELECT 문법
    • LIMIT : 결과 중 일부만 데이터 가져오기
    • COUNT : 결과 수 세기 (데이터 행의 수 세기)
    • DISTINCT : 특정 컬럼값 출력시 중복된 값을 출력하지 않음
    • SUM, AVG, MAX, MIN : 집계 함수, 보통 GROUP BY와 함께 사용
    • GROUP BY : 말 그대로 그룹핑, 문제에서 보통 '~별(로)', '~ 종류에 따른' 이런 문장 구조
    • ORDER BY : 데이터 정렬, DESC (내림차순) | ASC (오름차순, default값)
    • AS : 별칭 부여, count, round 등으로 복잡해진 컬럼명을 가독성 좋게 변경
      • ex) COUNT(*) AS total → count all을 total로 표시 
  • 특정 테이블의 컬럼과 컬럼값 확인을 위해 LIMIT 1 을 많이 사용함
SELECT * FROM city LIMIT 1

city 테이블에 있는 컬럼을 확인할 수 있다.

/* 영화 테이블(film) 에서 영화 등급 종류 알아내기
도메인 : 각 컬럼값 이해를 위한 배경 지식 ex) 각 영화의 영화 등급은 rating 컬럼에 들어 있음 */
SELECT distinct rating FROM film;

/* 영화 렌탈 테이블(rental) 에서 10개 데이터만 출력하기
rental 테이블은 DVD 를 언제, 누가 빌려갔고, 반환했는지에 대한 정보 */
SELECT * FROM rental LIMIT 10;

/* 영화 렌탈 테이블(rental) 에서 inventory_id 가 367 이고, staff_id가 1인 로우(Row) 전체 출력
inventory_id 는 빌려간 DVD 의 ID 를 의미 */
SELECT * FROM rental
WHERE inventory_id=367 AND staff_id=1;

/* payment 테이블에서 렌탈비용 합계, 평균, 최대값, 최소값 구하기
payment 테이블은 렌탈 비용을 포함한 정보 */
SELECT sum(amount) AS SUM_price, 
	   avg(amount) AS AVG_price, 
       max(amount) AS MAX_price, 
       min(amount) AS MIN_price
FROM payment;

SELECT rating, COUNT(rating) AS cnt_rating FROM film
WHERE rating='PG' or rating='G'
# WHERE rating IN ('PG', 'G') 더 짧게 작성 가능
GROUP BY rating;

# 영화(film table)에서 영화가 G 등급인 영화 제목을 출력
SELECT title FROM film
WHERE rating='G';

# 영화(film table)에서 release 연도가 2006 또는 2007 연도이고, 영화가 PG 또는 G 등급인 영화 제목을 출력
SELECT * FROM film
WHERE (release_year=2006 OR release_year=2007)
AND (rating='PG' OR rating='G');

/* SELECT title FROM film
WHERE release_year IN (2006, 2007)
AND rating IN ('PG', 'G'); */

/* film테이블에서 rating (등급)으로 그룹을 묶어서, 각 등급별 영화 갯수와 각 등급별 평균 렌탈 비용 출력.
단, 평균 렌탈비용이 높은 순으로 출력 (등급과 각 등급별 갯수, 각 등급별 평균 렌탈 비용 출력하기) */
SELECT rating,
	   COUNT(*) AS total_films, 
       round(AVG(rental_rate), 2) AS avg_rental_rate
FROM film
GROUP BY rating
ORDER BY avg_rental_rate DESC;

# 각 등급별 영화 길이가 130분 이상인 영화의 갯수와 등급을 출력
SELECT rating,
       COUNT(*) AS total_films
FROM film
WHERE length>=130
GROUP BY rating;
테이블과 테이블 간의 관계
inventory_id, customer_id 와 같이 다른 테이블에 상세 정보가 있고, 각 정보를 구별할 수 있는 ID 값으로 연결되어
있는 경우가 많음
👉하나의 테이블에 모든 정보를 담지 않는다!

2. world 데이터 실습

sakila data와 똑같은 문법 순서대로 쿼리를 작성해본다.

(중복되는 문제는 작성해보고 출력이 정확히 되는지만 확인하고 일지에 적지 않았다.)

# city 테이블에서 국가코드 수 추출
SELECT COUNT(DISTINCT CountryCode) AS cnt_code FROM city;
# DISTINCT 키워드를 통해 중복값을 제외한 국가코드의 수를 카운트한다.

# countrylanguage 테이블에서 전체 언어 수 추출
SELECT COUNT(DISTINCT language) AS tot_lang FROM countrylanguage;
# 중복값을 제외한 언어만 카운트해야 한다.

# 각 국가별 도시 중 최대 Population과 해당 국가코드를 출력
SELECT CountryCode, MAX(population) AS max_pop
FROM city
GROUP BY CountryCode;

# 각 Continent 별 총 SurfaceArea 과 해당 Continent 를 출력 (country 테이블 기반)
SELECT Continent, SUM(SurfaceArea) AS tot_SA
FROM country
GROUP BY Continent;

# 각 Region 별 평균 GNP 와 최소 IndepYear, 그리고 해당 Region 를 출력
SELECT Region,
	ROUND(AVG(GNP), 2) AS avg_gnp,
	MIN(IndepYear) AS min_indepyear
FROM country
GROUP BY Region;

# 각 Region별, IndepYear가 1900 이상인 국가만 평균 GNP가 낮은 순으로 5개의 Region만 평균 GNP를 출력
SELECT Region, ROUND(AVG(GNP), 2) AS AvgGNP
FROM country
WHERE IndepYear >= 1900
GROUP BY Region
ORDER BY AvgGNP
LIMIT 5;

마지막 문제


문제가 길어지면 쉬운 쿼리도 복잡하고 헷갈리는 경우가 많았는데 하나의 큰 문제를 단계별로 쪼개서 생각하고 작성한 뒤 합쳐보니 구조나 데이터 관계도 이해하기 쉬워졌다. 예전에는 문장이 늘어나는 게 싫어서 최소한으로 개행하고 들여쓰기를 했는데, 쿼리가 길어지면 엔터와 탭을 잘 써야 readability가 좋아진다 하셨다. 아직은 연습문제라 쿼리가 그렇게 길지 않지만 나중에 코딩테스트와 실무를 대비해 일찍 습관을 들이도록 해야겠다.

(+ 또한 가독성이 좋게 키워드는 대문자, 컬럼명이나 테이블명은 소문자로 쓰는 습관&별칭은 되도록 키워드와 겹치지 않도록 _ (under bar)나 축약하여 대소문자 구분으로 확실하게 표기(ex: avg_gnp, AvgGNP 등등) )

Date : 2023-07-28

Topic : MySQL에서 SQL Query 작성해보기


Note

테이블 생성

USE customer_db;
CREATE TABLE customer_db (
	no INT UNSIGNED NOT NULL AUTO_INCREMENT,
	name VARCHAR(50) NOT NULL,
	age TINYINT,
	phone VARCHAR(20),
    email VARCHAR(30) NOT NULL,
    address VARCHAR(30) NOT NULL,
	PRIMARY KEY(no)
	);

/* 컬럼 추가 */
ALTER TABLE customer_db ADD COLUMN hobby VARCHAR(20);
/* 컬럼 타입 수정 */
ALTER TABLE customer_db MODIFY COLUMN name VARCHAR(20);
/* 컬럼 변경 (폰에서 모바일로) */
ALTER TABLE customer_db CHANGE COLUMN phone mobile VARCHAR(30);
/* 컬럼 삭제 */
ALTER TABLE customer_db DROP COLUMN hobby;

/* 테이블 구조를 확인할 때 DESC [테이블명] */
DESC customer_db;

출력

- UNSIGNED : 음수 제외한 정수 타입만

- AUTO_INCREMENT : 새 레코드가 테이블에 삽입될 때 열에 대해 고유한 값을 자동으로 생성 (각 레코드에 고유 식별

가 있는지 확인하기 위해 일반적으로 기본 키에 사용) 


연습 문제1 (데이터 검색)

CREATE TABLE mytable (
       id INT UNSIGNED NOT NULL AUTO_INCREMENT,
       name VARCHAR(20) NOT NULL,
       model_num VARCHAR(10) NOT NULL,
       model_type VARCHAR(10) NOT NULL,
       PRIMARY KEY(id)
);
INSERT INTO mytable (name, model_num, model_type) 
VALUES ('i5', '13400F', '랩터레이크');
INSERT INTO mytable (name, model_num, model_type) 
VALUES ('i5', '12400F', '엘더레이크');
INSERT INTO mytable (name, model_num, model_type) 
VALUES ('i5', '13400', '랩터레이크');
INSERT INTO mytable (name, model_num, model_type) 
VALUES ('i3', '13500', '랩터레이크');
INSERT INTO mytable (name, model_num, model_type) 
VALUES ('i5', '13500', '랩터레이크');

SELECT * FROM mytable;

- EX1: model_num 이 13400 으로 시작하는 로우(Row) 검색하기

SELECT * FROM mytable WHERE model_num LIKE '13400%';

 

- EX2: name 이 i5 인 로우(Row) 검색하기

SELECT * FROM mytable WHERE name LIKE '%i5%';

 

- EX3: model_type 이 엘더레이크 인 로우(Row) 를 1개만 검색하기(LIMIT 사용)

SELECT * FROM mytable WHERE model_type LIKE '%엘더레이크%' LIMIT 1;

 

연습문제2 (테이블 수정, 데이터 수정, 검색)

- EX1 : lowest_price(컬럼명) INT UNSIGNED(데이터 타입) 으로 컬럼 추가

ALTER TABLE mytable ADD COLUMN lowest_price INT UNSIGNED;

- EX2 : 웹페이지(https://www.enuri.com/list.jsp?cate=070701)에서 1~5위까지 lowest_price 값 수정하기

  • lowest_price값은 정품 최저 가격으로 입력
  • 예시) 222,180원 → 222180
/* id값을 기준으로 조건을 걸어 최저가 데이터 수정하기 */
UPDATE mytable SET lowest_price = 254310 WHERE id = 1;
UPDATE mytable SET lowest_price = 171910 WHERE id = 2;
UPDATE mytable SET lowest_price = 285920 WHERE id = 3;
UPDATE mytable SET lowest_price = 150810 WHERE id = 4;
UPDATE mytable SET lowest_price = 313700 WHERE id = 5;

- EX3 : lowest_price 이 300000 이하인 로우(Row) 중에서 name과 model_num만 검색하기
. 이하는 같거나, 작은 값을 의미하고, 조건으로는 <= 와 같이 작성한다.

SELECT name, model_num FROM mytable WHERE lowest_price <= 300000;

name, model_num만 출력하라고 했지만 조건값 성립을 확인하기 위해 lowest_price까지 출력

 

- EX4 : lowest_price 이 400000 이상인 로우(Row) 만 검색하기
. 이상은 같거나, 큰 값을 의미하고, 조건으로는 >= 와 같이 작성한다.

SELECT * FROM mytable WHERE lowest_price >= 400000;

DCL(Data Control Language)

▶️SQL DCL 명령은 mysql 관리자(데이터베이스 관리자)를 위한 명령이므로, 가볍게 알아두기!

 

mysql 사용자 확인, 추가, 비밀번호 변경, 삭제

/* mysql 사용자 확인 */
# mysql -u root -p
use mysql;
select * from user;

/* 사용자 추가 */
# mysql -u root -p
use mysql;

/* 1) 로컬에서만 접속 가능한 userid 생성 */
create user 'userid'@localhost identified by '비밀번호';
/* 2) 모든 호스트에서 접속 가능한 userid 생성 */ 
create user 'userid'@'%' identified by '비밀번호';

/* 사용자 비밀번호 변경 */
SET PASSWORD FOR 'userid'@'%' = '신규비밀번호';

/* 사용자 삭제 */
# mysql -u root -p
use mysql;
drop user 'userid'@'%';

 

mysql 접속 허용 관련 설정

/* 현재 부여된 권한 확인하기 */
SHOW GRANTS for 아이디;
예) SHOW GRANTS for 'davelee'@'%'

/* 로컬에서만 접속 허용 */
GRANT ALL ON DATABASE.TABLE to 'root'@localhost;

/* 특정 권한만 허용 */
GRANT SELECT, UPDATE ON DATABASE.TABLE to
'root'@localhost;

/* 옵션 상세
ALL – 모든 권한 / SELECT, UPDATE – 조회, 수정 권한등으로 권한 제한 가능
예) GRANT INSERT,UPDATE,SELECT ON *.* TO
'username'@'localhost';
(2) DATABASE.TABLE – 특정 데이터베이스에 특정 테이블에만 권한을 줄 수 있
음 / *.* – 모든 데이터베이스에 모든 테이블 권한을 가짐
(3) root – 계정명
(4) funcoding – 계정 비밀번호 */

연습

use mysql;
select * from user;
create user '아이디'@localhost identified by '비밀번호';
GRANT ALL ON *.* to '아이디'@localhost;


SQL 실습은 매번 DDL, DML만 하다가 DCL은 처음 만져봤는데 연습해본다고 이것저것 많이 실행을 했더니 유저 아이디가 여러 개 생성 됐는데 삭제가 안돼서 당황스럽다..ㅎㅎ 왜 쿼리 그대로 작성하는데 나는 실행이 안되는건지 sql이든 파이썬이든 배울 때마다 의문이 든다. 초심자 코드의 신비...ㅠㅠ 주말에 다시 열심히 복습하고 ! 강사님께 질문할 것도 많이 생각해봐야겠다. SQL 첫 수업 끝⭐

Date :  2023-07-27

Topic : SubQuery, case when, with


Note

SubQuery란?

하나의 SQL 쿼리 안에 또다른 SQL 쿼리가 있는 것. subquery를 사용하지 않고도 원하는 데이터를 추출할 수 있지만, 서브쿼리를 이용하여 하위 쿼리의 결과를 상위 쿼리에서 사용하면 SQL 쿼리가 훨씬 간단해진다. 즉, 더 편하고 간단하게 사용하기 위한 기능!

* 주어진 데이터를 유의미한 정보로 만들기

  • 문자열을 한 번에 정리하기
  • 조건에 따라 데이터를 구분

subquery 사용 방법

  • EX 1 : kakaopay로 결제한 유저들의 정보 보기
    • 기존 join을 사용한 방식
      select u.user_id, u.name, u.email from users u
      inner join orders o on u.user_id = o.user_id
      where o.payment_method = 'kakaopay'
      👉 테이블을 합친 뒤에 값들을 필터링하여 payment_method가 kakaopay인 값들을 남기는 방식 (선 연결 - 후 필터링)

    • subquery를 사용한 방식

1. 우선 kakaopay로 결제한 user_id를 모두 구해보기 → 이 값을 K라고 임의로 지칭.

select user_id from orders
where payment_method = 'kakaopay'

2. 그 후에, user_id가 K에 있는 유저들만 골라보기 → 이게 바로 서브쿼리!

select u.user_id, u.name, u.email from users u 
where u.user_id in ( 
	select user_id from orders
    	where payment_method = 'kakaopay'
        )

👉 테이블을 합치기 전 값들을 필터링한 뒤 테이블로 합치는 방식 (선 필터링 - 후 연결)

서브쿼리가 있는 코드와 없는 코드의 차이점

: 위에 작성한 것을 토대로 하면, 첫 번째 쿼리문은 A라는 유저가 a를 구매한 것, A라는 유저가 b를 구매한 것 모두 테이블로 합쳐져서(join되어) A 유저의 데이터가 2번 들어가지만,

두 번째 쿼리문에서는 A라는 유저가 카카오페이로 결제한 경우(a, b)가 orders 테이블에 있으므로 'A 유저를 하나의 테이블에 넣는다' 라는 순서의 차이가 생긴다.

따라서 이런 결과의 차이로 추출된 데이터는 비슷해보이지만 출력값의 개수가 다르게 나오게 된다!

 

자주 쓰이는 subquery 유형

  • where (조건문)
    : 기존 테이블에 함께 보고싶은 통계 데이터를 손쉽게 붙이는 것에 사용

where 필드명 in (subquery)

ex) 카카오페이로 결제한 주문건 유저들만, 유저 테이블에서 출력해주고 싶을 때

select * from users u 
where u.user_id in (
	select o.user_id from orders o 
    	where o.payment_method = 'kakaopay' 
    	);

< 쿼리 실행 순서 >
(1) from 실행: users 데이터를 가져와줌
(2) Subquery 실행: 해당되는 user_id의 명단을 뽑아줌
(3) where .. in 절에서 subquery의 결과에 해당되는 'user_id의 명단' 조건으로 필터링 해줌
(4) 조건에 맞는 결과 출력


  • select (결과값)
    : Subquery의 결과를 조건에 활용하는 방식으로 사용

select 필드명, 필드명, (subquery) from ..

  • ex) '오늘의 다짐' 데이터를 보고 싶은데 '오늘의 다짐' 좋아요의 수가, 본인이 평소에 받았던 좋아요 수에 비해 얼마나 높고 낮은지

1. 서브쿼리 작성
→ 평균 먼저 구하기 (예시: user_id='4b8a10e6')

select avg(likes)
from checkins c2
where c2.user_id = '4b8a10e6'

2. 상위 쿼리 작성

SELECT c.checkin_id,
	c.user_id,
	c.likes,
	(SELECT AVG(likes) from checkins 
	WHERE user_id = c.user_id 
	) as avg_likes_users
FROM checkins c

< 쿼리 실행 순서 >
(1) 밖의 select * from 문에서 데이터를 한줄한줄 출력하는 과정에서
(2) select 안의 subquery가 매 데이터 한줄마다 실행되는데
(3) 그 데이터 한 줄의 user_id를 갖는 데이터의 평균 좋아요 값을 subquery에서 계산해서
(4) 함께 출력해준다!


  • from : 내가 만든 Select와 이미 있는 테이블을 Join하고 싶을 때 사용 (가장 많이 사용하는 유형!⭐⭐⭐)
    ex) 유저 별 좋아요 평균 구하기
  1. 서브쿼리 작성
    → checkins 테이블을 user_id로 group by
    select user_id, round(avg(likes),1) as avg_like from checkins
    group by user_id
  2. 상위 쿼리 작성
    → 해당 유저 별 포인트를 구해서 포인트와 like의 상관정도를 알 수 있음
    SELECT pu.user_id, pu.`point`, a.avg_likes
    FROM point_users pu
    INNER JOIN (
    	SELECT c.user_id, ROUND(AVG(likes),1) as avg_likes
    	FROM checkins c 
    	GROUP BY c.user_id
    	) a ON pu.user_id = a.user_id;
    < 쿼리 실행 순서 >
    (1) 먼저 서브쿼리의 select가 실행되고,
    (2) 이것을 테이블처럼 여기고 밖의 select가 실행!

with

: 더 깔끔하게 쿼리를 작성할 수 있다.
ex) 코스 제목별 like 개수, 전체, 비율을 구할 때 이렇게 서브쿼리가 계속 붙으면 inner join 안쪽이 헷갈리게 된다.

select c.title,
       a.cnt_checkins,
       b.cnt_total,
       (a.cnt_checkins/b.cnt_total) as ratio
from
(
	select course_id, count(distinct(user_id)) as cnt_checkins from checkins
	group by course_id
) a
inner join
(
	select course_id, count(*) as cnt_total from orders
	group by course_id 
) b on a.course_id = b.course_id
inner join courses c on a.course_id = c.course_id

→ 이때 아래처럼 with절을 사용하면 결과는 같지만 훨씬 깔끔해보인다!

with table1 as (
	select course_id, count(distinct(user_id)) as cnt_checkins from checkins
	group by course_id
), table2 as (
	select course_id, count(*) as cnt_total from orders
	group by course_id
)
select c.title,
       a.cnt_checkins,
       b.cnt_total,
       (a.cnt_checkins/b.cnt_total) as ratio
from table1 a inner join table2 b on a.course_id = b.course_id
inner join courses c on a.course_id = c.course_id

문자열 정리하기

: 실전 업무에서는 문자열 데이터를 원하는 형태로 한번 정리해야 하는 경우가 많다.

  • 문자열 쪼개기
    → SUBSTRING_INDEX을 사용해주면 된다.
    ex) 이메일 주소에서 @앞의 아이디만 가져오거나, @뒤의 이메일 도메인을 가져오려면?
    * 이메일에서 아이디만 가져오려면?
select user_id, email, SUBSTRING_INDEX(email, '@', 1) from users

👉 @를 기준으로 텍스트를 쪼개고, 그 중 첫 번째 조각을 가져오라는 뜻!

*이메일에서 이메일 도메인만 가져오려면?

select user_id, email, SUBSTRING_INDEX(email, '@', -1) from users

👉 @를 기준으로 텍스트를 쪼개고, 그 중 마지막 조각을 가져오라는 뜻!

  • 문자열 일부만 출력하기
    → SUBSTRING을 사용해주면 된다.SUBSTRING(문자열, 출력을 하고싶은 첫 글자의 위치몇개의 글자를 출력하고 싶은지)

ex 1 ) orders 테이블에서 created_at을 날짜까지만 출력하려면?

select order_no, created_at, substring(created_at,1,10) as date from orders

ex 2 ) 일별로 몇 개씩 주문이 일어났는지,

select substring(created_at,1,10) as date, count(*) as cnt_date from orders
group by date

Case

: 경우에 따라 원하는 값을 새 필드에 출력.
ex) 10000점보다 높은 포인트를 가지고 있으면 '잘 하고 있어요!', 평균보다 낮으면 '조금 더 달려주세요!' 라고 표시해 주려면 어떻게 해야할까?

select pu.point_user_id, pu.point,
case 
when pu.point > 10000 then '잘 하고 있어요!'
else '조금 더 달려주세요!'
END as '구분'
from point_users pu;

case와 subquery를 이용하여 통계 내기

  • 우선 몇 가지로 구분을 나누고,
select pu.point_user_id, pu.point,
case 
when pu.point >= 10000 then '1만 이상'
when pu.point >= 5000 then '5천 이상'
else '5천 미만'
END as level
from point_users pu
  • 서브쿼리를 이용해서 group by로 통계를 낸다.
	SELECT a.level, COUNT(*) as cnt 
	FROM (
		SELECT pu.user_id , pu.`point` , 
				(case when pu.`point` > 10000 then '1만 이상'
					  when pu.`point` > 5000 then '5천 이상'
				else '5천 미만' end)
				as level
		FROM point_users pu 
	) a
	group by a.level
  • with절을 사용하면?
	with table1 as (SELECT pu.user_id , pu.`point` , 
				(case when pu.`point` > 10000 then '1만 이상'
					  when pu.`point` > 5000 then '5천 이상'
				else '5천 미만' end)
				as level
		FROM point_users pu )

	SELECT a.level, COUNT(*) as cnt 
	FROM table1 a
	group by a.level

연습문제

  • EX 1 : 평균 이상 포인트를 가지고 있으면 '잘 하고 있어요' / 낮으면 '열심히 합시다!' 표시하기!
    * hint : CASE 문법 사용, CASE 안에서 Subquery로 평균을 계산하여 비교해보기
	SELECT point_user_id, `point`,
			(
			case when point >
			(select avg(point) from point_users pu) then '잘하고 있어요!'
			else '열심히 합시다!' end
			) as msg
	FROM point_users pu
  • EX 2 : 이메일 도메인별 유저의 수 세어보기
    * hint : SUBSTRING_INDEX와 Group by를 잘 사용하면 끝!
	SELECT domain, count(domain) as cnt_domain
	FROM (select SUBSTRING_INDEX(u.email,'@',-1) as domain
		  from users u) a
	GROUP BY domain
  • EX 3 : '화이팅'이 포함된 오늘의 다짐만 출력해보기
    * hint : like를 어떻게 썼더라~
SELECT * FROM checkins c 
WHERE c.comment LIKE '%화이팅%'
  • EX 4 : 수강등록정보(enrolled_id)별 전체 강의 수와 들은 강의의 수, 그리고 진도율 출력해보기
    * hint : 진도율 = (들은 강의의 수 / 전체 강의 수)
	SELECT a.enrolled_id, cnt_done, cnt_tot,
		   ROUND((cnt_done/cnt_tot),2) as ratio
	FROM
		(SELECT enrolled_id, COUNT(*) as cnt_tot
		FROM enrolleds_detail ed 
		GROUP BY enrolled_id) a
	INNER JOIN
		(SELECT enrolled_id, COUNT(*) as cnt_done
		FROM enrolleds_detail ed
		WHERE done = '1'
		GROUP BY enrolled_id) b
	ON a.enrolled_id = b.enrolled_id;

with절을 사용하여 작성

	with table1 as (
		SELECT enrolled_id, COUNT(*) as cnt_tot
		FROM enrolleds_detail ed 
		GROUP BY enrolled_id
	),
	table2 as (
		SELECT enrolled_id, COUNT(*) as cnt_done
		FROM enrolleds_detail ed
		WHERE done = '1'
		GROUP BY enrolled_id
	)

	SELECT a.enrolled_id,
		   cnt_done,
		   cnt_tot,
		   ROUND((cnt_done/cnt_tot),2) as ratio
	FROM table1 a
	INNER JOIN table2 b
	ON a.enrolled_id = b.enrolled_id;
  • EX 5 : 그러나, 더 간단하게 만들 수 있지 않을까!
	select enrolled_id,
	       sum(done) as cnt_done,
	       count(*) as cnt_total
	from enrolleds_detail ed
	group by enrolled_id

해설: 수강한 강의는 데이터 값이 1(들은 강의)과 0(안 들은 강의)로 저장한다. 들은 강의는 done=1이고, 만약 들은 강의가 3개라고 하면(counting:3) 1+1+1이므로 합계는 3이 된다. 따라서 sum(done)을 쓴다면 done의 합계가 나올테니 count한 값과 같다. 이는 데이터가 1이기 때문에 이렇게 간단한 쿼리로 작성할 수 있던 것이다. 정석은 없지만 조금만 생각의 전환을 하면 복잡하지 않게 결과를 도출할 수 있다.

  • EX 6 : 이씨 성을 가진 유저의 포인트의 평균보다 큰 유저들의 데이터 추출하기
    * hint : 위 구문의 서브쿼리 내에서 users와 inner join 하기
	SELECT * FROM point_users pu 
	WHERE point > (
		SELECT ROUND(AVG(`point`),2) as avg_pnt
		FROM point_users pu
		INNER JOIN users u 
		on pu.user_id = u.user_id
		WHERE name LIKE '%이**'
		);

필요한 경우, Subquery 안에서 여러 테이블을 Join 할 수 있다.

  • EX 7 : checkins 테이블에 과목명별 평균 likes수 필드 우측에 붙여보기
    * hint : Select 절에 들어가는 Subquery
	SELECT c.checkin_id,
		   c2.title,
		   c.user_id,
		   c.likes,
		   (
		   SELECT ROUND(AVG(likes),1) FROM checkins
		   WHERE course_id = c.course_id
		   ) as avg_course_id
	FROM checkins c
	inner join courses c2
	on c.course_id = c2.course_id 
  • EX 8 : course_id별 checkins개수와 전체 인원, 비율 구하기
    * 작성 순서 :
    1. course_id별 유저의 체크인 수를 구하고
    2. course_id별 인원을 구하고
    3. [진짜 구하려는 값] course_id별 checkins개수에 전체 인원을 붙인다
      => inner join 활용
    4. 비율을 구할 땐 checkins개수를 전체 개수로 나누고
    5. 보기 좋게 course_id 대신 제목을 붙이기 위해 title 필드로 변경.
      ※쿼리가 길어지므로 줄맞춤을 잘 해주고,
	SELECT c.title,
		   a.cnt_checkins, 
		   b.cnt_tot,
		   (a.cnt_checkins/b.cnt_tot) as ratio
	FROM (
		SELECT course_id, count(DISTINCT(user_id)) as cnt_checkins
		FROM checkins c 
		GROUP BY c.course_id
		) a 
	INNER JOIN
		(
		SELECT course_id, COUNT(*) as cnt_tot
		FROM orders o
		GROUP BY course_id
		) b
	ON a.course_id = b.course_id
	INNER JOIN courses c
	ON a.course_id = c.course_id 
  • EX 9 : 위 구문을 with절로 보기 좋게 정리하기
	with table1 as (
		SELECT course_id, count(DISTINCT(user_id)) as cnt_checkins
		FROM checkins c 
		GROUP BY c.course_id
		),
		table2 as (
		SELECT course_id, COUNT(*) as cnt_tot
		FROM orders o
		GROUP BY course_id
		)

	SELECT c.title,
		   a.cnt_checkins, 
		   b.cnt_tot,
		   (a.cnt_checkins/b.cnt_tot) as ratio
	FROM table1 a 
	INNER JOIN
	table2 b
	ON a.course_id = b.course_id
	INNER JOIN courses c
	ON a.course_id = c.course_id 

with절에는 임시테이블을 형성하여 조건이 들어 있고 select문을 보면 쿼리 형식을 한 눈에 볼 수 있게 된다.


회고

  • select (subquery) 예제에서 where user.id = c.user.id인 이유
    → checkin 테이블이 서브쿼리와 전체쿼리에 두 번 적용되니, 상위 쿼리에는 별칭 c를 붙여 구분을 한다. 그리고 checkins c의 user.id를 서브쿼리 user.id와 연결되어 좋아요 평균을 구하고 select를 통해 마지막 필드로 추출하게 된다.
  • from절에 사용한 서브쿼리는 → 마치 원래 있던 테이블처럼 사용할 수 있게 된다.
    내가 만들고자 하는 테이블을 각각 어떻게 생성하고 조인할지 등등 쪼개서 생각하는 것이 중요
  • 서브쿼리와 메인쿼리에 같은 테이블을 사용할 때, alias 혼용하지 않도록 주의한다. 이 문제로 에러가 자주 났다. 항상 인지하고 쿼리를 작성할 것.
    ex) checkins c (메인쿼리), checkins c1 (서브쿼리)
  • 쿼리가 길어질수록 헷갈리기 쉬우니 줄맞춤(tab)과 alias를 잘 활용해준다.
  • 퀴즈2를 푸는데 이런 에러가 났다.
    SQL Error [1248][42000]: Every derived table must have its own alias
    처음 보는 에러였지만 문장 그대로, 파생된 모든 테이블에는 고유한 별칭이 있어야 한다는 것을 알았다. from절에 만든 서브쿼리에 별칭을 만들어주면 해결된다.
  • like 쓸 때 @와 % 헷갈리지 않기. %사용해야 함!!
    ex) 이메일 도메인만 추출할 때, like '%naver.com'를 자꾸 'like @naver.com'라고 쓴다던가,,
  • SQL Error [1241][21000]: Operand should contain 1 column(s)
    → 피연산자는 1개의 열을 포함해야 한다.
  • 아무래도 입문자다 보니 처음보는 에러가 많아서 쿼리를 짤 때 시간이 더 오래 걸린다. 에러 위주로 정리해보기
  • 위에 퀴즈5 해설에도 썼지만 가끔은 어려워 보이는 문제도 조금만 관점을 바꿔서 생각해보면 아주 간단하게 짤 수 있다. with절도 마찬가지로 처음 배울 땐 괜히 행도 길어지는 것 같고 더 복잡해보였는데 구조를 알고 나니 서브쿼리만 사용했을 때보다 훨씬 직관적이었다. 내가 아는 정보 내에서 최대한 효율적으로 쿼리를 짜는 습관을 들일 수 있도록 많은 실습이 필요할 것이다.!
아무 것도,, 안하고 싶다,,,

Sephora Online Data Analysis : 온라인 시장에서의 화장품 매출 증진을 위한 인사이트 발굴

 

Python Mini Project

Sephora Online Data Analysis: 온라인 시장에서의 화장품 매출 증진을 위한 인사이트 발굴

bb2-bb5.notion.site

🔻Click✔️

더보기

초안

  • 베이스 데이터로 세울 수 있는 시나리오(가설)
    1. 시간대-상품 카테고리: 특정 시간대에 어떤 상품 카테고리가 인기가 있는지
      1. 평일 6-10시 시간대에는 어떤 상품 카테고리가 인기가 있을까?
    2. 성별-연령대: 특정 상품 카테고리를 성별과 연령대에 따라 분석 가능
      1. 유아용품은 어떤 성별과 연령대에서 인기가 있는지
      2. 패션 카테고리에서 어떤 성별과 연령대의 소비가 높은지
    3. 요일-평일/휴일: 특정 상품 카테고리가 요일 또는 평일/휴일에 따라 어떻게 변하는지
      1. 화장품은 주로 어떤 요일에 소비가 많은지
      2. 평일과 휴일에 어떤 상품 카테고리의 소비가 다른지
    4. 건수합계 비교: 각 상품 카테고리의 건수합계를 비교하여 가장 인기 있는 or 선호도가 낮은 상품 카테고리를 확인
      1. 가장 많은 건수합계를 가진 상품 카테고리
    5. 시간대별 성별 차이: 특정 시간대에서 남성과 여성의 소비 패턴이 다를지
      1. 6-10시 시간대에 남성과 여성의 구매량 차이
    6. 연령대별 상품 카테고리 선호도: 각 연령대에서 어떤 상품 카테고리가 인기 있는지
      1. 2O대와 60대이상에서 가전 제품에 대한 관심도의 차이
    7. 요일별 휴일/평일 소비 차이: 특정 요일에서 평일과 휴일의 소비 패턴이 다를 수 있음(가설)
      1. 토요일과 월요일에서 생활/사무용품 카테고리의 소비 차이는 있을까?
    8. 건수합계-상품 카테고리 유형: 어떤 상품 카테고리가 전체 건수합계에 더 큰 영향을 미치는지
      1. 생활/사무용품과 홈쇼핑 중에서 어떤 카테고리가 전체 건수합계에 더 많은 영향을 미치는지
  • 각 가설들을 다른 데이터와 접목시켜 새로운 인사이트를 도출
    1. 시간대-상품 카테고리: 특정 시간대에 인기 있는 상품 카테고리는 무엇인가?
      • 어떤 지역이나 계절에 따른 인기 상품을 파악하면 해당 상품을 관련 상품과 함께 판매함으로써 추가적인 수익을 올릴 수 있다.
    2. 요일-평일/휴일: 특정 상품 카테고리가 요일 또는 평일/휴일에 따라 어떻게 변하는가?
      • 특정 요일이나 휴일에 해당 상품을 할인 판매함으로써 추가적인 수익을 올릴 수 있습니다.
    3. 요일별 휴일/평일 소비 차이: 특정 요일에서 평일과 휴일의 소비 패턴이 다를 수 있다. (가설)
      • 해당 요일에 맞는 마케팅 전략을 세움으로써 추가적인 수익 기대
    마케팅 전략 수립
    1. 성별-연령대: 성별과 연령대에 따라 특정 상품 카테고리의 소비 패턴을 분석한다.
      • 해당 성별과 연령대의 고객에게 해당 상품을 추천하는 마케팅 전략을 수립
    2. 시간대별 성별 차이: 특정 시간대에서 남성과 여성의 소비 패턴이 다른가?
      • 해당 시간대에 남성과 여성에게 맞는 상품을 추천하는 마케팅 전략 수립
    3. 연령대별 상품 카테고리 선호도: 각 연령대에서 어떤 상품 카테고리가 인기 있는가?
      • 해당 연령대의 고객에게 맞는 상품을 추천하는 마케팅 전략 수립
    재고 파악 및 불필요한 지출 점검
    1. 건수합계 비교: 각 상품 카테고리의 건수합계를 비교하여 가장 인기 있는 또는 선호도가 낮은 상품 카테고리를 파악한다.
      • 건수합계-상품 카테고리 유형: 어떤 상품 카테고리가 전체 건수합계에 더 큰 영향을 미치는가?
    2. 건수합계-상품 카테고리 유형: 어떤 상품 카테고리가 전체 건수합계에 더 큰 영향을 미치는가?
      • 해당 데이터를 바탕으로, 인기 상품을 재고로 유지하는 것이 경제적이고, 인기가 낮은 상품은 할인 판매함으로써 재고 줄이기 가능
  • 추가 수익을 올리는 방법
  • Sephora Senario
    • **인기도(loves_count)-평가(rating)**를 분석하여 고객들이 선호하는 제품을 파악
    • 리뷰(reviews) 데이터 활용, 제품의 품질과 성능에 대한 피드백 (전처리 까다로울 듯)
    • 가격(price_usd)-할인 가격(sale_price_usd), 제품의 가격 변동과 세일 이벤트에 대한 인사이트
      • 혹은 너무 자주 or 너무 많이 세일하는 품목은 인기가 높아서일까? 없어서일까?
    • limited_edition-sephora_exclusive 특별한 제품 전략?
      • sephora_exclusive : 세포라에서만 구매할 수 있는 제품의 인기와 효과
    • 제품의 주요 카테고리(primary_category)-세부 카테고리(secondary_category, tertiary_category) :정보를 분석하여 특정 제품 분야의 인기와 성장 가능성을 파악

진행 과정

  1. 주제 선정 - Sephora Online Data Analysis
  2. 분석 목표 및 인사이트 — 온라인 시장에서의 화장품 매출 증진을 위한 인사이트 발굴 —
  3. 데이터 수집 - kaggle (+googling)
    1. 데이터 전처리
    2. product_info dataset 1
    3. (skincare) reviews dataset 6 (0~250, 250~500, … , 1500)
      1. 6개 리뷰 데이터를 하나로 병합 후 구하고자 하는 기간 설정 (2022년 1월 ~ 2023년 3월) ⇒ 283,817 행, 18개 열
      2. 상품 데이터는 8,494행, 27열
    4. 상품+리뷰 데이터 병합(a+b)으로 새로운 merged_df 생성 (메인데이터)
    5. 사용하지 않을 컬럼 삭제 후 26개 열만 남김
    6. 결측치, 이상치 체크 (missingno 라이브러리 활용) ⇒ 삭제하기로 최종 결정
  1. 데이터 분석(과정)
  2. 취합 및 발표 자료 제작(ppt)
  3. 9조 파이썬 미니 프로젝트.pdf
  4. 최종 발표(7/26)

발표 자료

✅click [슬라이드 정리]

Date :  2023-07-27

Topic : JOIN, NULL, UNION


Note

JOIN이란?
👉두 테이블의 공통된 정보(=key값)를 기준으로 테이블을 연결해서 한 테이블처럼 보는 것을 의미. 엑셀의 vlookup과 같다!

 

자주 쓰는 join

  • left join : 왼쪽 테이블을 기준으로 공통된 데이터를 연결.
    따라서, left join은 어디에/무엇을 붙일건지, 순서가 중요
  • inner join : 각 테이블의 공통된 데이터.(=교집합)

연습문제

EX 1 : orders 테이블에 users 테이블 연결하려면,

SELECT * FROM orders o 
INNER JOIN users u 
ON o.user_id = u.user_id;

※ 주문을 하기 위해서는 회원정보가 있어야 할테니, orders 테이블에 담긴 user_id는 모두 users 테이블에 존재함.
※ 위 쿼리가 실행되는 순서: from → join → select
(join은 항상 from과 붙어 있다.)

연결의 기준이 되고싶은 테이블을 from 절에,
기준이 되는 테이블에 붙이고 싶은 테이블을 Join 절에 위치!

EX 2 : checkins 테이블에 courses 테이블 연결해서 통계치 내보기

'오늘의 다짐' 정보에 과목 정보를 연결해 과목별 '오늘의 다짐' 갯수를 세어보려면,

SELECT c2.title, COUNT(*) as cnt_title
FROM checkins c  
INNER JOIN courses c2
ON c.course_id = c2.course_id 
GROUP BY c2.title ;

결과 👉



EX 3 : point_users 테이블에 users 테이블 연결해서 순서대로 정렬해보기

유저의 포인트 정보가 담긴 테이블에 유저 정보를 연결해서, 많은 포인트를 얻은 순서대로 유저의 데이터를 뽑으려면,

SELECT * FROM point_users pu 
INNER JOIN users u 
ON pu.user_id = u.user_id 
ORDER BY pu.point DESC ;

결과 👉

 

EX 4 : orders 테이블에 users 테이블 연결해서 통계치 내보기

주문 정보에 유저 정보를 연결해 네이버 이메일을 사용하는 유저 중, 성씨별 주문건수를 세어보려면,

SELECT u.name, COUNT(u.name) as name_cnt
FROM orders o 
INNER JOIN users u 
ON o.user_id = u.user_id 
WHERE o.email LIKE '%naver.com'
GROUP BY u.name ;

결과 👉

위 쿼리가 실행되는 순서: from → join → where → group by → select
1. from : orders 테이블 데이터 전체를 가져옴.
2. join : users와 orders 테이블 연결, orders 테이블의 user_id와 동일한 user_id를 갖는 users 테이블 데이터를 붙임. (users 테이블에 u라는 별칭)
3. where : users 테이블 email 필드값이 naver.com으로 끝나는 값만 가져옴.
4. group by : users 테이블의 name값이 같은 값들을 그룹화.
5. select : users 테이블의 name 필드와 name 필드를 기준으로 뭉쳐진 갯수를 세어서 출력.


EX 5 : 결제 수단 별 유저 포인트의 평균값 구해보기

SELECT o.payment_method, ROUND(AVG(pu.point),2) as point_avg
FROM point_users pu 
INNER JOIN orders o 
ON pu.user_id = o.user_id 
WHERE pu.point
GROUP BY o.payment_method ;

결과 👉


평균값에서 나타내고자 하는 자리까지만 round(__,_)로 나타내준다. 위 값은 소수점 아래 2자리수까지 표기한 것이고, 정수만 표기하려면 round(avg(pu.point),0)로 작성하면 된다.

EX 6 : 결제하고 시작하지 않은 유저들을 성씨별로 세어보기

SELECT u.name, COUNT(u.name) as name_cnt
FROM enrolleds e 
INNER JOIN users u 
ON e.user_id = u.user_id 
WHERE is_registered = 0
GROUP BY u.name
ORDER BY name_cnt DESC ;

결과 👉

_시작하지 않은_ => is_registered = 0

 

EX 7 : 과목 별로 시작하지 않은 유저들을 세어보기

SELECT c.title, count(*) as title_cnt
FROM courses c 
INNER JOIN enrolleds e 
ON c.course_id = e.course_id 
WHERE is_registered = 0
GROUP BY c.title ;

결과 👉



EX 8 : 웹개발, 앱개발 종합반의 week 별 체크인 수를 세어볼까요? 보기 좋게 정리해보기!

SELECT title, week, COUNT(*) as week_cnt
FROM courses c1 
INNER JOIN checkins c2 
ON c1.course_id = c2.course_id 
GROUP BY title, week
ORDER BY title, week;

※ 위처럼 작성해도 정답은 나오지만 title과 week가 각각 어느 테이블에 속한 컬럼인지 표기하는 것이 쿼리가 길어졌을 때 헷갈리지 않는다. 따라서 보기 좋게 아래처럼 수정해주는 것이 좋다.

SELECT title, week, COUNT(*) as week_cnt
FROM courses c1 
INNER JOIN checkins c2 
ON c1.course_id = c2.course_id 
GROUP BY c1.title, c2.week
ORDER BY c1.title, c2.week;

꿀팁! → group by, order by에 콤마로 이어서 두 개 필드를 건다. EX8을 예시로 보면, title과 week로 각각 그룹화한 후 각각 정렬할 수 있다. (꼭 필드 하나만 적용하는 것이 아님!!)


is NULL, is not NULL

: inner join이라면 공통분모가 나오지만 left join 같은 경우, 왼쪽 테이블을 기준으로 차집합의 형태로 값이 나오므로 NULL값이 존재할 수 있다.
ex) users 테이블에 point_users 테이블을 left join으로 연결했을 때, 유저 중 강의를 시작하지 않은 사람들은 포인트가 적립되지 않았을 것이므로 데이터가 표시되지 않을 것이다( [NULL]값)

select * from users u
left join point_users pu on u.user_id = pu.user_id

따라서, 시작하지 않은 유저를 검색하려면 is null 키워드를 이용할 수 있다.(반대의 경우 is not null 입력)
그리고 count는 NULL을 세지 않는다.

select name, count(*) from users u
left join point_users pu on u.user_id = pu.user_id
where pu.point_user_id is NULL
group by name

 

EX 9 : 7월10일 ~ 7월19일에 가입한 고객 중, 포인트를 가진 고객의 숫자, 그리고 전체 숫자, 그리고 비율을 추출하려면,

SELECT  COUNT(pu.point_user_id) as pnt_cnt,
		COUNT(*) as tot_cnt,
		ROUND(COUNT(pu.point_user_id)/COUNT(*),2) as ratio
	FROM users u 
	LEFT JOIN point_users pu ON u.user_id = pu.user_id 
WHERE u.created_at BETWEEN '2020-07-10' AND '2020-07-20'

결과 👉

count는 null을 세지 않으므로, 전체(=202)에서 포인트를 가진 고객의 숫자를 count하면 null을 제외한 값(=82)이 나온다. 따라서 82/202를 round로 소수점을 정리해주면(두 자리까지 구한다고 했을 때), 0.41이라는 비율이 나온다.
count와 round 등 여러 조건이 붙어 컬럼명이 길어지므로 적절한 alias를 이용해준다.


Union

: 데이터 결과물을 합쳐서 한번에 보고 싶은 경우, union을 이용해 두 쿼리를 묶어준다.
※ 노란색과 파란색 박스의 필드명이 같아야 함!

	(select '7월' as month, c1.title, c2.week, count(*) as cnt from courses c1
	inner join checkins c2 on c1.course_id = c2.course_id
	inner join orders o on c2.user_id = o.user_id
	where o.created_at < '2020-08-01'
	group by c1.title, c2.week
	order by c1.title, c2.week)
UNION ALL	
	(select '8월' as month, c1.title, c2.week, count(*) as cnt from courses c1
	inner join checkins c2 on c1.course_id = c2.course_id
	inner join orders o on c2.user_id = o.user_id
	where o.created_at >= '2020-08-01'
	group by c1.title, c2.week
	order by c1.title, c2.week)
order by month, title, week;

결과 👉

union을 사용하면 내부 정렬이 먹지 않는다!


연습문제

: enrolled_id별 수강완료(done=1)한 강의 갯수를 세어보고, 완료한 강의 수가 많은 순서대로 정렬해보기. user_id도 같이 출력되어야 한다.

SELECT e.enrolled_id, e.user_id, COUNT(*) as e_cnt
FROM enrolleds e
INNER JOIN enrolleds_detail ed
ON e.enrolled_id = ed.enrolled_id 
WHERE ed.done = 1
GROUP BY e.enrolled_id, e.user_id
ORDER BY e_cnt DESC ;

결과 👉

새로 배운 내용

✅ union all은 합집합의 개념이므로 order by를 통해 정렬한 것이 의미가 없어진다. 내부 정렬이 되지 않으므로 union으로 합쳐진 최종 결과물에 다시 order by를 적용하거나 subquery를 사용하도록 한다.
✅ count는 NULL을 세지 않는다.
✅ 콤마(,)로 group by, order by를 두 개 이상 필드로 조건을 걸 수 있다.
✅ 엔터티 관계도를 봤을 때 각 테이블의 최상단 칼럼이 주로 기본값이 될 수 있다. 해당 데이터 하나를 구분짓도록 해주는 고유번호라고 볼 수 있다.

 


회고

확실히 쿼리를 직접 작성해보는 게 문법을 익히는데 빠르다. 에러가 자주 나는 구문 위주로 추가 작성해보기
(EX 8,9는 다시 풀어보는 문제였는데도 복잡하고 헷갈렸다. 최대한 정답을 참고 하지 않도록 여러번 복습하고 다시 풀어보기)

join을 배우고 나니 error : ~is ambiguous가 자주 뜬다. 문장 그대로 중의적이라는 뜻인데, 여러 테이블을 사용하고 각 테이블에서도 다른 테이블과 중복되는 필드가 있으니, 출력하고자 하는 데이터가 어떤 내용으로 사용하는지에 중점을 두어 연결해주어야 한다.
ex) 고객들의 정보를 가져와야 한다면, 고객 정보 중 가장 기본값이 되는 user_id로 연결해준다.

조인해야 하는 테이블이 무엇인지, 조인하는 필드가 무엇인지 잘 선별하도록 해야 한다. 어디에 어떤 데이터가 있는지 기억이 안나면 속성값을 일일이 클릭하기 번거로웠는데, 엔터티 관계도를 켜두니 쿼리 작성을 시작할 때 훨씬 직관적이라 수월했다.

 

Date : 2023-07-26

Topic : GROUP BY, ORDER BY, ALIAS


Note

데이터 분석의 목적

쌓여 있는 날 것의 데이터를 의미를 갖는 '정보'로 변환. 이때 의미 있는 정보는 통계치로 나타낼 수 있다. (아래 더 구체적으로 작성)
그리고 정보들을 범주(category)별로 묶어줄 수 있는데 이때 사용하는 것이 GROUP BY이다.

GROUP BY

: 동일한 범주끼리 묶어서 통계를 내준다.

  • ex ) 성씨별 회원수를 Group by로 쉽게 구해보기
	SELECT name, COUNT(*) 
	FROM users u 
	GROUP BY name ;
  • 위 쿼리가 실행되는 순서 : from → group by → select
    1. from users : users 테이블 데이터 전체를 가져온다.
    2. group by name : users 테이블 데이터에서 같은 name을 갖는 데이터를 합친다.
    3. select name, count(*) : name에 따라 합쳐진 데이터가 각각 몇 개가 합쳐진 것인지 센다.
    예) 이, 이, 김, 김, 박 이렇게 데이터가 있었다면, 이는 2개, 김은 2개, 박은 1개!

GROUP BY의 기능 : 통계

  • 최대(max)
  • 최소(min)
  • 평균(avg)
  • 합계(sum)
  • 개수(count)
	select 범주가 담긴 필드명, max(최댓값을 알고 싶은 필드명) from 테이블명
	group by 범주가 담긴 필드명;

⭐최솟값, 평균, 합계를 구하고 싶을 땐 위 구문에서 최댓값을 min, avg, sum으로 치환해주면 된다!

예외로 개수(count)를 구할 땐

	select 범주별로 세어주고 싶은 필드명, count(*) from 테이블명
	group by 범주별로 세어주고 싶은 필드명;

이 규칙으로 하면 된다.

ORDER BY

: 문자열(ㄱ-ㅎ or A-Z), 시간(숫자 0-9) 기준 데이터를 깔끔하게 정렬하는 기능.
아래와 같이 정렬하고자 하는 필드를 넣어 작성해주면 된다.

	select name, count(*) from users
	group by name
	order by count(*);

기본적으로 오름차순(asceding)으로 정렬되며(default값이라 아무 것도 안 써도 됨) 내림차순(descending)으로 하고 싶을 땐 DESC를 뒤에 적어준다.
내림차순은 시간, 날짜 등 최근 기록을 확인할 때 편리하다.

  • ex) like를 많이 받은 순서대로 '오늘의 다짐'을 출력해보자
	select * from checkins
	order by likes desc;
  • 위 쿼리가 실행되는 순서 : from → group by → select → order by
    1. from users: users 테이블 데이터 전체를 가져온다.
    2. group by name : users 테이블 데이터에서 같은 name을 갖는 데이터를 합친다.
    3. select name, count( ) : name에 따라 합쳐진 데이터가 각각 몇 개가 합쳐진 것인지 센다.
    예) 이, 이, 김, 김, 박 이렇게 데이터가 있었다면, 이는 2개, 김은 2개, 박은 1개겠죠!
    4. order by count(*) : 합쳐진 데이터의 개수에 따라 오름차순으로 정렬한다.
  • ex ) 웹개발 종합반의 결제수단별 주문건수 세어보기
	select payment_method, count(*) from orders
	where course_title = "웹개발 종합반"
	group by payment_method;
  • 위 쿼리가 실행되는 순서 : from → where → group by → select
    1. from orders: users 테이블 데이터 전체를 가져온다.
    2. where course-title = "웹개발 종합반" : 웹개발 종합반 데이터만 남긴다.
    3. group by payment-method : 같은 payment-method을 갖는 데이터를 합친다.
    4. select payment-method, count(*) : payment-method에 따라 합쳐진 데이터가 각각 몇 개가 합쳐진 것인지 센다.
    예) CARD, CARD, kakaopay 이렇게 데이터가 있었다면, CARD는 2개, kakaopay는 1개!
👉 만약 order by가 추가된다면? order by는 맨 나중에 실행된다. (결과물을 정렬해주는 것이기 때문!)

Alias

: 쿼리가 길고 복잡해지면 헷갈리기 쉬우므로 Alias(별칭) 기능을 활용한다.
혼동을 최소화하고 원하는 이름으로 결과를 출력할 수 있다.

  • 테이블명 뒤에 붙이기 (이하 orders -> o로 표기 가능)
	select * from orders o
	where o.course_title = '앱개발 종합반'
  • 출력될 필드에 붙이기 (count(*) 필드가 cnt로 출력됨)
	select payment_method, count(*) as cnt from orders o
	where o.course_title = '앱개발 종합반'
	group by payment_method

연습문제

1. Gmail 을 사용하는 성씨별 회원수 세어보기

	SELECT name, COUNT(*) 
	FROM users u
	WHERE email LIKE '%gmail.com'
	GROUP BY name ;

2. course_id별 '오늘의 다짐'에 달린 평균 like 개수 구해보기

	SELECT course_id, AVG(likes)  
	FROM checkins c 
	GROUP BY course_id ;

3. 네이버 이메일을 사용하여 앱개발 종합반을 신청한 주문의 결제수단별 주문건수 세어보기

	SELECT payment_method, COUNT(*) 
	FROM orders o
	WHERE email LIKE '%naver.com'
	AND course_title  = '앱개발 종합반' 
	GROUP BY payment_method ;

새로 배운 내용

  • 에러가 안 나는 쿼리를 작성하기 위해서는 SQL 쿼리가 실행되는 순서를 아는 것이 중요
  • 오름차순(asceding)/default, 내림차순(descending)/DESC
  • 내림차순은 시간, 날짜 등 최근 데이터부터 확인할 때 편리.

회고

  • [ max, min, avg, sum (@@값을 알고 싶은 필드명) ]
    ↔ count (세어보려는 필드명)
    ※구문이 살짝 다르다. 헷갈림 주의!
  • 예제2 문장 해석이 헷갈려서 계속 같은 실수를 한다.
    [course_id별 '오늘의 다짐'에 달린 평균 like 개수 구한다]라고 했을 때 '개수'라는 단어 때문에 계속 count를 쓰는데, '평균'이 포인트이므로 avg(likes)로 써야 한다. → 반복해서 다시 풀어보기

  • 자주 하는 실수 :
    • select * ← 전체값을 수정하지 않는다.
      • 결과 : 구하고자 하는 필드와 통계치가 나오지 않는다.
      • 해결안 : *을 [필드명, 통계]으로 정확히 작성한다.
    • select문 안에 count(*)만 적는다.
      • 결과 : 원하는 통계치는 나왔지만 각각 어떤 범주에 대한 통계치인지는 나오지 않는다.
      • 해결안 : select문 안에 group by에 들어간 필드를 똑같이 적어준다.
        → [select 필드명, 통계값 from 테이블명]

원하는 결과가 출력되지 않을 땐, SQL 쿼리의 실행순서에 따라 차근차근 생각해봐야 어디서 쿼리를 잘못 짰는지 가장 빠르고 정확하게 찾아낼 수 있다.

+ Recent posts