SQL 프로그래밍 (2)
포스트
취소

SQL 프로그래밍 (2)

table of contents

SQL 명령어 순서와 우선순위

SQL 명령어의 체계는 구문 작성 순서(Lexical Order), 논리적 실행 순서(Logical Execution Order), 그리고 연산자 우선순위(Operator Precedence)로 구분됩니다.

1. SQL 구문 작성 순서 (Lexical Order)

사용자가 쿼리문을 작성할 때 반드시 지켜야 하는 문법적 순서입니다 [1].

순서절 (Clause)설명
1SELECT출력할 컬럼 및 집계 함수 정의
2FROM데이터를 추출할 주 테이블 지정
3JOIN다른 테이블과의 결합 (ON 조건 포함)
4WHERE행(Row) 단위 필터링 조건
5GROUP BY특정 컬럼 기준 데이터 그룹화
6HAVING그룹화된 결과에 대한 집계 조건
7ORDER BY결과의 정렬 순서 정의
8LIMIT / OFFSET출력할 행의 수 및 시작 위치 제한

2. 논리적 실행 순서 (Logical Execution Order)

데이터베이스 관리 시스템(DBMS)이 쿼리를 최적화하고 데이터를 추출하는 실제 논리적 단계입니다. SELECT 절에서 정의한 별칭(Alias)을 WHERE 절에서 사용할 수 없는 이유는 이 순서 때문입니다 [1].

  1. FROM (+ JOIN/ON): 데이터의 소스를 식별하고 가상 테이블을 생성합니다.
  2. WHERE: 가상 테이블의 각 행에 조건을 적용하여 필터링합니다.
  3. GROUP BY: 행을 그룹화하여 단일 행으로 요약할 준비를 합니다.
  4. HAVING: 그룹화된 요약 결과에 필터링 조건을 적용합니다.
  5. SELECT: 최종 데이터셋에 포함될 컬럼을 선택하고 표현식을 계산합니다.
  6. DISTINCT: 중복된 결과 행을 제거합니다.
  7. ORDER BY: 지정된 기준에 따라 최종 행들을 정렬합니다.
  8. LIMIT / OFFSET: 클라이언트에게 반환할 최종 행의 범위를 결정합니다.

3. 연산자 우선순위 (Operator Precedence)

WHERE 절과 같은 조건식에서 여러 연산자가 혼용될 때 처리되는 우선순위입니다 [2], [3].

  1. 괄호 (): 가장 높은 우선순위로 내부 연산을 먼저 처리합니다.
  2. 산술 연산자: 곱셈(), 나눗셈(/), 나머지(%) 연산 후 덧셈(+), 뺄셈()을 수행합니다.
  3. 비교 연산자: =, <>, <, <=, >, >= 등의 대소 및 일치 비교를 수행합니다.
  4. 특수 비교 연산자: IS NULL, LIKE, IN, BETWEEN 연산이 수행됩니다.
  5. 논리 NOT: 조건의 진위 여부를 반전시킵니다.
  6. 논리 AND: 두 조건이 모두 참인 경우를 판별합니다.
  7. 논리 OR: 두 조건 중 하나라도 참인 경우를 판별합니다.

참고문헌

  1. Microsoft. (2023, May 23). SELECT (Transact-SQL) - SQL Server. Microsoft Learn. https://learn.microsoft.com/en-us/sql/t-sql/queries/select-transact-sql?view=sql-server-ver17
  2. MySQL. (2024). MySQL 8.0 Reference Manual: 12.4.1 Operator Precedence. MySQL. https://dev.mysql.com/doc/refman/8.0/en/operator-precedence.html
  3. PostgreSQL Global Development Group. (2024). PostgreSQL 16 Documentation: 4.1.6. Operator Precedence. PostgreSQL. https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-PRECEDENCE

내가 어제 CRUD 필기 안했더라

  • create, read, update, delete 줄여서 CRUD

  • create는 테이블 만들 때 쓰는거임 코드 생략

  • insert는 필기 중에 있었어

    1
    
      INSERT INTO autoever.members (`name`, `age`) value('다은', 23);
    
  • update는 값 바꾸기인데 where 안쓰면 전부 통으로 바꾼다 주의

    1
    2
    
      UPDATE autoever.members SET age=37 WHERE member_id=9;
      UPDATE autoever.members SET name='가은' WHERE member_id=8;
    
  • delete 당연히 삭제죠

    1
    2
    
      DELETE FROM autoever.members WHERE member_id=8;
      DELETE FROM autoever.members WHERE member_id=9;
    

