SQLite – 테이블을 다른 데이터베이스로 이관하기

테이블을 다른 데이터베이스 파일로 이관하는 방법은 단순하다. 새로운 DB 파일을 새 데이터베이스로 연결해서, CREATE TABLE .. AS SELECT .. 구문을 사용해서 복사한다. 다음 예제는 외부 DB 파일을 반입한 다음, 특정 테이블 하나의 내용으로 새로운 테이블을 채우는 과정을 보여준다.

ATTACH 'file-archives.db' AS other;
CREATE TABLE other.images AS SELECT * FROM main.images;
DETACH others;

CREATE TABLE ... AS ... 를 사용하는 이 방법은 사실 한가지 함정을 가지고 있다. 이 쿼리는 테이블을 복제한다기보다는 한쪽의 데이터를 다른 테이블을 만들어서 밀어넣는 역할을 수행한다. 따라서 기존 테이블의 스키마 구조를 온전하게 유지하지 못한다. 단지 SELECT 쿼리의 결과 그리드를 테이블로 단순 변환하는 것에 지나지 않는다.

만약 전체 테이블을 통째로 복제하고 싶다면? 다음의 내용을 dump-command.sql 로 저장하자.

.output some_table_backup.sql
.dump some_table
.output

이 명령은 해당 테이블을 완전히 재구성할 수 있는 sql 문을 파일로 만들어낸다.  예를 들어 원래의 DB 파일 이름이 oldies.db 라고 한다면

sqlite3 --init dump-command.sql oldies.db

를 실행하면 dump-command.sql 파일이 생성된다. 이 파일은 덤프한 시점의 테이블을 그대로 복원할 수 있는 쿼리문이 모두 저장된다. 당연히 새로운 DB 파일에서 이를 실행하면 다른 DB로 해당 데이터를 복제하게 되는 셈이다.  다음 명령을 실행하면 new.db 데이터 베이스가 생성되면서 (혹은 기존에 있던 거라면 열리면서) some_table이 생성되고 그 내용이 완전히 복원되어 있는 것을 확인할 수 있다.

sqlite3 --init some_table_backup.sql new.db

 

(연재) SQLite3 강좌 – 테이블에서 조회하기 2

지난 글에서 SQLite3에서 SELECT 명령의 사용방법에 대해서 살펴보았는데, FROM을 통해서 단일 테이블 혹은 단일 테이블 내의 범위를 부분적으로 얻어내는 서브 쿼리를 통해서 보다 정교한 범위의 데이터를 얻고, 또 WHERE절을 사용해서 결과를 필터링 하는 방법에 대해서도 살펴보았다. 그외에 GROUP BY나 그외 aggregation 연산을 통한 쿼리 방법에 대해서는 자세하게 다루지 않았는데, 그 전에 JOIN에 대해서 간단하게 짚고 넘어가고자 한다.

JOIN 이란 무엇인가

JOIN은 간단히 정의하자면 “두 개 이상의 그리드를 연결해서 하나의 그리드처럼 만드는” 것이다. 테이블 하나를 하나의 그리드라고 보면 두 개의 테이블을 옆으로 이어붙이는 것이라 이해하면 된다. 그런데 무턱대고 두 개의 그리드를 이어붙일 수는 없다. (심지어 두 그리드는 행의 개수가 다를 수도 있다) 논리적으로 두 개의 그리드를 옆으로 이어붙이기 위해서는 하나의 ‘접합점’이 필요하다. 이 접합점은 바로 두 테이블에서 같은 값들이 들어가는 공통된 칼럼이다.

