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

คีย์​ของ​ผู้​เช่า​เป็น​ค่า​ตั้ง​ของ session และ​อายุ​ของ​มัน​วัด​ออก​มา​เป็น​จำนวน​แถว​ได้

บท​ที่ 1 จบ​ลง​ที่​ตัวเลข​สอง​ตัว เจ้าของ​ตาราง​ใต้ FORCE ได้ 0 เมื่อ​ไม่​ตั้ง app.tenant และ​ได้ 3 เมื่อ​ตั้ง​เป็น​ผู้​เช่า​ใบ​ใด​ใบ​หนึ่ง สอง​ค่า​นั้น​วัด​ได้​แล้ว แต่​ยัง​ไม่มี​ข้อ​ไหน​บอก​ว่า​ค่าที่​ตั้ง​ไป​นั้น​ถูก​เก็บ​ไว้​ที่ไหน และ​มัน​อยู่​ได้​นาน​แค่​ไหน

บท​นี้​วัด​สาม​อย่าง คือ​ค่า​นั้น​ถูก​อ่าน​อย่างไร มัน​จบ​อายุ​ลง​เมื่อไร และ​คำ​สั่ง​ที่​ล้าง​มัน​ทิ้ง คืน​สถานะ​กลับ​ไป​เป็น​แบบ​ที่​ไม่​เคย​ตั้ง​เลย​จริง​หรือ​ไม่

คำ​ตอบ​ของ​ข้อ​สุดท้าย​คือ ไม่ และ​ความ​ต่าง​นั้น​วัด​ได้​ด้วย​รหัส​เดียว ไม่ใช่​ด้วย​จำนวน​แถว

📦 clinic — ซี​รีส์​เดิม แต่​หกบท​แรก​เป็น SQL ล้วน

คอร์ส​นี้​อยู่​ใน​ซี​รีส์​คลินิก​เดียว​กับ คอร์ส​คัด​กรอง​ผู้​เรียก และ คอร์ส CORS กับ CSRF แต่​หกบท​แรก​ไม่มี​แอป​มา​เกี่ยวข้อง มี​แต่​ตาราง policy และ role

ตัวเลข​ทุก​ตัว​ใน​บท​นี้​คุณ​รัน​เอง​ได้ สั่ง scripts/clinic-rls/up.sh ซึ่ง​ยก container ชื่อ kaen-rls-pg ขึ้น​ที่ port 55438 แล้ว​สั่ง python3 scripts/clinic-rls/spike.py เก็บ​กวาด​ด้วย docker rm -f kaen-rls-pg

app role อ่าน​ตาม​ค่าที่​ตั้ง​ไว้ สี่​ค่า​จาก​ลำดับ​เดียว

หัวข้อ​ที่​มีชื่อ​ว่า “app role อ่าน​ตาม​ค่าที่​ตั้ง​ไว้ สี่​ค่า​จาก​ลำดับ​เดียว”

ผู้​อ่าน​ใน​ทรง​นี้​ไม่ใช่​เจ้าของ​ตาราง​และ​ไม่มี​ธง BYPASSRLS นิพจน์​ของ policy จึง​ถูก​เรียก​ใช้​ทุก​ครั้ง

ทรง​ที่ ③ สร้าง​ตาราง t_app ด้วย DDL ก้อน​เดียว​กับ​บท​ที่ 1 ทุก​บรรทัด แล้ว​ผูก policy ใบ​เดิม​เข้าไป spike.py สั่ง​กับ​ตาราง​ใบ​นี้​แค่ enable row level security ไม่​เคย​สั่ง force เลย​สัก​ครั้ง

ตาราง​ใบ​นี้​ไม่​ต้องการ force เพราะ​กฎ​การ​ยกเว้น​ที่​บท​ที่ 1 วัด​ไว้ ครอบ​เฉพาะ​เจ้าของ​ตาราง ซึ่ง​เอกสาร​ของ PostgreSQL 18 เป็น​คน​กำหนด ส่วน​ผู้​อ่าน​ใน​ทรง​นี้​คือ kaen_rls_app ซึ่ง​เป็น role คนละ​ใบ​กับ​เจ้าของ

