2025년 5월 22일·4분 읽기

관리자 필터를 빠르게 만드는 PostgreSQL 생성 열

PostgreSQL 생성 열이 관리자 화면의 필터와 정렬을 어떻게 빠르게 만들고 SQL을 읽기 쉽게 유지하는지, 실용 예시와 점검표로 알아보세요.

관리자 필터를 빠르게 만드는 PostgreSQL 생성 열

관리자 화면이 빨리 느려지고 지저분해지는 이유

관리자 화면은 보통 단순하게 시작합니다: 테이블 하나, 필터 몇 개, 아마도 "최신순" 정렬 하나. 그러다 실제 요구가 쌓입니다. 지원팀은 이름, 이메일, 전화로 고객을 찾는 검색을 원합니다. 영업팀은 "마지막 활동"으로 정렬하기를 원합니다. 재무는 "연체 잔액"을 원합니다. 각 요청은 조건, 조인, 추가 계산을 더합니다.

대부분의 관리자 목록이 느려지는 이유는 동일합니다: 클릭할 때마다 쿼리가 바뀝니다. 필터링과 정렬은 데이터베이스가 많은 행을 스캔하게 만들 수 있는데, 특히 쿼리가 각 행에 대해 값을 계산한 뒤에야 어떤 행이 일치하는지 결정할 수 있을 때 그렇습니다.

전형적인 전환점은 WHEREORDER BY에 표현식이 가득 차기 시작할 때입니다. 단순 컬럼 대신 lower(email), date_trunc('day', last_seen_at), 여러 상태를 하나의 "버킷"으로 매핑하는 CASE 문 등으로 필터링합니다. 이런 표현식은 단지 느릴 뿐 아니라 SQL을 읽기 어렵게 만들고, 인덱싱을 복잡하게 하며, 실수하기 쉬워집니다.

지저분한 관리자 SQL은 보통 몇 가지 반복 패턴에서 나옵니다:

  • 여러 필드를 서로 다른 규칙으로 검사하는 하나의 "검색" 입력
  • 파생 값(전체 이름, 우선순위 점수, "마지막 의미 있는 이벤트")으로 정렬
  • 화면 전반에 복사된 비즈니스 규칙(활성 vs 비활성, 결제 완료 vs 연체)
  • 여기저기 뿌려진 작은 "헬퍼" 조정들(trim, lower, coalesce)
  • 목록, 필터, 정렬에서 동일한 계산 값이 반복 사용됨

팀들은 종종 앱 레이어에 숨기려 합니다: 동적 쿼리 빌더, 조건부 조인, 코드에서 미리 계산하기 등. 그 방법이 통할 수 있지만 UI와 데이터베이스에 로직이 분산되어 디버깅이 어려워지고 느린 쿼리를 추적하기 힘들어집니다.

목표는 간단합니다: 읽기 쉽고 빠른 쿼리. 계산된 값이 관리자 화면 전반에 반복된다면 PostgreSQL 생성 열이 규칙을 한곳에 유지하면서 데이터베이스가 최적화할 수 있게 해줍니다.

생성 열을 쉽게 이해하기

생성 열은 다른 컬럼으로부터 값이 계산되는 일반 테이블 컬럼입니다. 값을 직접 쓰지 않습니다. PostgreSQL이 당신이 정의한 표현식으로 값을 채웁니다.

PostgreSQL의 생성 열은 저장(STORED)됩니다. 행이 삽입되거나 업데이트될 때 PostgreSQL이 값을 계산하고 다른 컬럼처럼 디스크에 저장합니다. 관리자 화면에서는 보통 이 방식이 적합합니다: 읽기가 빠르고 계산된 값을 인덱싱할 수 있습니다.

이는 같은 계산을 모든 쿼리 안에서 반복하는 것과 다릅니다. 계속 WHERE lower(email) = lower($1)을 쓰거나 last_name || ', ' || first_name으로 정렬하면 비용을 반복해서 지불하고 SQL이 시끄러워집니다. 생성 열은 그 반복 계산을 테이블 정의로 옮깁니다. 쿼리는 더 단순해지고, 결과는 어디서나 일관됩니다.

원본 데이터가 바뀌면 PostgreSQL이 해당 행의 생성 값을 자동으로 업데이트합니다. 애플리케이션이 동기화를 기억할 필요가 없습니다.

유용한 사고 모델:

  • 공식을 한 번 정의하세요.
  • PostgreSQL이 쓰기 시 계산합니다.
  • 쿼리는 일반 컬럼처럼 읽습니다.
  • 저장되기 때문에 인덱스할 수 있습니다.

나중에 공식을 바꾸면 스키마 변경이 필요합니다. 기존 행은 새 표현식에 맞게 업데이트되므로 마이그레이션처럼 계획하세요.

필터와 정렬에서 계산된 필드의 좋은 사용 사례

생성 열은 값이 항상 다른 컬럼에서 유도되고 그 값을 자주 필터하거나 정렬할 때 빛납니다. 일회성 리포트에는 덜 유용합니다.

실제로 사람들이 사용하는 검색 친화적 필드

관리자 검색은 순수한 검색인 경우가 드뭅니다. 사람들은 검색창이 지저분한 텍스트, 대소문자 불일치, 추가 공백을 다루길 기대합니다. 정규화된 "검색 키"를 생성해두면 WHERE 절이 읽기 쉬워지고 화면 전반에서 같은 동작을 보장합니다.

좋은 후보는 합쳐진 전체 이름(소문자, 트림 처리), 대소문자 구분 없는 검색을 위한 소문자화된 텍스트, 공백을 축약한 정리된 문자열, 여러 필드에서 유도한 상태 레이블 등입니다.

예: 모든 쿼리에 lower(trim(first_name || ' ' || last_name))를 반복하는 대신 full_name_key를 한 번 생성하고 그걸로 필터하세요.

사람이 기대하는 방식의 정렬 키

정렬은 계산된 필드가 빠르게 효과를 발휘하는 곳입니다. 정렬은 PostgreSQL이 많은 행에 대해 표현식을 평가하게 만들 수 있으므로, 정렬 키가 일반 인덱스 컬럼이면 ORDER BY 비용이 크게 줄어듭니다.

일반적인 정렬 키는 숫자 랭크(플랜 티어를 1, 2, 3 으로 매핑), 두 타임스탬프 중 최대값 같은 단일 "최신 활동" 타임스탬프, 텍스트로 올바르게 정렬되도록 패딩 처리한 코드 등이 있습니다.

정렬 키가 일반 인덱스 컬럼일 때 ORDER BY는 매우 저렴해집니다.

빠른 필터용 파생 플래그

관리자 사용자는 "연체"나 "고가치" 같은 체크박스를 좋아합니다. 논리가 안정적이고 행 데이터만 기반으로 할 때 생성 열로 잘 작동합니다.

예를 들어 고객 목록에 "읽지 않은 메시지 있음"과 "연체"가 필요하면 unread_count > 0에서 유도한 has_unread 불리언과 due_date < now()paid_at is null에서 유도한 is_overdue 같은 생성 열을 만들어 UI 필터가 단순 조건으로 매핑되게 할 수 있습니다.

생성 열, 인덱스, 다른 옵션 사이의 선택

관리자 화면에는 빠른 필터링, 빠른 정렬, 그리고 몇 달 후에도 읽을 수 있는 SQL이 필요합니다. 실제 결정은 계산을 어디에 둘지입니다: 테이블에 둘지, 인덱스에 둘지, 뷰에 둘지, 앱 코드에 둘지.

생성 열은 값이 실제 컬럼처럼 동작하길 원할 때 적합합니다: 참조하기 쉽고, SELECT에서 보이며, 새 필터를 추가할 때 깜빡하기 어렵습니다. 또한 일반 인덱스와 자연스럽게 어울립니다.

표현식 인덱스는 테이블 정의를 바꾸지 않기 때문에 빠르게 추가할 수 있습니다. 속도만 중요하고 더 지저분한 SQL을 견딜 수 있다면 표현식 인덱스면 충분한 경우가 많습니다. 단점은 가독성이 떨어지고 플래너가 표현식을 정확히 매치해야 한다는 점입니다.

는 여러 테이블을 조인해 재사용 가능한 데이터 "형태"가 필요할 때 도움이 됩니다. 하지만 복잡한 뷰는 비용이 큰 작업을 숨기고 디버깅할 또 다른 장소를 만들 수 있습니다.

