데이터베이스 쿼리는 필터, 정렬, 프로젝션, 페이지네이션을 엔진이 계획하고 실행할 수 있는 언어로 기술한 구조화된 데이터 요청입니다. 모든 쿼리 언어의 밑바탕에 있는 핵심 개념은 선언성입니다. 결과를 기술하면 경로는 엔진이 고릅니다. 다섯 가지 동사와 비용 모델을 한 번 익혀 두면, SQL, 문서 API, GraphQL, SDK 빌더 같은 모든 문법이 새로운 언어가 아니라 억양의 차이가 됩니다.
핵심 요약
| 질문 | 답변 |
|---|---|
| 정의 | 정확한 요청: 어떤 레코드, 어떤 순서, 어떤 필드, 몇 개 |
| 패러다임 | 선언형 – 무엇을 말하면 옵티마이저가 어떻게를 결정 |
| 파이프라인 | 파싱 → 검증 → 계획/최적화 → 실행 |
| 비용 모델 | 인덱스 탐색 vs. 전체 스캔 – EXPLAIN이 어느 쪽인지 알려 줌 |
| 보안 규칙 | 항상 매개변수화, 입력을 쿼리에 이어 붙이지 않음 |
네 가지 쿼리 언어로 쓴 같은 쿼리
게시된 글, 조회수 1,000 초과, 최신순이라는 같은 요청을 언어 계열별로 쓰면 다음과 같습니다:
-- SQL: 원조 선언형 언어
SELECT title, views FROM articles
WHERE status = 'published' AND views > 1000
ORDER BY published_at DESC
LIMIT 20;
// 문서 쿼리 API
db.articles.find(
{ status: 'published', views: { $gt: 1000 } }, // 필터
{ title: 1, views: 1 } // 프로젝션
).sort({ publishedAt: -1 }).limit(20)
// GraphQL: 클라이언트가 돌려받을 형태를 선언
query {
articles(where: { status: "published", views_gt: 1000 },
orderBy: publishedAt_DESC, first: 20) {
title
views
}
}
그리고 SDK 빌더입니다. 대부분의 앱 코드가 실제로 쓰는 인터페이스로, 같은 구조로 컴파일됩니다:
// JavaScript / Node.js — Back4app JS SDK
// Filter, sort, project, paginate — the full anatomy in one query
const query = new Parse.Query('Article');
query.equalTo('status', 'published'); // filter
query.greaterThan('views', 1000); // filter (range)
query.descending('publishedAt'); // sort
query.select('title', 'views'); // project: only these fields
query.limit(20).skip(40); // paginate: page 3
const articles = await query.find(); // Flutter / Dart — Back4app Flutter SDK
// Filter, sort, project, paginate — the full anatomy in one query
final query = QueryBuilder<ParseObject>(ParseObject('Article'))
..whereEqualTo('status', 'published') // filter
..whereGreaterThan('views', 1000) // filter (range)
..orderByDescending('publishedAt') // sort
..keysToReturn(['title', 'views']) // project: only these fields
..setLimit(20)..setAmountToSkip(40); // paginate: page 3
final response = await query.query(); // iOS / Swift — Back4app Swift SDK
// Filter, sort, project, paginate — the full anatomy in one query
let query = Article.query("status" == "published", "views" > 1000)
.order([.descending("publishedAt")]) // sort
.select("title", "views") // project: only these fields
.limit(20).skip(40) // paginate: page 3
query.find { result in
if case .success(let articles) = result { render(articles) }
} // Android / Kotlin — Back4app Android SDK
// Filter, sort, project, paginate — the full anatomy in one query
val query = ParseQuery.getQuery<ParseObject>("Article")
query.whereEqualTo("status", "published") // filter
query.whereGreaterThan("views", 1000) // filter (range)
query.orderByDescending("publishedAt") // sort
query.selectKeys(listOf("title", "views")) // project: only these fields
query.limit = 20; query.skip = 40 // paginate: page 3
query.findInBackground { articles, e -> if (e == null) render(articles) } 데이터베이스의 쿼리 실행 과정
선언형 쿼리가 작동하는 이유는 옵티마이저에 있습니다. 옵티마이저는 사용 가능한 인덱스를 따져 보고, 행 수를 추정해, 가장 저렴한 계획을 고르며, EXPLAIN은 그 결정을 보여 줍니다. 한 줄로 요약한 성능 개선 방법은 이렇습니다. EXPLAIN을 실행하고, 전체 스캔을 찾고, 필터가 절실히 필요로 하는 인덱스를 추가하고, 다시 확인합니다.
선언형 vs. 명령형
| 구분 | 선언형 쿼리 | 명령형 코드 |
|---|---|---|
| 작성하는 것 | 원하는 결과 | 결과를 계산하는 단계 |
| 최적화 주체 | 엔진, 실행할 때마다 | 개발자, 작성할 때 한 번 |
| 데이터 크기에 적응 | 예 – 통계에 따라 계획이 바뀜 | 아니요 – 루프는 그냥 루프 |
| 사용되는 곳 | SQL, 문서 API, GraphQL, 빌더 | 레코드를 순회하는 애플리케이션 루프 |
| 실패 징후 | 잘못된 실행 계획(인덱스/힌트로 해결) | N+1 루프, 메모리 폭증 |
이 모두에 공통으로 적용되는 구조는 다음과 같습니다. 필터(WHERE), 정렬(ORDER BY), 프로젝션(SELECT 목록 – 필요한 것만 요청), 페이지네이션(LIMIT과 오프셋 또는 커서), 집계(GROUP BY 등)입니다. 다섯 가지 동사가 모든 인터페이스에 통합니다.
쿼리 장애 대부분을 막는 두 가지 규칙
항상 매개변수화합니다. 인젝션은 공격자의 입력이 쿼리 구조가 되는 것이며, 입력을 값으로 바인딩하는 플레이스홀더로 완전히 해결됩니다:
// 취약: 입력이 쿼리 구조에 그대로 이어 붙여짐
db.query(`SELECT * FROM users WHERE name = '${input}'`); // 절대 금지
// 안전: 매개변수화 – 입력은 구문이 아니라 데이터
db.query('SELECT * FROM users WHERE name = $1', [input]);
SDK 빌더와 GraphQL 변수는 구조적으로 이렇게 동작합니다. 상위 수준 쿼리 인터페이스가 조용히 가져다주는 보안상의 이점 중 하나입니다.
접근 형태에 맞춰 페이지네이션합니다. 오프셋 페이지네이션(LIMIT 20 OFFSET 400)은 어느 페이지로든 이동할 수 있지만 깊이에 비례해 비용이 늘고 동시 쓰기에 흔들립니다. 커서 페이지네이션(WHERE published_at < $last)은 어떤 깊이에서도 안정적이고 비용이 일정하지만 앞으로만 이동합니다. 피드와 무한 스크롤에는 커서가 맞고, 페이지 번호가 있는 관리자 테이블은 오프셋을 써도 무방한 영역입니다.
일반적인 사용 사례
- 애플리케이션 읽기. 목록 화면, 상세 화면, 검색입니다. 필터-정렬-프로젝션-페이지네이션이 본래 쓰임새대로 쓰이는 곳입니다.
- 집계와 리포팅. 개수, 합계, 그룹별 요약을 앱 코드에서 계산하지 않고, 데이터가 있는 엔진으로 내려보냅니다.
- API 인터페이스. 자동 생성 API는 URL 매개변수나 GraphQL 선택 집합을 바로 이런 쿼리로 변환합니다. 쿼리의 구조는 모든 추상화를 뚫고 드러납니다.
- 실시간 필터. 라이브 구독은 상시 실행되는 쿼리입니다. 같은 조건식이 계속해서 평가됩니다.
- 프로덕션 디버깅. EXPLAIN과 슬로 쿼리 로그는 “갑자기 느려졌다” 유형의 장애를 진단하는 반복 과정입니다.
원시 쿼리 언어, 빌더, SDK 중 무엇을 선택해야 할까? 결정 매트릭스
| 선택… | 이런 경우… | 주의할 점… |
|---|---|---|
| 원시 쿼리 언어 | 복잡한 집계, 리포트, 마이그레이션 | 문자열을 이어 붙이면 인젝션, 이식성 |
| 쿼리 빌더 / SDK | 애플리케이션 CRUD와 목록 – 대부분의 코드 | N+1 루프, 빌더가 실행 계획을 가림 |
| GraphQL | 클라이언트가 필드를 고르고 관계를 중첩해야 함 | 제한이 없으면 쿼리 비용이 끝없이 커짐 |
| 저장 프로시저/서버 측 로직 | 데이터 가까이에서 처리하는 다단계 작업 | 코드베이스에서 보이지 않는 로직 |
추상화 계층 논의에서 나온 현실적인 규칙이 그대로 적용됩니다. 일상적인 90%에는 빌더를, 제어가 그만한 가치를 하는 곳에는 원시 쿼리를 씁니다. 어느 쪽이든 쿼리 구조에 대한 지식은 그대로 옮겨 갑니다.
한계와 트레이드오프
- 선언형은 공짜가 아닙니다. 옵티마이저의 판단은 통계와 인덱스 수준을 넘어서지 못합니다. 올바른 쿼리라도 인덱스가 잘못되면 여전히 느립니다.
- 쿼리는 비용을 숨깁니다. 빌더 체인 한 줄이 수백만 행의 스캔일 수 있습니다. EXPLAIN만이 정직한 거울입니다.
- N+1 함정은 쿼리 위에 있습니다. 개별적으로는 완벽한 쿼리도 루프 안에서 실행되면 전체적으로는 심각한 성능 문제를 일으킵니다. 배칭과 include는 바로 이 때문에 존재합니다.
- 깊은 페이지네이션은 성능이 떨어집니다. 오프셋 깊이는 선형 비용입니다. 피드는 처음부터 커서를 중심으로 설계해야 합니다.
- 언어 난립은 현실입니다. SQL, 문서 API, GraphQL, 검색 DSL마다 팀이 치러야 할 비용이 있습니다. 공통된 쿼리 구조가 그 해독제입니다.
Back4app에서의 쿼리
Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. Back4app의 쿼리 전략은 SDK 빌더 경로를 철저하게 구현한 것입니다. 모든 SDK의 쿼리 빌더(위의 코드 탭)는 필터, 정렬, 프로젝션, 페이지네이션을 인젝션 위험이 없는 매개변수화된 쿼리로 컴파일하고, include()는 N+1 루프 없이 관계를 처리하며, 같은 조건식이 실시간 업데이트를 위한 라이브 쿼리를 구동하고, GraphQL은 응답 형태를 직접 선언하려는 클라이언트를 지원합니다. 다섯 가지 동사, 모든 인터페이스, 하나의 백엔드입니다.
자주 묻는 질문
데이터베이스 쿼리를 쉽게 설명하면 무엇입니까?
데이터베이스에 데이터를 조회하거나 변경하도록 요구하는 구조화된 요청입니다. "조회수가 1,000을 넘는 게시된 글을 최신순으로 20개씩 달라"는 식입니다. 쿼리 언어로 작성하거나 SDK로 구성하며, 엔진이 파싱할 수 있도록 엄격한 문법을 따르고, 기술한 바로 그 데이터 조각만 정확히 반환합니다.
SQL은 선언형입니까, 명령형입니까?
선언형입니다. 원하는 데이터가 무엇인지만 기술하면, 어떤 인덱스를 쓸지, 어떤 순서로 조인할지, 어떤 알고리즘을 쓸지 같은 가져오는 방법은 엔진의 옵티마이저가 결정합니다. 이 역할 분담이 현대 쿼리의 핵심 개념이며, SQL 밖에서도 유효합니다. 문서 쿼리 API와 GraphQL도 선언형입니다. 명령형 코드는 레코드를 순회하고, 선언형 쿼리는 결과를 기술합니다.
쿼리는 실제로 어떻게 실행됩니까?
네 단계의 파이프라인을 거칩니다. 엔진은 텍스트를 구문 트리로 파싱하고, 스키마에 비추어 검증하고, 추정 비용에 따라 인덱스, 조인 순서, 알고리즘을 골라 계획을 세우고 최적화한 다음, 선택한 계획을 실행해 결과를 스트리밍합니다. 이 계획은 직접 확인할 수 있습니다. EXPLAIN은 옵티마이저가 무엇을 결정했는지 정확히 보여 주며, 모든 성능 디버깅은 여기서 시작합니다.
쿼리는 어떤 요소로 구성됩니까?
다섯 가지 동사가 거의 모든 것을 다룹니다. 필터(어떤 레코드, WHERE), 정렬(어떤 순서, ORDER BY), 프로젝션(어떤 필드, SELECT 목록), 페이지네이션(어디서부터 몇 개, LIMIT/OFFSET 또는 커서), 집계(계산된 요약, GROUP BY)입니다. 모든 쿼리 언어와 SDK가 이 다섯 가지를 똑같이 표현하며, 달라지는 것은 문법뿐입니다.
인덱스는 어떻게 쿼리를 빠르게 만듭니까?
스캔을 탐색(seek)으로 바꿉니다. 일치하는 레코드를 찾으려고 모든 레코드를 읽는 대신(테이블 크기에 선형), 엔진은 정렬된 구조를 따라 곧바로 그 레코드에 도달합니다(로그 시간). 1,000행에서는 차이가 보이지 않지만 1,000만 행에서는 결정적입니다. EXPLAIN을 보면 쿼리가 어느 쪽으로 동작하는지 알 수 있으며, 큰 테이블에서의 순차 스캔은 대표적인 위험 신호입니다.
SQL 인젝션이란 무엇이며, 매개변수화된 쿼리는 이를 어떻게 막습니까?
인젝션은 공격자의 입력이 쿼리의 구조를 바꾸는 것입니다. 따옴표와 주석을 이용한 고전적인 수법은 로그인 검사를 항상 참인 조건으로 만들어 버립니다. 매개변수화된 쿼리는 코드와 데이터를 분리해 이 구멍을 막습니다. 쿼리 텍스트에는 플레이스홀더가 있고, 값은 따로 바인딩되며, 사용자 입력은 언제나 값으로만 취급될 뿐 쿼리 문법으로 파싱되지 않습니다. SDK와 쿼리 빌더는 구조적으로 매개변수화합니다.
오프셋 페이지네이션과 커서 페이지네이션의 차이는 무엇입니까?
오프셋 페이지네이션(N개를 건너뛰고 20개 가져오기)은 단순하고 어느 페이지로든 이동할 수 있지만, 엔진이 건너뛴 행을 세고 버려야 하므로 500페이지가 1페이지보다 비싸고, 동시에 일어나는 쓰기 때문에 페이지 사이에서 결과가 밀릴 수 있습니다. 커서 페이지네이션("이 키 다음부터 20개")은 깊이와 관계없이 빠르고 안정적이지만, 임의의 페이지로 건너뛸 수는 없습니다. 피드에는 커서가 맞고, 작은 관리자 테이블에는 오프셋으로 충분합니다.
SDK와 쿼리 빌더를 쓰면 쿼리를 몰라도 됩니까?
문법 작성을 대신해 줄 뿐, 의미 이해를 대신해 주지는 않습니다. 빌더 체인도 같은 필터-정렬-프로젝션-페이지네이션 구조로 컴파일되고, 같은 인덱스를 타거나 놓칩니다. 대표적인 실패가 N+1 패턴입니다. 루프 안에서 항목마다 쿼리를 하나씩 보내는 것으로, 코드에서는 보이지 않지만 프로덕션에서는 치명적입니다. 쿼리의 구조와 비용 모델은 모든 인터페이스에 그대로 통합니다.