예를 들어 발매된 음반의 정보를 담고 있는 albums라는 테이블과 수록곡이 등재된 tracks라는 테이블이 있다고 가정하자. tracks 테이블에는 각각의 트랙이 수록된 앨범 정보를 나타내는 album_id 라는 칼럼이 있고, 이 칼럼은 albums 테이블의 id 칼럼을 참조하는 외래 키(foreign key)로 설정되어 있다고 하자. (꼭 외래키로 지정되어 있을 필요는 없다. 그저 공통된 값이 있으면 된다.) 그렇다면 tracks 테이블의 album_id 칼럼과 albums 테이블의 id 칼럼을 접합점으로 해서 두 개의 테이블을 나란히 연결할 수 있다. 이렇게 연결된 테이블을 사용하면 특정한 노래에 대해서 노래가 수록된 앨범의 제목이나 앨범의 발매 년도 등, 앨범 관련한 정보를 함께 참조할 수 있게 된다.

SQLite3는 세 가지 조인 방식을 지원하고 있는데, INNER JOIN, LEFT JOIN, CROSS JOIN 이 그것이다. 보다 큰 규모의 다른 DBMS에서는 RIGHT JOIN이나 OUTER JOIN, FULL OUTER JOIN 등의 옵션도 지원하는 경우가 있다. 여기서는 SQLite3의 JOIN 방식에 대해서 알아보자.

INNER JOIN

INNER JOIN은 가장 흔하게 쓰이는 JOIN 방식으로 두 개의 테이블이 공통된 칼럼을 가지고 있고, 해당 칼럼의 값이 같은 레코드끼리 연결한다. 이 때 두 그리드가 연결되는 방식은 교집합과 비슷하다. 만약 앨범 정보가 없는 트랙이 있거나, 혹은 앨범 정보는 있지만, 해당 앨범의 수록곡 정보가 없는 경우가 있다면 이들 한쪽만 정보가 있는 칼럼은 INNER JOIN 에서는 제외된다.

JOIN의 일반적인 문법은 다음과 같다.

SELECT {결과칼럼...} FROM {Table1} [as alias1]
{INNER|LEFT|CROSS} JOIN {Table2} [as alias2]
ON {연결조건}  / USING (일치칼럼)
...
  • FROM 절에 쓰이는 테이블 명이 왼쪽에 오는 그리드가 된다. as 를 통해서 별칭을 붙여줄 수 있다.
  • JOIN의 타입과 오른쪽에 올 그리드를 지정한다. 역시 테이블 명 뒤에는 as를 통해서 별칭을 붙여줄 수 있다.
  • 두 테이블이 접합에 사용하는 칼럼 외에도 같은 이름의 칼럼을 가지고 있다면 테이블명.칼럼명 이나 별칭.칼럼명의 형태로 지정할 수 있다.
  • 통상 특정 칼럼의 값이 같아야 하니 ON table1.colA = table2.colB 와 같은 식으로 조건을 정의한다.
  • 만약 두 테이블의 공통 칼럼이 이름이 같고, 그 칼럼의 값이 같은 행끼리 연결한다면 USING (col) 로 축약해서 쓸 수 있다.

JOIN으로 연결한 결과는 하나의 그리드로 보게되며, 따라서 SELECT 문의 WHERE, ORDER BY LIMIT, ㅌ을 그대로 사용할 수 있다. 예를 들어 각 노래의 제목과 그 노래가 수록된 앨범명을 보고 싶다면 다음과 같이 쿼리를 작성할 수 있다.

아래 쿼리는 http://www.sqlitetutorial.net/tryit/ 에서 테스트 해 볼 수 있다.

SELECT name, title FROM tracks 
-- name은 tracks에서 곡 제목을, 
-- title은 albums에서 앨범 제목을 나타내는 칼럼이다.
INNER JOIN albums ON tracks.almubid = albums.albumid;

각 앨범의 수록곡 개수를 세고 싶다면 albumid 별로 그룹핑해서 각 트랙의 개수를 세면 된다.

-- 앨범별 수록곡 개수
SELECT title, count(name) FROM tracks
INNER JOIN albums USING (albumid)
GROUP BY albumid
ORDER BY title;

JOIN은 그리드를 왼쪽에서 오른쪽으로 붙여나간다고 하였다. 따라서 2개, 3개, 4개의 테이블을 연이어 조인할 수 있다.

