SELECT

SELECT

SELECT 문은 데이터베이스에서 데이터를 조회하는 데 사용됩니다. 조회 결과는 각 행이 동일한 개수의 열을 가진 0개 이상의 데이터 행으로 반환됩니다.

출처: 문서

본문

1. 개요

select-stmt:

WITH RECURSIVE common-table-expression , SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , , compound-operator select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr

common-table-expression:

table-name ( column-name ) AS NOT MATERIALIZED ( select-stmt ) ,

compound-operator:

UNION UNION INTERSECT EXCEPT ALL

expr:

literal-value bind-parameter schema-name . table-name . column-name unary-operator expr expr binary-operator expr function-name ( function-arguments ) filter-clause over-clause ( expr ) , CAST ( expr AS type-name ) expr COLLATE collation-name expr NOT LIKE GLOB REGEXP MATCH expr expr ESCAPE expr expr ISNULL NOTNULL NOT NULL expr IS NOT DISTINCT FROM expr expr NOT BETWEEN expr AND expr expr NOT IN ( select-stmt ) expr , schema-name . table-function ( expr ) table-name , NOT EXISTS ( select-stmt ) CASE expr WHEN expr THEN expr ELSE expr END raise-function

filter-clause:

FILTER ( WHERE expr )

function-arguments:

DISTINCT expr , * ORDER BY ordering-term ,

literal-value:

CURRENT_TIMESTAMP numeric-literal string-literal blob-literal NULL TRUE FALSE CURRENT_TIME CURRENT_DATE

over-clause:

