
예시 문제
| 단계 | 설명 |
|---|---|
| 1.FROM | USED_GOODS_BOARD 테이블과 USED_GOODS_USER 테이블을 INNER JOIN 하여 결합 |
| 2.WHERE | B.STATUS = 'DONE' 조건을 만족하는 행 필터링 |
| 3.GROUP BY | B.WRITER_ID를 기준으로 데이터 그룹화 |
| 4.HAVING | 가격 합계가 700,000원을 초과하는 그룹 필터링 |
| 5.SELECT | 각 작가 ID와 가격 합계를 선택 |
SQL 10번 문제 中 WHERE 절에 "is not" 은 안되고 "!=" 은 되던데
한 번 정리할 필요가 있을 듯 하다.
# 젊은 동물의 아이디와 이름을 조회, 아이디 칼럼 정렬
-- 젊은 동물의 아이디와 이름을 조회, 아이디 칼럼 정렬
SELECT animal_id, name
FROM animal_ins
WHERE intake_condition != 'aged'
-- WHERE intake_condition IS NOT 'aged' 는 안된다.
ORDER BY animal_id;
/*
select count(user_id) USERS
from user_info
where age = null
*/
select count(user_id) USERS
from user_info
where age is null
# 이번에는 where 절에 is 가 오네..? null 값을 찾을 때만 is 를 사용 할 수 있는거야?
# 조건문 where 절에서 연산자를 사용해야 하는 상황과 is를 사용해야 하는 상황 구분해야 할 듯.
SQL에서 'NULL' 값은 특별하다!
'NULL'을 확인할 때: "IS NULL" or "IS NOT NULL"을 사용해야 한다.
비교연산자(예: =, !=, <, >)로는 비교가 불가능.
쿼리 WHERE age = NULL은 어떠한 결과도 반환하지 않는다.
240626TIL <- 에서 MAX()를 사용했을 때 해당 행에서 MAX() 칼럼만 따로 노는 문제에 대해 다뤘다.
CODEKATA <- 문제를 풀면서 같은 상황에 처했다. 하지만 이번에는 240626TIL 처럼 WHERE = MAX()를 통해 필터링이 불가능했다.
MAX()의 결과가 여러 줄의 행을 가지고 있기 때문이다. WHERE =같은 단일값 필터링이 불가한 것이다.
Subquery returns more than 1 row
정리
- 서브쿼리의 결과가 다중 행을 반환한다면, WHERE 절에=연산자를 사용하여 필터링하는 방법을 사용할 수 없다.
- 이 경우, 서브쿼리의 결과를 필터링하기 위해JOIN을 사용하는 것이 가장 일반적이다.
JOIN 함수와 AND 연산자를 통해 필터링하는 과정을 살펴보자.
SELECT MAIN.FOOD_TYPE,
MAIN.REST_ID,
MAIN.REST_NAME,
SUB.FAVORITES
FROM REST_INFO MAIN INNER JOIN
(
SELECT FOOD_TYPE,
MAX(FAVORITES) FAVORITES
FROM REST_INFO
GROUP BY FOOD_TYPE
) SUB ON MAIN.FOOD_TYPE = SUB.FOOD_TYPE AND MAIN.FAVORITES = SUB.FAVORITES
ORDER BY 1 DESC
문제 설명
음식 종류별로 즐겨찾기가 가장 많은 식당을 구하고, 해당 식당의 음식 종류, ID, 식당 이름, 즐겨찾기 수를 조회합니다. 결과는 음식 종류를 기준으로 내림차순(DESC)으로 정렬합니다.
REST_INFO 데이터 테이블