-- 각 트랙과 수록된 앨범, 아티스트 명을 출력한다.
SELECT tracks.name, title, artists.name
FROM tracks
INNER JOIN albums USING (albumid)
INNER JOIN artists USING (artistid)
WHERE trackid < 200; -- 결과가 너무 많아져서 붙임

LEFT JOIN

INNER JOIN에서는 접합 칼럼을 기준으로 조건에 맞는 행이 없는 경우는 모두 탈락시킨다고 하였다. LEFT JOIN은 말 그대로 접합부의 왼쪽 그리드를 기준으로 삼는 JOIN 방식이다. 따라서 INNER JOIN과는 달리 일치하는 값이 없는 경우라도 접합점의 왼쪽에 있는 테이블의 행은 알 수 없는 값을 모두 NULL로 채워서 결과를 만든다. 따라서 기준이 되는 테이블에서는 누락되는 행 없이 결과를 조회할 수 있다.

음반 관련 데이터베이스에는 artists라는 테이블이 있다. 여기에는 앨범을 발매한 가수 혹은 작곡가로 참여한 사람의 고유 식별자와 이름이 기재된다. 이 때 전업 작곡가의 경우에는 artists 테이블에는 이름이 기록되지만, 발매한 앨범이 없기 때문에 INNER JOIN으로 albums 테이블과 조인하면 이들의 이름이 포함되지 않는다. 사람의 이름을 기준으로 앨범 발매 여부와 상관없이 앨범 타이틀과 아티스트명을 조회하려면 다음과 같이 한다.

-- 아티스트와 그 아티스트가 발매한 앨범
SELECT name, title FROM artists
LEFT JOIN albums ON artists.artistid = albums.artistid;

두 테이블 A와 B 가 있을 때, INNER JOIN의 경우 A를 기준으로 B를 접합하는 경우와 B를 기준으로 A를 접합하는 경우의 쿼리는 다르지만 그 결과는 동일하다. 어차피 접합점이 되는 칼럼을 기준으로 결과가 생성되기 때문이다. 따라서 INNER JOIN의 경우 양쪽 테이블 위치에 상관없이 결과 자체는 대칭이라 할 수 있다. 하지만 LEFT JOIN은 왼쪽에 위치하는 테이블을 기준으로 행이 정의되기 때문에 위 쿼리에서 albums를 FROM에 쓴다면 결과가 달라질 것이다.

SELECT count(*) FROM albums
LEFT JOIN artists USING (artistid);  -- 347

SELECT count(*) FROM artists
LEFT JOIN albums USING (artistid); -- 418

참고로 3개 이상의 테이블을 연결하는 것은 앞에서부터 연결된 결과 그리드에 뒤의 테이블을 누적해서 더하는 개념이며 개별 테이블이 더해지는 개념이 아니다. 다음 쿼리는 아티스트와 그 발매 앨범, 앨범별 수록곡의 수를 쿼리하는데, 두 개의 LEFT JOIN이 들어간다. artists – albums – tracks가 연결되는데, albums와 tracks는 albums.albumid = tracks.albumid 로 연결되는데,  (A + B ) + (A + C) 의 개념이 아니라 (A + B) + C 와 같은 식으로 테이블이 합쳐진다고 볼 수 있다.

-- 아티스트의 각 앨범과 앨범 수록곡의 수
SELECT a.name, title, count(b.trackid)
FROM artists as a
LEFT JOIN albums USING (artistid)
LEFT JOIN tracks USING (albumid)
GROUP BY b.albumid;

SELF JOIN