OVER window-name ( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS

raise-function:

RAISE ( ROLLBACK , expr ) IGNORE ABORT FAIL

type-name:

name ( signed-number , signed-number ) ( signed-number )

signed-number:

+ numeric-literal -

join-clause:

table-or-subquery join-operator table-or-subquery join-constraint

join-constraint:

USING ( column-name ) , ON expr

join-operator:

NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS

ordering-term:

expr COLLATE collation-name DESC ASC NULLS FIRST NULLS LAST

result-column:

expr AS column-alias * table-name . *

table-or-subquery:

schema-name . table-name AS table-alias INDEXED BY index-name NOT INDEXED table-function-name ( expr ) , AS table-alias ( select-stmt ) ( table-or-subquery ) , join-clause

window-defn:

( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS

SELECT 문은 데이터베이스를 조회하는 데 사용됩니다. SELECT 문의 결과는 각 행이 고정된 수의 열을 가지는 0개 이상의 데이터 행입니다. SELECT 문은 데이터베이스를 변경하지 않습니다.

위의 "select-stmt" 구문 다이어그램은 SELECT 문 구문을 단일 다이어그램에 가능한 한 많이 보여 주려고 합니다. 일부 독자들은 그것이 도움이 된다고 느끼기 때문입니다. 다음 "factored-select-stmt"는 동일한 구문을 표현하는 대안적인 구문 다이어그램으로, 구문을 더 작은 덩어리로 나누어 보여 줍니다.

factored-select-stmt:

WITH RECURSIVE common-table-expression , select-core ORDER BY LIMIT expr compound-operator ordering-term , OFFSET expr , expr

common-table-expression:

table-name ( column-name ) AS NOT MATERIALIZED ( select-stmt ) ,

select-stmt:

WITH RECURSIVE common-table-expression , SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , , compound-operator select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr

join-clause:

table-or-subquery join-operator table-or-subquery join-constraint

join-constraint:

USING ( column-name ) , ON expr

join-operator:

NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS

result-column:

expr AS column-alias * table-name . *

table-or-subquery:

schema-name . table-name AS table-alias INDEXED BY index-name NOT INDEXED table-function-name ( expr ) , AS table-alias ( select-stmt ) ( table-or-subquery ) , join-clause

window-defn:

( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS

compound-operator:

UNION UNION INTERSECT EXCEPT ALL

expr:

literal-value bind-parameter schema-name . table-name . column-name unary-operator expr expr binary-operator expr function-name ( function-arguments ) filter-clause over-clause ( expr ) , CAST ( expr AS type-name ) expr COLLATE collation-name expr NOT LIKE GLOB REGEXP MATCH expr expr ESCAPE expr expr ISNULL NOTNULL NOT NULL expr IS NOT DISTINCT FROM expr expr NOT BETWEEN expr AND expr expr NOT IN ( select-stmt ) expr , schema-name . table-function ( expr ) table-name , NOT EXISTS ( select-stmt ) CASE expr WHEN expr THEN expr ELSE expr END raise-function

filter-clause:

FILTER ( WHERE expr )

function-arguments:

DISTINCT expr , * ORDER BY ordering-term ,

literal-value:

CURRENT_TIMESTAMP numeric-literal string-literal blob-literal NULL TRUE FALSE CURRENT_TIME CURRENT_DATE

over-clause:

OVER window-name ( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS

raise-function:

RAISE ( ROLLBACK , expr ) IGNORE ABORT FAIL

select-stmt:

WITH RECURSIVE common-table-expression , SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , , compound-operator select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr

join-clause:

table-or-subquery join-operator table-or-subquery join-constraint

join-constraint:

USING ( column-name ) , ON expr

join-operator:

NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS

result-column:

expr AS column-alias * table-name . *

table-or-subquery:

schema-name . table-name AS table-alias INDEXED BY index-name NOT INDEXED table-function-name ( expr ) , AS table-alias ( select-stmt ) ( table-or-subquery ) , join-clause

window-defn:

( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS

type-name:

name ( signed-number , signed-number ) ( signed-number )

signed-number:

+ numeric-literal -

ordering-term:

expr COLLATE collation-name DESC ASC NULLS FIRST NULLS LAST

select-core:

SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , ,

join-clause:

table-or-subquery join-operator table-or-subquery join-constraint

join-constraint:

USING ( column-name ) , ON expr

join-operator:

NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS

result-column:

expr AS column-alias * table-name . *

table-or-subquery:

schema-name . table-name AS table-alias INDEXED BY index-name NOT INDEXED table-function-name ( expr ) , AS table-alias ( select-stmt ) ( table-or-subquery ) , join-clause

select-stmt:

WITH RECURSIVE common-table-expression , SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , , compound-operator select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr

window-defn:

( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS

구문 다이어그램에는 실제로는 허용되지 않는 경로가 있음에 유의하세요. 몇 가지 예는 다음과 같습니다.

  • VALUES 절은 WITH 절을 사용하는 복합 SELECT의 첫 번째 요소가 될 수 있지만, VALUES 절만으로 구성된 단순 SELECT 앞에는 WITH 절이 올 수 없습니다.

  • WITH 절은 복합 SELECT의 첫 번째 SELECT에 나타나야 합니다. compound-operator 뒤에는 올 수 없습니다.

이러한 제한 및 그 밖의 유사한 구문 제한은 본문에서 설명합니다.

SELECT 문은 SQL 언어에서 가장 복잡한 명령입니다. 설명을 따라가기 쉽도록, 아래의 일부 단락에서는 SELECT 문이 반환하는 데이터가 일련의 단계로 결정되는 방식을 설명합니다. 이는 순전히 예시적인 것임을 명심하는 것이 중요합니다. 실제로 SQLite나 다른 어떤 SQL 엔진도 이 특정 과정이나 다른 특정 과정을 따르도록 요구되지 않습니다.

2. 단순 SELECT 처리

SELECT 문의 핵심은 아래 select-coresimple-select-stmt 문법 다이어그램에 나오는 ‘단순 SELECT(simple SELECT)’예요. 실제로 대부분의 SELECT 문은 단순 SELECT 문이에요.

simple-select-stmt:

WITH RECURSIVE common-table-expression , select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr

common-table-expression:

table-name ( column-name ) AS NOT MATERIALIZED ( select-stmt ) ,

select-stmt:

WITH RECURSIVE common-table-expression , SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , , compound-operator select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr

compound-operator:

UNION UNION INTERSECT EXCEPT ALL

join-clause:

table-or-subquery join-operator table-or-subquery join-constraint

join-constraint:

USING ( column-name ) , ON expr

join-operator:

NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS

result-column:

expr AS column-alias * table-name . *

table-or-subquery:

schema-name . table-name AS table-alias INDEXED BY index-name NOT INDEXED table-function-name ( expr ) , AS table-alias ( select-stmt ) ( table-or-subquery ) , join-clause

window-defn:

( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS

expr:

literal-value bind-parameter schema-name . table-name . column-name unary-operator expr expr binary-operator expr function-name ( function-arguments ) filter-clause over-clause ( expr ) , CAST ( expr AS type-name ) expr COLLATE collation-name expr NOT LIKE GLOB REGEXP MATCH expr expr ESCAPE expr expr ISNULL NOTNULL NOT NULL expr IS NOT DISTINCT FROM expr expr NOT BETWEEN expr AND expr expr NOT IN ( select-stmt ) expr , schema-name . table-function ( expr ) table-name , NOT EXISTS ( select-stmt ) CASE expr WHEN expr THEN expr ELSE expr END raise-function

filter-clause:

FILTER ( WHERE expr )

function-arguments:

DISTINCT expr , * ORDER BY ordering-term ,

literal-value:

CURRENT_TIMESTAMP numeric-literal string-literal blob-literal NULL TRUE FALSE CURRENT_TIME CURRENT_DATE

over-clause:

OVER window-name ( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS

raise-function:

RAISE ( ROLLBACK , expr ) IGNORE ABORT FAIL

select-stmt:

WITH RECURSIVE common-table-expression , SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , , compound-operator select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr

compound-operator:

UNION UNION INTERSECT EXCEPT ALL

join-clause:

table-or-subquery join-operator table-or-subquery join-constraint

join-constraint:

USING ( column-name ) , ON expr

join-operator:

NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS

result-column:

expr AS column-alias * table-name . *

table-or-subquery:

schema-name . table-name AS table-alias INDEXED BY index-name NOT INDEXED table-function-name ( expr ) , AS table-alias ( select-stmt ) ( table-or-subquery ) , join-clause

window-defn:

( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )

frame-spec:

`GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EX

2.2. CROSS JOIN의 특별 처리

SQLite에서 "INNER JOIN", "JOIN", "," 조인 연산자 사이에는 차이가 없어요. 이들은 SQLite에서 완전히 서로 바꿔 쓸 수 있어요.
"CROSS JOIN" 조인 연산자는 "INNER JOIN", "JOIN", "," 연산자와 같은 결과를 만들어 내지만, 조인에서 테이블 순서를 쿼리 최적화 프로그램이 재배치하지 못하게 한다는 점에서 쿼리 최적화 프로그램이 다르게 처리해요. 애플리케이션 프로그래머는 CROSS JOIN 연산자를 사용해서 SELECT 문을 구현하기 위해 선택되는 알고리즘에 직접 영향을 줄 수 있어요. 쿼리 최적화 프로그램을 수동으로 제어하려는 특정 상황을 제외하고는 CROSS JOIN 사용을 피하세요. 애플리케이션 개발 초기에 CROSS JOIN을 사용하는 것은 성급한 최적화이므로 피하세요. CROSS JOIN의 특별 처리는 SQLite 고유 기능이며 표준 SQL의 일부가 아니에요.

2.3. WHERE 절 필터링

WHERE 절이 지정되면, WHERE 식은 입력 데이터의 각 행에 대해 불리언 식으로 평가돼요. 계속 진행하기 전에 WHERE 절 식이 true로 평가되는 행만 데이터셋에 포함돼요. WHERE 절이 false 또는 NULL로 평가되면 그 행은 결과에서 제외돼요.

JOIN, INNER JOIN, CROSS JOIN의 경우 WHERE 절의 제약 조건 식과 ON 절의 제약 조건 식 사이에는 차이가 없어요. 그러나 LEFT, RIGHT, FULL OUTER JOIN의 경우 그 차이는 매우 중요해요. 외부 조인에서는 다른 피연산자의 일치하지 않는 행에 대한 추가 NULL 행이 ON 절 처리 후, WHERE 절 처리 전에 추가돼요. 따라서 ON 절에 "left.x=right.y" 형태의 제약 조건이 있으면 추가된 all-NULL 행도 통과하게 돼요. 하지만 같은 제약 조건이 WHERE 절에 있으면 "right.y" 또는 "left.x" 중 하나가 NULL일 때 "left.x=right.y" 식이 true가 되는 것을 막아서, 그 행이 출력에서 제외돼요.

2.4. 결과 행 집합 생성

FROM 절의 입력 데이터가 WHERE 절 식(있는 경우)으로 필터링되면, 단순 SELECT의 결과 행 집합이 계산돼요. 정확한 계산 방식은 단순 SELECT가 집계(aggregate) 쿼리인지 비집계(non-aggregate) 쿼리인지, 그리고 GROUP BY 절이 지정되었는지에 따라 달라져요.

SELECT와 FROM 키워드 사이의 식 목록을 결과 식 목록(result expression list)이라고 해요. 결과 식이 특별한 식 "*"이면 입력 데이터의 모든 열이 그 하나의 식 자리에 대체돼요. 식이 FROM 절에 있는 테이블이나 하위 쿼리의 별칭 뒤에 ".*"가 붙은 형태이면, 그 이름이 지정된 테이블이나 하위 쿼리의 모든 열이 해당 단일 식 자리에 대체돼요. "*" 또는 "alias.*" 식을 결과 식 목록 이외의 문맥에서 사용하면 오류예요. FROM 절이 없는 단순 SELECT 쿼리에서 "*" 또는 "alias.*" 식을 사용하는 것도 오류예요.

단순 SELECT 문이 반환하는 행의 열 수는 * 및 alias.* 식이 대체된 후의 결과 식 목록에 있는 식의 수와 같아요. 각 결과 행은 결과 식 목록의 식들을 입력 데이터의 단일 행 기준으로, 또는 집계 쿼리의 경우 행 그룹 기준으로 평가하여 계산돼요.

  • SELECT 문이 비집계 쿼리라면, 결과 식 목록의 각 식은 WHERE 절로 필터링된 데이터셋의 각 행에 대해 평가돼요.

  • SELECT 문이 GROUP BY 절이 없는 집계 쿼리라면, 결과 집합의 각 집계 식은 전체 데이터셋에 대해 한 번 평가돼요. 결과 집합의 각 비집계 식은 데이터셋에서 임의로 선택된 하나의 행에 대해 한 번 평가돼요. 각 비집계 식에 대해 동일한 임의 선택 행이 사용돼요. 또는 데이터셋에 행이 없으면 각 비집계 식은 전부 NULL 값으로 구성된 행을 대상으로 평가돼요.

    결과 집합의 집계 및 비집계 식을 평가하여 만들어진 단일 결과 집합 데이터 행은 GROUP BY 절이 없는 집계 쿼리의 결과를 구성해요. GROUP BY 절이 없는 집계 쿼리는 입력 데이터 행이 0개여도 항상 정확히 한 행의 데이터를 반환해요.

  • SELECT 문이 GROUP BY 절이 있는 집계 쿼리라면, GROUP BY 절의 일부로 지정된 각 식은 아래 ORDER BY 식에 대해 설명된 처리 규칙에 따라 데이터셋의 각 행에 대해 평가돼요. 그런 다음 각 행은 그 결과에 따라 "그룹"에 배정돼요. GROUP BY 식의 평가 결과가 같은 행은 같은 그룹에 배정돼요. 행을 그룹화할 때 NULL 값은 서로 같은 것으로 간주돼요. GROUP BY 절에서 식을 평가할 때 텍스트 값을 비교할 콜레이션 순서를 선택하는 일반적인 규칙이 적용돼요. GROUP BY 절의 식은 결과에 나타나는 식일 필요는 없어요. GROUP BY 절의 식은 집계 식일 수 없어요.

    HAVING 절이 지정되면, 각 행 그룹에 대해 불리언 식으로 한 번 평가돼요. HAVING 절의 평가 결과가 false이면 그 그룹은 버려져요. HAVING 절이 집계 식이면 그룹의 모든 행에 걸쳐 평가돼요. HAVING 절이 비집계 식이면 그룹에서 임의로 선택된 하나의 행을 기준으로 평가돼요. HAVING 식은 결과에 없는 값(집계 함수도 포함)을 참조할 수 있어요.

    그런 다음 결과 집합의 각 식은 각 행 그룹에 대해 한 번씩 평가돼요. 식이 집계 식이면 그룹의 모든 행에 걸쳐 평가돼요. 그렇지 않으면 그룹 내에서 임의로 선택된 단일 행을 대상으로 평가돼요. 결과 집합에 비집계 식이 둘 이상이면 그러한 모든 식은 같은 행을 대상으로 평가돼요.

    입력 데이터셋 행의 각 그룹은 결과 행 집합에 하나의 행을 기여해요. DISTINCT 키워드와 관련된 필터링이 적용된다는 전제 아래, GROUP BY 절이 있는 집계 쿼리가 반환하는 행 수는 필터링된 입력 데이터셋에 GROUP BY 절과 HAVING 절을 적용하여 만들어지는 행 그룹의 수와 같아요.

2.5. 집계 쿼리에서의 베어 열

일반적인 경우 집계 쿼리의 모든 열 이름은 집계 함수의 인자이거나 GROUP BY 절에 나타나요. 집계 함수 안에 있지 않고 (GROUP BY 절이 있다면) 그 절에도 나타나지 않는 열 이름을 포함하는 결과 열을 "베어(bare)" 열이라고 해요.

예:

SELECT a, b, sum(c) FROM tab1 GROUP BY a;

위 쿼리에서 "a" 열은 GROUP BY 절의 일부이므로 출력의 각 행은 "a"에 대한 서로 다른 값 중 하나를 포함해요. "c" 열은 sum() 집계 함수 안에 있으므로 그 출력 열은 "a" 값이 같은 행들에 있는 모든 "c" 값의 합이에요. 하지만 베어 열 "b"의 결과는 무엇일까요? 답은 "b" 결과가 집계를 구성하는 입력 행 중 하나의 "b" 값이 된다는 것이에요.¹ 문제는 "b"를 계산하는 데 어떤 입력 행이 사용되는지 보통 알 수 없어서, 많은 경우 "b"의 값이 정의되지 않는다는 것이에요.

집계 함수가 min() 또는 max()인 경우에는 특별한 처리가 일어나요. 예:

SELECT a, b, max(c) FROM tab1 GROUP BY a;

쿼리에 min() 또는 max() 집계가 정확히 하나 있으면, 결과 집합의 모든 베어 열은 최솟값 또는 최댓값을 포함하는 입력 행에서 값을 가져와요. 따라서 위 쿼리에서 출력의 "b" 열 값은 "c" 값이 가장 큰 입력 행의 "b" 열 값이에요.

min() 및 max()의 이러한 특별 동작에는 제한 사항이 있어요.

  • 동일한 최솟값 또는 최댓값이 두 개 이상의 행에서 발생하면, 베어 값은 그중 아무 행에서나 선택될 수 있어요. 선택은 임의적이에요. 베어 값이 어느 행에서 선택될지 예측할 방법이 없어요. 같은 쿼리 내에서도 베어 열에 따라 선택이 달라질 수 있어요.

  • 쿼리에 min() 또는 max() 집계가 둘 이상 있으면, 베어 열 값은 그중 하나의 집계가 최솟값 또는 최댓값을 갖는 행 중 하나에서 가져와요. 어떤 min() 또는 max() 집계가 베어 열 값 선택을 결정할지는 임의적이에요. 같은 쿼리 내에서도 베어 열에 따라 선택이 달라질 수 있어요.

  • min() 또는 max() 집계에 대한 이 특별 처리는 해당 집계의 내장 구현에서만 작동해요. 애플리케이션이 내장 min() 또는 max() 집계를 애플리케이션 정의 대체 구현으로 재정의하면, 베어 열에 선택되는 값은 임의의 행에서 가져와요.

대부분의 다른 SQL 데이터베이스 엔진은 베어 열을 허용하지 않아요. 쿼리에 베어 열을 포함하면 다른 데이터베이스 엔진은 보통 오류를 발생시켜요. 쿼리에 베어 열을 포함할 수 있는 기능은 SQLite 고유의 확장 기능이에요. 이것은 버그가 아니라 기능으로 간주돼요. 자세한 내용은 SQLite 포럼 스레드 7481d2a6df8980ff에서 논의를 참조하세요.

2.5.1. 참고

  • 집계가 하나 이상의 입력 행으로 구성된다면, 베어 열은 집계 입력 중 임의의 행 값을 가져요. 선택되는 특정 행은 호출 때마다 바뀔 수 있어요. 집계가 비어 있으면(WHERE 절과 일치하는 행이 없어서 집계에 입력 행이 없는 경우), 베어 열은 FROM 절의 테이블 어디에도 없는 값을 포함해 임의의 값을 가질 수 있어요.

2.6. 중복 행 제거(DISTINCT 처리)

ALL 또는 DISTINCT 키워드 중 하나는 단순 SELECT 문에서 SELECT 키워드 뒤에 올 수 있어요. 단순 SELECT가 SELECT ALL이면 SELECT는 결과 행 전체 집합을 반환해요. ALL이나 DISTINCT가 모두 없으면 ALL이 지정된 것처럼 동작해요. 단순 SELECT가 SELECT DISTINCT이면 반환되기 전에 결과 행 집합에서 중복 행이 제거돼요. 중복 행을 판별할 때 값은 IS DISTINCT FROM 연산자를 사용해 비교해요. 따라서 두 NULL 값은 같다고 간주돼요. 정수와 부동 소수점 숫자는 같은 수량을 나타내면 서로 동일해요. 텍스트 값은 적절한 정렬 순서(collation sequence)로 비교해요. 텍스트 값을 비교할 정렬 순서를 선택하는 일반적인 규칙이 적용돼요. DISTINCT 비교에는 BLOB 친화성이 사용되므로 유형 강제 변환은 발생하지 않아요.

3. 복합 SELECT 문

두 개 이상의 단순 SELECT 문은 UNION, UNION ALL, INTERSECT 또는 EXCEPT 연산자를 사용해 서로 연결하여 복합 SELECT를 형성할 수 있어요. 다음 다이어그램에 나와 있어요:

compound-select-stmt:

`WITH RECURSIVE common-table-expression , select-core ORDER BY LIMIT expr UNION UNION ALL select-core INTERSECT EXCEPT ordering-term , OFFSET expr , expr`

common-table-expression:

`table-name ( column-name ) AS NOT MATERIALIZED ( select-stmt ) ,`

select-stmt:

`WITH RECURSIVE common-table-expression , SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , , compound-operator select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr`

compound-operator:

`UNION UNION INTERSECT EXCEPT ALL`

join-clause:

`table-or-subquery join-operator table-or-subquery join-constraint`

join-constraint:

`USING ( column-name ) , ON expr`

join-operator:

`NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS`

result-column:

`expr AS column-alias * table-name . *`

table-or-subquery:

`schema-name . table-name AS table-alias INDEXED BY index-name NOT INDEXED table-function-name ( expr ) , AS table-alias ( select-stmt ) ( table-or-subquery ) , join-clause`

window-defn:

`( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )`

frame-spec:

`GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS`

expr:

`literal-value bind-parameter schema-name . table-name . column-name unary-operator expr expr binary-operator expr function-name ( function-arguments ) filter-clause over-clause ( expr ) , CAST ( expr AS type-name ) expr COLLATE collation-name expr NOT LIKE GLOB REGEXP MATCH expr expr ESCAPE expr expr ISNULL NOTNULL NOT NULL expr IS NOT DISTINCT FROM expr expr NOT BETWEEN expr AND expr expr NOT IN ( select-stmt ) expr , schema-name . table-function ( expr ) table-name , NOT EXISTS ( select-stmt ) CASE expr WHEN expr THEN expr ELSE expr END raise-function`

filter-clause:

`FILTER ( WHERE expr )`

function-arguments:

`DISTINCT expr , * ORDER BY ordering-term ,`

literal-value:

`CURRENT_TIMESTAMP numeric-literal string-literal blob-literal NULL TRUE FALSE CURRENT_TIME CURRENT_DATE`

over-clause:

`OVER window-name ( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )`

frame-spec:

`GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS`

raise-function:

`RAISE ( ROLLBACK , expr ) IGNORE ABORT FAIL`

select-stmt:

`WITH RECURSIVE common-table-expression , SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , , compound-operator select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr`

compound-operator:

`UNION UNION INTERSECT EXCEPT ALL`

join-clause:

`table-or-subquery join-operator table-or-subquery join-constraint`

join-constraint:

`USING ( column-name ) , ON expr`

join-operator:

`NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS`

result-column:

`expr AS column-alias * table-name . *`

table-or-subquery:

`schema-name . table-name AS table-alias INDEXED BY index-name NOT INDEXED table-function-name ( expr ) , AS table-alias ( select-stmt ) ( table-or-subquery ) , join-clause`

window-defn:

`( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )`

frame-spec:

`GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS`

type-name:

`name ( signed-number , signed-number ) ( signed-number )`

signed-number:

`+ numeric-literal -`

ordering-term:

`expr COLLATE collation-name DESC ASC NULLS FIRST NULLS LAST`

select-core:

`SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , ,`

join-clause:

`table-or-subquery join-operator table-or-subquery join-constraint`

join-constraint:

`USING ( column-name ) , ON expr`

join-operator:

`NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS`

result-column:

`expr AS column-alias * table-name . *`

table-or-subquery:

`schema-name . table-name AS table-alias INDEXED BY index-name NOT INDEXED table-function-name ( expr ) , AS table-alias ( select-stmt ) ( table-or-subquery ) , join-clause`

select-stmt:

`WITH RECURSIVE common-table-expression , SELECT DISTINCT result-column , ALL FROM table-or-subquery join-clause , WHERE expr GROUP BY expr HAVING expr , WINDOW window-name AS window-defn , VALUES ( expr ) , , compound-operator select-core ORDER BY LIMIT expr ordering-term , OFFSET expr , expr`

compound-operator:

`UNION UNION INTERSECT EXCEPT ALL`

window-defn:

`( base-window-name PARTITION BY expr , ORDER BY ordering-term , frame-spec )`

frame-spec:

`GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING RANGE ROWS UNBOUNDED PRECEDING expr PRECEDING CURRENT ROW expr PRECEDING CURRENT ROW expr FOLLOWING expr PRECEDING CURRENT ROW expr FOLLOWING EXCLUDE CURRENT ROW EXCLUDE GROUP EXCLUDE TIES EXCLUDE NO OTHERS`

복합 SELECT에서는 모든 구성 SELECT가 동일한 개수의 결과 열을 반환해야 해요. 복합 SELECT의 구성 요소는 단순 SELECT 문이어야 하므로 ORDER BY 또는 LIMIT 절을 포함할 수 없어요. ORDER BY와 LIMIT 절은 복합 SELECT 전체의 끝에서만 사용할 수 있으며, 그 경우에도 복합 SELECT의 마지막 요소가 VALUES 절이 아니어야 해요.

UNION ALL 연산자로 만든 복합 SELECT는 UNION ALL 연산자 왼쪽에 있는 SELECT의 모든 행과 오른쪽에 있는 SELECT의 모든 행을 반환해요. UNION 연산자는 최종 결과 집합에서 중복 행이 제거된다는 점만 제외하면 UNION ALL과 동일하게 동작해요. INTERSECT 연산자는 왼쪽과 오른쪽 SELECT 결과의 교집합을 반환해요. EXCEPT 연산자는 왼쪽 SELECT가 반환한 행 중 오른쪽 SELECT도 반환하는 행을 제외한 부분집합을 반환해요. INTERSECT 및 EXCEPT 연산자의 결과에서는 결과 집합이 반환되기 전에 중복 행이 제거돼요.

복합 SELECT 연산자의 결과에서 중복 행을 결정할 때 NULL 값은 다른 NULL 값과 동일한 것으로 간주되며 모든 비-NULL 값과는 구별되는 것으로 간주돼요. 두 텍스트 값을 비교할 때 사용되는 정렬 순서는 왼쪽 및 오른쪽 SELECT 문의 열이 같음(=) 연산자의 왼쪽 및 오른쪽 피연산자인 것처럼 결정돼요. 단, 접미사 COLLATE 연산자로 지정된 정렬 순서에 더 높은 우선순위가 부여되지는 않아요. 복합 SELECT의 일부로 행을 비교할 때 값에는 친화성 변환이 적용되지 않아요.

세 개 이상의 단순 SELECT가 복합 SELECT로 연결될 때는 왼쪽에서 오른쪽으로 그룹화돼요. 즉, "A", "B", "C"가 모두 단순 SELECT 문이라면 (A op B op C)는 ((A op B) op C)로 처리돼요.

4. ORDER BY 절

둘 이상의 행을 반환하는 SELECT 문에 ORDER BY 절이 없으면 행이 반환되는 순서는 정의되지 않아요. 또는 SELECT 문에 ORDER BY 절이 있으면 ORDER BY에 연결된 표현식 목록이 사용자에게 행이 반환되는 순서를 결정해요.

복합 SELECT 문에서는 마지막(가장 오른쪽) 단순 SELECT만 ORDER BY 절을 가질 수 있어요. 그 ORDER BY 절은 복합 SELECT의 모든 요소에 적용돼요. 복합 SELECT의 가장 오른쪽 요소가 VALUES 절이면 해당 문에는 ORDER BY 절을 사용할 수 없어요.

행은 먼저 ORDER BY 목록에서 가장 왼쪽 표현식을 평가한 결과를 기준으로 정렬되고, 동률이면 두 번째로 왼쪽인 표현식을 평가하여 해소되며, 이런 식으로 계속돼요. 모든 ORDER BY 표현식이 동일한 값으로 평가되는 두 행의 반환 순서는 정의되지 않아요. 각 ORDER BY 표현식 뒤에는 선택적으로 ASC(작은 값이 먼저 반환됨) 또는 DESC(큰 값이 먼저 반환됨) 키워드 중 하나가 올 수 있어요. ASC나 DESC가 모두 지정되지 않으면 기본적으로 행은 오름차순(작은 값이 먼저)으로 정렬돼요.

SQLite는 정렬 목적상 NULL 값을 다른 어떤 값보다 작은 것으로 간주해요. 따라서 NULL은 ASC 정렬 순서의 시작 부분과 DESC 정렬 순서의 끝 부분에 자연스럽게 나타나요. 이 동작은 "ASC NULLS LAST" 또는 "DESC NULLS FIRST" 구문을 사용해 변경할 수 있어요.

각 ORDER BY 표현식은 다음과 같이 처리돼요:

  • ORDER BY 표현식이 상수 정수 K이면 해당 표현식은 결과 집합의 K번째 열에 대한 별칭으로 간주돼요(열은 왼쪽에서 오른쪽으로 1부터 번호가 매겨져요).
  • ORDER BY 표현식이 출력 열 중 하나의 별칭에 해당하는 식별자이면 해당 표현식은 그 열의 별칭으로 간주돼요.
  • 그 외의 경우, ORDER BY 표현식이 다른 어떤 표현식이면 이를 평가하고 반환된 값을 사용해 출력 행의 순서를 정해요. SELECT 문이 단순 SELECT이면 ORDER BY에는 임의의 표현식이 포함될 수 있어요. 그러나 SELECT가 복합 SELECT이면 출력 열의 별칭이 아닌 ORDER BY 표현식은 출력 열로 사용된 표현식과 정확히 동일해야 해요.

행을 정렬할 때 값은 비교 표현식과 동일한 방식으로 비교돼요. 두 텍스트 값을 비교하는 데 사용되는 정렬 순서는 다음과 같이 결정돼요:

  • ORDER BY 표현식에 접미사 COLLATE 연산자를 사용해 정렬 순서가 지정된 경우에는 지정된 정렬 순서가 사용돼요.
  • 그렇지 않고 ORDER BY 표현식이 접미사 COLLATE 연산자를 사용해 정렬 순서가 지정된 표현식의 별칭인 경우에는 별칭이 가리키는 표현식에 지정된 정렬 순서가 사용돼요.
  • 그렇지 않고 ORDER BY 표현식이 열이거나 열인 표현식의 별칭인 경우에는 해당 열의 기본 정렬 순서가 사용돼요.
  • 그 외에는 BINARY 정렬 순서가 사용돼요.

복합 SELECT 문에서 모든 ORDER BY 표현식은 복합 SELECT의 결과 열 중 하나에 대한 별칭으로 처리돼요. ORDER BY 표현식이 정수 별칭이 아니면 SQLite는 복합 SELECT에서 가장 왼쪽 SELECT의 결과 열 중 위의 두 번째 또는 세 번째 규칙과 일치하는 열을 검색해요. 일치하는 항목을 찾으면 검색을 중지하고 해당 표현식을 일치한 결과 열에 대한 별칭으로 처리해요. 그렇지 않으면 그다음 오른쪽 SELECT를 시도하고, 이런 식으로 계속해요. 어떤 구성 SELECT의 결과 열에서도 일치하는 표현식을 찾을 수 없으면 오류예요. ORDER BY 절의 각 항은 개별적으로 처리되며 복합 SELECT에서 서로 다른 SELECT 문의 결과 열과 일치될 수 있어요.

5. LIMIT 절

LIMIT 절은 전체 SELECT 문이 반환하는 행 수에 상한을 두는 데 사용해요.

복합 SELECT에서는 마지막, 즉 가장 오른쪽의 단순 SELECT만 LIMIT 절을 포함할 수 있어요. 복합 SELECT에서 LIMIT 절은 마지막 SELECT뿐 아니라 전체 복합 문에 적용돼요. 가장 오른쪽 단순 SELECT가 VALUES 절이면 LIMIT 절을 사용할 수 없어요.

LIMIT 절에는 정수로 평가되거나 손실 없이 정수로 변환할 수 있는 값으로 평가되는 스칼라 표현식이라면 무엇이든 사용할 수 있어요. 표현식이 NULL 값이나 손실 없이 정수로 변환할 수 없는 다른 값으로 평가되면 오류가 반환돼요. LIMIT 표현식이 음수로 평가되면 반환되는 행 수에 상한이 없어요. 그 외에는 SELECT가 결과 집합의 처음 N개 행만 반환해요. 여기서 N은 LIMIT 표현식이 평가되는 값이에요. 또는 LIMIT 절이 없을 때 SELECT 문이 N개보다 적은 행을 반환할 경우, 전체 결과 집합이 반환돼요.

LIMIT 절 다음에 올 수 있는 선택적 OFFSET 절에 연결된 표현식 역시 정수로 평가되거나 손실 없이 정수로 변환 가능한 값으로 평가되어야 해요. SELECT 문에 OFFSET 절이 있으면 반환되는 결과 집합에서 처음 M개 행이 생략되고 그다음 N개 행이 반환돼요. 여기서 M과 N은 각각 OFFSET 절과 LIMIT 절이 평가하는 값이에요. 또는 LIMIT 절이 없었다면 SELECT가 M+N개보다 적은 행을 반환했을 경우, 처음 M개 행은 건너뛰고 나머지 행(있으면)이 반환돼요. OFFSET 절이 음수로 평가되면 결과는 0으로 평가된 것과 동일해요.

별도의 OFFSET 절 대신 LIMIT 절에 쉼표로 구분된 두 스칼라 표현식을 지정할 수도 있어요. 이 경우 첫 번째 표현식은 OFFSET 표현식으로, 두 번째는 LIMIT 표현식으로 사용돼요. OFFSET 절을 사용할 때는 두 표현식 중 두 번째가 OFFSET이고 첫 번째가 LIMIT이므로, 이는 직관에 반해요. OFFSET과 LIMIT의 순서가 뒤바뀐 것은 의도적인 것으로, 다른 SQL 데이터베이스 시스템과의 호환성을 최대화하기 위한 거예요. 하지만 혼동을 피하기 위해 프로그래머는 "OFFSET" 키워드를 사용하는 LIMIT 절 형태를 쓰고, 쉼표로 구분된 오프셋이 있는 LIMIT 절은 피하는 것이 좋아요.

6. VALUES 절

VALUES(expr-list)라는 표현은 SELECT expr-list와 같은 뜻이에요. VALUES(expr-list-1),...,(expr-list-N)라는 표현은 SELECT expr-list-1 UNION ALL ... UNION ALL SELECT expr-list-N와 같은 뜻이에요. 두 형태는 같지만, 복합 SELECT의 SELECT 문 개수는 SQLITE_LIMIT_COMPOUND_SELECT로 제한되는 반면 VALUES 절의 행 수에는 임의의 제한이 없어요.

VALUES 절 사용에는 구문 다이어그램에 표시되지 않은 몇 가지 제한이 있어요:

  • VALUES 절 뒤에는 ORDER BY를 사용할 수 없어요.

  • VALUES 절 뒤에는 LIMIT를 사용할 수 없어요.

7. WITH 절

SELECT 문 앞에는 선택적으로 WITH 절 하나를 둘 수 있어요. 이 WITH 절은 SELECT 문 안에서 사용할 하나 이상의 공통 테이블 표현식을 정의해요.

8. FROM 절의 테이블 값 함수

숨은 열을 포함하는 가상 테이블은 FROM 절에서 테이블 값 함수처럼 사용할 수 있어요. 테이블 값 함수에 전달되는 인자는 가상 테이블의 HIDDEN 열에 대한 제약 조건이 돼요. 추가 정보는 가상 테이블 문서에서 찾을 수 있어요.

9. 표준 SQL과의 차이점

SQLite의 SELECT 구문은 표준 SQL과 약간 달라요. 이러한 차이에는 몇 가지 이유가 있어요:

  • 2000년대 중반에는 메모리 제한이 있는 폴더폰 등에서 너무 많은 공간을 차지하지 않도록 라이브러리 용량을 최대한 작게 유지하는 데 중점을 두었어요.

  • SQLite 초기에는 리드 개발자가 포스텔의 법칙을 따라 입력을 관대하고 유연하게 받아들이려고 했어요.

  • 초기 SQLite 파서에는 이상한 입력을 받아들이는 버그가 있었어요.

  • 리드 개발자의 SQL 지식이 완벽하지 않았어요.

입력 관련 특이한 동작의 원인이 무엇이든, 우리는 일반적으로 이를 '고치려' 하지 않아요. 입력 구문에 새로운 제한을 가하면 SQLite를 사용하는 수백만 개의 애플리케이션 중 적어도 일부가 깨질 가능성이 높기 때문이에요. 우리는 그렇게 되길 원하지 않아요. SQLite 개발 팀의 목표는 가능한 한 최대로 이전 버전과의 호환성을 유지하는 것이에요. 따라서 구문상 특이한 동작이 해롭지 않다면, 고치려고 시도하기보다는 그대로 두고 여기에 문서화해요.

9.1. 이상한 JOIN 이름

SQLite는 JOIN 연산자에 대한 일반적인 구문을 모두 받아들여요:

join-operator:

NATURAL LEFT OUTER JOIN , RIGHT FULL INNER CROSS

하지만 여기서 그치지 않아요. SQLite는 조인 연산자를 지정하는 방식이 실제로 매우 유연해요. 일반 구문은 다음과 같아요:

**
*blah blah blah* **JOIN**

blah는 1~3회 나타나며, 각 blah는 "CROSS", "FULL", "INNER", "LEFT", "NATURAL", "OUTER", "RIGHT" 중 하나일 수 있어요. SQLite 파서는 이 각각의 키워드를 조인의 속성으로 취급하며, 어떤 순서로든 결합할 수 있어요. 이로 인해 구문 다이어그램에 명시된 것 외에도 다양하고 창의적인 새로운 조인 유형이 가능해져요. 이러한 비표준 조인 유형 중 일부는 명시적으로 허용되지 않아요. 예를 들어 "INNER OUTER JOIN"은 모순되므로 사용할 수 없어요. 하지만 "NATURAL LEFT OUTER JOIN"과 같은 뜻인 "OUTER LEFT NATURAL JOIN" 같은 것은 사용할 수 있어요. 또는 "FULL JOIN"과 같은 "LEFT RIGHT JOIN"을 사용할 수 있어요.

기억하세요: 이런 비표준 조인 유형을 사용할 수는 있지만 사용하지 않는 편이 좋아요. 다른 SQL 데이터베이스 엔진과의 이식성을 위해 표준 JOIN 구문을 사용하세요.

9.2. 유연한 조인 구문

표준 SQL은 SQLite보다 조인 구문에 대한 제약이 더 엄격해요. 표준 SQL에서 쉼표 조인, CROSS JOIN, NATURAL JOIN을 제외한 모든 조인에는 ON 절이나 USING 절이 있어야 하며, 쉼표 조인, CROSS JOIN, NATURAL JOIN에는 ON이나 USING 절이 있으면 안 돼요. SQLite는 조인 구문에 대해 그렇게까지 까다롭지 않아요. SQLite는 쉼표 조인이나 CROSS JOIN에서 ON 또는 USING 절을 받아들이고 처리하며, 어떤 조인이든 ON 또는 USING 절을 생략하는 것도 허용해요. SQLite에서 유일한 제한은 다음과 같아요:

  • NATURAL 조인에는 ON 또는 USING 절을 사용할 수 없어요.

  • 같은 조인에 ON 절과 USING 절을 둘 다 사용할 수 없어요.

SQLite는 외부 조인에서도 ON 또는 USING 절을 생략하는 것을 허용해요. 다만 그렇게 하면 외부 조인에 제약이 없게 되어(마치 ON 절이 "ON true"인 것처럼) 외부 조인이 내부 조인처럼 동작해요.

9.3. 쉼표 조인과 CROSS JOIN의 우선순위

표준 SQL에서 JOIN 키워드를 사용하는 조인은 쉼표 조인보다 우선순위가 높아요. 즉, JOIN 연산자가 쉼표 연산자보다 먼저 수행돼요. SQLite에서는 모든 조인의 우선순위가 같아서 그렇지 않아요.

다음 예를 생각해 보세요:

... FROM t1, t2 NATURAL FULL JOIN t3 ...

표준 SQL에서는 t2와 t3 사이의 FULL JOIN이 먼저 일어나고, 그다음 그 조인의 결과가 t1과 크로스 조인돼요. 하지만 SQLite는 항상 모든 조인을 왼쪽에서 오른쪽으로 처리해요. 따라서 SQLite는 먼저 t1과 t2를 크로스 조인한 다음, 그 크로스 조인의 결과가 t3와의 FULL JOIN으로 이어져요. 내부 조인은 본질적으로 결합 법칙이 성립하므로, FROM 절에 외부 조인이 하나 이상 포함된 경우에만 차이가 드러나요.

다음과 같은 스타일 규칙을 지키면 이 문제를 피하고 SQL 문을 모든 시스템에서 이식 가능하게 만들 수 있어요:

  • 쉼표 조인과 JOIN 키워드를 섞지 마세요. 쉼표 조인을 사용해도 괜찮지만, 사용한다면 FROM 절 전체에서 쉼표 조인만 사용해야 해요.

  • 다른 외부 조인 연산자보다 LEFT JOIN을 선호하세요.

  • 확신이 없으면 괄호를 사용해서 의도한 정확한 조인 순서를 지정하세요.

이 제안 중 하나만 지켜도 문제를 피하기에 충분해요. 대부분의 프로그래머는 말하지 않아도 본능적으로 이 모든 제안을 따르므로, SQLite에서 쉼표 조인과 JOIN 키워드 사이의 우선순위 차이가 없는 점은 실제로 거의 문제가 되지 않아요. 하지만 혹시 문제가 나타날 경우를 대비해 이 문제를 알고는 있어야 해요.

이 페이지는 2026-01-26 15:45:07Z에 마지막으로 업데이트되었어요.

더 알아보기 (Learn more)