(SQLite3) SELECT 문을 알아보자

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

기본 컨셉

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

SELECT 1 + 1;

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

따라서 실제로는 다음의 개념으로 SELECT 명령을 이해해야 한다.

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

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

SELECT문은 매우 다양한 상황과 목적에서 사용되며, 지원 및 고려해야하는 조건과 요소가 많기 때문에 그 문법이 실은 매우 복잡하다. 이 때문에 SQL 중에서도 학습량이 가장 많다. 일단 기본적인 SELECT 구문의 얼개를 살펴보면 대략 다음과 같다고 할 수 있다.

  1. SELECT [DISTINCT] {결과 칼럼 세트} : 결과에 표시될 칼럼을 선택한다. DISTINCT가 쓰이면 중복된 행을 제거한다. 칼럼들은 컴마로 구분하며, *를 쓰면 전체 칼럼을 조회한다.
  2. FROM 테이블|서브쿼리 : FROM 은 어디서 데이터를 조회할 것인지를 선택한다. 조회 대상은 2차원 그리드이며, 주로 하나의 테이블이다. 물론 다른 SELECT문의 결과 역시 그리드이기 때문에 다른 SELECT문을 쓸 수도 있다. 또한 JOIN 절이 들어가서 두 개 이상의 그리드를 결합할 수 있다.
  3. WHERE 조건 : 필터링 조건을 지정한다.
  4. ORDER BY 조건 [ASC|DESC] : 정렬조건을 선택한다.
  5. LIMIT {최대개수} [OFFSET {오프셋}] : 조회 결과의 범위와 양을 선택한다.
  6. 그 외에 결과를 특정한 항목을 기준으로 그룹으로 나누고 각 그룹별 통계등의 연산을 수행하기 위해 GROUP BY, HAVING 절을 추가적으로 사용할 수 있다.

(일반적인) 가장 심플한 SELECT문의 구성은 1, 2를 포함한다. SELECT 칼럼1, 칼럼2,... FROM 테이블;의 형태로 대상 테이블에서 관심있는 칼럼들만 조회할 수 있다.

간단한 예를 살펴보자. 아래 쿼리는 게시판의 글 내용을 저장하는 테이블에서 최근 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 은 조회 결과의 시작점이다. 따라서 50개씩 한 페이지라고 할 때 두 번째 페이지는 오프셋 50부터 50개 (51~100번)를 조회해야 하므로 LIMIT 50 OFFSET 50 이라고 한다.

결과를 정렬하기 – ORDER BY

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 : 서브쿼리를 만족하는 행이 하나 이상 포함되어 있는지 검사.

다음은 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의 결과는 테이블과 비슷하게 행과 열로 이루어진 그리드이고, 따라서 SELECT 의 결과 자체는 일종의 임시테이블로 볼 수 있으며, 이는 SELECT 에서 FROM 다음에 오는 조회 대상이 될 수 있다. (그 외에도 JOIN 대상이 될 수 있다.)

다음 예는 앨범에 관한 테이블과 음악가에 대한 테이블을 조인하여 앨범명과 음악가를 짝지어 조회하는 쿼리이다. 보통 이 경우에는 테이블을 조인하고 연결된 테이블에서 WHERE 절을 사용해서 필터링하기도 하지만, JOIN의 대상이 테이블이 아닌 서브 쿼리로 만들어서 연결할 수 있음을 보이고 있다.

SELECT title, b.name as artist FROM Albums
INNER JOIN (SELECT * FROM Artists WHERE NAME LIKE "A%") AS B
  ON a.ArtistId = b.ArtistId
ORDER BY title ASC;

그 외

LIMIT x OFFSET y는 줄여서 LIMIT y, x로 대체할 수 있다.