---
title: 쿼리 API
description: HTTPS로 SQL을 제출하고 성공할 때까지 폴링한 다음 결과를 Parquet로 다운로드하거나 인라인 JSON으로 읽습니다.
species: reference
---

# 쿼리 API

읽기 전용 SQL 쿼리를 제출하고 ID로 폴링한 다음 결과를 가져오세요. 기본적으로 서명된 Parquet 다운로드 URL을 반환하며, 더 편리하다면 인라인 JSON 행으로 받을 수 있습니다. 이 페이지의 모든 작업은 `curl`로 실행할 수 있습니다.

모든 요청은 bearer 자격 증명으로 인증합니다. 개인 액세스 토큰(`pat_…`)은 대시보드에서 생성하며 생성 시 한 번만 표시됩니다.

```bash fragment
Authorization: Bearer pat_...
```

토큰은 데이터 접근 권한을 부여하므로 서버 측에 보관하세요. 조직 소유 서비스 키(`nsk_…`)도 같은 방식으로 작동합니다. [인증](/docs/api/authentication)을 참고하세요.

## 수명 주기

쿼리는 비동기로 실행됩니다. SQL을 제출하면 `status: "running"`과 `id`가 포함된 `query` 객체를 즉시 받습니다. `status`가 `succeeded` 또는 `failed`가 될 때까지 1~2초마다 폴링하세요. 오래 실행되는 쿼리는 간격을 늘리세요. 폴링도 요청 빈도 제한에 포함됩니다.

## 쿼리 제출

```endpoint
POST /queries
```

실행 중인 쿼리와 함께 `202`를 반환합니다. `Location` 헤더는 쿼리 URL을 가리킵니다.

| 본문 필드 | 타입 | |
|---|---|---|
| `sql` | string | 필수. 읽기 전용 문 1개입니다. 아래 제한 사항을 참고하세요. |

```bash
curl https://api.supernova.ai/queries \
  -H "Authorization: Bearer pat_..." \
  -H "Content-Type: application/json" \
  -d '{"sql": "select currency, count(*) as n from stripe.customers group by currency"}'
```

```json
{
  "object": "query",
  "id": "qr_...",
  "status": "running",
  "created": 1751731200,
  "sql": "select currency, count(*) as n ...",
  "livemode": true
}
```

`Idempotency-Key` 헤더를 사용하면 안전하게 재시도할 수 있습니다. 같은 키로 다시 요청하면 쿼리를 다시 실행하지 않고 원래 쿼리 객체를 반환하며 `Idempotent-Replayed: true` 헤더를 추가합니다. 키는 처음 실행한 정확한 `sql`에 연결됩니다. 다른 SQL에 재사용하면 `409 idempotency_key_reused`로 거부되므로, 수정한 재시도가 아무 표시 없이 이전 쿼리의 결과를 반환하지 않습니다. 키는 24시간 후 만료됩니다.

## 상태 및 결과 조회

```endpoint
GET /queries/{id}
```

쿼리 상태를 반환하며, 성공하면 결과도 반환합니다.

| 쿼리 매개변수 | 타입 | |
|---|---|---|
| `format` | string | `files`(기본값)는 서명된 Parquet 다운로드 URL을 반환하고, `inline`은 응답에 행을 포함합니다. |
| `limit` | integer | `inline` 전용. 페이지당 행 수로, 기본값은 100, 최댓값은 1000입니다. |
| `starting_after` | string | `inline` 전용. 이전 페이지의 `next_cursor`입니다. |

```bash
curl https://api.supernova.ai/queries/qr_... \
  -H "Authorization: Bearer pat_..."
```

```json
{
  "object": "query",
  "id": "qr_...",
  "status": "succeeded",
  "created": 1751731200,
  "duration_ms": 812,
  "row_count": null,
  "files": [
    { "url": "https://storage.googleapis.com/...", "bytes": 18734, "expires": 1751734800 }
  ]
}
```

`duration_ms`는 실행 전 대기 시간을 제외한 실행 시간입니다. `format=inline`에서는 응답의 행을 페이지로 나누어 반환합니다. `row_count`는 모든 페이지의 합계입니다.

```json
{
  "object": "query",
  "id": "qr_...",
  "status": "succeeded",
  "created": 1751731200,
  "duration_ms": 812,
  "columns": ["currency", "n"],
  "row_count": 3,
  "data": [
    { "currency": "usd", "n": 1230 },
    { "currency": "eur", "n": 88 },
    { "currency": "gbp", "n": 41 }
  ],
  "has_more": false,
  "next_cursor": null
}
```

실패한 쿼리는 결과 대신 구조화된 오류를 포함합니다.

```json
{
  "object": "query",
  "id": "qr_...",
  "status": "failed",
  "created": 1751731200,
  "duration_ms": 64,
  "error": { "message": "Binder Error: Table \"custmers\" does not exist" }
}
```

## 결과 파일

서명된 URL에는 별도 인증이 필요하지 않습니다. URL을 가진 사람은 만료 전까지 누구나 파일을 다운로드할 수 있으므로 데이터 자체처럼 취급하세요. URL은 1시간 동안 유효하며 폴링할 때마다 새로 발급됩니다. 현재 결과는 파일 1개지만 향후 큰 결과가 여러 파일로 나뉠 수 있으므로 `files`를 목록으로 처리하세요. URL은 HTTP 범위 요청을 지원하므로 Parquet 리더가 필요한 부분만 가져올 수 있습니다.