ทรง​ที่ ③ · session เดียว · ผู้​อ่าน​คือ app role บน​ตาราง t_app
select 'KAEN|appNoGuc|'||count(*) from t_app where true;
set app.tenant = 'clinic-a';
select 'KAEN|appGucA|'||count(*) from t_app where true;
set app.tenant = 'clinic-b';
select 'KAEN|appGucB|'||count(*) from t_app where true;
set app.tenant = 'clinic-zzz';
select 'KAEN|appGucUnknown|'||count(*) from t_app where true;
สิ่ง​ที่​เพิ่ง​สั่ง​ไป​ก่อน​หน้าคีย์แถว​ที่​นับ​ได้
ยัง​ไม่มี​ใคร​ตั้ง app.tenant ใน session นี้appNoGuc0
set app.tenant = 'clinic-a'appGucA3
set app.tenant = 'clinic-b'appGucB3
set app.tenant = 'clinic-zzz'appGucUnknown0

ค่าที่ policy อ่าน​คือ GUCGrand Unified Configuration (GUC)ตัวแปร​ตั้ง​ค่าที่​ผูก​กับ session ซึ่ง policy ของ​คอร์ส​นี้​อ่าน​คีย์​ของ​ผู้​เช่า​จาก​มัน · app role อ่าน​ตาราง t_app ได้ 3 แถว​เมื่อ​ตั้ง​เป็น​ผู้​เช่า a และ 3 แถว​เมื่อ​ตั้ง​เป็น b · ไม่​ตั้ง​เลย​ได้ 0 แถว และ​ผู้​เช่า​ที่​ไม่มี​อยู่​จริง​ก็ได้ 0 แถว ซึ่ง​เป็น​จำนวน ไม่ใช่ error ซึ่ง​เป็น​ค่า​ตั้ง​ที่​ผูก​อยู่​กับ session ส่วน​ฟังก์ชัน​ที่​อ่าน​มัน​คือ current_settingcurrent_setting()ฟังก์ชัน​ที่​อ่าน​ค่า​ของ GUC · ใช้ได้​สอง​รูป · ทรง​ที่​วัด​คือ session เดียว​ที่​อ่าน​ตาราง t_guc · รูป​ที่​ไม่​ใส่​อาร์กิวเมนต์​ที่​สอง ยก​รหัส 42704 (csNoArgNeverSet) เมื่อ​ไม่มี​ใคร​เคย​ตั้ง​ค่า​นั้น​ใน session นี้ ส่วน​หลัง RESET รูป​เดียวกัน​ได้ 00000 (csNoArgAfterReset) และ​ค่าที่​ได้​เป็น​สตริง​ยาว 0 (csTrueAfterResetLen) ซึ่ง​มี​สอง​รูป และ​หัวข้อ​ท้าย​บท​วัด​ว่า​สอง​รูป​นั้น​ต่าง​กัน​อย่างไร

แถว​สุดท้าย​คือ​แถว​ที่​คน​มอง​ข้าม​บ่อย​ที่สุด clinic-zzz ไม่ใช่​ผู้​เช่า​ที่​มี​อยู่​ใน​ตาราง แต่​คำ​สั่ง​ไม่​ได้​ล้ม​เหลว มัน​คืน 0 ซึ่ง​เป็น​จำนวน ไม่ใช่ error

คีย์​ของ​ผู้​เช่า​ที่​พิมพ์​ผิด​หนึ่ง​ตัว​อักษร จึง​เดิน​ทาง​ผ่าน​ระบบ​มา​ถึง​ตรง​นี้​ได้​อย่าง​เงียบ​สนิท แล้ว​ออก​ไป​เป็น​หน้า​จอ​ที่​ว่าง​เปล่า ไม่ใช่​หน้า​จอ​ที่​ฟ้อง​ว่า​มี​อะไร​ผิด

ลำดับ​เดียว session เดียว สาม transaction และ​คำ​สั่ง​ที่​ตั้ง​ค่า​ต่าง​กัน​แค่​คำ​เดียว

ทรง​ที่ ⑩ สร้าง​ตาราง t_guc ด้วย DDL ก้อน​เดิม​และ policy ใบ​เดิม ทั้ง​ลำดับ​ข้าง​ล่าง​รัน​อยู่​ใน session เดียว​ตั้งแต่​บรรทัด​แรก​ถึง​บรรทัด​สุดท้าย ข้อ​นี้​สำคัญ เพราะ​สิ่ง​ที่​บท​นี้​วัด​คือ​อายุ​ของ​ค่า​ภายใน session หนึ่ง​ใบ

