반응형

SQL 행 관련 순서함수(오프셋 함수) 완벽 정리 🚀

안녕하세요! 오늘은 SQL 윈도우 함수 중에서도 행의 순서를 기반으로 데이터를 처리하는 오프셋 함수(OFFSET Functions)에 대해 알아보겠습니다.

오프셋 함수는 특정 행을 기준으로 앞뒤에 위치한 행의 값을 쉽게 참조할 수 있도록 도와줍니다. 주로 데이터를 시계열적으로 분석하거나 비교할 때 매우 유용합니다.


🔍 오프셋 함수의 종류와 특징

SQL에서 가장 자주 사용하는 오프셋 함수는 크게 세 가지입니다:

1️⃣ LAG()

LAG() 함수는 현재 행에서 지정한 수만큼 앞에 있는 행의 값을 반환합니다.

사용법

LAG(컬럼명, offset, 기본값) OVER (ORDER BY 컬럼명)
  • offset(생략 가능): 몇 번째 앞의 행을 참조할지 지정 (기본값 1)
  • 기본값(생략 가능): 참조할 행이 없을 경우 반환할 값 (기본값 NULL)

예시

SELECT emp_name, salary,
       LAG(salary, 1, 0) OVER (ORDER BY hire_date) AS prev_salary
FROM employee;
  • 각 직원의 이전 직원의 급여를 보여줍니다. 첫 행의 경우 이전 행이 없으므로 0을 반환합니다.

2️⃣ LEAD()

LEAD() 함수는 현재 행에서 지정한 수만큼 뒤에 있는 행의 값을 반환합니다.

사용법

LEAD(컬럼명, offset, 기본값) OVER (ORDER BY 컬럼명)
  • offset(생략 가능): 몇 번째 뒤의 행을 참조할지 지정 (기본값 1)
  • 기본값(생략 가능): 참조할 행이 없을 경우 반환할 값 (기본값 NULL)

예시

SELECT emp_name, salary,
       LEAD(salary, 1, 0) OVER (ORDER BY hire_date) AS next_salary
FROM employee;
  • 각 직원의 다음 직원의 급여를 표시합니다. 마지막 행은 다음 행이 없으므로 0을 반환합니다.

3️⃣ FIRST_VALUE(), LAST_VALUE()

  • FIRST_VALUE(): 지정된 윈도우 범위 내의 첫 번째 행 값을 반환합니다.
  • LAST_VALUE(): 지정된 윈도우 범위 내의 마지막 행 값을 반환합니다.

사용법

