> ## 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.

# select

> SELECT 절로 조회할 필드 지정

`select`는 조회할 필드를 지정하는 메서드입니다. 타입 안전하게 필드를 선택하고, SQL 집계 함수 및 표현식을 사용할 수 있습니다.

## 기본 사용법

### 전체 필드 조회

```typescript theme={null}
const users = await puri.table("users").selectAll();

// SELECT * FROM users
```

### 특정 필드 선택

```typescript theme={null}
const users = await puri.table("users").select({
  id: "users.id",
  email: "users.email",
  name: "users.name",
});

// SELECT users.id, users.email, users.name FROM users
```

### 필드 별칭 (Alias)

```typescript theme={null}
const users = await puri.table("users").select({
  userId: "users.id",
  userName: "users.name",
});

// SELECT users.id as userId, users.name as userName FROM users
```

## SQL 집계 함수

Puri는 타입 안전한 SQL 함수들을 제공합니다.

### COUNT

```typescript theme={null}
const result = await puri
  .table("users")
  .select({
    total: Puri.count(), // COUNT(*)
  })
  .first();

console.log(result.total); // number

// 특정 컬럼 카운트
const result = await puri
  .table("posts")
  .select({
    published: Puri.count("posts.published"),
  })
  .first();
```

### SUM, AVG, MAX, MIN

```typescript theme={null}
const stats = await puri
  .table("orders")
  .select({
    total: Puri.sum("orders.amount"),
    average: Puri.avg("orders.amount"),
    max: Puri.max("orders.amount"),
    min: Puri.min("orders.amount"),
  })
  .first();

console.log(stats.total); // number
console.log(stats.average); // number
```

### 문자열 함수

```typescript theme={null}
const users = await puri.table("users").select({
  id: "users.id",
  full_name: Puri.concat("users.first_name", "' '", "users.last_name"),
  email_upper: Puri.upper("users.email"),
  email_lower: Puri.lower("users.email"),
});
```

## Raw SQL 표현식

복잡한 SQL 표현식을 사용할 수 있습니다.

### 타입별 Raw 함수

```typescript theme={null}
const result = await puri.table("users").select({
  id: "users.id",

  // 문자열 반환
  display_name: Puri.rawString("CONCAT(users.first_name, ' ', users.last_name)"),

  // 숫자 반환
  age: Puri.rawNumber("YEAR(CURRENT_DATE) - YEAR(users.birth_date)"),

  // 불리언 반환
  is_adult: Puri.rawBoolean("YEAR(CURRENT_DATE) - YEAR(users.birth_date) >= 18"),

  // 날짜 반환
  signup_date: Puri.rawDate("DATE(users.created_at)"),

  // 문자열 배열 반환
  tags: Puri.rawStringArray("ARRAY_AGG(tags.name)"),
});

// 타입이 자동으로 추론됨
result[0].display_name; // string
result[0].age; // number
result[0].is_adult; // boolean
result[0].signup_date; // Date
result[0].tags; // string[]
```

## 중첩 객체 선택

중첩 객체 구조로 SELECT를 작성할 수 있습니다.

```typescript theme={null}
const users = await puri
  .table("users")
  .leftJoin("posts", "users.id", "posts.user_id")
  .select({
    id: "users.id",
    email: "users.email",
    post: {
      id: "posts.id",
      title: "posts.title",
    },
  });

// 자동으로 flat하게 변환됨:
// SELECT
//   users.id,
//   users.email,
//   posts.id as post__id,
//   posts.title as post__title
// FROM users LEFT JOIN posts ...

// 결과는 hydrate되어 중첩 객체로 반환
console.log(users[0].post.title);
```

## appendSelect

기존 SELECT에 필드를 추가합니다.

```typescript theme={null}
let query = puri.table("users").select({
  id: "users.id",
  email: "users.email",
});

// 필드 추가
query = query.appendSelect({
  name: "users.name",
  created_at: "users.created_at",
});

// SELECT users.id, users.email, users.name, users.created_at FROM users
```

<Warning>`select()`는 기존 SELECT를 **덮어쓰고**, `appendSelect()`는 **추가**합니다.</Warning>

## Full-Text Search 함수

### ts\_highlight (PostgreSQL)

PostgreSQL의 tsvector를 사용한 하이라이팅입니다.

```typescript theme={null}
const posts = await puri
  .table("posts")
  .whereTsSearch("posts.content_tsv", "typescript")
  .select({
    id: "posts.id",
    title: Puri.tsHighlight("posts.title", "typescript"),
    content: Puri.tsHighlight("posts.content", "typescript", {
      startSel: "<mark>",
      stopSel: "</mark>",
      maxFragments: 3,
      maxWords: 50,
    }),
  });
```

**옵션:**