ทรง​ที่ ⑩ · session เดียว​ตลอด​ลำดับ · ตาราง t_guc
select 'KAEN|gucStartNeverSet|'||count(*) from t_guc where true;
begin;
set local app.tenant = 'clinic-a';
select 'KAEN|gucInsideSetLocal|'||count(*) from t_guc where true;
commit;
select 'KAEN|gucAfterCommitSetLocal|'||count(*) from t_guc where true;
begin;
set app.tenant = 'clinic-a';
select 'KAEN|gucInsideSet|'||count(*) from t_guc where true;
commit;
select 'KAEN|gucAfterCommitSet|'||count(*) from t_guc where true;
begin;
set app.tenant = 'clinic-b';
select 'KAEN|gucInsideSetB|'||count(*) from t_guc where true;
rollback;
select 'KAEN|gucAfterRollbackSet|'||count(*) from t_guc where true;
select 'KAEN|gucValueAfterRollback|'||coalesce(current_setting('app.tenant', true),'<NULL>');
ตำแหน่ง​ใน​ลำดับคีย์ค่าที่​วัด​ได้
ก่อน transaction แรก ยัง​ไม่​เคย​ตั้ง​ค่าgucStartNeverSet0
ใน transaction หลัง set localgucInsideSetLocal3
หลัง commit ของ transaction นั้นgucAfterCommitSetLocal0
ใน transaction หลัง set ธรรมดาgucInsideSet3
หลัง commit ของ transaction นั้นgucAfterCommitSet3
ใน transaction หลัง set เป็น clinic-bgucInsideSetB3
หลัง rollback ของ transaction นั้นgucAfterRollbackSet3
ค่าที่​อ่าน​ออก​มา​หลัง rollbackgucValueAfterRollbackclinic-a

SET LOCALSET LOCALรูป​ที่​ผูก​อายุ​ของ​ค่า​ไว้​กับ transaction · ทุก​ค่า​ต่อ​จาก​นี้​วัด​บน​ตาราง t_guc ใน session เดียว · ใน transaction เห็น 3 แถว หลัง commit เหลือ 0 แถว · ส่วน SET ธรรมดา หลัง commit ยัง​เห็น 3 แถว และ rollback คืน​ค่า​ก่อน​หน้า​คือ clinic-a ไม่ใช่​คืน​สถานะ​ที่​ไม่​เคย​ตั้ง คือ​รูป​ที่​ผูก​อายุ​ของ​ค่า​ไว้​กับ transaction gucInsideSetLocal = 3 แล้ว gucAfterCommitSetLocal = 0 คือ​การ​วัด​อายุ​นั้น​ออก​มา​ตรงๆ ค่า​หมด​อายุ​ที่ commit ไม่​ได้​หมด​อายุ​ที่​ปลาย session

สอง​แถว​ถัด​มา​คือ​คำ​สั่ง​เดียวกัน​ที่​ถอด​คำ​ว่า local ออก​ไป​คำ​เดียว แล้ว​ได้ 3 ทั้ง​สอง​ครั้ง ค่าที่​ตั้ง​ด้วย​รูป​นี้​อยู่​ต่อ​หลัง commit ใน session เดิม

สอง​แถว​ล่าง​สุด​เป็น​คู่​ที่​ต้อง​อ่าน​พร้อม​กัน gucInsideSetB = 3 คือ​ค่าที่​วัด​ได้ ตอน app.tenant เป็น clinic-b และ gucAfterRollbackSet = 3 คือ​ค่าที่​วัด​ได้​หลัง rollback

จำนวน​แถว​เท่า​กัน​ทั้ง​สอง​ฝั่ง แต่ gucValueAfterRollback บอก​ว่า​ค่าที่ session ถือ​อยู่​หลัง rollback คือ clinic-a ซึ่ง​เป็น​ค่าที่​ตั้ง​ไว้​ก่อน​หน้า ไม่ใช่​ค่า​ว่าง และ​ไม่ใช่ clinic-b