FIRST_VALUE(컬럼명) OVER (ORDER BY 컬럼명 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

예시

SELECT emp_name, salary,
       FIRST_VALUE(salary) OVER (ORDER BY hire_date
                                 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS first_salary,
       LAST_VALUE(salary) OVER (ORDER BY hire_date
                                ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS last_salary
FROM employee;
  • 입사일 기준으로 처음 입사한 직원의 급여와 현재까지 마지막 직원의 급여를 각각 반환합니다.

⚠️ 주의사항

  • 오프셋 함수를 사용할 때는 반드시 ORDER BY 절을 명시해야 합니다.
  • FIRST_VALUE(), LAST_VALUE() 함수는 프레임절(Frame Clause)을 함께 사용할 때 정확한 결과를 얻을 수 있습니다.

🎯 요약 정리

함수명 역할 주요사용예시
LAG() 현재 행 이전의 행 값을 참조 이전 시점의 데이터와 비교 분석
LEAD() 현재 행 이후의 행 값을 참조 다음 시점의 데이터와 비교 분석
FIRST_VALUE() 윈도우 내 첫 번째 행의 값 반환 누적 비교나 최초값 기준 비교 시 사용
LAST_VALUE() 윈도우 내 마지막 행의 값 반환 누적 비교나 최신값 기준 비교 시 사용

 

반응형
반응형

SQL 윈도우 함수의 프레임절(Frame Clause) 완벽 이해하기 📈

안녕하세요, 오늘은 SQL의 강력한 기능 중 하나인 윈도우 함수(Window Functions)에서 자주 사용되는 프레임절(Frame Clause)에 대해 자세히 알아보겠습니다.

📌 프레임절(Frame Clause)이란?

프레임절은 윈도우 함수를 사용할 때 분석할 데이터의 범위를 지정하는 역할을 합니다. 윈도우 함수는 OVER 절을 이용해 특정 범위를 정의하는데, 이때 프레임절로 범위를 구체적으로 설정합니다.

기본 형태는 다음과 같습니다

윈도우함수() OVER (
    PARTITION BY 컬럼명
    ORDER BY 컬럼명
    [ROWS 또는 RANGE 프레임시작 AND 프레임종료]
);

🔍 ROWS와 RANGE의 차이점

1️⃣ ROWS

ROWS는 행의 물리적 위치를 기준으로 프레임을 정의합니다.

  • ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING: 현재 행을 기준으로 이전 행과 다음 행을 포함
  • ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: 첫 행부터 현재 행까지 포함

예시:

SELECT emp_name, salary,
       AVG(salary) OVER (
           ORDER BY salary
           ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
       ) AS avg_salary
FROM employee;
  • 각 직원의 급여를 기준으로 앞뒤 한 명씩(총 3명)의 급여 평균을 계산합니다.

2️⃣ RANGE

RANGE는 논리적인 값을 기준으로 프레임을 정의합니다. ORDER BY로 정렬된 값에 따라 프레임 범위가 결정됩니다.

  • RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: ORDER BY의 값이 시작부터 현재 행까지 같은 모든 행을 포함합니다.

예시:

SELECT salary,
       SUM(bonus) OVER (
           ORDER BY salary
           RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
       ) AS cumulative_bonus
FROM employee;
  • 급여를 기준으로 누적된 보너스를 합산하여 나타냅니다. 급여가 동일한 모든 직원은 같은 프레임에 포함됩니다.

📝 프레임절 사용 시 주의사항

  • ROWS를 사용할 때는 행 위치가 명확히 구분되지만, RANGE를 사용할 때는 동일 값이 여러 개일 경우 같은 프레임으로 묶입니다.
  • 성능적인 측면에서 ROWS가 RANGE보다 일반적으로 더 빠르게 처리됩니다.

🚩 프레임절 자주 쓰는 표현

표현식 의미

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 처음 행부터 현재 행까지 포함
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING 현재 행부터 마지막 행까지 포함
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING 현재 행의 이전 행과 다음 행 포함
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 논리적 값 기준 처음부터 현재 값까지 포함

💡 외우기 쉬운 꿀팁!

  • ROWS는 “Row by Row(행 단위로)“를 생각하면 물리적 행 위치로 기억하기 쉬워요!
  • RANGE는 “Logical RANGE(논리적 범위)“로 논리적 값을 기준으로 기억하세요.
  • “PRECEDING(이전), CURRENT(현재), FOLLOWING(이후)” 단어 뜻 그대로 외우면 이해하기 편합니다.

🎯 요약 및 결론

  • 프레임절은 윈도우 함수에서 분석 범위를 명확히 정의합니다.
  • 물리적 행 위치를 명확히 다룰 때는 ROWS를, 논리적 값의 범위를 다룰 때는 RANGE를 사용합니다.
  • 적절한 프레임 설정을 통해 원하는 데이터를 정확히 분석할 수 있습니다.

 

반응형
반응형

SQL NULL 관련 함수 완벽 정리 🌟

오늘은 SQL에서 자주 사용되면서도 초보자들이 헷갈려 하는 NULL 처리 함수들에 대해 알아보겠습니다.

SQL에서 NULL이란 ‘값이 없다’ 또는 ‘알 수 없는 값’을 의미합니다. 따라서 일반적인 값과는 다르게 처리해야 하는데요, 이때 유용하게 사용할 수 있는 함수들이 있습니다. 대표적인 NULL 처리 함수들을 하나씩 살펴볼까요?


🔍 1. NVL(컬럼명, 대체값)

NVL 함수는 Oracle에서 제공하는 함수로, 특정 컬럼이 NULL일 경우 지정한 다른 값으로 대체해줍니다.

사용법

SELECT NVL(COMM, 0)
FROM EMP;

의미: EMP 테이블의 COMM 컬럼 값이 NULL이라면 0으로 출력합니다.


🔍 2. IFNULL(컬럼명, 대체값)

MySQL/MariaDB에서 NVL 함수와 유사한 역할을 합니다.

사용법

SELECT IFNULL(COMM, 0)
FROM EMP;

의미: COMM 컬럼 값이 NULL이라면 0으로 표시합니다.


🔍 3. ISNULL(컬럼명, 대체값)

SQL Server에서 사용하는 NULL 대체 함수입니다.

사용법

SELECT ISNULL(COMM, 0)
FROM EMP;

의미: COMM 컬럼 값이 NULL일 때 0으로 변경하여 출력합니다.


🔍 4. COALESCE(컬럼1, 컬럼2, …, 컬럼n)

COALESCE 함수는 표준 SQL 함수로 여러 개의 컬럼 또는 표현식을 입력받아, NULL이 아닌 첫 번째 값을 반환합니다.

사용법

SELECT COALESCE(COMM, BONUS, 0)
FROM EMP;

의미: COMM 값이 NULL이라면 BONUS를 확인하고, BONUS도 NULL이라면 마지막 값 0을 출력합니다.

  • COALESCE는 여러 개의 컬럼을 사용할 수 있어 NVL이나 IFNULL보다 확장성이 높습니다.

🔍 5. NULLIF(표현식1, 표현식2)

두 표현식을 비교하여 값이 같다면 NULL을 반환하고, 다르면 표현식1의 값을 반환합니다.

사용법

SELECT NULLIF(SAL, 0)
FROM EMP;

의미: SAL이 0일 경우 NULL로 처리하여 출력하고, 0이 아니면 SAL의 원래 값을 출력합니다.


🌟 각 함수별 DB 지원 정리

함수명 Oracle MySQL/MariaDB SQL Server 표준 SQL
NVL ✔️
IFNULL ✔️
ISNULL ✔️
COALESCE ✔️ ✔️ ✔️ ✔️
NULLIF ✔️ ✔️ ✔️ ✔️

🎯 언제, 무엇을 쓰면 좋을까?

  • Oracle 환경이라면 NVL을, MySQL이라면 IFNULL을 선택하세요.
  • SQL Server에서는 ISNULL이 최적입니다.
  • 여러 컬럼 중 첫 번째로 나타나는 NULL이 아닌 값을 원할 땐 COALESCE를 쓰세요!
  • 특정 값이 나오면 강제로 NULL로 변환하고 싶다면 NULLIF가 딱입니다.

 

반응형
반응형

데이터베이스 설계 과정에서 테이블의 행(row)을 고유하게 식별하는 방법으로는 크게 본질식별자인조식별자가 있습니다. 각각의 식별자는 특징과 용도에 따라 적절히 활용됩니다. 오늘은 이 두 가지 식별자에 대해 알아보겠습니다.


1️⃣ 본질식별자

본질식별자는 테이블에 저장되는 데이터의 본질적인 특성을 기반으로 하여 정의되는 식별자입니다. 쉽게 말해, 업무적으로 자연스럽게 만들어지는 식별자입니다.

📌 특징

  • 업무 프로세스에서 자연스럽게 생성됩니다.
  • 테이블의 데이터 자체가 본질식별자를 포함하고 있습니다.
  • 데이터의 의미를 직접적으로 나타내는 경우가 많습니다.

📖 예시

  • 학번: 학생이 학교에 입학할 때 부여되는 번호로, 각 학생을 명확히 구별할 수 있습니다. 학번 자체만으로도 학생이라는 데이터의 본질적 특성을 반영하고 있습니다.
  • 고객번호: 고객을 고유하게 구분하기 위해 업무적으로 생성되는 번호입니다.

2️⃣ 인조식별자

인조식별자는 데이터베이스 내에서 데이터를 쉽게 관리하기 위해 인위적으로 만들어진 식별자입니다. 업무 프로세스와 직접적으로 관련되지 않으며, 주로 데이터의 본질식별자가 복잡할 때 사용됩니다.

📌 특징

  • 데이터베이스 시스템에서 자동으로 생성됩니다.
  • 본질적인 데이터 특성과는 무관하게 운영됩니다.
  • 일반적으로 순차적인 숫자나 UUID 등의 형태로 만들어집니다.

📖 예시

  • 등록ID: 수강 기록 데이터를 관리할 때 시스템이 자동으로 1, 2, 3...과 같은 순차적인 번호를 부여합니다.
  • 주문번호: 주문 엔티티가 고객번호+주문날짜+순번과 같이 복잡한 구성을 가지고 있을 때, 이를 간략화하고 쉽게 관리하기 위해 인조식별자로 단순화된 번호를 사용합니다.

인조식별자만으로는 데이터의 구체적인 내용을 알기 어렵습니다. 예를 들어, 등록ID만으로 특정 학생이 언제 어떤 과목을 수강했는지를 알 수 없으며, 반드시 학번, 학기, 과목코드 등의 다른 정보를 함께 사용해야 정확한 의미를 알 수 있습니다.


🔍 요약 정리

구분 본질식별자(Natural Key) 인조식별자(Surrogate Key)
정의 업무에서 자연스럽게 생성되는 키 시스템에서 인위적으로 생성하는 키
예시 학번, 고객번호 등록ID, 주문번호
특징 데이터의 본질적 의미 반영 데이터 관리 편의를 위한 임의적 생성
반응형
반응형

문제 설명

다음 SQL의 결과는 무엇일까요?

SELECT COUNT(*)
FROM T1
WHERE NOT EXISTS (
  SELECT 'X'
  FROM T2
  WHERE T1.COL1 = T2.COL1
);

T1 테이블

COL1

10
20

T2 테이블

COL1

10
NULL

① 2  ② 0  ③ 1  ④ 오류 발생


쿼리 분석 및 핵심 포인트

이 문제는 단순한 NOT EXISTS 서브쿼리 문제가 아니에요. NULL과의 비교 연산이 핵심 포인트입니다.

1.  NOT EXISTS란?

  • NOT EXISTS (SELECT ... FROM T2 WHERE 조건)은 T2의 조건절을 만족하는 값이 존재하지 않을 때 참이 됩니다.
  • 즉, T2에서 T1.COL1 = T2.COL1절대 참이 되지 않는 경우에만 해당 T1 행이 결과에 포함돼요.

 

2.  비교 연산자 =  NULL

여기서 주의할 점! SQL에서 NULL = 어떤 값은 무조건 UNKNOWN이 되고, 이는 WHERE 절에서 거짓(FALSE)으로 처리돼요.

예시:

  • 10 = 10 → TRUE
  • 10 = NULL → UNKNOWN → WHERE에서 걸러짐
  • NULL = NULL → UNKNOWN

실제 동작 흐름 분석

T1의 각 행에 대해 서브쿼리를 돌려보자!

T1의 첫 번째 행: COL1 = 10

서브쿼리:

SELECT 'X' FROM T2 WHERE T1.COL1 = T2.COL1;
-- 즉, WHERE 10 = T2.COL1;
  • T2에 10이 있음 → 조건 만족하는 행 있음 → EXISTS는 TRUE → NOT EXISTS는 FALSE
  • → T1의 이 행은 제외됨

 

T1의 두 번째 행: COL1 = 20

서브쿼리:

SELECT 'X' FROM T2 WHERE 20 = T2.COL1;
  • T2에는 20이 없음
  • 20 = NULL → 비교 불가(UNKNOWN)→ 이 행은 결과에 포함됨
  • → 조건을 만족하는 행이 하나도 없음EXISTS는 FALSE → NOT EXISTS는 TRUE

 

정답

→ 조건을 만족하는 행은 딱 T1의 두 번째 행 1개

③ 1


마무리 요약 정리

포인트 설명
NOT EXISTS 조건을 만족하는 값이 없을 때 참
NULL = 값 항상 UNKNOWN으로 평가됨
조건절 내 NULL 처리 조심해야 하며, 존재 유무 판단 시 혼란을 유발함
NULL이 있는 컬럼과 비교하는 서브쿼리에서는, 절대 = 조건이 항상 원하는 결과를 주지 않음! EXISTS/NOT EXISTS에서 더더욱 주의!
반응형
반응형

💡 문제

다음 SQL문 중에서 다른 결과를 반환하는 것은 무엇일까요?

-- 1번
SELECT * FROM TBL WHERE V1 = 'A' AND V2 IN ('T1', 'T2', 'T3');

-- 2번
SELECT * FROM TBL WHERE V1 = 'A' AND V2 = 'T1' OR V2 = 'T2' OR V2 = 'T3';

-- 3번
SELECT * FROM TBL WHERE (V1, V2) IN (('A', 'T1'), ('A', 'T2'), ('A', 'T3'));

-- 4번
SELECT * FROM TBL WHERE V1 = 'A' AND (V2 = 'T1' OR V2 = 'T2' OR V2 = 'T3');

📌 핵심 개념 요약

1. SQL 논리 연산자 우선순위

  • NOT > AND > OR
  • 괄호가 없으면 AND가 먼저 계산됨!

2. 튜플 비교 (row value constructor)

  • (컬럼1, 컬럼2) IN ((값1, 값2), ...) 형식
  • 컬럼 쌍이 정확히 매칭될 때만 TRUE

 

📊 각 쿼리 분석

 

✅ 1번

SELECT * FROM TBL WHERE V1 = 'A' AND V2 IN ('T1', 'T2', 'T3');
  • V1 = 'A'이면서, V2T1, T2, T3 중 하나일 때만 조회
  • ✅ 조건이 명확함
  • 정상적으로 필터링된 결과

 

❌ 2번 (정답)

SELECT * FROM TBL WHERE V1 = 'A' AND V2 = 'T1' OR V2 = 'T2' OR V2 = 'T3';
  • 괄호 없음! 그래서 아래처럼 해석됨👇
(V1 = 'A' AND V2 = 'T1') OR V2 = 'T2' OR V2 = 'T3'

 

👉 이렇게 되면?

  • V1='A'이고 V2='T1' → ✅
  • 하지만 V2='T2' 또는 V2='T3'인 행은 V1이 무엇이든 조회됨! → ❌
  • 즉 결과가 달라짐

 

✅ 3번

SELECT * FROM TBL WHERE (V1, V2) IN (('A', 'T1'), ('A', 'T2'), ('A', 'T3'));
  • (V1, V2) 쌍이 (‘A’, ‘T1’), (‘A’, ‘T2’), (‘A’, ‘T3’) 중 하나일 때만 조회
  • 1번 쿼리와 논리적으로 완전 동일한 결과
  • 튜플 비교 방식의 간결한 표현

 

✅ 4번

SELECT * FROM TBL WHERE V1 = 'A' AND (V2 = 'T1' OR V2 = 'T2' OR V2 = 'T3');
  • 괄호로 OR 조건을 묶었기 때문에
V1 = 'A' AND V2 IN ('T1', 'T2', 'T3')

와 동일한 의미입니다.

 

SQL에서는 AND가 OR보다 우선순위가 높기 때문에, 괄호가 없으면 해석이 달라질 수 있습니다.
위의 2번 쿼리는 V1='A' 조건이 일부 OR 조건에만 적용되므로 불필요한 레코드까지 조회됩니다.
정확한 조건식 표현을 위해 괄호로 논리 구조를 명확히 하는 것이 중요합니다!

 

🧠 꿀팁 암기

🎯 논리 연산자 우선순위: NOT > AND > OR
🎯 괄호 없이 쓰면 예기치 않은 결과 가능성 있음
🎯 튜플 비교는 깔끔하고 안전한 대안!

반응형
반응형

SQLD 문제

다음 중애서 도메인(Domain)에 대한 특징으로 옳지 않은 것은?

1) 릴레이션의 속성에 대한 데이터 타입과 크기이다.

2) 속성에 대하여 NOT NULL 제약사항을 설정하여 NULL 값을 허용하지 않는다.

3) 속성에 값을 입력할 때 CHECK 기능을 사용해서 입력값을 검사한다.

