0Pricing
SQL Academy · 강의

배열과 정규화된 테이블 비교

배열을 선택하는 것이 적절한 경우를 알아봅니다.

배열과 정규화된 테이블 비교은(는) CoddyKit의 무료 SQL Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 SQL Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. SQL Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

여러 값을 저장하는 두 가지 방법

하나의 행에 서로 관련된 여러 값을 저장해야 할 때 PostgreSQL에서는 두 가지 주요 방법을 제공합니다. 같은 행에 배열 열로 저장하거나, 각 값이 자신의 행을 갖는 별도의 하위 테이블을 만드는 방법입니다.

각 방법을 언제 사용할지 이해하는 것은 효율적이고 유지 관리하기 쉬운 데이터베이스를 설계하는 데 중요한 기술입니다.

정규화된 방식

완전히 정규화된 schema에서는 각 데이터가 자신의 행에 저장됩니다. 사용자 한 명이 여러 전화번호를 가질 수 있다면 users를 참조하는 외래 키가 있는 user_phones 테이블을 만듭니다.

이는 전형적인 관계형 모델이며 대부분의 상황에서 기본적으로 선택하는 방식입니다.

CREATE TABLE users (
  id   SERIAL PRIMARY KEY,
  name TEXT NOT NULL
);

CREATE TABLE user_phones (
  id      SERIAL PRIMARY KEY,
  user_id INT REFERENCES users(id),
  phone   TEXT NOT NULL
);

INSERT INTO users (name) VALUES ('Alice'), ('Bob');

INSERT INTO user_phones (user_id, phone) VALUES
  (1, '+1-555-0101'),
  (1, '+1-555-0102'),
  (2, '+1-555-0200');

배열 방식

PostgreSQL의 TEXT[] 또는 다른 자료형 뒤에 []를 붙인 형태를 사용하면 여러 값을 하나의 열 안에 직접 저장할 수 있습니다. 별도의 테이블이 필요하지 않습니다.

같은 전화번호 데이터를 사용자마다 하나의 간결한 행에 저장할 수 있습니다.

CREATE TABLE users_with_phones (
  id     SERIAL PRIMARY KEY,
  name   TEXT    NOT NULL,
  phones TEXT[]
);

INSERT INTO users_with_phones (name, phones) VALUES
  ('Alice', ARRAY['+1-555-0101', '+1-555-0102']),
  ('Bob',   ARRAY['+1-555-0200']);

배열 조회는 간단합니다

배열 열 내부를 검색할 때는 ANY 연산자나 @> (포함) 연산자를 사용하면 간단합니다. 간단한 WHERE 절만으로 특정 전화번호를 가진 모든 사용자를 찾을 수 있습니다.

-- Find users who have a specific phone number
SELECT name
FROM users_with_phones
WHERE '+1-555-0101' = ANY(phones);

-- Or using the array-contains operator
SELECT name
FROM users_with_phones
WHERE phones @> ARRAY['+1-555-0101'];

배열이 유리한 경우: 간단한 조회

다음과 같은 경우에는 배열이 적합합니다.

  • 값 목록을 하나의 단위로 함께 읽는 경우(태그, 라벨, 카테고리)
  • 개별 요소를 기준으로 조인할 필요가 전혀 없는 경우
  • 목록에 자연스러운 상한이 있고 일부 요소만 수정하는 일이 드문 경우

대표적인 예는 블로그 게시물에 태그를 저장하는 것입니다. 항상 모든 태그를 한 번에 가져오며, 복잡한 조인에서 단일 태그를 기준으로 게시물을 조회하는 일은 드뭅니다.

CREATE TABLE posts (
  id         SERIAL PRIMARY KEY,
  title      TEXT    NOT NULL,
  tags       TEXT[]
);

INSERT INTO posts (title, tags) VALUES
  ('Intro to SQL',      ARRAY['sql', 'beginner', 'database']),
  ('Advanced Indexes',  ARRAY['sql', 'performance', 'indexes']),
  ('NoSQL Overview',    ARRAY['nosql', 'beginner']);

-- Get all posts tagged 'beginner'
SELECT title FROM posts
WHERE 'beginner' = ANY(tags);

정규화된 테이블이 유리한 경우: 관계

다음과 같은 경우에는 정규화된 테이블을 선택하는 편이 좋습니다.

  • 개별 값에 고유한 속성이 필요한 경우(예: 전화번호에 집 전화 또는 직장 전화라는 유형이 있는 경우)
  • 개별 값을 기준으로 조인해야 하는 경우
  • 값이 독립적으로 자주 변경되는 경우
  • 외래 키를 통한 참조 무결성이 필요한 경우
-- Phone numbers need a 'type' attribute — array can't do this cleanly
CREATE TABLE user_phones (
  id      SERIAL PRIMARY KEY,
  user_id INT REFERENCES users(id),
  phone   TEXT NOT NULL,
  type    TEXT CHECK (type IN ('home', 'work', 'mobile'))
);

INSERT INTO user_phones (user_id, phone, type) VALUES
  (1, '+1-555-0101', 'home'),
  (1, '+1-555-0102', 'work');

인덱싱의 차이

정규화된 테이블에서는 외래 키 열이나 값 열에 표준 B-트리 인덱스를 추가할 수 있습니다. 배열에서는 배열 내부를 빠르게 검색하려면 GIN 인덱스(일반화 역색인)가 필요합니다.