จำนวน​แถว​จึง​ไม่ใช่​หลัก​ฐาน​ว่า​ผู้​เช่า​คน​ไหน​ถูก​อ่าน ตาราง​นี้​มี​สาม​แถว​ต่อ​ผู้​เช่า​หนึ่ง​คน เลข 3 สอง​ตัว​นั้น​จึง​เท่า​กัน​ได้ ทั้ง​ที่​คีย์​ที่ policy อ่าน เปลี่ยน​ไป​แล้ว​ระหว่าง​สอง​การ​วัด

ข้อ​นี้​เป็น​ตัวอย่าง​ขนาด​เล็ก​ของ​สิ่ง​ที่​บท​ที่ 6 ขยาย​ออก​มา​เต็ม​บท

ท่อน​นี้​รัน​ใน session ใหม่​ที่​ยัง​ไม่มี​ใคร​ตั้ง app.tenant แม้แต่​ครั้ง​เดียว

ทรง​ที่ ⑪ ท่อน​แรก​ไม่​ได้​นับ​แถว​เลย มัน​วัด​สิ่ง​ที่​ฟังก์ชัน​คืน​กลับ​มา​เมื่อ​ไม่มี​ค่า​ให้​อ่าน spike.py เรียก​คำ​สั่ง​ที่​คาด​ว่า​จะ​พัง​ผ่าน​ฟังก์ชัน​ตัว​ช่วย​ชื่อ public.kaen_try ซึ่ง​คืน​ค่า​เป็น​สตริง​รูป <SQLSTATE> <จำนวนแถว> และ​คืน​จำนวน​แถว​เป็น -1 เมื่อ​คำ​สั่ง​ยก​ข้อ​ผิด​พลาด

ทรง​ที่ ⑪ ท่อน​แรก · session ใหม่ · ไม่​เคย​มี​ใคร​ตั้ง app.tenant
select 'KAEN|csNoArgNeverSet|'||public.kaen_try('select current_setting(''app.tenant'')');
select 'KAEN|csTrueNeverSet|'||coalesce(current_setting('app.tenant', true),'<NULL>');
รูป​ที่​เรียกคีย์ค่าที่​วัด​ได้
current_setting('app.tenant')csNoArgNeverSet42704 -1
current_setting('app.tenant', true)csTrueNeverSet<NULL>

แถว​บน​คือ 42704 undefined_objectSQLSTATE 42704 (undefined_object)รหัส​ที่ current_setting แบบ​ไม่​ใส่​อาร์กิวเมนต์​ที่​สอง​ยก​ออก​มา เมื่อ​ไม่มี​ใคร​เคย​ตั้ง​ค่า​นั้น​ใน session นี้ · หลัง RESET รหัส​นี้​หาย​ไป ทั้ง​ที่​จำนวน​แถว​ที่​อ่าน​จาก​ตาราง t_guc เป็น 0 เท่า​กัน​ทั้ง​สอง​สถานะ คือ gucStartNeverSet และ csCountAfterReset ซึ่ง​เป็น​รหัส​ที่​รูป​ไม่​ใส่​อาร์กิวเมนต์​ที่​สอง​ยก​ออก​มา เมื่อ​ไม่มี​ใคร​เคย​ตั้ง​ค่า​นั้น​ใน session นี้ ส่วน -1 มา​จาก kaen_try ไม่​ได้​มา​จาก PostgreSQL

แถว​ล่าง​คือ​รูป​ที่​ใส่ true เป็น​อาร์กิวเมนต์​ที่​สอง ซึ่ง​ไม่​ยก​อะไร​เลย​และ​คืน NULL ค่า <NULL> ที่​เห็น​ใน​ตาราง​มา​จาก coalesce ใน​คำ​สั่ง ไม่ใช่​สตริง​ที่​ฟังก์ชัน​คืน​มา

policy ทุก​ใบ​ที่​บท​ที่ 1 กับ​บท​นี้​ใช้ เป็น​รูป​ที่​สอง นิพจน์​คือ tenant_id = current_setting('app.tenant', true) ซึ่ง​เทียบ tenant_id กับ NULL เมื่อ​ไม่มี​ใคร​ตั้ง​ค่า และ​การ​เทียบ​แบบ​นั้น​ไม่​คืน true ให้​แถว​ไหน

