<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개키우는개발자 : )</title>
    <link>https://dog-developers.tistory.com/</link>
    <description>모두 행복하세요</description>
    <language>ko</language>
    <pubDate>Wed, 8 Apr 2026 21:36:53 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>DOGvelopers</managingEditor>
    <image>
      <title>개키우는개발자 : )</title>
      <url>https://tistory1.daumcdn.net/tistory/2966941/attach/cc059df46404430782322a5942ec96fb</url>
      <link>https://dog-developers.tistory.com</link>
    </image>
    <item>
      <title>SQLite, 이제 프로덕션에서 써도 됩니다 - 2026년 신규 프로젝트와 MVP에 최적인 이유</title>
      <link>https://dog-developers.tistory.com/330</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;SQLite는 테스트용 아니야?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 년 전까지만 해도 맞는 말이었다. 하지만 2026년 현재, SQLite를 프로덕션에서 쓰는 서비스가 급격히 늘고 있다. Rails 8이 SQLite를 기본 DB로 밀고 있고, Turso, Litestream 같은 도구들이 등장하면서 MVP나 신규 프로젝트에서 SQLite를 선택하는 게 더 이상 이상한 일이 아니게 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQLite가 달라진 점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최신 버전 (3.51.x, 2025년)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;jsonb_each(), jsonb_tree()&lt;/b&gt; - JSON 처리 강화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;unistr()&lt;/b&gt; - 유니코드 문자열 함수 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sqlite3_setlk_timeout()&lt;/b&gt; - 락 타임아웃 세밀한 제어&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 최적화&lt;/b&gt; - 읽기 트랜잭션 커밋 속도 개선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IN 연산자 최적화&lt;/b&gt; - 서브쿼리 재사용 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;25주년 기념 릴리스 (3.50.0)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 5월에 나온 25주년 버전에서 대대적인 개선이 있었다. SQLite 팀이 &quot;이제 웹 앱에서도 써도 된다&quot;고 자신감을 보인 버전이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 갑자기 SQLite가 뜨는 걸까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 하드웨어가 빨라졌다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLite 전문가 Ben Johnson의 벤치마크에 따르면, 현대 SSD에서 SQLite는 초당 수만 건의 읽기/쓰기가 가능하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;읽기: 초당 400,000+ 쿼리
쓰기: 초당 10,000+ 쿼리 (WAL 모드)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 웹 앱은 이 정도면 충분하고도 남는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. WAL 모드의 등장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL(Write-Ahead Logging) 모드가 게임 체인저였다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;PRAGMA journal_mode=WAL;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기와 쓰기가 동시에 가능&lt;/li&gt;
&lt;li&gt;읽기 성능 대폭 향상&lt;/li&gt;
&lt;li&gt;동시 접속 처리 능력 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Litestream으로 백업 문제 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLite의 가장 큰 약점이었던 &quot;백업/복제&quot;가 해결됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Litestream&lt;/b&gt;은 SQLite의 WAL 변경사항을 S3 같은 스토리지로 실시간 스트리밍한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# litestream.yml
dbs:
  - path: /data/myapp.db
    replicas:
      - url: s3://mybucket/myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1초 단위 백업 가능&lt;/li&gt;
&lt;li&gt;Point-in-time 복구 지원&lt;/li&gt;
&lt;li&gt;재해 복구 시간 2분 이내&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Rails 8의 공식 지원&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rails 8이 SQLite를 1급 시민으로 대우하기 시작했다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;# Rails 8 기본 설정
rails new myapp  # SQLite가 기본!
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Solid Queue (백그라운드 잡) - SQLite 지원&lt;/li&gt;
&lt;li&gt;Solid Cache (캐시) - SQLite 지원&lt;/li&gt;
&lt;li&gt;Solid Cable (웹소켓) - SQLite 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 없이 Rails 앱을 운영할 수 있게 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQLite vs PostgreSQL/MySQL 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 SQLite PostgreSQL/MySQL&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;설치&lt;/td&gt;
&lt;td&gt;파일 하나&lt;/td&gt;
&lt;td&gt;서버 설치 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;DBA 필요할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;서버 비용 + 관리 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;백업&lt;/td&gt;
&lt;td&gt;파일 복사 (+ Litestream)&lt;/td&gt;
&lt;td&gt;pg_dump, mysqldump&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동시 쓰기&lt;/td&gt;
&lt;td&gt;1개 (순차 처리)&lt;/td&gt;
&lt;td&gt;다수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복제&lt;/td&gt;
&lt;td&gt;Litestream, Turso&lt;/td&gt;
&lt;td&gt;네이티브 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장성&lt;/td&gt;
&lt;td&gt;단일 서버&lt;/td&gt;
&lt;td&gt;수평 확장 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;언제 SQLite를 선택해야 할까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQLite가 딱 맞는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. MVP / 프로토타입&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 빠른 개발이 중요
- 인프라 고민 최소화
- 나중에 마이그레이션 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 1인 개발 / 소규모 팀&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;- 서버 관리할 여력이 없음
- 월 $5 VPS로 운영하고 싶음
- 복잡한 인프라 싫음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 읽기 위주 서비스&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 블로그, 문서 사이트
- 읽기 90% 이상인 서비스
- 컨텐츠 중심 앱
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 임베디드 / 로컬 앱&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 데스크톱 앱
- 모바일 앱
- CLI 도구
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 엣지 컴퓨팅&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- Cloudflare Workers
- Fly.io 엣지 배포
- 각 리전에 로컬 DB
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQLite를 피해야 하는 경우&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초당 수천 건 이상의 쓰기&lt;/li&gt;
&lt;li&gt;복잡한 트랜잭션이 많은 금융 서비스&lt;/li&gt;
&lt;li&gt;여러 서버에서 동시 쓰기 필요&lt;/li&gt;
&lt;li&gt;팀에서 PostgreSQL/MySQL 경험이 풍부&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 운영 사례&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Joy of Rails&lt;/h3&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- SQLite로 런칭
- 3개월 운영
- DB 관련 장애 0건
- PostgreSQL로 전환 계획 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;37signals (Basecamp, HEY)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rails 8의 SQLite 지원은 37signals의 경험에서 나왔다. 실제 대규모 서비스에서 검증된 설정이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;벤치마크 결과 (Rails 8 기준)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;일일 10만 히트 이상 처리 가능
N+1 쿼리가 PostgreSQL보다 덜 치명적
(네트워크 레이턴시가 없어서)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQLite 프로덕션 설정 가이드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. WAL 모드 활성화&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA busy_timeout=5000;
PRAGMA cache_size=-20000;  -- 20MB 캐시
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Rails 8 설정&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# config/database.yml
production:
  adapter: sqlite3
  database: storage/production.sqlite3
  pool: 5
  timeout: 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Litestream 설정&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# litestream.yml
dbs:
  - path: storage/production.sqlite3
    replicas:
      - type: s3
        bucket: my-backup-bucket
        path: db/production.sqlite3
        sync-interval: 1s
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Kamal 배포 (Rails 8)&lt;/h3&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;# config/deploy.yml
volumes:
  - &quot;myapp_storage:/rails/storage&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;볼륨을 마운트해서 DB 파일을 영속화한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Turso - SQLite의 클라우드 버전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLite 호환 분산 데이터베이스 Turso도 주목할 만하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import { createClient } from &quot;@libsql/client&quot;;

const client = createClient({
  url: &quot;libsql://my-db.turso.io&quot;,
  authToken: &quot;...&quot;
});

