(RDB) 13 วิธีในการทำความเข้าใจ Join
(justinjaffray.com)- inner join ในฐานข้อมูลเชิงสัมพันธ์ไม่ใช่แค่ไวยากรณ์ SQL ธรรมดา แต่สามารถตีความโครงสร้างเดียวกันได้หลายแบบ ทั้งในมุมการค้นหา, nested loop, โมเดลเชิงตรรกะ, การตรวจสอบชนิดข้อมูล และมุมมองพีชคณิต
- ในตารางที่ผ่านการทำ normalization แล้ว join จะกลายเป็นเครื่องมือที่ใช้งานได้จริงที่สุดในการตาม reference เพื่อประกอบ ข้อมูลที่จัดเก็บไว้โดยไม่ซ้ำซ้อน กลับเข้าด้วยกัน
- ในมุมมองการ implement สามารถมองได้ว่าเป็นการวนดูคู่ของแถวแล้วเหลือไว้เฉพาะชุดที่ตรงตามเงื่อนไข หรือเป็นการเลือกเฉพาะชุดค่าที่มีอยู่ในความสัมพันธ์ทั้งสองฝั่งจากโดเมนของคอลัมน์
- ในโมเดลการเขียนโปรแกรม สามารถอธิบาย join ได้ด้วย
flatMap, SQLLATERAL, การแก้ ปัญหา N+1 ของ ORM, การตรวจสอบชนิดข้อมูลด้วย trait ใน Rust และandThenของ Set monad - ในเชิงคณิตศาสตร์ ทั้งเส้นทางบนกราฟ, โมเดลที่เล็กที่สุด, ความสัมพันธ์ที่อนุญาตได้ที่ใหญ่ที่สุด, ขอบเขตบนน้อยสุด ของลำดับบางส่วน และผลคูณแบบริงของนิพจน์ความสัมพันธ์ ล้วนเผยให้เห็นคุณสมบัติเดียวกันของ join
ในข้อมูลที่ผ่าน normalization แล้ว join จะกลายเป็นการค้นหา
- ในเชิงปฏิบัติที่สุด join สามารถมองได้ว่าเป็นการ ค้นหา ค่าบางอย่าง หรือการแนบข้อมูลที่ซ้ำซ้อนเข้ากับข้อมูลเดิม
- ตัวอย่างเริ่มจากวิธีเก็บ
user,country,country_codeไว้ในตารางเดียว- ค่า
countryเดียวกันจะมีcountry_codeซ้ำตามไปด้วย ทำให้เกิด ความซ้ำซ้อน - หากเป็นข้อมูลที่เปลี่ยนบ่อย ก็ต้องอัปเดตทุกตำแหน่งพร้อมกัน ทำให้มีโอกาสผิดพลาดและไม่มีประสิทธิภาพมากขึ้น
- ค่า
- ในรูปแบบที่ normalize แล้ว ความสัมพันธ์ระหว่าง
countryกับcountry_codeจะถูกแยกเป็นอีกตารางหนึ่ง และตารางผู้ใช้จะอ้างอิงเพียงcountry_id - เมื่อทำ
INNER JOINระหว่างusersกับcountriesด้วยcountry_idก็จะได้รูปแบบเดิมของuser,country,country_codeกลับมา - คำอธิบายต่อจากนี้สมมติว่า join โดยนัยตามคอลัมน์ที่มีชื่อเดียวกัน แต่จะไม่ยึดติดกับรายละเอียดไวยากรณ์ SQL อย่างเคร่งครัด
มุมมองการ implement: join ที่วนผ่านแถวและคอลัมน์
- เมื่อมีเซตสองชุด
R,Sและภาคแสดงpjoin จะวนผ่านr ∈ Rและs ∈ Sทุกตัว แล้วส่งออกเฉพาะกรณีที่p(r, s)เป็นจริง- หาก ผลคูณคาร์ทีเซียน ของคอลเลกชันสองชุดคือการเชื่อมแถวที่เป็นไปได้ทั้งหมด join ก็คือสับเซตของสิ่งนั้นที่ตรงตามเงื่อนไข
- หากมองโดยยึดคอลัมน์เป็นศูนย์กลาง ให้ถือ โดเมน ของแต่ละคอลัมน์เป็นเซตของค่าที่เป็นไปได้ แล้ววนผ่านชุดค่าของคอลัมน์
- หากมี
R(a, b)และS(b, c)ก็จะวนผ่านโดเมนของa,b,c - จะส่งออก
[a, b, c]ก็ต่อเมื่อ(a, b)อยู่ในRและ(b, c)อยู่ในS
- หากมี
join ในฐานะความเป็นจริงทางเลือกที่เข้ากันได้
- ตัวอย่างของ John และ Sally อธิบาย join ว่าเป็นการเหลือไว้เฉพาะ ความเป็นจริงที่เข้ากันได้ ในสถานการณ์ที่แต่ละฝ่ายมีข้อมูลเพียงบางส่วน
- John รู้ชุดค่าที่เป็นไปได้ของสัตว์เลี้ยงของตนกับสัตว์จรจัด ส่วน Sally ก็มีชุดค่าที่เป็นไปได้ของสัตว์เลี้ยงของตนกับสัตว์จรจัดเช่นกัน
- กรณีที่ John มีสุนัขและสัตว์จรจัดเป็นสุนัข กับกรณีที่ Sally มีแมวและสัตว์จรจัดเป็นหนู ไม่สามารถเป็นจริงพร้อมกันได้
- เพราะทั้งสองคนต้องสังเกตสัตว์จรจัดตัวเดียวกัน
- เมื่อนำสองตารางมา join ตาม
strayก็จะเหลือเฉพาะชุดสัตว์เลี้ยงของ John, สัตว์จรจัด, สัตว์เลี้ยงของ Sally ที่ไม่ขัดแย้งกัน
join ในโมเดลการเขียนโปรแกรม
flatMapคือฟังก์ชันที่สร้างอาร์เรย์ใหม่สำหรับแต่ละองค์ประกอบของอาร์เรย์เดิม แล้วนำผลลัพธ์มาต่อกัน และสามารถใช้ implement join ได้SELECT * FROM r INNER JOIN s ON pเขียนแทนได้เป็นr.flatMap(x => s.filter(y => p(x, y)))- ไวยากรณ์
LATERALใน SQL บางรูปแบบเปลี่ยน join ให้อยู่ในรูปflatMap
- หากด้านขวาของ
LATERALไม่อ้างอิงคอลัมน์ฝั่งซ้าย ก็จะเทียบเท่ากับ ผลคูณคาร์ทีเซียน- query decorrelation อาศัยวิธีลบการอ้างอิงคอลัมน์ทางด้านขวาออกผ่านการ rewrite ต่อเนื่อง
- ปัญหา N+1 ที่พบบ่อยใน ORM ก็สามารถอธิบายด้วย join ได้
- หากรันคิวรีเพิ่มเติมสำหรับแต่ละแถวใน result set ในฐานข้อมูลที่ใช้ connection อย่าง Postgres ต้นทุนคงที่ของคิวรีแต่ละตัวจะสูง
- ผลลัพธ์ของการขอให้ฐานข้อมูล “ช่วยทำการค้นหาเหล่านี้ทั้งหมด” ก็คือ join อย่างเช่น
users INNER JOIN countries - ในฐานข้อมูลแบบ in-process อย่าง Sqlite ปัญหานี้จะน้อยกว่า
เส้นทางบนกราฟและโมเดลเชิงตรรกะ
- เนื่องจากความสัมพันธ์คือการ “เชื่อมโยง” เซตสองชุดเข้าด้วยกัน จึงสามารถมองเป็นกราฟได้
- ตาราง
usersเชื่อมเซตของชื่อผู้ใช้กับเซตของcountry_id - ความสัมพันธ์ที่เชื่อม
country_idกับรหัสประเทศสองตัวอักษรก็สามารถแสดงเป็นกราฟแยกได้เช่นกัน
- ตาราง
- หากเซตทางขวาของกราฟแรกและเซตทางซ้ายของกราฟที่สองใช้ vertex set เดียวกัน ก็สามารถรวมเป็นกราฟเดียวได้
- เมื่อลิสต์เส้นทางทั้งหมดที่เริ่มจากเซตด้านซ้าย ผ่าน vertex ตรงกลาง ไปยังเซตด้านขวา สิ่งนั้นก็คือ join ของสองความสัมพันธ์
- ในตรรกะเชิงรูปแบบ ความสัมพันธ์ถูกมองเป็น ภาคแสดง และเซตของข้อเท็จจริงที่ทำให้ชุดประโยคเป็นจริงถูกมองเป็นโมเดล
- หาก
users(A, B)และcountries(B, C, D)เป็นจริง ก็ให้มีนัยว่าQ(A, B, C, D)เป็นจริง - โมเดลที่ตรงตามเงื่อนไขนี้อาจมีได้หลายแบบ
- เพื่อให้ได้ผลลัพธ์มาตรฐาน จึงเลือก โมเดลที่เล็กที่สุด ในบรรดาโมเดลที่ตรงตามเงื่อนไข
- โมเดลที่เล็กที่สุดนี้เท่ากับผลลัพธ์การ join ของ
usersกับcountry
- หาก
join ในมุมการตรวจสอบชนิดข้อมูล
- ระบบชนิดข้อมูลสไตล์ ML คล้ายกับ Prolog และ Datalog อย่างมาก จึงสามารถแสดงในลักษณะที่คล้าย join ได้
- ในตัวอย่าง Rust ความสัมพันธ์ถูกนิยามเป็น trait
UsersและCountryCodeทำหน้าที่เป็นความสัมพันธ์Smudge,Sissel,Petee,Canada,UnitedStates,CA,USถูกนิยามเป็นชนิดข้อมูลแบบเฉพาะเจาะจง
- การ implement trait อย่าง
(Smudge, Canada): Users,(Canada, CA): CountryCodeสอดคล้องกับแถวของความสัมพันธ์ - เพื่อให้
(A, B, C)อยู่ใน join ต้องเป็นทั้ง(A, B): Usersและ(B, C): CountryCode test::<(Smudge, _, CA)>()ตรวจสอบชนิดข้อมูลสำเร็จ แต่test::<(Smudge, _, US)>()ล้มเหลว เพราะไม่มีการ implement(Canada, US): CountryCode
join ในฐานะการดำเนินการของ Set monad
- ตัวอย่าง
SomeและNoneใน JavaScript เริ่มจากวิธีรวม optional record- หาก record สองตัวมี
countryเดียวกัน ก็จะ merge แล้วคืนค่าSome - หากเข้ากันไม่ได้หรือไม่มีค่า ก็จะคืนค่า
None
- หาก record สองตัวมี
andThenดึงค่าภายใน optional value ออกมา แล้วนำฟังก์ชัน merge ไปใช้- หากยังใช้ฟังก์ชัน
combineเดิม แต่เปลี่ยน container เป็นRelก็จะสามารถประมวลผลเซตของความสัมพันธ์ได้Rel.mapใช้ฟังก์ชันกับทุกแถวRel.andThenนำความสัมพันธ์ที่ได้จากแต่ละแถวมาต่อกันด้วยflatMap
- เมื่อรัน
combineเดียวกันกับความสัมพันธ์usersและความสัมพันธ์countriesจะได้ผลลัพธ์ join ที่เพิ่มรหัสประเทศให้กับSmudge,Sissel,Petee
ความสัมพันธ์ที่อนุญาตได้ที่ใหญ่ที่สุดและ join ของลำดับบางส่วน
- นิยามว่าความสัมพันธ์ที่สาม
Tซึ่งมีคอลัมน์ทั้งหมดของความสัมพันธ์สองตัวR,Sนั้น อนุญาตได้ หากไม่ประดิษฐ์ข้อมูลใหม่ขึ้นมา- เมื่อจำกัดแถวใดของ
Tให้เหลือเฉพาะคอลัมน์ของRแถวนั้นต้องอยู่ในR - เช่นเดียวกัน เมื่อจำกัดให้เหลือเฉพาะคอลัมน์ของ
Sแถวนั้นก็ต้องอยู่ในS
- เมื่อจำกัดแถวใดของ
- ตัวอย่างเช่น
Smudge, Canada, USไม่ได้รับอนุญาต- หากดูเฉพาะ
country,country_codeจะได้Canada, USแต่แถวนี้ไม่มีอยู่ในS
- หากดูเฉพาะ
- ความสัมพันธ์ว่างก็อนุญาตได้ แต่ความสัมพันธ์ที่อนุญาตได้ที่ใหญ่ที่สุดจะมี
Smudge-Canada-CA,Sissel-Canada-CA,Petee-United States-US - ความสัมพันธ์ที่อนุญาตได้ที่ใหญ่ที่สุด นี้ก็คือ join ของสองความสัมพันธ์
- ในมุมมองลำดับบางส่วน นิยาม
R ≤ Qดังนี้Qมีคอลัมน์ทั้งหมดของR- เมื่อจำกัดแต่ละแถวของ
Qให้เหลือเฉพาะคอลัมน์ของRจะได้แถวของR
- ในลำดับบางส่วนนี้ จะมีขอบเขตบนน้อยสุดของสองความสัมพันธ์
R,SคือR ∨ Sและสิ่งนี้คือ join ในความหมายเดียวกับ relational join
join ในฐานะผลคูณแบบริง
- ความสัมพันธ์สามารถแสดงในเชิงพีชคณิตได้เช่นกัน
- หนึ่งแถวแสดงเป็น ผลคูณ ของคู่คอลัมน์-ค่า
- หนึ่งความสัมพันธ์แสดงเป็น ผลรวม ของหลายแถว
- ตัวอย่างเช่น เทอมที่คูณ
user = Smudgeกับcountry_id = 1จะเป็นหนึ่งแถว - มีการเพิ่มกฎเพื่อทำให้นิพจน์ง่ายลง
- Idempotence:
[x = y][x = y] = [x = y] - Contradiction:
[x = y][x = z] = 0ify ≠ z
- Idempotence:
- เมื่อนำความสัมพันธ์ผู้ใช้
Rกับความสัมพันธ์ lookup ประเทศSมาคูณกัน แล้วกระจายด้วยกฎการแจกแจงและกฎการสลับที่ เทอมที่ขัดแย้งกันจะหายไป เหลือเฉพาะเทอมที่เข้ากันได้ - นิพจน์ที่เหลือคือ
Smudge-1-Canada-CA,Sissel-1-Canada-CA,Petee-2-United States-USซึ่งก็คือ join ของสองความสัมพันธ์อย่างพอดี - วิธีนี้ยังสามารถมองเป็น tensor contraction ได้
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
พอเริ่มมอง join เป็นมิติของพื้นที่ ก็เข้าใจง่ายขึ้นมาก
ถ้าวางแต่ละมิติเป็นตารางแยกกัน เช่น
Dim_X,Dim_Y,Dim_Zแล้วผูกด้วยEntityIdเดียวกัน ก็จะมองได้ว่าเป็นการประกอบตำแหน่ง 3 มิติของเอนทิตีหนึ่งถ้าจะสร้าง 3 มิติ ต้องมี inner join อย่างน้อย 2 ครั้ง และมิติที่ไม่ใช่พื้นที่อย่างเวลาก็ขยายด้วยวิธีเดียวกันได้
ถ้าไม่จำกัดช่วงเวลาเฉพาะ ก็จะกลายเป็นรายงานที่บรรจุตำแหน่งทั้งหมดที่เอนทิตีหนึ่งเคยอยู่ตามกาลเวลา
join ประเภทอื่น ๆ ก็เข้าใจได้ง่ายขึ้นในฐานะรูปแบบแปรผันของแนวคิดนี้ ถ้าจับคอนเซปต์ได้ถึงขั้นสามารถ “หมุน” สคีมาในหัวได้
https://dbdb.io/db/hyperdex
EntityPosition(EntityId, X, Y, Z)ไปเลยอย่างไรก็ตาม ในแง่การประกอบชิ้นส่วนจากหลายมิติและการจัดการ aggregation ก็ทำให้นึกถึงฝั่ง data warehouse
JOIN,INNER JOINด้วย การระบุตารางในFROMแล้วเขียนเงื่อนไข join ในประโยคWHEREเหมือนสมการ ดูชัดเจนกว่ามากถ้าในประโยค
FROMซับซ้อนมีJOINหลายตัวปนกันก็อ่านยาก และการอ่านนิพจน์เงื่อนไขที่เทียบเท่ากันในWHEREดูตรงไปตรงมากว่ามุมมองที่ 0 คือ “join เป็นตัวดำเนินการของ พีชคณิตเชิงสัมพันธ์”
https://en.m.wikipedia.org/wiki/Relational_algebra
natural join
R ⋈ Sคือเซตของชุด tuple ที่มีชื่อแอตทริบิวต์ร่วมกันเท่ากัน และเป็นการดำเนินการเชิงสัมพันธ์ที่สอดคล้องกับตรรกะAND⋈มองได้ว่าเป็นผลคูณคาร์ทีเซียนที่ใช้ predicate กรองแถวที่ไม่ควรอยู่ในผลลัพธ์ออก และหลายส่วนของ SQL ก็เข้าใจได้ดีจากมุมมองนี้หลายวันมานี้กำลังหาข้อมูลเรื่อง การทำงาน/การวางแผนการรันคิวรี อยู่ แต่หาเนื้อหาฝั่งการ implement อย่าง predicate, index ที่มีอยู่, join ได้ยาก
ผลค้นหาของ Google ปนเปื้อนไปด้วยเนื้อหาวิธีใช้งาน
เท่าที่หาได้ตอนนี้มีแค่เอกสารของ CMU Database Group ซึ่งยอดเยี่ยม
https://pi3.informatik.uni-mannheim.de/~moer/querycompiler.p...
คอร์ส “Database Systems on Modern CPU Architectures” ของ TUM ก็น่าจะช่วยได้ และเอกสารปี 2020 มีวิดีโอบรรยายครบชุด
https://db.in.tum.de/teaching/ss20/moderndbs/?lang=en
https://www.sqlite.org/optoverview.html
https://www.sqlite.org/queryplanner.html
https://www.postgresql.org/docs/current/planner-optimizer.ht...
การรันคิวรีกับการวางแผนคิวรีแทบจะเป็นคนละเรื่องกัน
สำหรับเปเปอร์ด้านการ optimize join ผมยังคิดว่าเปเปอร์ Selinger ต้นฉบับดีที่สุด
https://courses.cs.duke.edu/compsci516/cps216/spring03/paper...
แม้จะไม่รองรับ outer join และมีเทคนิคที่มีประสิทธิภาพกว่านี้ออกมาแล้ว แต่สำหรับคนที่ดู optimizer สาย System R ก็ยังอ่านแล้วคุ้นเคย
src/backend/optimizer/READMEของ Postgres ก็มีเนื้อหาหลายอย่างที่หาอ่านที่อื่นได้ยากบทเรียนของ Andy Pavlo จาก CMU แทบจะเป็นหนึ่งในไม่กี่แหล่งออนไลน์ที่อธิบายเรื่องนี้ และ PDF “Building Query Compilers” แม้จะยังไม่สมบูรณ์ แต่รวบรวมเปเปอร์แกนหลักของ Moerkotte และคนอื่น ๆ ไว้ จึงน่าอ่านถ้าจะทำ implementation สมัยใหม่
การหา index ที่ใช้ได้มักไม่ยาก เพราะดูว่ามี sargable predicate หรือไม่ก็พอ แต่การประมาณ selectivity นั้นยาก และการประมาณ selectivity หลังผ่าน join แทบจะเป็นหนึ่งในปัญหาที่ยากที่สุดของ optimizer
เช่น ถ้ามี
A=x AND B=y AND C=zและมีข้อมูล selectivity/cardinality ของ index(A,B),(B,C)เท่านั้น การจะประมาณ selectivity ของทั้งสามเงื่อนไขก็ไม่ใช่เรื่องง่ายยังมีเปเปอร์ที่ถึงขั้นเรียกร้องตัวแก้ปัญหา “second-order cone programming” เพื่อแก้เรื่องนี้ด้วย
query planningในคำค้นดูแล้ว แค่มองคร่าว ๆ ก็ได้ผลลัพธ์ฝั่ง implementation มากขึ้นวิธีที่ 14 คือ multi-join และยังเรียกว่า “worst-case optimal join” ด้วย แต่ชื่อนี้ไม่ค่อยดีนัก
หมายถึงการ join ตารางตั้งแต่ 3 ตารางขึ้นไปเข้าด้วยกันโดยไม่สร้างผลลัพธ์กลาง แทนที่จะ join ตารางทีละสองตารางแล้วสร้างผลลัพธ์กลางไปเรื่อย ๆ
บทความบล็อกที่เกี่ยวข้องและวิดีโอสั้นอยู่ที่ https://relational.ai/blog/dovetail-join และเปเปอร์ต้นฉบับอยู่ที่ https://dl.acm.org/doi/pdf/10.1145/3180143
ผมทำงานที่ RelationalAI และพวกเรากับบริษัทฐานข้อมูลสตาร์ทอัพอีกไม่กี่แห่งกำลังนำอัลกอริทึม join ใหม่นี้ ซึ่งมีการวิจัยในวงวิชาการมาราว 10 ปี ออกสู่ตลาด
https://justinjaffray.com/a-gentle-ish-introduction-to-worst...
ANDของ join จะกลายเป็นNORและ Tetris ใช้ประโยชน์จากจุดนี้ขอบเขต worst-case จะไม่ได้แน่นกว่าสำหรับ WCOJ แบบ stateless/streaming แต่ข้อมูลจริงมักมี box certificate ที่เล็กกว่ามาก
ผมยังไม่เห็นว่า Dovetail join รองรับ recursive query หรือก็คือ datalog ใด ๆ ที่ระบุแค่ความสัมพันธ์ผลลัพธ์ แล้วให้เอนจินจัดการความสัมพันธ์กลางเองหรือไม่
เลยสงสัยว่ารองรับ query แบบนั้นหรือเปล่า
ควรมีบทความแบบนี้มากขึ้น ที่แสดงความละเอียดอ่อนของ relational model ให้เห็น โดยเฉพาะกับ นักพัฒนาระดับแอปพลิเคชัน
คำอธิบายและการสำรวจจากมุมมอง functional programming ก็กระชับและน่าเชื่อถือ
ดูเหมือนจะพลาดโอกาสอีกครั้งในการสอนเรื่อง ปัญหา N+1
การ join กับ non-clustered index ก็ยังเป็น N+1 อยู่ เพียงแต่ไม่ใช่ N+1 ที่วิ่งไปมาระหว่างเครือข่ายกับดิสก์ แต่เป็น N+1 บนดิสก์เท่านั้น
inner join คือ Cartesian product ที่มีเงื่อนไขกำกับ
inner join ที่มีเงื่อนไข equi-join จะสร้างเงื่อนไขโดยตรง ส่วนเงื่อนไข join แบบไม่เท่ากันจำเป็นต้องประเมินจริง
เป็นคำอธิบายที่ดี คำว่า “วิธีที่ถูกต้องคือการ normalize ตาราง” นั้นถูกต้องสำหรับ transactional database แต่ใน data warehouse การ denormalize ในระดับหนึ่งเป็นที่ยอมรับกันอย่างแพร่หลาย
พอดูตัวอย่าง normalization แล้วนึกถึงสมัยก่อนที่ออกแบบตารางโดยคิดว่า primary key แบบตัวเลขเร็วกว่าสตริง
ผลคือมี
idที่ไม่มีความหมายเกิดขึ้น และถ้าต้องการค่าที่ unique จริง ๆ ก็ต้อง joinวันหนึ่งผมตระหนักว่า ถ้าใช้ unique key เดียวกันในสองตาราง ก็ลดการ join ได้ ซึ่งเรียบง่ายแต่ได้ผลมาก
idที่ unique อยู่ดี ช่วยเรื่อง logging และไม่ต้องคอยสนใจ “คีย์จริง” ที่ประกอบด้วยหลายฟิลด์แต่จะสร้าง unique index ให้ค่าที่เป็นสตริง และที่สำคัญกว่านั้นคือผูก integrity constraint ไว้กับฝั่งนั้น
ตารางที่เต็มไปด้วยสตริงที่มีความหมายอ่านง่ายกว่าตารางที่เต็มไปด้วย
idตัวเลขหรือ UUID มาก