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

ชื่อ​ไทย 200 ชื่อ เรียง 13 ทาง ได้ 2 ลำดับ และ 188 ตำแหน่ง​ไม่​ตรง​กัน

คุณ​มี​ราย​ชื่อ 200 ชื่อ คุณ​เขียน ORDER BY name แล้ว​ส่ง​ขึ้น production วัน​หนึ่ง​มี​คน​ถาม​ว่า​ทำไม ไกรสร ถึง​ไป​อยู่​เกือบ​ท้าย​สุด

คุณ​เปิด​ดู มัน​อยู่​ตำแหน่ง​ที่ 194 จาก 200 จริง แล้ว​คุณ​เปิด​ดู​รายการ​เดียวกัน​บน​เครื่อง​ของ​เพื่อน​ร่วม​ทีม ชื่อ​เดียวกัน​นั้น​อยู่​ตำแหน่ง​ที่ 17

query ประโยค​เดียวกัน ตาราง​ใบ​เดียวกัน ชื่อ​ชุด​เดียวกัน ห่าง​กัน 177 ตำแหน่ง

หน้า​นี้​เอา​คลัง​ชื่อ​ไทย 200 ชื่อ​ไป​เรียง​ด้วย 13 ทาง​ที่​นัก​เขียน​โปรแกรม​เจอ​จริง บน SQLite, PostgreSQL 18 และ .NET แล้ว​นับ​ว่า​ผล​ไม่​ตรง​กัน​กี่​ตำแหน่ง คำ​ตอบ​คือ 188 จาก 200 หรือ 94% ของ​รายการ

และ​ธี​สิ​ส​ของ​หน้า​นี้​อยู่​ตรง​นี้ — ไม่มี​ทาง​ไหน​ใน​สิบ​สาม​ทาง​นั้น​เป็นการ​เลือก​เรื่อง​ภาษา​ไทย ไม่มี​ใคร​นั่ง​ลง​แล้ว​ตัดสิน​ใจ​ว่า​จะ​เรียง​ชื่อ​ไทย​แบบ​ไหน สิ่ง​ที่​ตัดสิน​แทน​พวก​เขา​คือ base image ของ​คอนเทนเนอร์ กับ​การ​ที่​มี​คน​พิมพ์ CompareOrdinal แทนที่​จะ​ไม่​พิมพ์​อะไร​เลย

ทำไม​ชื่อ​ที่​ขึ้น​ต้น​ด้วย เ แ โ ใ ไ ถึง​ถูก​ดัน​ไป​กอง​ท้าย​รายการ​ทั้งหมด

ภาษา​ไทย​เขียน​สระ​ห้า​ตัว​นี้​ไว้ ก่อน พยัญชนะ​ที่​มัน​ออก​เสียง ตาม เกษม เขียน เ ก่อน ก แต่​อ่าน​ว่า กะ-เษม สายตา​เห็น เ มา​ก่อน หู​ได้ยิน ก มา​ก่อน

ฐาน​ข้อมูล​ที่​เรียง​ตาม codepoint เห็น​แต่​สายตา มัน​ไม่รู้​เรื่อง​เสียง

สระ​หน้า​ทั้ง​ห้า​อยู่​ที่ U+0E40 ถึง U+0E44 ส่วน​พยัญชนะ​ไทย​ทั้งหมด​อยู่​ที่ U+0E01 ถึง U+0E2E ตัว​ที่​สูง​ที่สุด​คือ ฮ ที่ U+0E2E ซึ่ง​ยัง​ต่ำ​กว่า เ อยู่ดี

ผล​คือ​ชื่อ​ที่​ขึ้น​ต้น​ด้วย​สระ​หน้า ทุก​ชื่อ ต้อง​เรียง​หลัง​ชื่อ​ที่​ขึ้น​ต้น​ด้วย​พยัญชนะ ทุก​ชื่อ ไม่ใช่​บาง​ครั้ง แต่​ทุก​ครั้ง เพราะ​มัน​เป็น​ผล​ของ​ตัวเลข​ล้วน ๆ

ใน​คลัง​นี้​มีชื่อ​สระ​หน้า 42 ชื่อ เรียง​ตาม codepoint แล้ว​มัน​ไป​กอง​อยู่​ตำแหน่ง​ที่ 159 ถึง 200 ติด​กัน​เป็น​บล็อก​เดียว ไม่มี​ชื่อ​พยัญชนะ​แทรก​เลย​สัก​ตัว

เรียง​อีก​แบบ​หนึ่ง มัน​กระจาย​อยู่​ตั้งแต่​ตำแหน่ง​ที่ 12 ถึง 200

