table of contents
- SQL 명령어 순서와 우선순위
- 내가 어제 CRUD 필기 안했더라
- 기타 검색 및 그룹 기능
- 문제를 푸세요
- 어떤 생각으로 SQL 쓰셨어요?
- 1. SQL 논리적 실행 순서에 따른 사고 흐름
- 2. 복잡한 요구사항 분석 및 설계 단계
- 3. 예시를 통한 사고 흐름 적용
- 4. 팁: 복잡할 땐 단계별로 쪼개기 (WITH 절 활용)
SQL 명령어 순서와 우선순위
SQL 명령어의 체계는 구문 작성 순서(Lexical Order), 논리적 실행 순서(Logical Execution Order), 그리고 연산자 우선순위(Operator Precedence)로 구분됩니다.
1. SQL 구문 작성 순서 (Lexical Order)
사용자가 쿼리문을 작성할 때 반드시 지켜야 하는 문법적 순서입니다 [1].
| 순서 | 절 (Clause) | 설명 |
|---|---|---|
| 1 | SELECT | 출력할 컬럼 및 집계 함수 정의 |
| 2 | FROM | 데이터를 추출할 주 테이블 지정 |
| 3 | JOIN | 다른 테이블과의 결합 (ON 조건 포함) |
| 4 | WHERE | 행(Row) 단위 필터링 조건 |
| 5 | GROUP BY | 특정 컬럼 기준 데이터 그룹화 |
| 6 | HAVING | 그룹화된 결과에 대한 집계 조건 |
| 7 | ORDER BY | 결과의 정렬 순서 정의 |
| 8 | LIMIT / OFFSET | 출력할 행의 수 및 시작 위치 제한 |
2. 논리적 실행 순서 (Logical Execution Order)
데이터베이스 관리 시스템(DBMS)이 쿼리를 최적화하고 데이터를 추출하는 실제 논리적 단계입니다. SELECT 절에서 정의한 별칭(Alias)을 WHERE 절에서 사용할 수 없는 이유는 이 순서 때문입니다 [1].
- FROM (+ JOIN/ON): 데이터의 소스를 식별하고 가상 테이블을 생성합니다.
- WHERE: 가상 테이블의 각 행에 조건을 적용하여 필터링합니다.
- GROUP BY: 행을 그룹화하여 단일 행으로 요약할 준비를 합니다.
- HAVING: 그룹화된 요약 결과에 필터링 조건을 적용합니다.
- SELECT: 최종 데이터셋에 포함될 컬럼을 선택하고 표현식을 계산합니다.
- DISTINCT: 중복된 결과 행을 제거합니다.
- ORDER BY: 지정된 기준에 따라 최종 행들을 정렬합니다.
- LIMIT / OFFSET: 클라이언트에게 반환할 최종 행의 범위를 결정합니다.
3. 연산자 우선순위 (Operator Precedence)
WHERE 절과 같은 조건식에서 여러 연산자가 혼용될 때 처리되는 우선순위입니다 [2], [3].
- 괄호
(): 가장 높은 우선순위로 내부 연산을 먼저 처리합니다. - 산술 연산자: 곱셈(), 나눗셈(
/), 나머지(%) 연산 후 덧셈(+), 뺄셈()을 수행합니다. - 비교 연산자:
=,<>,<,<=,>,>=등의 대소 및 일치 비교를 수행합니다. - 특수 비교 연산자:
IS NULL,LIKE,IN,BETWEEN연산이 수행됩니다. - 논리 NOT: 조건의 진위 여부를 반전시킵니다.
- 논리 AND: 두 조건이 모두 참인 경우를 판별합니다.
- 논리 OR: 두 조건 중 하나라도 참인 경우를 판별합니다.
참고문헌
- 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
- 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
- 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. 수업용 스키마 문제
문제 보기
모든 필드 출력하되 + id가 2, 4번 레코드만 출력하기
1 2 3 4 5 6
SELECT * FROM members WHERE member_id IN (2, 4)
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
나이가 30대인 멤버들의 모든 컬럼 출력
1 2 3 4 5 6
SELECT * FROM members WHERE age BETWEEN 30 AND 39
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
id 을 “번호”, name을 “성함”, age를 “나이” 로 출력하되 나이를 기준으로 오름차순
1 2 3 4 5
SELECT member_id AS `번호`, name AS `이름`, age AS `나이` FROM members ORDER BY `나이` ASC
이름을 가나다순으로 정렬하고, 이름과 나이 만 출력
1 2 3 4 5 6 7
SELECT `name`, age FROM members ORDER BY `name` ASC -- 인덱싱 가능 -- ORDER BY 1 ASC
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;
5 번 레코드 삭제
1 2 3 4 5 6 7 8
DELETE FROM members WHERE member_id = 5; SELECT * FROM members;
레코드 추가 (정선, 32)
1 2 3 4 5 6 7
INSERT INTO members (`name`, `age`) VALUE ('정선', 32); SELECT * FROM members;
나이가 가장 많은 멤버의 이름과 나이를 출력
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 스키마 문제
문제 보기
인구수가 1000명 미만이면서, A로 시작하는 도시 찾기
1 2 3 4 5 6
SELECT * FROM city WHERE Population < 1000 AND name LIKE 'A%'
일본 (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
kim 이라는 글자가 포함된 도시명 찾기
1 2 3 4 5 6
SELECT `name` FROM city WHERE `name` LIKE '%kim%'
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개의 도시 코드, 인구수 출력
1 2 3 4 5 6
SELECT CountryCode, Population FROM city ORDER BY Population ASC LIMIT 5
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
문제 보기
CustomerID 33초과이면서, EmployeeID 5 미만 출력
1 2 3 4 5 6
SELECT * FROM Orders WHERE CustomerID > 33 AND EmployeeID < 5
OrderID 10,000 미만 또는 ShipperID 5 미만 출력
1 2 3 4 5 6
SELECT * FROM Orders WHERE OrderID < 10000 OR ShipperID < 5
OrderDetails의 ProductID가 14보다 큰 모든 필드 출력
1 2 3 4 5 6
SELECT * FROM OrderDetails WHERE ProductID > 14
CustomerName, city 필드 출력 10 < CustomerID <= 50 레코드출력 (Customers에 데이터 존재)
1 2 3 4 5 6
SELECT CustomerName, city FROM Customers WHERE CustomerID > 10 AND CustomerID <= 50
Orders에 CustomerId 가 30번대만 출력 (모든 필드 출력, and 사용)
1 2 3 4 5 6
SELECT * FROM Orders WHERE CustomerID >= 30 AND CustomerID < 40
ProductID가 짝수이면서, SupplierId 홀수인 ProductName과 ProductID 출력 (Products)
1 2 3 4 5 6
SELECT ProductName, ProductID FROM Products WHERE ProductID % 2 = 0 AND SupplierID % 2 = 1
Customers에 Address를 “주소”, city를 “도시”, PostalCode를 “우편번호” 로 출력, 우편번호 내림차순으로 정렬
1 2 3 4 5
SELECT Address AS `주소`, city AS `도시`, PostalCode AS `우편번호` FROM Customers ORDER BY PostalCode DESC
OrderDetails에 OrderDetailID를 기준으로 내림차순으로 정렬하고, 5개만 출력하기 (모든 컬럼)
1 2 3 4 5 6
SELECT * FROM OrderDetails ORDER BY OrderDetailID DESC LIMIT 5
Orders에 모든 컬럼을 OrderId를 기준으로 내림차순 정렬후, 3개를 건너띄고 7개만큼 출력하기
1 2 3 4 5 6
SELECT * FROM Orders ORDER BY OrderId DESC LIMIT 3, 7
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
OrderDetails의 모든 컬럼을 출력하되, ProductID가 1로 시작하는 모든 레코드 출력
1 2 3 4 5 6
SELECT * FROM OrderDetails WHERE ProductID LIKE '1%'
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) |
|---|---|---|
| 1 | FROM (+ JOIN) | “어떤 주머니(테이블)에서 데이터를 꺼낼 것인가?” (데이터 원천 확보) |
| 2 | WHERE | “필요 없는 행(Row)을 미리 버릴 조건은 무엇인가?” (개별 행 필터링) |
| 3 | GROUP BY | “데이터를 어떤 기준(그룹)으로 묶을 것인가?” (집계 단위 설정) |
| 4 | HAVING | “그룹화된 결과 중 버릴 그룹은 무엇인가?” (그룹 필터링) |
| 5 | SELECT | “최종적으로 어떤 컬럼을 보여줄 것인가?” (출력 데이터 결정) |
| 6 | ORDER BY | “어떤 순서로 정렬하여 보여줄 것인가?” (결과 정렬) |
| 7 | LIMIT / 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와 총 금액을 금액이 높은 순으로 추출하라.”
- FROM:
Orders테이블과Customers테이블을CustomerID로 JOIN 한다. - WHERE:
OrderDate가 ‘2023-01-01’부터 ‘2023-12-31’ 사이인 행만 남긴다. - GROUP BY: 고객별로 정보를 봐야 하므로
CustomerID로 그룹화한다. - HAVING: 그룹화된 데이터 중
COUNT(OrderID) >= 3이고SUM(Price) >= 500,000인 그룹만 남긴다. - SELECT:
CustomerID와SUM(Price)를 선택한다. - 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 ...
이 방식은 마치 프로그래밍에서 함수를 분리하는 것과 같아 디버깅과 논리 파악이 훨씬 수월해집니다.