---
term: '데이터베이스 쿼리와 쿼리 언어'
seoTitle: '데이터베이스 쿼리란? 쿼리 언어와 실행 원리'
headline: '데이터베이스 쿼리란?'
slug: database-queries
category: database
shortDefinition: '데이터베이스 쿼리는 필터, 정렬, 프로젝션, 페이지네이션을 엔진이 계획하고 실행할 수 있는 언어로 기술한 구조화된 데이터 요청입니다.'
relatedTerms:
  - database-index
  - n-plus-one-query-problem
  - relational-queries-document-databases
  - graphql-vs-rest
contrastsWith:
  - auto-generated-database-apis
faq:
  - question: '데이터베이스 쿼리를 쉽게 설명하면 무엇입니까?'
    answer: '데이터베이스에 데이터를 조회하거나 변경하도록 요구하는 구조화된 요청입니다. "조회수가 1,000을 넘는 게시된 글을 최신순으로 20개씩 달라"는 식입니다. 쿼리 언어로 작성하거나 SDK로 구성하며, 엔진이 파싱할 수 있도록 엄격한 문법을 따르고, 기술한 바로 그 데이터 조각만 정확히 반환합니다.'
  - question: 'SQL은 선언형입니까, 명령형입니까?'
    answer: '선언형입니다. 원하는 데이터가 무엇인지만 기술하면, 어떤 인덱스를 쓸지, 어떤 순서로 조인할지, 어떤 알고리즘을 쓸지 같은 가져오는 방법은 엔진의 옵티마이저가 결정합니다. 이 역할 분담이 현대 쿼리의 핵심 개념이며, SQL 밖에서도 유효합니다. 문서 쿼리 API와 GraphQL도 선언형입니다. 명령형 코드는 레코드를 순회하고, 선언형 쿼리는 결과를 기술합니다.'
  - question: '쿼리는 실제로 어떻게 실행됩니까?'
    answer: '네 단계의 파이프라인을 거칩니다. 엔진은 텍스트를 구문 트리로 파싱하고, 스키마에 비추어 검증하고, 추정 비용에 따라 인덱스, 조인 순서, 알고리즘을 골라 계획을 세우고 최적화한 다음, 선택한 계획을 실행해 결과를 스트리밍합니다. 이 계획은 직접 확인할 수 있습니다. EXPLAIN은 옵티마이저가 무엇을 결정했는지 정확히 보여 주며, 모든 성능 디버깅은 여기서 시작합니다.'
  - question: '쿼리는 어떤 요소로 구성됩니까?'
    answer: '다섯 가지 동사가 거의 모든 것을 다룹니다. 필터(어떤 레코드, WHERE), 정렬(어떤 순서, ORDER BY), 프로젝션(어떤 필드, SELECT 목록), 페이지네이션(어디서부터 몇 개, LIMIT/OFFSET 또는 커서), 집계(계산된 요약, GROUP BY)입니다. 모든 쿼리 언어와 SDK가 이 다섯 가지를 똑같이 표현하며, 달라지는 것은 문법뿐입니다.'
  - question: '인덱스는 어떻게 쿼리를 빠르게 만듭니까?'
    answer: '스캔을 탐색(seek)으로 바꿉니다. 일치하는 레코드를 찾으려고 모든 레코드를 읽는 대신(테이블 크기에 선형), 엔진은 정렬된 구조를 따라 곧바로 그 레코드에 도달합니다(로그 시간). 1,000행에서는 차이가 보이지 않지만 1,000만 행에서는 결정적입니다. EXPLAIN을 보면 쿼리가 어느 쪽으로 동작하는지 알 수 있으며, 큰 테이블에서의 순차 스캔은 대표적인 위험 신호입니다.'
  - question: 'SQL 인젝션이란 무엇이며, 매개변수화된 쿼리는 이를 어떻게 막습니까?'
    answer: '인젝션은 공격자의 입력이 쿼리의 구조를 바꾸는 것입니다. 따옴표와 주석을 이용한 고전적인 수법은 로그인 검사를 항상 참인 조건으로 만들어 버립니다. 매개변수화된 쿼리는 코드와 데이터를 분리해 이 구멍을 막습니다. 쿼리 텍스트에는 플레이스홀더가 있고, 값은 따로 바인딩되며, 사용자 입력은 언제나 값으로만 취급될 뿐 쿼리 문법으로 파싱되지 않습니다. SDK와 쿼리 빌더는 구조적으로 매개변수화합니다.'
  - question: '오프셋 페이지네이션과 커서 페이지네이션의 차이는 무엇입니까?'
    answer: '오프셋 페이지네이션(N개를 건너뛰고 20개 가져오기)은 단순하고 어느 페이지로든 이동할 수 있지만, 엔진이 건너뛴 행을 세고 버려야 하므로 500페이지가 1페이지보다 비싸고, 동시에 일어나는 쓰기 때문에 페이지 사이에서 결과가 밀릴 수 있습니다. 커서 페이지네이션("이 키 다음부터 20개")은 깊이와 관계없이 빠르고 안정적이지만, 임의의 페이지로 건너뛸 수는 없습니다. 피드에는 커서가 맞고, 작은 관리자 테이블에는 오프셋으로 충분합니다.'
  - question: 'SDK와 쿼리 빌더를 쓰면 쿼리를 몰라도 됩니까?'
    answer: '문법 작성을 대신해 줄 뿐, 의미 이해를 대신해 주지는 않습니다. 빌더 체인도 같은 필터-정렬-프로젝션-페이지네이션 구조로 컴파일되고, 같은 인덱스를 타거나 놓칩니다. 대표적인 실패가 N+1 패턴입니다. 루프 안에서 항목마다 쿼리를 하나씩 보내는 것으로, 코드에서는 보이지 않지만 프로덕션에서는 치명적입니다. 쿼리의 구조와 비용 모델은 모든 인터페이스에 그대로 통합니다.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'Query language (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/Query_language'
  - name: 'PostgreSQL — Using EXPLAIN'
    url: 'https://www.postgresql.org/docs/current/using-explain.html'
  - name: 'GraphQL specification and docs'
    url: 'https://graphql.org/learn/'
  - name: 'SDK query documentation'
    url: 'https://docs.parseplatform.org/js/guide/#queries'
cta:
  title: '쿼리 언어 없이 쓰는 쿼리'
  text: 'Back4app에서는 모든 SDK의 쿼리 빌더가 데이터에 대한 인덱스 기반의 매개변수화된 쿼리로 컴파일됩니다. 다섯 가지 동사는 그대로이고 인젝션 위험은 없으며, 클라이언트가 필요한 것만 정확히 요청하고 싶을 때는 GraphQL도 쓸 수 있습니다.'
  linkText: '무료로 시작하기'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-10-30'
translationKey: database-queries
---

**데이터베이스 쿼리는 필터, 정렬, 프로젝션, 페이지네이션을 엔진이 계획하고 실행할 수 있는 언어로 기술한 구조화된 데이터 요청입니다.** 모든 쿼리 언어의 밑바탕에 있는 핵심 개념은 *선언성*입니다. 결과를 기술하면 경로는 엔진이 고릅니다. 다섯 가지 동사와 비용 모델을 한 번 익혀 두면, SQL, 문서 API, GraphQL, SDK 빌더 같은 모든 문법이 새로운 언어가 아니라 억양의 차이가 됩니다.

## 핵심 요약

| 질문 | 답변 |
| --- | --- |
| 정의 | 정확한 요청: 어떤 레코드, 어떤 순서, 어떤 필드, 몇 개 |
| 패러다임 | 선언형 – *무엇*을 말하면 옵티마이저가 *어떻게*를 결정 |
| 파이프라인 | 파싱 → 검증 → 계획/최적화 → 실행 |
| 비용 모델 | 인덱스 탐색 vs. 전체 스캔 – EXPLAIN이 어느 쪽인지 알려 줌 |
| 보안 규칙 | 항상 매개변수화, 입력을 쿼리에 이어 붙이지 않음 |

## 네 가지 쿼리 언어로 쓴 같은 쿼리

*게시된 글, 조회수 1,000 초과, 최신순*이라는 같은 요청을 언어 계열별로 쓰면 다음과 같습니다:

```sql
-- SQL: 원조 선언형 언어
SELECT title, views FROM articles
WHERE  status = 'published' AND views > 1000
ORDER  BY published_at DESC
LIMIT  20;
```

```javascript
// 문서 쿼리 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:**

```javascript
// 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
// 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();
```

**Swift:**

```swift
// 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) }
}
```

**Kotlin:**

```kotlin
// 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) }
```

## 데이터베이스의 쿼리 실행 과정

```mermaid
flowchart LR
  accTitle: 쿼리 실행 파이프라인
  accDescr: 엔진은 쿼리 텍스트를 구문 트리로 파싱하고, 스키마에 비추어 검증하고, 비용을 기준으로 인덱스와 조인 전략을 골라 계획하고 최적화한 다음, 계획을 실행해 결과를 반환합니다.
  Q["쿼리 텍스트"] --> P["파싱<br/>구문 트리"]
  P --> V["검증<br/>스키마와 대조"]
  V --> O["계획 및 최적화<br/>인덱스 · 조인 순서 · 비용"]
  O --> E["실행<br/>결과 스트리밍"]