특정한 조건에 해당하는 행을 찾기 위해서 가끔 어떤 테이블과 그 테이블 자신을 JOIN 하는 경우가 있다. 이것을 셀프 조인이라고 하는데, 실제로 많이 쓰이는 케이스이다. 예를 들어 employees라는 테이블을 생각해보자. 여기에는 각 직원의 이름과 성, 그리고 이 직원의 직속상사(보고를 받는 사람)가 누구인지에 대한 정보를 가지고 있다고 하자. 이 때 각 직원의 이름과 그 직속상사의 이름을 짝지은 결과를 조회하기 위한 쿼리를 보자. 참고로 두 개의 문자열을 연결하기 위해서는 || 연산자를 사용할 수 있다.

-- 각 직원의 풀네임과 그 직속 상관의 풀네임
SELECT a.firstname || ' ' || a.lastname AS fullname,
       b.firstname || ' ' || b.lastname AS directReportsTo
FROM employees a
INNER JOIN employees b ON a.reportsto = b.employeeid
;

JOIN에서는 그저 두 개의 그리드가 횡방향으로 연결되는 것이기 때문에 셀프 조인은 특별한 것이 아니다. 다만 두 개의 테이블 이름이 똑같기 때문에 혼동을 피하기 위해서 두 개의 그리드 모두에 각각의 별칭을 붙여서 사용한다. 별칭의 경우 as 를 써도 되고 쓰지 않아도 된다. (MySQL등 JOIN시 테이블 별칭을 쓸 때 as을 쓰지 않도록 강제하는 경우도 있으니 참고한다.)

참고로 위에서는 INNER JOIN을 썼는데, LEFT JOIN을 쓰면 어떻게 될까? reportsTo 칼럼이 NULL인 레코드가 검색될 수 있다. 그렇다면 이 사람은 누구에게도 보고하지 않아도되는, CEO가 될 것이다.

CROSS JOIN

CROSS JOIN은 극히 특별한 케이스에서 좀 유용할 수 있다. (사실 왜 특별하게 CROSS JOIN 이라고 이름이 붙었는지는 알 수 없는데…) 두 테이블 A, B에 대해서 JOIN이 수행될 때, ON 이나 USING 으로 한정하는 절이 없으면, 실질적으로 발생하는 데이터는 두 테이블의 데카르트 곱이다. 테이블 A가 m 개 행을 가지고 있고, 테이블 B가 n개 행을 가지고 있다면, 두 테이블의 데카트르 곱은 A의 1행이 B의 1 … n 의 모든 행에 대응되는 결과를 만들고, 다시 A의 2행이 B의 모든 행에 대응되고… 이런식으로 m * n 개의 행을 가진 모든 조합의 경우를 만들어 낼 수 있다. CROSS JOIN은 그 특성상 매우 큰 그리드를 생성하므로 다시 한 번 말하지만, 주의해서 사용해야 한다.

굳이 CROSS JOIN으로 명시하지 않더라도 INNER JOIN, LEFT JOIN을 접합 조건 없이 사용하거나, 단순히 FROM 에서 두 개의 테이블을 컴마로 나열하기만 해도 같은 효과를 얻을 수 있다.

이상으로 SQLite에서 여러 테이블을 조인하는 방법에 대해서 살펴보았다. 다시 한 번 정리하자면 JOIN은 두 개의 그리드를 옆으로 이어붙이는 용도로 사용하며, 두 개 이상의 그리드에 대해서 적용이 가능하다. 그 중에서 INNER JOIN과 LEFT JOIN이 많이 쓰이며, INNER JOIN은 두 그리드의 공통 칼럼 값을 기준으로 결과를 생성하고, LEFT JOIN은 두 그리드 중 왼쪽의 것을 기준으로 결과를 생성한다는 차이가 있다. JOIN을 사용하게 되면 각각의 테이블에 분산되어 있는 데이터를 하나로 묶어서 쿼리할 수 있고, JOIN으로 묶여진 결과는 하나의 큰 테이블처럼 생각할 수 있다.

(연재) SQLite3 강좌 – 테이블에서 조회하기 1

