SELECT
필터링, 조인, 그룹화, 정렬, 집합 연산을 활용해 하나 이상의 테이블에서 행을 조회하는 SELECT 문이에요. Turso에서 데이터를 읽는 기본 수단이라 문법 요소가 다양한데, 하나씩 짚어 보면 어렵지 않아요.
출처: 문서
본문
SELECT는 하나 이상의 테이블에서 행을 조회해요. Turso에서 데이터를 읽는 기본 방법이며, 필터링, 조인, 집계, 정렬, 서브쿼리, 집합 연산을 지원해요.
Syntax
[WITH cte_name AS (select_statement) [, ...]]
SELECT [DISTINCT] result_column [, ...]
[FROM table_or_subquery [join_clause ...]]
[WHERE expression]
[GROUP BY expression [, ...]]
[HAVING expression]
[WINDOW window_name AS (window_definition) [, ...]]
[compound_operator select_statement]
[ORDER BY ordering_term [, ...]]
[LIMIT expression [OFFSET expression]]
Parameters
| Parameter | Type | Description |
|---|---|---|
result_column |
expression, *, or table.* |
돌려줄 컬럼이나 표현식이에요. 모든 컬럼은 *로 나타내요. |
table_or_subquery |
identifier or subquery | 테이블 이름, 별칭이 붙은 테이블, 괄호로 감싼 SELECT예요. |
expression |
expression | 유효한 SQL 표현식이면 무엇이든 돼요. |
ordering_term |
expression + direction | 표현식 뒤에 선택적으로 ASC/DESC와 NULLS FIRST/LAST가 붙어요. |
compound_operator |
keyword | UNION, UNION ALL, INTERSECT, EXCEPT 중 하나예요. |
cte_name |
identifier | 공통 테이블 표현식(Common Table Expression)의 이름이에요. |
window_name |
identifier | 재사용 가능한 윈도우 정의의 이름이에요. |
Basic Queries
Selecting Columns
-- 모든 컬럼
SELECT * FROM employees;
-- 특정 컬럼만
SELECT name, department FROM employees;
-- 표현식과 별칭
SELECT name, salary * 12 AS annual_salary FROM employees;
Column and Table Aliases
AS를 사용해 컬럼이나 테이블에 별칭을 붙일 수 있어요. 컬럼 별칭에는 AS 키워드가 선택 사항이에요.
SELECT e.name, e.salary * 12 annual_pay
FROM employees AS e;
FROM Clause
FROM 절은 쿼리의 원본 테이블을 지정해요. 테이블 이름, 별칭이 붙은 테이블, 서브쿼리, 조인 표현식을 받아들여요.
-- 단일 테이블
SELECT * FROM employees;
-- 테이블 원천으로 쓰이는 서브쿼리
SELECT * FROM (SELECT name, salary FROM employees WHERE salary > 50000) AS high_earners;
WHERE Clause
조건에 따라 행을 걸러내요. 표현식이 참으로 평가되는 행만 결과에 포함돼요.
SELECT * FROM employees WHERE department = 'Engineering';
SELECT * FROM orders WHERE total > 100 AND status != 'cancelled';
Comparison Operators
| Operator | Description |
|---|---|
= |
같음 |
!= or <> |
같지 않음 |
< |
작음 |
> |
큼 |
<= |
작거나 같음 |
>= |
크거나 같음 |
IS |
같음 (NULL-safe) |
IS NOT |
같지 않음 (NULL-safe) |
IS DISTINCT FROM |
동일하지 않음 (NULL-safe, SQL 표준) |
IS NOT DISTINCT FROM |
동일함 (NULL-safe, SQL 표준) |
Logical Operators
AND, OR, NOT으로 조건을 조합해요.
SELECT * FROM products
WHERE (category = 'Electronics' OR category = 'Books')
AND price < 50
AND NOT discontinued;
Pattern Matching
-- LIKE: 대소문자를 구분하지 않는 패턴 매칭 (% = 임의의 문자들, _ = 한 문자)
SELECT * FROM employees WHERE name LIKE 'J%';
-- GLOB: 대소문자를 구분하는 패턴 매칭 (* = 임의의 문자들, ? = 한 문자)
SELECT * FROM files WHERE path GLOB '*.txt';
-- REGEXP: 정규 표현식 매칭
SELECT * FROM logs WHERE message REGEXP '^ERROR:';
Range and Membership Tests
-- BETWEEN (양쪽 끝을 포함해요)
SELECT * FROM orders WHERE total BETWEEN 100 AND 500;
-- 리스트와 함께 쓰는 IN
SELECT * FROM employees WHERE department IN ('Engineering', 'Design', 'Product');
-- 서브쿼리와 함께 쓰는 IN
SELECT * FROM employees WHERE department_id IN (SELECT id FROM departments WHERE active = 1);
NULL Tests
SELECT * FROM employees WHERE manager_id IS NULL;
SELECT * FROM employees WHERE phone IS NOT NULL;
CASE Expressions
SELECT name,
CASE
WHEN salary >= 100000 THEN 'Senior'
WHEN salary >= 60000 THEN 'Mid'
ELSE 'Junior'
END AS level
FROM employees;
-- 단순 CASE 형태
SELECT name,
CASE department
WHEN 'Engineering' THEN 'Eng'
WHEN 'Marketing' THEN 'Mkt'
ELSE 'Other'
END AS dept_code
FROM employees;
JOIN Clause
관련된 컬럼을 기준으로 둘 이상의 테이블에서 행을 결합해요.
Supported Join Types
| Join Type | Description |
|---|---|
INNER JOIN or JOIN |
두 테이블 모두에서 값이 일치하는 행만 돌려줘요. |
LEFT JOIN or LEFT OUTER JOIN |
왼쪽 테이블의 모든 행을 돌려주고, 맞는 짝이 없는 오른쪽 컬럼은 NULL이 돼요. |
FULL OUTER JOIN |
두 테이블의 모든 행을 돌려주고, 어느 쪽이든 짝이 없으면 NULL이 들어가요. |
NATURAL JOIN |
두 테이블에서 이름이 같은 모든 컬럼을 기준으로 조인해요. |
JOIN ... USING (column) |
두 테이블에 모두 존재하는 지정한 컬럼을 기준으로 조인해요. |
INNER JOIN
두 테이블 모두에서 조인 조건이 만족되는 행만 돌려줘요.
SELECT e.name, d.department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;
LEFT OUTER JOIN
왼쪽 테이블의 모든 행을 돌려줘요. 오른쪽 테이블에 맞는 행이 없으면 오른쪽 컬럼은 NULL이 돼요.
SELECT e.name, m.name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
FULL OUTER JOIN
두 테이블의 모든 행을 돌려줘요. 어느 한쪽의 행이 다른 쪽에서 짝을 찾지 못하면 빠진 쪽의 컬럼은 NULL이 돼요.
SELECT e.name, d.department_name
FROM employees e
FULL OUTER JOIN departments d ON e.department_id = d.id;
-- 부서가 없는 직원까지 모든 직원을 돌려줘요
-- 그리고 직원이 없는 부서까지 모든 부서도 돌려줘요
NATURAL JOIN
두 테이블에서 이름이 동일한 모든 컬럼을 기준으로 자동으로 조인해요. 공통 컬럼 이름 전체를 나열한 JOIN ... USING과 동일해요.
SELECT * FROM orders NATURAL JOIN customers;
-- orders와 customers가 공유하는 모든 컬럼 이름을 기준으로 조인해요
JOIN ... USING
양쪽 테이블에 반드시 존재해야 하는 지정한 컬럼을 기준으로 조인해요. 공유 컬럼은 결과에 한 번만 나타나요.
SELECT * FROM orders JOIN customers USING (customer_id);
Multi-Table Joins
SELECT o.id, c.name, p.product_name, oi.quantity
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.order_date > '2024-01-01';
GROUP BY and HAVING
GROUP BY
지정한 컬럼에서 값이 같은 행들을 묶어요. 보통 집계 함수와 함께 사용해요.
SELECT department, COUNT(*) AS employee_count, AVG(salary) AS avg_salary
FROM employees
GROUP BY department;
GROUP BY와 함께 쓸 수 있는 집계 함수들이에요.
| Function | Description |
|---|---|
COUNT(*) |
그룹 안의 행 수 |
COUNT(expression) |
NULL이 아닌 값의 개수 |
SUM(expression) |
NULL이 아닌 값의 합 |
AVG(expression) |
NULL이 아닌 값의 평균 |
MIN(expression) |
최솟값 |
MAX(expression) |
최댓값 |
TOTAL(expression) |
REAL로 더한 합 (빈 집합일 때 NULL 대신 0.0을 돌려줘요) |
GROUP_CONCAT(expression, separator) |
값들을 이어 붙인 결과 |
STRING_AGG(expression, separator) |
GROUP_CONCAT의 별칭 |
HAVING
집계가 끝난 그룹을 걸러내요. WHERE가 그룹핑 전에 개별 행을 걸러낸다면, HAVING은 그룹핑 후에 그룹을 걸러내요.
SELECT department, COUNT(*) AS headcount
FROM employees
GROUP BY department
HAVING COUNT(*) >= 5;
GROUP BY with Expressions
SELECT
CASE WHEN salary >= 80000 THEN 'High' ELSE 'Standard' END AS band,
COUNT(*) AS count
FROM employees
GROUP BY CASE WHEN salary >= 80000 THEN 'High' ELSE 'Standard' END;
DISTINCT
결과 집합에서 중복 행을 제거해요.
SELECT DISTINCT department FROM employees;
SELECT DISTINCT department, title FROM employees;
DISTINCT는 단일 컬럼이 아니라 결과 행 전체에 적용돼요. 두 행이 중복으로 취급되려면 모든 컬럼 값이 동일해야 해요.
ORDER BY
결과 집합을 정렬해요. ORDER BY가 없으면 행 순서는 정해져 있지 않아요.
SELECT * FROM employees ORDER BY salary DESC;
-- 여러 정렬 키
SELECT * FROM employees ORDER BY department ASC, salary DESC;
-- 컬럼 위치로 정렬
SELECT name, salary FROM employees ORDER BY 2 DESC;
-- 별칭으로 정렬
SELECT name, salary * 12 AS annual FROM employees ORDER BY annual DESC;
NULLS FIRST / NULLS LAST
정렬된 결과에서 NULL 값이 어디에 놓일지 제어해요.
-- NULL을 끝에 두기 (ASC의 기본은 NULLS LAST, DESC의 기본은 NULLS FIRST)
SELECT * FROM employees ORDER BY manager_id ASC NULLS LAST;
-- NULL을 앞에 두기
SELECT * FROM employees ORDER BY manager_id DESC NULLS FIRST;
| Direction | Default NULL Placement |
|---|---|
| ASC | NULLS LAST |
| DESC | NULLS FIRST |
LIMIT and OFFSET
돌려주는 행 수를 제한하고, 선택적으로 행을 일부 건너뛰게 해 줘요.
-- 최대 10행만 돌려요
SELECT * FROM employees ORDER BY salary DESC LIMIT 10;
-- 20행을 건너뛴 다음 10행을 돌려요
SELECT * FROM employees ORDER BY salary DESC LIMIT 10 OFFSET 20;
LIMIT과 OFFSET에는 항상 ORDER BY를 함께 쓰세요. ORDER BY가 없으면 건너뛰거나 돌려받는 행 집합이 임의로 정해져요.
Subqueries
서브쿼리는 다른 쿼리 안에 중첩된 SELECT 문장이에요.
Scalar Subqueries
단일 값을 돌려줘요. 표현식이 기대되는 어디에서든 사용할 수 있어요.
SELECT name, salary,
salary - (SELECT AVG(salary) FROM employees) AS diff_from_avg
FROM employees;
Subqueries with IN
값이 서브쿼리가 돌려주는 어떤 행과 일치하는지 검사해요.
SELECT * FROM products
WHERE category_id IN (SELECT id FROM categories WHERE active = 1);
SELECT * FROM products
WHERE category_id NOT IN (SELECT id FROM categories WHERE discontinued = 1);
Subqueries with EXISTS
서브쿼리가 최소 한 행이라도 돌려주는지 검사해요. 실제 값은 무시돼요.
SELECT * FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.total > 1000
);
Subqueries in FROM
FROM 절의 서브쿼리는 파생 테이블(derived table)로 동작하며 반드시 별칭이 필요해요.
SELECT dept, avg_salary
FROM (
SELECT department AS dept, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
) AS dept_stats
WHERE avg_salary > 70000;
Common Table Expressions (CTE)
WITH 절은 쿼리가 실행되는 동안 존재하는 이름 붙은 임시 결과 집합을 하나 이상 정의해요.
WITH regional_sales AS (
SELECT region, SUM(amount) AS total_sales
FROM orders
GROUP BY region
)
SELECT region, total_sales
FROM regional_sales
WHERE total_sales > 100000
ORDER BY total_sales DESC;
Multiple CTEs
WITH
active_customers AS (
SELECT * FROM customers WHERE active = 1
),
recent_orders AS (
SELECT * FROM orders WHERE order_date > '2024-01-01'
)
SELECT ac.name, COUNT(ro.id) AS order_count
FROM active_customers ac
JOIN recent_orders ro ON ac.id = ro.customer_id
GROUP BY ac.name;
Turso의 CTE는 SELECT 전용이에요. RECURSIVE CTE와 MATERIALIZED/NOT MATERIALIZED 힌트는 지원하지 않아요.
Window Functions
윈도우 함수는 현재 행과 관련된 행 집합 전체에 걸쳐 계산을 수행하되, 행들을 하나의 출력 행으로 뭉개지 않아요.
SELECT name, department, salary,
SUM(salary) OVER (PARTITION BY department ORDER BY salary) AS running_total
FROM employees;
Named Windows
WINDOW 절을 사용하면 재사용 가능한 윈도우 정의를 만들 수 있어요.
SELECT name, department, salary,
SUM(salary) OVER w AS dept_running_total,
COUNT(*) OVER w AS dept_running_count
FROM employees
WINDOW w AS (PARTITION BY department ORDER BY salary)
ORDER BY department, salary;
Window Clause Syntax
function_name() OVER (
[PARTITION BY expression [, ...]]
[ORDER BY expression [ASC | DESC] [, ...]]
)
| Component | Description |
|---|---|
PARTITION BY |
행들을 그룹(파티션)으로 나눠요. 함수는 파티션마다 초기화돼요. |
ORDER BY |
각 파티션 안에서 행의 순서를 정의해요. |
표준 집계 함수(SUM, AVG, COUNT, MIN, MAX, TOTAL, GROUP_CONCAT)는 모두 윈도우 함수로 사용할 수 있어요.
Set Operations
둘 이상의 SELECT 문장 결과를 결합해요.
| Operator | Description |
|---|---|
UNION |
결과를 합치고 중복을 제거해요. |
UNION ALL |
결과를 합치고 중복을 유지해요. |
INTERSECT |
두 결과 집합 모두에 존재하는 행만 돌려줘요. |
EXCEPT |
첫 번째 결과 집합에는 있고 두 번째에는 없는 행을 돌려줘요. |
모든 집합 연산은 각 SELECT의 컬럼 수가 같고 타입이 호환되어야 해요.
-- 직원이기도 한 고객
SELECT name FROM customers
INTERSECT
SELECT name FROM employees;
-- 두 테이블의 모든 사람, 중복 없음
SELECT name, email FROM customers
UNION
SELECT name, email FROM employees;
-- 직원이 아닌 고객
SELECT name FROM customers
EXCEPT
SELECT name FROM employees;
UNION ALL
중복이 허용된다면 UNION ALL이 더 빨라요. 중복 제거 단계를 건너뛰기 때문이에요.
SELECT id, 'order' AS source FROM orders
UNION ALL
SELECT id, 'return' AS source FROM returns;
Set Operations with ORDER BY
ORDER BY는 결합된 결과 집합 전체에 적용되며 마지막 SELECT 뒤에 나와야 해요.
SELECT name FROM customers
UNION
SELECT name FROM employees
ORDER BY name;
Examples
Paginated Report with Aggregation
SELECT
d.department_name,
COUNT(e.id) AS headcount,
ROUND(AVG(e.salary), 2) AS avg_salary,
MIN(e.hire_date) AS earliest_hire
FROM departments d
LEFT JOIN employees e ON d.id = e.department_id
GROUP BY d.department_name
HAVING COUNT(e.id) > 0
ORDER BY avg_salary DESC
LIMIT 10 OFFSET 0;
CTE with Filtered Join
WITH high_value_orders AS (
SELECT customer_id, COUNT(*) AS order_count, SUM(total) AS total_spent
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id
HAVING SUM(total) > 5000
)
SELECT c.name, h.order_count, h.total_spent
FROM customers c
JOIN high_value_orders h ON c.id = h.customer_id
ORDER BY h.total_spent DESC;
Subquery with EXISTS and LEFT JOIN
SELECT p.product_name, p.price, c.category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
)
ORDER BY p.price DESC
LIMIT 20;
See Also
- INSERT for adding rows to a table
- UPDATE for modifying existing rows
- DELETE for removing rows
- CREATE VIEW for saving a query as a view
- Data Types for storage classes and type affinity
더 알아보기 (Learn more)
- INSERT - 테이블에 행 추가하기
- UPDATE - 기존 행 수정하기
- DELETE - 행 삭제하기
- CREATE VIEW - 쿼리를 뷰로 저장하기
- Data Types - 스토리지 클래스와 타입 어피니티