```

선언형 쿼리가 작동하는 이유는 옵티마이저에 있습니다. 옵티마이저는 사용 가능한 [인덱스](/glossary/ko/database-index/)를 따져 보고, 행 수를 추정해, 가장 저렴한 계획을 고르며, [EXPLAIN](https://www.postgresql.org/docs/current/using-explain.html)은 그 결정을 보여 줍니다. 한 줄로 요약한 성능 개선 방법은 이렇습니다. EXPLAIN을 실행하고, 전체 스캔을 찾고, 필터가 절실히 필요로 하는 인덱스를 추가하고, 다시 확인합니다.

## 선언형 vs. 명령형

| 구분 | 선언형 쿼리 | 명령형 코드 |
| --- | --- | --- |
| 작성하는 것 | 원하는 결과 | 결과를 계산하는 단계 |
| 최적화 주체 | 엔진, 실행할 때마다 | 개발자, 작성할 때 한 번 |
| 데이터 크기에 적응 | 예 – 통계에 따라 계획이 바뀜 | 아니요 – 루프는 그냥 루프 |
| 사용되는 곳 | SQL, 문서 API, GraphQL, 빌더 | 레코드를 순회하는 애플리케이션 루프 |
| 실패 징후 | 잘못된 실행 계획(인덱스/힌트로 해결) | N+1 루프, 메모리 폭증 |

이 모두에 공통으로 적용되는 구조는 다음과 같습니다. **필터**(`WHERE`), **정렬**(`ORDER BY`), **프로젝션**(SELECT 목록 – 필요한 것만 요청), **페이지네이션**(`LIMIT`과 오프셋 또는 커서), **집계**(`GROUP BY` 등)입니다. 다섯 가지 동사가 모든 인터페이스에 통합니다.

## 쿼리 장애 대부분을 막는 두 가지 규칙

**항상 매개변수화합니다.** 인젝션은 공격자의 입력이 쿼리 *구조*가 되는 것이며, 입력을 값으로 바인딩하는 플레이스홀더로 완전히 해결됩니다:

```javascript
// 취약: 입력이 쿼리 구조에 그대로 이어 붙여짐
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](/glossary/ko/auto-generated-database-apis/)는 URL 매개변수나 GraphQL 선택 집합을 바로 이런 쿼리로 변환합니다. 쿼리의 구조는 모든 추상화를 뚫고 드러납니다.
- **실시간 필터.** 라이브 구독은 상시 실행되는 쿼리입니다. 같은 조건식이 계속해서 평가됩니다.
- **프로덕션 디버깅.** EXPLAIN과 슬로 쿼리 로그는 "갑자기 느려졌다" 유형의 장애를 진단하는 반복 과정입니다.