테이블에 저장된 데이터를 조회하기 위해서는 SELECT 구문을 사용한다. SELECT 구문운 SQL에서 가장 흔히 쓰이는 쿼리 구문인 동시에 가장 복잡한 구문이기도 하다. 테이블 조회와 관련하여 SQLite3는 표준 SQL에 정의된 거의 모든 기능을 제공한다. SQLite3의 SELECT 구문의 사용법에 대해서 살펴보자.

기본 컨셉

기본적으로 SELECT 는 DB 엔진으로부터 질의에 대한 답을 요청하는 명령이다. 가장 흔하게는 테이블 내의 레코드들을 조회하는 용도로 사용되지만, SELECT 명령의 본질은 DB가 수행할 수 있는 연산의 결과를 얻는 명령이다. 따라서 테이블 조회뿐만 아니라 다음과 같이 계산 결과를 얻도록 호출할 수도 있다.

SELECT 1 + 1;

위 쿼리는 단순히 2라는 결과를 얻게 된다. 테이블을 지정해서 데이터를 조회하지는 않지만 하나의 온전한 SELECT 구문이기도 하다.  물론 이것은 SELECT라는 명령이 DB엔진으로부터 어떠한 값을 요청한다는 개념을 보여주는 예이지, 실제로 이런 간단한 계산 때문에 데이터베이스를 사용하는 일은 드물다. SELECT 구문은 기본적으로 테이블의 내용을 조회하는 내용으로 알려져 있다.

하지만 실제로는 다음의 개념으로 SELECT 명령을 이해해야 한다.

  1. SELECT 명령은 어떤 “결과”를 요구하는 명령이다.
  2. SELECT 명령은 폭과 높이가 각각 0 이상인 2차원 그리드로 구성된 데이터에 대해서 수행된다.
  3. SELECT 명령의 결과 역시 폭과 높이가 각각 0 이상인 2차원 그리드로 구성될 수 있다.

이 관점에 따르면 SELECT 1 + 1; 은 첫째로 연산의 결과를 요구하고 있으며, 대상데이터는 가로가 0, 세로가 0인 그리드를 가지고 있다. 그리고 그 결과는 가로가 1, 세로가 1인 1X1 그리드로 단일값이 된다.  실제로 통상의 SELECT문이 대상으로 하는 테이블 역시 2차원 그리드이며, SELECT 문의 조회 결과 역시 여러 행일 수 있고, 한 행은 여러 칼럼으로 구분될 수 있다. 즉, 결과 역시 그리드가 될 수 있다.

SELECT 구문은 사실 매우 복잡하기 때문에, SQL에서도 가장 큰 비중을 차지한다. 일단 기본적인 SELECT 구문의 얼개를 살펴보면 대략 다음과 같다고 할 수 있다.

  1. SELECT [DISTINCT] {결과 칼럼 세트} : 결과에 표시될 칼럼을 선택한다. DISTINCT가 쓰이면 중복된 행을 제거한다.
  2. FROM 테이블|서브쿼리 : FROM 은 어디서 데이터를 조회할 것인지를 선택한다. 보통은 테이블 명을 쓴다. 이때 테이블 대신 서브 쿼리를 쓸 수도 있다. 즉 테이블이든 서브쿼리든 2차원 그리드라는 점을 기억하자.
  3. WHERE 조건 : 필터링 조건을 지정한다.
  4. ORDER BY 조건 [ASC|DESC] : 정렬조건을 선택한다.
  5. LIMIT {최대개수} [OFFSET {오프셋}] : 조회 결과의 범위와 양을 선택한다.

이것이 가장 심플한 SELECT 구문의 모양이다. 2, 3, 4, 5번은 각각 선택적으로 사용할 수 있다.

예를 들어 어떤 게시판에서 최근 글 순으로 50개를 표시하고 싶다면 다음과 같이 쿼리를 작성할 수 있을 것이다.

SELECT post_no, title, author, reg_date 
FROM board
ORDER BY reg_date DESC
LIMIT 50;