4) 하나의 릴레이션과 관계된 다른 릴레이션의 FK(Foreign Key) 제약조건이다.


도메인(Domain) 이란 릴레이션(=테이블)의 속성(컬럼)이 가질 수 있는 값들의 집합(타입, 범위, 형식)을 말합니다.

예를 들어,

emp_age NUMBER(3)

➡️ 여기에서 emp_age는 속성, NUMBER(3)은 도메인(정수형 숫자 3자리까지) 이라고 보면됩니다.


✍️ 보기 분석

✔ 1) 릴레이션의 속성에 대한 데이터 타입과 크기이다.

정답!

도메인은 컬럼의 데이터 타입과 크기를 포함하는 개념입니다.

  • 예) VARCHAR2(20) → 문자형 20자 = 도메인 OK

 

✔ 2) 속성에 대하여 NOT NULL 제약사항을 설정하여 NULL 값을 허용하지 않는다.

정답!

도메인은 NOT NULL 같은 제약조건도 포함할 수 있습니다.

  • 제약조건도 도메인의 일부로 간주함.
  • 도메인을 정의할 때, “이 컬럼은 반드시 값이 있어야 한다”는 것도 포함 가능.

 

✔ 3) 속성에 값을 입력할 때 CHECK 기능을 사용해서 입력값을 검사한다.