## 원시 쿼리 언어, 빌더, SDK 중 무엇을 선택해야 할까? 결정 매트릭스

| 선택… | 이런 경우… | 주의할 점… |
| --- | --- | --- |
| 원시 쿼리 언어 | 복잡한 집계, 리포트, 마이그레이션 | 문자열을 이어 붙이면 인젝션, 이식성 |
| 쿼리 빌더 / SDK | 애플리케이션 CRUD와 목록 – 대부분의 코드 | N+1 루프, 빌더가 실행 계획을 가림 |
| GraphQL | 클라이언트가 필드를 고르고 관계를 중첩해야 함 | 제한이 없으면 쿼리 비용이 끝없이 커짐 |
| 저장 프로시저/서버 측 로직 | 데이터 가까이에서 처리하는 다단계 작업 | 코드베이스에서 보이지 않는 로직 |

[추상화 계층 논의](/glossary/ko/database-abstraction-layer/)에서 나온 현실적인 규칙이 그대로 적용됩니다. 일상적인 90%에는 빌더를, 제어가 그만한 가치를 하는 곳에는 원시 쿼리를 씁니다. 어느 쪽이든 쿼리 구조에 대한 지식은 그대로 옮겨 갑니다.

## 한계와 트레이드오프

- **선언형은 공짜가 아닙니다.** 옵티마이저의 판단은 통계와 인덱스 수준을 넘어서지 못합니다. 올바른 쿼리라도 인덱스가 잘못되면 여전히 느립니다.
- **쿼리는 비용을 숨깁니다.** 빌더 체인 한 줄이 수백만 행의 스캔일 수 있습니다. EXPLAIN만이 정직한 거울입니다.
- **N+1 함정은 쿼리 위에 있습니다.** 개별적으로는 완벽한 쿼리도 루프 안에서 실행되면 전체적으로는 심각한 성능 문제를 일으킵니다. 배칭과 include는 바로 이 때문에 존재합니다.
- **깊은 페이지네이션은 성능이 떨어집니다.** 오프셋 깊이는 선형 비용입니다. 피드는 처음부터 커서를 중심으로 설계해야 합니다.
- **언어 난립은 현실입니다.** SQL, 문서 API, GraphQL, 검색 DSL마다 팀이 치러야 할 비용이 있습니다. 공통된 쿼리 구조가 그 해독제입니다.

## Back4app에서의 쿼리

Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. Back4app의 쿼리 전략은 SDK 빌더 경로를 철저하게 구현한 것입니다. 모든 SDK의 [쿼리 빌더](https://docs.parseplatform.org/js/guide/#queries)(위의 코드 탭)는 필터, 정렬, 프로젝션, 페이지네이션을 인젝션 위험이 없는 매개변수화된 쿼리로 컴파일하고, `include()`는 N+1 루프 없이 관계를 처리하며, 같은 조건식이 실시간 업데이트를 위한 [라이브 쿼리](https://www.back4app.com)를 구동하고, GraphQL은 응답 형태를 직접 선언하려는 클라이언트를 지원합니다. 다섯 가지 동사, 모든 인터페이스, 하나의 백엔드입니다.
