UUID vs ULID vs serial IDs: เรียนรู้ว่าแต่ละแบบมีผลอย่างไรต่อการจัดทำดัชนี การเรียง การชาร์ด และเวิร์กโฟลว์ส่งออก/นำเข้าในโปรเจกต์จริง

การเลือก ID ดูน่าเบื่อในสัปดาห์แรก แล้วคุณส่งของ ข้อมูลเติบโต และการตัดสินใจ "เรียบง่าย" นั้นก็ปรากฏในทุกที่: ดัชนี, URL, ล็อก, การส่งออก และการผสาน
คำถามจริงไม่ใช่ "อันไหนดีที่สุด?" แต่เป็น "คุณอยากหลีกเลี่ยงความเจ็บปวดอะไรทีหลัง?" การเปลี่ยน ID ยากเพราะมันถูกคัดลอกไปยังตารางอื่น ถูกแคชโดยไคลเอนต์ และระบบอื่นพึ่งพามัน
เมื่อ ID ไม่สอดคล้องกับวิวัฒนาการของผลิตภัณฑ์ คุณมักจะเห็นปัญหาในไม่กี่จุด:
มักมีการแลกเปลี่ยนระหว่างความสะดวกตอนนี้กับความยืดหยุ่นในอนาคต ตัวเลขลำดับ (serial integers) อ่านง่ายและมักเร็ว แต่จะเปิดเผยจำนวนเรคคอร์ดและทำให้การผสานข้อมูลยากขึ้น UUID แบบสุ่มดีสำหรับความเป็นเอกลักษณ์ข้ามระบบ แต่กดดันดัชนีและอ่านยากสำหรับคนดู ULID ต้องการความเป็นเอกลักษณ์ระดับโลกพร้อมการเรียงตามเวลาโดยประมาณ แต่ก็มีต้นทุนด้านการจัดเก็บและเครื่องมือ
วิธีคิดที่มีประโยชน์: ใครคือผู้ใช้หลักของ ID นี้?
ถ้า ID ส่วนใหญ่สำหรับคน (ทีมซัพพอร์ต ดีบัก และปฏิบัติการ) ค่าที่สั้นและอ่านง่ายมักชนะ ถ้าเป็นเครื่องจักร (การเขียนแบบกระจาย ไคลเอนต์ออฟไลน์ ระบบหลายภูมิภาค) ความเป็นเอกลักษณ์ทั่วโลกและการหลีกเลี่ยงการชนสำคัญกว่า
เมื่อคนถกเถียงกันเรื่อง "UUID vs ULID vs serial IDs" จริงๆ แล้วพวกเขากำลังเลือกว่าทุกแถวจะได้ป้ายระบุแบบใด ป้ายนี้มีผลต่อความง่ายในการแทรก เรียง ละลาย และย้ายข้อมูลในอนาคต
Serial ID คือเคาน์เตอร์ ฐานข้อมูลแจก 1 แล้ว 2 แล้ว 3 เป็นต้น (มักเก็บเป็น integer หรือ bigint) อ่านง่าย ประหยัดที่จัดเก็บ และมักเร็วเพราะแถวใหม่ไปลงท้ายดัชนี
UUID เป็นตัวระบุ 128-bit ที่ดูเหมือนสุ่ม เช่น 3f8a... ในหลายการตั้งค่ามันสามารถสร้างได้โดยไม่ต้องถามฐานข้อมูลสำหรับหมายเลขถัดไป ดังนั้นระบบต่างกันจึงสร้าง ID ได้เอง ข้อแลกเปลี่ยนคือการแทรกที่ดูสุ่มสามารถทำให้ดัชนีทำงานหนักขึ้นและกินพื้นที่มากกว่า bigint ธรรมดา
ULID ก็เป็น 128-bit แต่ถูกออกแบบให้เรียงตามเวลาโดยประมาณ ULID ใหม่มักจะเรียงหลัง ULID เก่า ในขณะที่ยังคงเป็นเอกลักษณ์ระดับโลก คุณจึงได้ประโยชน์บางส่วนของการสร้างได้จากที่ใดก็ได้แบบเดียวกับ UUID พร้อมพฤติกรรมการเรียงที่เป็นมิตรกว่า
สรุปง่ายๆ:
Serial ID พบได้บ่อยในแอปฐานข้อมูลเดียวและเครื่องมือภายใน UUID ปรากฏเมื่อข้อมูลถูกสร้างจากหลายบริการ อุปกรณ์ หรือภูมิภาค ULID เป็นที่นิยมเมื่อทีมต้องการการสร้าง ID กระจายแต่ยังใส่ใจการเรียงหรือการแบ่งหน้าแบบ "ล่าสุดก่อน"
Primary key โดยปกติจะสนับสนุนด้วยดัชนี (มักเป็น B-tree) ให้คิดว่าดัชนีเป็นสมุดโทรศัพท์ที่เรียงลำดับ: ทุกแถวใหม่ต้องมีรายการวางในตำแหน่งที่ถูกต้องเพื่อให้การค้นหายังคงเร็ว
กับ ID แบบสุ่ม (UUIDv4 แบบคลาสสิก) รายการใหม่ไปกระจายทั่วดัชนี นั่นหมายความว่าฐานข้อมูลต้องแตะหลายหน้าในดัชนี แบ่งหน้า (page split) บ่อยขึ้น และเขียนมากขึ้น เมื่อเวลาผ่านไปคุณจะเห็นการสึกหรอของดัชนี: ทำงานหนักขึ้นต่อการแทรก มากขึ้นการพลาดแคช และดัชนีใหญ่กว่าที่คาดไว้
กับ ID ที่เพิ่มขึ้นเป็นส่วนใหญ่ (serial/bigint หรือ ID เรียงตามเวลาอย่าง ULID หลายรูปแบบ) ฐานข้อมูลมักสามารถเพิ่มรายการใหม่ใกล้ท้ายดัชนีได้ นี่เป็นมิตรกับแคชเพราะหน้าล่าสุดยังร้อนอยู่ และการแทรกมักจะราบรื่นกว่าเมื่ออัตราการเขียนสูง
ขนาดคีย์สำคัญเพราะรายการในดัชนีไม่ฟรี:
คีย์ที่ใหญ่กว่าทำให้รายการต่อหน้าดัชนีใส่ได้น้อยลง นั่นมักนำไปสู่ดัชนีที่ลึกขึ้น การอ่านหน้ามากขึ้นต่อคิวรี และต้องการ RAM มากขึ้นเพื่อให้เร็ว
ถ้าคุณมีตาราง "events" ที่แทรกอย่างต่อเนื่อง Primary key แบบ UUID แบบสุ่มอาจเริ่มรู้สึกช้ากว่า bigint เร็วกว่าที่คาด แม้ว่าการค้นหาแถวเดียวยังคงดูโอเค หากคาดว่าจะมีการเขียนหนัก ค่าสำคัญแรกที่คุณจะสังเกตคือค่าใช้จ่ายของดัชนี
ถ้าคุณสร้าง "Load more" หรือ infinite scroll คุณคงรู้สึกถึงปัญหาของ ID ที่เรียงไม่ดี ID "เรียงดี" เมื่อตัดอันดับตามมันให้ลำดับที่มีความหมายและมีเสถียรภาพ (มักเป็นเวลาสร้าง) ทำให้การแบ่งหน้าคาดเดาได้
กับ ID แบบสุ่ม (เช่น UUIDv4) แถวใหม่กระจุกรวม การเรียงตาม id จะไม่ตรงกับเวลา และการแบ่งหน้าแบบเคอร์เซอร์เช่น "ให้ฉันไอเท็มหลังจาก id นี้" จะไม่เชื่อถือได้ คุณมักจะต้องกลับไปใช้ created_at ซึ่งโอเคแต่ต้องทำอย่างระมัดระวัง
ULID ถูกออกแบบมาให้เรียงตามเวลาโดยประมาณ ถ้าคุณเรียงตาม ULID (เป็นสตริงหรือรูปแบบไบนารี) ไอเท็มใหม่มักจะมาทีหลัง ทำให้การแบ่งหน้าแบบเคอร์เซอร์ง่ายขึ้นเพราะเคอร์เซอร์อาจเป็น ULID ล่าสุดที่เห็น
ULID ช่วยเรื่องการเรียงตามเวลาโดยประมาณสำหรับฟีด ตัวเคอร์เซอร์ที่ง่ายขึ้น และพฤติกรรมการแทรกที่สุ่มน้อยกว่าที่เห็นกับ UUIDv4
แต่ ULID ไม่รับประกันการเรียงตามเวลาที่สมบูรณ์เมื่อหลาย ID ถูกสร้างในมิลลิวินาทีเดียวกันจากหลายเครื่อง ถ้าคุณต้องการการเรียงที่แน่นอน คุณยังต้องเก็บ timestamp จริงไว้
created_at ยังคงดีกว่าการเรียงด้วย created_at มักปลอดภัยกว่าเมื่อคุณ backfill ข้อมูล นำเข้าข้อมูลประวัติ หรือจำเป็นต้องมีการตัดสินใจแบบ tie-breaking ชัดเจน
แพตเทิร์นปฏิบัติได้คือสั่งตาม (created_at, id) โดยที่ id เป็นตัวตัดสินเมื่อเวลาเท่ากัน
Sharding หมายถึงการแบ่งฐานข้อมูลหนึ่งเป็นหลายฐานข้อมูลย่อยเพื่อให้แต่ละชาร์ดเก็บส่วนของข้อมูล ทีมมักทำทีหลังเมื่อฐานข้อมูลเดี่ยวยากจะขยายหรือเป็นจุดล้มเหลวเดียวที่เสี่ยงเกินไป
การเลือก ID ของคุณสามารถทำให้การชาร์ดจัดการได้หรือเป็นเรื่องลำบาก
กับ ID แบบลำดับ (auto-increment serial หรือ bigint) ทุกชาร์ดจะสร้าง 1, 2, 3... ได้อย่างสบาย ดังนั้น ID เดียวกันอาจมีอยู่ในหลายชาร์ด ครั้งแรกที่คุณต้องผสานข้อมูล ย้ายแถว หรือสร้างฟีเจอร์ข้ามชาร์ด คุณจะเจอการชน
คุณหลีกเลี่ยงการชนได้ด้วยการประสาน (บริการ ID ส่วนกลาง หรือช่วงหมายเลขต่อชาร์ด) แต่สิ่งนั้นเพิ่มชิ้นส่วนที่ต้องดูแลและอาจกลายเป็นคอขวด
UUIDs และ ULIDs ลดการประสานเพราะแต่ละชาร์ดสามารถสร้าง ID ได้เองโดยมีความเสี่ยงซ้ำต่ำมาก ถ้าคิดว่าจะมีการแยกข้อมูลข้ามฐานข้อมูล นี่คือหนึ่งในเหตุผลสำคัญที่ไม่ควรใช้ลำดับบริสุทธิ์
การประนีประนอมที่พบได้บ่อยคือเพิ่ม prefix ของชาร์ดแล้วใช้ลำดับท้องถิ่นในแต่ละชาร์ด คุณอาจเก็บเป็นสองคอลัมน์ หรือบรรจุเข้าค่าหนึ่ง
มันใช้ได้ แต่สร้างฟอร์แมต ID แบบกำหนดเอง ทุกการรวมระบบต้องเข้าใจมัน การเรียงจะไม่เป็นลำดับเวลาทั่วโลกโดยไม่ใส่ตรรกะเพิ่ม และการย้ายข้อมูลระหว่างชาร์ดอาจต้องเขียน ID ใหม่ (ซึ่งทำให้การอ้างอิงพังถ้า ID เหล่านั้นแชร์กัน)
ถามคำถามเดียวแต่ตั้งแต่เนิ่นๆ: คุณจะต้องรวมข้อมูลจากหลายฐานข้อมูลและรักษาการอ้างอิงให้คงที่หรือไม่? ถ้าใช่ วางแผนสำหรับ ID ที่เป็นเอกลักษณ์ทั่วโลกตั้งแต่วันแรก หรือเตรียมงบสำหรับการย้ายข้อมูลทีหลัง
การส่งออกและนำเข้าเป็นจุดที่การเลือก ID หยุดเป็นทฤษฎี ชั่วคราว พอคุณโคลน prod ไป staging กู้แบ็กอัพ หรือผสานข้อมูลจากสองระบบ คุณจะรู้ว่า ID ของคุณพกพาได้หรือไม่
กับ serial (auto-increment) ปกติคุณจะไม่สามารถเล่นซ้ำการแทรกในฐานข้อมูลอื่นและคาดหวังให้การอ้างอิงยังคงถูกต้อง เว้นแต่คุณจะเก็บหมายเลขดั้งเดิมไว้ ถ้าคุณนำเข้าเฉพาะส่วนของแถว (เช่น 200 ลูกค้าและคำสั่งซื้อของพวกเขา) คุณต้องโหลดตารางตามลำดับและเก็บ primary key เดิมไว้ ถ้ามีการเปลี่ยนหมายเลข foreign key จะพัง
UUIDs และ ULIDs ถูกสร้างนอกลำดับฐานข้อมูล จึงย้ายข้ามสภาพแวดล้อมได้ง่ายกว่า คุณสามารถคัดลอกแถว เก็บ ID และความสัมพันธ์ยังชี้ถูกต้อง นี่ช่วยเมื่อกู้จากแบ็กอัพ ทำ partial export หรือต้องผสานชุดข้อมูล
ตัวอย่าง: ส่งออก 50 บัญชีจาก production เพื่อดีบักปัญหาใน staging กับ primary key เป็น UUID/ULID คุณสามารถนำเข้าบัญชีเหล่านั้นพร้อมแถวที่เกี่ยวข้อง (โปรเจกต์ ใบแจ้งหนี้ ล็อก) แล้วทุกอย่างจะชี้ไปยังพาเรนต์ถูกต้อง กับ serial คุณมักต้องสร้างตารางแปลง (old_id -> new_id) แล้วเขียน foreign key ใหม่ขณะนำเข้า
สำหรับการนำเข้าจำนวนมาก พื้นฐานสำคัญกว่าส่วนประเภท ID:
คุณสามารถตัดสินใจที่แข็งแรงได้รวดเร็วถ้าคุณโฟกัสที่สิ่งที่จะทำให้คุณเจ็บปวดทีหลัง
เขียนความเสี่ยงในอนาคตสำคัญๆ ลงมา เหตุการณ์ที่เป็นรูปธรรมช่วยได้: การแบ่งฐานข้อมูล การผสานข้อมูลลูกค้าจากระบบอื่น การเขียนออฟไลน์ การคัดลอกข้อมูลระหว่างสภาพแวดล้อมบ่อยๆ
ตัดสินใจว่าการเรียง ID ต้องตรงกับเวลาไหม ถ้าต้องการ "ใหม่สุดก่อน" โดยไม่ต้องมีคอลัมน์เพิ่ม ULID (หรือ ID แบบ time-sortable อื่นๆ เช่น UUIDv7) จะเหมาะ ถ้าคุณโอเคเรียงด้วย created_at UUID และ serial ก็ใช้ได้
ประเมินปริมาณการเขียนและความไวต่อดัชนี ถ้าคาดว่าจะมีการแทรกหนักและ primary key ถูกกดดันมาก BIGINT มักเป็นมิตรกับ B-tree มากที่สุด UUID แบบสุ่มมักก่อให้เกิดการสึกหรอมากกว่า
เลือกค่าเริ่มต้น แล้วเขียนข้อยกเว้น เก็บให้เรียบง่าย: ค่าเริ่มต้นเดียวสำหรับตารางส่วนใหญ่ และกฎชัดเจนเมื่อเบี่ยงเบน (มัก: ID ที่แสดงสู่สาธารณะ vs ID ภายใน)
เว้นที่ให้เปลี่ยนได้ หลีกเลี่ยงการเข้ารหัสความหมายลงใน ID ตัดสินใจว่าจะสร้าง ID ที่ไหน (DB vs app) และเก็บ constraint ให้ชัดเจน
กับดักใหญ่คือเลือก ID เพราะมันเป็นที่นิยม แล้วพบว่ามันขัดกับวิธีคุณคิวรี ขยาย หรือแชร์ข้อมูล ปัญหาส่วนใหญ่ปรากฏหลังผ่านไปหลายเดือน
ความล้มเหลวที่พบบ่อย:
123, 124, 125 คนสามารถเดาเรคอร์ดใกล้เคียงและสแกนระบบของคุณได้สัญญาณเตือนที่ควรจัดการตั้งแต่เนิ่นๆ:
เลือกชนิด primary key หนึ่งแบบแล้วใช้ให้สม่ำเสมอในตารางส่วนใหญ่ การผสมชนิด (bigint ที่หนึ่ง UUID ที่อื่น) ทำให้ join, API และการย้ายยากขึ้น
ประมาณขนาดดัชนีที่ระดับสเกลที่คาดไว้ คีย์กว้างกว่านำไปสู่ primary index ที่ใหญ่กว่าและ IO/RAM มากขึ้น
ตัดสินใจว่าคุณจะแบ่งหน้าอย่างไร หากแบ่งหน้าด้วย ID ให้แน่ใจว่า ID มีการเรียงที่คาดเดาได้ (หรือยอมรับว่ามันจะไม่) หากแบ่งหน้าด้วย timestamp ให้ index created_at และใช้มันอย่างสม่ำเสมอ
ทดสอบแผนการนำเข้าด้วยข้อมูลที่คล้าย production ตรวจสอบว่าคุณสามารถสร้างเรคคอร์ดซ้ำโดยไม่ทำให้ foreign key พัง และการนำเข้าซ้ำจะไม่สร้าง ID ใหม่เงียบๆ
เขียนกลยุทธ์การป้องกันการชน: ใครสร้าง ID (DB หรือแอป) และจะทำอย่างไรถ้าสองระบบสร้างเรคคอร์ดออฟไลน์แล้วซิงค์ทีหลัง
ตรวจสอบให้ URL สาธารณะและล็อกไม่เปิดเผยรูปแบบที่คุณกังวล (จำนวนเรคคอร์ด อัตราสร้างเบาะแสชาร์ดภายใน) ถ้าใช้ serial ให้ถือว่าคนจะเดา ID ใกล้เคียงได้
ผู้ก่อตั้งคนเดียวเปิดตัว CRM ง่ายๆ: contacts, deals, notes หนึ่งฐานข้อมูล Postgres หนึ่งเว็บแอป เป้าหมายหลักคือการส่งของ
ตอนแรก serial bigint primary key รู้สึกสมบูรณ์แบบ การแทรกเร็ว ดัชนียังเป็นระเบียบ และอ่านง่ายในล็อก
ปีต่อมาลูกค้าขอการส่งออกไตรมาสสำหรับการตรวจสอบ และผู้ก่อตั้งเริ่มนำเข้า leads จากเครื่องมือการตลาด ID ที่ครั้งหนึ่งเป็นภายในตอนนี้ปรากฏในไฟล์ CSV อีเมล และตั๋วซัพพอร์ต ถ้าสองระบบใช้ 1, 2, 3... การผสานจะยุ่งเหยิง คุณจะต้องเพิ่มคอลัมน์แหล่งข้อมูล ตารางแมป หรือเขียน ID ใหม่ขณะนำเข้า
ปีที่สองมีแอปมือถือที่ต้องสร้างเรคคอร์ดออฟไลน์แล้วซิงค์ทีหลัง ตอนนี้คุณต้องการ ID ที่สร้างได้บนไคลเอนต์โดยไม่ต้องคุยกับฐานข้อมูล และต้องการความเสี่ยงการชนต่ำเมื่อข้อมูลมาลงในสภาพแวดล้อมต่างกัน
การประนีประนอมที่มักโตด้วยดีคือ:
ถ้าคุณลังเลระหว่าง UUID, ULID, และ serial IDs ให้ตัดสินใจจากวิธีการที่ข้อมูลของคุณจะเคลื่อนย้ายและเติบโต
ประโยคเดียวสำหรับกรณีพบบ่อย:
bigint serial primary keyการผสมมักเป็นคำตอบที่ดีที่สุด ใช้ serial bigint สำหรับตารางภายในที่ไม่ออกนอกฐานข้อมูล (ตาราง join งาน background) และใช้ UUID/ULID สำหรับเอนทิตีสาธารณะเช่น users, orgs, invoices และสิ่งที่คุณอาจส่งออก ซิงค์ หรืออ้างอิงจากบริการอื่น
ถ้าคุณสร้างใน Koder.ai (koder.ai) ควรตัดสินใจรูปแบบ ID ก่อนจะสร้างตารางและ API จำนวนมาก โหมดวางแผนของแพลตฟอร์มและ snapshot/rollback ช่วยให้คุณปรับใช้และยืนยันการเปลี่ยนสกีมาได้ตั้งแต่เนิ่นๆ ขณะที่ระบบยังเล็กพอจะเปลี่ยนได้อย่างปลอดภัย
เริ่มจากระบุความเจ็บปวดในอนาคตที่คุณอยากหลีกเลี่ยง: การแทรกช้าจากการเขียนดัชนีแบบสุ่ม การแบ่งหน้าไม่สะดวก การย้ายข้อมูลเสี่ยง หรือการชนกันของ ID ระหว่างการนำเข้าและการผสาน หากคาดว่าจะมีการย้ายข้อมูลระหว่างระบบหรือสร้างข้อมูลจากหลายที่ ให้เลือก ID ที่เป็นเอกลักษณ์ทั่วโลก (UUID/ULID) และแยกข้อกังวลเรื่องการเรียงตามเวลาออกไป
Serial bigint เป็นตัวเลือกที่ดีเมื่อคุณใช้ฐานข้อมูลเดียว เขียนข้อมูลหนัก และ ID ไม่ได้ออกสู่ภายนอก มันกะทัดรัด เร็วบน B-tree index และอ่านง่ายในล็อก ข้อเสียหลักคือยากต่อการผสานข้อมูลทีหลังเพราะเกิดการชนได้ และจะเปิดเผยจำนวนเรคคอร์ดถ้าแสดงต่อสาธารณะ
เลือก UUID เมื่อเรคคอร์ดอาจถูกสร้างจากหลายบริการ หลายภูมิภาค อุปกรณ์ หรือไคลเอนต์ออฟไลน์ และคุณต้องการความเสี่ยงการชนต่ำโดยไม่ต้องประสานงาน UUID เหมาะสำหรับ ID ที่แชร์สู่ภายนอกเพราะเดายาก ข้อแลกเปลี่ยนคือดัชนีใหญ่ขึ้นและรูปแบบการแทรกที่ค่อนข้างสุ่มเมื่อเทียบกับคีย์แบบลำดับต่อเนื่อง
ULID เหมาะเมื่อคุณต้องการ ID ที่สร้างได้จากทุกที่และโดยทั่วไปเรียงตามเวลา ซึ่งช่วยให้การแบ่งหน้าแบบเคอร์เซอร์และพฤติกรรมการแทรกดีกว่า UUIDv4 แต่ ULID ไม่ใช่การประทับเวลาอย่างเคร่งครัด—หากต้องการลำดับที่แน่นอนควรเก็บ created_at จริงไว้ด้วย
ใช่ โดยเฉพาะกับ UUIDv4 แบบสุ่มบนตารางที่เขียนหนัก การแทรกแบบสุ่มจะกระจายไปทั่ว primary key index ทำให้เกิด page split, cache churn และดัชนีใหญ่ขึ้นเมื่อเวลาผ่านไป ผลที่คุณเห็นมักเป็นอัตราการแทรกที่ช้าลงต่อเนื่องและความต้องการหน่วยความจำ/IO เพิ่มขึ้น มากกว่าการค้นหาแถวเดียวที่ช้าลงทันที
การเรียงตาม ID แบบสุ่ม (เช่น UUIDv4) จะไม่ตรงกับเวลาสร้าง ดังนั้นเคอร์เซอร์แบบ "หลังจาก id นี้" จะไม่ให้ลำดับที่เสถียร วิธีแก้ทั่วไปคือแบ่งหน้าด้วย created_at แล้วใช้ ID เป็นตัวตัดสินเมื่อเวลาเท่ากัน เช่น (created_at, id) หากอยากแบ่งหน้าด้วย ID เพียงอย่างเดียว ให้ใช้ ID ที่เรียงตามเวลาเช่น ULID
Serial IDs จะชนกันข้ามชาร์ดเพราะแต่ละชาร์ดจะสร้าง 1, 2, 3... ได้เอง คุณหลีกเลี่ยงการชนได้ด้วยการประสาน (เช่น บริการออก ID ส่วนกลาง หรือตั้งช่วงเป็นของแต่ละชาร์ด) แต่นั่นเพิ่มความซับซ้อนและอาจเป็นคอขวด UUID/ULID ลดความจำเป็นในการประสานเพราะแต่ละชาร์ดสร้าง ID ได้อย่างปลอดภัยด้วยตัวเอง
UUIDs/ULIDs ง่ายกว่าเมื่อส่งออก/นำเข้าหรือผสานข้อมูลเพราะคุณสามารถคัดลอกแถวแล้วเก็บ ID ไว้ได้ ความสัมพันธ์ยังคงชี้ถูกต้อง โดยไม่ต้องเปลี่ยนหมายเลข ในกรณี serial คุณมักต้องสร้างตารางแปลง (old_id -> new_id) แล้วเขียน foreign key ใหม่ขณะนำเข้า ซึ่งทำผิดได้ง่าย ถ้าคุณทำการโคลนสภาพแวดล้อมบ่อยๆ ให้ใช้ ID ที่เป็นเอกลักษณ์ทั่วโลก
รูปแบบทั่วไปคือใช้สอง ID: primary key ภายในกะทัดรัด (serial bigint) สำหรับ join และประสิทธิภาพการจัดเก็บ และ public ID คงที่ (ULID หรือ UUID) สำหรับ URL, API, การส่งออก และการอ้างอิงข้ามระบบ คีย์สาธารณะควรถูกมองว่าเสถียรและไม่ควรนำกลับมาใช้ใหม่หรือแปลความหมาย
ตัดสินใจตั้งแต่เนิ่นๆ แล้วใช้สม่ำเสมอทั่วตารางและ API ใน Koder.ai เลือกกลยุทธ์ ID เริ่มต้นในโหมดวางแผนก่อนสร้างสกีมและ endpoints มากมาย แล้วใช้ snapshot/rollback เพื่อยืนยันการเปลี่ยนขณะที่โปรเจกต์ยังเล็ก การยากที่สุดไม่ใช่การสร้าง ID ใหม่ แต่เป็นการอัปเดต foreign key, แคช, payload ในล็อก และการเชื่อมต่อภายนอกที่ยังอ้างอิง ID เก่า