정답!

도메인 정의 시 CHECK 제약조건으로 값의 범위나 조건을 설정할 수 있습니다.

age NUMBER(3) CHECK (age BETWEEN 0 AND 120)

➡️ 나이는 0~120 사이 값만 허용 → 도메인의 일부임.

 

❌ 4) 하나의 릴레이션과 관계된 다른 릴레이션의 FK(Foreign Key) 제약조건이다.

오답! ❌❌❌

요게 바로 “도메인의 개념이 아닌 것”, 즉 정답입니다.

🔻왜냐면:

  • FK(외래키)는 두 릴레이션 간의 관계를 정의하는 것이고
  • 도메인은 단일 속성의 데이터 형식이나 제약조건에 대한 것입니다.
  • 즉, 도메인은 “속성”의 내부적 제약조건, FK는 “릴레이션 간” 외부적 제약조건입니다.
보기 옳음 여부 이유
1 O 도메인은 속성의 타입과 크기 포함
2 O NOT NULL도 도메인 제약조건 가능
3 O CHECK 조건도 도메인에 포함됨
4 FK는 릴레이션 간 관계 → 도메인 아님

 

반응형
반응형

SQL에서 데이터를 순위 매기듯 나열할 수 있다면 얼마나 유용할까요?

오늘은 그중에서도 가장 자주 쓰이는 순위 함수 ROW_NUMBER()에 대해 배우고,