รอย​ร้าว​นี้​ไม่​ได้​อยู่​ที่​ชื่อ​แปลก ๆ มัน​อยู่​ที่​ชื่อ​ธรรมดา​ที่สุด

ไกรสร เกษม แก้วตา โกศล ใจดี เป็น​ชื่อ​ไทย​ที่​พบ​ทั่วไป ไม่ใช่​ตัวอย่าง​ที่​หา​มา​ให้​พัง

ชื่อ​ที่​ขยับ​ไกล​ที่สุด​ใน​คลัง​นี้​ขยับ 177 ตำแหน่ง และ​ทั้ง​คลัง​ขยับ​เฉลี่ย 33.4 ตำแหน่ง

สอง​ลำดับ​ตรง​กัน​แค่ 12 ตำแหน่ง และ​ตำแหน่ง​ที่​ตรง​กัน​เกือบ​ทั้งหมด​อยู่​หัว​รายการ

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

ตำแหน่งเรียง​ตาม codepointเรียง​ผ่าน ICU
10กิตติ​ศักดิ์กิตติ​ศักดิ์
11กุลธิดากุลธิดา
12ขจรเกศริน
13ขจร​ศักดิ์เกษม
14ขนิษฐาเกียรติศักดิ์
15ขวัญ​ชัยแก้วตา
16ขวัญ​เรือนโกศล
17คณิต​าไกรสร

ฝั่ง​ขวา​แทรก เกศริน เกษม เกียรติศักดิ์ เข้า​มา​ต่อ​จาก​กลุ่ม ก เพราะ​มัน​อ่าน​ว่า ก ตาม​ด้วย​เสียง​สระ ส่วน​ฝั่ง​ซ้าย​ยัง​ไล่​พยัญชนะ​ต่อ​ไป​ที่ ข

ชื่อ​ที่​ขยับ​ไกล​ที่สุด​แปด​ตัว​เป็น​ชื่อ​สระ​หน้า​ทั้งหมด

ชื่อตำแหน่ง​เมื่อ​เรียง​ตาม codepointตำแหน่ง​เมื่อ​เรียง​ผ่าน ICUขยับ
ไกรสร19417177
โกศล18516169
แก้วตา17915164
ใจ​งาม19139152
ใจดี19240152
เกศริน15912147
เกษม16013147
เกียรติศักดิ์16114147

ไม่มี​คู่​ไหน​เลย​ที่ ICU บอก​ว่า​เท่า​กัน — คู่​ที่​เท่า​กัน​มี 0 คู่ แปล​ว่า​ลำดับ​ที่​ได้​ไม่​ได้​ขึ้น​กับ​ลำดับ​ที่​ชื่อ​เรียง​อยู่​ใน file ต้นทาง มัน​ขึ้น​กับ​กฎ​ล้วน ๆ

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

ตาราง​นี้​คือ​หัวใจ​ของ​หน้า​นี้ อ่าน column ขวา​สุด​ก่อน แล้ว​ค่อย​ย้อน​กลับ​มา​อ่าน​ว่า​อะไร​พา​ไป​ที่​นั่น

ทางที่มาลำดับ​ที่​ได้
SQLite · ORDER BY namecollation BINARY คือ​ค่า​เดียว​ที่​มี​ให้codepoint
.NET · string.CompareOrdinalท่า​ที่​ต้อง​พิมพ์​เพิ่ม​ถึง​จะ​ได้codepoint
PostgreSQL 18 บน postgres:18-alpine · ORDER BY nameไม่​ได้​ตั้ง collationcodepoint
PostgreSQL 18 บน alpine · COLLATE "C"สั่ง​ให้​เทียบ​ที​ละ byte ตรง ๆcodepoint
PostgreSQL 18 บน postgres:18 · COLLATE "C"สั่ง​ให้​เทียบ​ที​ละ byte ตรง ๆcodepoint
PostgreSQL 18 บน postgres:18 · ORDER BY nameไม่​ได้​ตั้ง collationICU
PostgreSQL บน alpine · COLLATE "th-TH-x-icu"สั่ง ICU เองICU
PostgreSQL บน Debian · COLLATE "th-TH-x-icu"สั่ง ICU เองICU
.NET · CompareInfo.GetCompareInfo("th-TH")สั่ง culture เองICU
.NET · Array.Sort(string[])ไม่​ได้​ใส่​ตัว​เทียบ​เลยICU
.NET · LINQ OrderBy(x => x)ไม่​ได้​ใส่​ตัว​เทียบ​เลยICU
.NET · CompareInfo.GetCompareInfo("en-US")culture อังกฤษ ไม่ใช่​ไทยICU
.NET · CultureInfo.InvariantCultureculture กลาง ไม่​ผูก​กับ​ภาษา​ไหนICU

5 ทาง​ให้​ลำดับ codepoint และ 8 ทาง​ให้​ลำดับ​ที่​ผ่าน ICU รวม​แล้ว​ได้ 2 ลำดับ ไม่ใช่ 13 ลำดับ

สอง​แถว​ที่​น่า​สนใจ​ที่สุด​คือ​สอง​แถว​สุดท้าย culture อังกฤษ​กับ culture กลาง ให้​ลำดับ​เดียว​กับ culture ไทย​เป๊ะ​ทุก​ตำแหน่ง

นั่น​บอก​ว่า​กฎ​ที่​สลับ​สระ​หน้า​กับ​พยัญชนะ​ไม่​ได้​อยู่​ใน​ส่วน​เฉพาะ​ของ​ภาษา​ไทย มัน​อยู่​ใน​ตาราง​กลาง ที่ ICU ใช้​กับ​ทุก​ภาษา นัก​เขียน C# จึง​ได้​ลำดับ​นั้น​มา​โดย​ไม่​ต้อง​รู้​ว่า​ตัว​เอง​ได้​มา

แอป​เดียว​ได้​สอง​ลำดับ​พร้อม​กัน ถ้า​มัน​เรียง​สอง​ที่

Array.Sort ใน C# ให้​ลำดับ​ที่​ผ่าน ICU ส่วน ORDER BY ใน SQLite ให้​ลำดับ codepoint

แอป​ตัว​เดียวกัน​ที่​เรียง​ราย​ชื่อ​ใน​หน่วย​ความ​จำ​หน้า​หนึ่ง แล้ว​เรียง​ด้วย query อีก​หน้า​หนึ่ง จะ​แสดง​สอง​ลำดับ​ให้​ผู้​ใช้​คน​เดียวกัน โดยที่​ไม่มี​บรรทัด​ไหน​ใน code ดู​ผิด

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

ตอน​แรก​ผม​วัด PostgreSQL บน postgres:18-alpine เพราะ​สคริปต์​ที่​รี​โป​นี้​มี​อยู่​แล้ว​ใช้ image นั้น ผลอ​อก​มา​ว่า ORDER BY name ที่​ไม่​ตั้ง​อะไร​เลย​ให้​ลำดับ codepoint

ผม​กำลัง​จะ​เขียน​ว่า “PostgreSQL ที่​ไม่​ตั้ง collation ให้​ลำดับ codepoint” ซึ่ง​อ่าน​แล้ว​สม​เหตุ​สม​ผล

แล้ว​ผม​นึก​ได้​ว่า alpine ใช้ musl ส่วน image หลัก​ของ PostgreSQL ใช้ glibc สอง​ตัว​นี้​เป็น C library คนละ​ตัว และ collation ของ libc มา​จาก C library ไม่​ได้​มา​จาก PostgreSQL

ผม​จึง​ส​ตาร์ต image ที่​สอง​แล้ว​วัด​ซ้ำ ผลก​ลับ​กัน​คนละ​ทาง

imagedatcollateproviderlocale ไทย​ใน libcORDER BY name ให้
postgres:18-alpineen_US.utf8libc0codepoint
postgres:18en_US.utf8libc0ICU

PostgreSQL 18.6 เท่า​กัน​ทั้ง​สอง​ฝั่ง datcollate เท่า​กัน provider เท่า​กัน และ​ทั้ง​สอง image ไม่มี locale ภาษา​ไทย​ติด​ตั้ง​ไว้​เลย​สัก​ตัว

ต่าง​กัน​ที่ image ที่ pull มา​อย่าง​เดียว

ถ้า​ผม​หยุด​ที่​การ​วัด​ครั้ง​แรก หน้า​นี้​จะ​บอก​คำ​ตอบ​ที่​ผิด​ให้​กับ​ทุก​คน​ที่​รัน PostgreSQL บน Debian ซึ่ง​เป็น image หลัก​ที่ Docker Hub แจก

ข้อ​นี้​เปลี่ยน​สถานะ​ของ​สคริปต์​ที่​รี​โป​นี้​ใช้​อยู่

scripts/pg-transaction-spike/up.sh ตรึง postgres:18-alpine ไว้ และ​บทความ​อื่น​บน​ไซต์​นี้​ใช้​มัน