트리거는 일반 컬럼을 동기화하는 데 쓸 수 있지만 부품이 많아집니다. 대량 업데이트를 느리게 하고 문제 해결 시 간과되기 쉬울 수 있습니다.

때로는 앱에서 채우는 일반 컬럼이 최선입니다. 사용자가 편집할 수 있거나 공식이 비즈니스 결정에 따라 자주 바뀌면(단지 행 데이터에 기반하지 않을 때) 명시적으로 유지하는 편이 더 명확합니다.

간단한 선택 가이드:

  • 읽기 쉬운 쿼리와 행 데이터만 기반으로 하는 안정적인 공식이면 생성 열을 사용하세요.
  • 특정 필터 속도가 중요하고 시끄러운 SQL을 감수할 수 있으면 표현식 인덱스를 사용하세요.
  • 여러 곳에서 재사용되는 조인된 리포트 형식이 필요하면 뷰를 고려하세요.
  • 테이블 간 로직이나 부작용이 필요하면 앱 로직 우선, 트리거는 최후의 수단으로 고려하세요.

단계별: 생성 열을 추가하고 쿼리에서 사용하기

원하는 곳에 배포하세요
AppMaster Cloud 또는 선호하는 클라우드 제공자에 관리자 앱을 배포하세요.
Deploy App

UI에서 느려지는 한 개의 관리자 목록 쿼리부터 시작하세요. 화면에서 가장 자주 쓰는 필터와 정렬을 적어두고, 그 단일 쿼리를 먼저 개선하세요.

반복 작업을 제거하는 계산 필드를 하나 선택하고 snake_case로 명확하게 이름을 지어 다른 사람이 표현식을 다시 읽지 않아도 무엇을 담는지 알게 하세요.

1) 생성 열 추가 (STORED)

ALTER TABLE customers
ADD COLUMN full_name_key text
GENERATED ALWAYS AS (
  lower(concat_ws(' ', last_name, first_name))
) STORED;

인덱스를 추가하기 전에 실제 행에서 검증하세요:

SELECT id, first_name, last_name, full_name_key
FROM customers
ORDER BY id DESC
LIMIT 5;

출력이 잘못되면 지금 표현식을 고치세요. STORED는 PostgreSQL이 각 삽입과 업데이트에서 값을 유지한다는 뜻입니다.

2) 관리자 화면에 맞는 인덱스 추가

화면이 상태로 필터하고 이름으로 정렬한다면 그 패턴을 인덱스하세요:

CREATE INDEX customers_status_full_name_key_idx
ON customers (status, full_name_key);

3) 관리자 쿼리를 새 컬럼을 사용하도록 업데이트하기

이전에는 지저분한 ORDER BY가 있었을 수 있습니다. 이후에는 명확합니다:

SELECT id, status, first_name, last_name
FROM customers
WHERE status = 'active'
ORDER BY full_name_key ASC
LIMIT 50 OFFSET 0;

사람들이 매일 필터하고 정렬하는 부분에 생성 열을 사용하세요. 희귀한 화면에 대해선 사용하지 마세요.

실제적인 인덱싱 패턴

모델에서 코드로 이동
노코드로 빌드하면서도 실제 Go, Vue3, 네이티브 모바일 소스 코드를 얻으세요.
Generate Code

관리자 화면은 몇 가지 행동을 반복합니다: 몇 가지 필드로 필터링하고, 한 열로 정렬하며, 페이지네이션합니다. 최선의 설정은 보통 "모든 걸 인덱스"가 아니라 "가장 흔한 쿼리 형태를 인덱스"하는 것입니다.

실용 규칙: 가장 흔한 필터 컬럼을 앞에 두고, 가장 흔한 정렬 컬럼을 마지막에 둡니다. 멀티테넌트라면 workspace_id 같은 값이 보통 앞에 옵니다: (workspace_id, status, created_at).