기타 검색 및 그룹 기능

  • 와일드 카드 검색

    1
    2
    3
    4
    5
    6
    7
    
      -- 와일드 카드 검색
      SELECT * FROM city
      WHERE name LIKE 'New%';  -- 뒤에 오는 뭐든 검색
    
      -- 글자 수 지정 와일드 카드 검색
      SELECT * FROM city
      WHERE countrycode LIKE 'K_R';  -- _ 글자 수만큼 채워진 단어 검색
    
  • group by

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      -- group by
      select CountryCode, sum(population)
      from city
      group by CountryCode
    
      -- group by having
      SELECT CountryCode, count(population) AS CityCount
      FROM city
      GROUP BY CountryCode
      HAVING CityCount > 200
    

문제를 푸세요

1. 수업용 스키마 문제

문제 보기
  1. 모든 필드 출력하되 + id가 2, 4번 레코드만 출력하기

    1
    2
    3
    4
    5
    6
    
     SELECT
         *
     FROM
         members
     WHERE
         member_id IN (2, 4)
    
  2. id, age 필드 출력하고, 1 <= id <= 3번 레코드만 출력하기 (조건에 AND 사용)

    1
    2
    3
    4
    5
    6
    
     SELECT
         member_id, age
     FROM
         members
     WHERE
         1 <= member_id AND member_id <= 3
    
  3. 나이가 30대인 멤버들의 모든 컬럼 출력

    1
    2
    3
    4
    5
    6
    
     SELECT
         *
     FROM
         members
     WHERE
         age BETWEEN 30 AND 39
    
  4. id는 홀수이면서, age는 짝수인 멤버들의 이름만 출력

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     SELECT
         name
     FROM
         members
     WHERE
         member_id % 2 = 1 AND age % 2 = 0
     -- mod()를 쓰세요: 특수문자 안써도 됨
         -- WHERE MOD(member_id, 2) = 1 AND MOD(age, 2) = 0
     -- &도 써보세요: 내부 연산이 더 효율적, 근데 가독성이 떨어질 수 있음
         -- WHERE (member_id & 1) = 1 AND (age & 1) = 0
    
  5. id 을 “번호”, name을 “성함”, age를 “나이” 로 출력하되 나이를 기준으로 오름차순

    1
    2
    3
    4
    5
    
     SELECT
         member_id AS `번호`, name AS `이름`, age AS `나이`
     FROM
         members
     ORDER BY `나이` ASC
    
  6. 이름을 가나다순으로 정렬하고, 이름과 나이 만 출력

    1
    2
    3
    4
    5
    6
    7
    
     SELECT
         `name`, age
     FROM
         members
     ORDER BY `name` ASC
     -- 인덱싱 가능
     -- ORDER BY 1 ASC
    
  7. id 1 번과 3 번 레코드의 나이를 모두 33로 수정

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     UPDATE members
     SET
         age = 33
     WHERE
         member_id IN (1, 3);
    
     SELECT
         *
     FROM
         members;
    
  8. 5 번 레코드 삭제

    1
    2
    3
    4
    5
    6
    7
    8
    
     DELETE FROM members
     WHERE
         member_id = 5;
    
     SELECT
         *
     FROM
         members;
    
  9. 레코드 추가 (정선, 32)

    1
    2
    3
    4
    5
    6
    7
    
     INSERT INTO
         members (`name`, `age`) VALUE ('정선', 32);
    
     SELECT
         *
     FROM
         members;
    
  10. 나이가 가장 많은 멤버의 이름과 나이를 출력

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    SELECT
        name, age
    FROM
        members
    ORDER BY age DESC
    LIMIT 1
    
    -- MAX() 함수 사용 -> 서브쿼리
    SELECT
        name, age
    FROM
        members
    WHERE
        age = (SELECT MAX(age) FROM members)
    

2. world 스키마 문제