쿼리 작성
다음은 각 음식 종류별로 즐겨찾기 수가 가장 많은 식당을 찾기 위해 서브쿼리와 JOIN을 사용하는 쿼리입니다.
SELECT R.FOOD_TYPE, R.REST_ID, R.REST_NAME, R.FAVORITES
FROM REST_INFO R
JOIN (
SELECT FOOD_TYPE, MAX(FAVORITES) AS MAX_FAVORITES
FROM REST_INFO
GROUP BY FOOD_TYPE
) AS T
ON R.FOOD_TYPE = T.FOOD_TYPE AND R.FAVORITES = T.MAX_FAVORITES
ORDER BY R.FOOD_TYPE DESC;
쿼리 설명
T는 각 FOOD_TYPE별로 최대 즐겨찾기 수(MAX_FAVORITES)를 계산합니다.REST_INFO 테이블을 서브쿼리 T와 JOIN하여 FOOD_TYPE과 즐겨찾기 수가 일치하는 행을 찾습니다.쿼리 결과
위의 쿼리를 실행하면 다음과 같은 결과가 나옵니다.
| FOOD_TYPE | REST_ID | REST_NAME | FAVORITES |
|---|---|---|---|
| 일식 | 00004 | 스시사카우스 | 230 |
| 양식 | 00003 | 따띠따띠뜨 | 102 |
| 한식 | 00001 | 은돼지식당 | 734 |
💡중요포인트💡
JOIN ON 뿐만 아니라 AND 를 통해 한 번 더 조건을 걸 수 있다.
-- 모든 ID, NAME, DATETIME을 조회, 이름 순 정렬 /
-- 같은 이름일 경우 날짜 내림차순 정렬
SELECT ANIMAL_ID, NAME, DATETIME
FROM animal_ins
ORDER BY NAME, DATETIME DESC;
SQL에서 ORDER BY 절에 여러개의 정렬 조건을 사용하면 수행 플로우는 다음과 같다.
ORDER BY CONDITION1, CONDITION2
1. 첫 번째 정렬 조건(CONDITION1) (이름 순 정렬)
2. 두 번째 정렬 조건(CONDITION2) (같은 이름? -> 날짜 기준 내림차순 정렬)
집계함수: max() min()등의 함수
HAVING:
그룹화된 결과에 대한 조건을 지정할 때 사용되며, GROUP BY 절과 함께 특정 조건을 만족하는 그룹만을 필터링할 때 사용
주요 집계 함수:
MAX(): 주어진 열의 최대값을 반환
MIN(): 주어진 열의 최소값을 반환
SUM(): 숫자 열의 총합을 반환
AVG(): 숫자 열의 평균값을 반환
COUNT(): 특정 조건을 만족하는 행의 수를 계산
if 절의 구조
if(조건, 조건을 충족할 때, 조건을 충족하지 않을 때)
예시
SELECT ANIMAL_TYPE,
IF(NAME IS NULL, 'No name', NAME) NAME,
SEX_UPON_INTAKE
FROM ANIMAL_INS
ORDER BY ANIMAL_ID
/*
[IF 함수를 대체할 수 있는 CASE함수]
CASE
WHEN NAME IS NULL THEN 'No name'
ELSE NAME
END AS NAME,
*/
날짜 데이터의 구조를 변경하고 싶을 때 사용하는 함수:
DATE_FORMAT()

registration_datetime 칼럼의 날짜 데이터 구성을 변경해보자.
SELECT
first_name,
last_name,
DATE_FORMAT(registration_datetime, '%a, %Y %M %e %H:%i:%s')
AS format_registration_datetime
FROM student_platform;

