關系代數、SQL語句和Go語言示例
近些年,數據庫領域發展日新月異,除傳統的關系型數據庫外,還出現了許多新型的數據庫,比如:以HBase、Cassandra、MongoDB為代表的NoSQL數據庫,以InfluxDB、TDEngine為代表的時序數據庫,以Neo4J、Dgraph為代表的圖數據庫,以Redis、Memcached等為代表的內存數據庫,以Milvus為代表的向量數據庫,以CockroachDB、TiDB為代表的HTAP融合數據庫以及云原生數據庫等。各類型數據庫都有自己的優勢,開發者可以根據應用場景選擇最合適的數據庫。
不過,關系型數據庫依舊是當今最流行的數據庫管理系統,廣泛應用于企業應用,也是大多數數應用開發人員日常接觸最多的一種數據庫類型。關系型數據庫通過關系模型和關系代數的理論基礎,實現了對關系數據的高效組織和操作。但許多開發人員在使用SQL進行數據庫開發時,往往感到關系代數晦澀難懂,對SQL語句的語義理解不透徹,這給數據庫應用開發帶來了困難。
在這篇文章中,我們就來研究一下關系模型和關系代數,探究其與SQL語句的對應關系,并用Go語言代碼示例實現相關查詢,期望能幫助讀者增進對關系數據庫的理解,減輕數據庫開發痛點,提高數據庫應用能力。
1. 關系模型(Relational Model)
20世紀70年代,IBM研究員E.F. Codd在“A Relational Model of Data for Large Shared Data Banks”這篇論文中提出了關系模型的概念。隨后,E.F.Codd又陸續發表了多篇文章,用數學理論奠定了關系數據庫的基礎,為關系數據庫建立了一個數據模型 —— 關系數據模型。
關系模型基于謂詞邏輯和集合論,有嚴格的數學基礎,提供了高級別的數據抽象層次,并不規定數據存取的具體過程,而是交由DBMS(數據庫管理系統)自己實現。
關系模型之所以成為DBMS領域的主流模型,正是由于其非常簡單(相較于更早的網絡模型(network model)和層次模型(hierarchical model)),下面是關系模型中定義的一些概念:
- 關系(Relation)
E.F.Codd的論文對關系(Relation)的定義是這樣的:“這里的關系是指公認的數學意義上的關系。給定集合S1, S2, ... ,Sn(不一定互不相關),如果 R是由n元組(n-tuples)組成的集合,其中每個元組的第一個元素來自S1,第二個元素來自S2,以此類推,那么R就是這n個集合(S1~Sn)上的一個關系”。
不用你說,我也知道這段文字太過抽象!下面我盡力用一個圖來呈現一下Relation的含義:

我們看到,關系(Relation)是一個集合,實質上是一個“二維表格結構”,把上圖中不屬于R中的元組去掉,看起來可能更清晰一些:

這個結構中的每一行就是1個n元組(n-tuples),列則是S1到Sn,一共n個列。n元組中的數據依次分別來自S1、S2、...Sn。
- 元組(Tuple)
關系(Relation)這個“二維表格結構”中的每一個n元組,即每一行,被稱作元組(Tuple)。
- 屬性(Attribute)
關系(Relation)這個“二維表格結構”中的每一列(Sn)被稱作一個屬性(Attribute)。
- 域(Domain)
屬性可能取值的范圍被稱為該屬性的域,以圖中屬性S3為例,S3-e1、S3-e2一直到S3-ek都在該屬性的域中,顯然{S3-e1, S3-e2, ..., S3-ek}這個集合是屬性S3的域的一個子集。有個特殊的值null是所有域的一個成員,它一般表示值為"unknown"。
論文在定義關系模型時,還定義了一些模型的額外特征,比如:
- 元組的順序是不重要的;
- 所有的元組(行)是不同的;
- ... ...
有了關系模型的定義,接下來就可以在模型基礎上定義以關系操作對象的運算了,這種運算的集合就構成了關系代數。
2. 關系代數(Relational Algebra)
關系代數由一系列操作組成,這些操作將一個或兩個關系作為輸入,并產生一個新的關系作為結果。概括來說就是關系代數的運算通過輸入有限數量的關系進行運算,運算結果仍為關系。
關系代數定義了一些基本關系運算和擴展關系運算,其中基本關系運算包括:
- 選擇(Selection)
- 投影(Projection)
- 笛卡兒積(Cartesian Product)
- 連接(Join)
- 除(Division)
- 關系并(Union)
- 關系差(Difference)
擴展運算包括:
- 關系交(Intersection)
- 重命名(Rename)
- ... ...
注:關于關系代數的基本關系運算與擴展關系運算的定義在不同書籍里或資料里有所不同。比如在《數據庫查詢優化器的藝術》一書中,作者認為:關系代數(Relational Algebra)是在集合代數基礎上發展起來的,其數據的操作可分為傳統的集合運算和專門的關系運算兩類。傳統的集合運算包括并(Union)、差(Difference)、交(Intersection)和笛卡兒積(Cartesion Product),專門的關系運算包括選擇(Select)、投影(Project)、連接(Join)和除(Division)。關系代數中五個基本的操作并(Union)、差(Difference)、笛卡兒積(Cartesion Product)、選擇(Select)和投影(Project)組成了關系代數完備的操作集。
關系代數中的一些操作(如選擇、投影和重命名操作)被稱為一元操作(unary operation),因為它們只對一個關系進行操作。其他操作,如關系并、笛卡爾積和關系差,則是對一對關系進行操作,因此稱為二元操作(binary operation):

到這里,我們知道了關系模型的概念定義以及基于關系的代數運算都有哪些。那么關系模型、代數運算與我們日常的關系數據庫以及我們使用的SQL語句的對應關系是什么呢?接下來我們就逐一說明一下。
3. 關系模型與關系數據庫實現的對應關系
講到這里,其實大家心里或多或少都有個數了,關系模型與關系數據庫實現中概念的對應關系十分明顯:
- 關系型數據庫中的表(table)對應關系模型中的關系(relation);
- 關系型數據庫中的表的記錄行(row)對應關系模型中的元組(triple);
- 關系型數據庫中的表的列(column)對應關系模型中的屬性(attribute);
- 關系型數據庫中的表的列數據類型(column type)對應關系模型中的屬性的域(domain)。
當然關系型數據庫與關系模型還有一些對應關系不是本文重點,比如:
- 關系模型中的關系完整性約束(如實體完整性、參照完整性等)對應于關系數據庫中的約束(如主鍵約束、外鍵約束等)。
- 關系模型中的范式理論(如第一范式、第二范式等)對應于關系數據庫中的數據規范化過程。
我們下面要關注的一個最重要的對應就是關系模型中的關系代數運算對應于關系數據庫中的查詢操作,我們可以使用SQL語句來實現關系模型中的運算,這也是下面我們要重點說明的內容,通過了解SQL語句背后實現的關系代數運算的本質,將可以幫助我們更好地理解關系模型,對后續數據庫設計以及數據操作的高效性都大有裨益。
4. 關系代數與SQL的對應關系
終于來到最重要的內容了,其實就是通過SQL如何實現關系代數的操作,這也是作為應用開發人員最最關心的內容。
4.1 預先定義的關系
為了便于后續的說明,這里我們預先定義一些關系(表),它們將用在后續說明各個關系運算符的示例中,這些表見下圖:

這里包含一個學生表(Students)、一個課程清單表(Courses)以及兩年年度的選課表:CourseSelection2022和CourseSelection2023(注:這里不討論表設計的合理性)。
文中使用sqlite做為數據庫管理系統(DBMS)的代表,主要是為了簡單,SQL標準的兼容性也不錯。下面的Go代碼用于創建上圖中的表并插入樣例數據:
// relational-algebra-examples/create_database/main.go
package main
import (
"database/sql"
"fmt"
_ "modernc.org/sqlite"
)
func createTable(db *sql.DB, sqlStmt string) error {
stmt, err := db.Prepare(sqlStmt)
if err != nil {
fmt.Println("prepare statement error:", err)
return err
}
_, err = stmt.Exec()
if err != nil {
fmt.Println("exec prepared statement error:", err)
return err
}
return nil
}
func createTables(db *sql.DB) error {
// 創建Students表
err := createTable(db, `CREATE TABLE IF NOT EXISTS Students (
Sno INTEGER PRIMARY KEY,
Sname TEXT,
Gender TEXT,
Age INTEGER
)`)
if err != nil {
fmt.Println("create table Students error:", err)
return err
}
// 創建Courses表
err = createTable(db, `CREATE TABLE IF NOT EXISTS Courses (
Cno INTEGER PRIMARY KEY,
Cname TEXT,
Credit INTEGER
)`)
if err != nil {
fmt.Println("create table Courses error:", err)
return err
}
// 2022選課表
err = createTable(db, `CREATE TABLE CourseSelection2022 (
Sno INTEGER,
Cno INTEGER,
Score INTEGER,
PRIMARY KEY (Sno, Cno),
FOREIGN KEY (Sno) REFERENCES Students(Sno),
FOREIGN KEY (Cno) REFERENCES Courses(Cno)
)`)
if err != nil {
fmt.Println("create table CourseSelection2022 error:", err)
return err
}
// 2023選課表
err = createTable(db, `CREATE TABLE CourseSelection2023 (
Sno INTEGER,
Cno INTEGER,
Score INTEGER,
PRIMARY KEY (Sno, Cno),
FOREIGN KEY (Sno) REFERENCES Students(Sno),
FOREIGN KEY (Cno) REFERENCES Courses(Cno)
)`)
if err != nil {
fmt.Println("create table CourseSelection2023 error:", err)
return err
}
return nil
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
func insertData(db *sql.DB) {
// 向Students表插入數據
stmt, err := db.Prepare("INSERT INTO Students VALUES (?, ?, ?, ?)")
checkErr(err)
_, err = stmt.Exec(1001, "張三", "M", 20)
checkErr(err)
_, err = stmt.Exec(1002, "李四", "F", 18)
checkErr(err)
_, err = stmt.Exec(1003, "王五", "M", 19)
checkErr(err)
// 向Courses表插入數據
stmt, err = db.Prepare("INSERT INTO Courses VALUES (?, ?, ?)")
checkErr(err)
_, err = stmt.Exec(1, "數據庫", 4)
checkErr(err)
_, err = stmt.Exec(2, "數學", 2)
checkErr(err)
_, err = stmt.Exec(3, "英語", 3)
checkErr(err)
// 插入2022選課數據
stmt, _ = db.Prepare("INSERT INTO CourseSelection2022 VALUES (?, ?, ?)")
_, err = stmt.Exec(1001, 1, 85)
checkErr(err)
_, err = stmt.Exec(1001, 2, 80)
checkErr(err)
_, err = stmt.Exec(1002, 1, 83)
checkErr(err)
_, err = stmt.Exec(1003, 1, 76)
checkErr(err)
// ...
// 插入2023選課數據
stmt, _ = db.Prepare("INSERT INTO CourseSelection2023 VALUES (?, ?, ?)")
stmt.Exec(1001, 3, 75)
checkErr(err)
stmt.Exec(1002, 2, 81)
checkErr(err)
stmt.Exec(1003, 3, 86)
checkErr(err)
}
func main() {
db, err := sql.Open("sqlite", "../test.db")
defer db.Close()
if err != nil {
fmt.Println("open test.db error:", err)
return
}
err = createTables(db)
if err != nil {
fmt.Println("create table error:", err)
return
}
insertData(db)
}這里我們使用了cznic大神[3]實現并開源的modernc.org/sqlite,這是一個純Go的sqlite3數據庫driver。Go社區另一個廣泛使用的sqlite3的driver庫為go-sqlite3,只不過go-sqlite3是使用cgo對sqlite3 C庫的封裝。
執行上面go代碼,便可以建立一個名為test.db的sqlite數據庫,我們通過sqlite官方的命令行工具(cli)也可以與該數據庫文件交互(這里我們使用的是容器版cli),比如:
$docker pull nouchka/sqlite3
// cd到test.db文件路徑下
$docker run -v {test.db文件所在目錄的絕對路徑}:/root/db -it nouchka/sqlite3
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open ./test.db
sqlite> .databases
main: /root/db/test.db r/w
sqlite> .tables
CourseSelection2022 Courses
CourseSelection2023 Students
sqlite>接下來,我們就先從關系代數運算中最容易理解的一元運算符開始說起。
4.2. 選擇(Selection)
“選擇”是一元關系運算,它的運算符為σ,語義如下:
R' = σ[p](R "p") = {t | t∈R ∩ p(t) = true } // 這里用[p]表示數學符號的下標其中R為關系,t為元組,p是謂詞(predicate)表達式的組合,可以由一個或多個謂詞表達式構成。
這個語義相對好理解一些:它對R的操作結果依然是關系R',即一個新元組集合,這個元組集合中的元組來自R,但必須滿足p(t) = true的條件。說直白一些,就是選擇滿足給定條件的元組。下面是一個“選擇”操作的示意圖:

我們可以用下面最常見的SQL語句實現對單一關系(表)的選擇運算:
SELECT * FROM R WHERE p(t) = true;對應Go示例的代碼片段如下:
// relational-algebra-examples/query/main.go
func doSelection(db *sql.DB) {
rows, _ := db.Query("SELECT * FROM CourseSelection2022 where score >= 80") // p(t)為score >= 80
var selections []CourseSelection
for rows.Next() {
var s CourseSelection
rows.Scan(&s.Sno, &s.Cno, &s.Score)
selections = append(selections, s)
}
fmt.Println(selections)
}輸出結果為:
[{1001 1 85} {1001 2 80} {1002 1 83}]4.3 投影(Projection)
“投影”也是一元關系運算,它的運算符為∏,語義如下:
R' = ∏[A1,A2,...,An](R "A1,A2,...,An") = {t[A1,A2,...,An]| t∈R } // 這里A1,A2,...,An表示從R中取出的列名顯然和“選擇”通過謂詞表達式選元組不同,“投影”選擇一個關系中的指定列(A1,A2,...,An),即選擇需要的屬性。下面是其運算過程的示意圖:

“投影”對應的SQL語句也是我們最熟悉的語句:
SELECT A1, A2, ..., An FROM R;對應Go示例的代碼片段如下:
// relational-algebra-examples/query/main.go
func doProjection(db *sql.DB) {
rows, _ := db.Query("SELECT Sno, Sname FROM Students") // A1 = Sno, A2 = Sname
var students []Student
for rows.Next() {
var s Student
rows.Scan(&s.Sno, &s.Sname)
students = append(students, s)
}
fmt.Println(students)
}輸出結果為:
[{1001 張三 0} {1002 李四 0} {1003 王五 0}]不過要注意的是:取消某些關系列后可能出現重復行,違反了關系的定義(關系是一個元組的集合),因此必須檢查并去除結果關系中重復的元組。
4.4 運算符的組合(Composition)
關系運算的輸入是關系,結果也是一個關系,因此我們可以將關系運算符組合成一個更復雜的關系運算符表達式來實現更復雜的運算。比如將上面的兩個一元關系運算符組合在一起“先選元組,再選屬性”:
R' = ∏[A1,A2,...,An](σ[p](R "A1,A2,...,An"))其運算過程如下圖所示:

上述運算符組合對應的SQL語句如下:
SELECT A1, A2, ..., An FROM R where p(t) = true;對應Go示例的代碼片段如下:
// relational-algebra-examples/query/main.go
func doCompositionOperation(db *sql.DB) {
rows, _ := db.Query("SELECT Sno, Sname FROM Students where age >= 20")
var students []Student
for rows.Next() {
var s Student
rows.Scan(&s.Sno, &s.Sname)
students = append(students, s)
}
fmt.Println(students)
}輸出結果為:
[{1001 張三 0}]無論是選擇運算還是投影運算,亦或是組合之后的運算,理解起來都相對容易,因為只涉及一個“關系”。接下來我們就看一下涉及兩個關系的二元運算符,我們先來看看集合運算。
4.5 關系交(Intersection)
如果沒有記錯,我們是在高中學習的集合代數。那時定義兩個集合的交集運算是這樣的:
對于集合A和B,其交運算(Intersction)為:
A ∩ B = { x | x ∈ A且 x ∈ B}用一個一維空間的數的集合的例子來說,就是當A = {1, 2, 3, 4, 5},B = { 3, 5, 6, 9}時,A ∩ B = {3, 5}。我們通常用維恩圖來示意集合運算:

在關系模型中,元組是一維集合,關系是元組的集合,即是一個二維集合,那么基于關系的交運算就要有一個前提:那就是參與運算的兩個關系的屬性必須是兼容的。
兩個關系的屬性兼容需滿足以下條件:
- 屬性數量相同
兩個關系中的屬性數量必須相同。
- 屬性類型相同或可轉換
兩個關系中對應位置的屬性類型必須相同或可以通過類型轉換進行兼容。例如,一個關系中的屬性類型是整數,而另一個關系中的屬性類型是浮點數,這種情況下屬性類型是兼容的,因為整數可以隱式轉換為浮點數。
- 屬性名稱可以不同
兩個關系中對應位置的屬性名稱可以不同,只要它們的屬性類型兼容即可。屬性名稱的不同不會影響屬性兼容性。
在關系模型中,兩個關系的屬性兼容性是判斷兩個關系是否可以進行某些操作(包括集合操作)的重要條件之一。
回到集合運算,如果兩個關系的屬性不兼容,則這兩個關系無法進行集合運算,比如Students表和Courses表的屬性個數不同,如果對它們進行關系交運算,會導致報錯:
SELECT * FROM Students INTERSECT SELECT * FROM Courses;
Parse error: SELECTs to the left and right of INTERSECT do not have the same number of result columns介紹完集合運算的前提后,我們再來看關系交運算,其語義入下:
R' = R1 ∩ R2即兩個關系R1和R2在屬性兼容的前提下進行關系交運算的結果為返回兩個關系中相同的元組。
關系交運算對應的SQL語句如下:
SELECT * FROM R1 INTERSECT SELECT * FROM R2;對應Go示例的代碼片段如下:
// relational-algebra-examples/query/main.go
func doIntersection(db *sql.DB) {
rows, _ := db.Query("SELECT * FROM CourseSelection2022 INTERSECT SELECT * FROM CourseSelection2023")
var selections []CourseSelection
for rows.Next() {
var s CourseSelection
rows.Scan(&s.Sno, &s.Cno, &s.Score)
selections = append(selections, s)
}
fmt.Println(selections)
}由于CourseSelection2022和CourseSelection2023這兩個關系沒有相同元組,所以上述Go程序輸出的結果為空。
4.6 關系并(Union)
和關系交一樣,兩個關系進行關系并運算的前提也是屬性兼容。關系并運算的語義如下:
R' = R1 ∪ R2即兩個關系R1和R2在屬性兼容的前提下進行關系并運算的結果為返回兩個關系中的所有元組,但要去除重復元組。
關系并對應的SQL語句如下:
SELECT * FROM R1 UNION SELECT * FROM R2;對應Go示例的代碼片段如下:
// relational-algebra-examples/query/main.go
func doUnion(db *sql.DB) {
rows, _ := db.Query("SELECT * FROM CourseSelection2022 UNION SELECT * FROM CourseSelection2023")
var selections []CourseSelection
for rows.Next() {
var s CourseSelection
rows.Scan(&s.Sno, &s.Cno, &s.Score)
selections = append(selections, s)
}
fmt.Println(selections)
}CourseSelection2022和CourseSelection2023這兩個關系沒有重復元組,所有關系并運算后得到的結果關系中包含了這兩個關系的全部元組,上述程序的輸出結果為:
[{1001 1 85} {1001 2 80} {1001 3 75} {1002 1 83} {1002 2 81} {1003 1 76} {1003 3 86}]4.7 關系差(Difference)
在集合代數中,對于集合A和B,其差運算為:
A - B = { x | x ∈ A且 x ? B}即從A集合中排除掉B集合中的元素。
在關系模型中,關系差運算即是從一個關系中排除另一個關系中的元組,其語義如下:
R' = R1-R2={t|t∈R1 ∩ t?R2} // t為關系中的元組在SQL中,我們可以用NOT IN實現:
SELECT * FROM R1 WHERE A1 NOT IN (SELECT A1 FROM R2 WHERE 條件)下面是對應的Go語言代碼片段:
// relational-algebra-examples/query/main.go
func doDifference(db *sql.DB) {
rows, _ := db.Query("SELECT * FROM CourseSelection2022 WHERE Cno NOT IN (SELECT Cno FROM CourseSelection2023)")
var selections []CourseSelection
for rows.Next() {
var s CourseSelection
rows.Scan(&s.Sno, &s.Cno, &s.Score)
selections = append(selections, s)
}
fmt.Println(selections)
}這段示例的含義是選出CourseSelection2022的元組,但去掉Cno值在CourseSelection2023出現過的元組。下面是運行結果:
[{1001 1 85} {1002 1 83} {1003 1 76}]注意:關系差運算的前提也是兩個關系的屬性兼容。
最后看看略復雜的二元運算符:笛卡爾積和連接。
4.8 笛卡爾積(Cartesian-product)
在關系代數中,關系積,即笛卡爾積(Cartesian Product)這種運算(也被稱為關系叉乘)用于取兩個關系的所有可能的組合。它的數學語義可以描述為:給定關系R1和R2,它們的笛卡爾積結果是一個新的關系,其中的元組由R1中的每個元組與R2中的每個元組的組合構成。
在SQL中,笛卡爾積可以通過使用CROSS JOIN關鍵字來實現:
SELECT * FROM R1 CROSS JOIN R2;也可以通過下面SQL語句來實現:
SELECT R1.*, R1.* FROM R1, R2;對應的Go代碼片段如下:
// relational-algebra-examples/query/main.go
// StudentCourse結果
type StudentCourse struct {
Sno int
Sname string
Gender string
Age int
Cno int
Cname string
Credit int
}
func doCartesianProduct(db *sql.DB) {
rows, _ := db.Query("SELECT * FROM Students CROSS JOIN Courses")
// rows, _ := db.Query("SELECT Students.*, Courses.* FROM Students, Courses")
var selections []StudentCourse
for rows.Next() {
var s StudentCourse
rows.Scan(&s.Sno, &s.Sname, &s.Gender, &s.Age, &s.Cno, &s.Cname, &s.Credit)
selections = append(selections, s)
}
fmt.Println(len(selections))
fmt.Println(selections)
}示例的運行結果如下:
9
[{1001 張三 M 20 1 數據庫 4} {1001 張三 M 20 2 數學 2} {1001 張三 M 20 3 英語 3} {1002 李四 F 18 1 數據庫 4} {1002 李四 F 18 2 數學 2} {1002 李四 F 18 3 英語 3} {1003 王五 M 19 1 數據庫 4} {1003 王五 M 19 2 數學 2} {1003 王五 M 19 3 英語 3}]我們看到對Students和Courses兩個關系(表)進行笛卡爾積運算后,結果包含了Students中的每個元組與Courses中的每個元組進行組合的結果(3x3=9個)。
需要注意的是,由于笛卡爾積可能導致非常大的結果集,因此在實際使用中應謹慎使用,并且通常需要與其他運算符和條件結合使用,以限制結果的大小和提高查詢效率。通常我們會用連接來達到這些目的。
4.9 連接(Join)
連接(Join)運算(?)是從兩個關系的笛卡兒積中選取屬性間滿足一定條件的元組形成一個新的關系,即將笛卡爾積和選擇(selection)運算合并達到一個操作中。從這個角度來看,笛卡爾積可以視為一種無條件的連接。
連接代數運算符是關系代數中很有用的關系代數運算符,也是日常經常使用的運算符,它有很多種不同的子類別,下面我們分別看看各種子類型的語義、SQL語句以及對應的Go代碼示例。
4.9.1 等值連接(Equijoin)
等值連接是通過比較兩個關系(表)之間的屬性值是否相等來進行連接的操作。連接條件使用等號(=)來比較屬性值的相等性。
我們直接看Go示例:
// relational-algebra-examples/query/main.go
func dumpOperationResult(operation string, rows *sql.Rows) {
cols, _ := rows.Columns()
w := tabwriter.NewWriter(os.Stdout, 0, 2, 1, ' ', 0)
defer w.Flush()
w.Write([]byte(strings.Join(cols, "\t")))
w.Write([]byte("\n"))
row := make([][]byte, len(cols))
rowPtr := make([]any, len(cols))
for i := range row {
rowPtr[i] = &row[i]
}
fmt.Printf("\n%s operation:\n", operation)
for rows.Next() {
rows.Scan(rowPtr...)
w.Write(bytes.Join(row, []byte("\t")))
w.Write([]byte("\n"))
}
}
func doEquijoin(db *sql.DB) {
rows, _ := db.Query("SELECT * FROM CourseSelection2022 JOIN Students ON CourseSelection2022.Sno = Students.Sno")
dumpOperationResult("Equijoin", rows)
}這個示例使用等值連接將CourseSelection2022表和Students表連接起來,連接條件是CourseSelection2022.Sno = Students.Sno,即學生編號相等,返回的結果將包含CourseSelection2022和Students兩個表中滿足連接條件的元組。
我們看看程序運行的輸出結果:
Equijoin operation:
Sno Cno Score Sno Sname Gender Age
1001 1 85 1001 張三 M 20
1001 2 80 1001 張三 M 20
1002 1 83 1002 李四 F 18
1003 1 76 1003 王五 M 19在這個結果中,我們看到一個“奇怪”的情況,那就是出現了兩個Sno屬性。在等值連接中,如果連接的兩個表中存在相同名稱的屬性(例如這里兩個表中都有名為"Sno"的屬性),那么在連接結果中會出現兩個相同名稱的屬性。
這是因為等值連接會將兩個表中具有相同連接條件的屬性進行匹配,并將匹配成功的元組進行組合。由于兩個表中都有名為"Sno"的屬性,因此連接結果中會保留這兩個屬性,以顯示連接操作前后的對應關系。
為了區分來自不同表的相同屬性名,通常在連接結果中會使用表別名或表名作為前綴,以區分它們的來源。這樣可以確保結果中的屬性名稱是唯一的,避免歧義。 例如,如果在等值連接中連接了名為"CourseSelection2022"的表和名為"Students"的表,并且兩個表中都有名為"Sno"的屬性,那么連接結果中可能會出現類似于"CourseSelection2022.Sno"和"Students.Sno"的屬性名稱,以明確它們的來源。
需要注意的是,數據庫管理系統的具體實現和查詢工具的設置可能會影響連接結果中屬性的顯示方式,但通常會采用類似的方式來區分相同屬性名的來源。
4.9.2 自然連接(Natural Join)
自然連接是基于兩個表中具有相同屬性名的屬性進行連接的操作,重點在于它會自動匹配具有相同屬性名的屬性,并根據這些屬性的相等性進行連接,而無需手工指定。
我們來看自然連接的Go示例:
// relational-algebra-examples/query/main.go
func doNaturaljoin(db *sql.DB) {
rows, _ := db.Query("SELECT * FROM CourseSelection2022 NATURAL JOIN Students")
dumpOperationResult("Naturaljoin", rows)
}這個示例使用自然連接將CourseSelection2022表和Students表連接起來,自然連接會自動基于兩個表中所有具有相同屬性名的屬性進行連接,返回的結果將包含CourseSelection2022和Students兩個表中所有滿足連接條件的元組,并自動消除重復屬性,這是與等值連接的一個明顯的區別。
我們看看程序運行的輸出結果:
Naturaljoin operation:
Sno Cno Score Sname Gender Age
1001 1 85 張三 M 20
1001 2 80 張三 M 20
1002 1 83 李四 F 18
1003 1 76 王五 M 19如果兩個表(比如R1和R2)有一個以上的屬性名相同,比如2個(比如:A1和A2),那就會自動針對這兩個屬性名(一起)在兩個表中進行等值連接:只有R2.A1 = R1.A1且R2.A2 = R1.A2時,才將元組連接并放入結果關系中。
4.9.3 θ連接(Theta Join)
θ連接是一種通用的連接操作,它使用比等號更一般化的連接條件進行連接。連接條件可以使用除了等號之外的比較運算符(如大于、小于、不等于等)來比較兩個表之間的屬性。
我們來看θ連接的Go示例:
// relational-algebra-examples/query/main.go
func doThetajoin(db *sql.DB) {
rows, _ := db.Query(`SELECT *
FROM CourseSelection2022
JOIN Students ON CourseSelection2022.Sno > Students.Sno`)
dumpOperationResult("Thetajoin", rows)
}這個示例使用Join將CourseSelection2022表和Students表連接起來,連接條件是CourseSelection2022.Sno > Students.Sno,即學生編號大于學生表中的學生編號,返回的結果將包含CourseSelection2022和`Students兩個表中滿足連接條件的元組。
Thetajoin operation:
Sno Cno Score Sno Sname Gender Age
1002 1 83 1001 張三 M 20
1003 1 76 1001 張三 M 20
1003 1 76 1002 李四 F 18這個結果的生成過程大致如下:
- 先看CourseSelection2022表的第一個元組,其Sno為1001,該Sno不大于Students表中的任一個Sno;
- 再看CourseSelection2022表的第二個元組,其Sno為1002,該Sno僅大于Students表中的Sno為1001的那一個元組,于是將CourseSelection2022表的第二個元組和Students表中第一個元組連接起來作為結果表中的第一個元組;
- 最后看CourseSelection2022表的第三個元組,其Sno為1003,該Sno大于Students表中的Sno為1001和1002的元組,于是將CourseSelection2022表的第三個元組分別和Students表中第一個和第二個元組連接起來作為結果表中的第二個和第三個元組。
4.9.4 半連接(Semi Join)
半連接是一種特殊的連接操作,它返回滿足連接條件的左側關系中的元組,并且只返回右側關系中與之匹配的屬性。半連接通常用于判斷兩個關系中是否存在匹配的元組,而不需要返回右側關系的詳細信息。
我們來看半連接的Go示例:
// relational-algebra-examples/query/main.go
func doSemijoin(db *sql.DB) {
rows, _ := db.Query(`SELECT *
FROM Students
WHERE EXISTS (
SELECT *
FROM CourseSelection2022
WHERE Students.Sno = CourseSelection2022.Sno
)`)
dumpOperationResult("Semijoin", rows)
}這個示例使用半連接操作,以Students表為左側關系,CourseSelection2022表為右側關系。它使用子查詢來判斷左側關系中是否存在滿足連接條件的元組,即Students.Sno = CourseSelection2022.Sno。它返回的結果將只包含滿足連接條件的Students表中的元組。
下面是程序輸出的結果:
Semijoin operation:
Sno Sname Gender Age
1001 張三 M 20
1002 李四 F 18
1003 王五 M 19半連接返回的結果關系中只包含左關系中的行,其中每一行只返回一次,即使在右關系中有多個匹配項。
4.9.5 反連接(Anti Join)
反連接是半連接的補集操作,它返回左側關系中不存在滿足連接條件的元組。反連接通常用于查找在左側關系中存在而在右側關系中不存在的元組。
我們來看反連接的Go示例:
// relational-algebra-examples/query/main.go
func doAntijoin(db *sql.DB) {
rows, _ := db.Query(`SELECT *
FROM Students
WHERE NOT EXISTS (
SELECT *
FROM CourseSelection2022
WHERE Students.Sno = CourseSelection2022.Sno
)`)
dumpOperationResult("Antijoin", rows)
}這個示例使用反連接操作,以Students表為左側關系,CourseSelection2022表為右側關系,并使用NOT EXISTS子查詢來判斷左側關系中不存在滿足連接條件的元組,即Students.Sno = CourseSelection2022.Sno。返回的結果將只包含左側關系Students表中不存在連接條件的元組。
Antijoin operation:
Sno Sname Gender Age我們看到輸出的元組集合為空。
4.9.6 左(外)連接(Left Outer Join)
左外連接是將左側關系中的所有元組與滿足連接條件的右側關系中的元組進行連接,并返回所有左側關系的元組。如果右側關系中沒有與左側關系匹配的元組,對應的屬性值將為NULL。
我們來看左(外)連接的Go示例:
// relational-algebra-examples/query/main.go
func doLeftjoin(db *sql.DB) {
rows, _ := db.Query(`SELECT *
FROM Students
LEFT JOIN CourseSelection2022 ON Students.Sno = CourseSelection2022.Sno`)
dumpOperationResult("Leftjoin", rows)
}這個示例使用左外連接將Students表和CourseSelection2022表連接起來,其連接條件是Students.Sno = CourseSelection2022.Sno,即學生編號相等。示例的返回結果將包含Students表中的所有元組,并將滿足連接條件的CourseSelection2022表中的元組加入結果中。如果沒有匹配的元組,右側關系中的屬性值將為NULL。 ` 下面是程序輸出的結果:
Leftjoin operation:
Sno Sname Gender Age Sno Cno Score
1001 張三 M 20 1001 1 85
1001 張三 M 20 1001 2 80
1002 李四 F 18 1002 1 83
1003 王五 M 19 1003 1 764.9.7 右(外)連接(Right Outer Join)
右外連接是將右側關系中的所有元組與滿足連接條件的左側關系中的元組進行連接,并返回所有右側關系的元組。如果左側關系中沒有與右側關系匹配的元組,對應的屬性值將為NULL。
我們來看右(外)連接的Go示例:
// relational-algebra-examples/query/main.go
func doRightjoin(db *sql.DB) {
rows, _ := db.Query(`SELECT *
FROM Students
RIGHT JOIN CourseSelection2022 ON Students.Sno = CourseSelection2022.Sno`)
dumpOperationResult("Rightjoin", rows)
}這個示例使用右外連接將Students表和CourseSelection2022表連接起來,它的連接條件是Students.Sno = CourseSelection2022.Sno,即學生編號相等。返回的結果將包含CourseSelection2022表中的所有元組,并將滿足連接條件的Students表中的元組加入結果中。如果沒有匹配的元組,左側關系中的屬性值將為NULL。
下面是程序輸出的結果:
Rightjoin operation:
Sno Sname Gender Age Sno Cno Score
1001 張三 M 20 1001 1 85
1001 張三 M 20 1001 2 80
1002 李四 F 18 1002 1 83
1003 王五 M 19 1003 1 764.9.8 全連接(Full Outer Join)
全連接是將左側關系和右側關系中的所有元組進行連接,并返回所有滿足連接條件的元組。如果左側關系或右側關系中沒有與對方匹配的元組,對應的屬性值將為NULL。
我們來看全連接的Go示例:
// relational-algebra-examples/query/main.go
func doFulljoin(db *sql.DB) {
rows, _ := db.Query(`SELECT *
FROM Students
FULL JOIN CourseSelection2022 ON Students.Sno = CourseSelection2022.Sno`)
dumpOperationResult("Fulljoin", rows)
}這個示例使用全連接將Students表和CourseSelection2022表連接起來,連接條件是Students.Sno = CourseSelection2022.Sno,即學生編號相等。示例返回的結果將包含Students表和CourseSelection2022表中的所有元組,并將滿足連接條件的元組進行組合。如果沒有匹配的元組,對應關系中的屬性值將為NULL。
下面是程序輸出的結果:
Fulljoin operation:
Sno Sname Gender Age Sno Cno Score
1001 張三 M 20 1001 1 85
1001 張三 M 20 1001 2 80
1002 李四 F 18 1002 1 83
1003 王五 M 19 1003 1 76以上就是本文要介紹的連接類型,這些連接類型提供了在關系數據庫中操作和組合表數據的靈活性,可以根據特定的需求選擇合適的連接方式來獲取所需的結果。
5. 小結
本文系統地介紹和講解了關系數據庫中的關系代數運算,包括選擇、投影、連接、交、并、積等,以及關系代數的SQL實現,并給出了Go語言示例。
關系模型是關系數據庫的理論基礎,關系代數通過對關系的運算來表達查詢,因此關系代數也構成了SQL查詢語言的理論基礎。理解關系代數與SQL的對應關系,可以更好地使用SQL語言操作關系型數據庫。
本文算是關系數據庫的入門文章,既能讓數據庫初學者快速掌握關系代數,也能讓有基礎的讀者回顧并深入理解概念內涵。通過閱讀學習,能幫助讀者把關系代數運用到實際數據庫應用中,解決查詢優化等問題。

























