psql และ pgAdmin
ตอนนี้ PostgreSQL รันอยู่แล้ว คุณต้องมีวิธีคุยกับมัน มีประตูที่ใช้กันทั่วไปสามบาน: เชลล์ command-line psql, เครื่องมือกราฟิกอย่าง pgAdmin และโค้ดแอปพลิเคชันของคุณเองผ่าน driver บทเรียนนี้ครอบคลุมทั้งสามอย่าง เพื่อให้คุณเลือกอันที่เหมาะกับสถานการณ์ได้
ทุกอย่างด้านล่างรันกับ PostgreSQL 18.6 (
postgres:18.6, psql 18.6 ภายใน container, aarch64, Docker Desktop บน Mac, กันยายน 2026) ส่วน GUI (pgAdmin, DBeaver) อธิบายตามเอกสารของเครื่องมือและ ไม่ได้ รันที่นี่
พื้นฐาน psql
หัวข้อที่มีชื่อว่า “พื้นฐาน psql”psql คือ client บน terminal อย่างเป็นทางการ เมื่อเชื่อมต่อแล้วคุณพิมพ์ SQL ตรง ๆ ได้ แต่พลังจริงของมันคือ meta-command — คำสั่งสั้น ๆ ที่ขึ้นต้นด้วย backslash และตรวจสอบฐานข้อมูลให้คุณ ต่อไปนี้คือคำสั่งที่คุณจะใช้อยู่ตลอด
| คำสั่ง | ทำอะไร |
|---|---|
\l | แสดงรายการฐานข้อมูลทั้งหมดในเซิร์ฟเวอร์ |
\c dbname | เชื่อมต่อไปยังอีกฐานข้อมูลหนึ่ง |
\dt | แสดงรายการ table ในฐานข้อมูลปัจจุบัน |
\d tablename | อธิบาย column, type และ constraint ของ table (\d+ เพิ่ม storage, stats target และอื่น ๆ) |
\x | สลับการแสดงผลแบบขยาย (หนึ่ง column ต่อหนึ่งบรรทัด) |
\timing | แสดงว่าแต่ละ statement ใช้เวลาเท่าไร |
\watch | รัน query ล่าสุดซ้ำทุกไม่กี่วินาที |
\copy | โหลดหรือส่งออกไฟล์จากเครื่อง client |
\conninfo | แสดงว่าคุณเชื่อมต่อกับอะไรอยู่ |
\q | ออกจาก psql |
เริ่มจากสำรวจเซิร์ฟเวอร์ใหม่ นี่คือ \l จริงจาก PostgreSQL 18.6 ใน Docker image: cluster ใหม่มี postgres, template0 และ template1 บวกฐานข้อมูล appdb ที่สร้างจาก POSTGRES_DB ในบทก่อน สังเกต column Locale Provider, Locale และ ICU Rules ที่บทเรียนเก่า ๆ ไม่มี และ locale ของ Docker image คือ en_US.utf8:
$ psql -U postgres -c "\l" List of databases Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges-----------+----------+----------+-----------------+------------+------------+--------+-----------+----------------------- appdb | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | postgres | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | template0 | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/postgres + | | | | | | | | postgres=CTc/postgres(4 rows)
$ psql -U postgres -d appdb -c "\dt" -c "SELECT 1 + 1 AS answer"Did not find any tables. answer-------- 2(1 row)การรัน SQL คือพิมพ์ statement แล้วปิดด้วย semicolon meta-command ช่วยให้คุณไม่ต้องจำ system query เบื้องหลัง \l, \dt และพวก — มันรัน query เหล่านั้นแล้วจัดรูปผลลัพธ์ให้ (\dt บนฐานข้อมูลเปล่าตอบว่า Did not find any tables.; ส่วน \d name กับ object ที่ไม่มีตอบว่า Did not find any relations.)
เซสชันจริงบนชุดข้อมูลของคอร์ส
หัวข้อที่มีชื่อว่า “เซสชันจริงบนชุดข้อมูลของคอร์ส”รันชุดข้อมูลมาตรฐานจากบทแรก แล้วลอง meta-command ที่สำคัญที่สุด ทุกบล็อกด้านล่างคือการรันจริง
-- sql/setup.sqlCREATE TABLE authors ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, born int);CREATE TABLE books ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text NOT NULL, author_id bigint REFERENCES authors (id), published int, copies_sold int NOT NULL DEFAULT 0);CREATE TABLE sales ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, book_id bigint REFERENCES books (id), region text NOT NULL, copies int NOT NULL);INSERT INTO authors (name, born) VALUES ('Mara Linde', 1968), ('Owen Pryce', 1975), ('Sela Vance', 1990);INSERT INTO books (title, author_id, published, copies_sold) VALUES ('Quiet Harbors', 1, 2003, 450), ('The Glass Orchard', 1, 2018, 1250), ('Tidal Notes', 2, 2011, 600), ('Unlinked Draft', NULL, 2020, 0);INSERT INTO sales (book_id, region, copies) VALUES (2, 'East', 900), (3, 'West', 400), (1, 'West', 400), (3, 'East', 200), (2, 'West', 150), (1, 'East', 50);\dt และ \d+ แสดงรูปร่าง PostgreSQL 18 ยังแสดง constraint NOT NULL พร้อมชื่อใต้ Not-null constraints: ซึ่งเวอร์ชันเก่าไม่มี:
-- sql/psql-describe.sql\dt\d+ books List of tables Schema | Name | Type | Owner--------+---------+-------+---------- public | authors | table | postgres public | books | table | postgres public | sales | table | postgres(3 rows)
Table "public.books" Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description-------------+---------+-----------+----------+------------------------------+----------+-------------+--------------+------------- id | bigint | | not null | generated always as identity | plain | | | title | text | | not null | | extended | | | author_id | bigint | | | | plain | | | published | integer | | | | plain | | | copies_sold | integer | | not null | 0 | plain | | |Indexes: "books_pkey" PRIMARY KEY, btree (id)Foreign-key constraints: "books_author_id_fkey" FOREIGN KEY (author_id) REFERENCES authors(id)Referenced by: TABLE "sales" CONSTRAINT "sales_book_id_fkey" FOREIGN KEY (book_id) REFERENCES books(id)Not-null constraints: "books_id_not_null" NOT NULL "id" "books_title_not_null" NOT NULL "title" "books_copies_sold_not_null" NOT NULL "copies_sold"Access method: heap\x สลับเป็นการแสดงผลแบบขยาย ทางออกเมื่อ table กว้างจนห่อบรรทัดอ่านไม่ออก:
-- sql/psql-expanded.sql\x onSELECT * FROM authors WHERE id = 1;\x offExpanded display is on.-[ RECORD 1 ]----id | 1name | Mara Lindeborn | 1968
Expanded display is off.\timing พิมพ์เวลาที่ผ่านไปฝั่ง client (Mac เครื่องนี้ table เล็กมาก จึงเป็นเสี้ยวมิลลิวินาที ตัวเลขของคุณจะต่างออกไป):
-- sql/psql-timing.sql\timing onSELECT count(*) FROM sales;\timing offTiming is on. count------- 6(1 row)
Time: 0.697 msTiming is off.\watch รัน query ซ้ำทุก i วินาที จำนวน c ครั้ง (ถ้าไม่ใส่ c จะรันไปเรื่อย ๆ จนกว่าคุณจะขัดจังหวะ):
-- sql/psql-watch.sqlSELECT sum(copies) AS total FROM sales \watch i=1 c=2Wed 30 Sep 2026 05:53:02 AM UTC (every 1s)
total------- 2100(1 row)
Wed 30 Sep 2026 05:53:03 AM UTC (every 1s)
total------- 2100(1 row)\copy รัน COPY ผ่านฝั่ง client ไฟล์จึงอยู่ในเครื่องที่รัน psql ไม่ใช่บนเซิร์ฟเวอร์ นั่นคือเหตุผลที่มันใช้ได้บนฐานข้อมูลแบบ managed ที่คุณไม่มี filesystem ของเซิร์ฟเวอร์ ตัวอย่างนี้ส่งออก books เป็น CSV แล้วโหลดเข้า table สำเนา:
-- sql/psql-copy.sql\copy books TO '/tmp/books.csv' WITH (FORMAT csv, HEADER)CREATE TABLE books_copy (LIKE books INCLUDING ALL);\copy books_copy (id, title, author_id, published, copies_sold) FROM '/tmp/books.csv' WITH (FORMAT csv, HEADER)SELECT count(*) FROM books_copy;COPY 4CREATE TABLECOPY 4 count------- 4(1 row)ตัวแปรของ psql ขยายด้วย colon เป็นความสะดวกสำหรับสคริปต์ ไม่ใช่สิ่งทดแทนการ bind parameter ที่ driver ทำให้:
-- sql/psql-variables.sql\set min_year 2010SELECT title FROM books WHERE published >= :min_year ORDER BY published; title------------------- Tidal Notes The Glass Orchard Unlinked Draft(3 rows)สคริปต์: -c, -f, -1 และ ON_ERROR_STOP
หัวข้อที่มีชื่อว่า “สคริปต์: -c, -f, -1 และ ON_ERROR_STOP”การใช้งานแบบโต้ตอบเป็นแค่ครึ่งหนึ่งของ psql -c รันคำสั่งเดียว -f รันไฟล์ และ flag ที่กำหนดว่าจะเกิดอะไรเมื่อมีสิ่งล้มเหลวคือสิ่งที่สำคัญในสคริปต์ ใช้ migration เล็ก ๆ นี้ ซึ่งบรรทัดที่สามตั้งใจให้ล้มเหลว:
CREATE TABLE notes (id int PRIMARY KEY, body text);INSERT INTO notes VALUES (1, 'first');INSERT INTO notes VALUES (1, 'duplicate id');INSERT INTO notes VALUES (2, 'second');สามโหมดทำงานต่างกัน transcript จริง:
$ psql -U postgres -d appdb -f migrate.sql # default: keeps going after an errorCREATE TABLEINSERT 0 1psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"DETAIL: Key (id)=(1) already exists.INSERT 0 12 rows in notes
$ psql -U postgres -d appdb -v ON_ERROR_STOP=1 -f migrate.sql # stop at the first errorCREATE TABLEINSERT 0 1psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"DETAIL: Key (id)=(1) already exists.exit status 31 rows in notes
$ psql -U postgres -d appdb -1 -v ON_ERROR_STOP=1 -f migrate.sql # -1: one transaction, all or nothingCREATE TABLEINSERT 0 1psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"DETAIL: Key (id)=(1) already exists.exit status 3tกฎที่ต้องจำ: โดยค่าเริ่มต้น psql ทำงานต่อและ exit status เป็น 0 แม้เกิด error; -v ON_ERROR_STOP=1 หยุดที่ error แรกด้วย exit status 3; การเพิ่ม -1 (--single-transaction) ครอบไฟล์ด้วย BEGIN/COMMIT ความล้มเหลวจึงไม่ทิ้งอะไรไว้ (psql reference)
ทัวร์ pgAdmin
หัวข้อที่มีชื่อว่า “ทัวร์ pgAdmin”ถ้าคุณชอบคลิกมากกว่าพิมพ์ pgAdmin คือเครื่องมือกราฟิกอย่างเป็นทางการ และการตั้งค่า Docker จากบทก่อนรันมันที่ http://localhost:8080 ได้ (ไม่ได้รันที่นี่ ส่วนนี้ตาม เอกสารของ pgAdmin) นี่คือวิธีหาทางในนั้น
เมื่อเปิด pgAdmin ครั้งแรก ให้ล็อกอินด้วยอีเมลและรหัสผ่านที่ตั้งไว้ในไฟล์ compose ด้านซ้ายแสดง browser tree โดยมีกลุ่ม Servers อยู่บนสุด เพื่อเชื่อมต่อกับฐานข้อมูลของคุณ ให้คลิกขวาที่ Servers แล้วเลือกลงทะเบียน server ใหม่ ในกล่องโต้ตอบ ตั้งชื่อการเชื่อมต่อในแท็บ General จากนั้นสลับไปแท็บ Connection แล้วกรอก host, port, username และ password เพราะ pgAdmin รันใน container ของตัวเอง host จึงเป็นชื่อ service postgres ไม่ใช่ localhost ส่วน port ยังเป็น 5432 และ user คือ postgres
เมื่อเชื่อมต่อแล้ว tree จะขยาย เปิด server ของคุณ แล้ว Databases แล้วฐานข้อมูล แล้ว Schemas แล้ว schema public และสุดท้าย Tables ลำดับชั้นนี้สะท้อนวิธีที่ PostgreSQL จัดระเบียบข้อมูลจริง ซึ่งบทถัดไปจะอธิบายละเอียด การคลิก object ใด ๆ จะแสดงรายละเอียดใน panel หลัก
เครื่องมือหลักคือ Query Tool เปิดจากแถบเครื่องมือหรือคลิกขวาที่ฐานข้อมูล มี SQL editor ให้พิมพ์ statement แล้วกดปุ่มรัน ผลลัพธ์แสดงเป็น grid ด้านล่าง และข้อความอย่างจำนวน row อยู่ในอีกแท็บ เพื่อดูเนื้อหา table โดยไม่ต้องเขียน SQL ให้คลิกขวาที่ table แล้วเลือกดู row ซึ่งเปิด grid เดียวกันที่เติม SELECT ไว้ให้แล้ว
ถ้า pgAdmin รู้สึกหนัก DBeaver เป็นทางเลือกฟรียอดนิยม — แอปเดสก์ท็อปที่เชื่อมต่อ PostgreSQL และฐานข้อมูลอื่นอีกมากด้วยรูปแบบ tree-and-editor ที่คล้ายกัน แนวคิดใช้ต่อกันได้ตรง ๆ ต่างกันแค่เมนู
เชื่อมต่อจากแต่ละ driver
หัวข้อที่มีชื่อว่า “เชื่อมต่อจากแต่ละ driver”ในโค้ดแอปพลิเคชันคุณเชื่อมต่อผ่าน driver — ไลบรารีที่พูดโปรโตคอลของ PostgreSQL รายละเอียดการเชื่อมต่อเหมือนที่คุณใช้ข้างบน ต่างกันแค่ syntax แต่ละโปรแกรมด้านล่างอ่าน URL จากตัวแปรสภาพแวดล้อม DATABASE_URL (ห้าม commit) และรันจริงหนึ่งครั้งกับ container PostgreSQL 18.6 ชั่วคราว
-- sql/connect.sql-- psql takes the connection string on the command line:-- psql "postgresql://postgres:<password>@localhost:5432/appdb"SELECT current_database();import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const res = await pool.query('SELECT current_database(), current_user');console.log(res.rows[0]);await pool.end();import osimport psycopg
with psycopg.connect(os.environ["DATABASE_URL"]) as conn: cur = conn.cursor() cur.execute("SELECT current_database(), current_user") print(cur.fetchone())package main
import ( "context" "fmt" "os"
"github.com/jackc/pgx/v5")
func main() { ctx := context.Background() conn, err := pgx.Connect(ctx, os.Getenv("DATABASE_URL")) if err != nil { fmt.Println(err) os.Exit(1) } defer conn.Close(ctx)
var db, user string if err := conn.QueryRow(ctx, "SELECT current_database(), current_user").Scan(&db, &user); err != nil { fmt.Println(err) os.Exit(1) } fmt.Println(db, user)}use sqlx::postgres::PgPoolOptions;
#[tokio::main]async fn main() -> Result<(), sqlx::Error> { let pool = PgPoolOptions::new() .max_connections(5) .connect(&std::env::var("DATABASE_URL").unwrap()) .await?;
let row: (String, String) = sqlx::query_as("SELECT current_database()::text, current_user::text") .fetch_one(&pool) .await?; println!("{:?}", row); Ok(())}output จริงของแต่ละตัว (ระบุ version ในแต่ละบรรทัด):
$ DATABASE_URL=postgresql://postgres:<password>@localhost:5432/appdb node app.mjs # pg 8.23.0, Node 24.13.0{ current_database: 'appdb', current_user: 'postgres' }
$ python app.py # psycopg 3.3.6, Python 3.12.2('appdb', 'postgres')
$ go run . # pgx v5.11.0, Go 1.27.1appdb postgres
$ cargo run # sqlx 0.9.0, Rust 1.98.1("appdb", "postgres")สังเกตว่า driver ทุกตัวรับ connection string รูปแบบเดียวกัน postgresql://user:password@host:port/database เรียนรู้รูปแบบเดียวนี้แล้วคุณเชื่อมต่อได้จากทุกที่
ใน pgAdmin: รายละเอียดการเชื่อมต่อเดียวกันอยู่ในกล่องลงทะเบียน server แทนที่จะเป็น string — host, port, user, password และฐานข้อมูลแต่ละอย่างมีช่องของตัวเอง
Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”เมื่อขอให้ผู้ช่วย AI เขียนสคริปต์ deploy ที่รัน migration แล้วรายงานความสำเร็จ มันเขียนแบบนี้ อ่านแล้วดูถูกต้อง และอันตราย:
🐛 หา bug ในโค้ดนี้
#!/usr/bin/env bash# AI comment: "apply the migration, then report success"psql -U postgres -d appdb -f migrate.sqlecho "migrations applied"เฉลย
กับ migrate.sql ข้างบน (statement ที่สามล้มเหลว) การรันสคริปต์จะพิมพ์ error จากนั้นพิมพ์ migrations applied และ exit ด้วย status 0:
CREATE TABLEINSERT 0 1psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"DETAIL: Key (id)=(1) already exists.INSERT 0 1migrations applieddeploy.sh exit status 0โดยค่าเริ่มต้น psql ไม่หยุดเมื่อเจอ error และไม่ทำให้สคริปต์ล้มเหลว: row ที่ซ้ำถูกข้าม แต่ทุกอย่างหลังจากนั้นยังรันต่อ งาน CI เห็น build เป็นสีเขียว และฐานข้อมูลถูก migrate ไปครึ่งเดียว วิธีแก้คือ -v ON_ERROR_STOP=1 เพื่อให้ error แรกหยุดการรัน, -1 เพื่อไม่ทิ้งอะไรค้าง, set -e เพื่อให้เชลล์สคริปต์หยุด และ -X เพื่อไม่ให้ ~/.psqlrc ที่หลงเหลืออยู่เปลี่ยนพฤติกรรม การรันเดิมตอนนี้ล้มเหลวด้วย status 3 และ transaction ถูก rollback (table notes ไม่มีอยู่หลังจากนั้น):
set -euo pipefailpsql -X -U postgres -d appdb -v ON_ERROR_STOP=1 -1 -f migrate.sqlecho "migrations applied"CREATE TABLEINSERT 0 1psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"DETAIL: Key (id)=(1) already exists.deploy2.sh exit status 3Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”psql เป็น client บางเบาที่ส่งข้อความไปยังเซิร์ฟเวอร์และพิมพ์สิ่งที่ตอบกลับ meta-command (backslash) ทำงานฝั่ง client และไม่เคยไปถึงเซิร์ฟเวอร์ ส่วนอย่างอื่น (SQL) ไปถึง นั่นอธิบายว่าทำไม \copy อ่านไฟล์ในเครื่องคุณ ทำไม \watch เป็นลูปของ psql ไม่ใช่ฟีเจอร์ของฐานข้อมูล และทำไมสคริปต์ต้องมี ON_ERROR_STOP: client ไม่ใช่เซิร์ฟเวอร์ที่ตัดสินว่าจะทำต่อหรือไม่
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- connection string มีรูปแบบ
postgresql://user:password@host:port/databaseแต่ละส่วนเป็นทางเลือกเมื่อมีค่าเริ่มต้นรองรับ - จากภายใน container อื่น (เช่น pgAdmin) host คือชื่อ service ไม่ใช่
localhostส่วนจากเครื่องของคุณเองคือlocalhost - ใช้
psqlสำหรับตรวจสอบเร็ว ๆ และเขียนสคริปต์ ใช้ GUI อย่าง pgAdmin หรือ DBeaver สำหรับเรียกดูและสำรวจ และใช้ driver สำหรับทุกอย่างที่แอปพลิเคชันของคุณทำซ้ำ ๆ - ห้าม hard-code รหัสผ่านในโค้ดที่ commit อ่าน connection string จากตัวแปรสภาพแวดล้อม (เหมือนโปรแกรม driver ด้านบน) หรือไฟล์
~/.pgpass<password>ด้านบนไม่ใช่ค่าจริง - กลุ่ม meta-command
\dคือวิธีที่เร็วที่สุดในการจำรูปร่างที่แน่นอนของ table โดยไม่ต้องออกจากเชลล์ - ในสคริปต์ ใส่
-X -v ON_ERROR_STOP=1เสมอ และเพิ่ม-1สำหรับ migration
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
psql CLI | เร็ว เขียนสคริปต์ได้ overhead ต่ำ | ต้องจำ meta-command และ syntax |
| pgAdmin GUI | เห็นโครงสร้างข้อมูลชัดเจน เป็นมิตรกับผู้เริ่มต้น | หนักกว่า และต้องมีเครื่องเดสก์ท็อปที่เปิดเบราว์เซอร์ |
| driver ในโค้ดแอปพลิเคชัน | สิ่งที่ production รันจริงทุกวัน | การเขียนโค้ดเพิ่มเพียงเพื่อรัน query เดียวไม่คุ้ม |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “ใช้ GUI อย่างเดียวก็พอ” วินาทีที่คุณ SSH เข้าเครื่อง production ที่ไม่มี GUI คุณจะติดถ้าไม่มี
\dt,\dและพวก - “สคริปต์พิมพ์
migrations appliedแปลว่าสำเร็จ” ไม่จริงถ้าไม่มีON_ERROR_STOP: psql exit0หลัง error เป็นค่าเริ่มต้น (พิสูจน์ด้านบน) - “pgAdmin และ DBeaver เชื่อมต่อผ่าน
localhost” จากเครื่อง host ใช่ แต่จาก container อื่นต้องใช้ชื่อ service เช่นpostgresตามที่ตั้งใน compose - “
\copyกับCOPYเหมือนกัน”COPYอ่านและเขียนไฟล์บน เซิร์ฟเวอร์ ส่วน\copyใช้ไฟล์บน client - “คัดลอก SQL ข้ามเครื่องมือไม่เป็นไร” psql และ GUI บางตัวจัดการ autocommit ต่างกัน
BEGINที่คัดลอกมาโดยไม่มีCOMMITอาจทิ้ง transaction ค้างไว้