직접 실습 테이블을 만들고 쿼리도 돌려보는 실습 포스트를 준비했어요!


🎯 ROW_NUMBER() 함수란?

ROW_NUMBER()는 SQL의 윈도우 함수(Window Function) 중 하나로, 지정한 정렬 기준에 따라 각 행에 고유한 순위를 부여합니다.

 

  • 같은 값이 있어도 중복 없이 1, 2, 3… 식으로 순서가 매겨져요.
  • 그룹화(PARTITION BY)와 정렬 기준(ORDER BY)을 통해 다양한 방식으로 순위를 나눌 수 있어요.

 

📌 문법

ROW_NUMBER() OVER (PARTITION BY 컬럼명 ORDER BY 정렬기준)
구성요소 설명
PARTITION BY 그룹을 나누는 기준 (선택사항)
ORDER BY 순서를 매길 기준 (필수)

🧪 실습 테이블 만들기 (오라클 기준)

 

📂 1. employees 테이블 생성

CREATE TABLE employees (
    emp_id     NUMBER PRIMARY KEY,
    emp_name   VARCHAR2(100),
    dept_name  VARCHAR2(50),
    salary     NUMBER
);

 

💾 2. 샘플 데이터 삽입

INSERT INTO employees VALUES (1, '홍길동', '영업부', 5000);
INSERT INTO employees VALUES (2, '김철수', '영업부', 5500);
INSERT INTO employees VALUES (3, '이영희', '영업부', 5000);
INSERT INTO employees VALUES (4, '박민수', '개발부', 6000);
INSERT INTO employees VALUES (5, '최지우', '개발부', 6500);
INSERT INTO employees VALUES (6, '정해인', '개발부', 6000);
INSERT INTO employees VALUES (7, '장원영', '총무부', 4500);
INSERT INTO employees VALUES (8, '안유진', '총무부', 4600);

📊 실습 1: 부서별 급여순 1등만 출력

SELECT *
FROM (
    SELECT
        emp_id,
        emp_name,
        dept_name,
        salary,
        ROW_NUMBER() OVER (
            PARTITION BY dept_name
            ORDER BY salary DESC
        ) AS rn
    FROM employees
)
WHERE rn = 1;

부서별 급여가 가장 높은 직원만 뽑아낸 결과입니다!
ROW_NUMBER()가 부서별로 급여 내림차순 정렬 후 순위를 부여하고,
rn = 1 조건으로 1등만 골라냈습니다.

 

📊 실습 2: 부서별 전체 순위 출력

SELECT
    emp_id,
    emp_name,
    dept_name,
    salary,
    ROW_NUMBER() OVER (
        PARTITION BY dept_name
        ORDER BY salary DESC
    ) AS rn
FROM employees
ORDER BY dept_name, rn;


🎁 RANK() vs DENSE_RANK() vs ROW_NUMBER()

함수 동일한 값 처리 예시 결과
ROW_NUMBER() 무조건 1, 2, 3… 1, 2, 3
RANK() 동일값은 같은 순위, 다음 순위 건너뜀 1, 1, 3
DENSE_RANK() 동일값은 같은 순위, 다음 순위 건너뛰지 않음 1, 1, 2
반응형
반응형

SQL을 사용하다 보면 데이터를 가공하거나 요약해야 할 때가 많습니다.
이럴 때 사용하는 것이 바로 함수(Function)인데요,
함수는 크게 단일행 함수(Single-Row Function)다중행 함수(Multi-Row Function)로 나눌 수 있습니다.


📌 단일행 함수 (Single-Row Function)

✅ 정의

  • 각 행(Row)에 대해 하나의 결과를 반환하는 함수입니다.
  • 입력된 행 수만큼 결과가 출력됩니다.
  • SELECT, WHERE, ORDER BY, HAVING 절 등에서 사용 가능합니다.

🛠️ 종류 및 예시

1. 문자열 함수

  • LOWER(문자열): 문자열을 소문자로 변환
  • UPPER(문자열): 문자열을 대문자로 변환
  • SUBSTR(문자열, 시작위치, 길이): 문자열의 일부를 추출
  • LENGTH(문자열): 문자열의 길이를 반환
SELECT UPPER('sql') AS 대문자변환,
       LOWER('SQL') AS 소문자변환,
       SUBSTR('SQL Tutorial', 5, 8) AS 부분문자열,
       LENGTH('SQL') AS 문자열길이
FROM dual;

2. 숫자 함수

  • ABS(숫자): 절대값 반환
  • ROUND(숫자, 소수점자리수): 반올림
  • TRUNC(숫자, 소수점자리수): 버림
  • MOD(숫자1, 숫자2): 나머지 반환
SELECT ABS(-10) AS 절대값,
       ROUND(123.456, 2) AS 반올림,
       TRUNC(123.456, 2) AS 버림,
       MOD(10, 3) AS 나머지
FROM DUAL;

