> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# limit & offset

> 페이지네이션과 결과 제한

`limit`과 `offset`은 쿼리 결과의 개수를 제한하고 페이지네이션을 구현하는 메서드입니다.

## limit

조회할 레코드 수를 제한합니다.

### 기본 사용법

```typescript theme={null}
// 상위 10개만
const users = await puri
  .table("users")
  .select({ id: "users.id", name: "users.name" })
  .orderBy("users.created_at", "desc")
  .limit(10);

// LIMIT 10
```

### 최신 N개

```typescript theme={null}
// 최신 게시물 20개
const recentPosts = await puri
  .table("posts")
  .select({
    id: "posts.id",
    title: "posts.title",
    created_at: "posts.created_at",
  })
  .orderBy("posts.created_at", "desc")
  .limit(20);
```

## offset

건너뛸 레코드 수를 지정합니다.

### 기본 사용법

```typescript theme={null}
// 처음 10개를 건너뛰고 그 다음 10개
const users = await puri
  .table("users")
  .select({ id: "users.id", name: "users.name" })
  .orderBy("users.id", "asc")
  .limit(10)
  .offset(10); // 11~20번째 레코드

// LIMIT 10 OFFSET 10
```

## 페이지네이션

`limit`과 `offset`을 조합하여 페이지네이션을 구현합니다.

### 기본 페이지네이션

```typescript theme={null}
function getPage(page: number, pageSize: number = 20) {
  return puri
    .table("posts")
    .select({
      id: "posts.id",
      title: "posts.title",
      created_at: "posts.created_at",
    })
    .orderBy("posts.created_at", "desc")
    .limit(pageSize)
    .offset((page - 1) * pageSize);
}

// 1페이지 (1~20)
const page1 = await getPage(1); // offset: 0, limit: 20

// 2페이지 (21~40)
const page2 = await getPage(2); // offset: 20, limit: 20

// 3페이지 (41~60)
const page3 = await getPage(3); // offset: 40, limit: 20
```

### 총 개수와 함께

```typescript theme={null}
async function getPageWithTotal(page: number, pageSize: number = 20) {
  // 총 개수
  const [{ total }] = await puri
    .table("posts")
    .select({ total: Puri.count() })
    .where("posts.published", true);

  // 페이지 데이터
  const posts = await puri
    .table("posts")
    .select({
      id: "posts.id",
      title: "posts.title",
    })
    .where("posts.published", true)
    .orderBy("posts.created_at", "desc")
    .limit(pageSize)
    .offset((page - 1) * pageSize);

  return {
    data: posts,
    pagination: {
      page,
      pageSize,
      total,
      totalPages: Math.ceil(total / pageSize),
    },
  };
}
```

## 실전 예시