* `parser`: 쿼리 파서 (기본: `"websearch_to_tsquery"`)
* `config`: 텍스트 검색 설정 (기본: `"simple"`)
* `startSel`: 시작 태그 (기본: `"<b>"`)
* `stopSel`: 종료 태그 (기본: `"</b>"`)
* `maxWords`: 최대 단어 수
* `minWords`: 최소 단어 수
* `maxFragments`: 최대 조각 수

### ts\_rank (PostgreSQL)

검색 관련도 점수를 계산합니다.

```typescript theme={null}
const posts = await puri
  .table("posts")
  .whereTsSearch("posts.content_tsv", "typescript")
  .select({
    id: "posts.id",
    title: "posts.title",

    // 기본 rank
    rank: Puri.tsRank("posts.content_tsv", "typescript"),

    // 가중치 적용
    weighted_rank: Puri.tsRank("posts.content_tsv", "typescript", {
      weights: [0.1, 0.2, 0.4, 1.0], // D, C, B, A 가중치
      normalization: 1, // 길이 정규화
    }),

    // Cover Density rank
    rank_cd: Puri.tsRankCd("posts.content_tsv", "typescript"),
  })
  .orderBy("rank", "desc");
```

**옵션:**

* `parser`: 쿼리 파서 (기본: `"websearch_to_tsquery"`)
* `config`: 텍스트 검색 설정 (기본: `"simple"`)
* `weights`: \[D, C, B, A] 가중치 배열
* `normalization`: 정규화 옵션 (0-32)

### PGroonga 함수

PGroonga 확장을 사용한 전문 검색입니다.

```typescript theme={null}
const posts = await puri
  .table("posts")
  .whereSearch("posts.title", "타입스크립트")
  .select({
    id: "posts.id",

    // 단일 컬럼 하이라이팅
    title: Puri.highlight("posts.title", "타입스크립트"),

    // 여러 검색어
    content: Puri.highlight("posts.content", ["타입스크립트", "자바스크립트"]),

    // 여러 컬럼 하이라이팅 (배열 반환)
    highlights: Puri.highlight(["posts.title", "posts.content"], ["타입스크립트", "Node.js"]),

    // 검색 점수
    score: Puri.score(),
  })
  .orderBy("score", "desc");

// 타입 자동 추론
console.log(posts[0].title); // string
console.log(posts[0].highlights); // string[]
console.log(posts[0].score); // number
```

### pg\_trgm 유사도 함수

PostgreSQL의 `pg_trgm` 확장이 제공하는 유사도 점수를 SELECT에서 계산합니다. `whereFuzzy`로 후보를 필터링한 뒤, 이 함수들로 컬럼별 가중치 점수를 매길 수 있습니다.

```typescript theme={null}
const results = await puri
  .table("items")
  .whereFuzzy("items.search_text", query)
  .select({
    id: "items.id",
    title_ko: "items.title_ko",
    title_en: "items.title_en",

    // word similarity — 쿼리가 컬럼의 부분 문자열과 얼마나 유사한지 (0~1)
    title_score: Puri.wordSimilarity("items.title_ko", query),

    // similarity — 두 문자열 전체의 유사도 (0~1)
    full_score: Puri.similarity("items.title_ko", query),

    // strict word similarity — word similarity보다 엄격한 부분 문자열 매칭 (0~1)
    strict_score: Puri.strictWordSimilarity("items.title_ko", query),
  })
  .orderBy("title_score", "desc");

// 타입 자동 추론
results[0].title_score; // number
results[0].full_score; // number
results[0].strict_score; // number
```

**가중치 점수 계산 예시:**

유사도 함수의 반환값(`SqlExpression<"number">`)을 `rawNumber`와 조합하여 컬럼별 가중치 점수를 계산할 수 있습니다.

```typescript theme={null}
const results = await puri
  .table("items")
  .whereFuzzy("items.search_text", query)
  .select({
    id: "items.id",
    title_ko: "items.title_ko",

    // 개별 유사도 점수 (가중치 계산에 활용)
    ko_score: Puri.wordSimilarity("items.title_ko", query),
    en_score: Puri.wordSimilarity("items.title_en", query),
  })
  .orderBy("ko_score", "desc");
```

**함수별 비교:**

| 함수                                         | PostgreSQL 함수                           | 용도                 |
| ------------------------------------------ | --------------------------------------- | ------------------ |
| `Puri.wordSimilarity(column, query)`       | `word_similarity(query, column)`        | 쿼리와 컬럼의 부분 문자열 유사도 |
| `Puri.similarity(column, query)`           | `similarity(column, query)`             | 전체 문자열 유사도         |
| `Puri.strictWordSimilarity(column, query)` | `strict_word_similarity(query, column)` | 엄격한 부분 문자열 유사도     |

<Info>
  이 함수들은 `SqlExpression<"number">`를 반환하므로 `select` 내에서 사용할 수 있습니다. `pg_trgm` 확장이 설치되어 있어야 합니다.
