ข้ามไปยังเนื้อหา

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 คือ 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.sql
CREATE 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 on
SELECT * FROM authors WHERE id = 1;
\x off
Expanded display is on.
-[ RECORD 1 ]----
id | 1
name | Mara Linde
born | 1968
Expanded display is off.

\timing พิมพ์เวลาที่ผ่านไปฝั่ง client (Mac เครื่องนี้ table เล็กมาก จึงเป็นเสี้ยวมิลลิวินาที ตัวเลขของคุณจะต่างออกไป):

-- sql/psql-timing.sql
\timing on
SELECT count(*) FROM sales;
\timing off
Timing is on.
count
-------
6
(1 row)
Time: 0.697 ms
Timing is off.

\watch รัน query ซ้ำทุก i วินาที จำนวน c ครั้ง (ถ้าไม่ใส่ c จะรันไปเรื่อย ๆ จนกว่าคุณจะขัดจังหวะ):

-- sql/psql-watch.sql
SELECT sum(copies) AS total FROM sales \watch i=1 c=2
Wed 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 4
CREATE TABLE
COPY 4
count
-------
4
(1 row)

ตัวแปรของ psql ขยายด้วย colon เป็นความสะดวกสำหรับสคริปต์ ไม่ใช่สิ่งทดแทนการ bind parameter ที่ driver ทำให้:

-- sql/psql-variables.sql
\set min_year 2010
SELECT title FROM books WHERE published >= :min_year ORDER BY published;
title
-------------------
Tidal Notes
The Glass Orchard
Unlinked Draft
(3 rows)

การใช้งานแบบโต้ตอบเป็นแค่ครึ่งหนึ่งของ 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 error
CREATE TABLE
INSERT 0 1
psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"
DETAIL: Key (id)=(1) already exists.
INSERT 0 1
2 rows in notes
$ psql -U postgres -d appdb -v ON_ERROR_STOP=1 -f migrate.sql # stop at the first error
CREATE TABLE
INSERT 0 1
psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"
DETAIL: Key (id)=(1) already exists.
exit status 3
1 rows in notes
$ psql -U postgres -d appdb -1 -v ON_ERROR_STOP=1 -f migrate.sql # -1: one transaction, all or nothing
CREATE TABLE
INSERT 0 1
psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"
DETAIL: Key (id)=(1) already exists.
exit status 3
t

กฎที่ต้องจำ: โดยค่าเริ่มต้น psql ทำงานต่อและ exit status เป็น 0 แม้เกิด error; -v ON_ERROR_STOP=1 หยุดที่ error แรกด้วย exit status 3; การเพิ่ม -1 (--single-transaction) ครอบไฟล์ด้วย BEGIN/COMMIT ความล้มเหลวจึงไม่ทิ้งอะไรไว้ (psql reference)

ถ้าคุณชอบคลิกมากกว่าพิมพ์ 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 — ไลบรารีที่พูดโปรโตคอลของ 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();

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.1
appdb 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 และฐานข้อมูลแต่ละอย่างมีช่องของตัวเอง

เมื่อขอให้ผู้ช่วย AI เขียนสคริปต์ deploy ที่รัน migration แล้วรายงานความสำเร็จ มันเขียนแบบนี้ อ่านแล้วดูถูกต้อง และอันตราย:

🐛 หา bug ในโค้ดนี้

#!/usr/bin/env bash
# AI comment: "apply the migration, then report success"
psql -U postgres -d appdb -f migrate.sql
echo "migrations applied"
เฉลย

กับ migrate.sql ข้างบน (statement ที่สามล้มเหลว) การรันสคริปต์จะพิมพ์ error จากนั้นพิมพ์ migrations applied และ exit ด้วย status 0:

CREATE TABLE
INSERT 0 1
psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"
DETAIL: Key (id)=(1) already exists.
INSERT 0 1
migrations applied
deploy.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 ไม่มีอยู่หลังจากนั้น):

scripts/deploy.sh
set -euo pipefail
psql -X -U postgres -d appdb -v ON_ERROR_STOP=1 -1 -f migrate.sql
echo "migrations applied"
CREATE TABLE
INSERT 0 1
psql:migrate.sql:3: ERROR: duplicate key value violates unique constraint "notes_pkey"
DETAIL: Key (id)=(1) already exists.
deploy2.sh exit status 3

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
ตัวเลือกBenefitCost
psql CLIเร็ว เขียนสคริปต์ได้ overhead ต่ำต้องจำ meta-command และ syntax
pgAdmin GUIเห็นโครงสร้างข้อมูลชัดเจน เป็นมิตรกับผู้เริ่มต้นหนักกว่า และต้องมีเครื่องเดสก์ท็อปที่เปิดเบราว์เซอร์
driver ในโค้ดแอปพลิเคชันสิ่งที่ production รันจริงทุกวันการเขียนโค้ดเพิ่มเพียงเพื่อรัน query เดียวไม่คุ้ม
  • “ใช้ GUI อย่างเดียวก็พอ” วินาทีที่คุณ SSH เข้าเครื่อง production ที่ไม่มี GUI คุณจะติดถ้าไม่มี \dt, \d และพวก
  • “สคริปต์พิมพ์ migrations applied แปลว่าสำเร็จ” ไม่จริงถ้าไม่มี ON_ERROR_STOP: psql exit 0 หลัง error เป็นค่าเริ่มต้น (พิสูจน์ด้านบน)
  • “pgAdmin และ DBeaver เชื่อมต่อผ่าน localhost” จากเครื่อง host ใช่ แต่จาก container อื่นต้องใช้ชื่อ service เช่น postgres ตามที่ตั้งใน compose
  • “\copy กับ COPY เหมือนกัน” COPY อ่านและเขียนไฟล์บน เซิร์ฟเวอร์ ส่วน \copy ใช้ไฟล์บน client
  • “คัดลอก SQL ข้ามเครื่องมือไม่เป็นไร” psql และ GUI บางตัวจัดการ autocommit ต่างกัน BEGIN ที่คัดลอกมาโดยไม่มี COMMIT อาจทิ้ง transaction ค้างไว้
meta-command ใดของ psql แสดงรายการ table ในฐานข้อมูลปัจจุบัน?
รูปแบบมาตรฐานของ connection string ของ PostgreSQL คืออะไร?
จาก container ของ pgAdmin ควรใช้ชื่อ host อะไรเพื่อเข้าถึง service ฐานข้อมูล?
สคริปต์ deploy รัน psql -f migrate.sql และมี statement หนึ่งในไฟล์ล้มเหลว exit status ของ psql คืออะไร?
การเพิ่ม -1 (--single-transaction) ให้ psql -f file.sql เปลี่ยนอะไร?