probe ตัว​ใด​ก็ตาม​ที่​วัด​เรื่อง ลำดับ​ของ​ข้อความ ผ่าน​สคริปต์​นั้น​จะ​ได้​คำ​ตอบ​ที่​ไม่​ตรง​กับ PostgreSQL ที่​คน​ส่วน​ใหญ่​รัน — ข้อ​นี้​ไม่​กระทบ probe ที่​วัด WAL หรือ opcode เพราะ​ตัวเลข​พวก​นั้น​ไม่​ได้​ขึ้น​กับ collation

หน้า​นี้​จึง​มี up-glibc.sh ของ​ตัว​เอง และ​วัด​ทั้ง​สอง​ฝั่ง​แทนที่​จะ​แก้​สคริปต์​เดิม

ข้อ​แก้ตัว​นี้​ตาย​ที่ SQLite ซึ่ง​ไม่มี collation ให้​ตั้ง​ตั้งแต่​แรก

sqlite3 ที่มา​กับ Python มี collation อยู่ 3 ตัว คือ BINARY, NOCASE, RTRIM ทั้ง​สาม​ตัว​เรียง​ตาม codepoint เหมือน​กัน​หมด

สั่ง COLLATE "th-TH-x-icu" ลง​ไป​ตรง ๆ แล้ว​มัน​ตอบ​ว่า

no such collation sequence: th-TH-x-icu

compile_options ของ​มัน​ไม่มี ICU อยู่​เลย และ​นี่​คือ​ค่า​ปกติ SQLite ที่​แจก​มา​กับ runtime ส่วน​ใหญ่​ไม่​ได้​คอม​ไพล์ ICU มา​ด้วย เพราะ ICU ใหญ่​กว่า​ตัว SQLite เอง

แปล​ว่า​สำหรับ SQLite ทาง​แก้​ไม่ใช่​การ​ตั้ง​ค่า ทาง​เลือก​มี​สาม​ทาง

  • คอม​ไพล์ SQLite ใหม่​โดย​เปิด SQLITE_ENABLE_ICU
  • ลง​ส่วน​ขยาย ICU แล้ว​โหลด​ตอน runtime
  • ลง​ทะเบียน collation ของ​ตัว​เอง​จาก​ฝั่ง​แอป แล้ว​เรียก ICU ที่​นั่น

ทั้ง​สาม​ทาง​ไม่ใช่​การ​แก้ config และ​ไม่มี​ทาง​ไหน​ทำได้​จาก​ใน SQL

ส่วน PostgreSQL ตั้ง​ได้​จริง เขียน COLLATE "th-TH-x-icu" ต่อ​ท้าย ORDER BY หรือ​ประกาศ collation ไว้​ที่ column หรือ​สร้าง​ฐาน​ข้อมูล​ด้วย ICU provider ตั้งแต่​ต้น

ย้าย​ไป PostgreSQL ไม่​ได้​แปล​ว่า​ลำดับ​จะ​ถูก

ถ้า​คุณ​ย้าย​จาก SQLite ไป PostgreSQL แล้ว​ไม่​เขียน COLLATE เพิ่ม สิ่ง​ที่​ตัดสิน​ลำดับ​ให้​คุณ​คือ base image ตาม​ตาราง​ข้าง​บน

ย้าย​ไป Debian แล้ว​ลำดับ​เปลี่ยน ย้าย​ไป alpine แล้ว​ลำดับ​เหมือน​เดิม ทั้ง​สอง​กรณี​คุณ​ไม่​ได้​ตัดสิน​ใจ​อะไร​เกี่ยว​กับ​ภาษา​ไทย​เลย

หน้า​นี้​ไม่​ตอบ​คำถาม​นั้น เพราะ​มัน​ตอบ​ด้วย​การ​วัด​ไม่​ได้

ลำดับ​ที่​ผ่าน ICU ไม่​ได้ “ถูก” มัน​ทำ​ตาม​กฎ collation ที่ Unicode กับ CLDR กำหนด​ไว้ ส่วน​ลำดับ codepoint ก็​ไม่​ได้ “พัง” มัน​ทำ​ตาม​สิ่ง​ที่​มัน​สัญญา​ไว้​เป๊ะ คือ​เทียบ​ตัวเลข​ที​ละ​ตัว

สิ่ง​ที่​พูด​ได้​อย่าง​มี​หลัก​ฐาน​มี​สอง​ข้อ