</Info>

## 실전 예시

<Tabs>
  <Tab title="사용자 통계" icon="chart-bar">
    ```typescript theme={null}
    const stats = await puri.table("users")
      .select({
        total_users: Puri.count(),
        active_users: Puri.rawNumber(
          "SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END)"
        ),
        avg_age: Puri.avg("users.age"),
        oldest: Puri.max("users.age"),
        youngest: Puri.min("users.age")
      })
      .first();

    console.log(stats);
    // {
    //   total_users: 1000,
    //   active_users: 850,
    //   avg_age: 32.5,
    //   oldest: 65,
    //   youngest: 18
    // }
    ```
  </Tab>

  <Tab title="주문 요약" icon="cart-shopping">
    ```typescript theme={null}
    const orderSummary = await puri.table("orders")
      .where("orders.status", "completed")
      .select({
        order_count: Puri.count(),
        total_amount: Puri.sum("orders.amount"),
        avg_amount: Puri.avg("orders.amount"),
        max_amount: Puri.max("orders.amount"),
        
        // 날짜 형식 변환
        first_order: Puri.rawDate("MIN(orders.created_at)"),
        last_order: Puri.rawDate("MAX(orders.created_at)")
      })
      .first();
    ```
  </Tab>

  <Tab title="사용자 + 게시물" icon="user-pen">
    ```typescript theme={null}
    const users = await puri.table("users")
      .leftJoin("posts", "users.id", "posts.user_id")
      .select({
        // 사용자 정보
        id: "users.id",
        name: "users.name",
        email: "users.email",
        
        // 게시물 정보 (중첩)
        post: {
          id: "posts.id",
          title: "posts.title",
          views: "posts.views"
        },
        
        // 계산 필드
        full_info: Puri.concat(
          "users.name",
          "' ('",
          "users.email",
          "')'"
        )
      });

    // 중첩 객체로 반환
    console.log(users[0].post.title);
    ```
  </Tab>

  <Tab title="검색 + 하이라이팅" icon="magnifying-glass">
    ```typescript theme={null}
    const searchResults = await puri.table("posts")
      .whereTsSearch("posts.content_tsv", "typescript programming")
      .select({
        id: "posts.id",
        
        // 제목 하이라이팅
        title: Puri.tsHighlight("posts.title", "typescript programming", {
          startSel: "<mark>",
          stopSel: "</mark>"
        }),
        
        // 내용 발췌 + 하이라이팅
        excerpt: Puri.tsHighlight("posts.content", "typescript programming", {
          startSel: "<mark>",
          stopSel: "</mark>",
          maxFragments: 2,
          maxWords: 30
        }),
        
        // 관련도 점수
        rank: Puri.tsRank("posts.content_tsv", "typescript programming", {
          weights: [0.1, 0.2, 0.4, 1.0]
        })
      })
      .orderBy("rank", "desc")
      .limit(20);

    // 하이라이팅된 HTML 반환
    console.log(searchResults[0].title);
    // "Introduction to <mark>TypeScript</mark> <mark>Programming</mark>"
    ```
  </Tab>
</Tabs>

## 타입 안정성

SELECT의 반환 타입은 자동으로 추론됩니다.

```typescript theme={null}
const result = await puri
  .table("users")
  .select({
    id: "users.id",
    email: "users.email",
    count: Puri.count(),
    upper_name: Puri.upper("users.name"),
  })
  .first();

// 타입 자동 추론
result.id; // number
result.email; // string
result.count; // number
result.upper_name; // string

result.unknown; // ❌ 타입 에러
```

## 주의사항

### 1. select vs appendSelect

```typescript theme={null}
// ❌ 나쁜 예: 이전 select가 덮어씌워짐
query.select({ id: "users.id" }).select({ name: "users.name" }); // id가 사라짐!

// ✅ 좋은 예: appendSelect로 추가
query.select({ id: "users.id" }).appendSelect({ name: "users.name" });
```

### 2. 테이블 접두사 필수

```typescript theme={null}
// ❌ 잘못됨
puri.table("users").select({
  id: "id", // 어떤 테이블의 id인지 불명확
});

// ✅ 올바름
puri.table("users").select({
  id: "users.id", // 명확한 테이블 지정
});
```

### 3. Raw SQL 타입 지정

```typescript theme={null}
// Raw SQL 사용 시 반환 타입 명시
const result = await puri.table("users").select({
  // ✅ 타입 명시
  age: Puri.rawNumber("YEAR(CURRENT_DATE) - YEAR(birth_date)"),

  // ❌ 타입 없음 (any)
  unknown: puri.raw("SOME_COMPLEX_SQL"),
});
```

## 다음 단계

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

  <Card title="join" icon="link" href="/ko/api-reference/puri-methods/join">
    테이블 조인하기
  </Card>

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

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