텍스트 검색은 별개 문제입니다. 많은 검색 박스가 결국 ILIKE '%term%'로 끝나는데, 기본 btree 인덱스로는 속도 내기 어렵습니다. 도움이 되는 패턴은 원시 텍스트 대신 정규화된 헬퍼 컬럼(소문자화, 트림, 결합)을 검색하는 것입니다. UI가 접두사 검색(term%)을 사용한다면 해당 정규화 컬럼에 대한 btree 인덱스가 도움이 됩니다. 반드시 포함 검색(%term%)이 필요하다면 큰 테이블에서는 UI 동작을 조정하거나(예: "이메일은 시작 문자열만") 검색 대상을 좁히는 방법을 고려하세요.

또한 인덱스를 추가하기 전에 선택도를 확인하세요. 행의 95%가 같은 값을 가진다면(예: status = 'active') 해당 컬럼만 인덱스해도 크게 도움이 되지 않습니다. 더 선택도가 높은 컬럼과 묶거나 소수 케이스에 대해 부분 인덱스를 사용하세요.

현실적인 예: 빠르게 유지되는 고객 관리자 목록

전형적인 고객 관리자 페이지를 상상해보세요: 검색 박스, 몇 가지 필터(비활성, 잔액 범위), 정렬 가능한 "마지막 접속" 열. 시간이 지나면서 SQL은 읽기 어려워집니다: LOWER(), TRIM(), COALESCE(), 날짜 계산, 반복되는 CASE 블록 등.

이를 빠르고 읽기 쉬운 상태로 유지하는 한 방법은 반복되는 표현식을 생성 열로 밀어넣는 것입니다.

테이블과 생성 열 예시

customers 테이블에 name, email, last_seen, balance가 있다고 가정하고 세 개의 계산 필드를 추가합니다:

  • search_key: 간단한 검색을 위한 정규화된 텍스트 블롭
  • is_inactive: 날짜 논리를 반복하지 않고 필터할 수 있는 불리언
  • balance_bucket: 빠른 세분화를 위한 레이블
ALTER TABLE customers
  ADD COLUMN search_key text
    GENERATED ALWAYS AS (
      lower(trim(coalesce(name, ''))) || ' ' || lower(trim(coalesce(email, '')))
    ) STORED,
  ADD COLUMN is_inactive boolean
    GENERATED ALWAYS AS (
      last_seen IS NULL OR last_seen < (now() - interval '90 days')
    ) STORED,
  ADD COLUMN balance_bucket text
    GENERATED ALWAYS AS (
      CASE
        WHEN balance < 0 THEN 'negative'
        WHEN balance < 100 THEN '0-99'
        WHEN balance < 500 THEN '100-499'
        ELSE '500+'
      END
    ) STORED;

이제 관리자 쿼리는 UI처럼 읽힙니다.

읽기 쉬운 필터 + 정렬

"비활성 고객, 최신 활동 순"은 다음과 같습니다:

SELECT id, name, email, last_seen, balance
FROM customers
WHERE is_inactive = true
ORDER BY last_seen DESC NULLS LAST
LIMIT 50;

기본 검색은:

SELECT id, name, email, last_seen, balance
FROM customers
WHERE search_key LIKE '%' || lower(trim($1)) || '%'
ORDER BY last_seen DESC NULLS LAST
LIMIT 50;

진짜 이점은 일관성입니다. 같은 필드가 여러 화면에서 로직을 다시 쓰지 않고 동작을 제공합니다:

  • 고객 목록 검색 상자는 search_key를 사용합니다
  • "비활성 고객" 탭은 is_inactive를 사용합니다
  • 잔액 필터 칩은 balance_bucket을 사용합니다

흔한 실수와 함정

관리 작업 자동화
관리자 화면에서 자동화 액션을 생성하려면 Business Process Editor를 사용하세요.
Build Workflow

생성 열은 간단한 해결책처럼 보일 수 있습니다: 수학을 테이블에 넣고 쿼리를 깔끔하게 유지하세요. 하지만 화면의 필터/정렬 방식과 맞아떨어지고 올바른 인덱스를 추가했을 때만 도움이 됩니다.

