본문 바로가기
WIKI 기술 지식 베이스

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)는 모두 윈도우 함수로 사용할 수 있어요.

Turso의 윈도우 함수는 기본 프레임 정의(RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)를 사용해요. 커스텀 프레임 지정(명시적인 경계를 가진 ROWS, RANGE, GROUPS)은 지원하지 않아요. `row_number()` 랭킹 함수와 `FILTER (WHERE ...)` 절은 지원하고, 그 외 랭킹·내비게이션 함수(`rank`, `dense_rank`, `lag`, `lead` 등)는 아직 사용할 수 없어요.

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

더 알아보기 (Learn more)

  • INSERT - 테이블에 행 추가하기
  • UPDATE - 기존 행 수정하기
  • DELETE - 행 삭제하기
  • CREATE VIEW - 쿼리를 뷰로 저장하기
  • Data Types - 스토리지 클래스와 타입 어피니티