위 쿼리 구문은 다음과 같은 구조로 이루어진다.

  1. SELECT 뒤에는 결과 그리드의 각 칼럼을 명시한다. 조회 대상에서 같은 이름의 칼럼이 있으면 그 값이 사용될 것이다.
  2. FROM 뒤에는 조회 대상이 되는 소스 그리드를 명시하는데, 여기서는 board라는 테이블이다.
  3. ORDER BY 절을 통해서 등록일의 역순, 즉 최근 등록순으로 정렬한다.
  4. LIMIT 50 은 출력의 양을 50개 행으로 제한한다.

LIMIT 다음에는 선택적으로 OFFSET이 올 수 있다. OFFSET 은 범위를 지정한다. 예를 들어 위 쿼리가 게시물 목록의 1 페이지라면 2페이지는 50개를 띄운 이후 범위에서부터 가져오면 된다. 따라서 OFFSET 50 을 LIMIT 50 뒤에 추가한다.

결과 정렬

ORDER BY 절은 특정한 칼럼의 값을 기준으로 결과를 정렬한다. 정렬의 기준이 될 칼럼명과 정렬의 방법(오름차순, 내림차순)을 쓴다. 정렬 방법을 명시하지 않는 경우 오름차순(ASC)으로 간주된다. 컴마(,)를 사용해서 정렬조건을 2개 이상 지정할 수 있고, 이 때에는 첫번째 정렬 조건에서 같은 순위를 갖는 행들이 두 번째 정렬조건에 의해서 다시 순서를 갖게 된다. 아무런 정렬조건이 없을 때 데이터는 테이블에 삽입된 순서대로 정렬된다.

필터링 – WHERE 절

WHERE 절은 결과를 필터링하기 위해 사용한다. 기본 모양은 WHERE 검색조건 이며, 이 검색 조건은 주로 비교식의 형태를 갖는다. 비교식은 좌변 연산자 우변 의 모양을 갖는다. 연산자는 기본적인 = , != , <> , < , > , <=, >= 의 비교 연산외에 IN , LIKE , BETWEEN , GLOB 등을 사용할 수 있다.

WHERE col_1 = 100
WHERE col2 IN (2, 7, 9)
WHERE col3 LIKE 'An%'
WHERE col4 BETWEEN 10 AND 20

WHERE 절의 내용은 참/거짓 값으로 평가된다. 따라서 NOT, AND, OR 의 논리 연산를 사용할 수 있다. 그외에 추가적으로 사용할 수 있는 연산자에는 이런 것들이 있다.

  • ALL : 주어진 모든 평가식이 참일 때 참
  • ANY : 주어진 모든 평가식 중 하나가 참이면 참
  • EXISTS : 서브쿼리를 만족하는 행이 하나 이상 포함되어 있는지 검사.
WHERE col1 = ANY (SELECT col1_2 FROM other_table WHERE colx > 99)
WHERE col1 = ALL (SELECT col1_2 FROM other_table WHERE colx = 99)
WHERE EXISTS (SELECT col1_2 FROM other_table WHERE coly <> 100)
  1. 서브 쿼리의 col1_2 칼럼중에서 현재 조회 결과 중에서 col1과 같은 것이 하나라도 있으면 그것을 포함한다.
  2. 서브 쿼리의 col1_2 칼럼이 모두 col1과 같은 것만 필터링한다.
  3. 서브 쿼리의 칼럼 결과가 있으면 만족하는 것으로 취급된다. 따라서 EXISTS 평가식의 서브 쿼리에는 현재 결과의 칼럼이 들어가거나 할 수 있다.

LIKE 비교

LIKE는 문자열의 전체가 아닌 일부가 맞는지를 평가한다. 보통 우변에 % 문자를 포함시키는데, 이는 유닉스 와일드카드와 비슷하게 기능한다.

WHERE name LIKE 'Ap%'  - Ap 혹은 Ap로 시작하는 문자열에 매칭
WHERE name LIKE '%ed'  - ed 혹은 ...ed로 끝나는 문자열에 매칭
WHERE name LIKE '%or%` - ...or... 이 들어가는 문자열에 매칭 