가장 흔한 실수들:

  • 인덱싱 없이도 속도가 빨라진다고 가정하는 것. 계산된 값도 대규모에서 빠르려면 인덱스가 필요합니다.
  • 너무 많은 로직을 한 필드에 몰아넣는 것. 생성 열이 미니 프로그램이 되면 사람들이 신뢰를 잃습니다. 짧게 유지하고 이름을 명확히 하세요.
  • 불변이 아닌 함수를 사용하는 것. PostgreSQL은 저장된 생성 열의 표현식이 immutable이어야 합니다. now()random() 같은 것은 허용되지 않거나 기대와 달라 문제를 일으킵니다.
  • 쓰기 비용을 무시하는 것. 삽입과 업데이트는 계산된 값을 유지해야 합니다. 대량 데이터 수입이나 통합이 느려진다면 빠른 읽기가 의미가 없어질 수 있습니다.
  • 거의 중복된 컬럼을 만드는 것. 하나 또는 두 개의 표준 패턴(예: 단일 정규화 키)을 정하고 비슷한 컬럼을 다섯 개씩 쌓지 마세요.

관리자 목록이 포함 검색(ILIKE '%ann%')을 많이 사용하면 생성 열만으로 해결되지 않습니다. 다른 검색 접근법이 필요할 수 있습니다. 하지만 일상적인 "필터와 정렬" 쿼리에는 생성 열과 적절한 인덱스를 함께 쓰면 성능이 훨씬 예측 가능해집니다.

배포 전 빠른 체크리스트

공통 모듈 빠르게 추가
관리 도구에 인증 및 Stripe 결제 모듈을 빠르게 추가하세요.
Add Modules

변경을 배포하기 전에 계산 값, 쿼리, 인덱스가 서로 맞는지 확인하세요.

  • 공식이 안정적이고 한 문장으로 설명할 수 있는가?
  • 쿼리가 실제로 WHERE 및/또는 ORDER BY에서 생성 열을 사용하고 있는가?
  • 인덱스가 실사용에 맞춰져 있는가, 단순 테스트용이 아닌가?
  • NULL, 빈 문자열, 이상한 공백, 혼합 대소문자 같은 엣지 케이스에서 결과를 기존 로직과 비교했는가?
  • 테이블이 바쁠 경우(임포트, 백그라운드 업데이트, 통합) 쓰기 성능을 테스트했는가?

다음 단계: 관리자 화면에 적용하기

작업량이 큰 2-3개의 관리자 화면(예: 주문, 고객, 티켓) 중 작은 고임팩트 지점을 선택하세요. 느린 부분이 무엇인지 적으세요(날짜 범위 필터, "마지막 활동"으로 정렬, 결합된 이름으로 검색, 상태 레이블로 필터 등). 그런 다음 화면 전반에서 재사용할 짧은 계산 필드 집합을 표준화하세요.

측정 가능하고 되돌리기 쉬운 롤아웃 계획:

  • 생성 열을 명확한 이름으로 추가하세요.
  • 기존 로직을 대체하는 경우 잠시 동안 옆에 두고(old and new side by side) 비교하세요.
  • 화면의 주요 필터/정렬에 맞는 인덱스를 추가하세요.
  • 화면 쿼리를 새 컬럼을 사용하도록 바꾸세요.
  • 변경 전후(쿼리 시간과 스캔된 행 수)를 측정하고 개선이 확인되면 기존 우회 방법을 제거하세요.

내부 관리자 도구를 AppMaster (appmaster.io)로 빌드하고 있다면, 이런 계산 필드는 공유 데이터 모델에 잘 맞습니다: 데이터베이스가 규칙을 가지며 UI 필터는 여러 화면에서 표현식을 반복하지 않고 단순한 필드 이름을 가리킬 수 있습니다.

자주 묻는 질문

관리자 화면에 언제 PostgreSQL 생성 열을 사용해야 하나요?

생성된 열은 WHERE이나 ORDER BY에서 동일한 표현식을 반복해 쓰는 경우에 도움이 됩니다. 예를 들어 이름 정규화, 상태 매핑, 정렬 키 생성 같은 작업입니다. 특히 하루 종일 열어보는 관리자 목록처럼 예측 가능한 필터링과 정렬이 필요한 경우 유용합니다.

저장된 생성 열과 표현식 인덱스의 차이는 무엇인가요?

저장된 생성 열은 삽입이나 업데이트 시 계산되어 일반 컬럼처럼 디스크에 저장되므로 읽기는 빠르고 인덱스화할 수 있습니다. 반면 표현식 인덱스는 테이블 구조를 바꾸지 않고 인덱스에 결과를 저장하지만, 플래너가 정확히 같은 표현식을 인식해야 효과적이며 SQL이 더 지저분해질 수 있습니다.

