Advent of Code 2024 ที่ทำด้วย SQL ล้วน
(databasearchitects.blogspot.com)- สามารถแก้โจทย์ทั้งหมดของ Advent of Code 2024 ได้ด้วย SQL ล้วน และประเด็นสำคัญคือ SQL บังคับให้คิดต่างจากวิธีแก้ปริศนาทั่วไป
- การไล่สำรวจฟิลด์ขนาดเล็กสามารถจัดการภายใน SQL ได้ค่อนข้างเป็นธรรมชาติ ตั้งแต่การ parse อินพุตไปจนถึงการค้นหาและการ aggregate บนพื้นฐานของ recursive query
- ในโจทย์ที่สถานะเพิ่มขึ้นมากอย่าง Day 16 ปัญหาไม่ได้อยู่ที่การแสดงผลลัพธ์เอง แต่อยู่ที่ต้นทุนการประเมินผล และมีความไม่มีประสิทธิภาพสูงจนต้องใช้ หน่วยความจำเกิน 200GB กับอินพุตจริง
- ปัญหา maximum clique ของ Day 23 เข้ากับ อัลกอริทึม Bron-Kerbosch ได้ดี แต่โครงสร้างที่พยายามจัดการหลาย set ขัดกับโมเดล recursive SQL ที่ส่งผ่านได้เพียง set เดียว
- การเขียนอัลกอริทึมซับซ้อนด้วย SQL เป็นไปได้ แต่หากมี การอัปเดตสถานะ ระหว่าง recursion และการจัดการสถานะที่สมบูรณ์กว่านี้ การรันภายในฐานข้อมูลจะใช้งานได้จริงมากขึ้น
แก้ Advent of Code 2024 ด้วย SQL เท่านั้น
- แก้ Advent of Code 2024 ด้วย SQL ล้วน และสามารถแก้โจทย์ทั้งหมดได้โดยใช้เพียง SQL
- เผยแพร่โซลูชันทั้งหมดไว้ใน GitHub repository
- ทำให้ต้องคิดเกี่ยวกับโจทย์ในอีกแบบหนึ่ง และในหลายกรณี SQL ทำหน้าที่เป็น เครื่องมือที่ใช้งานสบาย กว่าที่คาด
Day 11: SQL ที่เข้ากันดีกับปัญหาการไล่สำรวจขนาดเล็ก
- โซลูชันทั้งหมดของ Day 11 ประกอบเป็น SQL ไฟล์เดียว รวมถึงอินพุตของปริศนาด้วย
- การประมวลผลอินพุตเป็นขั้นตอนที่ค่อย ๆ แปลงสตริงให้เป็นโครงสร้างตาราง
- วางอินพุตของปริศนาไว้เป็นสตริง
- แยกอินพุตออกเป็น บรรทัด แต่ละบรรทัด
- แปลงอักขระแต่ละตัวให้เป็นพิกัดและค่า เพื่อสร้างตารางในรูปแบบ 2D array
- ส่วนอัลกอริทึมยังคงค่อนข้างสั้น
- ใช้ recursive query เพื่อไล่สำรวจฟิลด์
- ดึงคำตอบของปริศนาออกจากผลการไล่สำรวจ
- สำหรับการไล่สำรวจขนาดเล็กแบบนี้ SQL ทำงานได้ดีเพียงพอ
Day 16: ต้นทุนการเก็บสถานะของ recursive SQL
- Day 16 ไล่สำรวจฟิลด์คล้ายกับ Day 11 และคำนวณ ระยะทางการไล่สำรวจขั้นต่ำ ของแต่ละจุดที่ไปถึง
- แสดงเป็น SQL ได้ง่าย แต่กระบวนการประเมินผลสิ้นเปลือง
- กับอินพุตปริศนาจริง เมื่อฟิลด์ใหญ่ขึ้น recursive query จะสร้างและเก็บสถานะจำนวนมาก
- สิ่งที่จำเป็นจริง ๆ มีเพียง ผลลัพธ์ของรอบสุดท้าย ของ recursive query
- ถึงอย่างนั้น tuple ที่คำนวณได้ส่วนใหญ่ก็ยังถูกเก็บไว้
- ด้วยเหตุนี้ การรัน query ดังกล่าวจึงต้องใช้ หน่วยความจำมากกว่า 200GB
- หากใช้ iteration semantic ระหว่าง recursion จะลดการใช้หน่วยความจำที่มากเกินไปได้
- Umbra สามารถทำสิ่งนี้ได้
- Postgres และ DuckDB ไม่รองรับสิ่งนี้
- ดังนั้นจึงไม่ได้ใช้ฟีเจอร์นี้ในโซลูชัน
Day 23: ข้อจำกัดของอัลกอริทึมที่ต้องใช้หลาย set
- Day 23 เป็นโจทย์ที่ต้องหา maximum clique ในกราฟแบบ sparse
- ปัญหานี้สามารถคำนวณได้อย่างสมเหตุสมผลด้วย อัลกอริทึม Bron-Kerbosch
- แต่อัลกอริทึมนี้พยายามรักษาหลาย set ไว้ ขณะที่ recursive SQL ส่งผ่านได้เพียง set เดียว
- แม้จะ implement ได้ แต่การแสดงด้วย SQL ซับซ้อนขึ้นมาก และโค้ดที่ได้ก็กลายเป็น รูปแบบที่ไม่ค่อยเรียบร้อย
ฟีเจอร์ที่ recursive SQL ยังต้องการเพิ่มเติม
- แม้อัลกอริทึมซับซ้อนก็เขียนด้วย SQL ได้ และในหลายกรณีโค้ด SQL อ่านและเขียนได้ดีกว่าที่คาด
- หาก recursive SQL มีเมกะนิซึมสำหรับ การอัปเดตสถานะ ก็อาจทำให้มีประสิทธิภาพขึ้นและเขียนง่ายขึ้น
- มีงานวิจัยเกี่ยวกับ trampoline mechanism เพื่อรองรับ control flow ที่ซับซ้อนขึ้นใน recursion กำลังดำเนินอยู่ และแนวทางนี้ก็น่าจะมีประโยชน์
- จำเป็นต้องพิจารณาเมกะนิซึมสำหรับการจัดการสถานะที่ซับซ้อนขึ้นควบคู่กันไปด้วย
- เพียงมีฟีเจอร์เพิ่มเติมเล็กน้อย SQL ก็อาจเป็นตัวเลือกที่แข็งแรงสำหรับการรันอัลกอริทึมซับซ้อนโดยตรงภายในฐานข้อมูล
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
คนที่ทำเรื่องแบบนี้ได้ต้องเก่งจริง ๆ เท่านั้น เป็นศิลปะล้วน ๆ และในโลกของการเขียนโปรแกรมยังมีอะไรแบบนี้ไม่มากพอ
เห็นหัวข้อนี้แล้วมีปฏิกิริยาคล้ายกับตอนเห็นเมนูใหม่ของ Taco Bell เป็นความรู้สึกที่ผสมกันแปลก ๆ ระหว่างความอยาก ความละอาย และความทึ่งต่อ ความคิดสร้างสรรค์ของมนุษย์
สำหรับโจทย์อย่าง Advent of Code ส่วนที่ยากที่สุดน่าจะเป็น การ parse อินพุต
บางทีถ้าขุดเข้าไปในอินเทอร์เฟซแท็บเล็ตอาจจะดูวัตถุดิบได้ แต่ตอนนี้เหมือนเป็นเกมวัดดวง พูดจริง ๆ คือ https://www.amazon.com/Joe-Celkos-SQL-Smarties-Programming-d... เป็นคอร์สชั้นเยี่ยมสำหรับเรียนรู้ งานฝีมือ SQL ขั้นสุด
รู้สึกว่า HN ทั้งหมดก็เป็นเรื่องเกี่ยวกับความคิดสร้างสรรค์ของมนุษย์อยู่แล้ว และไม่แน่ใจว่าควรรับทุกอย่างด้วยความรู้สึกเหมือนกำลังดูเมนู Taco Bell หรือเปล่า
ทำได้ดีมาก ตอนแรกดูเหมือนเรื่องบ้า ๆ แต่ผมมองว่า SQL ขนาดใหญ่ เป็นหนึ่งในวิธีที่ดีที่สุดในการรองรับความซับซ้อน
ที่มันซับซ้อนก็เพราะตัวปัญหาซับซ้อนอยู่แล้ว SQL เป็นมาตรฐาน กระชับ เร็วมาก ทดสอบได้จริง และเป็นภาษาที่มีตรรกะ ไม่ใช่ว่าทุกคนจะเข้ามา maintain ได้ทันที แต่ถ้าเขียนด้วย Java เป็นหลายบรรทัดหลายฟังก์ชันก็เป็นแบบเดียวกัน
อีกอย่างที่ดีคือ SQL มีความลึก มันค้ำจุนโลกข้อมูลมานานกว่า 40 ปี จึงไม่แปลกที่ผู้คนจะเรียกร้องฟีเจอร์เฉพาะทาง model clause ของ Oracle เป็นหนึ่งในฟีเจอร์ที่ผมชอบ เพราะสามารถ implement อาร์เรย์หลายมิติได้ และเพื่อนผมเคยใช้มัน implement Conway's Game of Life ด้วยจำนวนบรรทัดน้อยกว่าที่คาดไว้มาก
สุดท้ายเขียนใหม่เป็น native code จนลดเหลือต่ำกว่า 1 วินาที และงานส่วนใหญ่คือการพิสูจน์ว่าได้ผลลัพธ์เดียวกัน พร้อมเขียนและจัดทำเอกสาร test case เพื่อไม่ให้คนต่อไปต้องลำบากแบบเดียวกัน หลังจากนั้นผมก็หลีกเลี่ยงการใส่ business logic จำนวนมากไว้ใน SQL โดยทั่วไป
โดยส่วนตัวคิดว่าสิ่งที่ซับซ้อนควรทดสอบได้ง่ายทั้งแบบ manual และ automated SQL ทดสอบแบบ manual ได้ง่าย แต่ automated test ยากกว่าโค้ดในภาษาโปรแกรมมิง ก้อน spaghetti code อย่างน้อยยังคลี่ให้ไม่แน่นนักแล้วโจมตีเป็นส่วน ๆ ได้ แต่ SQL spaghetti ที่พันกันยุ่งนี่ไม่รู้จะจัดการยังไง
ผมก็ไม่ได้เห็นด้วยเต็มที่กับคำพูดที่ว่ายิ่งจำนวนบรรทัดมาก ความเสี่ยงบั๊กก็ยิ่งสูง เพราะไม่ใช่ทุกบรรทัดเท่ากัน SQL บรรทัดเดียว 400 ตัวอักษรมีโอกาสจะกวาดตาหาปัญหาได้ยากกว่าโค้ด Java 400 บรรทัด และถึงผมจะไม่ชอบ Java ด้วยหลายเหตุผล ผมก็ยังมองแบบนั้น
ถ้าชอบ ความท้าทายเสื่อม ๆ แบบนี้ ปีนี้ผมลองทำ Advent of Code ด้วย Google Sheets
ไปได้แค่วันที่ 6 และก็ไม่ได้เก็บดาวครบสองดวงทุกวัน ค่อนข้างมั่นใจว่าเฉลยวันที่ 7 ถูกต้อง แต่พอใช้อินพุตยาว ๆ ก็ชนข้อจำกัดจำนวนอักขระต่อเซลล์
ขอให้สนุก แต่อย่าเปิดบนมือถือจะดีกว่า บางชีตทำให้แอปเด้ง
https://docs.google.com/spreadsheets/d/10FY-89y19tnRM_EAAnCd...
ตลอดอาชีพ ผมเขียน SQL มากกว่าโค้ดชนิดอื่นใด ช่วง 5 ปีหลังใช้น้อยลง เลยคงลืมไปมาก แต่เมื่อก่อนสนุกกับมันจริง ๆ
พอหยุดคิดแบบวนซ้ำ แล้วเริ่มคิดแบบ การดำเนินการกับเซต มันก็จะค่อนข้างเป็นธรรมชาติและทรงพลัง
ถ้าสคีมาถูกจัดโครงสร้างดีและสอดคล้องกับมุมมองของผู้มีส่วนได้ส่วนเสียทางธุรกิจ ตรรกะทางธุรกิจที่นิยามด้วย query SQL ก็อาจค่อนข้างเข้าใจง่าย
โค้ด, เฟรมเวิร์ก, ORM, “แนวปฏิบัติที่ดีที่สุด”, แพตเทิร์น ฯลฯ สุดท้ายก็เป็นสิ่งที่ทำให้วอกแวก วิธีเอาข้อมูลเข้าออกฐานข้อมูลมีเป็นล้านแบบ และงานย้ายบิตเองมีคุณค่าต่ำ มีโซลูชันซอฟต์แวร์ที่ทำเกินเหตุอยู่มาก ทั้งที่แค่คำสั่ง merge ง่าย ๆ หรือการนำเข้า CSV ก็พอแล้ว
ความเข้าใจผิดและความรู้สึกแย่ ๆ ต่อ SQL จำนวนมากเกิดจากการต้องรับมือกับสคีมาที่ยุ่งเหยิง ตัวภาษาเองเป็นภาษาเฉพาะโดเมนจริง ๆ ถ้าแต่แรกไม่จำเป็นต้องเขียน query แบบนั้น ก็คงไม่บ่นหนักขนาดนั้นเรื่อง query ซ้อนกันน่ากลัวและความเจ็บปวดจากไวยากรณ์ SQL ที่ตามมา ถ้าจัด tuple และ relation ให้ตรงกับวิธีที่ธุรกิจมักพูดถึง เมื่อเวลาผ่านไปก็จะต้องสู้กับสิ่งเหล่านี้น้อยลง หลายครั้งเราอาจรีแฟกเตอร์สคีมาตั้งแต่ต้นไม่ได้ แต่สามารถวาง replica หรือ view ไว้รอบ ๆ สคีมาที่แย่ แล้วใช้เป็นเป้าหมายของการพัฒนาใหม่และการรีแฟกเตอร์ได้
แน่นอนว่า SQL มีข้อบกพร่อง และบางอย่างก็ร้ายแรง เช่น ความสามารถในการทดสอบ ถึงอย่างนั้น สุดท้ายแล้วก็อยากให้การเขียนโปรแกรมทั้งหมดเป็นแบบนั้น ให้คอมพิวเตอร์ตัดสินใจว่าจะทำข้างในอย่างไร ส่วนมนุษย์โฟกัสที่ตรรกะ
เคยลองอ่าน Prolog แบบคร่าว ๆ เพื่อก้าวไปอีกขั้น แต่ยังไม่สำเร็จ ส่วนหนึ่งก็เพื่อพยายามลืม SQL บางส่วน ไม่ให้ตัวเองติดอยู่กับมันมากเกินไป บางทีอนาคตของการเขียนโปรแกรมอาจอยู่ตรงไหนสักแห่ง ระหว่าง SQL กับ Prolog ก็ได้
ถ้าคิดแต่ในมุมการดำเนินการกับเซตอย่างเดียว ก็ง่ายมากที่จะได้ query ที่ใช้เวลา 5 นาทีแทนที่จะเป็น 5 มิลลิวินาที กระบวนการในหัวแทบจะเป็นการวนคิดเสมอว่า “จะเริ่มจากตารางไหน, จะดูแถวไหนตามลำดับใด, จะ join กับอะไรด้วยเงื่อนไขอะไร, จะ aggregate อย่างไร” มันทำให้คิดใกล้กับโมเดลทางความคิดแบบ loop และ aggregate มากกว่าการดำเนินการกับเซต
หลายคนกระโดดข้ามไปยังเรื่องไร้สาระสารพัด แต่ซอฟต์แวร์เอนจิเนียริงส่วนใหญ่คือการนำข้อมูลที่ถูกต้องใส่ในรูปแบบที่ถูกต้องและเคลื่อนย้ายมันอย่างเชื่อถือได้
เมื่อไม่นานมานี้ผมรีแฟกเตอร์ codebase แบบกระจายที่ซับซ้อนครั้งใหญ่ แต่สิ่งที่นับว่าเป็น “งาน” จริง ๆ แทบมีแค่การออกแบบสคีมาใหม่ ส่วนที่เหลือเป็นการโค้ดที่ใช้เวลามาก แต่จริง ๆ แล้วใกล้เคียงกับการ implement
นอกจาก SQL ก็มีวิธีอื่นในการนิยามสคีมา แต่ SQL เป็นวิธีที่สมบูรณ์แบบสำหรับการเรียนรู้วิศวกรรมระบบจริง ๆ
ผมใช้ SQL เยอะมาก และ implement ตรรกะทางธุรกิจส่วนใหญ่ของแอปพลิเคชันประมวลผลสตรีมด้วย SQL โดยเฉพาะอย่างยิ่ง ผมชอบแนวทาง นำการคำนวณไปหา data แทนที่จะย้าย data ไปหาการคำนวณมากจริง ๆ
แต่ก็มักเจอนักพัฒนาที่ไม่ชอบไอเดียนั้น พวกเขาอยากยอมรับต้นทุน I/O มหาศาลเพื่อย้ายข้อมูลทั้งหมดไปยัง backend แล้วแสดงการคำนวณด้วยภาษาโปรแกรม “จริง ๆ”
แนวคิดของ SQL นั้นดี แต่ผมมองว่าปัญหาอยู่ที่ภาษา SQL มีส่วนที่กระอักกระอ่วนมากเกินไป และก็ไม่น่าแปลกใจ เพราะแทบไม่มีการแข่งขันมาราว 40 ปี โมเดลโปรแกรมในหัวถือว่าใช้ได้ แต่ถ้าจะเห็นความสง่างาม ต้องมองทะลุไวยากรณ์ไปยังโปรแกรมที่กำลังใช้อยู่จริง
ผมคิดว่าสิ่งที่ต้องการคือ ภาษาโปรแกรม ที่ดีจริง ๆ ซึ่งออกแบบมาให้ target ฐานข้อมูลเดิมที่มีอยู่ (Postgres, MSSQL) แล้วคอมไพล์เป็น dialect ของ SQL มีตัวเลือกที่ดูมีแววอยู่บ้าง แต่ก็ถูกผูกกับบางขอบเขต เช่น PreQL ที่ไม่อนุญาตให้เปลี่ยนแปลงข้อมูล หรือไม่ก็ผูกกับฐานข้อมูลอื่น
ผมอยากทำเองอยู่เหมือนกัน แต่งานเยอะเกินไป เส้นทางสู่การยอมรับก็ยาวไกล ไม่มีอะไรรับประกันความสำเร็จ และก็นึกโมเดลรายได้ไม่ออก
ภาษา backend ยอดนิยมทั้งหลายถูกสร้างโดยบริษัทใหญ่ แต่การเขียนโค้ดด้วย SQL ดูเหมือนจะติดอยู่ใน ภาวะกลืนไม่เข้าคายไม่ออก คือจะถูกดูแคลนจนกว่าจะมีภาษาที่ดีกว่า และภาษาที่ดีกว่าก็จะไม่เกิดขึ้นจนกว่ามันจะได้รับความนิยมมากขึ้น
common table expression และ window function สร้างความแตกต่างได้มาก โดยเฉพาะ window function ที่ทำให้สมองบิดไปหน่อย แต่ช่วยให้งานยากง่ายขึ้นอีกนิด
ผมใช้ BigQuery อยู่ ซึ่งรองรับ struct และ array และเพิ่งจะเริ่ม group array ได้ไม่นาน แต่ยังไม่มีอะไรอย่างการตรวจสอบความเท่ากัน
BigQuery ค่อย ๆ เพิ่ม syntactic sugar เช่น aggregate user-defined function และ polymorphic user-defined function ที่ใช้พารามิเตอร์
ANY TYPEทำให้ใส่ตรรกะที่นำกลับมาใช้ซ้ำลงในฟังก์ชันที่สะอาดขึ้นได้มากขึ้น แต่ส่วนตัวอยากให้ temporary function ถูกประกาศและมี scope เหมือน common table expression เพื่อให้ integrate กับเครื่องมืออย่าง DBT ที่อยากยัดทุกอย่างไว้ใน statement เดียวได้ดีขึ้นถ้าให้เลือกฟีเจอร์เดียวที่จะเพิ่ม productivity ได้มากที่สุด ก็คือการระบุพฤติกรรมของ null ใน
JOIN USINGได้ การเขียนfoo.bar IS NOT DISTINCT FROM bar.barแบบเต็ม ๆ ใน join นั้นไม่เป็นธรรมชาติและดูไม่สวย ถ้าเป็นอะไรทำนองUSING (bar RESPECT NULLS)น่าจะดีกว่ามากในทางกลับกัน ยิ่งเป็นโครงสร้างแบบ microservices ที่ service เล็ก ๆ แต่ละตัวเป็นเจ้าของฐานข้อมูลของตัวเอง และมีเพียงครึ่งหนึ่งเป็นฐานข้อมูลเชิงสัมพันธ์ ก็ยิ่งอยากวางโค้ดซับซ้อนไว้ในตัวฐานข้อมูลน้อยลง เพราะมักย้ายไปมาระหว่าง instance หรือ cluster เดี่ยว ๆ โดยเอาไปแค่ data dump ที่ค่อนข้างเรียบง่าย หรือไม่ก็ต่อ replica ใหม่เข้าไปแบบเรือของเธซีอุส
การทำได้ด้วย SQL ล้วน ๆ ก็น่าประทับใจมากอยู่แล้ว แต่เครื่องหมายที่แท้จริงของ พลังวิศวกรสายสุดโต่ง น่าจะเป็นไซต์ Blogspot ที่ดูแลต่อเนื่องมา 10 ปี
อธิบายให้ชัดคงยาก แต่ให้ความรู้สึกแบบ “ผู้เชี่ยวชาญในสายเฉพาะทาง” อย่างแรง ถึงจะไม่รู้จักผู้เขียน แต่ถ้าเป็นกลุ่มคนไม่กี่คนที่ดูแลไซต์ Blogspot ชื่อ “database architects” มานาน 10 ปี ก็น่าจะเป็นคนที่ในคอมมูนิตี้ที่ใช่ไม่จำเป็นต้องแนะนำตัวเลย
อนึ่ง ช่วงไม่กี่วันที่ผ่านมาได้ลองทำ Advent of Code ด้วย EdgeQL และเป็นประสบการณ์ที่ค่อนข้างน่าสนใจ
โพสต์ทวีตไว้สองสามอัน และคิดว่าน่าจะต้องเขียนเป็นบทความบล็อก
https://x.com/1st1/status/1864069589245858083
เทียบกับ SQL: https://x.com/1st1/status/1864412869108092997
แย่สุด ๆ แต่ก็ทำได้ดี
เผื่อคนที่ไม่รู้ ผู้เขียนเป็นหนึ่งใน นักวิจัยฐานข้อมูล ระดับแนวหน้าของโลก