LIKE 매칭에서는 % 외에도 _ 도 쓰인다. 이는 임의의 한 글자라는 의미이다. '__pple' 는 ##pple 에 매칭되기 때문에 apple 에는 매칭되지 않는다. (%pple 에는 apple이 매칭되겠지만)  만약 찾고자 하는 문자열이 순수하게 _ 를 담고 있다면 이를 이스케이프해야 한다. 쿼리 내 문자열 리터럴에는 백슬래시를 사용한 이스케이프를 사용하지 않고 ESCAPE '_' 를 뒤에 붙여서 알려준다.

WHERE col1 LIKE 'get_value' ESCAPE '_'
                            ~~~~~~~~~~

GLOB 연산

GLOB 연산자는 LIKE와 비슷한데, 유닉스 와일드카드를 사용하게 해준다. 이는 *, ?, [a-z], [A-Z], [0-9] 와 같이 LIKE 연산보다는 조금 세부적인 필터링이 가능하다.

결과에서 중복된 행을 제거하기 DISTINCT

SELECT DISTINCT col1, col2,... 과 같이 DISTINCT 를 사용해서 쿼리를 보내면 중복되는 행을 제거한 결과를 얻을 수 있다. 이는 모든 조회, 필터링, 범위 선택을 마친 출력될 결과 그리드에 대해서 “완전히 중복되는 행”을 필터링한다. (특정한 칼럼만 중복되는 경우는 제외되지 않는다.) 만약 특정한 칼럼이 중복되어 있는 데이터에서 그 칼럼의 중복을 없애고자 한다면 GROUP BY를 사용해야 한다.

SELECT의 결과와 서브 쿼리

FROM 절이 없는 경우(앞서 1+1의 계산에서 보았듯이), 데이터베이스가 데이터를 조회할 대상은 0행, 0칼럼의 크기가 없는 가상의 테이블이 된다. 또 SELECT 구분의 결과는 결과 칼럼 셋에서 지정한 너비의 칼럼 수와 전체 결과 수의 행수를 갖는 그리드가 된다. 따라서 맨 처음 소개했던 SELECT 1 + 1; 은 크기가 없는 데이터셋을 사용하며 그 결과는 1 X 1 크기의 그리드가 된다. 만약 SELECT 1 + 1, 2 * 2, 3 + 8; 이라는 쿼리라면 1행 3열의 그리드가 그 결과가 될 것이다.

SELECT의 결과가 행과 열로 이루어진 그리드이고, 이러한 사각형 모양의 데이터 셋은 테이블의 모양과도 매우 비슷하다. 따라서 SELECT 의 결과 자체는 일종의 임시테이블로 볼 수 있으며, 따라서 SELECT 구문의 FROM 절은 테이블이 아닌 다른 SELECT 구문의 결과에서부터 데이터를 조회하는 것도 가능하다. 이를 서브 쿼리라고 한다.

굳이 예를 들자면, SELECT * FROM board; 라는 쿼리는 board 테이블의 전체 내용을 그대로 결과로 만드는 쿼리이다. 즉 이 쿼리문이 그대로 FROM 에 쓰이면 FROM board와 동치가 된다. 이 말은 최근 글 50개를 다음과 같이 작성하는 것도 가능하다는 의미이다.

SELECT title, author, reg_date
FROM (
    SELECT * FROM board
)
ORDER BY reg_date DESC
LIMIT 50, 50; -- LIMIT 50 OFFSET 50을 이렇게 줄여 쓸 수 있다.

서브 쿼리를 활용하면 단일 쿼리에서는 구성하기 어려운 정렬, 그룹핑 조건을 비교적 쉽게 하나의 쿼리로 구성할 수 있다. 그저 다른 SELECT 문의 결과를 테이블처럼 사용하면 되는 것이기 때문에 유용하게 사용할 수 있을 것이다.