นี่​คือ​เหตุผล​ที่ appNoGuc เป็น 0 ไม่ใช่ error คอร์ส​นี้​เลือก​รูป​ที่​เงียบ​ไว้​ตั้งแต่​ต้น แล้ว​วัด​ราคา​ของ​การ​เลือก​นั้น​ใน​บท​ที่ 7

ท่อน​นี้​เป็น session อีก​ใบ ที่​ตั้ง​ค่า​แล้ว​สั่ง​ล้าง แล้ว​วัด​สิ่ง​ที่​เหลือ​อยู่​ด้วย​สาม​คำ​สั่ง

ทรง​ที่ ⑪ ท่อน​ที่​สอง · session ใหม่​อีก​ใบ · ตั้ง​แล้ว​สั่ง reset
set app.tenant = 'clinic-a';
select 'KAEN|csCountBeforeReset|'||count(*) from t_guc where true;
reset app.tenant;
select 'KAEN|csNoArgAfterReset|'||public.kaen_try('select current_setting(''app.tenant'')');
select 'KAEN|csTrueAfterReset|'||coalesce(current_setting('app.tenant', true),'<NULL>');
select 'KAEN|csTrueAfterResetLen|'||length(coalesce(current_setting('app.tenant', true),'x'));
select 'KAEN|csCountAfterReset|'||count(*) from t_guc where true;
สิ่ง​ที่​วัดคีย์ค่าที่​วัด​ได้
แถว​ที่​นับ​ได้​ก่อน​สั่ง resetcsCountBeforeReset3
รูป​ไม่​ใส่​อาร์กิวเมนต์​ที่​สอง หลัง resetcsNoArgAfterReset00000 1
รูป​ที่​ใส่ true หลัง resetcsTrueAfterResetสตริง​ว่าง
ความ​ยาว​ของ​ค่า​นั้นcsTrueAfterResetLen0
แถว​ที่​นับ​ได้​หลัง resetcsCountAfterReset0

csNoArgAfterReset ขึ้น​ต้น​ด้วย 00000 ซึ่ง​แปล​ว่า​คำ​สั่ง​เดิม​ที่​เคย​ยก​รหัส ไม่​ยก​อะไร​แล้ว และ 1 ท้าย​ค่า​นั้น​คือ​จำนวน​แถว​ที่ kaen_try รายงาน​กลับ​มา

แถว​ที่​วัด​ความ​ยาว​มี​ไว้​แยก​สอง​อย่าง​ที่​หน้าตา​เหมือน​กัน คำ​สั่ง​นั้น​ใส่ 'x' ไว้​ใน coalesce ถ้า​ค่าที่​ได้​เป็น NULL ความ​ยาว​จะ​เป็น 1 ค่าที่​วัด​ได้​จริง​คือ 0 สิ่ง​ที่​เหลือ​อยู่​หลัง reset จึง​เป็น​สตริง​ว่าง ไม่ใช่ NULL

สอง​สถานะ​ที่​จำนวน​แถว​แยก​ไม่​ออก

gucStartNeverSet = 0 และ csCountAfterReset = 0 เป็น​ค่าที่​วัด​จาก​ตาราง t_guc ใบ​เดียวกัน ด้วย​คำ​สั่ง​นับ​รูป​เดียวกัน และ​ได้​เลข​เดียวกัน

แต่​สถานะ​ข้าง​หลัง​สอง​ค่า​นั้น​คนละ​สถานะ ค่า​แรก​มา​จาก session ที่​ไม่​เคย​มี​ใคร​ตั้ง app.tenant ส่วน​ค่าที่​สอง​มา​จาก session ที่​ตั้ง​ไป​แล้ว​และ​สั่ง​ล้าง​ทิ้ง

สิ่ง​เดียว​ใน​บท​นี้​ที่​แยก​สอง​สถานะ​ออก​จาก​กัน​ได้ คือ​รูป​ที่​ไม่​ใส่​อาร์กิวเมนต์​ที่​สอง มัน​ให้ 42704 -1 กับ​สถานะ​แรก และ​ให้ 00000 1 กับ​สถานะ​ที่​สอง

แผนผัง​ของ​สอง​อายุ และ​สอง​สถานะ​ที่​ว่าง​ไม่​เหมือน​กัน

หัวข้อ​ที่​มีชื่อ​ว่า “แผนผัง​ของ​สอง​อายุ และ​สอง​สถานะ​ที่​ว่าง​ไม่​เหมือน​กัน”