SELECT
first_name,
last_name,
DATE_FORMAT(registration_datetime, '%a, %Y %M %e %H:%i:%s')
AS format_registration_datetime
FROM student_platform;
ex) 2024-06-29 = "%Y-%m-%d"
#sql_19_흉부외과 또는 일반외과 의사 목록 출력하기
SELECT DR_NAME,
DR_ID,
MCDP_CD,
DATE_FORMAT(HIRE_YMD, "%Y-%m-%d") HIRE_YMD
FROM DOCTOR
WHERE MCDP_CD = 'GS'OR MCDP_CD = 'CS'
ORDER BY HIRE_YMD DESC, DR_NAME ASC
# WHERE 절에 들어가는 복수의 조건들 표현
# 꼭 저렇게 자세히 써야해?
따라서 여러 조건을 확인할 때는 논리연산자(AND,OR,NOT)를 각각 사용해주거나 'IN'연산자를 사용할 수 있다.
SELECT column_name(s)
FROM table_name
WHERE column_name IN (value1, value2, ...);
CODEKATA
ex) 특정 이름들이 포함된 칼럼 조회
WHERE NAME IN ('Lucy','Ella','Pickle','Rogan','Sabrina','Mitty')
SQL구문이 더 간결하고 가독성이 높아진다!
-- '통풍시트', '열선시트', '가죽시트' 중 하나 이상의 옵션이 포함된 자동차 조회
SELECT CAR_TYPE,
COUNT(CAR_ID) CARS
FROM CAR_RENTAL_COMPANY_CAR
WHERE FIND_IN_SET('통풍시트',OPTIONS) > 0
OR FIND_IN_SET('열선시트',OPTIONS) > 0
OR FIND_IN_SET('가죽시트',OPTIONS) > 0
GROUP BY CAR_TYPE
ORDER BY CAR_TYPE