문제 보기
  1. 인구수가 1000명 미만이면서, A로 시작하는 도시 찾기

    1
    2
    3
    4
    5
    6
    
     SELECT
         *
     FROM
         city
     WHERE
         Population < 1000 AND name LIKE 'A%'
    
  2. 일본 (JPN) 에서, 인구수가 100만명 ~ 200만명 인 도시를 찾아 도시 이름과, 국가 코드 (JPN) 필드만 출력하기

    1
    2
    3
    4
    5
    6
    7
    
     SELECT
         `name`, CountryCode
     FROM
         city
     WHERE
         CountryCode = 'JPN'
             AND Population BETWEEN 1000000 AND 2000000
    
  3. kim 이라는 글자가 포함된 도시명 찾기

    1
    2
    3
    4
    5
    6
    
     SELECT
         `name`
     FROM
         city
     WHERE
         `name` LIKE '%kim%'
    
  4. world 스키마의 레코드 총 개수 출력

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
     -- world 스키마에 있는 테이블 목록: city, country, countrylanguage
     SELECT
         (SELECT
                 COUNT(*)
             FROM
                 city) + (SELECT
                 COUNT(*)
             FROM
                 country) + (SELECT
                 COUNT(*)
             FROM
                 countrylanguage) AS TotalRecords
    
  5. 하위 5개의 도시 코드, 인구수 출력

    1
    2
    3
    4
    5
    6
    
     SELECT
         CountryCode, Population
     FROM
         city
     ORDER BY Population ASC
     LIMIT 5
    
  6. TOP 5개의 국가 코드, 인구수 출력

    1
    2
    3
    4
    5
    6
    
     SELECT
         CountryCode, Population
     FROM
         city
     ORDER BY Population DESC
     LIMIT 5
    