ทุก​กล่อง​ปลาย​ทางใน​ภาพ​นี้​มี​คีย์​ของ​มัน​กำกับ และ​ทุก​คีย์​มา​จาก​ลำดับ​ที่​อยู่​ใน​บท​นี้

flowchart TB
  S0["session ใหม่ · ไม่เคยมีใครตั้ง app.tenant"] --> S1["นับแถว · gucStartNeverSet = 0"]
  S0 --> E1["current_setting ไม่ใส่อาร์กิวเมนต์ที่สอง · csNoArgNeverSet = 42704 -1"]
  S0 --> E2["current_setting ที่ใส่ true · csTrueNeverSet คือ NULL"]
  S1 --> L1["begin แล้ว set local app.tenant = clinic-a"]
  L1 --> L2["ใน transaction · gucInsideSetLocal = 3"]
  L2 --> L3["หลัง commit · gucAfterCommitSetLocal = 0"]
  S1 --> P1["begin แล้ว set app.tenant = clinic-a"]
  P1 --> P2["ใน transaction · gucInsideSet = 3"]
  P2 --> P3["หลัง commit · gucAfterCommitSet = 3"]
  R0["session อีกใบ · set เป็น clinic-a แล้วสั่ง reset"] --> R1["current_setting ไม่ใส่อาร์กิวเมนต์ที่สอง · csNoArgAfterReset = 00000 1"]
  R0 --> R2["ความยาวของค่าที่เหลือ · csTrueAfterResetLen = 0"]
  R0 --> R3["นับแถว · csCountAfterReset = 0"]

คำ​บรรยาย​ภาพ: กล่อง gucStartNeverSet กับ​กล่อง csCountAfterReset ให้​เลข​เดียวกัน​บน​ตาราง​เดียวกัน แต่​มา​จาก session คนละ​ใบ​ที่​อยู่​คนละ​สถานะ กิ่ง​ซ้าย​บน​กับ​กิ่ง​ล่าง​จึง​แยก​ออก​จาก​กัน​ได้​ด้วย​รหัส​เท่านั้น

ตัวเลข​ทั้ง​บท​มา​จาก SQL ที่​รัน​จริง ส่วน​กฎ​ว่า​รูป​ไหน​ยก​รหัส​อะไร มา​จาก​เอกสาร

spike.py รัน​ชุด​ข้อ​อ้าง​ทั้ง​ชุด​สาม​รอบ​ต่อ​การ​เรียก​หนึ่ง​ครั้ง แล้ว​เทียบ​ผล​ที​ละ​คีย์ รอบ​ที่​รัน​เมื่อ 2026-09-06 ได้ 52 ข้อ​อ้าง​ตรง​ทั้งหมด 98 ค่าที่​วัด​ได้ และ 0 ค่าที่​ขยับ​ระหว่าง​รอบ

สิ่ง​ที่​บท​นี้ ไม่​ได้ พิสูจน์​คือ​กลไก​ข้าง​ใน​ของ SET LOCAL บท​นี้​ไม่​ได้​แสดง​ว่า​ตัว​จัดการ​ค่า​ตั้ง เก็บ​ค่า​เก่า​ไว้​ที่ไหน​ระหว่าง transaction ข้อ​อ้าง​แบบ​นั้น​อ้าง​เอกสาร​ของ PostgreSQL 18

บท​นี้​ยัง​ไม่​ได้​วัด​ว่า​หลัง commit ของ set local ค่าที่​เหลือ​อยู่​เป็น​สตริง​ว่าง​หรือ​เป็น NULL spike.py ไม่​ได้​อ่าน​ค่า​ตรง​จุด​นั้น มัน​อ่าน​แต่​จำนวน​แถว จึง​ไม่มี​ข้อ​อ้าง​เรื่อง​นี้​ใน​บท

ตัวเลข​ทุก​ตัว​ผูก​กับ​รุ่น​ที่​ระบุ​ไว้

ผล​ทั้งหมด​ใน​บท​นี้​วัด​บน PostgreSQL 18.6 บน aarch64-unknown-linux-musl ใน container ของ​คอร์ส​นี้​เอง major อื่น​เป็น​คนละ​การ​วัด และ​บท​นี้​ไม่​ได้​อ้าง​ถึง​มัน

