오늘은 SQL을 마무리하고 여러 실습 문제를 풀어보았다.
데이터를 요약 → 데이터의 특징을 알 수 있음 (EDA)
COUNT(필드명) : 결측이 아닌 개수 반환SUM(필드명) : 결측이 아닌 값의 합계 반환AVG(필드명) : 결측이 아닌 값의 평균 반환MAX(필드명) : 최댓값 반환MIN(필드명) : 최솟값 반환VARIANCE(필드명) : 분산 반환STDEV(필드명, 구분자) : 표준편차 반환SELECT sepal_length
, COUNT(*) AS cnt
, SUM(sepal_length) AS sum
, AVG(sepal_length) AS avg
, AVG(POWER(sepal_length, 2)) - POWER(AVG(sepal_length), 2) AS var
, SQRT(AVG(POWER(sepal_length, 2)) - POWER(AVG(sepal_length), 2)) AS std
FROM IRIS
GROUP BY species;
날짜 시간 : 실수형(연산 가능)
DATE(필드명, 조정자) : 날짜를 YYYY-MM-DD 형태의 문자열로 반환TIME(필드명, 조정자) : 시간을 HH:MM:SS 형태의 문자열로 반환DATETIME(필드명, 조정자) : 날짜시간을 YYYY-MM-DD HH:MM:SS 형태의 문자열로 반환STRFTIME(포맷, 필드명) : 날짜시간 필드를 포맷에 해당하는 문자열로 반환JULIANDAY(필드명) : 율리우스일 반환| 조정자 | 내용 |
|---|---|
(-)N year | 날짜를 N년 미래 또는 -N년 과거로 조정 |
(-)N month | 날짜를 N월 미래 또는 -N월 과거로 조정 |
(-)N day | 날짜를 N일 미래 또는 -N일 과거로 조정 |
(-)N hour | 날짜를 N시 미래 또는 -N시 과거로 조정 |
(-)N minute | 날짜를 N분 미래 또는 -N분 과거로 조정 |
(-)N second | 날짜를 N초 미래 또는 -N초 과거로 조정 |
start of month | 날짜를 월초로 조정 start of year , start of day 도 있음 |
weekday N | 날짜를 다음주 N번째 요일로 조정(0 : 일요일) |
localtime | 현지 시간의 날짜와 시간 반환 |
| 포맷 | 내용 | 예시 | 포맷 | 내용 | 예시 |
|---|---|---|---|---|---|
| %Y | 연도(4자리) | 2025 | %y | 연도(2자리) | 25 |
| %m | 월(정수, 01~12) | 12 | %M | 분(정수, 00~59) | 02 |
| %d | 일(정수, 01~31) | 31 | %S | 초(정수, 00~59) | 03 |
| %j | 연중 일의 순번(001~366) | 365 | %f | 천 분의 1초 | 03.456 |
| %H | 시(24시간, 00~23) | 15 | %s | 유닉스 시간 | 1703984523 |
| %I | 시(12시간, 01~12) | 01 | %W | 연중 주의 순번(00~52) | 52 |
| %p | AM/PM 표기 | AM | %w | 주중 요일의 순번(0~6) | 0(일요일) |
SELECT DATE('now', 'localtime');
SELECT TIME('now', 'loctime');
SELECT DATETIME('now', 'localtime');
SELECT * FROM DT;
SELECT date, DATE(date) FROM DT;
SELECT date, TIME(date) FROM DT;
SELECT date, DATE(date), TIME(date) FROM DT;
SELECT date, DATE(date, '1 year') FROM DT;
SELECT date, DATE(date, '1 month', '1 day') FROM DT;
SELECT date, DATE(date, 'start of month') FROM DT;
SELECT date, TIME(date, '1 hour') FROM DT;
SELECT date, TIME(date, '1 minute', '1 second') FROM DT;
SELECT date, TIME(date, 'start of day', '-1 second') FROM DT;
SELECT date, STRFTIME('%Y년 %m월 %d일', date) FROM DT;
SELECT date, STRFTIME('%H시 %M분 %S초', date) FROM DT;
SELECT date, STRFTIME('%I:%M:%S %p', date) FROM DT;
SELECT STRFTIME('%s', 'now');
SELECT STRFTIME('%s', date) FROM DT;
SELECT STRFTIME('%s', 'now') - STRFTIME('%s', date) FROM DT;
SELECT (STRFTIME('%s', 'now') - STRFTIME('%s', '2000-01-01')) / 86400;
SELECT JULIANDAY('now') - JULIANDAY(date) FROM DT;
SELECT FLOOR(JULIANDAY('now') - JULIANDAY('2000-01-01'));
SELECT STRFTIME('%w', 'now');
SELECT STRFTIME('%w', '2000-01-01');
SELECT species FROM IRIS;
SELECT DISTINCT species FROM IRIS;
SELECT species FROM IRIS GROUP BY species;
SELECT *
FROM HR_MEMBER
INNER JOIN HR_OFFICE
ON HR_MEMBER.직원명 = HR_OFFICE.직원명;
SELECT A.*, B.내선, B.부서
FROM HR_MEMBER AS A
INNER JOIN HR_OFFICE AS B
ON A.직원명 = B.직원명;
SELECT A.*, B.내선, B.부서, C.나이, C.혈액형, C.동아리
FROM HR_MEMBER AS A
INNER JOIN HR_OFFICE AS B
ON A.직원명 = B.직원명
INNER JOIN HR_PERSON AS C
ON B.직원명 = C.이름;
SELECT A.*, B.내선, B.부서
FROM HR_MEMBER AS A
FULL OUTER JOIN HR_OFFICE AS B
ON A.직원명 = B.직원명;
SELECT A.*, B.내선, B.부서
FROM HR_MEMBER AS A
FULL OUTER JOIN HR_OFFICE AS B
ON A.직원명 = B.직원명
WHERE A.직원명 ISNULL or B.직원명 ISNULL;
SELECT A.*, B.내선, B.부서
FROM HR_MEMBER AS A
LEFT OUTER JOIN HR_OFFICE AS B
ON A.직원명 = B.직원명;
-- 오른쪽 외부 병합
SELECT A.*, B.내선, B.부서
FROM HR_MEMBER AS A
RIGHT OUTER JOIN HR_OFFICE AS B
ON A.직원명 = B.직원명;
SELECT A.*, B.내선, B.부서
FROM HR_MEMBER AS A
LEFT OUTER JOIN HR_OFFICE AS B
ON A.직원명 = B.직원명
WHERE B.직원명 ISNULL;
SELECT A.*, C.혈액형, C.동아리
FROM HR_MEMBER AS A
LEFT OUTER JOIN HR_PERSON AS C
ON A.직원명 = C.이름 AND A.나이 = C.나이;
SELECT *
FROM HR_PERSON
WHERE 혈액형 = 'O';
SELECT *
FROM (SELECT * FROM HR_PERSON WHERE 혈액형 = 'O');
SELECT *
FROM HR_MEMBER
WHERE 직원명 IN (SELECT 이름 FROM HR_PERSON WHERE 혈액형 = 'O');
SELECT *
FROM HR_MEMBER
WHERE 직원명 IN (SELECT 이름 FROM HR_PERSON WHERE 혈액형 = 'A');
SELECT A.*, B.내선, B.부서, CONCAT(B.내선, '-', B.부서) AS 조직
FROM HR_MEMBER AS A
LEFT JOIN HR_OFFICE AS B
ON A.직원명 = B.직원명;
SELECT A.*, B.조직
FROM HR_MEMBER AS A
LEFT JOIN (SELECT *, CONCAT(부서, '-', 내선) AS 조직 FROM HR_OFFICE) AS B
ON A.직원명 = B.직원명;
-- 뷰 생성
CREATE VIEW VW_HR_OFFICE AS
SELECT *, CONCAT(부서, '-', 내선) AS 조직 FROM HR_OFFICE;
-- 뷰 활용
SELECT A.*, B.조직
FROM HR_MEMBER AS A
LEFT JOIN VW_HR_OFFICE AS B
ON A.직원명 = B.직원명;
인덱스는 데이터를 빠르게 찾기 위해 데이터베이스가 미리 만들어 두는 탐색용 구조
CREATE INDEX idx_apt_price_addr ON APT_PRICE(주소);
CREATE INDEX idx_apt_detail_addr ON APT_DETAIL(지번주소);
SELECT COUNT(*) AS cnt
FROM APT_PRICE
WHERE 단지명 LIKE '%래미안%';
SELECT COUNT(*) AS cnt
FROM APT_PRICE
WHERE 단지명 LIKE '%래미안%' AND 시군구 = '강남구';
SELECT *
FROM APT_PRICE
WHERE 단지명 LIKE '%래미안%' AND 시군구 = '강남구' AND 거래금액 >= 500000;
SELECT *
FROM APT_PRICE
WHERE 단지명 LIKE '%한남더힐%'
ORDER BY 거래금액 DESC
LIMIT 10;
SELECT *
, ROUND((거래금액 / 전용면적 * 3.3), 0) AS 평당금액
FROM APT_PRICE
ORDER BY 평당금액 DESC;
SELECT 등기일자
, 계약일자
, JULIANDAY(등기일자) - JULIANDAY(계약일자) AS 처리일수
FROM APT_PRICE;
SELECT COUNT(*) AS cnt
FROM APT_PRICE
WHERE 등기일자 IS NULL;
SELECT COUNT(*) - COUNT(등기일자) AS 결측건수
FROM APT_PRICE;
SELECT 시군구, ROUND(1.0 * (COUNT(*) - COUNT(등기일자)) / COUNT(*) * 100, 1) AS 결측비율
FROM APT_PRICE
GROUP BY 시군구
ORDER BY 결측비율 DESC;
SELECT 법정동
, ROUND(AVG(거래금액), 0) AS 평균가격
, ROUND(1.0 * (COUNT(*) - COUNT(등기일자)) / COUNT(*) * 100, 1) AS 결측비율
FROM APT_PRICE
WHERE 시군구 = '도봉구'
GROUP BY 법정동
ORDER BY 결측비율 DESC;
SELECT 단지명
, ROUND(AVG(거래금액), 0) AS 평균가격
, ROUND(1.0 * (COUNT(*) - COUNT(등기일자)) / COUNT(*) * 100, 1) AS 결측비율
FROM APT_PRICE
WHERE 시군구 = '도봉구' AND 법정동 = '창동'
GROUP BY 단지명
ORDER BY 평균가격 DESC, 결측비율 DESC;
SELECT AVG(JULIANDAY(등기일자) - JULIANDAY(계약일자)) AS 평균처리일수
FROM APT_PRICE; --78 day
-- COALESCE 사용
SELECT 등기일자
, 계약일자
, COALESCE(등기일자, DATE(계약일자, '+78 day')) AS 보정등기일자
FROM APT_PRICE;
-- IFNULL 사용
SELECT 등기일자
, 계약일자
, IFNULL(등기일자, DATE(계약일자, '+78 day')) AS 보정등기일자
FROM APT_PRICE;
SELECT 단지명
, COUNT(*) AS 거래건수
, ROUND(AVG(거래금액), 0) AS 평균가격
FROM APT_PRICE
WHERE 시군구 = '도봉구'
GROUP BY 단지명
ORDER BY 거래건수 DESC
LIMIT 10;
SELECT 단지명
, COUNT(*) AS 거래건수
, ROUND(AVG(거래금액), 0) AS 평균가격
FROM APT_PRICE
WHERE 시군구 = '도봉구'
GROUP BY 단지명
HAVING 거래건수 >= 30
ORDER BY 평균가격 DESC
LIMIT 10;
SELECT STRFTIME('%m', 계약일자) AS 계약월
, COUNT(*) AS 거래건수
, ROUND(AVG(거래금액), 0) AS 평균가격
FROM APT_PRICE
GROUP BY STRFTIME('%m', 계약일자);
SELECT COUNT(*) AS 전체건수 FROM APT_PRICE;
SELECT COUNT(DISTINCT(단지명)) AS 고유값_개수 FROM APT_PRICE;
SELECT COUNT(DISTINCT(주소)) AS 고유값_개수 FROM APT_PRICE;
CONCAT 사용(|| 로 대체 가능)SELECT COUNT(DISTINCT(CONCAT(주소, ' ' , 단지명))) AS 고유값_개수 FROM APT_PRICE;
SELECT COUNT(DISTINCT(주소 || ' ' || 단지명)) AS 고유값_개수 FROM APT_PRICE;
CREATE VIEW VW_APT_RECENT AS
SELECT 단지명, 주소, MAX(계약일자) AS 최신계약일자
FROM APT_PRICE
GROUP BY 단지명, 주소;
SELECT A.*
FROM APT_PRICE AS A
LEFT JOIN VW_APT_RECENT AS B
ON A.단지명 = B.단지명
AND A.주소 = B.주소
AND A.계약일자 = B.최신계약일자;
SELECT COUNT(DISTINCT 단지명) AS 고유값_개수 FROM APT_PRICE; -- 4432
SELECT COUNT(*) AS 중복건수
FROM (SELECT 단지명 FROM APT_PRICE GROUP BY 단지명) AS A
INNER JOIN (SELECT 아파트명 FROM APT_DETAIL GROUP BY 아파트명) AS B
ON A.단지명 = B.아파트명; -- 1803
SELECT COUNT(DISTINCT 주소) AS 고유값_개수 FROM APT_PRICE; -- 5063
SELECT COUNT(*) AS 중복건수
FROM (SELECT DISTINCT 주소 FROM APT_PRICE) AS A
INNER JOIN (SELECT DISTINCT 지번주소 FROM APT_DETAIL) AS B
ON A.주소 = B.지번주소; -- 0
SELECT 주소 FROM APT_PRICE LIMIT 10;
SELECT 지번주소 FROM APT_DETAIL LIMIT 10;
SELECT COUNT(*) AS 중복건수
FROM (SELECT DISTINCT 주소 FROM APT_PRICE) AS A
INNER JOIN (SELECT DISTINCT REPLACE(지번주소, '서울시', '서울특별시') AS 주소 FROM APT_DETAIL) AS B
ON A.주소 = B.주소; -- 4773
SELECT 지번주소, COUNT(*) AS 중복건수
FROM APT_DETAIL
GROUP BY 지번주소
HAVING 중복건수 > 1;
CREATE VIEW VW_APT_DETAIL AS
SELECT *, REPLACE(지번주소, '서울시', '서울특별시') AS 주소
FROM APT_DETAIL;
SELECT COUNT(*) AS cnt FROM VW_APT_DETAIL; -- 10725
DROP VIEW VW_APT_DETAIL;
CREATE VIEW VW_APT_DETAIL AS
SELECT *, REPLACE(지번주소, '서울시', '서울특별시') AS 주소
FROM APT_DETAIL
WHERE rowid IN (SELECT MIN(rowid) FROM APT_DETAIL GROUP BY 지번주소);
SELECT COUNT(*) AS cnt FROM VW_APT_DETAIL; -- 10690
CREATE VIEW VW_APT_MERGED AS
SELECT A.*, B.세대수, B.주차대수, B.건설사
FROM APT_PRICE AS A
INNER JOIN VW_APT_DETAIL AS B
ON A.주소 = B.주소;
SELECT * FROM VW_APT_MERGED LIMIT 10;
CREATE INDEX idx_apt_price_addr ON APT_PRICE(주소);
CREATE INDEX idx_apt_detail_addr ON APT_DETAIL(지번주소);
IF EXISTS : 있을시에만 적용DROP VIEW IF EXISTS VW_APT_MERGED;
CREATE VIEW VW_APT_MERGED AS
SELECT A.*, B.세대수, B.주차대수, B.건설사
FROM APT_PRICE AS A
INNER JOIN VW_APT_DETAIL AS B
ON A.주소 = B.주소;
SELECT * FROM VW_APT_MERGED;
현업에서 사용하는 SQL 방식이나 복잡한 내용들도 잘 이해할 수 있게 알려주셔서 너무 좋았던 SQL 수업이었다. 앞으로 SQL 문제를 계속 풀어보면서 SQL 자격증도 준비해보면 좋을 것 같다.