-- 가장 비싼 가격의 항목의 정보를 전부 구하기
SELECT PRODUCT_ID,
PRODUCT_NAME,
PRODUCT_CD,
CATEGORY,
MAX(PRICE) PRICE
FROM FOOD_PRODUCT;
-- 이렇게 작성하면 MAX(PRICE)와는 상관없는 칼럼들이 나온다.
-- 올바른 작성 예시
SELECT *
FROM FOOD_PRODUCT
WHERE PRICE = (SELECT MAX(PRICE) FROM FOOD_PRODUCT);
1번 코드로 작성하면,
PRICE 칼럼에는 최대값이 출력되지만 나머지 칼럼에는 전부 최대값과는 관련없는 데이터가 출력된다.
| PRODUCT_ID | PRODUCT_NAME | PRODUCT_CD | CATEGORY | PRICE |
|---|---|---|---|---|
| 최대값아님 | 최대값아님 | 최대값아님 | 최대값아님 | 최대값 |
2번 코드처럼 WHERE 서브퀘리를 통해 해당 행 정보를 전부 출력해야 한다.
| PRODUCT_ID | PRODUCT_NAME | PRODUCT_CD | CATEGORY | PRICE |
|---|---|---|---|---|
| 최대값 | 최대값 | 최대값 | 최대값 | 최대값 |
평균 가격을 소수점 둘째 자리까지 반올림
SELECT ROUND(AVG(PRICE), 2) AS avg_price
FROM BOOK;
값 AS 형식)
select date_format(cast(date as date), '%Y-%m') m
날짜를 다루는 칼럼은 보통 "date"로 이름짓기 때문에 막연하게 time data겠거니~ 하고 검증을 뛰어넘는 경우가 있다.
그럴 경우 date_format 함수 같은 time data를 대상으로 하는 함수를 사용하지 못할 뿐 만 아니라 정렬 과정에서도 오류가 날 수 있기에 주의하자.
---------------------------case함수 구조------------------------
CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
...
ELSE default_result
END
-----------------------------예시--------------------------------
-- CASE WHEN 구조 예시
SELECT game_actor_id,
level,
CASE
WHEN 1 < level AND level <= 10 THEN '1~10Lv 이하'
WHEN 10 < level AND level <= 20 THEN '10~20Lv 이하'
WHEN 20 < level AND level <= 30 THEN '20~30Lv 이하'
END AS level_range
FROM game_actors;
🅰️쿼리는 실행되지만 확인되지 않는 값들은 'NULL'을 반환한다.
DISTINCT(NAME) ❌
DISTINCT NAME ⭕
EX) "부산" 이 포함된 칼럼 찾기
WHERE name LIKE '%부산%'
SELECT ANIMAL_ID,
NAME,
CASE WHEN SEX_UPON_INTAKE LIKE "%Spayed%" THEN "O"
WHEN SEX_UPON_INTAKE LIKE "%Neutered%" THEN "O"
ELSE "X" END 중성화
FROM ANIMAL_INS
SELECT CATEGORY,
COUNT(PRODUCT_CODE) PRODUCTS
FROM
(SELECT SUBSTR(PRODUCT_CODE,1,2) CATEGORY,
PRODUCT_CODE,
PRICE
FROM PRODUCT) A
GROUP BY CATEGORY
날짜 데이터에 대해
| 설명 | 함수 |
|---|---|
| 날짜에 간격을 더하기 | DATE_ADD(date, INTERVAL value unit) |
| 날짜에서 간격을 빼기 | DATE_SUB(date, INTERVAL value unit) |
| 두 날짜 사이의 일 수 계산 | DATEDIFF(date1, date2) |
| 두 날짜 시간 값 사이의 차이를 특정 단위로 계산 | TIMESTAMPDIFF(unit, datetime_expr1, datetime_expr2) |
| 날짜 값을 지정된 형식으로 변환 | DATE_FORMAT(date, format) |
| 날짜 또는 날짜 시간 값에서 연도, 월, 일 추출 | YEAR(date), MONTH(date), DAY(date) |
| 날짜 시간 값에서 시, 분, 초 추출 | HOUR(datetime), MINUTE(datetime), SECOND(datetime) |
특정 날짜에 주어진 간격을 더합니다.
함수: DATE_ADD(date, INTERVAL value unit)
------
예시: SELECT DATE_ADD('2024-06-28', INTERVAL 10 DAY) AS new_date;
------
결과: 2024-07-08
특정 날짜에서 주어진 간격을 뺍니다.
함수: DATE_SUB(date, INTERVAL value unit)
------
예시: SELECT DATE_SUB('2024-06-28', INTERVAL 10 DAY) AS new_date;
------
결과: 2024-06-18
두 날짜 사이의 일(day) 수를 계산합니다.
함수: DATEDIFF(date1, date2)
------
예시: SELECT DATEDIFF('2024-06-28', '2024-06-18') AS days_diff;
------
결과: 10
두 날짜 또는 날짜 시간 값 사이의 차이를 특정 단위로 계산합니다.
함수: TIMESTAMPDIFF(unit, datetime_expr1, datetime_expr2)
------
예시: SELECT TIMESTAMPDIFF(MINUTE, '2024-06-28 12:00:00', '2024-06-28 14:30:00') AS minutes_diff;
------
결과: 150
날짜 값을 지정된 형식으로 변환합니다.
함수: DATE_FORMAT(date, format)
------
예시: SELECT DATE_FORMAT('2024-06-28', '%Y-%m-%d') AS formatted_date;
------
결과: 2024-06-28
| 형식 지정자 | 설명 |
|---|---|
%a | 요일 이름 약어 |
%Y | Year, in 4-digits. |
%M | 월 이름 |
%m | 월 (01-12) |
%d | 일 (00-31) |
%e | 일 (1-31) |
%H | 시 (00-23) |
%k | 시 (0-23) |
%l | 시 (1-12) |
%i | 분 (00-59) |
%s | 초 (00-59) |
-- 연도, 월, 일, 시, 분, 초 추출
SELECT YEAR('2024-06-28') AS year,
MONTH('2024-06-28') AS month,
DAY('2024-06-28') AS day;
---
결과: 2024, 6, 28
-- 시간 정보를 제외하고 날짜(date)만 조회
SELECT DATE('2017-06-15 09:34:21') AS date_only;
-- 결과: 2017-06-15
| DATE("2017-06-15 09:34:21") |
|---|
| 2017-06-15 |
-- TIMESTAMPDIFF(단위, datetime_expr1, datetime_expr2)
SELECT TIMESTAMPDIFF(MONTH, '2003-02-01', '2003-05-01') AS months_diff;
-- 결과: 3
SELECT TIMESTAMPDIFF(YEAR, '2002-05-01', '2001-01-01') AS years_diff;
-- 결과: -1
SELECT TIMESTAMPDIFF(MINUTE, '2003-02-01', '2003-05-01 12:05:55') AS minutes_diff;
-- 결과: 128885

