Database, schema และการเชื่อมต่อ
PostgreSQL server เดียวเก็บ table ได้มากกว่าหนึ่งชุด และจัดระเบียบไว้เป็นชั้น ๆ การเข้าใจการซ้อนกันนี้ — cluster, database, schema, table — ช่วยให้กระจ่างว่าข้อมูลของคุณอยู่ที่ไหนและต้องอ้างชื่ออย่างไร บทเรียนนี้จะไล่ลงไปทีละชั้นพร้อมแสดงวิธีสร้างแต่ละชั้น
ชั้นต่าง ๆ
หัวข้อที่มีชื่อว่า “ชั้นต่าง ๆ”server หนึ่งที่รันอยู่เรียกว่า cluster cluster หนึ่งบรรจุ database หนึ่งหรือมากกว่า ซึ่งแยกออกจากกันอย่างสมบูรณ์: การเชื่อมต่อหนึ่งสื่อสารกับฐานข้อมูลเพียงหนึ่งเดียวในแต่ละครั้ง ภายในแต่ละ database มี schema ที่เป็นการจัดกลุ่ม table (และ object อื่น ๆ) ที่มีชื่อ ภายในแต่ละ schema มี table เอง แผนภาพแสดงการซ้อนกัน
flowchart TD CL[Cluster: one server] --> DB1[Database: appdb] CL --> DB2[Database: postgres] DB1 --> S1[Schema: public] DB1 --> S2[Schema: billing] S1 --> T1[Table: customers] S1 --> T2[Table: orders] S2 --> T3[Table: invoices]
การสร้าง database
หัวข้อที่มีชื่อว่า “การสร้าง database”database คือหน่วยของการแยกตัว คุณให้แต่ละแอปพลิเคชันมี database ของตัวเองเพื่อให้ table ของแต่ละแอปไม่ชนกัน การสร้างใช้ statement เดียวจบ
CREATE DATABASE shop;await pool.query('CREATE DATABASE shop');conn.autocommit = Truecur.execute("CREATE DATABASE shop")_, err := conn.Exec(ctx, "CREATE DATABASE shop")sqlx::query("CREATE DATABASE shop") .execute(&pool) .await?;หากต้องการเริ่มใช้ database ใหม่ ให้เชื่อมต่อใหม่เข้าไปที่ database นั้น ใน psql คือ \c shop ส่วนใน driver คุณเปิดการเชื่อมต่อใหม่โดยใช้ shop เป็น database ใน connection string คุณไม่สามารถรัน CREATE DATABASE ภายใน transaction ได้ จึงเป็นเหตุผลที่ตัวอย่าง Python เปิด autocommit
การสร้าง schema และ schema public
หัวข้อที่มีชื่อว่า “การสร้าง schema และ schema public”ภายใน database, schema ให้คุณจัดกลุ่ม table ที่เกี่ยวข้องและหลีกเลี่ยงชื่อที่ชนกัน ทุก database ใหม่เริ่มต้นด้วย schema หนึ่งที่มีอยู่แล้ว ชื่อว่า public และเว้นแต่คุณจะระบุเป็นอย่างอื่น table ของคุณจะไปอยู่ที่นั่น
CREATE SCHEMA billing;await pool.query('CREATE SCHEMA billing');cur.execute("CREATE SCHEMA billing")_, err := conn.Exec(ctx, "CREATE SCHEMA billing")sqlx::query("CREATE SCHEMA billing") .execute(&pool) .await?;ใน pgAdmin: schema ใหม่ปรากฏใต้ Schemas ใน browser tree เมื่อคุณรีเฟรช database คุณยังสร้างได้โดยการคลิกขวาที่ Schemas DBeaver แสดงการจัดกลุ่มเดียวกันใน navigator ของตัวเอง
ชื่อแบบ qualified และ search_path
หัวข้อที่มีชื่อว่า “ชื่อแบบ qualified และ search_path”table มีชื่อเต็มแบบ qualified name ในรูปแบบ schema.table เช่น billing.invoices คุณเขียนชื่อแบบ qualified ได้เสมอและ PostgreSQL จะรู้แน่ชัดว่าคุณหมายถึง table ใด
เมื่อคุณเขียนชื่อแบบ unqualified อย่าง invoices ตัว PostgreSQL จะไล่ดู search_path — รายการ schema ที่เรียงลำดับไว้ — แล้วใช้ตัวที่ตรงกันตัวแรก โดย default แล้ว search path จัด public ไว้ก่อน จึงเป็นเหตุผลที่ table ที่คุณสร้างโดยไม่ระบุ schema จะไปลงที่ public และเรียกกลับมาได้ด้วยชื่อเปล่า ๆ
คุณตรวจสอบและเปลี่ยน search path สำหรับ session ของคุณได้
SHOW search_path;SET search_path TO billing, public;await pool.query('SET search_path TO billing, public');cur.execute("SET search_path TO billing, public")_, err := conn.Exec(ctx, "SET search_path TO billing, public")sqlx::query("SET search_path TO billing, public") .execute(&pool) .await?; search_path----------------- "$user", public(1 row)เมื่อ billing อยู่ก่อนใน path, invoices แบบ unqualified ตอนนี้จะ resolve ไปยัง billing.invoices การเขียนชื่อแบบ qualified ขจัดความสงสัยทั้งหมดและเป็นนิสัยที่ปลอดภัยที่สุดในฐานข้อมูลที่ใช้ร่วมกัน
Role เทียบกับ database
หัวข้อที่มีชื่อว่า “Role เทียบกับ database”สองแนวคิดนี้สับสนกันได้ง่าย database คือภาชนะสำหรับข้อมูลของคุณ ส่วน role คือบัญชีที่เชื่อมต่อและได้รับสิทธิ์ในการกระทำต่อข้อมูลนั้น role เป็นทั้ง user และ group ใน PostgreSQL
ทั้งสองแนวคิดเป็นอิสระต่อกัน: role เดียวสามารถเป็นเจ้าของและเชื่อมต่อกับ database หลายอันได้ และ database หนึ่งก็สามารถถูกใช้โดยหลาย role ได้เช่นกัน
ตลอดโมดูลนี้คุณล็อกอินเป็น role postgres ที่เป็น superuser ระบบจริงจะสร้าง role ที่จำกัดสิทธิ์ต่อแอปพลิเคชันแทน ซึ่งโมดูล operations จะครอบคลุมเรื่องนั้น
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- การเชื่อมต่อมุ่งเป้าไปที่ database เพียงหนึ่งเดียวเสมอ หากต้องการสลับ คุณเชื่อมต่อใหม่
- database ใหม่มาพร้อม schema
publicดังนั้น table ที่คุณสร้างโดยไม่ตั้งชื่อ schema จะไปลงที่public - ชื่อแบบ unqualified ถูก resolve ผ่าน
search_pathส่วนรูปแบบschema.tableแบบ qualified ไม่กำกวมและปลอดภัยที่สุดในฐานข้อมูลที่ใช้ร่วมกัน CREATE DATABASEรันภายใน transaction block ไม่ได้ ดังนั้นเครื่องมือต่าง ๆ จึงต้อง commit คำสั่งนี้เดี่ยว ๆ- role (บัญชี) และ database (ภาชนะข้อมูล) เป็นแนวคิดที่แยกจากกัน อย่าปฏิบัติต่ออันหนึ่งเสมือนเป็นอีกอัน
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| หนึ่ง database ต่อหนึ่งแอป | แยกตัวชัดเจน, mental model ง่าย | connection pool แยกกัน ใช้ resource มากขึ้น |
| หลาย schema ในหนึ่ง database | ใช้ connection pool ร่วมกัน, join ข้าม schema ได้ | ต้องคุม naming และ permission ให้ไม่ชนกันเอง |
ชื่อแบบ qualified (schema.table) | ไม่กำกวม อ่านแล้วรู้ทันทีว่า table อยู่ที่ไหน | เขียนยาวกว่า |
พึ่ง search_path | เขียนสั้น กระชับ | เปราะบาง เพราะผลลัพธ์เปลี่ยนตาม session |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- พึ่ง
search_pathdefault ใน database ที่ใช้ร่วมกัน — ถ้ามีหลาย schema ชื่อ table ซ้ำกัน การเขียนชื่อ unqualified อาจ resolve ไปผิด schema โดยไม่มี error แจ้งเตือน - เชื่อมต่อด้วย role
postgres(superuser) ในสภาพแวดล้อมจริง — ควรสร้าง role ที่จำกัดสิทธิ์เฉพาะแอปพลิเคชัน เพื่อลดความเสียหายถ้า credential รั่วไหล - ลืมว่า
CREATE DATABASEรันใน transaction block ไม่ได้ — ถ้ารันผ่าน migration tool หรือ script ที่ wrap ทุกอย่างด้วย transaction จะเจอ error ทันที ต้องแยก statement นี้ออกมา commit เดี่ยว ๆ
💡 ตัวอย่างจากของจริง
แพลตฟอร์ม SaaS แบบ multi-tenant — หลายระบบ (เช่นหลาย setup ของ Rails บน Postgres) เลือกใช้ schema-per-tenant ภายใน database เดียว เพื่อให้ได้ความเป็นส่วนตัวของข้อมูลระดับหนึ่งโดยไม่ต้องดูแล database แยกเป็นร้อยเป็นพันตัว
ทีมที่แยก database ต่อแอป — บริษัทจำนวนมากยึดหลักหนึ่ง database ต่อหนึ่ง service เพื่อให้การ scale, backup และ access control ของแต่ละทีมเป็นอิสระต่อกันอย่างสมบูรณ์