<Tabs>
  <Tab title="무한 스크롤" icon="arrows-down-to-line">
    ```typescript theme={null}
    async function getInfiniteScroll(cursor: number = 0, limit: number = 20) {
      const posts = await puri.table("posts")
        .select({
          id: "posts.id",
          title: "posts.title",
          created_at: "posts.created_at"
        })
        .where("posts.published", true)
        .where("posts.id", ">", cursor)  // 커서 이후
        .orderBy("posts.id", "asc")
        .limit(limit);

      return {
        data: posts,
        nextCursor: posts.length > 0 ? posts[posts.length - 1].id : null,
        hasMore: posts.length === limit
      };
    }

    // 사용 예시
    const result1 = await getInfiniteScroll(0, 20);     // 첫 페이지
    const result2 = await getInfiniteScroll(result1.nextCursor, 20);  // 다음 페이지
    ```
  </Tab>

  <Tab title="데이터 테이블" icon="table">
    ```typescript theme={null}
    interface TableParams {
      page: number;
      pageSize: number;
      sortBy?: string;
      sortOrder?: "asc" | "desc";
      filters?: Record<string, any>;
    }

    async function getTableData(params: TableParams) {
      const { page, pageSize, sortBy = "id", sortOrder = "desc", filters = {} } = params;

      // 기본 쿼리
      let query = puri.table("users")
        .select({
          id: "users.id",
          name: "users.name",
          email: "users.email",
          status: "users.status",
          created_at: "users.created_at"
        });

      // 필터 적용
      if (filters.status) {
        query = query.where("users.status", filters.status);
      }
      if (filters.search) {
        query = query.where("users.name", "like", `%${filters.search}%`);
      }

      // 총 개수
      const countQuery = query.clone();
      const [{ total }] = await countQuery
        .clear("select")
        .select({ total: Puri.count() });

      // 정렬 및 페이지네이션
      const data = await query
        .orderBy(`users.${sortBy}` as any, sortOrder)
        .limit(pageSize)
        .offset((page - 1) * pageSize);

      return {
        data,
        pagination: {
          page,
          pageSize,
          total,
          totalPages: Math.ceil(total / pageSize)
        }
      };
    }
    ```
  </Tab>

  <Tab title="Top N 쿼리" icon="ranking-star">
    ```typescript theme={null}
    // 상위 10개 상품
    async function getTopProducts(limit: number = 10) {
      return puri.table("products")
        .select({
          id: "products.id",
          name: "products.name",
          sales: "products.sales",
          rating: "products.rating"
        })
        .where("products.published", true)
        .orderBy("products.sales", "desc")
        .limit(limit);
    }

    // 최근 활동 사용자
    async function getActiveUsers(days: number = 7, limit: number = 20) {
      const since = new Date();
      since.setDate(since.getDate() - days);

      return puri.table("users")
        .select({
          id: "users.id",
          name: "users.name",
          last_login: "users.last_login"
        })
        .where("users.last_login", ">=", since)
        .orderBy("users.last_login", "desc")
        .limit(limit);
    }
    ```
  </Tab>

  <Tab title="배치 처리" icon="layer-group">
    ```typescript theme={null}
    async function processBatch<T>(
      query: Puri<any, any, T>,
      batchSize: number = 100,
      callback: (batch: T[]) => Promise<void>
    ) {
      let offset = 0;
      let hasMore = true;

      while (hasMore) {
        // 배치 조회
        const batch = await query
          .clone()
          .limit(batchSize)
          .offset(offset);

        if (batch.length === 0) {
          hasMore = false;
          break;
        }

        // 배치 처리
        await callback(batch);

        offset += batchSize;
        hasMore = batch.length === batchSize;
      }
    }

    // 사용 예시
    await processBatch(
      puri.table("users").select({ id: "users.id", email: "users.email" }),
      500,
      async (users) => {
        // 500개씩 처리
        await sendEmailBatch(users);
      }
    );
    ```
  </Tab>
</Tabs>

## limit(0)의 의미

`limit(0)`은 결과를 반환하지 않습니다.

```typescript theme={null}
// 0개 반환 (빈 배열)
const users = await puri.table("users").limit(0);

// []
```

<Warning>
  일부 데이터베이스에서 `limit(0)`은 구조 확인용으로 사용되지만, Puri에서는 빈 배열을 반환합니다.
</Warning>

## 성능 최적화

### 1. 인덱스 활용

```typescript theme={null}
// ✅ 좋음: 정렬 컬럼에 인덱스
await puri
  .table("posts")
  .orderBy("posts.created_at", "desc") // created_at 인덱스 필요
  .limit(20);

// ❌ 나쁨: 인덱스 없는 컬럼 정렬
await puri
  .table("posts")
  .orderBy("posts.content", "desc") // 인덱스 없음
  .limit(20);
```

### 2. Offset 크기

```typescript theme={null}
// ⚠️ 주의: 큰 offset은 느림
await puri.table("posts").limit(20).offset(100000); // 10만 개를 건너뛰고 20개 조회 (느림)

// ✅ 좋음: 커서 기반 페이지네이션
await puri.table("posts").where("posts.id", ">", lastId).limit(20);
```

### 3. COUNT(\*) 최적화

```typescript theme={null}
// ❌ 나쁨: 매번 전체 카운트
const [{ total }] = await puri.table("posts").select({ total: Puri.count() });

// ✅ 좋음: 캐싱
const cachedTotal = await cache.get("posts:total");
if (!cachedTotal) {
  const [{ total }] = await puri.table("posts").select({ total: Puri.count() });
  await cache.set("posts:total", total, 300); // 5분 캐시
}
```