GIN 인덱스는 잘 작동하지만 B-트리 인덱스보다 크기가 크고 업데이트 속도가 느립니다.

-- Index for fast array element lookups
CREATE INDEX idx_posts_tags ON posts USING GIN (tags);

-- Now this query uses the index efficiently
EXPLAIN SELECT title FROM posts
WHERE tags @> ARRAY['sql'];

행 전체의 집계: 정규화 방식이 유리합니다

개별 값을 기준으로 개수를 세거나 그룹화하거나 집계해야 할 때는 정규화된 테이블이 훨씬 자연스럽습니다. 배열 내부에서 집계하려면 unnest()를 사용해야 하는데, 이 함수는 먼저 배열을 행으로 펼칩니다. 즉, 질의 시점에 정규화된 구조를 사실상 다시 만드는 셈입니다.

-- Count posts per tag (array approach — needs unnest)
SELECT tag, COUNT(*) AS post_count
FROM posts, unnest(tags) AS tag
GROUP BY tag
ORDER BY post_count DESC;

-- With a normalized post_tags table this would be simpler:
-- SELECT tag, COUNT(*) FROM post_tags GROUP BY tag;

배열 요소 수정

배열 안의 단일 요소를 업데이트하거나 삭제하려면 문법이 번거롭습니다. 전체 배열을 바꾸거나 array_remove()를 사용해야 합니다. 정규화된 테이블에서는 특정 행에 대해 간단히 DELETE 또는 UPDATE를 실행하면 됩니다.

-- Remove a single tag from an array column
UPDATE posts
SET tags = array_remove(tags, 'beginner')
WHERE id = 1;

-- Append a new tag
UPDATE posts
SET tags = array_append(tags, 'tutorial')
WHERE id = 1;

SELECT title, tags FROM posts WHERE id = 1;

유효한 값 적용

정규화된 테이블에서는 외래 키를 사용해 모든 값이 미리 정해진 집합에서 오도록 강제할 수 있습니다. 배열은 다른 테이블을 참조할 수 없으므로 외래 키를 지원하지 않습니다.

각 요소에 대해 참조 무결성을 보장해야 한다면 하위 테이블만이 유일한 선택입니다.

-- Normalized: only valid category IDs allowed (FK enforced)
CREATE TABLE categories (
  id   SERIAL PRIMARY KEY,
  name TEXT UNIQUE NOT NULL
);

CREATE TABLE post_categories (
  post_id     INT REFERENCES posts(id),
  category_id INT REFERENCES categories(id),
  PRIMARY KEY (post_id, category_id)
);

-- Array: no constraint possible — any text value is accepted
-- UPDATE posts SET tags = ARRAY['totally_invalid_tag'] WHERE id = 1;

실용적인 선택 가이드

데이터가 단순한 평면 목록이고, 항상 하나의 단위로 읽으며, 요소별 추가 속성이 없고, 참조 무결성이 필요하지 않다면 배열을 사용하세요(예: 태그, 라벨, 검색 키워드).

각 요소에 고유한 속성이 있거나, 개별 값을 기준으로 조인 또는 집계를 수행하거나, 외래 키가 필요하거나, 개별 요소를 자주 업데이트하거나 삭제한다면 정규화된 하위 테이블을 사용하세요.

-- Summary example: tags as array (good fit)
SELECT title, tags
FROM posts
WHERE tags @> ARRAY['sql']
ORDER BY title;

-- Unnest when you need row-level processing
SELECT title, unnest(tags) AS tag
FROM posts
ORDER BY title, tag;

빠른 확인

정규화된 하위 테이블 대신 PostgreSQL 배열로 데이터를 저장하기에 가장 적합한 상황은 무엇인가요?

레슨 요약

이번 레슨에서는 PostgreSQL에서 배열과 정규화된 테이블을 사용할 때의 주요 장단점을 배웠습니다.

  • 배열은 태그처럼 평면적이고 한 단위로 읽는 목록을 간결하고 편리하게 저장할 수 있지만, 외래 키가 없고 요소별 업데이트가 번거로우며 빠른 검색을 위해 GIN 인덱스가 필요합니다.
  • 정규화된 테이블은 요소별 속성, 외래 키 무결성, 효율적인 집계, 간단한 행 단위 업데이트를 지원하지만 추가 조인이 필요합니다.
  • 올바른 선택은 데이터를 어떻게 저장하는지만이 아니라 어떻게 조회하고, 업데이트하고, 서로 연결하는지에 따라 결정됩니다.

자주 묻는 질문

“배열과 정규화된 테이블 비교” 강의는 무료인가요?

네 — “배열과 정규화된 테이블 비교” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 SQL Academy 강의 전체를 잠금 해제할 수 있습니다. SQL Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“배열과 정규화된 테이블 비교”에서 뭘 배우나요?

배열을 선택하는 것이 적절한 경우를 알아봅니다. 브라우저에서 직접 실행하는 실습 코드로 SQL Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

SQL Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 SQL Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“배열과 정규화된 테이블 비교” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 SQL Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 SQL Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 배열 열 기초
  2. 배열 내부 검색
  3. UNNEST와 집계
  4. 배열과 정규화된 테이블 비교
← SQL Academy(으)로 돌아가기