1、SQL與笛卡爾積
首先,先簡單解釋一下笛卡爾積。
現在,我們有兩個集合A和B。
A = {0,1} B = {2,3,4}
集合 A×B 和 B×A的結果集就可以分別表示爲以下這種形式:
A×B = {(0,2),(1,2),(0,3),(1,3),(0,4),(1,4)};
B×A = {(2,0),(2,1),(3,0),(3,1),(4,0),(4,1)};
以上A×B和B×A的結果就可以叫做兩個集合相乘的笛卡爾積
。
從以上的數據分析我們可以得出以下兩點結論:
-
兩個集合相乘,不滿足交換率,既 A×B ≠ B×A;
-
A集合和B集合相乘,包含了集合A中元素和集合B中元素按順序結合的所有的可能性。既兩個集合相乘得到的新集合的元素個數是 A集合的元素個數 × B集合的元素個數;
-
其實和高中數學裏的排列很類似,不過排列裏含有(2,0)、(0,2),而笛卡爾積只有其中一個:AxB則是(0,2),BxA則是(2,0)。
數據庫表連接數據行匹配時所遵循的算法就是以上提到的笛卡爾積,表與表之間的連接可以看成是在做乘法運算。
比如現在數據庫中有兩張表,student表和 student_subject表,如下所示:
我們執行以下的sql語句,只是純粹的進行表連接。
SELECT * from student JOIN student_subject;
SELECT * from student_subject JOIN student;
看一下執行結果:
從執行結果上來看,結果符合我們以上提出的兩點結論;
以第一條sql語句爲例我們來看一下他的執行流程,
-
from語句把student表 和 student_subject表從數據庫文件加載到內存中。
-
join語句相當於對兩張表做了乘法運算,把student表中的每一行記錄按照順序和student_subject表中記錄依次匹配。
-
匹配完成後,我們得到了一張有 (student中記錄數 × student_subject表中記錄數)條的臨時表。 在內存中形成的臨時表如表1.0所示。我們又把內存中表1.0所示的表稱爲
笛卡爾積表
。
針對以上的理論,我們提出一個問題,難道表連接的時候都要先形成一張笛卡爾積表嗎?
如果兩張表的數據量都比較大的話,那樣就會佔用很大的內存空間這顯然是不合理的。所以,我們在進行表連接查詢的時候一般都會使用JOIN xxx ON xxx的語法,ON語句的執行是在JOIN語句之前的,也就是說兩張表數據行之間進行匹配的時候,會先判斷數據行是否符合ON語句後面的條件,再決定是否JOIN。
因此,有一個顯而易見的SQL優化的方案是,當兩張表的數據量比較大,又需要連接查詢時,應該使用 FROM table1 JOIN table2 ON xxx的語法,避免使用 FROM table1,table2 WHERE xxx 的語法,因爲後者會在內存中先生成一張數據量比較大的笛卡爾積表,增加了內存的開銷。
根據上一篇博客(http://www.cnblogs.com/cdf-opensource-007/p/6502556.html),及本篇博客的分析,我們可以總結出一條查詢sql語句的執行流程。
1. From
2. ON
3. JOIN
4. WHERE
5. GROUP BY
6. SELECT
7. HAVING
8. ORDER BY
9. LIMIT
最後,針對兩張數據庫表連接的底層實現,我用java代碼模擬了一下,感興趣的可以看一下,能夠幫助我們理解:
package com.opensource.util;
import java.util.Arrays;
public class DecareProduct {
public static void main(String[] args) {
//使用二維數組,模擬student表
String[][] student ={
{"0","jsonp"},
{"1","alice"}
};
//使用二維數組,模擬student_subject表
String[][] student_subject2 ={
{"0","0","語文"},
{"1","0","數學"}
};
//模擬 SELECT * from student JOIN student_subject;
String[][] resultTowArray1 = getTwoDimensionArray(student,student_subject2);
//模擬 SELECT * from student_subject JOIN student;
String[][] resultTowArray2 = getTwoDimensionArray(student_subject2,student);
int length1 = resultTowArray1.length;
for (int i = 0; i <length1 ; i++) {
System.out.println(Arrays.toString(resultTowArray1[i]));
}
System.err.println("-----------------------------------------------");
int length2 = resultTowArray2.length;
for (int i = 0; i <length2 ; i++) {
System.out.println(Arrays.toString(resultTowArray2[i]));
}
}
/**
* 模擬兩張表連接的操作
* @param towArray1
* @param towArray2
* @return
*/
public static String[][] getTwoDimensionArray(String[][] towArray1,String[][] towArray2){
//獲取二維數組的高(既該二維數組中有幾個一維數組,用來指代數據庫表中的記錄數)
int high1 = towArray1.length;
int high2 = towArray2.length;
//獲取二維數組的寬度(既二位數組中,一維數組的長度,用來指代數據庫表中的列)
int wide1 = towArray1[0].length;
int wide2 = towArray2[0].length;
//計算出兩個二維數組進行笛卡爾乘積運算後獲得的結果集數組的高度和寬度,既笛卡爾積表的行數和列數
int resultHigh = high1 * high2;
int resultWide = wide1 + wide2;
//初始化結果集數組,既笛卡爾積表
String[][] resultArray = new String[resultHigh][resultWide];
//迭代變量
int index = 0;
//先對第二二維數組遍歷
for (int i = 0; i < high2; i++) {
//拿出towArray2這個二維數組的元素
String[] tempArray = towArray2[i];
//循環嵌套,對第towArray1這個二維數組遍歷
for (int j = 0; j < high1; j++) {
//初始化一個長度爲'resultWide'的數組,作爲結果集數組的元素,既笛卡爾積表中的一行
String[] tempExtened = new String[resultWide];
//拿出towArray1這個二維數組的元素
String[] tempArray1 = towArray1[j];
//把tempArray1和tempArray兩個數組的元素拷貝到結果集數組的元素中去。(這裏用到了數組擴容)
System.arraycopy(tempArray1, 0, tempExtened, 0, tempArray1.length);
System.arraycopy(tempArray, 0, tempExtened, tempArray1.length, tempArray.length);
//把tempExtened放入結果集數組中
resultArray[index] = tempExtened;
//迭代加一
index++;
}
}
return resultArray;
}
}
執行結果:
幾個join的笛卡爾積:
- 兩表直接連接,笛卡爾積的結果數量是兩表的數據量相乘
- 帶where/on條件id相等的笛卡爾積和inner join結果相同,但是inner join效率快一點
- left join:TEST_A表的ID爲空時拼接TEST_B表的內容爲空,
- right join則相反
- full join:等於left join和right join的並集
因此如果程序的確需要多表聯合查詢,儘量兩兩連接,並通過where或on或inner join縮小結果集,再將結果集對其他表繼續連接……
2、JavaIO的裝飾模式與笛卡爾積
在學習 java.io
包的時候,InputStream 那一羣類很讓人反感,子類繁多就不用說,使用起來非常奇怪,因爲它使用了裝飾模式……
假設我們想以緩存的方式從文件中讀取字節流,一般常見的操作總是:先創建一個FileInputStream
,然後把這個FileInputStream
放入BufferedInputStream
構造函數中去創建BufferedInputStream。完成這些工作後才能開始讀取文件:
try (FileInputStream fis = new FileInputStream("c:/a.txt");
BufferedInputStream bis = new BufferedInputStream(fis)) {
byte[] buffer = new byte[1024];
int len;
StringBuilder result = new StringBuilder();
while ((len = bis.read(buffer)) != -1) {
result.append(new String(buffer, 0, len));
}
} catch (IOException e) {
//handle
}
爲什麼 sun 不能直接創建以緩存方式從文件中讀取數據的輸入流類
呢?
或者說爲什麼InputStream選擇裝飾者模式,而非直接繼承的方法來擴展,也就是裝飾者模式VS繼承。
爲了回答這個問題,就以InputStream與FilterInputStream兩者組合,如果我用了繼承,看看我們的類圖是什麼樣的:
似曾相識,我們再看一下:
InputStream:[ FileInputStream
,ByteArrayInput Stream
,SequenceInputStream
,ObjectInputream
,PipedInputStream
,StringBufferInputStream
……還包括其他二方、三方繼承InputStream自實現的InputStream子類,目前至少有兩百多個各種實現
]
FilterInputStream(它也繼承自InputStream):[BufferedInputStream
,DataInputStream
,PushbackInputStream
……]
兩者假設進行任意組合,即可構成一個所謂的輸出流類
,那麼這種輸出流類
的數量將是一個笛卡兒積,即爆炸增長,同時InputStream內部還可以進行互相組合。
而如果採用裝飾模式,具體你們怎麼搭配我不關心,只需要套個裝飾,即變成了一個新的功能的輸出流類
!
SQL與笛卡爾積 轉自: