13 cách để hiểu phép join trong (RDB)
(justinjaffray.com)- inner join trong cơ sở dữ liệu quan hệ không chỉ là cú pháp SQL đơn thuần; cùng một cấu trúc có thể được diễn giải khác nhau qua góc nhìn truy vấn, vòng lặp lồng nhau, mô hình logic, kiểm tra kiểu và đại số
- Trong các bảng đã chuẩn hóa, join trở thành công cụ thực tế nhất để kết hợp lại thông tin được lưu trữ không trùng lặp bằng cách lần theo các tham chiếu
- Từ góc độ triển khai, có thể xem đây là việc duyệt qua các cặp hàng rồi chỉ giữ lại những tổ hợp thỏa điều kiện, hoặc chọn các tổ hợp giá trị chỉ tồn tại ở cả hai quan hệ trong miền của các cột
- Trong mô hình lập trình, có thể giải thích join bằng
flatMap, SQLLATERAL, việc giải quyết vấn đề N+1 trong ORM, kiểm tra kiểu dựa trên trait của Rust, vàandThencủa Set monad - Về mặt toán học, đường đi trên đồ thị, mô hình nhỏ nhất, quan hệ khả dĩ lớn nhất, cận trên tối thiểu trong thứ tự bộ phận, và tích vòng của biểu thức quan hệ đều bộc lộ cùng một tính chất của join
Trong dữ liệu đã chuẩn hóa, join trở thành truy vấn tra cứu
- Về mặt thực tiễn nhất, có thể xem join là thao tác tra cứu một giá trị nào đó, hoặc gắn thêm thông tin dư thừa vào dữ liệu hiện có
- Ví dụ bắt đầu từ cách lưu
user,country,country_codetrong cùng một bảng- Với mỗi giá trị
countrygiống nhau,country_codebị lặp lại nên phát sinh trùng lặp - Nếu dữ liệu thường xuyên thay đổi, phải cập nhật tất cả các vị trí cùng lúc, làm tăng lỗi và giảm hiệu quả
- Với mỗi giá trị
- Ở dạng chuẩn hóa, quan hệ giữa
countryvàcountry_codeđược tách ra thành bảng riêng, còn bảng người dùng chỉ tham chiếucountry_id - Khi
INNER JOINusersvàcountriestheocountry_id, ta có thể thu lại dạng ban đầu gồmuser,country,country_code - Từ đây, phần giải thích tiếp theo giả định rằng việc join được thực hiện ngầm theo các cột cùng tên, nhưng không ràng buộc quá chặt vào chi tiết cú pháp SQL
Góc nhìn triển khai: join duyệt qua hàng và cột
- Với hai tập
R,Svà vị từp, join sẽ duyệt mọir ∈ R,s ∈ S, sau đó chỉ xuất ra các trường hợp màp(r, s)là đúng- Nếu tích Descartes của hai tập là mọi cách nối hàng có thể có, thì join là tập con trong đó thỏa điều kiện
- Nhìn theo hướng cột, có thể coi miền của mỗi cột là tập các giá trị khả dĩ, rồi duyệt các tổ hợp giá trị cột
- Nếu có
R(a, b)vàS(b, c)thì sẽ duyệt miền củaa,b,c - Chỉ khi
(a, b)thuộcRvà(b, c)thuộcSthì mới xuất ra[a, b, c]
- Nếu có
Join như những hiện thực thay thế tương thích
- Ví dụ về John và Sally giải thích join như cách chỉ giữ lại những hiện thực tương thích khi mỗi người chỉ biết một phần thông tin
- John biết các tổ hợp có thể có giữa pet của mình và con vật stray, Sally cũng biết các tổ hợp có thể có giữa pet của cô ấy và con vật stray
- Nếu John có dog thì trường hợp stray là dog, và nếu Sally có cat thì trường hợp stray là mouse, không thể đồng thời đúng
- Bởi vì hai người phải đang quan sát cùng một stray
- Khi join hai bảng theo
stray, chỉ còn lại các tổ hợp pet của John · stray · pet của Sally không mâu thuẫn với nhau
Join trong mô hình lập trình
flatMapvốn là hàm tạo một mảng mới cho từng phần tử của mảng gốc rồi nối các kết quả lại, và có thể dùng để triển khai joinSELECT * FROM r INNER JOIN s ON pcó thể biểu diễn thànhr.flatMap(x => s.filter(y => p(x, y)))- Cú pháp
LATERALtrong một số biến thể SQL biến join thành dạngflatMap
- Nếu vế phải của
LATERALkhông tham chiếu tới cột ở vế trái thì nó tương đương với tích Descartes- Kỹ thuật decorrelation của truy vấn dựa vào việc loại bỏ các tham chiếu cột ở vế phải bằng một chuỗi phép viết lại liên tiếp
- Vấn đề N+1 thường gặp trong ORM cũng có thể được giải thích bằng join
- Nếu chạy thêm truy vấn cho từng hàng trong tập kết quả, thì với các cơ sở dữ liệu dùng connection như Postgres, chi phí cố định của từng truy vấn riêng lẻ là khá lớn
- Kết quả của việc yêu cầu cơ sở dữ liệu “hãy thực hiện tất cả các tra cứu này” chính là một join như
users INNER JOIN countries - Với cơ sở dữ liệu in-process như Sqlite, vấn đề này ít nghiêm trọng hơn
Đường đi trên đồ thị và mô hình logic
- Vì quan hệ là thứ “liên kết” hai tập với nhau nên có thể xem nó như một đồ thị
- Bảng
usersnối tập tên người dùng với tậpcountry_id - Quan hệ nối
country_idvới mã quốc gia hai ký tự cũng có thể được biểu diễn thành một đồ thị riêng
- Bảng
- Nếu tập bên phải của đồ thị thứ nhất và tập bên trái của đồ thị thứ hai cùng chia sẻ một vertex set, ta có thể gộp chúng lại thành một
- Liệt kê mọi đường đi từ tập bên trái, đi qua vertex ở giữa rồi sang tập bên phải sẽ cho ra join của hai quan hệ
- Trong logic hình thức, quan hệ được xem là vị từ, còn mô hình là tập các sự kiện làm cho tập mệnh đề trở thành đúng
- Nếu
users(A, B)vàcountries(B, C, D)là đúng thì có một hàm ý rằngQ(A, B, C, D)là đúng - Có thể tồn tại nhiều mô hình thỏa điều kiện này
- Để có kết quả chuẩn tắc, ta chọn mô hình nhỏ nhất trong các mô hình thỏa điều kiện
- Mô hình nhỏ nhất này chính là kết quả join của
usersvàcountry
- Nếu
Join như kiểm tra kiểu
- Hệ kiểu kiểu ML rất giống Prolog và Datalog, nên có thể biểu diễn theo cách gần với join
- Trong ví dụ Rust, các quan hệ được định nghĩa bằng trait
UsersvàCountryCodeđóng vai trò các quan hệSmudge,Sissel,Petee,Canada,UnitedStates,CA,USđược định nghĩa như các kiểu cụ thể
- Các triển khai trait như
(Smudge, Canada): Users,(Canada, CA): CountryCodetương ứng với các hàng của quan hệ - Để
(A, B, C)được nằm trong join thì cần có(A, B): Usersvà(B, C): CountryCode test::<(Smudge, _, CA)>()sẽ kiểm tra kiểu thành công, nhưngtest::<(Smudge, _, US)>()sẽ thất bại vì không có triển khai(Canada, US): CountryCode
Join như phép toán của Set monad
- Ví dụ
SomevàNonetrong JavaScript bắt đầu từ cách gộp các record tùy chọn- Nếu hai record có cùng
countrythì gộp chúng lại và trả vềSome - Nếu không tương thích hoặc không có giá trị thì trả về
None
- Nếu hai record có cùng
andThenlấy giá trị bên trong optional ra rồi áp dụng hàm gộp- Giữ nguyên cùng hàm
combinenhưng đổi container thànhRel, ta có thể xử lý tập quan hệRel.mapáp dụng hàm cho mọi hàngRel.andThennối các quan hệ sinh ra từ từng hàng bằngflatMap
- Khi chạy cùng
combinetrên quan hệusersvà quan hệcountries, ta thu được kết quả join trong đóSmudge,Sissel,Peteeđược gắn thêm mã quốc gia
Quan hệ khả dĩ lớn nhất và join trong thứ tự bộ phận
- Một quan hệ thứ ba
Tcó đầy đủ mọi cột của hai quan hệR,Sđược gọi là khả dĩ nếu nó không bịa ra thông tin mới- Khi giới hạn một hàng của
Txuống các cột củaR, hàng đó phải thuộcR - Tương tự, khi giới hạn xuống các cột của
S, hàng đó cũng phải thuộcS
- Khi giới hạn một hàng của
- Ví dụ,
Smudge, Canada, USlà không khả dĩ- Nếu chỉ nhìn
country,country_codethì sẽ thànhCanada, US, nhưng đó không phải là hàng tồn tại trongS
- Nếu chỉ nhìn
- Quan hệ rỗng cũng là khả dĩ, nhưng quan hệ khả dĩ lớn nhất sẽ chứa
Smudge-Canada-CA,Sissel-Canada-CA,Petee-United States-US - Quan hệ khả dĩ lớn nhất này chính là join của hai quan hệ
- Ở góc nhìn thứ tự bộ phận, ta định nghĩa
R ≤ Qnhư sauQbao gồm mọi cột củaR- Nếu giới hạn từng hàng của
Qxuống các cột củaR, ta sẽ thu được một hàng củaR
- Trong thứ tự bộ phận này, tồn tại cận trên tối thiểu
R ∨ Scủa hai quan hệR,S, và đó chính là join theo nghĩa quan hệ
Join như tích vòng
- Quan hệ cũng có thể được biểu diễn dưới dạng đại số
- Một hàng được biểu diễn bằng tích của các cặp cột-giá trị
- Một quan hệ được biểu diễn bằng tổng của nhiều hàng
- Ví dụ, hạng tử là tích của
user = Smudgevàcountry_id = 1sẽ tương ứng với một hàng - Có thêm các quy tắc để đơn giản hóa biểu thức
- Idempotence:
[x = y][x = y] = [x = y] - Contradiction:
[x = y][x = z] = 0ify ≠ z
- Idempotence:
- Khi nhân quan hệ người dùng
Rvới quan hệ tra cứu quốc giaS, rồi khai triển bằng luật phân phối và giao hoán, các hạng tử mâu thuẫn sẽ biến mất và chỉ còn lại các hạng tử tương thích - Biểu thức còn lại là
Smudge-1-Canada-CA,Sissel-1-Canada-CA,Petee-2-United States-US, và đó chính xác là join của hai quan hệ - Cách này cũng có thể được xem là tensor contraction
1 bình luận
Ý kiến trên Hacker News
Từ khi bắt đầu hình dung join theo chiều không gian, tôi thấy nó dễ hiểu hơn nhiều
Nếu đặt mỗi chiều thành một bảng riêng như
Dim_X,Dim_Y,Dim_Zrồi gắn chúng bằng cùngEntityId, có thể xem đó là cách cấu thành vị trí 3 chiều của một thực thểĐể tạo 3 chiều thì cần ít nhất 2 phép inner join, và cũng có thể mở rộng theo cách tương tự cho các chiều phi không gian như thời gian
Nếu không giới hạn một thời điểm cụ thể, nó sẽ trở thành một báo cáo chứa mọi vị trí mà một thực thể từng có theo thời gian
Các kiểu join khác cũng dễ hiểu hơn theo biến thể này nếu bạn nắm được khái niệm đủ để có thể “xoay” schema trong đầu
https://dbdb.io/db/hyperdex
EntityPosition(EntityId, X, Y, Z)Tuy vậy, ở chỗ phải ghép các mảnh của nhiều chiều và xử lý tổng hợp thì nó gợi nhớ đến phía data warehouse
JOIN,INNER JOIN. Liệt kê bảng trongFROMrồi viết điều kiện join trong mệnh đềWHEREnhư một phương trình có vẻ rõ ràng hơn nhiềuKhi nhiều
JOINtrộn lẫn trong một mệnh đềFROMphức tạp thì rất khó đọc, còn đọc các biểu thức điều kiện tương đương trongWHERElại trực quan hơnGóc nhìn số 0 là: “join là toán tử của đại số quan hệ”
https://en.m.wikipedia.org/wiki/Relational_algebra
Natural join
R ⋈ Slà tập hợp các tổ hợp tuple có cùng tên thuộc tính chung, và là phép toán quan hệ tương ứng với phép logicANDCó thể xem
⋈là tích Descartes rồi dùng vị từ để lọc ra các hàng không được phép xuất hiện trong kết quả, và khá nhiều phần của SQL sẽ dễ hiểu theo góc nhìn nàyTôi đã tìm tài liệu về triển khai thực thi/lập kế hoạch truy vấn suốt mấy ngày nay, nhưng rất khó tìm được tài liệu đi vào phần hiện thực như vị từ, chỉ mục sẵn có, hay join
Kết quả Google bị ô nhiễm bởi các tài liệu hướng dẫn cách dùng
Đến giờ thứ duy nhất tôi tìm được là tài liệu của CMU Database Group, và nó rất xuất sắc
https://pi3.informatik.uni-mannheim.de/~moer/querycompiler.p...
Khóa học “Database Systems on Modern CPU Architectures” của TUM cũng có thể hữu ích, và tài liệu năm 2020 có đầy đủ video bài giảng
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...
Thực thi truy vấn và lập kế hoạch truy vấn trên thực tế gần như là hai thứ riêng biệt
Về các bài báo tối ưu hóa join, tôi vẫn cho rằng bài Selinger gốc là tốt nhất
https://courses.cs.duke.edu/compsci516/cps216/spring03/paper...
Nó không hỗ trợ outer join và sau đó đã có những kỹ thuật hiệu quả hơn, nhưng với ai xem các optimizer kiểu System R thì nó vẫn là tài liệu quen thuộc và dễ đọc
src/backend/optimizer/READMEcủa Postgres cũng có nhiều nội dung khó tìm thấy ở nơi khácCác bài giảng của Andy Pavlo ở CMU gần như là tài liệu duy nhất trên mạng giải thích phần này, còn PDF “Building Query Compilers” tuy chưa hoàn chỉnh nhưng có các bài báo cốt lõi của Moerkotte và cộng sự nên vẫn đáng đọc nếu muốn làm hiện thực hiện đại
Việc tìm các chỉ mục có thể áp dụng thường không quá khó vì chỉ cần xem có sargable predicate hay không, nhưng ước lượng độ chọn lọc thì khó, và ước lượng độ chọn lọc sau join gần như là bài toán khó nhất trong optimizer
Ví dụ, nếu có
A=x AND B=y AND C=zvà chỉ có thông tin độ chọn lọc/cardinality của các chỉ mục(A,B),(B,C), thì việc ước lượng độ chọn lọc của toàn bộ ba điều kiện cũng không hề đơn giảnThậm chí có bài báo đòi hỏi bộ giải cho “quy hoạch nón bậc hai” để giải chuyện này
query planning, thì nhìn qua đã thấy xuất hiện nhiều kết quả thiên về hiện thực hơnCách thứ 14 là multiway join, còn được gọi là “worst-case optimal join”, nhưng cái tên này không hay lắm
Nghĩa là thay vì join từng cặp bảng và liên tục tạo kết quả trung gian, ta join từ 3 bảng trở lên cùng lúc mà không cần kết quả trung gian
Bài blog liên quan và video ngắn có tại https://relational.ai/blog/dovetail-join, còn bài báo gốc ở https://dl.acm.org/doi/pdf/10.1145/3180143
Tôi đang làm việc tại RelationalAI, và thuật toán join mới này, đã được nghiên cứu trong giới học thuật khoảng 10 năm, đang được chúng tôi cùng một vài công ty cơ sở dữ liệu khởi nghiệp khác đưa ra thị trường
https://justinjaffray.com/a-gentle-ish-introduction-to-worst...
ANDcủa join sẽ thànhNOR, và Tetris tận dụng điểm nàyCận worst-case không chặt hơn so với WCOJ không trạng thái/streaming, nhưng dữ liệu thực tế thường có chứng chỉ hộp nhỏ hơn nhiều
Tôi chưa thấy liệu dovetail join có hỗ trợ truy vấn đệ quy, tức datalog tùy ý nơi chỉ định quan hệ đầu ra còn các quan hệ trung gian để engine tự xử lý, hay không
Tôi tò mò không biết nó có hỗ trợ các truy vấn như vậy không
Cần có nhiều bài như thế này hơn để cho lập trình viên ở tầng ứng dụng thấy được sự tinh tế của mô hình quan hệ
Phần giải thích và khai phá từ góc nhìn lập trình hàm cũng ngắn gọn và thuyết phục
Có vẻ đã bỏ lỡ thêm một cơ hội để dạy về vấn đề N+1
Join vào chỉ mục không được phân cụm vẫn là N+1, chỉ là N+1 trên đĩa chứ không phải N+1 đi qua lại giữa mạng và đĩa
Inner join là tích Descartes kèm điều kiện
Inner join với điều kiện equi-join thì sinh trực tiếp điều kiện, còn điều kiện non-equi join thì cần đánh giá thực sự
Giải thích hay. Câu “cách đúng là chuẩn hóa các bảng” thì đúng với cơ sở dữ liệu giao dịch, nhưng trong kho dữ liệu, một mức độ phi chuẩn hóa nào đó được chấp nhận khá rộng rãi
Ví dụ về chuẩn hóa làm tôi nhớ đến thời trước từng thiết kế bảng với suy nghĩ khóa chính số sẽ nhanh hơn chuỗi
Rồi như vậy sinh ra các
idvô nghĩa, và để lấy được giá trị duy nhất thực sự mình muốn thì lại phải joinĐến một ngày tôi nhận ra rằng nếu dùng cùng một khóa duy nhất ở hai bảng thì có thể giảm số lần join, và dù đơn giản nhưng rất hiệu quả
idduy nhất cho mỗi bảng. Nó hữu ích cho việc ghi log và không phải bận tâm đến khóa “thật” gồm nhiều trườngThay vào đó, tôi đặt chỉ mục duy nhất trên các giá trị chuỗi, và quan trọng hơn là áp các ràng buộc toàn vẹn lên đó
Một bảng đầy các chuỗi có ý nghĩa dễ đọc hơn nhiều so với một bảng đầy
idsố hoặc UUID