3. 날짜 함수

  • SYSDATE: 현재 날짜와 시간 반환
  • ADD_MONTHS(날짜, 개월수): 지정한 개월 수만큼 더한 날짜 반환
  • MONTHS_BETWEEN(날짜1, 날짜2): 두 날짜 사이의 개월 수 반환
SELECT SYSDATE AS 현재날짜,
       ADD_MONTHS(SYSDATE, 3) AS 세달후,
       MONTHS_BETWEEN(SYSDATE, TO_DATE('2025-01-01', 'YYYY-MM-DD')) AS 개월차이
FROM DUAL;

4. 변환 함수

  • TO_CHAR(날짜 또는 숫자, 포맷): 날짜 또는 숫자를 문자열로 변환
  • TO_DATE(문자열, 포맷): 문자열을 날짜로 변환
  • TO_NUMBER(문자열): 문자열을 숫자로 변환
SELECT TO_CHAR(SYSDATE, 'YYYY-MM-DD') AS 문자열날짜,
       TO_DATE('2025-05-17', 'YYYY-MM-DD') AS 날짜형식,
       TO_NUMBER('12345') AS 숫자변환
FROM DUAL;

5. NULL 처리 함수

  • NVL(표현식, 대체값): NULL이면 대체값 반환
  • NULLIF(표현식1, 표현식2): 두 표현식이 같으면 NULL 반환, 다르면 표현식1 반환
  • COALESCE(표현식1, 표현식2, ...): NULL이 아닌 첫 번째 표현식 반환
SELECT NVL(NULL, '대체값') AS NVL_예시,
       NULLIF(10, 10) AS NULLIF_예시,
       COALESCE(NULL, NULL, '첫번째NULL아닌값') AS COALESCE_예시
FROM DUAL;


📌 다중행 함수 (Multi-Row Function)

✅ 정의

  • 여러 행을 그룹화하여 하나의 결과를 반환하는 함수입니다.
  • 주로 집계(Aggregation) 함수라고도 불립니다.
  • SELECT 절에서 사용되며, GROUP BY 절과 함께 자주 사용됩니다.

🛠️ 종류 및 예시

1. 집계 함수

  • SUM(컬럼): 합계
  • AVG(컬럼): 평균
  • MAX(컬럼): 최대값
  • MIN(컬럼): 최소값
  • COUNT(컬럼 또는 *): 개수
SELECT SUM(salary) AS 총급여,
       AVG(salary) AS 평균급여,
       MAX(salary) AS 최고급여,
       MIN(salary) AS 최저급여,
       COUNT(*) AS 직원수
FROM employees;

2. 그룹 함수와 GROUP BY 절

  • 특정 컬럼을 기준으로 그룹화하여 집계함수를 적용할 수 있습니다.
SELECT department_id,
       AVG(salary) AS 부서별평균급여
FROM employees
GROUP BY department_id;

3. HAVING 절

  • GROUP BY로 그룹화한 결과에 조건을 걸 때 사용합니다.
SELECT department_id,
       AVG(salary) AS 부서별평균급여
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 5000;

🔍 단일행 함수 vs 다중행 함수 비교

구분 단일행 함수 다중행 함수
처리 대상 각 행(Row) 여러 행(Row)
반환 결과 행마다 하나의 결과 그룹마다 하나의 결과
사용 위치 SELECT, WHERE, ORDER BY, HAVING 등 SELECT (주로 GROUP BY와 함께 사용)
반응형
반응형

💾 비정규형 테이블 (Non-Normalized Table)

-- 비정규형 주문 테이블 (ProductList에 다중값 저장)
CREATE TABLE orders_raw (
    order_id    VARCHAR(10),
    customer    VARCHAR(100),
    product_list VARCHAR(255),   -- 쉼표로 묶은 다중 값
    total_price INT
);

INSERT INTO orders_raw VALUES ('001', 'Alice', 'TV,Phone', 1500);
INSERT INTO orders_raw VALUES ('002', 'Bob', 'Laptop', 1000);

❌ 문제점

  • product_list 컬럼에 쉼표로 구분된 다중 값이 들어 있음
  • 제품 하나하나에 대한 정보가 분리되어 있지 않아서 분석이나 검색이 어려움
  • 수정, 삭제, 추가 시 데이터 이상(Anomaly)이 발생할 가능성 큼

 

🥇 1정규형 (1NF) - 원자값으로 분리

테이블의 모든 컬럼은 더 이상 쪼갤 수 없는 단일 값(Atomic Value)만 포함해야 합니다.
-- 1NF: 하나의 제품 단위로 분해
CREATE TABLE orders_1nf (
    order_id    VARCHAR(10),
    customer    VARCHAR(100),
    product     VARCHAR(100),
    total_price INT
);

INSERT INTO orders_1nf VALUES ('001', 'Alice', 'TV', 1500);
INSERT INTO orders_1nf VALUES ('001', 'Alice', 'Phone', 1500);
INSERT INTO orders_1nf VALUES ('002', 'Bob', 'Laptop', 1000);

 

🧠 설명

  • 제품을 개별 행으로 분리
  • 이렇게 하면 특정 제품에 대한 주문을 쉽게 조회하거나 통계 내기도 쉬워져요.

 

🥈 2정규형 (2NF) - 부분 종속 제거

복합키가 있는 테이블에서, 기본키 일부에만 의존하는 컬럼을 분리합니다.
-- 주문 테이블 (기본 정보만)
CREATE TABLE orders (
    order_id    VARCHAR(10) PRIMARY KEY,
    customer    VARCHAR(100),
    total_price INT
);

