關(guān)系代數(shù)、SQL語(yǔ)句和Go語(yǔ)言示例
近些年,數(shù)據(jù)庫(kù)領(lǐng)域發(fā)展日新月異,除傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)外,還出現(xiàn)了許多新型的數(shù)據(jù)庫(kù),比如:以HBase、Cassandra、MongoDB為代表的NoSQL數(shù)據(jù)庫(kù),以InfluxDB、TDEngine為代表的時(shí)序數(shù)據(jù)庫(kù),以Neo4J、Dgraph為代表的圖數(shù)據(jù)庫(kù),以Redis、Memcached等為代表的內(nèi)存數(shù)據(jù)庫(kù),以Milvus為代表的向量數(shù)據(jù)庫(kù),以CockroachDB、TiDB為代表的HTAP融合數(shù)據(jù)庫(kù)以及云原生數(shù)據(jù)庫(kù)等。各類型數(shù)據(jù)庫(kù)都有自己的優(yōu)勢(shì),開(kāi)發(fā)者可以根據(jù)應(yīng)用場(chǎng)景選擇最合適的數(shù)據(jù)庫(kù)。
不過(guò),關(guān)系型數(shù)據(jù)庫(kù)依舊是當(dāng)今最流行的數(shù)據(jù)庫(kù)管理系統(tǒng),廣泛應(yīng)用于企業(yè)應(yīng)用,也是大多數(shù)數(shù)應(yīng)用開(kāi)發(fā)人員日常接觸最多的一種數(shù)據(jù)庫(kù)類型。關(guān)系型數(shù)據(jù)庫(kù)通過(guò)關(guān)系模型和關(guān)系代數(shù)的理論基礎(chǔ),實(shí)現(xiàn)了對(duì)關(guān)系數(shù)據(jù)的高效組織和操作。但許多開(kāi)發(fā)人員在使用SQL進(jìn)行數(shù)據(jù)庫(kù)開(kāi)發(fā)時(shí),往往感到關(guān)系代數(shù)晦澀難懂,對(duì)SQL語(yǔ)句的語(yǔ)義理解不透徹,這給數(shù)據(jù)庫(kù)應(yīng)用開(kāi)發(fā)帶來(lái)了困難。
在這篇文章中,我們就來(lái)研究一下關(guān)系模型和關(guān)系代數(shù),探究其與SQL語(yǔ)句的對(duì)應(yīng)關(guān)系,并用Go語(yǔ)言代碼示例實(shí)現(xiàn)相關(guān)查詢,期望能幫助讀者增進(jìn)對(duì)關(guān)系數(shù)據(jù)庫(kù)的理解,減輕數(shù)據(jù)庫(kù)開(kāi)發(fā)痛點(diǎn),提高數(shù)據(jù)庫(kù)應(yīng)用能力。
1. 關(guān)系模型(Relational Model)
20世紀(jì)70年代,IBM研究員E.F. Codd在“A Relational Model of Data for Large Shared Data Banks”這篇論文中提出了關(guān)系模型的概念。隨后,E.F.Codd又陸續(xù)發(fā)表了多篇文章,用數(shù)學(xué)理論奠定了關(guān)系數(shù)據(jù)庫(kù)的基礎(chǔ),為關(guān)系數(shù)據(jù)庫(kù)建立了一個(gè)數(shù)據(jù)模型 —— 關(guān)系數(shù)據(jù)模型。
關(guān)系模型基于謂詞邏輯和集合論,有嚴(yán)格的數(shù)學(xué)基礎(chǔ),提供了高級(jí)別的數(shù)據(jù)抽象層次,并不規(guī)定數(shù)據(jù)存取的具體過(guò)程,而是交由DBMS(數(shù)據(jù)庫(kù)管理系統(tǒng))自己實(shí)現(xiàn)。
關(guān)系模型之所以成為DBMS領(lǐng)域的主流模型,正是由于其非常簡(jiǎn)單(相較于更早的網(wǎng)絡(luò)模型(network model)和層次模型(hierarchical model)),下面是關(guān)系模型中定義的一些概念:
- 關(guān)系(Relation)
E.F.Codd的論文對(duì)關(guān)系(Relation)的定義是這樣的:“這里的關(guān)系是指公認(rèn)的數(shù)學(xué)意義上的關(guān)系。給定集合S1, S2, ... ,Sn(不一定互不相關(guān)),如果 R是由n元組(n-tuples)組成的集合,其中每個(gè)元組的第一個(gè)元素來(lái)自S1,第二個(gè)元素來(lái)自S2,以此類推,那么R就是這n個(gè)集合(S1~Sn)上的一個(gè)關(guān)系”。
不用你說(shuō),我也知道這段文字太過(guò)抽象!下面我盡力用一個(gè)圖來(lái)呈現(xiàn)一下Relation的含義:
我們看到,關(guān)系(Relation)是一個(gè)集合,實(shí)質(zhì)上是一個(gè)“二維表格結(jié)構(gòu)”,把上圖中不屬于R中的元組去掉,看起來(lái)可能更清晰一些:
這個(gè)結(jié)構(gòu)中的每一行就是1個(gè)n元組(n-tuples),列則是S1到Sn,一共n個(gè)列。n元組中的數(shù)據(jù)依次分別來(lái)自S1、S2、...Sn。
- 元組(Tuple)
關(guān)系(Relation)這個(gè)“二維表格結(jié)構(gòu)”中的每一個(gè)n元組,即每一行,被稱作元組(Tuple)。
- 屬性(Attribute)
關(guān)系(Relation)這個(gè)“二維表格結(jié)構(gòu)”中的每一列(Sn)被稱作一個(gè)屬性(Attribute)。
- 域(Domain)
屬性可能取值的范圍被稱為該屬性的域,以圖中屬性S3為例,S3-e1、S3-e2一直到S3-ek都在該屬性的域中,顯然{S3-e1, S3-e2, ..., S3-ek}這個(gè)集合是屬性S3的域的一個(gè)子集。有個(gè)特殊的值null是所有域的一個(gè)成員,它一般表示值為"unknown"。
論文在定義關(guān)系模型時(shí),還定義了一些模型的額外特征,比如:
- 元組的順序是不重要的;
- 所有的元組(行)是不同的;
- ... ...
有了關(guān)系模型的定義,接下來(lái)就可以在模型基礎(chǔ)上定義以關(guān)系操作對(duì)象的運(yùn)算了,這種運(yùn)算的集合就構(gòu)成了關(guān)系代數(shù)。
2. 關(guān)系代數(shù)(Relational Algebra)
關(guān)系代數(shù)由一系列操作組成,這些操作將一個(gè)或兩個(gè)關(guān)系作為輸入,并產(chǎn)生一個(gè)新的關(guān)系作為結(jié)果。概括來(lái)說(shuō)就是關(guān)系代數(shù)的運(yùn)算通過(guò)輸入有限數(shù)量的關(guān)系進(jìn)行運(yùn)算,運(yùn)算結(jié)果仍為關(guān)系。
關(guān)系代數(shù)定義了一些基本關(guān)系運(yùn)算和擴(kuò)展關(guān)系運(yùn)算,其中基本關(guān)系運(yùn)算包括:
- 選擇(Selection)
- 投影(Projection)
- 笛卡兒積(Cartesian Product)
- 連接(Join)
- 除(Division)
- 關(guān)系并(Union)
- 關(guān)系差(Difference)
擴(kuò)展運(yùn)算包括:
- 關(guān)系交(Intersection)
- 重命名(Rename)
- ... ...
注:關(guān)于關(guān)系代數(shù)的基本關(guān)系運(yùn)算與擴(kuò)展關(guān)系運(yùn)算的定義在不同書(shū)籍里或資料里有所不同。比如在《數(shù)據(jù)庫(kù)查詢優(yōu)化器的藝術(shù)》一書(shū)中,作者認(rèn)為:關(guān)系代數(shù)(Relational Algebra)是在集合代數(shù)基礎(chǔ)上發(fā)展起來(lái)的,其數(shù)據(jù)的操作可分為傳統(tǒng)的集合運(yùn)算和專門(mén)的關(guān)系運(yùn)算兩類。傳統(tǒng)的集合運(yùn)算包括并(Union)、差(Difference)、交(Intersection)和笛卡兒積(Cartesion Product),專門(mén)的關(guān)系運(yùn)算包括選擇(Select)、投影(Project)、連接(Join)和除(Division)。關(guān)系代數(shù)中五個(gè)基本的操作并(Union)、差(Difference)、笛卡兒積(Cartesion Product)、選擇(Select)和投影(Project)組成了關(guān)系代數(shù)完備的操作集。
關(guān)系代數(shù)中的一些操作(如選擇、投影和重命名操作)被稱為一元操作(unary operation),因?yàn)樗鼈冎粚?duì)一個(gè)關(guān)系進(jìn)行操作。其他操作,如關(guān)系并、笛卡爾積和關(guān)系差,則是對(duì)一對(duì)關(guān)系進(jìn)行操作,因此稱為二元操作(binary operation):
到這里,我們知道了關(guān)系模型的概念定義以及基于關(guān)系的代數(shù)運(yùn)算都有哪些。那么關(guān)系模型、代數(shù)運(yùn)算與我們?nèi)粘5年P(guān)系數(shù)據(jù)庫(kù)以及我們使用的SQL語(yǔ)句的對(duì)應(yīng)關(guān)系是什么呢?接下來(lái)我們就逐一說(shuō)明一下。
3. 關(guān)系模型與關(guān)系數(shù)據(jù)庫(kù)實(shí)現(xiàn)的對(duì)應(yīng)關(guān)系
講到這里,其實(shí)大家心里或多或少都有個(gè)數(shù)了,關(guān)系模型與關(guān)系數(shù)據(jù)庫(kù)實(shí)現(xiàn)中概念的對(duì)應(yīng)關(guān)系十分明顯:
- 關(guān)系型數(shù)據(jù)庫(kù)中的表(table)對(duì)應(yīng)關(guān)系模型中的關(guān)系(relation);
- 關(guān)系型數(shù)據(jù)庫(kù)中的表的記錄行(row)對(duì)應(yīng)關(guān)系模型中的元組(triple);
- 關(guān)系型數(shù)據(jù)庫(kù)中的表的列(column)對(duì)應(yīng)關(guān)系模型中的屬性(attribute);
- 關(guān)系型數(shù)據(jù)庫(kù)中的表的列數(shù)據(jù)類型(column type)對(duì)應(yīng)關(guān)系模型中的屬性的域(domain)。
當(dāng)然關(guān)系型數(shù)據(jù)庫(kù)與關(guān)系模型還有一些對(duì)應(yīng)關(guān)系不是本文重點(diǎn),比如:
- 關(guān)系模型中的關(guān)系完整性約束(如實(shí)體完整性、參照完整性等)對(duì)應(yīng)于關(guān)系數(shù)據(jù)庫(kù)中的約束(如主鍵約束、外鍵約束等)。
- 關(guān)系模型中的范式理論(如第一范式、第二范式等)對(duì)應(yīng)于關(guān)系數(shù)據(jù)庫(kù)中的數(shù)據(jù)規(guī)范化過(guò)程。
我們下面要關(guān)注的一個(gè)最重要的對(duì)應(yīng)就是關(guān)系模型中的關(guān)系代數(shù)運(yùn)算對(duì)應(yīng)于關(guān)系數(shù)據(jù)庫(kù)中的查詢操作,我們可以使用SQL語(yǔ)句來(lái)實(shí)現(xiàn)關(guān)系模型中的運(yùn)算,這也是下面我們要重點(diǎn)說(shuō)明的內(nèi)容,通過(guò)了解SQL語(yǔ)句背后實(shí)現(xiàn)的關(guān)系代數(shù)運(yùn)算的本質(zhì),將可以幫助我們更好地理解關(guān)系模型,對(duì)后續(xù)數(shù)據(jù)庫(kù)設(shè)計(jì)以及數(shù)據(jù)操作的高效性都大有裨益。
4. 關(guān)系代數(shù)與SQL的對(duì)應(yīng)關(guān)系
終于來(lái)到最重要的內(nèi)容了,其實(shí)就是通過(guò)SQL如何實(shí)現(xiàn)關(guān)系代數(shù)的操作,這也是作為應(yīng)用開(kāi)發(fā)人員最最關(guān)心的內(nèi)容。
4.1 預(yù)先定義的關(guān)系
為了便于后續(xù)的說(shuō)明,這里我們預(yù)先定義一些關(guān)系(表),它們將用在后續(xù)說(shuō)明各個(gè)關(guān)系運(yùn)算符的示例中,這些表見(jiàn)下圖:
這里包含一個(gè)學(xué)生表(Students)、一個(gè)課程清單表(Courses)以及兩年年度的選課表:CourseSelection2022和CourseSelection2023(注:這里不討論表設(shè)計(jì)的合理性)。
文中使用sqlite做為數(shù)據(jù)庫(kù)管理系統(tǒng)(DBMS)的代表,主要是為了簡(jiǎn)單,SQL標(biāo)準(zhǔn)的兼容性也不錯(cuò)。下面的Go代碼用于創(chuàng)建上圖中的表并插入樣例數(shù)據(jù):
// 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 {
// 創(chuàng)建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
}
// 創(chuàng)建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表插入數(shù)據(jù)
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表插入數(shù)據(jù)
stmt, err = db.Prepare("INSERT INTO Courses VALUES (?, ?, ?)")
checkErr(err)
_, err = stmt.Exec(1, "數(shù)據(jù)庫(kù)", 4)
checkErr(err)
_, err = stmt.Exec(2, "數(shù)學(xué)", 2)
checkErr(err)
_, err = stmt.Exec(3, "英語(yǔ)", 3)
checkErr(err)
// 插入2022選課數(shù)據(jù)
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選課數(shù)據(jù)
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]實(shí)現(xiàn)并開(kāi)源的modernc.org/sqlite,這是一個(gè)純Go的sqlite3數(shù)據(jù)庫(kù)driver。Go社區(qū)另一個(gè)廣泛使用的sqlite3的driver庫(kù)為go-sqlite3,只不過(guò)go-sqlite3是使用cgo對(duì)sqlite3 C庫(kù)的封裝。
執(zhí)行上面go代碼,便可以建立一個(gè)名為test.db的sqlite數(shù)據(jù)庫(kù),我們通過(guò)sqlite官方的命令行工具(cli)也可以與該數(shù)據(jù)庫(kù)文件交互(這里我們使用的是容器版cli),比如:
$docker pull nouchka/sqlite3
// cd到test.db文件路徑下
$docker run -v {test.db文件所在目錄的絕對(duì)路徑}:/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>
接下來(lái),我們就先從關(guān)系代數(shù)運(yùn)算中最容易理解的一元運(yùn)算符開(kāi)始說(shuō)起。
4.2. 選擇(Selection)
“選擇”是一元關(guān)系運(yùn)算,它的運(yùn)算符為σ,語(yǔ)義如下:
R' = σ[p](R "p") = {t | t∈R ∩ p(t) = true } // 這里用[p]表示數(shù)學(xué)符號(hào)的下標(biāo)
其中R為關(guān)系,t為元組,p是謂詞(predicate)表達(dá)式的組合,可以由一個(gè)或多個(gè)謂詞表達(dá)式構(gòu)成。
這個(gè)語(yǔ)義相對(duì)好理解一些:它對(duì)R的操作結(jié)果依然是關(guān)系R',即一個(gè)新元組集合,這個(gè)元組集合中的元組來(lái)自R,但必須滿足p(t) = true的條件。說(shuō)直白一些,就是選擇滿足給定條件的元組。下面是一個(gè)“選擇”操作的示意圖:
我們可以用下面最常見(jiàn)的SQL語(yǔ)句實(shí)現(xiàn)對(duì)單一關(guān)系(表)的選擇運(yùn)算:
SELECT * FROM R WHERE p(t) = true;
對(duì)應(yīng)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)
}
輸出結(jié)果為:
[{1001 1 85} {1001 2 80} {1002 1 83}]
4.3 投影(Projection)
“投影”也是一元關(guān)系運(yùn)算,它的運(yùn)算符為∏,語(yǔ)義如下:
R' = ∏[A1,A2,...,An](R "A1,A2,...,An") = {t[A1,A2,...,An]| t∈R } // 這里A1,A2,...,An表示從R中取出的列名
顯然和“選擇”通過(guò)謂詞表達(dá)式選元組不同,“投影”選擇一個(gè)關(guān)系中的指定列(A1,A2,...,An),即選擇需要的屬性。下面是其運(yùn)算過(guò)程的示意圖:
“投影”對(duì)應(yīng)的SQL語(yǔ)句也是我們最熟悉的語(yǔ)句:
SELECT A1, A2, ..., An FROM R;
對(duì)應(yīng)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)
}
輸出結(jié)果為:
[{1001 張三 0} {1002 李四 0} {1003 王五 0}]
不過(guò)要注意的是:取消某些關(guān)系列后可能出現(xiàn)重復(fù)行,違反了關(guān)系的定義(關(guān)系是一個(gè)元組的集合),因此必須檢查并去除結(jié)果關(guān)系中重復(fù)的元組。
4.4 運(yùn)算符的組合(Composition)
關(guān)系運(yùn)算的輸入是關(guān)系,結(jié)果也是一個(gè)關(guān)系,因此我們可以將關(guān)系運(yùn)算符組合成一個(gè)更復(fù)雜的關(guān)系運(yùn)算符表達(dá)式來(lái)實(shí)現(xiàn)更復(fù)雜的運(yùn)算。比如將上面的兩個(gè)一元關(guān)系運(yùn)算符組合在一起“先選元組,再選屬性”:
R' = ∏[A1,A2,...,An](σ[p](R "A1,A2,...,An"))
其運(yùn)算過(guò)程如下圖所示:
上述運(yùn)算符組合對(duì)應(yīng)的SQL語(yǔ)句如下:
SELECT A1, A2, ..., An FROM R where p(t) = true;
對(duì)應(yīng)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)
}
輸出結(jié)果為:
[{1001 張三 0}]
無(wú)論是選擇運(yùn)算還是投影運(yùn)算,亦或是組合之后的運(yùn)算,理解起來(lái)都相對(duì)容易,因?yàn)橹簧婕耙粋€(gè)“關(guān)系”。接下來(lái)我們就看一下涉及兩個(gè)關(guān)系的二元運(yùn)算符,我們先來(lái)看看集合運(yùn)算。
4.5 關(guān)系交(Intersection)
如果沒(méi)有記錯(cuò),我們是在高中學(xué)習(xí)的集合代數(shù)。那時(shí)定義兩個(gè)集合的交集運(yùn)算是這樣的:
對(duì)于集合A和B,其交運(yùn)算(Intersction)為:
A ∩ B = { x | x ∈ A且 x ∈ B}
用一個(gè)一維空間的數(shù)的集合的例子來(lái)說(shuō),就是當(dāng)A = {1, 2, 3, 4, 5},B = { 3, 5, 6, 9}時(shí),A ∩ B = {3, 5}。我們通常用維恩圖來(lái)示意集合運(yùn)算:
在關(guān)系模型中,元組是一維集合,關(guān)系是元組的集合,即是一個(gè)二維集合,那么基于關(guān)系的交運(yùn)算就要有一個(gè)前提:那就是參與運(yùn)算的兩個(gè)關(guān)系的屬性必須是兼容的。
兩個(gè)關(guān)系的屬性兼容需滿足以下條件:
- 屬性數(shù)量相同
兩個(gè)關(guān)系中的屬性數(shù)量必須相同。
- 屬性類型相同或可轉(zhuǎn)換
兩個(gè)關(guān)系中對(duì)應(yīng)位置的屬性類型必須相同或可以通過(guò)類型轉(zhuǎn)換進(jìn)行兼容。例如,一個(gè)關(guān)系中的屬性類型是整數(shù),而另一個(gè)關(guān)系中的屬性類型是浮點(diǎn)數(shù),這種情況下屬性類型是兼容的,因?yàn)檎麛?shù)可以隱式轉(zhuǎn)換為浮點(diǎn)數(shù)。
- 屬性名稱可以不同
兩個(gè)關(guān)系中對(duì)應(yīng)位置的屬性名稱可以不同,只要它們的屬性類型兼容即可。屬性名稱的不同不會(huì)影響屬性兼容性。
在關(guān)系模型中,兩個(gè)關(guān)系的屬性兼容性是判斷兩個(gè)關(guān)系是否可以進(jìn)行某些操作(包括集合操作)的重要條件之一。
回到集合運(yùn)算,如果兩個(gè)關(guān)系的屬性不兼容,則這兩個(gè)關(guān)系無(wú)法進(jìn)行集合運(yùn)算,比如Students表和Courses表的屬性個(gè)數(shù)不同,如果對(duì)它們進(jìn)行關(guān)系交運(yùn)算,會(huì)導(dǎo)致報(bào)錯(cuò):
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
介紹完集合運(yùn)算的前提后,我們?cè)賮?lái)看關(guān)系交運(yùn)算,其語(yǔ)義入下:
R' = R1 ∩ R2
即兩個(gè)關(guān)系R1和R2在屬性兼容的前提下進(jìn)行關(guān)系交運(yùn)算的結(jié)果為返回兩個(gè)關(guān)系中相同的元組。
關(guān)系交運(yùn)算對(duì)應(yīng)的SQL語(yǔ)句如下:
SELECT * FROM R1 INTERSECT SELECT * FROM R2;
對(duì)應(yīng)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這兩個(gè)關(guān)系沒(méi)有相同元組,所以上述Go程序輸出的結(jié)果為空。
4.6 關(guān)系并(Union)
和關(guān)系交一樣,兩個(gè)關(guān)系進(jìn)行關(guān)系并運(yùn)算的前提也是屬性兼容。關(guān)系并運(yùn)算的語(yǔ)義如下:
R' = R1 ∪ R2
即兩個(gè)關(guān)系R1和R2在屬性兼容的前提下進(jìn)行關(guān)系并運(yùn)算的結(jié)果為返回兩個(gè)關(guān)系中的所有元組,但要去除重復(fù)元組。
關(guān)系并對(duì)應(yīng)的SQL語(yǔ)句如下:
SELECT * FROM R1 UNION SELECT * FROM R2;
對(duì)應(yīng)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這兩個(gè)關(guān)系沒(méi)有重復(fù)元組,所有關(guān)系并運(yùn)算后得到的結(jié)果關(guān)系中包含了這兩個(gè)關(guān)系的全部元組,上述程序的輸出結(jié)果為:
[{1001 1 85} {1001 2 80} {1001 3 75} {1002 1 83} {1002 2 81} {1003 1 76} {1003 3 86}]
4.7 關(guān)系差(Difference)
在集合代數(shù)中,對(duì)于集合A和B,其差運(yùn)算為:
A - B = { x | x ∈ A且 x ? B}
即從A集合中排除掉B集合中的元素。
在關(guān)系模型中,關(guān)系差運(yùn)算即是從一個(gè)關(guān)系中排除另一個(gè)關(guān)系中的元組,其語(yǔ)義如下:
R' = R1-R2={t|t∈R1 ∩ t?R2} // t為關(guān)系中的元組
在SQL中,我們可以用NOT IN實(shí)現(xiàn):
SELECT * FROM R1 WHERE A1 NOT IN (SELECT A1 FROM R2 WHERE 條件)
下面是對(duì)應(yīng)的Go語(yǔ)言代碼片段:
// 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出現(xiàn)過(guò)的元組。下面是運(yùn)行結(jié)果:
[{1001 1 85} {1002 1 83} {1003 1 76}]
注意:關(guān)系差運(yùn)算的前提也是兩個(gè)關(guān)系的屬性兼容。
最后看看略復(fù)雜的二元運(yùn)算符:笛卡爾積和連接。
4.8 笛卡爾積(Cartesian-product)
在關(guān)系代數(shù)中,關(guān)系積,即笛卡爾積(Cartesian Product)這種運(yùn)算(也被稱為關(guān)系叉乘)用于取兩個(gè)關(guān)系的所有可能的組合。它的數(shù)學(xué)語(yǔ)義可以描述為:給定關(guān)系R1和R2,它們的笛卡爾積結(jié)果是一個(gè)新的關(guān)系,其中的元組由R1中的每個(gè)元組與R2中的每個(gè)元組的組合構(gòu)成。
在SQL中,笛卡爾積可以通過(guò)使用CROSS JOIN關(guān)鍵字來(lái)實(shí)現(xiàn):
SELECT * FROM R1 CROSS JOIN R2;
也可以通過(guò)下面SQL語(yǔ)句來(lái)實(shí)現(xiàn):
SELECT R1.*, R1.* FROM R1, R2;
對(duì)應(yīng)的Go代碼片段如下:
// relational-algebra-examples/query/main.go
// StudentCourse結(jié)果
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)
}
示例的運(yùn)行結(jié)果如下:
9
[{1001 張三 M 20 1 數(shù)據(jù)庫(kù) 4} {1001 張三 M 20 2 數(shù)學(xué) 2} {1001 張三 M 20 3 英語(yǔ) 3} {1002 李四 F 18 1 數(shù)據(jù)庫(kù) 4} {1002 李四 F 18 2 數(shù)學(xué) 2} {1002 李四 F 18 3 英語(yǔ) 3} {1003 王五 M 19 1 數(shù)據(jù)庫(kù) 4} {1003 王五 M 19 2 數(shù)學(xué) 2} {1003 王五 M 19 3 英語(yǔ) 3}]
我們看到對(duì)Students和Courses兩個(gè)關(guān)系(表)進(jìn)行笛卡爾積運(yùn)算后,結(jié)果包含了Students中的每個(gè)元組與Courses中的每個(gè)元組進(jìn)行組合的結(jié)果(3x3=9個(gè))。
需要注意的是,由于笛卡爾積可能導(dǎo)致非常大的結(jié)果集,因此在實(shí)際使用中應(yīng)謹(jǐn)慎使用,并且通常需要與其他運(yùn)算符和條件結(jié)合使用,以限制結(jié)果的大小和提高查詢效率。通常我們會(huì)用連接來(lái)達(dá)到這些目的。
4.9 連接(Join)
連接(Join)運(yùn)算(?)是從兩個(gè)關(guān)系的笛卡兒積中選取屬性間滿足一定條件的元組形成一個(gè)新的關(guān)系,即將笛卡爾積和選擇(selection)運(yùn)算合并達(dá)到一個(gè)操作中。從這個(gè)角度來(lái)看,笛卡爾積可以視為一種無(wú)條件的連接。
連接代數(shù)運(yùn)算符是關(guān)系代數(shù)中很有用的關(guān)系代數(shù)運(yùn)算符,也是日常經(jīng)常使用的運(yùn)算符,它有很多種不同的子類別,下面我們分別看看各種子類型的語(yǔ)義、SQL語(yǔ)句以及對(duì)應(yīng)的Go代碼示例。
4.9.1 等值連接(Equijoin)
等值連接是通過(guò)比較兩個(gè)關(guān)系(表)之間的屬性值是否相等來(lái)進(jìn)行連接的操作。連接條件使用等號(hào)(=)來(lái)比較屬性值的相等性。
我們直接看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)
}
這個(gè)示例使用等值連接將CourseSelection2022表和Students表連接起來(lái),連接條件是CourseSelection2022.Sno = Students.Sno,即學(xué)生編號(hào)相等,返回的結(jié)果將包含CourseSelection2022和Students兩個(gè)表中滿足連接條件的元組。
我們看看程序運(yùn)行的輸出結(jié)果:
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
在這個(gè)結(jié)果中,我們看到一個(gè)“奇怪”的情況,那就是出現(xiàn)了兩個(gè)Sno屬性。在等值連接中,如果連接的兩個(gè)表中存在相同名稱的屬性(例如這里兩個(gè)表中都有名為"Sno"的屬性),那么在連接結(jié)果中會(huì)出現(xiàn)兩個(gè)相同名稱的屬性。
這是因?yàn)榈戎颠B接會(huì)將兩個(gè)表中具有相同連接條件的屬性進(jìn)行匹配,并將匹配成功的元組進(jìn)行組合。由于兩個(gè)表中都有名為"Sno"的屬性,因此連接結(jié)果中會(huì)保留這兩個(gè)屬性,以顯示連接操作前后的對(duì)應(yīng)關(guān)系。
為了區(qū)分來(lái)自不同表的相同屬性名,通常在連接結(jié)果中會(huì)使用表別名或表名作為前綴,以區(qū)分它們的來(lái)源。這樣可以確保結(jié)果中的屬性名稱是唯一的,避免歧義。 例如,如果在等值連接中連接了名為"CourseSelection2022"的表和名為"Students"的表,并且兩個(gè)表中都有名為"Sno"的屬性,那么連接結(jié)果中可能會(huì)出現(xiàn)類似于"CourseSelection2022.Sno"和"Students.Sno"的屬性名稱,以明確它們的來(lái)源。
需要注意的是,數(shù)據(jù)庫(kù)管理系統(tǒng)的具體實(shí)現(xiàn)和查詢工具的設(shè)置可能會(huì)影響連接結(jié)果中屬性的顯示方式,但通常會(huì)采用類似的方式來(lái)區(qū)分相同屬性名的來(lái)源。
4.9.2 自然連接(Natural Join)
自然連接是基于兩個(gè)表中具有相同屬性名的屬性進(jìn)行連接的操作,重點(diǎn)在于它會(huì)自動(dòng)匹配具有相同屬性名的屬性,并根據(jù)這些屬性的相等性進(jìn)行連接,而無(wú)需手工指定。
我們來(lái)看自然連接的Go示例:
// relational-algebra-examples/query/main.go
func doNaturaljoin(db *sql.DB) {
rows, _ := db.Query("SELECT * FROM CourseSelection2022 NATURAL JOIN Students")
dumpOperationResult("Naturaljoin", rows)
}
這個(gè)示例使用自然連接將CourseSelection2022表和Students表連接起來(lái),自然連接會(huì)自動(dòng)基于兩個(gè)表中所有具有相同屬性名的屬性進(jìn)行連接,返回的結(jié)果將包含CourseSelection2022和Students兩個(gè)表中所有滿足連接條件的元組,并自動(dòng)消除重復(fù)屬性,這是與等值連接的一個(gè)明顯的區(qū)別。
我們看看程序運(yùn)行的輸出結(jié)果:
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
如果兩個(gè)表(比如R1和R2)有一個(gè)以上的屬性名相同,比如2個(gè)(比如:A1和A2),那就會(huì)自動(dòng)針對(duì)這兩個(gè)屬性名(一起)在兩個(gè)表中進(jìn)行等值連接:只有R2.A1 = R1.A1且R2.A2 = R1.A2時(shí),才將元組連接并放入結(jié)果關(guān)系中。
4.9.3 θ連接(Theta Join)
θ連接是一種通用的連接操作,它使用比等號(hào)更一般化的連接條件進(jìn)行連接。連接條件可以使用除了等號(hào)之外的比較運(yùn)算符(如大于、小于、不等于等)來(lái)比較兩個(gè)表之間的屬性。
我們來(lái)看θ連接的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)
}
這個(gè)示例使用Join將CourseSelection2022表和Students表連接起來(lái),連接條件是CourseSelection2022.Sno > Students.Sno,即學(xué)生編號(hào)大于學(xué)生表中的學(xué)生編號(hào),返回的結(jié)果將包含CourseSelection2022和`Students兩個(gè)表中滿足連接條件的元組。
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
這個(gè)結(jié)果的生成過(guò)程大致如下:
- 先看CourseSelection2022表的第一個(gè)元組,其Sno為1001,該Sno不大于Students表中的任一個(gè)Sno;
- 再看CourseSelection2022表的第二個(gè)元組,其Sno為1002,該Sno僅大于Students表中的Sno為1001的那一個(gè)元組,于是將CourseSelection2022表的第二個(gè)元組和Students表中第一個(gè)元組連接起來(lái)作為結(jié)果表中的第一個(gè)元組;
- 最后看CourseSelection2022表的第三個(gè)元組,其Sno為1003,該Sno大于Students表中的Sno為1001和1002的元組,于是將CourseSelection2022表的第三個(gè)元組分別和Students表中第一個(gè)和第二個(gè)元組連接起來(lái)作為結(jié)果表中的第二個(gè)和第三個(gè)元組。
4.9.4 半連接(Semi Join)
半連接是一種特殊的連接操作,它返回滿足連接條件的左側(cè)關(guān)系中的元組,并且只返回右側(cè)關(guān)系中與之匹配的屬性。半連接通常用于判斷兩個(gè)關(guān)系中是否存在匹配的元組,而不需要返回右側(cè)關(guān)系的詳細(xì)信息。
我們來(lái)看半連接的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)
}
這個(gè)示例使用半連接操作,以Students表為左側(cè)關(guān)系,CourseSelection2022表為右側(cè)關(guān)系。它使用子查詢來(lái)判斷左側(cè)關(guān)系中是否存在滿足連接條件的元組,即Students.Sno = CourseSelection2022.Sno。它返回的結(jié)果將只包含滿足連接條件的Students表中的元組。
下面是程序輸出的結(jié)果:
Semijoin operation:
Sno Sname Gender Age
1001 張三 M 20
1002 李四 F 18
1003 王五 M 19
半連接返回的結(jié)果關(guān)系中只包含左關(guān)系中的行,其中每一行只返回一次,即使在右關(guān)系中有多個(gè)匹配項(xiàng)。
4.9.5 反連接(Anti Join)
反連接是半連接的補(bǔ)集操作,它返回左側(cè)關(guān)系中不存在滿足連接條件的元組。反連接通常用于查找在左側(cè)關(guān)系中存在而在右側(cè)關(guān)系中不存在的元組。
我們來(lái)看反連接的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)
}
這個(gè)示例使用反連接操作,以Students表為左側(cè)關(guān)系,CourseSelection2022表為右側(cè)關(guān)系,并使用NOT EXISTS子查詢來(lái)判斷左側(cè)關(guān)系中不存在滿足連接條件的元組,即Students.Sno = CourseSelection2022.Sno。返回的結(jié)果將只包含左側(cè)關(guān)系Students表中不存在連接條件的元組。
Antijoin operation:
Sno Sname Gender Age
我們看到輸出的元組集合為空。
4.9.6 左(外)連接(Left Outer Join)
左外連接是將左側(cè)關(guān)系中的所有元組與滿足連接條件的右側(cè)關(guān)系中的元組進(jìn)行連接,并返回所有左側(cè)關(guān)系的元組。如果右側(cè)關(guān)系中沒(méi)有與左側(cè)關(guān)系匹配的元組,對(duì)應(yīng)的屬性值將為NULL。
我們來(lái)看左(外)連接的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)
}
這個(gè)示例使用左外連接將Students表和CourseSelection2022表連接起來(lái),其連接條件是Students.Sno = CourseSelection2022.Sno,即學(xué)生編號(hào)相等。示例的返回結(jié)果將包含Students表中的所有元組,并將滿足連接條件的CourseSelection2022表中的元組加入結(jié)果中。如果沒(méi)有匹配的元組,右側(cè)關(guān)系中的屬性值將為NULL。 ` 下面是程序輸出的結(jié)果:
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 76
4.9.7 右(外)連接(Right Outer Join)
右外連接是將右側(cè)關(guān)系中的所有元組與滿足連接條件的左側(cè)關(guān)系中的元組進(jìn)行連接,并返回所有右側(cè)關(guān)系的元組。如果左側(cè)關(guān)系中沒(méi)有與右側(cè)關(guān)系匹配的元組,對(duì)應(yīng)的屬性值將為NULL。
我們來(lái)看右(外)連接的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)
}
這個(gè)示例使用右外連接將Students表和CourseSelection2022表連接起來(lái),它的連接條件是Students.Sno = CourseSelection2022.Sno,即學(xué)生編號(hào)相等。返回的結(jié)果將包含CourseSelection2022表中的所有元組,并將滿足連接條件的Students表中的元組加入結(jié)果中。如果沒(méi)有匹配的元組,左側(cè)關(guān)系中的屬性值將為NULL。
下面是程序輸出的結(jié)果:
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 76
4.9.8 全連接(Full Outer Join)
全連接是將左側(cè)關(guān)系和右側(cè)關(guān)系中的所有元組進(jìn)行連接,并返回所有滿足連接條件的元組。如果左側(cè)關(guān)系或右側(cè)關(guān)系中沒(méi)有與對(duì)方匹配的元組,對(duì)應(yīng)的屬性值將為NULL。
我們來(lái)看全連接的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)
}
這個(gè)示例使用全連接將Students表和CourseSelection2022表連接起來(lái),連接條件是Students.Sno = CourseSelection2022.Sno,即學(xué)生編號(hào)相等。示例返回的結(jié)果將包含Students表和CourseSelection2022表中的所有元組,并將滿足連接條件的元組進(jìn)行組合。如果沒(méi)有匹配的元組,對(duì)應(yīng)關(guān)系中的屬性值將為NULL。
下面是程序輸出的結(jié)果:
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
以上就是本文要介紹的連接類型,這些連接類型提供了在關(guān)系數(shù)據(jù)庫(kù)中操作和組合表數(shù)據(jù)的靈活性,可以根據(jù)特定的需求選擇合適的連接方式來(lái)獲取所需的結(jié)果。
5. 小結(jié)
本文系統(tǒng)地介紹和講解了關(guān)系數(shù)據(jù)庫(kù)中的關(guān)系代數(shù)運(yùn)算,包括選擇、投影、連接、交、并、積等,以及關(guān)系代數(shù)的SQL實(shí)現(xiàn),并給出了Go語(yǔ)言示例。
關(guān)系模型是關(guān)系數(shù)據(jù)庫(kù)的理論基礎(chǔ),關(guān)系代數(shù)通過(guò)對(duì)關(guān)系的運(yùn)算來(lái)表達(dá)查詢,因此關(guān)系代數(shù)也構(gòu)成了SQL查詢語(yǔ)言的理論基礎(chǔ)。理解關(guān)系代數(shù)與SQL的對(duì)應(yīng)關(guān)系,可以更好地使用SQL語(yǔ)言操作關(guān)系型數(shù)據(jù)庫(kù)。
本文算是關(guān)系數(shù)據(jù)庫(kù)的入門(mén)文章,既能讓數(shù)據(jù)庫(kù)初學(xué)者快速掌握關(guān)系代數(shù),也能讓有基礎(chǔ)的讀者回顧并深入理解概念內(nèi)涵。通過(guò)閱讀學(xué)習(xí),能幫助讀者把關(guān)系代數(shù)運(yùn)用到實(shí)際數(shù)據(jù)庫(kù)應(yīng)用中,解決查詢優(yōu)化等問(wèn)題。