3. W3school에 있는 DB로 문제 풀기

  • DB 구조

      erDiagram
          CUSTOMERS ||--o{ ORDERS : "places"
          EMPLOYEES ||--o{ ORDERS : "manages"
          SHIPPERS ||--o{ ORDERS : "ships"
          ORDERS ||--|{ ORDERDETAILS : "contains"
          PRODUCTS ||--|{ ORDERDETAILS : "is ordered in"
          CATEGORIES ||--o{ PRODUCTS : "classifies"
          SUPPLIERS ||--o{ PRODUCTS : "supplies"
    
          CUSTOMERS {
              int CustomerID PK
              varchar CustomerName
              varchar ContactName
              varchar Address
              varchar City
              varchar PostalCode
              varchar Country
          }
    
          CATEGORIES {
              int CategoryID PK
              varchar CategoryName
              varchar Description
          }
    
          EMPLOYEES {
              int EmployeeID PK
              varchar LastName
              varchar FirstName
              date BirthDate
              varchar Photo
              longtext Notes
          }
    
          ORDERS {
              int OrderID PK
              int CustomerID FK
              int EmployeeID FK
              date OrderDate
              int ShipperID FK
          }
    
          ORDERDETAILS {
              int OrderDetailID PK
              int OrderID FK
              int ProductID FK
              int Quantity
          }
    
          PRODUCTS {
              int ProductID PK
              varchar ProductName
              int SupplierID FK
              int CategoryID FK
              varchar Unit
              decimal Price
          }
    
          SHIPPERS {
              int ShipperID PK
              varchar ShipperName
              varchar Phone
          }
    
          SUPPLIERS {
              int SupplierID PK
              varchar SupplierName
              varchar ContactName
              varchar Address
              varchar City
              varchar PostalCode
              varchar Country
              varchar Phone
          }
      

다음 링크의 스키마에서 문제 풀어보기

https://www.w3schools.com/mysql/trymysql.asp?filename=trysql_select_all

문제 보기
  1. CustomerID 33초과이면서, EmployeeID 5 미만 출력

    1
    2
    3
    4
    5
    6
    
     SELECT
         *
     FROM
         Orders
     WHERE
         CustomerID > 33 AND EmployeeID < 5
    
  2. OrderID 10,000 미만 또는 ShipperID 5 미만 출력

    1
    2
    3
    4
    5
    6
    
     SELECT
         *
     FROM
         Orders
     WHERE
         OrderID < 10000 OR ShipperID < 5
    
  3. OrderDetails의 ProductID가 14보다 큰 모든 필드 출력

    1
    2
    3
    4
    5
    6
    
     SELECT
         *
     FROM
         OrderDetails
     WHERE
         ProductID > 14
    
  4. CustomerName, city 필드 출력 10 < CustomerID <= 50 레코드출력 (Customers에 데이터 존재)

    1
    2
    3
    4
    5
    6
    
     SELECT
         CustomerName, city
     FROM
         Customers
     WHERE
         CustomerID > 10 AND CustomerID <= 50
    
  5. Orders에 CustomerId 가 30번대만 출력 (모든 필드 출력, and 사용)

    1
    2
    3
    4
    5
    6
    
     SELECT
         *
     FROM
         Orders
     WHERE
         CustomerID >= 30 AND CustomerID < 40
    
  6. ProductID가 짝수이면서, SupplierId 홀수인 ProductName과 ProductID 출력 (Products)

    1
    2
    3
    4
    5
    6
    
     SELECT
         ProductName, ProductID
     FROM
         Products
     WHERE
         ProductID % 2 = 0 AND SupplierID % 2 = 1
    
  7. Customers에 Address를 “주소”, city를 “도시”, PostalCode를 “우편번호” 로 출력, 우편번호 내림차순으로 정렬

    1
    2
    3
    4
    5
    
     SELECT
         Address AS `주소`, city AS `도시`, PostalCode AS `우편번호`
     FROM
         Customers
     ORDER BY PostalCode DESC
    
  8. OrderDetails에 OrderDetailID를 기준으로 내림차순으로 정렬하고, 5개만 출력하기 (모든 컬럼)

    1
    2
    3
    4
    5
    6
    
     SELECT
         *
     FROM
         OrderDetails
     ORDER BY OrderDetailID DESC
     LIMIT 5
    
  9. Orders에 모든 컬럼을 OrderId를 기준으로 내림차순 정렬후, 3개를 건너띄고 7개만큼 출력하기

    1
    2
    3
    4
    5
    6
    
     SELECT
         *
     FROM
         Orders
     ORDER BY OrderId DESC
     LIMIT 3, 7
    
  10. OrderDetails에서 다음 alias 지정

    • OrderDetailID => 상세주문번호
    • OrderID => 주문번호
    • ProductID => 상품번호
    • Quantity => 수량

    OrderID 10,000 이하 또는 ProductID 50 이상을 출력하되, OrderDetailID 기준 내림차순 정렬하며, 8개만 출력

    1
    2
    3
    4
    5
    6
    7
    8
    
    SELECT
        OrderDetailID AS `상세주문번호`, OrderID AS `주문번호`, ProductID AS `상품번호`, Quantity AS `수량`
    FROM
        OrderDetails
    WHERE
        OrderID <= 10000 OR ProductID >= 50
    ORDER BY OrderDetailID DESC
    LIMIT 8
    
  11. OrderDetails의 모든 컬럼을 출력하되, ProductID가 1로 시작하는 모든 레코드 출력

    1
    2
    3
    4
    5
    6
    
    SELECT
        *
    FROM
        OrderDetails
    WHERE
        ProductID LIKE '1%'
    
  12. OrderDetails의 모든 컬럼을 출력하되, ProductID의 십의자리가 1인 모든 레코드 출력

    1
    2
    3
    4
    5
    6
    
    SELECT
        *
    FROM
        OrderDetails
    WHERE
        ProductID % 100 >= 10 AND ProductID % 100 < 20
    

어떤 생각으로 SQL 쓰셨어요?

SQL은 명령형 언어(Python, C++ 등)와 달리 선언형 언어입니다. 즉, ‘어떻게(How) 처리할 것인가’보다 ‘무엇을(What) 가져올 것인가’에 집중해야 합니다. 복잡한 요구사항이 주어졌을 때 쿼리를 논리적으로 구성하기 위해서는 작성 순서가 아닌 SQL 엔진의 논리적 실행 순서를 기준으로 사고하는 것이 가장 효율적입니다.

1. SQL 논리적 실행 순서에 따른 사고 흐름

쿼리를 작성할 때는 아래의 실행 순서에 따라 데이터를 단계적으로 필터링하고 가공한다고 생각하십시오.

순서구문 (Clause)사고 과정 (Mental Model)
1FROM (+ JOIN)“어떤 주머니(테이블)에서 데이터를 꺼낼 것인가?” (데이터 원천 확보)
2WHERE“필요 없는 행(Row)을 미리 버릴 조건은 무엇인가?” (개별 행 필터링)
3GROUP BY“데이터를 어떤 기준(그룹)으로 묶을 것인가?” (집계 단위 설정)
4HAVING“그룹화된 결과 중 버릴 그룹은 무엇인가?” (그룹 필터링)
5SELECT“최종적으로 어떤 컬럼을 보여줄 것인가?” (출력 데이터 결정)
6ORDER BY“어떤 순서로 정렬하여 보여줄 것인가?” (결과 정렬)
7LIMIT / OFFSET“몇 개만 보여줄 것인가?” (결과 개수 제한)

2. 복잡한 요구사항 분석 및 설계 단계

요구사항이 많을 때는 다음 3단계 프로세스를 거쳐 쿼리를 설계하십시오.

1단계: 데이터 소스 식별 (FROM / JOIN)

  • 사고 방식: “내가 필요한 정보가 어디에 흩어져 있는가?”
  • 필요한 컬럼들이 담긴 테이블들을 나열하고, 이들을 연결할 Key(PK, FK)를 찾습니다.
  • 주의: JOIN은 데이터 양을 폭발시킬 수 있으므로, 기준이 되는 테이블이 무엇인지 명확히 정해야 합니다.

2단계: 조건 필터링 (WHERE / HAVING)

  • 사고 방식: “계산하기 전에 미리 걸러낼 수 있는 것은 무엇인가?”
  • 집계(SUM, AVG 등)를 하기 전에 걸러낼 수 있는 조건은 반드시 WHERE에서 처리하여 연산 대상을 줄입니다.
  • 집계된 결과값(예: 평균 매출이 100만 원 이상인 그룹)에 대한 조건만 HAVING으로 보냅니다.

3단계: 집계 및 변환 (GROUP BY / SELECT)

  • 사고 방식: “결과물의 한 행(Row)이 의미하는 바가 무엇인가?”
  • 요구사항에서 “~별로”라는 표현이 나오면 그것이 GROUP BY의 기준이 됩니다.
  • SELECT 단계에서 산술 연산, CASE 문을 활용한 분기 처리, 데이터 타입 변환 등을 수행합니다.

3. 예시를 통한 사고 흐름 적용

문제 상황: “2023년에 3번 이상 주문하고, 총 구매 금액이 50만 원 이상인 고객의 ID와 총 금액을 금액이 높은 순으로 추출하라.”

  1. FROM: Orders 테이블과 Customers 테이블을 CustomerID로 JOIN 한다.
  2. WHERE: OrderDate가 ‘2023-01-01’부터 ‘2023-12-31’ 사이인 행만 남긴다.
  3. GROUP BY: 고객별로 정보를 봐야 하므로 CustomerID로 그룹화한다.
  4. HAVING: 그룹화된 데이터 중 COUNT(OrderID) >= 3 이고 SUM(Price) >= 500,000인 그룹만 남긴다.
  5. SELECT: CustomerIDSUM(Price)를 선택한다.
  6. ORDER BY: SUM(Price)를 기준으로 내림차순(DESC) 정렬한다.

4. 팁: 복잡할 땐 단계별로 쪼개기 (WITH 절 활용)

요구사항이 너무 복잡하여 한 번에 쿼리를 쓰기 어렵다면, CTE(Common Table Expression)를 활용하여 가독성을 높이십시오.

1
2
3
4
5
6
7
8
9
10
WITH FilteredOrders AS (
    -- 1~2단계를 여기서 먼저 처리
    SELECT ... FROM ... WHERE ...
),
AggregatedData AS (
    -- 3~4단계를 여기서 처리
    SELECT ... FROM FilteredOrders GROUP BY ... HAVING ...
)
-- 마지막으로 최종 출력
SELECT * FROM AggregatedData ORDER BY ...

이 방식은 마치 프로그래밍에서 함수를 분리하는 것과 같아 디버깅과 논리 파악이 훨씬 수월해집니다.

이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.

SQL 프로그래밍 (1)

스프링부트 (1)