생성 열이 자동으로 내 쿼리를 빠르게 만들어 주나요?

그 자체로는 아닙니다. 생성 열은 쿼리를 단순하게 만들고 계산된 값을 인덱싱하기 쉽게 해주지만, 실제로 대규모에서 빠르게 만들려면 일반적으로 해당 사용 패턴에 맞는 인덱스가 필요합니다.

관리자 검색 및 정렬에 추가할 최선의 생성 열은 무엇인가요?

보통은 자주 필터링하거나 정렬하는 값이 좋습니다: 정규화된 검색 키, '성명' 정렬 키, is_overdue 같은 파생 불리언, 또는 사람들이 기대하는 정렬 순서를 반영한 랭킹 숫자 등입니다. 여러 쿼리에서 반복 작업을 제거할 한 가지 값을 고르세요. 일회성 계산용으로는 적절하지 않습니다.

필터와 정렬을 하는 관리자 목록에 적합한 인덱스는 어떻게 고르나요?

가장 흔한 필터 컬럼을 앞에 두고 주요 정렬 키를 마지막에 두세요. 예: (workspace_id, status, full_name_key)처럼 화면 패턴에 맞추면 PostgreSQL이 먼저 빠르게 필터링하고 추가 정렬 작업을 줄일 수 있습니다.

생성 열이 `ILIKE '%term%'` 같은 느린 포함 검색을 해결해주나요?

그다지 도움이 되지 않습니다. 생성 열은 텍스트를 정규화해 동작을 일관되게 만들 순 있지만, ILIKE '%term%' 같은 포함 검색은 큰 테이블에서 기본 btree 인덱스로는 여전히 느립니다. 가능하면 접두사 검색(term%)으로 UI를 조정하거나 검색 대상을 축소하는 다른 필터를 사용하세요.

‘비활성’ 플래그처럼 `now()`에 의존하는 생성 열을 만들 수 있나요?

저장된 생성 열의 표현식은 불변(immutable)이어야 하므로 now() 같은 함수는 보통 허용되지 않으며, 개념적으로도 값이 오래되어 곧 바뀔 수 있어 잘 맞지 않습니다. '90일 비활성' 같은 시간 기반 플래그는 잡(job)으로 유지하는 일반 컬럼을 사용하거나, 자주 사용되지 않는다면 쿼리 시점에 계산하는 것이 좋습니다.

나중에 생성 열의 표현식을 변경해야 하면 어떻게 되나요?

가능하지만 실제 마이그레이션처럼 계획해야 합니다. 표현식을 변경하면 스키마 변경이 필요하고 기존 행의 값이 다시 계산되어야 하므로, 테이블이 크면 시간이 걸리고 쓰기 부하가 생길 수 있어 통제된 배포 창에서 진행하세요.

생성 열이 삽입 및 업데이트 성능에 오버헤드를 추가하나요?

네. 삽입과 업데이트 시 값이 계산되고 저장되어야 하므로 생성 열이 많거나 복잡한 표현식을 추가하면 대량 쓰기 작업(임포트, 동기화)이 느려질 수 있습니다. 표현식을 짧게 유지하고 실제로 사용하는 것만 추가하며 바쁜 테이블에서 쓰기 성능을 측정하세요.

기존 관리자 화면을 빠르게 만들기 위해 생성 열을 도입하는 가장 안전한 방법은 무엇인가요?

안전한 롤아웃 방법은: 생성 열을 추가하고 실제 몇 행에서 검증한 뒤, 화면의 주요 필터/정렬 패턴과 일치하는 인덱스를 추가하고, 관리 쿼리를 새 컬럼을 사용하도록 바꿔 전후의 쿼리 시간과 스캔된 행 수를 비교하는 것입니다. 이 과정을 거치면 변경 효과를 확인하고 기존 우회 방법을 제거할 수 있습니다.

쉬운 시작
멋진만들기

무료 요금제로 AppMaster를 사용해 보세요.
준비가 되면 적절한 구독을 선택할 수 있습니다.

시작하다