กับดักที่นักพัฒนาต้องระวัง
(qouteall.fun)- สรุปกับดักที่ไม่เป็นไปตามสัญชาตญาณที่นักพัฒนามักตกหลุมพราง พร้อมอธิบายสาเหตุของบั๊กที่เกิดขึ้นได้บ่อย
- ครอบคลุมปัญหาที่พบได้บ่อยในเทคโนโลยีหลากหลายด้าน เช่น HTML, CSS, Unicode/การเข้ารหัสข้อความ, เลขทศนิยมลอยตัว, เวลา
- เน้นว่าความเข้าใจผิดหรือข้อผิดพลาดอาจเกิดขึ้นได้จาก ความแตกต่างเล็กน้อยของไวยากรณ์และพฤติกรรมการทำงาน ในแต่ละภาษาและเฟรมเวิร์ก
- อธิบายด้วยตัวอย่างถึงกับดักที่อาจเกิดขึ้นในสภาพแวดล้อมใช้งานจริงในโดเมนหลักของฝั่งแบ็กเอนด์ เช่น concurrency, networking, database
- แนะนำสถานการณ์ปัญหา วิธีแก้ไข และแนวทางปรับปรุงพฤติกรรมที่ไม่คาดคิดผ่านตัวอย่างและลิงก์อ้างอิงที่หลากหลาย
HTML และ CSS
-
ค่าเริ่มต้นของ min-width ใน Flexbox/Grid
min-widthมีค่าเริ่มต้นเป็นautomin-width: autoจะถูกกำหนดตามขนาดของคอนเทนต์ และมีลำดับความสำคัญเหนือflex-shrink,overflow: hidden,width: 0,max-width: 100%- แนะนำให้ระบุ
min-width: 0อย่างชัดเจน
-
ความแตกต่างระหว่างแนวนอนและแนวตั้งใน CSS
width: autoจะพยายามเติมพื้นที่ของพาเรนต์ ส่วนheight: autoจะพอดีกับคอนเทนต์width: autoขององค์ประกอบแบบ inline, inline-block, float จะไม่ขยายออกmargin: 0 autoใช้จัดกึ่งกลางแนวนอน ส่วนmargin: auto 0ไม่สามารถจัดกึ่งกลางแนวตั้งได้ (แต่ในflex-direction: columnสามารถจัดกึ่งกลางแนวตั้งได้)- การรวม margin จะเกิดขึ้นเฉพาะในแนวตั้งเท่านั้น
- หากเปลี่ยนทิศทางเลย์เอาต์ เช่น
writing-mode: vertical-rlพฤติกรรมก็จะกลับด้านตามไปด้วย
-
Block Formatting Context (BFC)
- สร้าง BFC ได้ด้วย
display: flow-root(นอกจากนี้overflow: hidden/auto/scroll,display: tableฯลฯ ก็ทำได้ แต่มีผลข้างเคียง) - ป้องกันปัญหา margin ของ sibling ที่ติดกันในแนวตั้งซ้อนทับกัน หรือ margin ของลูกทะลุออกนอกพาเรนต์ได้ด้วย BFC
- หากพาเรนต์มีเพียงลูกที่เป็น float ความสูงอาจยุบเหลือ 0 → แก้ได้ด้วย BFC
- หากมี
borderหรือpaddingจะไม่เกิดการรวม margin
- สร้าง BFC ได้ด้วย
-
Stacking Context
- เงื่อนไขที่ทำให้เกิด stacking context ใหม่
- คุณสมบัติการเรนเดอร์ เช่น
transform,filter,perspective,mask,opacity position: fixedหรือsticky- กำหนด
z-index+ กำหนดตำแหน่งabsolute/relative - กำหนด
z-index+ เป็นองค์ประกอบภายใน flexbox/grid isolation: isolate
- คุณสมบัติการเรนเดอร์ เช่น
- คุณลักษณะ
z-indexมีผลเฉพาะภายใน stacking context นั้นเท่านั้น- พิกัดของ
position: absolute/fixedอ้างอิงจาก ancestor ที่ถูกกำหนดตำแหน่งไว้ซึ่งอยู่ใกล้ที่สุด stickyจะไม่ทำงานข้าม stacking context- แม้เป็น
overflow: visibleก็ยังอาจถูกตัดโดย stacking context background-attachment: fixedจะถูกจัดวางโดยอิงตาม stacking context
- เงื่อนไขที่ทำให้เกิด stacking context ใหม่
-
หน่วย viewport
- ในเบราว์เซอร์มือถือ เมื่อแถบที่อยู่/แถบนำทางหายไปจากหน้าจอขณะเลื่อน ค่า
100vhจะเปลี่ยนไป - วิธีแก้สมัยใหม่: ใช้
100dvh
- ในเบราว์เซอร์มือถือ เมื่อแถบที่อยู่/แถบนำทางหายไปจากหน้าจอขณะเลื่อน ค่า
-
เกณฑ์อ้างอิงของ Absolute Position
position: absoluteไม่ได้อ้างอิงพาเรนต์เสมอไป แต่จะอ้างอิงrelative/absoluteหรือ ancestor ที่เป็น stacking context ที่อยู่ใกล้ที่สุด
-
พฤติกรรมของ Blur
backdrop-filter: blurจะไม่พิจารณาองค์ประกอบรอบข้าง
-
การทำให้ Float ไม่มีผล
- หากพาเรนต์เป็น
flexหรือgridค่าfloatของลูกจะไม่มีผล
- หากพาเรนต์เป็น
-
หน่วยเปอร์เซ็นต์ของ width/height
- หากขนาดของพาเรนต์ยังไม่ได้ถูกกำหนดล่วงหน้า จะไม่ทำงาน (เพื่อหลีกเลี่ยงการอ้างอิงแบบวนซ้ำ)
-
คุณสมบัติขององค์ประกอบ Inline
display: inlineจะไม่สนใจwidth,height,margin-top,margin-bottom
-
การจัดการ Whitespace
- โดยปกติการขึ้นบรรทัดใหม่ใน HTML จะถูกมองเป็นช่องว่าง และช่องว่างที่ต่อเนื่องกันจะถูกรวมเหลือหนึ่งช่อง
<pre>ป้องกันการยุบช่องว่าง แต่มีพฤติกรรมเฉพาะที่จุดเริ่มต้น/สิ้นสุด- โดยทั่วไปช่องว่างต้น/ท้ายคอนเทนต์จะถูกละเลย แต่
<a>เป็นข้อยกเว้น - ช่องว่าง/การขึ้นบรรทัดใหม่ระหว่าง
inline-blockจะแสดงเป็นช่องว่างจริง (แต่จะไม่เกิดใน flex/grid)
-
text-align
- ใช้ได้กับการจัดข้อความและองค์ประกอบ inline แต่ใช้จัดองค์ประกอบ block ไม่ได้
-
box-sizing
- ค่าเริ่มต้นคือ
content-box→ ไม่รวม padding/border - เมื่อกำหนด
width: 100%+paddingอาจล้นพื้นที่พาเรนต์ได้ - วิธีแก้:
box-sizing: border-box
- ค่าเริ่มต้นคือ
-
Cumulative Layout Shift
- หากไม่กำหนดแอตทริบิวต์
widthและheightให้<img>อาจเกิดอาการเลย์เอาต์สั่นจากการโหลดภาพล่าช้า - แนะนำ: กำหนดแอตทริบิวต์เพื่อป้องกัน CLS
- หากไม่กำหนดแอตทริบิวต์
-
คำขอเครือข่ายสำหรับการดาวน์โหลดไฟล์ใน Chrome
- จะไม่แสดงในแผง Network ของ DevTools (เพราะถูกจัดการในแท็บอื่น)
- หากต้องการวิเคราะห์ ให้ใช้
chrome://net-export/
-
ปัญหาการพาร์ส JavaScript ใน HTML
- ในกรณีอย่าง
<script>console.log('</script>')</script>จะมอง</script>ตัวแรกเป็นแท็กปิด - อ้างอิง: Safe JSON in script tags
- ในกรณีอย่าง
Unicode และการเข้ารหัสข้อความ
-
Code point และ grapheme cluster
- grapheme cluster คือ “หน่วยอักขระ” ใน GUI
- อักขระ ASCII ที่มองเห็นได้โดยทั่วไปมี 1 code point = 1 grapheme cluster
- อีโมจิอาจเป็น grapheme cluster หนึ่งตัวที่ประกอบด้วยหลาย code point
- ใน UTF-8 code point หนึ่งตัวใช้ 1~4 ไบต์ ดังนั้นจำนวนไบต์กับจำนวน code point จึงไม่เท่ากันเสมอ
- ใน UTF-16 code point หนึ่งตัวใช้ 2 ไบต์หรือ 4 ไบต์ (surrogate pair)
- มาตรฐานไม่ได้จำกัดจำนวน code point ภายใน cluster แต่ในการใช้งานจริงมักมีการจำกัดเพื่อเหตุผลด้านประสิทธิภาพ
-
ความแตกต่างของการทำงานกับสตริงในแต่ละภาษา
- Rust: ใช้ UTF-8 สำหรับสตริงภายใน,
len()คือจำนวนไบต์, ไม่สามารถทำ indexing โดยตรงได้,chars().count()คือจำนวน code point, ตรวจสอบความถูกต้องของ UTF-8 อย่างเข้มงวด - Golang: สตริงเป็นอาร์เรย์ของไบต์ในทางปฏิบัติ, ความยาวและ indexing เป็นระดับไบต์, โดยทั่วไปใช้ UTF-8
- Java, C#, JS: อิงกับ UTF-16, วัดความยาวเป็นหน่วยละ 2 ไบต์, indexing ก็เป็นหน่วย 2 ไบต์เช่นกัน, มี surrogate pair
- Python:
len()คืนค่าจำนวน code point, การ indexing จะคืนสตริงที่มี code point เดียว - C++:
std::stringไม่มีข้อจำกัดเรื่อง encoding, ทำงานเหมือนเวกเตอร์ของไบต์, ความยาว/การ indexing เป็นระดับไบต์ - ในบรรดาภาษาที่กล่าวถึง ไม่มีภาษาใดวัดความยาวหรือ indexing ในระดับ grapheme cluster
- Rust: ใช้ UTF-8 สำหรับสตริงภายใน,
-
BOM (Byte Order Mark)
- ไฟล์ข้อความบางชนิดมี BOM เช่น EF BB BF → ระบุว่าเป็นการเข้ารหัสแบบ UTF-8
- มักใช้ใน Windows และซอฟต์แวร์ที่ไม่ใช่ Windows อาจจัดการ BOM ไม่ได้
-
ข้อควรระวังอื่น ๆ
- เมื่อแปลงข้อมูลไบนารีเป็นสตริง ส่วนที่ไม่ถูกต้องจะถูกแทนที่ด้วย � (U+FFFD)
- มี Confusable characters อยู่ (อักขระที่ดูคล้ายกัน)
- Normalization: ตัวอย่างเช่น é สามารถแทนได้ด้วย U+00E9 (โค้ดพอยต์เดียว) หรือ U+0065+U+0301 (สองโค้ดพอยต์)
- มี Zero-width characters และ Invisible characters
- ความแตกต่างของการขึ้นบรรทัดใหม่: Windows ใช้ CRLF
\r\n, Linux/MacOS ใช้ LF\n - Han unification: อักขระที่มีรูปร่างต่างกันเล็กน้อยในแต่ละภาษาจะใช้โค้ดพอยต์เดียวกัน
- ฟอนต์จะเรนเดอร์ให้เหมาะสมโดยรวมรูปแบบเฉพาะของแต่ละภาษาไว้
- เมื่อต้องทำ internationalization ต้องเลือกฟอนต์รูปแบบที่ถูกต้อง
จำนวนทศนิยมลอยตัว (Floating point)
-
คุณสมบัติของ NaN
- NaN จะไม่เท่ากับค่าใด ๆ เลยแม้แต่ตัวมันเอง (
NaN == NaNจะเป็น false เสมอ) NaN != NaNจะเป็น true เสมอ- ผลลัพธ์ของการคำนวณที่มี NaN มักจะแพร่ต่อเป็น NaN
- NaN จะไม่เท่ากับค่าใด ๆ เลยแม้แต่ตัวมันเอง (
-
ค่าพิเศษ
- มีทั้ง +Inf และ -Inf ซึ่งต่างจาก NaN
- -0.0 เป็นค่าที่แยกจาก +0.0
- ในการเปรียบเทียบจะถือว่าเท่ากัน แต่ในการคำนวณบางอย่างจะทำงานต่างกัน
- ตัวอย่าง:
1.0 / +0.0 == +Inf,1.0 / -0.0 == -Inf
-
ความเข้ากันได้กับ JSON
- มาตรฐาน JSON ไม่อนุญาต NaN และ Inf
- JS
JSON.stringifyจะแปลง NaN, Inf เป็นnull - Python
json.dumps(...)จะพิมพ์ NaN, Infinity ออกมาตามเดิม (ผิดมาตรฐาน)- หากใช้ตัวเลือก
allow_nan=Falseแล้วมี NaN/Inf จะเกิดValueError
- หากใช้ตัวเลือก
- Golang
json.Marshalจะคืนค่า error หากมี NaN/Inf อยู่
- JS
- มาตรฐาน JSON ไม่อนุญาต NaN และ Inf
-
ปัญหาด้านความแม่นยำ
- การเปรียบเทียบจำนวนทศนิยมลอยตัวโดยตรงอาจล้มเหลว → แนะนำรูปแบบ
abs(a - b) < ε - JS จัดการตัวเลขทั้งหมดเป็นจำนวนทศนิยมลอยตัว
- ช่วงจำนวนเต็มที่ปลอดภัยคือ
-(2^53 - 1)~2^53 - 1 - หากเกินช่วงนี้ การแทนค่าจำนวนเต็มจะไม่แม่นยำ
- แนะนำให้ใช้
BigIntกับจำนวนเต็มขนาดใหญ่ - หาก JSON มีจำนวนเต็มที่เกินช่วงปลอดภัย ค่าผลลัพธ์จาก
JSON.parseอาจไม่แม่นยำ - timestamp ระดับมิลลิวินาทีปลอดภัยถึงปี 287,396 แต่ระดับนาโนวินาทีจะมีปัญหา
- ช่วงจำนวนเต็มที่ปลอดภัยคือ
- การเปรียบเทียบจำนวนทศนิยมลอยตัวโดยตรงอาจล้มเหลว → แนะนำรูปแบบ
-
กฎของการคำนวณที่ใช้ไม่ได้เสมอไป
- เนื่องจากการสูญเสียความแม่นยำตามลำดับการคำนวณ กฎการเปลี่ยนหมู่และกฎการแจกแจงจึงไม่เป็นจริงอย่างเคร่งครัด
- การคำนวณแบบขนาน (เช่น การคูณเมทริกซ์, การหาผลรวม) อาจสร้างผลลัพธ์ที่ไม่กำหนดแน่นอน
-
ประสิทธิภาพ
- การหารช้ากว่าการคูณมาก
- หากต้องหารด้วยจำนวนเดียวกันหลายครั้ง สามารถ optimize ได้ด้วยการหาค่าส่วนกลับก่อนแล้วคูณแทน
-
ความแตกต่างตามฮาร์ดแวร์
- การรองรับ FMA (Fused Multiply-Add): ฮาร์ดแวร์บางตัวคำนวณค่ากลางด้วยความแม่นยำสูงกว่า
- การจัดการช่วง Subnormal: ฮาร์ดแวร์รุ่นใหม่รองรับ แต่บางรุ่นเก่าอาจปัดเป็น 0
- ความแตกต่างของโหมดการปัดเศษ
- มีทั้ง RNTE (ปัดไปยังเลขคู่ที่ใกล้ที่สุด), RTZ (ตัดเข้าหา 0) เป็นต้น
- x86/ARM สามารถตั้งค่าได้เป็นสถานะแบบ thread-local ที่เปลี่ยนแปลงได้
- GPU มีโหมดการปัดเศษต่างกันตามระดับคำสั่ง
- ความแตกต่างของการทำงานของฟังก์ชันคณิตศาสตร์ เช่น ฟังก์ชันตรีโกณมิติ, ลอการิทึม
- x86 มี legacy 80-bit FPU และ per-core rounding mode → ไม่แนะนำให้ใช้
- นอกจากนี้ยังมีปัจจัยอีกหลายอย่างที่ทำให้ผลลัพธ์ floating point ต่างกันตามฮาร์ดแวร์
-
วิธีเพิ่มความแม่นยำ
- ออกแบบกราฟการคำนวณให้ตื้นลง (ลดโครงสร้างที่เป็นการคูณต่อเนื่อง)
- หลีกเลี่ยงกรณีที่ค่ากลางมีขนาดใหญ่มากหรือเล็กมาก
- ใช้การคำนวณระดับฮาร์ดแวร์อย่าง FMA
เวลา (Time)
-
Leap second
- Unix timestamp จะไม่สนใจ leap second
- เมื่อเกิด leap second เวลาบริเวณใกล้เคียงอาจยืดหรือหดลง (Leap smear)
-
เขตเวลา (Time zone)
- UTC และ Unix timestamp ใช้ร่วมกันทั่วโลก
- เวลาที่มนุษย์อ่านได้ขึ้นอยู่กับเขตเวลาของแต่ละพื้นที่
- แนะนำให้เก็บ timestamp ใน DB แล้วแปลงใน UI
-
เวลาออมแสง (DST)
- บางพื้นที่มีการปรับนาฬิกา 1 ชั่วโมงในช่วงฤดูร้อน
-
การซิงก์ NTP
- ระหว่างการซิงก์อาจเกิดสถานการณ์ที่เวลา “ย้อนกลับ” ได้
-
การตั้งค่าเขตเวลาของเซิร์ฟเวอร์
- แนะนำให้ตั้งค่าเซิร์ฟเวอร์เป็น UTC
- ในระบบแบบกระจาย หากแต่ละโหนดใช้เขตเวลาต่างกันจะเกิดปัญหา
- หลังเปลี่ยนเขตเวลาของระบบอาจต้องตั้งค่า DB ใหม่หรือรีสตาร์ต
-
นาฬิกาฮาร์ดแวร์ vs นาฬิการะบบ
- นาฬิกาฮาร์ดแวร์ไม่มีแนวคิดเรื่องเขตเวลา
- Linux: จัดการนาฬิกาฮาร์ดแวร์เป็น UTC
- Windows: จัดการนาฬิกาฮาร์ดแวร์เป็นเวลาท้องถิ่น
Java
==ใช้เปรียบเทียบ reference ของอ็อบเจ็กต์ ส่วนการเปรียบเทียบเนื้อหาของอ็อบเจ็กต์ต้องใช้.equals- หากไม่ override
equalsและhashcodeการตัดสินว่าอ็อบเจ็กต์เหมือนกันใน map/set จะอิงตาม reference - หากแก้ไขเนื้อหาของอ็อบเจ็กต์ที่เป็น key ของ map หรือสมาชิกของ set การทำงานของคอนเทนเนอร์จะพัง
- เมธอดที่คืนค่า
List<T>อาจคืน mutableArrayListหรือ immutableCollections.emptyList()แล้วแต่กรณี และหากแก้ไขอย่างหลังจะเกิดUnsupportedOperationException - มีกรณีที่เมธอดซึ่งคืนค่า
Optional<T>กลับคืนnull(ไม่แนะนำ) - หากมี return ในบล็อก
finallyexception ที่เกิดในtryหรือcatchจะถูกเพิกเฉยและใช้ค่าที่finallyคืนแทน - มีไลบรารีที่เพิกเฉยต่อ interrupt และกระบวนการ initialize class ที่รวม IO อาจพังเพราะ interrupt ได้
- exception ของ task ที่ส่งเข้า thread pool ด้วย
.submit()จะไม่ถูกพิมพ์ลง log โดยปริยาย และตรวจได้ผ่าน future เท่านั้น; ถ้ามองข้าม future ก็จะตรวจ exception ไม่ได้- งาน
scheduleAtFixedRateจะหยุดเงียบ ๆ เมื่อเกิด exception
- งาน
- หาก numeric literal ขึ้นต้นด้วย 0 จะถูกมองเป็นเลขฐานแปด (
0123→ 83) - debugger จะเรียก
.toString()ของตัวแปรภายในฟังก์ชัน และtoString()ของบางคลาสมีผลข้างเคียง ทำให้พฤติกรรมของโค้ดระหว่างดีบักอาจต่างออกไปได้ (ปิดได้ใน IDE)
Golang
append()จะนำหน่วยความจำเดิมกลับมาใช้เมื่อยังมี capacity เหลือ และการ append กับ subslice อาจเขียนทับหน่วยความจำของ parent ได้deferจะทำงานตอนฟังก์ชันคืนค่า ไม่ใช่ตอนจบบล็อกสโคปdeferจับ mutable variable ไว้- เกี่ยวกับ
nil- nil slice กับ empty slice ไม่เหมือนกัน
- string เป็น nil ไม่ได้ มีได้แค่สตริงว่าง
- nil map อ่านได้แต่เขียนไม่ได้
- พฤติกรรมพิเศษของ interface nil: ถ้า data pointer เป็น null แต่ type info ไม่เป็น null จะไม่เท่ากับ
nil
- Dead wait: มีกรณีบั๊ก concurrency จริงใน Go
- มี Timeout หลายประเภท ซึ่งอธิบายรายละเอียดไว้ใน net/http
C/C++
- หากเก็บพอยน์เตอร์ไปยังสมาชิกของ
std::vectorแล้ว vector มีการ grow จะเกิดการจัดสรรหน่วยความจำใหม่ ทำให้พอยน์เตอร์ใช้ไม่ได้ std::stringที่สร้างจาก literal string อาจเป็นอ็อบเจ็กต์ชั่วคราว ทำให้การเรียกc_str()มีความเสี่ยง- หากแก้ไขคอนเทนเนอร์ระหว่างการวนซ้ำ จะทำให้ iterator ใช้ไม่ได้
std::removeไม่ได้ลบจริง แต่เป็นการจัดเรียงสมาชิกใหม่ การลบต้องใช้erase- หาก numeric literal เริ่มต้นด้วย 0 จะถูกตีความเป็นเลขฐาน 8 (
0123→ 83) - Undefined behavior (UB): ระหว่างกระบวนการ optimize นั้น UB สามารถถูกเปลี่ยนแปลงได้อย่างอิสระ จึงอันตรายหากเขียนโค้ดโดยพึ่งพามัน
- การเข้าถึงหน่วยความจำที่ยังไม่ได้ initialize เป็น UB
- การแปลง
char*เป็น struct pointer แล้วเข้าถึงก่อนที่อายุการใช้งานของอ็อบเจ็กต์จะเริ่มต้นเป็น UB แนะนำให้ initialize ด้วยmemcpy - การเข้าถึงหน่วยความจำที่ไม่ถูกต้อง (เช่น null pointer) เป็น UB
- integer overflow/underflow เป็น UB (ส่วน unsigned สามารถ underflow ต่ำกว่า 0 ได้)
- Aliasing: หากพอยน์เตอร์คนละชนิดอ้างถึงหน่วยความจำเดียวกัน จะเกิด UB ตามกฎ strict aliasing
- ข้อยกเว้น: 1) ชนิดที่มีความสัมพันธ์แบบสืบทอด 2) การแปลงเป็น
char*,unsigned char*,std::byte*(แต่ไม่รวมการแปลงกลับ) - แนะนำให้ใช้
memcpyหรือstd::bit_castสำหรับการแปลงแบบบังคับ
- ข้อยกเว้น: 1) ชนิดที่มีความสัมพันธ์แบบสืบทอด 2) การแปลงเป็น
- การเข้าถึง unaligned memory เป็น UB
- การจัดแนวหน่วยความจำ (Memory Alignment)
- จำนวนเต็ม 64 บิต ต้องอยู่ที่แอดเดรสที่หารด้วย 8 ลงตัว
- บน ARM การเข้าถึงแบบ unaligned อาจทำให้โปรแกรม crash ได้
- หากตีความ byte buffer เป็น struct โดยตรง อาจเกิดปัญหา alignment
- alignment อาจทำให้เกิด struct padding และสิ้นเปลืองหน่วยความจำ
- คำสั่ง SIMD บางชนิด (เช่น AVX) รองรับเฉพาะข้อมูลที่จัดแนวแล้ว โดยทั่วไปต้องการ alignment 32 ไบต์
Python
- อาร์กิวเมนต์เริ่มต้นของฟังก์ชันจะไม่ถูกสร้างใหม่ทุกครั้งที่เรียก แต่จะเก็บค่าเริ่มต้นเดิมไว้
SQL Databases
-
การจัดการ Null
x = nullใช้งานไม่ได้ ต้องใช้x is null- Null ไม่เท่ากับตัวมันเอง (คล้าย NaN)
- Unique index อนุญาตให้มี Null ซ้ำได้ (ยกเว้น Microsoft SQL Server)
- วิธีจัดการ Null ใน
select distinctแตกต่างกันไปในแต่ละ DB count(x)และcount(distinct x)จะไม่สนใจแถวที่มีค่า Null
-
พฤติกรรมทั่วไป
- การแปลงวันที่แบบ implicit อาจขึ้นอยู่กับ timezone
- join ที่ซับซ้อนร่วมกับ distinct อาจช้ากว่าการใช้ nested query
- ใน MySQL(InnoDB) หากฟิลด์สตริงไม่ใช่
utf8mb4จะเกิดข้อผิดพลาดเมื่อแทรกอักขระ UTF-8 แบบ 4 ไบต์ - MySQL(InnoDB) โดยปกติ ไม่แยกตัวพิมพ์เล็ก-ใหญ่
- MySQL(InnoDB) อนุญาตการแปลงแบบ implicit:
select '123abc' + 1;→ 124 - gap lock ของ MySQL(InnoDB) อาจทำให้เกิด deadlock ได้
- ใน MySQL(InnoDB) หาก
group byไม่ตรงกับคอลัมน์ในselectจะคืนผลลัพธ์ที่ไม่เป็น deterministic - ใน SQLite หากไม่ใช้
strictประเภทของฟิลด์แทบไม่มีความหมายมากนัก - Foreign key อาจก่อให้เกิด lock โดยปริยายและนำไปสู่ deadlock ได้
- กลไก locking อาจทำให้ repeatable read isolation ของแต่ละ DB ถูกทำลายได้
- Distributed SQL DB อาจไม่รองรับ locking หรือมีพฤติกรรมเฉพาะตัว (แตกต่างกันไปตาม DB)
-
ประสิทธิภาพ/การปฏิบัติการ
- ปัญหา N+1 query จะไม่ปรากฏใน slow query log เพราะแต่ละ query รันเร็ว
- ทรานแซกชันที่รันยาวนานอาจทำให้เกิดปัญหา lock เป็นต้น → แนะนำให้จบทรานแซกชันให้เร็ว
- กรณีของการ lock ทั้งตาราง
- ใน MySQL 8.0+ การเพิ่ม unique index/foreign key ส่วนใหญ่ทำพร้อมกันได้
- MySQL เวอร์ชันเก่าอาจเกิดการ lock ทั้งตาราง
- หาก
mysqldumpไม่มีตัวเลือก--single-transactionจะเกิด read lock ทั้งตาราง - ใน PostgreSQL การใช้
create unique indexหรือalter table ... add foreign keyจะทำให้เกิด read lock ทั้งตาราง- วิธีหลีกเลี่ยง: ใช้
create unique index concurrently - สำหรับ foreign key ให้ใช้รูปแบบ
... not validแล้วตามด้วยvalidate constraint
- วิธีหลีกเลี่ยง: ใช้
-
Range query
- ช่วงที่ไม่ทับซ้อนกัน:
- เงื่อนไขแบบตรงไปตรงมา
p >= start and p <= endไม่มีประสิทธิภาพ (แม้จะมี composite index) - วิธีที่มีประสิทธิภาพ:
(ต้องมีเพียง index ของคอลัมน์ start)select * from (select ... from ranges where start <= p order by start desc limit 1) where end >= p
- เงื่อนไขแบบตรงไปตรงมา
- ช่วงที่อาจทับซ้อนกันได้:
- ไม่มีประสิทธิภาพเมื่อใช้ B-tree index ทั่วไป
- แนะนำให้ใช้ spatial index ใน MySQL และ GiST ใน PostgreSQL
- ช่วงที่ไม่ทับซ้อนกัน:
Concurrency and Parallelism
-
volatile
volatileไม่สามารถใช้แทน lock ได้ และไม่ได้ให้ atomicity- ข้อมูลที่ lock ปกป้องอยู่แล้วไม่จำเป็นต้องใช้
volatile(เพราะ lock รับประกัน memory order) - C/C++:
volatileป้องกันได้เพียง optimization บางส่วน และไม่ได้เพิ่ม memory barrier - Java: การเข้าถึง
volatileให้ sequentially-consistent ordering (JVM จะใส่ memory barrier หากจำเป็น) - C#: การเข้าถึง
volatileให้ release-acquire ordering (CLR จะใส่ memory barrier หากจำเป็น) - ช่วยป้องกัน optimization ที่ผิดพลาดซึ่งเกี่ยวข้องกับการ reorder การอ่าน/เขียนหน่วยความจำได้
-
ปัญหา TOCTOU (Time-of-check to time-of-use)
-
การจัดการเงื่อนไขบังคับในชั้นแอปพลิเคชันสำหรับ SQL DB
- ในกรณีที่บังคับใช้ข้อจำกัดที่ไม่สามารถแสดงด้วย unique index แบบง่ายได้ในแอปพลิเคชัน (เช่น unique ข้ามสองตาราง, unique แบบมีเงื่อนไข, unique ภายในช่วงเวลา):
- MySQL(InnoDB): ที่ระดับ repeatable read หากทำ
select ... for updateแล้วค่อย insert และมี index บนคอลัมน์ unique ก็ถือว่าใช้ได้เพราะอาศัย gap lock (แต่ gap lock อาจทำให้เกิด deadlock ภายใต้โหลดสูง → ต้องมี deadlock detection และ retry) - PostgreSQL: logic เดียวกันในระดับ repeatable read ยังไม่เพียงพอเมื่อมี concurrency (ปัญหา write skew)
- วิธีแก้:
- ใช้ serializable isolation level
- ใช้ข้อจำกัดของ DB แทนแอปพลิเคชัน
- unique แบบมีเงื่อนไข → partial unique index
- unique ข้ามสองตาราง → แทรกข้อมูลที่ซ้ำกันลงในตารางแยก แล้วใส่ unique index
- การกีดกันภายในช่วงเวลา → range type + exclude constraint
- วิธีแก้:
- MySQL(InnoDB): ที่ระดับ repeatable read หากทำ
- ในกรณีที่บังคับใช้ข้อจำกัดที่ไม่สามารถแสดงด้วย unique index แบบง่ายได้ในแอปพลิเคชัน (เช่น unique ข้ามสองตาราง, unique แบบมีเงื่อนไข, unique ภายในช่วงเวลา):
-
Atomic reference counting
- หากมีหลายเธรดเปลี่ยนตัวนับเดียวกันบ่อย ๆ เช่น
Arc,shared_ptrจะทำให้ประสิทธิภาพลดลง
- หากมีหลายเธรดเปลี่ยนตัวนับเดียวกันบ่อย ๆ เช่น
-
Read-write lock
- บาง implementation ไม่รองรับการอัปเกรดจาก read lock เป็น write lock
- หากพยายามขอ write lock ขณะยังถือ read lock อยู่ อาจเกิด deadlock ได้
Common in many languages
- การตรวจสอบ Null/None/nil ที่ตกหล่น เป็นสาเหตุของข้อผิดพลาดที่พบบ่อย
- หากแก้ไขคอนเทนเนอร์ระหว่างวนลูป อาจเกิด data race ในเธรดเดียว ได้
- ความผิดพลาดจากการแชร์ข้อมูลแบบ mutable: เช่น ใน Python
[[0] * 10] * 10ไม่ใช่การสร้างอาร์เรย์ 2D ที่ถูกต้อง (low + high) / 2อาจเกิด overflow → วิธีที่ปลอดภัยคือlow + (high - low) / 2- การประเมินแบบ short circuit:
a() || b()ถ้า a เป็น true จะไม่รัน b,a() && b()ถ้า a เป็น false จะไม่รัน b - ค่าเริ่มต้นของ profiler จะรวมแค่ CPU time เท่านั้น → การรอ DB เป็นต้นจะไม่แสดงใน flamegraph ทำให้ตีความผิดได้
- dialect ของ regular expression ต่างกันในแต่ละภาษา → regex ที่ทำงานใน JS อาจใช้ใน Java ไม่ได้
Linux and bash
- หลังย้ายไดเรกทอรีแล้ว
pwdจะแสดงพาธเดิม ส่วนพาธจริงคือpwd -P cmd > file 2>&1→ ทั้ง stdout+stderr ลงไฟล์,cmd 2>&1 > file→ มีแค่ stdout ลงไฟล์ ส่วน stderr ยังออกตามเดิม- ชื่อไฟล์ แยกตัวพิมพ์เล็ก-ใหญ่ (ต่างจาก Windows)
- ไฟล์ executable มี ระบบ capability อยู่ด้วย (ตรวจสอบได้ด้วย
getcap) - ความเสี่ยงจากตัวแปรที่ unset: ถ้า
DIRunset แล้วรันrm -rf $DIR/อาจกลายเป็นrm -rf /→ ป้องกันได้ด้วยset -u - การนำค่ามาใช้กับ environment: ถ้าต้องการให้สคริปต์มีผลกับ shell ปัจจุบัน ให้ใช้
source script.sh→ ถ้าต้องการให้มีผลถาวรให้เพิ่มใน~/.bashrc - Bash มี การแคชคำสั่ง: ถ้าย้ายไฟล์ใน
$PATHอาจเกิดENOENT→ รีเฟรชแคชด้วยhash -r - หากใช้ตัวแปรโดยไม่ใส่เครื่องหมายอัญประกาศ บรรทัดใหม่จะถูกมองเป็นช่องว่าง
set -e: จะออกจากสคริปต์ทันทีเมื่อเกิดข้อผิดพลาด แต่จะไม่ทำงานภายใน conditional (||,&&,if)- K8s livenessProbe ชนกับ debugger: debugger แบบ breakpoint จะหยุดทั้งแอป ทำให้ health check ไม่ตอบสนอง → Pod อาจถูกปิดได้
React
- แก้ไข state โดยตรง ในโค้ด render
- ใช้ Hook ภายใน if/loop → ผิดกฎ
- ลืมใส่ค่าที่จำเป็นใน dependency array ของ
useEffect - ลืมโค้ด clean up ใน
useEffect - กับดักของ closure: เก็บ state เก่าไว้จนเกิดบั๊ก
- เปลี่ยนข้อมูลในตำแหน่งที่ไม่ถูกต้อง → กลายเป็น component ที่ไม่บริสุทธิ์
- ไม่ใช้
useCallback→ ทำให้เกิดการ re-render ที่ไม่จำเป็น - หากส่ง ค่าที่ไม่ได้ memoize เข้าไปใน component ที่ memoize ไว้ จะทำให้การ optimize ของ memo ใช้ไม่ได้
Git
-
Rebase คือการเขียนประวัติใหม่
- หลัง rebase แล้ว push ปกติจะชนกัน → ต้อง force push เท่านั้น
- ถ้าประวัติของ remote branch เปลี่ยน เวลาทำ pull ก็ควรใช้
--rebase --force-with-leaseในบางกรณีช่วยป้องกันการทับ commit ของนักพัฒนาคนอื่นได้ แต่ถ้า fetch อย่างเดียวแล้วไม่ pull ก็ยังป้องกันไม่ได้
-
ปัญหาเวลา revert merge
- การ revert merge ให้ผลไม่สมบูรณ์ → ถ้า merge branch เดิมอีกครั้งจะไม่มีการเปลี่ยนแปลง
- วิธีแก้: revert ตัว revert อีกที หรือใช้วิธีที่สะอาดกว่า (backup → reset → cherry-pick → force push)
-
ข้อควรระวังเกี่ยวกับ GitHub
- ถ้า commit secret อย่าง API key ไปแล้ว ต่อให้ force push ทับ GitHub ก็ยังมีประวัติเหลืออยู่
- ถ้า B เป็น fork ของ private repo A แม้ B จะเป็น private แต่ถ้า A กลายเป็น public เนื้อหาใน B ก็จะถูกเปิดเผยด้วย (ลบแล้วก็ยังเข้าถึงได้)
-
git stash pop: ถ้าเกิด conflict stash จะไม่ถูก drop -
.DS_Storeถูกสร้างอัตโนมัติโดย macOS → แนะนำให้เพิ่ม**/.DS_Storeใน.gitignore
Networking
- เราเตอร์และไฟร์วอลล์บางตัวจะตัด TCP connection ที่ idle แบบเงียบ ๆ → อาจทำให้ connection pool ของ HTTP client หรือ DB client ใช้งานไม่ได้ → วิธีแก้คือ ตั้งค่า TCP keepalive
- ผลลัพธ์ของ
tracerouteเชื่อถือได้ไม่มาก → บางกรณีtcptracerouteอาจมีประโยชน์กว่า - TCP slow start เป็นสาเหตุให้ latency เพิ่มขึ้นได้ → แก้ได้ด้วยการปิด
tcp_slow_start_after_idle - ปัญหา TCP sticky packet: อัลกอริทึม Nagle ทำให้การส่งแพ็กเก็ตล่าช้า → แก้ได้ด้วยการเปิด
TCP_NODELAY - เมื่อวาง backend ไว้หลัง Nginx ต้องตั้งค่าการ reuse connection → ถ้าไม่ตั้งค่า อาจเชื่อมต่อไม่สำเร็จเพราะพอร์ตภายในไม่พอในสภาวะโหลดสูง
- โดยปกติ Nginx จะ buffer packet → ทำให้ SSE(EventSource) เกิดความหน่วง
- มาตรฐาน HTTP ไม่ได้ห้าม body ในคำขอ GET หรือ DELETE → บางระบบใช้งาน body แต่ไลบรารีและเซิร์ฟเวอร์จำนวนมากไม่รองรับ
- สามารถโฮสต์หลายเว็บไซต์บน IP เดียวได้ → ใช้ HTTP
Hostheader และ SNI ของ TLS ในการแยก → จึงมีเว็บไซต์ที่เข้าแบบ IP ตรง ๆ ไม่ได้ - CORS: เมื่อขอข้อมูลข้าม origin เบราว์เซอร์จะบล็อกการเข้าถึง response → ฝั่งเซิร์ฟเวอร์ต้องตั้ง header
Access-Control-Allow-Origin- ถ้ารวมการส่ง cookie ด้วย ต้องมีการตั้งค่าเพิ่มเติม
- ถ้า frontend และ backend ใช้ โดเมนและพอร์ตเดียวกัน ก็จะไม่มีปัญหา CORS
Other
-
ข้อควรระวังเกี่ยวกับ YAML
- YAML ไวต่อช่องว่าง →
key:valueผิด,key: valueจึงถูกต้อง - รหัสประเทศ
NOถ้าเขียนโดยไม่ใส่เครื่องหมายอัญประกาศ อาจถูกตีความเป็นfalse - ถ้าเขียน Git commit hash โดยไม่ใส่เครื่องหมายอัญประกาศ อาจถูกแปลงเป็นตัวเลขได้
- YAML ไวต่อช่องว่าง →
-
ปัญหา CSV ใน Excel
- เมื่อเปิด CSV ใน Excel จะมีการ แปลงค่าอัตโนมัติ
- แปลงเป็นวันที่:
1/2,1-2→2-Jan - แปลงเลขขนาดใหญ่แบบไม่แม่นยำ:
12345678901234567890→12345678901234500000
- แปลงเป็นวันที่:
- สาเหตุคือ Excel จัดการตัวเลขภายในแบบ floating point
- เคยมีกรณีที่ชื่อยีน SEPT1 ถูกเปลี่ยนผิดพลาด เพราะปัญหานี้
- เมื่อเปิด CSV ใน Excel จะมีการ แปลงค่าอัตโนมัติ
ยังไม่มีความคิดเห็น