-- USER_INFO 테이블에서 2021년에 가입한 회원 중
-- 나이가 20세 이상 29세 이하인 회원 수 구하기
SELECT COUNT(USER_ID)
FROM USER_INFO
WHERE DATE_FORMAT(CAST(JOINED AS DATE), '%Y') = 2021
AND AGE >= 20
AND AGE <= 29;
#동물의 아이디와 이름, 중성화 여부를 아이디 순으로 조회하는 SQL문을 작성해주세요.
#중성화가 되어있다면 'O', 아니라면 'X'라고 표시해주세요.
# Spayed: 중성화된(암컷)
# Neutered: 중성화된(수컷)
# Intact: 손대지 않은
/*
SELECT ANIMAL_ID,
NAME,
CASE WHEN SEX_UPON_INTAKE = LIKE "Spayed" THEN "O"
WHEN SEX_UPON_INTAKE = LIKE "Neutered" THEN "O"
ELSE "X" END 중성화
FROM ANIMAL_INS
LIKE 함수는 그냥 바로 적어라. = LIKE 하면 안돼~
*/
SELECT ANIMAL_ID,
NAME,
CASE WHEN SEX_UPON_INTAKE LIKE "%Spayed%" THEN "O"
WHEN SEX_UPON_INTAKE LIKE "%Neutered%" THEN "O"
ELSE "X" END 중성화
FROM ANIMAL_INS
SELECT HOUR(DATETIME) HOUR,
COUNT(ANIMAL_ID) COUNT
FROM ANIMAL_OUTS
WHERE HOUR(DATETIME) >= 9 AND HOUR(DATETIME) <= 19
GROUP BY HOUR
ORDER BY HOUR
SELECT B.CATEGORY,
SUM(SALES) TOTAL_SALES
FROM BOOK_SALES S INNER JOIN BOOK B ON S.BOOK_ID = B.BOOK_ID
WHERE DATE_FORMAT(S.SALES_DATE, '%Y-%m') = '2022-01'
GROUP BY B.CATEGORY
ORDER BY CATEGORY
SELECT ANIMAL_ID,
NAME
FROM
(
SELECT GO.ANIMAL_ID,
GO.NAME,
TIMESTAMPDIFF(DAY, COME.DATETIME, GO.DATETIME) DURATION # 2. TIMESTAMPDIFF()함수로 기간 차이 구하기
FROM ANIMAL_OUTS GO LEFT JOIN ANIMAL_INS COME
ON GO.ANIMAL_ID = COME.ANIMAL_ID # 1. 조인
ORDER BY DURATION DESC
) A # 까먹지 마라
LIMIT 2
덧셈(+), 뺄셈(-), 곱셈(*) 생략
/ : 나누기
% : 나머지
** : 거듭제곱
// : 나눗셈의 몫
#나눗셈의 몫 예시
num1 = 10
num2 = 3
# result는 3
def solution(num1, num2):
result = num1//num2
return result
NULL 필터링 시, NAME IS NULL 이라고 작성해야 함. NAME IS "NULL" 이라고 하면 오류가 나타난다.
if() 함수는 select 문 칼럼 위치에 들어간다.
# sql_11_이름에 el이 들어가는 동물 찾기
select ANIMAL_ID, NAME
from animal_ins
where name like "%EL%" and animal_type = "dog"
order by name
# 이름 순으로 정렬하라고... 문제 똑바로 읽자.
#sql_16_경기도에 위치한 식품창고 목록 출력하기
SELECT WAREHOUSE_ID,
WAREHOUSE_NAME,
ADDRESS,
IF(FREEZER_YN IS NULL, 'N', FREEZER_YN) FREEZER_YN
FROM FOOD_WAREHOUSE
WHERE WAREHOUSE_NAME LIKE "%경기%"
ORDER BY WAREHOUSE_ID ASC