const result = await client.execute(&quot;SELECT * FROM users&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQLite 호환 SQL&lt;/li&gt;
&lt;li&gt;글로벌 복제&lt;/li&gt;
&lt;li&gt;엣지 배포&lt;/li&gt;
&lt;li&gt;무료 티어 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MVP 빠르게 시작하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rails 8&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;rails new myapp
cd myapp
rails generate scaffold Post title:string body:text
rails db:migrate
rails server
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝. PostgreSQL 설치도 없고, Docker도 필요 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Node.js (Better-sqlite3)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const Database = require('better-sqlite3');
const db = new Database('myapp.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE
  )
`);

const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
insert.run('Kim', 'kim@test.com');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Python (sqlite3 내장)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import sqlite3

conn = sqlite3.connect('myapp.db')
cursor = conn.cursor()

cursor.execute('''
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE
    )
''')

cursor.execute('INSERT INTO users (name, email) VALUES (?, ?)', 
               ('Kim', 'kim@test.com'))
conn.commit()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 묻는 질문&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q: 동시 접속 많으면 어떡해요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기는 무한대로 동시 가능하다. 쓰기만 순차 처리되는데, WAL 모드에서는 초당 1만 건 이상 처리 가능하다. 대부분의 웹 앱은 이 정도면 충분하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q: 서버 죽으면 데이터 날아가지 않나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Litestream으로 S3에 실시간 백업하면 최대 1초 분량만 손실된다. PostgreSQL의 WAL 백업과 비슷한 수준의 안정성을 확보할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q: 나중에 PostgreSQL로 옮기기 어렵지 않나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 SQL은 호환된다. Rails나 Django 같은 ORM을 쓰면 마이그레이션이 더 쉽다. 다만 PostgreSQL 전용 기능(JSONB, Array, 확장 등)은 못 쓴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q: 어느 정도 규모까지 가능해요?&lt;/h3&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- 일일 10만 히트: 여유 있음
- 일일 100만 히트: 가능 (최적화 필요)
- 그 이상: PostgreSQL 고려
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황 추천&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MVP / 프로토타입&lt;/td&gt;
&lt;td&gt;SQLite ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1인 개발 사이드 프로젝트&lt;/td&gt;
&lt;td&gt;SQLite ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;읽기 위주 서비스&lt;/td&gt;
&lt;td&gt;SQLite ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스타트업 초기&lt;/td&gt;
&lt;td&gt;SQLite ✓ (나중에 전환 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대규모 쓰기 트래픽&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복잡한 분석 쿼리&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;팀에서 PG 경험 풍부&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2026년의 SQLite는&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로덕션에서 써도 되고&lt;/li&gt;
&lt;li&gt;MVP에 최적이며&lt;/li&gt;
&lt;li&gt;인프라 비용을 극적으로 줄일 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡하게 생각하지 말자. 월 $5 VPS 하나에 앱과 DB를 같이 올리고, Litestream으로 S3에 백업하면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 진짜 스케일이 필요해지면 그때 PostgreSQL로 옮겨도 늦지 않다. 대부분의 서비스는 그 &quot;나중&quot;이 안 온다.&lt;/p&gt;</description>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/330</guid>
      <comments>https://dog-developers.tistory.com/330#entry330comment</comments>
      <pubDate>Mon, 19 Jan 2026 21:40:46 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL vs MySQL 2026 완벽 비교 - 어떤 DB를 선택해야 할까?</title>
      <link>https://dog-developers.tistory.com/329</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;새 프로젝트를 시작할 때마다 고민되는 질문이다. &quot;PostgreSQL이랑 MySQL 중에 뭘 써야 하지?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 기준으로 두 데이터베이스를 비교하고, 상황별 선택 가이드를 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한눈에 보는 비교표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 PostgreSQL MySQL&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;타입&lt;/td&gt;
&lt;td&gt;객체 관계형 (ORDBMS)&lt;/td&gt;
&lt;td&gt;관계형 (RDBMS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라이선스&lt;/td&gt;
&lt;td&gt;PostgreSQL License (완전 무료)&lt;/td&gt;
&lt;td&gt;GPL (상용 시 유료 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최신 버전 (2026)&lt;/td&gt;
&lt;td&gt;17.x&lt;/td&gt;
&lt;td&gt;8.4 LTS / 9.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;강점&lt;/td&gt;
&lt;td&gt;복잡한 쿼리, 데이터 무결성&lt;/td&gt;
&lt;td&gt;단순 읽기, 빠른 속도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;약점&lt;/td&gt;
&lt;td&gt;메모리 사용량 높음&lt;/td&gt;
&lt;td&gt;복잡한 쿼리 성능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적합한 용도&lt;/td&gt;
&lt;td&gt;금융, 분석, 대규모 시스템&lt;/td&gt;
&lt;td&gt;웹사이트, 중소규모 앱&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 성능 비교&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;읽기 성능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 SELECT 쿼리는 MySQL이 빠르다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;MySQL: 단순 읽기 위주 &amp;rarr; 승리
PostgreSQL: 복잡한 읽기 &amp;rarr; 승리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 여러 사용자가 접속해도 단일 프로세스로 처리한다. PostgreSQL은 사용자마다 새 프로세스를 생성해서 메모리를 더 쓴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쓰기 성능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈번한 UPDATE가 있다면 주의가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;MySQL: UPDATE 시 기존 행을 직접 수정
PostgreSQL: UPDATE 시 기존 행 삭제 표시 + 새 행 추가 (MVCC)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 UPDATE는 내부적으로 DELETE + INSERT처럼 동작한다. UPDATE가 많은 서비스에서는 VACUUM 관리가 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복잡한 쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN이 많거나 서브쿼리가 복잡하면 PostgreSQL이 압도적이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 복잡한 분석 쿼리 예시
WITH monthly_sales AS (
    SELECT 
        DATE_TRUNC('month', order_date) AS month,
        SUM(amount) AS total
    FROM orders
    GROUP BY 1
)
SELECT 
    month,
    total,
    LAG(total) OVER (ORDER BY month) AS prev_month,
    total - LAG(total) OVER (ORDER BY month) AS diff
FROM monthly_sales;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 Hash Join, Merge Join, Nested Loop 중 최적을 선택한다. MySQL은 주로 Nested Loop만 사용해서 대량 JOIN에서 느릴 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 벤치마크 (우아한형제들 기술블로그 참고)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 MySQL PostgreSQL&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단순 OLTP&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000만 건 JOIN&lt;/td&gt;
&lt;td&gt;22초&lt;/td&gt;
&lt;td&gt;3초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병렬 쿼리&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;완전 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 쿼리에서는 PostgreSQL이 7배 이상 빠를 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기능 비교&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JSON 지원&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 JSON을 지원하지만 수준이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PostgreSQL&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- JSONB 타입 (바이너리, 인덱싱 가능)
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    data JSONB
);

-- JSON 필드 인덱스
CREATE INDEX idx_products_data ON products USING GIN (data);

-- JSON 쿼리
SELECT * FROM products 
WHERE data-&amp;gt;&amp;gt;'category' = 'electronics';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- JSON 타입
CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    data JSON
);

-- JSON 쿼리 (인덱스 제한적)
SELECT * FROM products 
WHERE JSON_EXTRACT(data, '$.category') = 'electronics';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 JSONB는 인덱싱이 완벽하게 지원된다. MySQL JSON도 8.0부터 많이 좋아졌지만 PostgreSQL이 앞선다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 타입 PostgreSQL MySQL&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B-tree&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GiST (공간)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIN (전문검색)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BRIN (대용량)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부분 인덱스&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-text&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL이 더 다양한 인덱스를 지원한다. 특히 부분 인덱스는 대용량 테이블에서 유용하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- PostgreSQL 부분 인덱스
CREATE INDEX idx_active_users ON users(email) 
WHERE status = 'active';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확장 기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PostgreSQL 대표 확장&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PostGIS: 지리공간 데이터&lt;/li&gt;
&lt;li&gt;pgvector: AI/ML 벡터 저장&lt;/li&gt;
&lt;li&gt;pg_trgm: 유사 문자열 검색&lt;/li&gt;
&lt;li&gt;TimescaleDB: 시계열 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL 대표 확장&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스토리지 엔진 선택 (InnoDB, MyISAM)&lt;/li&gt;
&lt;li&gt;MySQL Router: 로드밸런싱&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 확장 생태계가 더 풍부하다. 특히 AI 시대에 pgvector는 큰 장점이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. SQL 문법 차이&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AUTO_INCREMENT vs SERIAL&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- MySQL
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50)
);

-- PostgreSQL
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50)
);

-- PostgreSQL (명시적)
CREATE TABLE users (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name VARCHAR(50)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문자열 비교&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- MySQL (대소문자 구분 안함)
SELECT * FROM users WHERE name = 'John';  -- 'john'도 매칭

-- PostgreSQL (대소문자 구분함)
SELECT * FROM users WHERE name = 'John';  -- 정확히 'John'만
SELECT * FROM users WHERE name ILIKE 'john';  -- 대소문자 무시
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LIMIT 문법&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- MySQL
SELECT * FROM users LIMIT 10 OFFSET 20;
SELECT * FROM users LIMIT 20, 10;  -- MySQL만 가능

-- PostgreSQL
SELECT * FROM users LIMIT 10 OFFSET 20;
SELECT * FROM users FETCH FIRST 10 ROWS ONLY;  -- 표준 SQL
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UPSERT&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- MySQL
INSERT INTO users (id, name, email) VALUES (1, 'Kim', 'kim@test.com')
ON DUPLICATE KEY UPDATE name = VALUES(name), email = VALUES(email);

-- PostgreSQL
INSERT INTO users (id, name, email) VALUES (1, 'Kim', 'kim@test.com')
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 운영 관점 비교&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복제 (Replication)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 PostgreSQL MySQL&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;물리적 복제&lt;/td&gt;
&lt;td&gt;✓ (Streaming)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;논리적 복제&lt;/td&gt;
&lt;td&gt;✓ (10버전+)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;멀티마스터&lt;/td&gt;
&lt;td&gt;확장으로 가능&lt;/td&gt;
&lt;td&gt;기본 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설정 난이도&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL이 복제 설정이 더 쉽다. PostgreSQL은 더 세밀한 제어가 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;백업&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PostgreSQL&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# 논리 백업
pg_dump mydb &amp;gt; backup.sql

# 물리 백업
pg_basebackup -D /backup -Fp -Xs -P
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# 논리 백업
mysqldump mydb &amp;gt; backup.sql

# 물리 백업 (Enterprise 또는 Percona XtraBackup)
xtrabackup --backup --target-dir=/backup
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 비슷한 수준의 백업 도구를 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모니터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PostgreSQL&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 실행 중인 쿼리
SELECT * FROM pg_stat_activity;

-- 테이블 통계
SELECT * FROM pg_stat_user_tables;

-- 슬로우 쿼리 (pg_stat_statements 확장)
SELECT query, calls, mean_time FROM pg_stat_statements 
ORDER BY mean_time DESC LIMIT 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 실행 중인 쿼리
SHOW PROCESSLIST;

-- 테이블 상태
SHOW TABLE STATUS;

-- 슬로우 쿼리 (설정 필요)
SELECT * FROM mysql.slow_log;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 클라우드 지원&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 PostgreSQL MySQL&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RDS&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aurora&lt;/td&gt;
&lt;td&gt;Aurora PostgreSQL&lt;/td&gt;
&lt;td&gt;Aurora MySQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버리스&lt;/td&gt;
&lt;td&gt;Aurora Serverless v2&lt;/td&gt;
&lt;td&gt;Aurora Serverless v2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기타 클라우드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Supabase&lt;/b&gt;: PostgreSQL 기반 (Firebase 대안)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PlanetScale&lt;/b&gt;: MySQL 기반 (서버리스)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Neon&lt;/b&gt;: PostgreSQL 서버리스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 기반 Supabase가 요즘 인기가 많다. 오픈소스 + 실시간 기능 + Auth까지 제공한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 선택 가이드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PostgreSQL을 선택해야 할 때&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 쿼리와 분석이 많은 경우&lt;/li&gt;
&lt;li&gt;데이터 무결성이 중요한 금융/의료 시스템&lt;/li&gt;
&lt;li&gt;JSON, 지리공간 데이터를 많이 다루는 경우&lt;/li&gt;
&lt;li&gt;AI/ML 벡터 저장이 필요한 경우 (pgvector)&lt;/li&gt;
&lt;li&gt;대규모 트랜잭션 처리 (OLTP + OLAP)&lt;/li&gt;
&lt;li&gt;표준 SQL 준수가 중요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL을 선택해야 할 때&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순한 CRUD 위주의 웹 애플리케이션&lt;/li&gt;
&lt;li&gt;읽기 작업이 대부분인 서비스&lt;/li&gt;
&lt;li&gt;WordPress, Laravel 등 MySQL 기본 지원 프레임워크 사용&lt;/li&gt;
&lt;li&gt;빠른 개발과 쉬운 운영이 필요한 경우&lt;/li&gt;
&lt;li&gt;팀에서 MySQL 경험이 많은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 유형별 추천&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 추천&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;블로그, 쇼핑몰&lt;/td&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스타트업 MVP&lt;/td&gt;
&lt;td&gt;MySQL (빠른 시작)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;금융 서비스&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 분석 플랫폼&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI/챗봇 서비스&lt;/td&gt;
&lt;td&gt;PostgreSQL (pgvector)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지도 기반 서비스&lt;/td&gt;
&lt;td&gt;PostgreSQL (PostGIS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대규모 SaaS&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress 사이트&lt;/td&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;레거시 마이그레이션&lt;/td&gt;
&lt;td&gt;기존 시스템 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 2026년 트렌드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PostgreSQL의 상승세&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stack Overflow 설문에서 가장 사랑받는 DB 1위&lt;/li&gt;
&lt;li&gt;AI/ML 붐으로 pgvector 수요 급증&lt;/li&gt;
&lt;li&gt;Supabase, Neon 등 신규 서비스 성장&lt;/li&gt;
&lt;li&gt;클라우드 네이티브 환경에서 선호도 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL의 현재&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여전히 웹 애플리케이션 시장 점유율 1위&lt;/li&gt;
&lt;li&gt;8.4 LTS로 안정성 강화&lt;/li&gt;
&lt;li&gt;9.x에서 Vector 타입 추가 (PostgreSQL 추격)&lt;/li&gt;
&lt;li&gt;PlanetScale 등 서버리스 옵션 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;복잡한 것 &amp;rarr; PostgreSQL
단순한 것 &amp;rarr; MySQL
모르겠으면 &amp;rarr; PostgreSQL (더 범용적)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 현재, PostgreSQL의 인기가 빠르게 상승 중이다. 특히 AI/ML, 데이터 분석 분야에서는 PostgreSQL이 사실상 표준이 되어가고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 MySQL도 여전히 강력한 선택지다. WordPress, Laravel 생태계에서는 MySQL이 더 편하고, 단순한 웹 애플리케이션에서는 충분히 좋은 성능을 낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 &quot;정답&quot;은 없다. 프로젝트 요구사항, 팀 경험, 장기 유지보수를 고려해서 선택하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마이그레이션 참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 PostgreSQL로 넘어가거나 그 반대의 경우가 있다면 별도의 마이그레이션 가이드를 참고하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법 차이와 데이터 타입 변환에 주의가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/329</guid>
      <comments>https://dog-developers.tistory.com/329#entry329comment</comments>
      <pubDate>Mon, 19 Jan 2026 21:33:52 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL IN 절 성능 최적화 - IN vs ANY vs EXISTS 완벽 비교</title>
      <link>https://dog-developers.tistory.com/328</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절에서 여러 값을 조회할 때 IN을 많이 쓴다. 그런데 IN, ANY, EXISTS 중에 뭐가 가장 빠를까? PostgreSQL의 실행계획을 분석하면서 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IN, ANY, EXISTS 기본 문법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 세 가지 문법을 비교해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IN 절&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT * FROM users
WHERE id IN (1, 2, 3, 4, 5);

-- 서브쿼리 사용
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE status = 'completed');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ANY 연산자&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT * FROM users
WHERE id = ANY(ARRAY[1, 2, 3, 4, 5]);

-- 서브쿼리 사용
SELECT * FROM users
WHERE id = ANY(SELECT user_id FROM orders WHERE status = 'completed');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EXISTS&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT * FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.user_id = u.id AND o.status = 'completed'
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 환경 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 성능을 비교하기 위해 테스트 데이터를 만든다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 사용자 테이블 (100만 건)
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (name, email)
SELECT 
    'user_' || i,
    'user_' || i || '@test.com'
FROM generate_series(1, 1000000) AS i;

-- 주문 테이블 (500만 건)
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id),
    amount INT,
    status VARCHAR(20),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO orders (user_id, amount, status)
SELECT 
    (random() * 999999 + 1)::INT,
    (random() * 100000)::INT,
    CASE (random() * 3)::INT 
        WHEN 0 THEN 'pending'
        WHEN 1 THEN 'completed'
        ELSE 'cancelled'
    END
FROM generate_series(1, 5000000);

-- 인덱스 생성
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);

-- 통계 갱신
ANALYZE users;
ANALYZE orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;케이스 1: 리터럴 값 목록 (적은 수)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 몇 개 안 될 때는 IN과 ANY 모두 동일한 실행계획을 생성한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- IN 절
EXPLAIN ANALYZE
SELECT * FROM users WHERE id IN (1, 2, 3, 4, 5);

-- ANY 연산자
EXPLAIN ANALYZE
SELECT * FROM users WHERE id = ANY(ARRAY[1, 2, 3, 4, 5]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행계획 (둘 다 동일)&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Index Scan using users_pkey on users
  Index Cond: (id = ANY ('{1,2,3,4,5}'::integer[]))
  Actual Time: 0.015..0.025 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 IN 절을 내부적으로 ANY로 변환한다. 적은 수의 값이면 성능 차이 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;케이스 2: 리터럴 값 목록 (많은 수)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 많아지면 이야기가 달라진다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 1000개 ID로 조회
EXPLAIN ANALYZE
SELECT * FROM users 
WHERE id IN (SELECT generate_series(1, 1000));
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행계획&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Nested Loop Semi Join
  -&amp;gt; Seq Scan on users
  -&amp;gt; Function Scan on generate_series
  Actual Time: 150.234..180.567 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1000개 이상이면 실행계획이 달라질 수 있다. 이럴 때는 임시 테이블이나 CTE를 활용하는 게 낫다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 임시 테이블 활용
CREATE TEMP TABLE temp_ids AS
SELECT generate_series(1, 1000) AS id;

EXPLAIN ANALYZE
SELECT u.* FROM users u
JOIN temp_ids t ON u.id = t.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행계획&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Hash Join
  Hash Cond: (u.id = t.id)
  -&amp;gt; Seq Scan on users u
  -&amp;gt; Hash
        -&amp;gt; Seq Scan on temp_ids t
  Actual Time: 45.123..78.456 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시 테이블 JOIN이 2배 이상 빠르다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;케이스 3: 서브쿼리 - IN vs EXISTS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 진짜 차이가 난다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IN 서브쿼리&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE status = 'completed');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행계획&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Hash Join
  Hash Cond: (users.id = orders.user_id)
  -&amp;gt; Seq Scan on users
  -&amp;gt; Hash
        -&amp;gt; HashAggregate
              -&amp;gt; Seq Scan on orders
                    Filter: (status = 'completed')
  Actual Time: 1234.567..1456.789 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EXISTS&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT * FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.user_id = u.id AND o.status = 'completed'
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행계획&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Hash Join
  Hash Cond: (u.id = o.user_id)
  -&amp;gt; Seq Scan on users u
  -&amp;gt; Hash
        -&amp;gt; HashAggregate
              -&amp;gt; Seq Scan on orders o
                    Filter: (status = 'completed')
  Actual Time: 1234.567..1456.789 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 10 이상에서는 IN과 EXISTS가 같은 실행계획을 생성한다. 옵티마이저가 똑똑해서 자동으로 최적화한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;케이스 4: NOT IN vs NOT EXISTS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부정 조건에서는 확실한 차이가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NOT IN (주의!)&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM orders WHERE status = 'completed');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행계획&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Seq Scan on users
  Filter: (NOT (hashed SubPlan 1))
  SubPlan 1
    -&amp;gt; Seq Scan on orders
          Filter: (status = 'completed')
  Actual Time: 2345.678..3456.789 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NOT EXISTS&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT * FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.user_id = u.id AND o.status = 'completed'
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행계획&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Hash Anti Join
  Hash Cond: (u.id = o.user_id)
  -&amp;gt; Seq Scan on users u
  -&amp;gt; Hash
        -&amp;gt; HashAggregate
              -&amp;gt; Seq Scan on orders o
                    Filter: (status = 'completed')
  Actual Time: 1234.567..1567.890 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NOT EXISTS가 약 2배 빠르다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NOT IN의 함정: NULL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NOT IN은 NULL이 있으면 예상과 다른 결과가 나온다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- user_id가 NULL인 주문이 있다면
INSERT INTO orders (user_id, amount, status) VALUES (NULL, 1000, 'completed');

-- NOT IN 결과: 아무것도 안 나올 수 있음!
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM orders WHERE status = 'completed');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL과 비교하면 UNKNOWN이 되어서 전체 결과가 영향받는다. NOT EXISTS는 이 문제가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론: NOT IN 대신 NOT EXISTS를 쓰자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;케이스 5: IN vs JOIN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때로는 JOIN이 더 빠르다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- IN 사용
EXPLAIN ANALYZE
SELECT * FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders WHERE amount &amp;gt; 50000);

-- JOIN 사용
EXPLAIN ANALYZE
SELECT DISTINCT u.* FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount &amp;gt; 50000;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 비슷하지만, 데이터 분포에 따라 JOIN이 유리할 수 있다. 실행계획을 확인하고 선택하자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;케이스 6: ANY와 배열&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANY는 배열과 함께 쓸 때 유용하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 배열 컬럼과 비교
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    tags TEXT[]
);

INSERT INTO products (name, tags) VALUES
('노트북', ARRAY['전자기기', '컴퓨터', '업무용']),
('마우스', ARRAY['전자기기', '주변기기']),
('책상', ARRAY['가구', '업무용']);

-- 특정 태그가 있는 상품 찾기
SELECT * FROM products
WHERE '업무용' = ANY(tags);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우에는 ANY가 자연스럽다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 비교 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10만 건 기준 테스트 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;케이스 IN EXISTS ANY 권장&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;리터럴 (소량)&lt;/td&gt;
&lt;td&gt;0.5ms&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;0.5ms&lt;/td&gt;
&lt;td&gt;아무거나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리터럴 (대량)&lt;/td&gt;
&lt;td&gt;150ms&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;145ms&lt;/td&gt;
&lt;td&gt;임시테이블+JOIN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서브쿼리&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;td&gt;아무거나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOT 조건&lt;/td&gt;
&lt;td&gt;3.4s&lt;/td&gt;
&lt;td&gt;1.5s&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;NOT EXISTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배열 비교&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;0.8ms&lt;/td&gt;
&lt;td&gt;ANY&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 가이드라인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 값이 적으면 (100개 이하) IN 사용&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 깔끔하고 읽기 좋음
SELECT * FROM users WHERE id IN (1, 2, 3);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 값이 많으면 임시 테이블 + JOIN&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1000개 이상이면 이 방식
WITH target_ids AS (
    SELECT unnest(ARRAY[1,2,3,...,1000]) AS id
)
SELECT u.* FROM users u
JOIN target_ids t ON u.id = t.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. NOT 조건은 무조건 EXISTS&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- NOT IN 대신 NOT EXISTS
SELECT * FROM users u
WHERE NOT EXISTS (
    SELECT 1 FROM blacklist b WHERE b.user_id = u.id
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 서브쿼리는 상관없음 (PostgreSQL 10+)&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;-- 둘 다 같은 실행계획
WHERE id IN (SELECT ...)
WHERE EXISTS (SELECT ... WHERE ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 실행계획 항상 확인&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스 활용 체크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IN 절이 인덱스를 타는지 확인하자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 인덱스 있는 컬럼
EXPLAIN SELECT * FROM users WHERE id IN (1, 2, 3);
-- Index Scan ✓

-- 인덱스 없는 컬럼
EXPLAIN SELECT * FROM users WHERE name IN ('user_1', 'user_2');
-- Seq Scan ✗ (인덱스 추가 고려)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PostgreSQL 17 개선사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 17에서는 IN 절 성능이 더 개선됐다. B-tree 인덱스를 사용하는 IN 절의 쿼리 성능이 향상됐다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- PostgreSQL 17에서 더 빨라진 패턴
SELECT * FROM large_table
WHERE indexed_column IN (val1, val2, val3, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황 권장&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;값 목록 (소량)&lt;/td&gt;
&lt;td&gt;IN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;값 목록 (대량)&lt;/td&gt;
&lt;td&gt;임시테이블 + JOIN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서브쿼리 (긍정)&lt;/td&gt;
&lt;td&gt;IN 또는 EXISTS (동일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서브쿼리 (부정)&lt;/td&gt;
&lt;td&gt;NOT EXISTS (필수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배열 비교&lt;/td&gt;
&lt;td&gt;ANY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NULL 가능성&lt;/td&gt;
&lt;td&gt;EXISTS (안전)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PostgreSQL 10 이상에서는 IN과 EXISTS가 대부분 동일하게 최적화됨&lt;/li&gt;
&lt;li&gt;NOT IN은 NULL 문제가 있으니 NOT EXISTS 사용&lt;/li&gt;
&lt;li&gt;항상 EXPLAIN ANALYZE로 실행계획 확인&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>PostgreSQL/고급</category>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/328</guid>
      <comments>https://dog-developers.tistory.com/328#entry328comment</comments>
      <pubDate>Mon, 19 Jan 2026 21:20:16 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 설치 Linux Ubuntu (2026년 최신) - APT로 설치하고 원격 접속까지</title>
      <link>https://dog-developers.tistory.com/327</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu에서 MySQL 설치하는 방법을 정리한다. APT 패키지 매니저로 쉽게 설치할 수 있다. 서버 운영을 고려해서 원격 접속 설정까지 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지원 Ubuntu 버전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu 버전 지원 MySQL&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;24.04 LTS&lt;/td&gt;
&lt;td&gt;MySQL 8.0, 8.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22.04 LTS&lt;/td&gt;
&lt;td&gt;MySQL 8.0, 8.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20.04 LTS&lt;/td&gt;
&lt;td&gt;MySQL 8.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Ubuntu 22.04 / 24.04 기준으로 작성했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 방법 선택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법 장점 단점&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;APT 기본 저장소&lt;/td&gt;
&lt;td&gt;간편함&lt;/td&gt;
&lt;td&gt;최신 버전 아닐 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL APT Repository&lt;/td&gt;
&lt;td&gt;최신 버전&lt;/td&gt;
&lt;td&gt;저장소 추가 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 둘 다 설명한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 1: APT 기본 저장소로 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법이다. Ubuntu 기본 저장소의 MySQL을 설치한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 패키지 목록 업데이트&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo apt update
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. MySQL Server 설치&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;sudo apt install mysql-server -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 중 root 비밀번호를 묻지 않는다. 설치 후 별도로 설정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3. 설치 확인&lt;/h3&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;mysql --version
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;mysql  Ver 8.0.xx-0ubuntu0.24.04.1 for Linux on x86_64 ((Ubuntu))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-4. 서비스 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo systemctl status mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;active (running) 상태면 정상&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법 2: MySQL 공식 APT Repository로 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 8.4 LTS나 9.x를 설치하려면 공식 저장소를 추가한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. MySQL APT Repository 다운로드&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;cd /tmp
wget https://dev.mysql.com/get/mysql-apt-config_0.8.32-1_all.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 번호는 변경될 수 있다. 공식 사이트에서 최신 버전 확인&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;https://dev.mysql.com/downloads/repo/apt/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. Repository 설치&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sudo dpkg -i mysql-apt-config_0.8.32-1_all.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 중 버전 선택 화면이 나온다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Which MySQL product do you wish to configure?
  MySQL Server &amp;amp; Cluster (Currently selected: mysql-8.4-lts)
  MySQL Tools &amp;amp; Connectors (Currently selected: Enabled)
  MySQL Preview Packages (Currently selected: Disabled)
  Ok
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql-8.4-lts 선택 후 Ok&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 패키지 목록 업데이트&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo apt update
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. MySQL 설치&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;sudo apt install mysql-server -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 중 root 비밀번호 입력 화면이 나온다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Enter root password: ********
Re-enter root password: ********
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-5. 인증 방식 선택&lt;/h3&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Use Strong Password Encryption (RECOMMENDED)
Use Legacy Authentication Method (Retain MySQL 5.x Compatibility)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 설치면 Strong Password 선택&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;초기 보안 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법 1로 설치했다면 root 비밀번호가 없다. 보안 설정 스크립트를 실행한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;sudo mysql_secure_installation
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 과정&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Securing the MySQL server deployment.

Connecting to MySQL using a blank password.

VALIDATE PASSWORD COMPONENT 설치?
Press y|Y for Yes, any other key for No: y

Please set the password strength level:
0 = LOW    (길이 8자 이상)
1 = MEDIUM (숫자, 대소문자, 특수문자 포함)
2 = STRONG (사전 단어 금지)
Enter 0, 1 or 2: 1

New password: ********
Re-enter new password: ********

Remove anonymous users? y
Disallow root login remotely? y
Remove test database? y
Reload privilege tables? y
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL 접속&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 1로 설치한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu에서 APT로 설치하면 root가 auth_socket 인증을 사용한다. sudo로 접속해야 한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 없이 바로 접속된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 2로 설치한 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 중 설정한 비밀번호로 접속&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;mysql -u root -p
Enter password: ********
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;접속 확인&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT VERSION();
SELECT USER();
SHOW DATABASES;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;root 인증 방식 변경 (선택)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auth_socket 대신 비밀번호 인증으로 변경하려면&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY '새비밀번호';
FLUSH PRIVILEGES;
EXIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 일반 로그인 가능&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;mysql -u root -p
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원격 접속 허용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 외부 접속을 허용하려면 추가 설정이 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. MySQL 설정 파일 수정&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bind-address 찾아서 수정&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 기본값 (로컬만 접속 가능)
bind-address = 127.0.0.1

# 모든 IP에서 접속 허용
bind-address = 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장 후 종료 (Ctrl+O, Enter, Ctrl+X)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. MySQL 재시작&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo systemctl restart mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 원격 접속용 사용자 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;root는 원격 접속 차단하고 별도 사용자를 만드는 게 안전하다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 사용자 생성 (모든 IP에서 접속 가능)
CREATE USER 'myuser'@'%' IDENTIFIED BY '비밀번호';

-- 권한 부여
GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'%';
FLUSH PRIVILEGES;

-- 특정 IP만 허용하려면
CREATE USER 'myuser'@'192.168.1.%' IDENTIFIED BY '비밀번호';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 방화벽 설정&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# UFW 사용 시
sudo ufw allow 3306/tcp
sudo ufw reload

# firewalld 사용 시
sudo firewall-cmd --permanent --add-port=3306/tcp
sudo firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 원격 접속 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 PC에서&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;mysql -h [서버IP] -u myuser -p
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스 관리&lt;/h2&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 상태 확인
sudo systemctl status mysql

# 시작
sudo systemctl start mysql

# 중지
sudo systemctl stop mysql

# 재시작
sudo systemctl restart mysql

# 부팅 시 자동 시작 활성화
sudo systemctl enable mysql

# 부팅 시 자동 시작 비활성화
sudo systemctl disable mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 파일 위치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항목 경로&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;설정 파일&lt;/td&gt;
&lt;td&gt;/etc/mysql/mysql.conf.d/mysqld.cnf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 디렉토리&lt;/td&gt;
&lt;td&gt;/var/lib/mysql&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로그 파일&lt;/td&gt;
&lt;td&gt;/var/log/mysql/error.log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;소켓 파일&lt;/td&gt;
&lt;td&gt;/var/run/mysqld/mysqld.sock&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 발생하는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Access denied for user 'root'@'localhost'&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu APT 설치 시 auth_socket 인증이 기본이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# sudo로 접속
sudo mysql

# 또는 인증 방식 변경
ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY '비밀번호';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Can't connect to MySQL server&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 실행 중인지 확인&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;sudo systemctl status mysql
sudo systemctl start mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원격 접속 안됨&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;bind-address 확인&lt;/li&gt;
&lt;li&gt;방화벽 확인&lt;/li&gt;
&lt;li&gt;사용자 권한 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT user, host FROM mysql.user;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;host가 'localhost'면 원격 접속 불가 '%' 또는 특정 IP가 있어야 함&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 충돌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB가 이미 설치되어 있으면 충돌한다.&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;# MariaDB 제거
sudo apt remove mariadb-server mariadb-client
sudo apt autoremove

# MySQL 설치
sudo apt install mysql-server
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;완전 삭제 (재설치 시)&lt;/h2&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# 서비스 중지
sudo systemctl stop mysql

# 패키지 삭제
sudo apt remove --purge mysql-server mysql-client mysql-common
sudo apt autoremove

# 데이터 디렉토리 삭제
sudo rm -rf /var/lib/mysql
sudo rm -rf /etc/mysql

# 잔여 설정 삭제
sudo apt clean
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계 명령어&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;설치 (기본)&lt;/td&gt;
&lt;td&gt;sudo apt install mysql-server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안 설정&lt;/td&gt;
&lt;td&gt;sudo mysql_secure_installation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;접속&lt;/td&gt;
&lt;td&gt;sudo mysql 또는 mysql -u root -p&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;원격 허용&lt;/td&gt;
&lt;td&gt;bind-address 수정 + 사용자 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu에서 MySQL 설치는 APT로 간단하게 끝난다. 서버 운영 시에는 원격 접속 설정과 보안에 신경 쓰자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://claude.ai/chat/44a4cb0b-5dab-4ee8-8067-26417b490838#&quot;&gt;MySQL 설치 Windows 11&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://claude.ai/chat/44a4cb0b-5dab-4ee8-8067-26417b490838#&quot;&gt;MySQL 설치 macOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MySQL 설치 Linux Ubuntu (현재 글)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>MySQL</category>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/327</guid>
      <comments>https://dog-developers.tistory.com/327#entry327comment</comments>
      <pubDate>Mon, 19 Jan 2026 21:17:04 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 설치 macOS (2026년 최신) - Homebrew로 5분만에 설치하기</title>
      <link>https://dog-developers.tistory.com/326</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;macOS에서 MySQL 설치하는 가장 쉬운 방법은 Homebrew다. 터미널 명령어 몇 줄이면 끝난다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버전 선택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 Homebrew 패키지 특징&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8.4 LTS&lt;/td&gt;
&lt;td&gt;mysql@8.4&lt;/td&gt;
&lt;td&gt;안정적, 장기지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최신 (9.x)&lt;/td&gt;
&lt;td&gt;mysql&lt;/td&gt;
&lt;td&gt;최신 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무나 학습용이면 8.4 LTS를 추천한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Homebrew 설치 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 열고 Homebrew가 설치되어 있는지 확인&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;brew --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치되어 있으면 버전이 표시된다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Homebrew 4.x.x
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Homebrew 미설치 시&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 후 터미널 재시작&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. MySQL 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.4 LTS 설치 (권장)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;brew install mysql@8.4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최신 버전 설치&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;brew install mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치에 2-3분 정도 소요된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 환경변수 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql@8.4로 설치하면 PATH에 자동 추가되지 않는다. 쉘 설정 파일에 추가해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;zsh 사용자 (기본)&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;echo 'export PATH=&quot;/opt/homebrew/opt/mysql@8.4/bin:$PATH&quot;' &amp;gt;&amp;gt; ~/.zshrc
source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;bash 사용자&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;echo 'export PATH=&quot;/opt/homebrew/opt/mysql@8.4/bin:$PATH&quot;' &amp;gt;&amp;gt; ~/.bash_profile
source ~/.bash_profile
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Intel Mac인 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로가 다르다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;echo 'export PATH=&quot;/usr/local/opt/mysql@8.4/bin:$PATH&quot;' &amp;gt;&amp;gt; ~/.zshrc
source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확인&lt;/h3&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;mysql --version
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;mysql  Ver 8.4.x for macos14.x on arm64 (Homebrew)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. MySQL 서비스 시작&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Homebrew Services로 시작 (권장)&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;brew services start mysql@8.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부팅 시 자동 시작된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수동으로 시작 (1회성)&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;/opt/homebrew/opt/mysql@8.4/bin/mysqld_safe --datadir=/opt/homebrew/var/mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;brew services list
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Name       Status  User   File
mysql@8.4  started myuser ~/Library/LaunchAgents/homebrew.mxcl.mysql@8.4.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 초기 보안 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 설치 직후에는 root 비밀번호가 없다. 보안 설정 스크립트를 실행한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;mysql_secure_installation
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 과정&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Securing the MySQL server deployment.

Connecting to MySQL using a blank password.

VALIDATE PASSWORD COMPONENT을 설치할까요?
Press y|Y for Yes, any other key for No: n
(비밀번호 정책 - 학습용은 No, 실무는 Yes)

New password: ********
Re-enter new password: ********
(root 비밀번호 설정)

Remove anonymous users?
Press y|Y for Yes, any other key for No: y
(익명 사용자 삭제 - Yes 권장)

Disallow root login remotely?
Press y|Y for Yes, any other key for No: y
(원격 root 로그인 차단 - Yes 권장)

Remove test database and access to it?
Press y|Y for Yes, any other key for No: y
(테스트 DB 삭제 - Yes 권장)

Reload privilege tables now?
Press y|Y for Yes, any other key for No: y
(권한 테이블 새로고침 - Yes)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 완료되면 보안 설정 끝&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. MySQL 접속 확인&lt;/h2&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;mysql -u root -p
Enter password: ********
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.4.x Homebrew

mysql&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버전 확인&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT VERSION();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스 목록&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SHOW DATABASES;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;종료&lt;/h3&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;EXIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 자주 쓰는 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 관리&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# 시작
brew services start mysql@8.4

# 중지
brew services stop mysql@8.4

# 재시작
brew services restart mysql@8.4

# 상태 확인
brew services list
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL 접속&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# root로 접속
mysql -u root -p

# 특정 DB로 바로 접속
mysql -u root -p mydb
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 디렉토리 위치&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# Apple Silicon Mac
/opt/homebrew/var/mysql

# Intel Mac
/usr/local/var/mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 파일 위치&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# Apple Silicon Mac
/opt/homebrew/etc/my.cnf

# Intel Mac
/usr/local/etc/my.cnf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. MySQL Workbench 설치 (선택)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GUI 툴이 필요하면 Workbench를 설치한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Homebrew Cask로 설치&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;brew install --cask mysqlworkbench
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;또는 공식 사이트에서 다운로드&lt;/h3&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;https://dev.mysql.com/downloads/workbench/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;macOS용 DMG 파일 다운로드 후 설치&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 자주 발생하는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;mysql 명령어를 찾을 수 없음&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;zsh: command not found: mysql
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결: 환경변수 설정 후 터미널 재시작&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소켓 에러&lt;/h3&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;ERROR 2002 (HY000): Can't connect to local MySQL server through socket
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결: MySQL 서비스가 실행 중인지 확인&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;brew services list
brew services restart mysql@8.4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비밀번호 분실&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서비스 중지&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;brew services stop mysql@8.4
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;안전 모드로 시작&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;mysqld_safe --skip-grant-tables &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;비밀번호 재설정&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;mysql -u root

mysql&amp;gt; FLUSH PRIVILEGES;
mysql&amp;gt; ALTER USER 'root'@'localhost' IDENTIFIED BY '새비밀번호';
mysql&amp;gt; EXIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서비스 재시작&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;brew services restart mysql@8.4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트 충돌&lt;/h3&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;ERROR 2002 (HY000): Can't connect to local MySQL server
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 MySQL이나 MariaDB가 실행 중일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 3306 포트 사용 프로세스 확인
lsof -i :3306

# 프로세스 종료
kill -9 [PID]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 완전 삭제 (재설치 시)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL을 완전히 삭제하고 재설치하려면&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 서비스 중지
brew services stop mysql@8.4

# 패키지 삭제
brew uninstall mysql@8.4

# 데이터 디렉토리 삭제
rm -rf /opt/homebrew/var/mysql

# 설정 파일 삭제
rm -rf /opt/homebrew/etc/my.cnf
rm -rf /opt/homebrew/etc/my.cnf.d
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계 명령어&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;설치&lt;/td&gt;
&lt;td&gt;brew install mysql@8.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATH 추가&lt;/td&gt;
&lt;td&gt;echo 'export PATH=...' &amp;gt;&amp;gt; ~/.zshrc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서비스 시작&lt;/td&gt;
&lt;td&gt;brew services start mysql@8.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안 설정&lt;/td&gt;
&lt;td&gt;mysql_secure_installation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;접속&lt;/td&gt;
&lt;td&gt;mysql -u root -p&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Homebrew 덕분에 macOS에서는 5분이면 MySQL 설치가 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 Linux(Ubuntu)에서 MySQL 설치하는 방법을 정리한다.&lt;/p&gt;</description>
      <category>MySQL</category>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/326</guid>
      <comments>https://dog-developers.tistory.com/326#entry326comment</comments>
      <pubDate>Mon, 19 Jan 2026 21:16:37 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 설치 Windows 11 (2026년 최신) - 다운로드부터 환경변수 설정까지</title>
      <link>https://dog-developers.tistory.com/325</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 1월 기준 MySQL 설치 방법을 정리한다. 현재 MySQL은 8.4 LTS와 9.x Innovation 두 가지 버전 라인이 있다. 입문자나 실무용으로는 8.4 LTS를 추천한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL 버전 선택 가이드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 타입 특징 추천 대상&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8.4 LTS&lt;/td&gt;
&lt;td&gt;장기지원&lt;/td&gt;
&lt;td&gt;안정적, 버그 수정 중심&lt;/td&gt;
&lt;td&gt;실무, 입문자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9.x&lt;/td&gt;
&lt;td&gt;Innovation&lt;/td&gt;
&lt;td&gt;새 기능 (Vector, JS 지원)&lt;/td&gt;
&lt;td&gt;최신 기능 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8.4 LTS는 2032년까지 지원 예정이라 오래 쓸 수 있다. 이 글에서는 8.4 LTS 기준으로 설치한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 설치 전 확인사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시스템 요구사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Windows 10 / 11 (64bit)&lt;/li&gt;
&lt;li&gt;최소 4GB RAM (8GB 권장)&lt;/li&gt;
&lt;li&gt;디스크 여유 공간 2GB 이상&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 MySQL 설치 여부 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 MySQL이나 MariaDB가 설치되어 있으면 충돌이 발생한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Windows + R &amp;rarr; services.msc 입력&lt;/li&gt;
&lt;li&gt;MySQL 또는 MariaDB 서비스가 있는지 확인&lt;/li&gt;
&lt;li&gt;있다면 제거 후 진행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 CMD에서 확인&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;netstat -an | findstr 3306
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3306 포트가 사용 중이면 다른 DB가 이미 설치된 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. MySQL 다운로드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다운로드 페이지 접속&lt;/h3&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;https://dev.mysql.com/downloads/installer/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 직접 다운로드&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;https://dev.mysql.com/downloads/windows/installer/8.0.html
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 파일 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 옵션이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 크기 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mysql-installer-web-community&lt;/td&gt;
&lt;td&gt;~2MB&lt;/td&gt;
&lt;td&gt;설치 중 다운로드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mysql-installer-community&lt;/td&gt;
&lt;td&gt;~300MB+&lt;/td&gt;
&lt;td&gt;오프라인 설치 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷이 안정적이면 web 버전, 불안정하면 전체 버전을 받는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그인 없이 다운로드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드 버튼 클릭 후 로그인 화면이 나오면 하단의 No thanks, just start my download. 클릭&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. MySQL 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 설치 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드한 파일 실행 &amp;rarr; 사용자 계정 컨트롤 [예] 클릭&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. Setup Type 선택&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;○ Developer Default    - 개발용 전체 패키지
○ Server only         - 서버만
○ Client only         - 클라이언트만
○ Full                - 모든 것
● Custom              - 선택 설치 (권장)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Custom을 선택한다. 필요한 것만 설치해서 깔끔하게 관리할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 설치할 항목 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 목록에서 아래 항목을 선택해서 오른쪽으로 이동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필수&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL Server 8.4.x - X64&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권장&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL Workbench 8.0.x - X64 (GUI 툴)&lt;/li&gt;
&lt;li&gt;MySQL Shell 8.4.x - X64 (CLI 툴)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Samples and Examples (학습용 샘플 DB)&lt;/li&gt;
&lt;li&gt;MySQL Documentation (오프라인 문서)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. Execute 클릭&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택한 항목이 다운로드 및 설치된다. 모든 항목에 초록색 체크가 표시되면 [Next]&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. MySQL Server 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. Type and Networking&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Config Type: Development Computer
             (개발용으로 리소스 적게 사용)

Connectivity:
☑ TCP/IP
  Port: 3306 (기본값 유지)
☑ Open Windows Firewall ports for network access
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3306 포트가 충돌나면 3307 등으로 변경&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Authentication Method&lt;/h3&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;● Use Strong Password Encryption (RECOMMENDED)
  - 보안 강화된 caching_sha2_password 사용
  
○ Use Legacy Authentication Method
  - 구버전 호환용 mysql_native_password 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 설치는 Strong Password 선택 (기본값)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래된 프로그램이나 PHP 구버전 연동 시 Legacy 선택&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. Accounts and Roles&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Root 비밀번호 설정&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;MySQL Root Password: ********
Repeat Password: ********
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호는 반드시 기억해야 한다. 분실하면 재설치하는 게 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요하면 [Add User]로 추가 계정 생성&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. Windows Service&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;☑ Configure MySQL Server as a Windows Service

Windows Service Name: MySQL84 (또는 MySQL80)

☑ Start the MySQL Server at System Startup
  - 부팅 시 자동 시작

Run Windows Service as:
● Standard System Account (권장)
○ Custom User
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 이름은 나중에 시작/중지할 때 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-5. Server File Permissions&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;● Yes, grant full access to the user ... (권장)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-6. Apply Configuration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Execute] 클릭하면 설정이 적용된다. 모든 항목에 체크 표시되면 [Finish]&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 설치 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL Command Line Client로 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 메뉴에서 MySQL 8.4 Command Line Client 실행&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Enter password: ********
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정한 root 비밀번호 입력&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;mysql&amp;gt; SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 8.4.x     |
+-----------+
1 row in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전이 표시되면 설치 성공&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 상태 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CMD에서&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;sc query MySQL84
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 services.msc에서 MySQL84 서비스 확인&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 환경변수 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경변수를 설정하면 CMD 어디서든 mysql 명령어를 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. MySQL 설치 경로 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 설치 경로&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;C:\Program Files\MySQL\MySQL Server 8.4\bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경로를 복사해둔다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. 환경변수 설정 방법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Windows + S &amp;rarr; &quot;환경 변수&quot; 검색&lt;/li&gt;
&lt;li&gt;&quot;시스템 환경 변수 편집&quot; 클릭&lt;/li&gt;
&lt;li&gt;[환경 변수] 버튼 클릭&lt;/li&gt;
&lt;li&gt;시스템 변수에서 Path 선택 &amp;rarr; [편집]&lt;/li&gt;
&lt;li&gt;[새로 만들기] 클릭&lt;/li&gt;
&lt;li&gt;복사한 경로 붙여넣기
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;C:\Program Files\MySQL\MySQL Server 8.4\bin
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;[확인] &amp;rarr; [확인] &amp;rarr; [확인]&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-3. 환경변수 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 CMD 창은 닫고 새로 열어야 적용된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;mysql -V
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;mysql  Ver 8.4.x for Win64 on x86_64 (MySQL Community Server - GPL)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전이 표시되면 환경변수 설정 완료&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 기본 사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;접속&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;mysql -u root -p
Enter password: ********
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스 목록 확인&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SHOW DATABASES;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스 생성&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE DATABASE mydb;
USE mydb;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;종료&lt;/h3&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;EXIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 자주 발생하는 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3306 포트 충돌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러: Port 3306 is already in use&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기존 MySQL/MariaDB 서비스 중지&lt;/li&gt;
&lt;li&gt;또는 다른 포트 (3307) 사용&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;netstat -ano | findstr 3306
taskkill /PID [프로세스ID] /F
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 시작 실패&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러: The service MySQL84 failed to start&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이벤트 뷰어에서 에러 로그 확인&lt;/li&gt;
&lt;li&gt;my.ini 설정 파일 확인&lt;/li&gt;
&lt;li&gt;데이터 디렉토리 권한 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환경변수 적용 안됨&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CMD 창을 새로 열었는지 확인&lt;/li&gt;
&lt;li&gt;경로에 오타 없는지 확인&lt;/li&gt;
&lt;li&gt;시스템 변수의 Path에 추가했는지 확인 (사용자 변수 X)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. MySQL 서비스 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 시작/중지 (CMD 관리자 권한)&lt;/h3&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;net start MySQL84
net stop MySQL84
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;sc query MySQL84
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 자동 시작 비활성화&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sc config MySQL84 start= demand
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발용으로만 쓸 때 필요할 때만 시작하려면 이렇게 설정&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;dev.mysql.com에서 MySQL Installer 다운로드&lt;/li&gt;
&lt;li&gt;Custom 설치로 필요한 것만 선택&lt;/li&gt;
&lt;li&gt;8.4 LTS 버전 권장 (장기 지원)&lt;/li&gt;
&lt;li&gt;환경변수에 bin 경로 추가&lt;/li&gt;
&lt;li&gt;CMD에서 mysql -V로 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 macOS에서 MySQL 설치하는 방법을 정리한다.&lt;/p&gt;</description>
      <category>MySQL</category>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/325</guid>
      <comments>https://dog-developers.tistory.com/325#entry325comment</comments>
      <pubDate>Mon, 19 Jan 2026 21:16:02 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL 테이블 생성 심화 - CTAS, 파티셔닝, 제약조건 완벽 가이드</title>
      <link>https://dog-developers.tistory.com/324</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;CREATE TABLE은 가장 기본적인 DDL이지만, 제대로 활용하면 성능과 유지보수에 큰 차이가 난다. 기본 문법은 이미 알고 있다고 가정하고, 실무에서 자주 쓰는 고급 기능들을 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CREATE TABLE IF NOT EXISTS&lt;/li&gt;
&lt;li&gt;CREATE TABLE AS SELECT (CTAS)&lt;/li&gt;
&lt;li&gt;제약조건 심화&lt;/li&gt;
&lt;li&gt;테이블 상속 (INHERITS)&lt;/li&gt;
&lt;li&gt;파티셔닝 (PARTITION BY)&lt;/li&gt;
&lt;li&gt;임시 테이블 (TEMPORARY)&lt;/li&gt;
&lt;li&gt;UNLOGGED 테이블&lt;/li&gt;
&lt;li&gt;LIKE 절로 테이블 복사&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CREATE TABLE IF NOT EXISTS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블이 이미 있으면 에러 없이 넘어간다. 스크립트 여러 번 실행할 때 유용하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 테이블이 있으면 에러 발생
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50)
);
-- ERROR: relation &quot;users&quot; already exists

-- 테이블이 있으면 무시
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50)
);
-- NOTICE: relation &quot;users&quot; already exists, skipping
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 스크립트나 초기화 스크립트에서 자주 쓴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. CREATE TABLE AS SELECT (CTAS)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT 결과로 테이블을 바로 생성한다. 데이터 백업이나 임시 테이블 만들 때 유용하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 기존 테이블 데이터로 새 테이블 생성
CREATE TABLE users_backup AS
SELECT * FROM users;

-- 특정 컬럼만 선택
CREATE TABLE user_names AS
SELECT id, name FROM users;

-- 조건 추가
CREATE TABLE active_users AS
SELECT * FROM users WHERE status = 'active';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 없이 구조만 복사&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- WHERE 1=0 으로 데이터 없이 구조만 복사
CREATE TABLE users_empty AS
SELECT * FROM users WHERE 1=0;

-- 또는 WITH NO DATA 사용 (PostgreSQL 9.0+)
CREATE TABLE users_empty AS
SELECT * FROM users WITH NO DATA;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CTAS로 만든 테이블은 제약조건이 복사되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 원본 테이블
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(100) UNIQUE NOT NULL
);

-- CTAS로 복사
CREATE TABLE users_copy AS SELECT * FROM users;

-- 제약조건 확인
\d users_copy
-- PRIMARY KEY, UNIQUE, NOT NULL 모두 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제약조건이 필요하면 LIKE 절을 사용하거나 별도로 추가해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 제약조건 심화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NOT NULL + DEFAULT 조합&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CHECK 제약조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값의 범위나 조건을 지정한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    price INT NOT NULL CHECK (price &amp;gt; 0),
    discount_rate DECIMAL(3,2) CHECK (discount_rate &amp;gt;= 0 AND discount_rate &amp;lt;= 1),
    status VARCHAR(20) CHECK (status IN ('active', 'inactive', 'deleted'))
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복합 UNIQUE 제약조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 컬럼의 조합이 유일해야 할 때&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE user_roles (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL,
    role_id INT NOT NULL,
    UNIQUE (user_id, role_id)  -- user_id + role_id 조합이 유일
);

-- 같은 유저에게 같은 역할 중복 불가
INSERT INTO user_roles (user_id, role_id) VALUES (1, 1);
INSERT INTO user_roles (user_id, role_id) VALUES (1, 1);  -- ERROR
INSERT INTO user_roles (user_id, role_id) VALUES (1, 2);  -- OK (다른 역할)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외래키 (FOREIGN KEY)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL REFERENCES users(id),
    product_id INT NOT NULL,
    FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ON DELETE 옵션&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CASCADE: 부모 삭제 시 자식도 삭제&lt;/li&gt;
&lt;li&gt;SET NULL: 부모 삭제 시 NULL로 변경&lt;/li&gt;
&lt;li&gt;SET DEFAULT: 부모 삭제 시 DEFAULT 값으로 변경&lt;/li&gt;
&lt;li&gt;RESTRICT: 자식이 있으면 부모 삭제 불가 (기본값)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 테이블 상속 (INHERITS)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 테이블의 구조를 상속받는다. PostgreSQL 고유 기능이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 부모 테이블
CREATE TABLE logs (
    id SERIAL PRIMARY KEY,
    message TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 자식 테이블 (부모 컬럼 + 추가 컬럼)
CREATE TABLE error_logs (
    error_code VARCHAR(10),
    stack_trace TEXT
) INHERITS (logs);

CREATE TABLE access_logs (
    ip_address INET,
    user_agent TEXT
) INHERITS (logs);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상속 테이블 조회&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 부모 테이블 조회 시 자식 데이터도 함께 조회됨
SELECT * FROM logs;

-- 부모 테이블만 조회 (ONLY 키워드)
SELECT * FROM ONLY logs;

-- 자식 테이블만 조회
SELECT * FROM error_logs;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의: 상속은 파티셔닝 이전에 쓰던 방식이다. PostgreSQL 10 이후로는 선언적 파티셔닝을 쓰는 게 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 파티셔닝 (PARTITION BY)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 테이블을 작은 단위로 나눈다. PostgreSQL 10부터 선언적 파티셔닝을 지원한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RANGE 파티셔닝 (범위 기준)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜별로 나누는 경우가 가장 많다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 부모 테이블 (파티션 정의)
CREATE TABLE logs (
    id SERIAL,
    message TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);

-- 파티션 테이블 생성
CREATE TABLE logs_2024_01 PARTITION OF logs
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE logs_2024_02 PARTITION OF logs
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

CREATE TABLE logs_2024_03 PARTITION OF logs
    FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LIST 파티셔닝 (값 기준)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 값별로 나눌 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE orders (
    id SERIAL,
    region VARCHAR(20) NOT NULL,
    amount INT
) PARTITION BY LIST (region);

CREATE TABLE orders_seoul PARTITION OF orders
    FOR VALUES IN ('seoul', 'incheon', 'gyeonggi');

CREATE TABLE orders_busan PARTITION OF orders
    FOR VALUES IN ('busan', 'ulsan', 'gyeongnam');

CREATE TABLE orders_etc PARTITION OF orders
    DEFAULT;  -- 나머지 값들
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HASH 파티셔닝 (해시 기준)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 균등하게 분산시킬 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE user_activities (
    id SERIAL,
    user_id INT NOT NULL,
    activity TEXT
) PARTITION BY HASH (user_id);

CREATE TABLE user_activities_0 PARTITION OF user_activities
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);

CREATE TABLE user_activities_1 PARTITION OF user_activities
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);

CREATE TABLE user_activities_2 PARTITION OF user_activities
    FOR VALUES WITH (MODULUS 4, REMAINDER 2);

CREATE TABLE user_activities_3 PARTITION OF user_activities
    FOR VALUES WITH (MODULUS 4, REMAINDER 3);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파티셔닝 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오래된 데이터 삭제가 빠름 (DROP PARTITION)&lt;/li&gt;
&lt;li&gt;특정 파티션만 스캔해서 성능 향상&lt;/li&gt;
&lt;li&gt;파티션별로 다른 테이블스페이스 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 임시 테이블 (TEMPORARY)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 끝나면 자동으로 삭제된다. 복잡한 쿼리의 중간 결과 저장할 때 유용하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 임시 테이블 생성
CREATE TEMPORARY TABLE temp_results (
    id INT,
    value TEXT
);

-- 또는 줄여서
CREATE TEMP TABLE temp_results (
    id INT,
    value TEXT
);

-- 세션 종료 시 자동 삭제
-- 다른 세션에서는 보이지 않음
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ON COMMIT 옵션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 단위로 데이터 관리&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 트랜잭션 끝나도 데이터 유지 (기본값)
CREATE TEMP TABLE temp1 (id INT) ON COMMIT PRESERVE ROWS;

-- 트랜잭션 끝나면 데이터 삭제 (테이블 구조는 유지)
CREATE TEMP TABLE temp2 (id INT) ON COMMIT DELETE ROWS;

-- 트랜잭션 끝나면 테이블 자체 삭제
CREATE TEMP TABLE temp3 (id INT) ON COMMIT DROP;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. UNLOGGED 테이블&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL(Write-Ahead Log)을 쓰지 않아서 빠르다. 서버 장애 시 데이터가 사라질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE UNLOGGED TABLE cache_data (
    key VARCHAR(100) PRIMARY KEY,
    value TEXT,
    expires_at TIMESTAMP
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 케이스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐시 테이블&lt;/li&gt;
&lt;li&gt;임시 작업용 테이블&lt;/li&gt;
&lt;li&gt;언제든 재생성 가능한 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10만 건 INSERT 기준&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 타입 소요시간&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;일반 테이블&lt;/td&gt;
&lt;td&gt;3.2초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNLOGGED 테이블&lt;/td&gt;
&lt;td&gt;0.8초&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 4배 빠르다. 단, 복제(Replication)가 안 되고 장애 시 데이터 손실 가능.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. LIKE 절로 테이블 복사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 테이블 구조를 복사한다. CTAS와 달리 제약조건도 복사할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 기본 (컬럼명, 타입, NOT NULL만 복사)
CREATE TABLE users_copy (LIKE users);

-- 모든 것 복사 (제약조건, 인덱스, DEFAULT 등)
CREATE TABLE users_copy (LIKE users INCLUDING ALL);

-- 선택적으로 복사
CREATE TABLE users_copy (
    LIKE users
    INCLUDING DEFAULTS
    INCLUDING CONSTRAINTS
    INCLUDING INDEXES
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INCLUDING 옵션&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DEFAULTS: DEFAULT 값&lt;/li&gt;
&lt;li&gt;CONSTRAINTS: CHECK 제약조건&lt;/li&gt;
&lt;li&gt;INDEXES: 인덱스&lt;/li&gt;
&lt;li&gt;STORAGE: STORAGE 설정&lt;/li&gt;
&lt;li&gt;COMMENTS: 코멘트&lt;/li&gt;
&lt;li&gt;GENERATED: GENERATED 컬럼&lt;/li&gt;
&lt;li&gt;ALL: 위 모든 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 테이블 생성 전 체크리스트&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 테이블명 컨벤션 (복수형 권장)
-- users, orders, products (O)
-- user, order, product (X)

-- 2. 컬럼 순서 (자주 조회하는 컬럼을 앞에)
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL,      -- 자주 조회
    status VARCHAR(20),         -- 자주 조회
    total_amount INT,
    created_at TIMESTAMP,
    -- ... 나머지 컬럼
    description TEXT            -- 큰 컬럼은 뒤에
);

-- 3. 적절한 데이터 타입 선택
-- INT vs BIGINT (20억 넘을 것 같으면 BIGINT)
-- VARCHAR(n) vs TEXT (길이 제한 필요하면 VARCHAR)
-- TIMESTAMP vs TIMESTAMPTZ (타임존 필요하면 TIMESTAMPTZ)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 대량 데이터 테이블은 처음부터 파티셔닝&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 나중에 파티셔닝 전환은 어렵다
-- 처음부터 파티셔닝으로 설계하자

CREATE TABLE logs (
    id BIGSERIAL,
    message TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) PARTITION BY RANGE (created_at);

-- 매월 파티션 자동 생성 스크립트 준비
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 개발/운영 환경 분리&lt;/h3&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;-- 개발 환경: 빠른 테스트용
CREATE UNLOGGED TABLE dev_test_data (...);

-- 운영 환경: 안전한 일반 테이블
CREATE TABLE prod_data (...);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 용도&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IF NOT EXISTS&lt;/td&gt;
&lt;td&gt;스크립트 반복 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CTAS&lt;/td&gt;
&lt;td&gt;데이터 백업, 임시 테이블&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LIKE INCLUDING ALL&lt;/td&gt;
&lt;td&gt;구조 + 제약조건 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PARTITION BY&lt;/td&gt;
&lt;td&gt;대용량 테이블 분할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TEMPORARY&lt;/td&gt;
&lt;td&gt;세션 내 임시 데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNLOGGED&lt;/td&gt;
&lt;td&gt;빠른 쓰기 (캐시용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 PostgreSQL IN 절 성능 최적화에 대해 정리한다.&lt;/p&gt;</description>
      <category>PostgreSQL/고급</category>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/324</guid>
      <comments>https://dog-developers.tistory.com/324#entry324comment</comments>
      <pubDate>Mon, 19 Jan 2026 21:08:51 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL UPDATE JOIN - 다른 테이블 참조해서 수정하기</title>
      <link>https://dog-developers.tistory.com/323</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;다른 테이블의 값을 참조해서 UPDATE 해야 할 때가 있다. 예를 들어 주문 테이블의 상품명을 상품 테이블에서 가져와서 수정하는 경우다. PostgreSQL에서는 FROM 절을 사용해서 JOIN UPDATE를 할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL과 문법이 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 UPDATE에 JOIN을 직접 쓴다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- MySQL 문법
UPDATE orders o
JOIN products p ON o.product_id = p.id
SET o.product_name = p.name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL에서는 이 문법이 안 된다. FROM 절을 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- PostgreSQL 문법
UPDATE orders o
SET product_name = p.name
FROM products p
WHERE o.product_id = p.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 PostgreSQL로 마이그레이션 할 때 자주 틀리는 부분이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 문법&lt;/h2&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;UPDATE
    TABLE_A a
SET
    a.column = b.column
FROM
    TABLE_B b
WHERE
    a.key = b.key;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FROM 절에 참조할 테이블을 작성&lt;/li&gt;
&lt;li&gt;WHERE 절에 조인 조건을 작성&lt;/li&gt;
&lt;li&gt;SET 절에서 참조 테이블의 컬럼 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트용 테이블을 만든다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 상품 테이블
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    price INT,
    category VARCHAR(50)
);

-- 주문 테이블
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    product_id INT,
    product_name VARCHAR(100),
    product_price INT,
    quantity INT,
    total_amount INT,
    order_date DATE DEFAULT CURRENT_DATE
);

-- 테스트 데이터
INSERT INTO products (name, price, category) VALUES
('노트북', 1500000, '전자기기'),
('마우스', 35000, '주변기기'),
('키보드', 89000, '주변기기'),
('모니터', 450000, '전자기기');

INSERT INTO orders (product_id, product_name, product_price, quantity) VALUES
(1, NULL, NULL, 2),
(2, NULL, NULL, 5),
(3, '구형키보드', 50000, 3),
(4, NULL, NULL, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 테이블에 상품 정보가 비어있거나 오래된 상태다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 1: 기본 UPDATE JOIN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 테이블에서 이름과 가격을 가져와서 주문 테이블을 수정한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE orders o
SET 
    product_name = p.name,
    product_price = p.price
FROM products p
WHERE o.product_id = p.id;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 확인&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt; id | product_id | product_name | product_price | quantity | total_amount
----+------------+--------------+---------------+----------+--------------
  1 |          1 | 노트북       |       1500000 |        2 |
  2 |          2 | 마우스       |         35000 |        5 |
  3 |          3 | 키보드       |         89000 |        3 |
  4 |          4 | 모니터       |        450000 |        1 |
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구형키보드가 키보드로, 가격도 89000으로 수정됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 2: 계산식과 함께 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조한 값으로 계산도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE orders o
SET 
    product_price = p.price,
    total_amount = p.price * o.quantity
FROM products p
WHERE o.product_id = p.id;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt; id | product_id | product_name | product_price | quantity | total_amount
----+------------+--------------+---------------+----------+--------------
  1 |          1 | 노트북       |       1500000 |        2 |      3000000
  2 |          2 | 마우스       |         35000 |        5 |       175000
  3 |          3 | 키보드       |         89000 |        3 |       267000
  4 |          4 | 모니터       |        450000 |        1 |       450000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 가격 &amp;times; 수량으로 total_amount가 계산됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 3: 조건 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 카테고리 상품만 수정할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE orders o
SET product_price = p.price * 0.9  -- 10% 할인
FROM products p
WHERE o.product_id = p.id
AND p.category = '주변기기';

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주변기기(마우스, 키보드)만 10% 할인 가격으로 수정된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 4: 여러 테이블 JOIN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FROM 절에 여러 테이블을 추가할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 카테고리 테이블 추가
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50),
    discount_rate DECIMAL(3,2) DEFAULT 0
);

INSERT INTO categories (name, discount_rate) VALUES
('전자기기', 0.05),
('주변기기', 0.10);

-- 상품 테이블에 category_id 추가
ALTER TABLE products ADD COLUMN category_id INT;
UPDATE products SET category_id = 1 WHERE category = '전자기기';
UPDATE products SET category_id = 2 WHERE category = '주변기기';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 테이블을 참조해서 UPDATE&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE orders o
SET 
    total_amount = (p.price * (1 - c.discount_rate)) * o.quantity
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE o.product_id = p.id;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리별 할인율이 적용된 금액으로 수정된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 5: 서브쿼리 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FROM 절 대신 서브쿼리로도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE orders
SET product_name = (
    SELECT name 
    FROM products 
    WHERE products.id = orders.product_id
)
WHERE product_id IN (SELECT id FROM products);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작은 하지만 성능이 좋지 않다. 행마다 서브쿼리가 실행되기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FROM 절 vs 서브쿼리 성능 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10만 건 기준 테스트 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법 소요시간 실행계획&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FROM 절 (JOIN)&lt;/td&gt;
&lt;td&gt;320ms&lt;/td&gt;
&lt;td&gt;Hash Join&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서브쿼리&lt;/td&gt;
&lt;td&gt;4200ms&lt;/td&gt;
&lt;td&gt;Nested Loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FROM 절 방식이 10배 이상 빠르다. 서브쿼리는 행마다 SELECT가 실행되고, FROM 절은 한번에 JOIN 처리된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행계획 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE도 EXPLAIN으로 실행계획을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)
UPDATE orders o
SET product_name = p.name
FROM products p
WHERE o.product_id = p.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Update on orders o  (cost=... rows=4)
  -&amp;gt;  Hash Join  (cost=... rows=4)
        Hash Cond: (o.product_id = p.id)
        -&amp;gt;  Seq Scan on orders o
        -&amp;gt;  Hash
              -&amp;gt;  Seq Scan on products p
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash Join이 나오면 정상이다. Nested Loop + Seq Scan 조합이 나오면 인덱스 추가를 고려한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자기 자신과 JOIN&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 테이블 내에서 다른 행을 참조할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 직원 테이블
CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50),
    manager_id INT,
    manager_name VARCHAR(50)
);

INSERT INTO employees (name, manager_id) VALUES
('김사장', NULL),
('이부장', 1),
('박과장', 2),
('최대리', 3);

-- 매니저 이름 채우기
UPDATE employees e
SET manager_name = m.name
FROM employees m
WHERE e.manager_id = m.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt; id |  name  | manager_id | manager_name
----+--------+------------+--------------
  1 | 김사장 |            |
  2 | 이부장 |          1 | 김사장
  3 | 박과장 |          2 | 이부장
  4 | 최대리 |          3 | 박과장
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RETURNING과 함께 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN UPDATE 결과도 바로 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPDATE orders o
SET 
    product_name = p.name,
    product_price = p.price
FROM products p
WHERE o.product_id = p.id
RETURNING o.id, o.product_name, o.product_price, p.category;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 테이블의 컬럼도 RETURNING에 포함할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 중복 매칭 주의&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FROM 절의 테이블과 1:N 관계면 문제가 생긴다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- products에 같은 상품이 여러 개 있으면
UPDATE orders o
SET product_name = p.name
FROM products p
WHERE o.product_id = p.id;
-- 어떤 행이 적용될지 보장 안됨
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1:1 또는 N:1 관계에서만 사용하거나, DISTINCT를 활용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 별칭 필수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 이름이 길면 별칭을 쓰는 게 좋다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 별칭 없이도 되지만
UPDATE orders
SET product_name = products.name
FROM products
WHERE orders.product_id = products.id;

-- 별칭 쓰는 게 가독성이 좋다
UPDATE orders o
SET product_name = p.name
FROM products p
WHERE o.product_id = p.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. MySQL 마이그레이션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 넘어올 때 문법 변환이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- MySQL
UPDATE a JOIN b ON a.id = b.a_id SET a.col = b.col;

-- PostgreSQL
UPDATE a SET col = b.col FROM b WHERE a.id = b.a_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN 위치가 다르다는 것만 기억하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PostgreSQL UPDATE JOIN은 FROM 절 사용&lt;/li&gt;
&lt;li&gt;MySQL과 문법이 다름 (JOIN &amp;rarr; FROM)&lt;/li&gt;
&lt;li&gt;서브쿼리보다 FROM 절이 10배 이상 빠름&lt;/li&gt;
&lt;li&gt;여러 테이블 JOIN 가능&lt;/li&gt;
&lt;li&gt;RETURNING과 함께 사용 가능&lt;/li&gt;
&lt;li&gt;1:N 관계에서는 중복 매칭 주의&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>PostgreSQL/고급</category>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/323</guid>
      <comments>https://dog-developers.tistory.com/323#entry323comment</comments>
      <pubDate>Mon, 19 Jan 2026 21:04:41 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL UPDATE RETURNING - 수정된 데이터 바로 가져오기</title>
      <link>https://dog-developers.tistory.com/322</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE를 실행하고 나서 수정된 데이터를 다시 SELECT 하는 경우가 많다. PostgreSQL에서는 RETURNING 절을 사용하면 UPDATE와 SELECT를 한번에 처리할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RETURNING이 필요한 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 이런 식으로 작성한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 1. 데이터 수정
UPDATE users
SET point = point + 100
WHERE id = 1;

-- 2. 수정된 데이터 조회
SELECT id, name, point FROM users WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 2번 실행해야 한다. API 개발할 때 이런 패턴이 자주 나온다. RETURNING을 쓰면 한번에 처리된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 문법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE 끝에 RETURNING 절을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE
    TABLE_NAME
SET
    COLUMN = VALUE
WHERE
    조건
RETURNING
    컬럼1, 컬럼2, ...;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT처럼 원하는 컬럼만 지정할 수 있고, * 로 전체 컬럼을 가져올 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트용 테이블을 만든다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100),
    point INT DEFAULT 0,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (name, email, point) VALUES
('김철수', 'kim@test.com', 1000),
('이영희', 'lee@test.com', 2500),
('박민수', 'park@test.com', 500);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 1: 수정된 값 바로 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트를 추가하고 결과를 바로 확인한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;UPDATE users
SET 
    point = point + 500,
    updated_at = CURRENT_TIMESTAMP
WHERE id = 1
RETURNING id, name, point, updated_at;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt; id |  name  | point |         updated_at
----+--------+-------+----------------------------
  1 | 김철수 |  1500 | 2024-01-15 14:30:25.123456
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE가 실행되면서 수정된 결과가 바로 반환된다. 별도로 SELECT를 실행할 필요가 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 2: 수정 전 값과 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 전 값은 가져올 수 없지만, 계산으로 유추할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;UPDATE users
SET point = point + 500
WHERE id = 1
RETURNING 
    id, 
    name, 
    point AS new_point, 
    point - 500 AS old_point;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt; id |  name  | new_point | old_point
----+--------+-----------+-----------
  1 | 김철수 |      2000 |      1500
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 3: 여러 행 UPDATE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건에 맞는 모든 행의 결과를 반환한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;UPDATE users
SET point = point * 2
WHERE point &amp;lt; 2000
RETURNING *;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; id |  name  |    email     | point |         updated_at
----+--------+--------------+-------+----------------------------
  1 | 김철수 | kim@test.com |  4000 | 2024-01-15 14:30:25.123456
  3 | 박민수 | park@test.com|  1000 | 2024-01-15 14:35:10.654321
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정된 행만 반환된다. 이영희는 point가 2500이라 조건에 안 맞아서 안 나온다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 4: 표현식 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RETURNING에서 계산이나 함수를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;UPDATE users
SET point = point - 100
WHERE id = 2
RETURNING 
    id,
    name,
    point,
    CASE 
        WHEN point &amp;gt;= 2000 THEN 'VIP'
        WHEN point &amp;gt;= 1000 THEN '일반'
        ELSE '신규'
    END AS grade;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt; id |  name  | point | grade
----+--------+-------+-------
  2 | 이영희 |  2400 | VIP
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 활용: API 응답 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel이나 Node.js에서 UPDATE 후 응답을 만들 때 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식 (쿼리 2번)&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// Laravel 예시
DB::update('UPDATE users SET point = point + ? WHERE id = ?', [100, $id]);
$user = DB::selectOne('SELECT * FROM users WHERE id = ?', [$id]);
return response()-&amp;gt;json($user);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RETURNING 사용 (쿼리 1번)&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;// Laravel 예시
$user = DB::selectOne('
    UPDATE users 
    SET point = point + ? 
    WHERE id = ? 
    RETURNING *
', [100, $id]);
return response()-&amp;gt;json($user);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 왕복이 줄어들어서 응답 속도가 빨라진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UPSERT와 함께 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT ... ON CONFLICT (UPSERT)에서도 RETURNING을 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INSERT INTO users (id, name, email, point)
VALUES (1, '김철수', 'kim@test.com', 100)
ON CONFLICT (id) DO UPDATE
SET 
    point = users.point + EXCLUDED.point,
    updated_at = CURRENT_TIMESTAMP
RETURNING 
    id, 
    name, 
    point,
    (xmax = 0) AS is_inserted;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt; id |  name  | point | is_inserted
----+--------+-------+-------------
  1 | 김철수 |  4100 | false
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;is_inserted가 true면 새로 추가된 것, false면 기존 데이터가 수정된 것이다. xmax는 PostgreSQL 내부 컬럼인데, 0이면 INSERT, 아니면 UPDATE다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DELETE에서도 사용 가능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제된 데이터를 확인할 때 유용하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DELETE FROM users
WHERE point &amp;lt; 500
RETURNING *;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제되기 전의 데이터가 반환된다. 실수로 삭제했을 때 복구용으로 쓸 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 트랜잭션 내에서 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RETURNING 결과를 받았다고 COMMIT 된 게 아니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;BEGIN;

UPDATE users SET point = 0 WHERE id = 1
RETURNING *;  -- 결과는 보이지만

ROLLBACK;  -- 롤백하면 원래대로
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. MySQL에는 없음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RETURNING은 PostgreSQL 전용이다. MySQL에서는 지원하지 않는다. MySQL에서 비슷하게 하려면 LAST_INSERT_ID()를 쓰거나 별도 SELECT가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 대량 UPDATE 시 주의&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100만 건을 UPDATE 하면서 RETURNING * 하면 100만 건이 반환된다. 메모리 문제가 생길 수 있으니 대량 처리 시에는 RETURNING을 빼거나 필요한 컬럼만 지정한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;INSERT에서도 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 생성된 ID를 바로 가져올 때 자주 쓴다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;INSERT INTO users (name, email, point)
VALUES ('최지우', 'choi@test.com', 0)
RETURNING id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt; id
----
  4
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SERIAL이나 UUID로 자동 생성되는 값을 바로 확인할 수 있다. 애플리케이션에서 다음 작업에 바로 사용 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1000건 UPDATE 기준으로 테스트했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법 소요시간 쿼리 수&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE + SELECT 분리&lt;/td&gt;
&lt;td&gt;45ms&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE RETURNING&lt;/td&gt;
&lt;td&gt;28ms&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 수가 절반으로 줄고, 네트워크 왕복도 줄어서 약 40% 빨라졌다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RETURNING은 UPDATE/INSERT/DELETE 결과를 바로 반환&lt;/li&gt;
&lt;li&gt;SELECT 없이 수정된 데이터 확인 가능&lt;/li&gt;
&lt;li&gt;API 개발 시 쿼리 수 절반으로 줄일 수 있음&lt;/li&gt;
&lt;li&gt;UPSERT (ON CONFLICT)와 함께 쓰면 강력함&lt;/li&gt;
&lt;li&gt;MySQL에는 없는 PostgreSQL 전용 기능&lt;/li&gt;
&lt;li&gt;대량 처리 시에는 메모리 주의&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 다른 테이블을 참조해서 UPDATE 하는 방법을 정리한다.&lt;/p&gt;</description>
      <category>PostgreSQL/고급</category>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/322</guid>
      <comments>https://dog-developers.tistory.com/322#entry322comment</comments>
      <pubDate>Mon, 19 Jan 2026 21:01:57 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL UPDATE 성능 최적화 - 대량 데이터 수정 시 주의사항</title>
      <link>https://dog-developers.tistory.com/321</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE는 단순히 데이터를 수정하는 것처럼 보이지만, 대량의 데이터를 처리할 때는 생각보다 많은 문제가 발생한다. 실무에서 100만 건 이상의 데이터를 UPDATE 할 때 겪었던 문제들과 해결 방법을 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UPDATE가 느린 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL에서 UPDATE는 내부적으로 DELETE + INSERT와 비슷하게 동작한다. 기존 행을 삭제 표시하고 새로운 행을 추가하는 방식이다. 이를 MVCC(Multi-Version Concurrency Control)라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 UPDATE를 많이 하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 크기가 커진다 (Dead Tuple 증가)&lt;/li&gt;
&lt;li&gt;인덱스도 새로 추가된다&lt;/li&gt;
&lt;li&gt;VACUUM이 필요해진다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 테이블의 Dead Tuple 확인
SELECT 
    relname,
    n_live_tup,
    n_dead_tup,
    round(n_dead_tup * 100.0 / nullif(n_live_tup + n_dead_tup, 0), 2) as dead_ratio
FROM pg_stat_user_tables
WHERE relname = 'link';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dead_ratio가 10% 이상이면 VACUUM을 고려해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대량 UPDATE 시 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100만 건을 한번에 UPDATE 하면 어떻게 될까&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 이렇게 하면 안됨
UPDATE user_log
SET status = 'archived'
WHERE created_at &amp;lt; '2024-01-01';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션이 너무 길어짐&lt;/li&gt;
&lt;li&gt;락이 오래 유지됨&lt;/li&gt;
&lt;li&gt;다른 작업이 대기 상태에 빠짐&lt;/li&gt;
&lt;li&gt;메모리 부족 가능성&lt;/li&gt;
&lt;li&gt;롤백 시 더 오래 걸림&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법 1: 배치 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번에 1000~10000건씩 나눠서 처리한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 배치로 나눠서 처리
DO $$
DECLARE
    batch_size INT := 5000;
    affected INT;
BEGIN
    LOOP
        UPDATE user_log
        SET status = 'archived'
        WHERE id IN (
            SELECT id FROM user_log
            WHERE created_at &amp;lt; '2024-01-01'
            AND status != 'archived'
            LIMIT batch_size
        );
        
        GET DIAGNOSTICS affected = ROW_COUNT;
        
        RAISE NOTICE '처리된 행: %', affected;
        
        COMMIT;
        
        EXIT WHEN affected = 0;
        
        -- 잠시 대기 (다른 트랜잭션에게 양보)
        PERFORM pg_sleep(0.1);
    END LOOP;
END $$;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 사이즈는 테이블 상황에 따라 조절한다. 보통 5000~10000 사이가 적당하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법 2: WHERE 조건에 인덱스 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE의 WHERE 조건에 인덱스가 없으면 풀스캔이 발생한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 인덱스 확인
SELECT indexname, indexdef 
FROM pg_indexes 
WHERE tablename = 'user_log';

-- 필요하면 인덱스 추가
CREATE INDEX CONCURRENTLY idx_user_log_created_at 
ON user_log(created_at) 
WHERE status != 'archived';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CONCURRENTLY 옵션을 사용하면 테이블 락 없이 인덱스를 생성할 수 있다. 운영 중인 서비스에서는 필수다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법 3: 실행계획 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE 전에 항상 실행계획을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)
UPDATE user_log
SET status = 'archived'
WHERE created_at &amp;lt; '2024-01-01'
AND status != 'archived';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Seq Scan이 나오면 인덱스 추가를 고려한다. Buffers 항목에서 shared hit이 낮고 read가 높으면 디스크 I/O가 많다는 의미다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법 4: 트리거 비활성화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블에 트리거가 있으면 UPDATE마다 실행된다. 대량 UPDATE 시에는 임시로 비활성화하는 것도 방법이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 트리거 비활성화
ALTER TABLE user_log DISABLE TRIGGER ALL;

-- UPDATE 실행
UPDATE user_log SET status = 'archived' WHERE ...;

-- 트리거 다시 활성화
ALTER TABLE user_log ENABLE TRIGGER ALL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 트리거가 하는 작업을 별도로 처리해야 할 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법 5: 새 테이블로 교체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 대량의 데이터를 수정해야 한다면 UPDATE 대신 새 테이블을 만드는 게 빠를 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 새 테이블 생성
CREATE TABLE user_log_new AS
SELECT 
    id,
    user_id,
    CASE 
        WHEN created_at &amp;lt; '2024-01-01' THEN 'archived'
        ELSE status
    END as status,
    created_at
FROM user_log;

-- 2. 인덱스, 제약조건 추가
ALTER TABLE user_log_new ADD PRIMARY KEY (id);
CREATE INDEX idx_user_log_new_user_id ON user_log_new(user_id);

-- 3. 테이블 교체
BEGIN;
ALTER TABLE user_log RENAME TO user_log_old;
ALTER TABLE user_log_new RENAME TO user_log;
COMMIT;

-- 4. 확인 후 기존 테이블 삭제
DROP TABLE user_log_old;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 테이블 전체의 50% 이상을 수정해야 할 때 고려한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UPDATE 후 VACUUM&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량 UPDATE 후에는 VACUUM을 실행해서 Dead Tuple을 정리한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 일반 VACUUM (테이블 락 없음)
VACUUM user_log;

-- 통계정보도 갱신
VACUUM ANALYZE user_log;

-- 공간까지 반환 (테이블 락 있음, 운영 중 주의)
VACUUM FULL user_log;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;autovacuum이 설정되어 있어도 대량 UPDATE 후에는 수동으로 실행하는 게 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 비교 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 100만 건 테이블에서 테스트한 결과다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법 소요시간 비고&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;한번에 UPDATE&lt;/td&gt;
&lt;td&gt;45초&lt;/td&gt;
&lt;td&gt;락 유지, 다른 작업 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배치 5000건&lt;/td&gt;
&lt;td&gt;62초&lt;/td&gt;
&lt;td&gt;총 시간은 늘지만 락 분산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배치 10000건&lt;/td&gt;
&lt;td&gt;55초&lt;/td&gt;
&lt;td&gt;적당한 균형&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;새 테이블 교체&lt;/td&gt;
&lt;td&gt;28초&lt;/td&gt;
&lt;td&gt;가장 빠름, 다운타임 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 따라 적절한 방법을 선택하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UPDATE는 내부적으로 DELETE + INSERT와 비슷하게 동작&lt;/li&gt;
&lt;li&gt;대량 UPDATE는 배치로 나눠서 처리&lt;/li&gt;
&lt;li&gt;WHERE 조건에 인덱스 필수&lt;/li&gt;
&lt;li&gt;실행계획 꼭 확인&lt;/li&gt;
&lt;li&gt;UPDATE 후 VACUUM 실행&lt;/li&gt;
&lt;li&gt;50% 이상 수정 시 새 테이블 교체 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 UPDATE와 함께 자주 쓰이는 RETURNING 절에 대해 정리한다.&lt;/p&gt;</description>
      <category>PostgreSQL/고급</category>
      <author>DOGvelopers</author>
      <guid isPermaLink="true">https://dog-developers.tistory.com/321</guid>
      <comments>https://dog-developers.tistory.com/321#entry321comment</comments>
      <pubDate>Mon, 19 Jan 2026 20:59:41 +0900</pubDate>
    </item>
  </channel>
</rss>