ข้อ​แรก ลำดับ codepoint ดัน​ชื่อ​สระ​หน้า​ทั้ง 42 ชื่อ​ไป​กอง​ท้าย​เป็น​บล็อก​เดียว และ​เหตุผล​คือ​สระ​หน้า​ถูก​เขียน​ไว้​ก่อน​พยัญชนะ​ที่​มัน​ออก​เสียง​ตาม ซึ่ง​ไม่ใช่​เรื่อง​บังเอิญ​ของ​คลัง​นี้

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

ถ้า​คุณ​จะ​อ้าง​ว่า “เรียง​แบบ​พจนานุกรม” ใน​เอกสาร​ของ​ทีม ให้​บอก​ด้วย​ว่า​พจนานุกรม​ฉบับ​ไหน ICU ทำ​ตาม CLDR ซึ่ง​เป็น​กฎ​ชุด​หนึ่ง ไม่ใช่​กฎ​ชุด​เดียว​ที่​มี​อยู่

คอร์ส​หนึ่ง​บน​ไซต์​นี้​เขียน​ข้อ​จำกัด​นี้​ไว้​เอง​เมื่อ​ปี​ที่​แล้ว แล้ว​ไม่มี​ใคร​วัด​มัน

บท​เรียน​เรื่อง access path ของ​คอร์ส query engine เขียน​ไว้​ตรง ๆ ว่า

S_OF กับ R_OF เก็บ​รหัส​ตาม​ลำดับ​ที่​ประกาศ​ไว้​ใน code ลำดับ​นั้น​ยืน​แทน collation ของ column จริง ไม่ใช่​ลำดับ​พจนานุกรม​ภาษา​ไทย

ประโยค​นั้น​ถูก​ต้อง และ​มัน​เป็นการ​ยอมรับ​ว่า​ของ​เล่น​ใน​บท​เรียน​นั้น​เลี่ยง​ปัญหา​ไป แต่​ไม่มี​หน้า​ไหน​บน​ไซต์​นี้​เคย​วัด​ว่า​ปัญหา​ที่​เลี่ยง​ไป​นั้น​ใหญ่​แค่​ไหน

ตอน​นี้​วัด​แล้ว 188 จาก 200 ตำแหน่ง

อ่าน​บท​เรียน​นั้น​ได้ที่ access path ของ query engine ส่วน​เรื่อง​ภาษา​ไทย​กับ code ใน​แง่​มุม​อื่น อยู่​ที่ ภาษา​ไทย​ทำให้ code ของ​คุณ​พัง

สาม​ข้อ เรียง​ตาม​ลำดับ​ที่​ให้​ผล​เร็ว​ที่สุด

เขียน​ลำดับ​ที่​ต้องการ​ลง​ไป​ตรง ๆ อย่า​ปล่อย​ให้​ค่า default ตัดสิน ใน PostgreSQL คือ COLLATE ที่ column หรือ​ที่ query ใน .NET คือ​ระบุ StringComparison ทุก​ครั้ง

ตรึง base image ไว้​ให้​แน่น ถ้า​ลำดับ​สำคัญ​กับ​แอป​ของ​คุณ การ​เปลี่ยน​จาก alpine เป็น Debian คือ​การ​เปลี่ยน​พฤติกรรม ไม่ใช่​แค่​การ​เปลี่ยน​ขนาด​ของ image

ทดสอบ​ด้วย​ชื่อ​ที่​ขึ้น​ต้น​ด้วย​สระ​หน้า ชุด​ทดสอบ​ที่​มี​แต่ สมชาย กับ สมหญิง จะ​เขียว​ตลอด ใส่ ไกรสร เกษม แก้วตา ใจดี ลงไป แล้ว​เทสต์​จะ​เริ่ม​บอก​ความ​จริง

เลข​ที่​ควร​จำ​จาก​หน้า​นี้

ชื่อ​ไทย 200 ชื่อ เรียง 13 ทาง ได้ 2 ลำดับ ต่าง​กัน 188 ตำแหน่ง ตรง​กัน​แค่ 12 ตำแหน่ง

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

เครื่อง​ที่​วัด

ตัวเลข​ทุก​ตัว​มา​จาก scripts/thai-collation/ และ​คลัง​ชื่อ​ถูก​ตรึง​ด้วย sha256

แขน SQLite วัด​สด​ทุก​ครั้ง​ที่​เกต​ของ​รี​โป​รัน ส่วน​แขน PostgreSQL กับ .NET ถูก​ตรึง​ไว้ เพราะ runner ของ CI ไม่มี Docker

SQLite 3.53.4 · PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) · .NET 10.0.10 บน macOS 26.6.1