## 커서 기반 페이지네이션

대용량 데이터에는 offset 대신 커서를 사용하세요.

### Offset 기반 (느림)

```typescript theme={null}
// ❌ 큰 offset은 성능 저하
async function getPage(page: number, pageSize: number = 20) {
  return puri
    .table("posts")
    .select({ id: "posts.id", title: "posts.title" })
    .orderBy("posts.id", "desc")
    .limit(pageSize)
    .offset((page - 1) * pageSize); // 페이지가 커질수록 느려짐
}

await getPage(1000, 20); // offset: 19980 (매우 느림)
```

### 커서 기반 (빠름)

```typescript theme={null}
// ✅ 커서 사용 (항상 빠름)
async function getNextPage(cursor: number | null, pageSize: number = 20) {
  let query = puri
    .table("posts")
    .select({ id: "posts.id", title: "posts.title" })
    .orderBy("posts.id", "desc")
    .limit(pageSize);

  if (cursor !== null) {
    query = query.where("posts.id", "<", cursor);
  }

  const posts = await query;

  return {
    data: posts,
    nextCursor: posts.length > 0 ? posts[posts.length - 1].id : null,
    hasMore: posts.length === pageSize,
  };
}

// 항상 빠름 (인덱스 사용)
const page1 = await getNextPage(null, 20);
const page2 = await getNextPage(page1.nextCursor, 20);
const page100 = await getNextPage(page99.nextCursor, 20); // 여전히 빠름
```

## 주의사항

### 1. ORDER BY 필수

```typescript theme={null}
// ❌ 위험: 정렬 없이 limit (순서 불확실)
await puri.table("users").limit(10);

// ✅ 올바름: 정렬 후 limit
await puri.table("users").orderBy("users.id", "asc").limit(10);
```

### 2. offset은 0부터

```typescript theme={null}
// ✅ 올바름
.offset(0)   // 첫 번째 레코드부터
.offset(10)  // 11번째 레코드부터

// ❌ 에러: 음수 불가
.offset(-1)  // Error: Invalid offset: must be >= 0
```

### 3. limit도 0 이상

```typescript theme={null}
// ✅ 올바름
.limit(0)    // 빈 배열
.limit(10)   // 10개

// ❌ 에러: 음수 불가
.limit(-1)   // Error: Invalid limit: must be >= 0
```

### 4. 일관된 정렬

```typescript theme={null}
// ⚠️ 주의: 정렬이 일관되지 않으면 중복/누락 가능
await puri
  .table("posts")
  .orderBy("posts.created_at", "desc") // 같은 시간 여러 개 있을 수 있음
  .limit(20)
  .offset(20); // 중복 또는 누락 가능

// ✅ 좋음: 고유 컬럼 추가
await puri
  .table("posts")
  .orderBy("posts.created_at", "desc")
  .orderBy("posts.id", "desc") // 동일 시간은 ID로 구분
  .limit(20)
  .offset(20);
```

## 타입 안정성

```typescript theme={null}
// ✅ 올바른 사용
await puri.table("users").limit(10).offset(0);

// ❌ 타입 에러: 음수
await puri.table("users").limit(-10); // Error: Invalid limit

// ❌ 타입 에러: 음수
await puri.table("users").offset(-5); // Error: Invalid offset
```

## 다음 단계

<CardGroup cols={2}>
  <Card title="order-by" icon="arrow-down-1-9" href="/ko/api-reference/puri-methods/order-by">
    결과 정렬하기
  </Card>

  <Card title="where" icon="filter" href="/ko/api-reference/puri-methods/where">
    조건 필터링하기
  </Card>

  <Card title="advanced-methods" icon="wand-magic-sparkles" href="/ko/api-reference/puri-methods/advanced-methods">
    고급 쿼리 메서드
  </Card>

  <Card title="Subset" icon="layer-group" href="/ko/database/subsets/what-are-subsets">
    서브셋으로 관계 데이터 로딩
  </Card>
</CardGroup>