```sql fragment
-- DuckDB, in your own environment
select * from read_parquet('https://storage.googleapis.com/...')
```

```text
# pandas
df = pd.read_parquet("https://storage.googleapis.com/...")
```

파일 전용 응답은 Parquet를 스캔하지 않으므로 `row_count`가 `null`입니다. 정확한 행 수는 Parquet 메타데이터나 인라인 페이지에서 확인할 수 있습니다.

결과를 신속히 가져오세요. 쿼리와 결과는 제출 후 최소 24시간 보존됩니다.

## SQL 제한 사항

문이 2개 이상이면 쿼리를 실행 전에 HTTP 400으로 거부합니다(`multiple_statements`). 읽기 전용이 아닌 쓰기, DDL, 세션 키워드도 거부합니다(`write_not_allowed`). 단, 문자열 리터럴 안에서는 허용되므로 `where type = 'DELETE'`는 작동합니다. `read_parquet`나 `read_csv` 같은 파일 또는 스토리지 함수는 문자열 리터럴 안에 있더라도 거부합니다(`forbidden_function`). 길이가 100,000자를 초과해도 거부합니다(`sql_too_long`). 스토리지 URI(`gs://`, `s3://`, `file://`)는 쿼리 텍스트 어디에 있어도 거부됩니다. 이 URI가 담긴 열을 필터링하려면 스킴 없이 부분 문자열로 일치시키세요. 예: `where path like '%bucket/x%'`.

## 테이블 권한

조직은 사람, 역할, 자격 증명별로 읽을 수 있는 테이블을 정할 수 있습니다. 권한을 설정한 경우, 해당 자격 증명으로 읽을 수 없는 테이블을 지정한 쿼리는 실행 전에 `403`과 테이블 이름을 반환하며 거부됩니다. 같은 테이블은 [카탈로그](/docs/api/catalog)에도 나타나지 않습니다.

```json
{
  "error": {
    "type": "permission_error",
    "code": "grant_table_denied",
    "message": "Table stripe.charges is not available to the API for your organization",
    "doc_url": "https://api.supernova.ai/docs#grant_table_denied",
    "request_id": "req_..."
  }
}
```

## 오류

오류는 해당 HTTP 상태와 JSON 본문을 반환합니다. 모든 응답에는 `Request-Id` 헤더가 있으므로 지원팀에 문의할 때 제공하세요. 특정 입력에 관련된 오류는 `param`으로 해당 입력을 지정하고, `doc_url`은 코드 설명으로 연결합니다.

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "write_not_allowed",
    "message": "Only read-only queries are allowed. ...",
    "doc_url": "https://api.supernova.ai/docs#write_not_allowed",
    "request_id": "req_..."
  }
}
```

이 리소스가 반환하는 코드는 다음과 같습니다.

| 상태 | 코드 | 의미 |
|---|---|---|
| 400 | `parameter_missing` | 필수 매개변수(예: `sql`)가 없음 |
| 400 | `write_not_allowed` | 쿼리에 쓰기, DDL 또는 세션 키워드가 포함됨 |
| 400 | `multiple_statements` | `;`로 구분된 문이 2개 이상임 |
| 400 | `forbidden_function` | 파일 또는 스토리지 함수를 참조함 |
| 400 | `sql_too_long` | `sql`이 100,000자를 초과함 |
| 400 | `invalid_format` | `format`이 `files` 또는 `inline`이 아님 |
| 400 | `invalid_cursor` | `starting_after`가 이전 페이지의 커서가 아님 |
| 403 | `grant_table_denied` | 이 자격 증명으로 읽을 수 없는 테이블을 쿼리에서 지정함 |
| 403 | `service_key_permissions_off` | 유효한 서비스 키이지만 조직에서 테이블 권한을 활성화하지 않음 |
| 403 | `grant_introspection_denied` | 제한된 자격 증명으로 허용되지 않는 테이블 목록 조회를 쿼리에서 수행함 |
| 404 | `query_not_found` | 이 계정에 해당 ID의 쿼리가 없음 |
| 409 | `idempotency_key_reused` | `Idempotency-Key`가 이미 다른 `sql`에 사용됨 |
| 429 | `rate_limited` | 요청 빈도 제한 초과. `Retry-After`를 따르세요. |
| 429 | `too_many_queries` | 계정에서 진행 중인 쿼리가 너무 많음 |

## 요청 빈도 제한

호출자마다 분당 최대 120회, 계정마다 모든 호출자를 합쳐 분당 최대 600회 요청할 수 있습니다. 어느 한도든 초과하면 초 단위 `Retry-After` 헤더와 함께 `429`를 반환합니다. 계정별로 동시에 진행할 수 있는 쿼리 수에도 상한이 있습니다(`too_many_queries`). 권장하는 1~2초 폴링 간격이면 호출자 1명이 여러 쿼리를 동시에 무리 없이 추적할 수 있습니다. 많은 쿼리를 병렬로 실행한다면 폴링 간격을 늘리세요.