-- 주문 상세 테이블 (제품 단위)
CREATE TABLE order_products (
    order_id    VARCHAR(10),
    product     VARCHAR(100),
    PRIMARY KEY (order_id, product),
    FOREIGN KEY (order_id) REFERENCES orders(order_id)
);

-- 데이터 삽입
INSERT INTO orders VALUES ('001', 'Alice', 1500);
INSERT INTO orders VALUES ('002', 'Bob', 1000);

INSERT INTO order_products VALUES ('001', 'TV');
INSERT INTO order_products VALUES ('001', 'Phone');
INSERT INTO order_products VALUES ('002', 'Laptop');

 

❗ 문제

  • total_price는 실제로 order_id에만 종속되는 값이에요.
  • 그런데 현재는 order_id + product라는 복합키에 total_price가 연결되어 있어서 중복 저장되고 있어요.

🧠 설명

  • total_price는 주문 전체에 대한 정보니까 주문 기본 테이블에!
  • 제품은 제품대로 order_products에 저장하면 중복 제거 성공!

 

✅ 3정규형 (3NF) - 이행 종속 제거

기본키가 아닌 컬럼이 다른 일반 컬럼에 종속되면, 별도로 분리해야 합니다.
-- 예시: 지역정보가 고객명에 따라 결정됨 (이행 종속)
-- orders 테이블에 다음처럼 저장된다고 가정
+----------+-----------+-------------+---------+
| OrderID  | Customer  | TotalPrice  | Region  |
+----------+-----------+-------------+---------+
| 001      | Alice     | 1500        | Seoul   |
| 002      | Bob       | 1000        | Busan   |

 

  • Region은 사실상 Customer에 종속된 정보예요.
  • 고객의 지역이 자주 바뀌는 경우, 여러 행을 동시에 수정해야 해서 갱신 이상이 발생할 수 있어요.

 

-- 고객 테이블로 분리 (Region은 Customer에 종속되므로)
CREATE TABLE customers (
    customer    VARCHAR(100) PRIMARY KEY,
    region      VARCHAR(100)
);

-- orders 테이블은 customer만 참조하도록 유지
-- (이미 생성되어 있으면 ALTER로 FK 설정해도 OK)

-- 고객 데이터 삽입
INSERT INTO customers VALUES ('Alice', 'Seoul');
INSERT INTO customers VALUES ('Bob', 'Busan');

🧠 설명

  • 고객 정보를 별도 테이블로 분리하여 데이터 중복 제거 + 관리 효율성 향상!

 

 

🧪 실습 쿼리 예시

-- 주문자별 주문 총액 확인
SELECT o.customer, o.total_price
FROM orders o;

-- 주문별 주문한 제품 확인
SELECT op.order_id, op.product
FROM order_products op;

-- 고객 지역 포함 전체 조회
SELECT o.order_id, o.customer, c.region, o.total_price
FROM orders o
JOIN customers c ON o.customer = c.customer;

 

 

🎯 요약

정규형 핵심 변경 내용 대표 테이블 구조
1NF 다중값 분리 (쉼표 → 행 분리) orders_1nf
2NF 주문정보 / 제품정보 분리 orders, order_products
3NF 고객의 지역정보 분리 customers (Customer → Region 종속)
반응형
반응형

SQL에서 그룹함수는 GROUP BY와 함께 쓰지만,
윈도우 함수(Window Function)행(row)을 유지하면서,
그 위에 순위, 누적합, 평균, 비율을 매겨주는 강력한 분석 도구에요!


🧪 실습용 테이블 (Oracle 기준)

CREATE TABLE employees (
  emp_id     NUMBER PRIMARY KEY,
  emp_name   VARCHAR2(50),
  dept_id    NUMBER,
  salary     NUMBER
);

INSERT INTO employees VALUES (1, '홍길동', 10, 3000);
INSERT INTO employees VALUES (2, '김영희', 10, 2800);
INSERT INTO employees VALUES (3, '이철수', 10, 3000);
INSERT INTO employees VALUES (4, '최지우', 20, 3500);
INSERT INTO employees VALUES (5, '박민수', 20, 2700);
INSERT INTO employees VALUES (6, '서지훈', 30, 4000);

 
✔️ 설정 포인트

  • 3개 부서 (dept_id)
  • 동일 급여자 있음
  • 부서별 순위/합계/비율 실습 가능

1️⃣  순위 함수 (Ranking Functions)

SELECT emp_name, dept_id, salary,
       RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rank,
       DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS dense_rank,
       ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS row_num
FROM employees;

 

함수의미특징
RANK()공동 순위 허용, 중복 발생 시 건너뜀1, 1, 3, 4…
DENSE_RANK()공동 순위 허용, 건너뛰지 않음1, 1, 2, 3…
ROW_NUMBER()무조건 순번 부여중복 없이 1, 2, 3, 4…

📌 PARTITION BY부서별로 그룹화
📌 ORDER BY급여 기준 정렬


2️⃣  집계 함수 (Aggregate Functions over Window)

SELECT emp_name, dept_id, salary,
       SUM(salary) OVER (PARTITION BY dept_id) AS dept_total,
       AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg,
       MAX(salary) OVER (PARTITION BY dept_id) AS dept_max,
       MIN(salary) OVER (PARTITION BY dept_id) AS dept_min
FROM employees;

 
🧠 설명:
각 사원에 대해 부서별 총합, 평균, 최댓값, 최솟값을 행 단위로 함께 출력
✔️ GROUP BY 없이도 집계 가능!
✔️ 행을 유지하면서 통계도 동시에 계산 가능함


3️⃣  행 순서 함수 (Row Navigation)