หน่วย​ของ​หลัก​ฐาน​ใน​บท​นี้​มี​สี่​หน่วย คือ​จำนวน​แถว​ที่​ผู้​อ่าน​นับ​ได้ รหัส SQLSTATE ความ​ยาว​ของ​สตริง และ​ค่า​ของ app.tenant ที่​อ่าน​ออก​มา​เป็น​สตริง ทั้ง​คอร์ส​ไม่มี​หน่วย​เวลา​สัก​ตัว

🔁 ข้อ​นี้​ไป​โผล่​อีก​ครั้ง​ที่ไหน
  • บท​ที่ 1 — ที่มา​ของ​ตาราง t_app และ​ของ role ทั้ง​สาม​ใบ​ที่​ทุก​ทรง​ใน​คอร์ส​นี้​ใช้
  • บท​ที่ 7 — บท​ที่​เก็บ​ผล​ของ​ข้อ reset โดยตรง เมื่อ connection ถูก​คืน​เข้า connection pool แล้ว​คำขอ​ใบ​ถัด​ไป​มา​เจอ​ค่าที่​ยัง​ค้าง​อยู่

รู้​แล้ว​ว่า​ค่า​นั้น​อยู่​ที่ไหน​และ​อยู่​ได้​นาน​แค่​ไหน ยัง​ไม่รู้​ว่า​มัน​กัน​การ​เขียน​ได้​แค่​ไหน

ทุก​ตัวเลข​ใน​บท​นี้​มา​จาก​คำ​สั่ง select ทั้งหมด ผู้​อ่าน​เห็น​สาม​แถว​หรือ​ศูนย์​แถว ตาม​ค่าที่ session ถือ​อยู่ และ​ไม่มี​คำ​สั่ง​ไหน​ล้ม​เหลว​เลย​สัก​คำ​สั่ง นอกจาก​รูป​ที่​ตั้งใจ​ให้​ล้ม

แต่ policy ใบ​เดียวกัน​นั้น​เขียน​ว่า for all ซึ่ง​ครอบ INSERT UPDATE และ DELETE ด้วย คำถาม​ถัด​ไป​คือ​คำ​สั่ง​ที่​ย้าย​แถว​ข้าม​ผู้​เช่า ถูกกัน​ไว้​ด้วย​อะไร และ​มัน​ส่ง​เสียง​ออก​มา​หรือ​ไม่

บท​ที่ 3 วัด​ฝั่ง​เขียน แล้ว​พบ​ว่า​นิพจน์​ที่​กัน​การ​เขียน​ไว้ ไม่ใช่​นิพจน์​ที่​คุณ​คิด​ว่า​เขียน​ไว้


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • PostgreSQL 18 — SET (เข้า​ถึง 2026-09-06) — ความ​หมาย​ของ SET LOCAL และ​อายุ​ของ​ค่าที่​ผูก​กับ transaction

  • PostgreSQL 18 — RESET (เข้า​ถึง 2026-09-06) — ความ​หมาย​ของ​คำ​สั่ง​ที่​ล้าง​ค่า​ตั้ง​ของ session

  • PostgreSQL 18 — System Administration Functions (เข้า​ถึง 2026-09-06) — ที่มา​ของ​กฎ​ว่า current_setting สอง​รูป​ต่าง​กัน​อย่างไร

  • PostgreSQL 18 — PostgreSQL Error Codes (เข้า​ถึง 2026-09-06) — ที่มา​ของ​ชื่อ undefined_object ที่​คู่​กับ​รหัส 42704

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก scripts/clinic-rls/spike.py ซึ่ง​สร้าง​ฐาน​ข้อมูล​กับ role ขึ้น​ใหม่​ทุกร​อบ แล้ว​รื้อ​ทิ้ง​เมื่อ​จบ · ต้อง​สั่ง scripts/clinic-rls/up.sh ก่อน ซึ่ง​ยก container ของ​คอร์ส​นี้​เอง​ขึ้น​ที่ port 55438

เช็กความเข้าใจ — บทที่ 2

ข้อ 1 / 3

ใน session เดียว สั่ง `begin` แล้ว `set local app.tenant = 'clinic-a'` นับแถวได้ 3 แล้วสั่ง `commit` · นับแถวอีกครั้งใน session เดิมทันที ได้เท่าไร