SELECT emp_name, dept_id, salary,
       LAG(salary, 1) OVER (PARTITION BY dept_id ORDER BY salary DESC) AS prev_salary,
       LEAD(salary, 1) OVER (PARTITION BY dept_id ORDER BY salary DESC) AS next_salary
FROM employees;

 
🧠 설명

  • LAG() : 현재 행 기준으로 이전 행의 값
  • LEAD() : 현재 행 기준으로 다음 행의 값

 
📌 사용 예

  • 전월 대비 매출 비교
  • 이전/다음 행과의 차이 분석

4️⃣  비율 함수 (Ratio Functions)

SELECT emp_name, dept_id, salary,
       RATIO_TO_REPORT(salary) OVER (PARTITION BY dept_id) AS dept_salary_ratio
FROM employees;

 
🧠 설명:
각 사원의 급여가 자신의 부서 내에서 차지하는 비율을 계산함
→ 부서 내에서 내가 몇 %나 차지하고 있는지 분석할 때 유용!
 
📌 예) 부서별 급여 합계가 100이라면, 내 급여는 그 중 몇 퍼센트?

반응형
반응형

SQL의 GROUP BY는 집계할 때 필수죠?

그런데 복잡한 다중 그룹을 요약하려면?

ROLLUP, CUBE, GROUPING SETS가 등장합니다! 🎉


🧪 실습용 테이블 만들기

CREATE TABLE sales (
  region VARCHAR(20),     -- 지역
  product VARCHAR(20),    -- 제품
  sales_amt INT           -- 매출액
);

INSERT INTO sales VALUES
('서울', '노트북', 1000),
('서울', '폰', 800),
('부산', '노트북', 1200),
('부산', '폰', 700),
('대전', '폰', 600);

1️⃣ 기본 GROUP BY

SELECT region, product, SUM(sales_amt) AS total_sales
FROM sales
GROUP BY region, product;

 

🧠 설명: 지역 + 제품별로 매출을 합산합니다.

✔️ 결과는 각 조합별 매출 합계만 나옴.


2️⃣  ROLLUP : 단계적 합계 구하기

SELECT region, product, SUM(sales_amt) AS total_sales
FROM sales
GROUP BY ROLLUP(region, product);

 

🧠 설명:

ROLLUP(A, B)은 다음 조합을 자동 계산해줍니다:

  1. (A, B) ← 그룹별
  2. (A, NULL) ← A 단위 소계
  3. (NULL, NULL) ← 전체 합계

✔️ 이걸로 “소계”와 “총합” 자동 계산 가능!


3️⃣  CUBE : 모든 조합 계산

SELECT region, product, SUM(sales_amt) AS total_sales
FROM sales
GROUP BY CUBE(region, product);

 

🧠 설명:

CUBE(A, B)는 가능한 모든 조합을 만들어줍니다:

  • (A, B)
  • (A, NULL)
  • (NULL, B)
  • (NULL, NULL)

즉, 행과 열의 소계까지 모두 구해줌!


4️⃣  GROUPING SETS : 내가 원하는 그룹만

SELECT region, product, SUM(sales_amt) AS total_sales
FROM sales
GROUP BY GROUPING SETS (
  (region, product),
  (region),
  (product),
  ()
);

 

🧠 설명:

GROUPING SETS직접 원하는 집계 조합만 지정할 수 있어요

위 예시는:

  1. (region, product) : 기본 조합
  2. (region) : 지역별 소계
  3. (product) : 제품별 소계
  4. () : 전체합계

✔️ 불필요한 조합은 생략하고!

✔️ 정확히 내가 원하는 집계만 나옴!


5️⃣  GROUPING()함수: NULL은 진짜 NULL일까?

집계 결과에서 NULL은 “소계”를 의미할 때도 있지만,

진짜 NULL일 수도 있죠? 🤔 그래서 도와주는 함수가 GROUPING()!

SELECT
  region,
  product,
  GROUPING(region) AS grp_region,
  GROUPING(product) AS grp_product,
  SUM(sales_amt) AS total_sales
FROM sales
GROUP BY ROLLUP(region, product);

 

  • GROUPING = 1 → 이건 집계용 NULL (즉 소계나 총계)
  • GROUPING = 0 → 이건 진짜 값

 

5️⃣  GROUPING() 함수 + CASE문으로 소계/총계 라벨 붙이기

아래 쿼리는 ROLLUP 결과에 "소계", "총계" 라는 이름을 직관적으로 붙여주는 방법이에요!

SELECT
  CASE 
    WHEN GROUPING(region) = 1 AND GROUPING(product) = 1 THEN '총계'
    WHEN GROUPING(region) = 0 AND GROUPING(product) = 1 THEN region || ' 소계'
    ELSE region
  END AS region_label,
  product,
  SUM(sales_amt) AS total_sales
FROM sales
GROUP BY ROLLUP(region, product);

조건 의미 출력값
GROUPING(region)=1 AND GROUPING(product)=1 둘 다 소계 → 전체 총계 '총계'
GROUPING(product)=1 product만 소계 → 지역 소계 '서울 소계', '부산 소계' 등
그 외 일반 행 그대로 지역명

🧾 요약 정리표

문법 설명 활용 예
ROLLUP(A, B) A, A+B, 전체 합계를 단계적으로 계산 보고서 소계/총계
CUBE(A, B) A, B, A+B, 전체 조합을 모두 계산 피벗 요약
GROUPING SETS 원하는 그룹 조합만 선택적으로 지정 가능 맞춤형 집계
GROUPING(col) 집계 NULL인지 실제 NULL인지 구분 UI 표시 분기처리
반응형

+ Recent posts