1、 " DUAL 表 " 2、 " ROWID 類型 " 2.1、 "利用 ROWID 查詢數據" 2.2、 "利用 ROWID 更新數據" 3、 " NULL 值 " 3.1、 "NULL 與空字元串" 3.2、 "NULL 與函數" 3.3、 "NULL 與索引" 3.4、 "NULL 與 S ...
- 1、DUAL 表
- 2、ROWID 類型
- 2.1、利用 ROWID 查詢數據
- 2.2、利用 ROWID 更新數據
- 3、NULL 值
- 3.1、NULL 與空字元串
- 3.2、NULL 與函數
- 3.3、NULL 與索引
- 3.4、NULL 與 SQL
- 4、總結
1、DUAL 表
DUAL 是 Oracle 中的一個虛擬表,用來構成 SELECT 的語法規則,Oracle 系統保證 DUAL 裡面永遠只有一條記錄。本人對“用來構成 SELECT 的語法規則”的理解是:由於 Oracle 中的 SELECT 語句必須包含 FROM 子句(這點與 SQL Server 等資料庫不同),但很多時候我們只需要查詢一些與任何真實表無關的變數或常量等,這時候如果沒有諸如 DUAL 之類的虛擬表來滿足語法規則,就不易實現了。素來以強大著稱的 Oracle 當然是不會允許這種缺陷存在的,於是 DUAL 表應運而生。示例:
SELECT 'A' res FROM DUAL; -- res: 'A',查詢常量 'A'
SELECT 1+2 res1,3*7 res2 FROM DUAL; -- res1: 3,res2: 21,做四則運算,相當於計算器
SELECT fn_today res FROM DUAL; -- res: 2017-01-10,調用用戶函數
SELECT ROWNUM res FROM DUAL; -- res: 1,調用系統函數
SELECT SYSDATE res FROM DUAL; -- res: 2017-02-05 21:31:05,查詢當前系統時間
SELECT SYS_GUID() res FROM DUAL; -- res: '68A531826DEF4DA8BD7F03C1EE0C1A7E',獲得一個GUID
SELECT DBMS_RANDOM.RANDOM res FROM DUAL; -- res: -212493338,獲得一個隨機數
SELECT USER res FROM DUAL; -- res: DEMO,查詢當前連接用戶名
SELECT SYS_CONTEXT('USERENV','TERMINAL') FROM DUAL; -- res: HZZ-PC,查詢當前主機名
2、ROWID 類型
ROWID 是 Oracle 中較為複雜的一個特殊對象,開發人員一般無需對 ROWID 做深入瞭解。ROWID 不僅是一種數據類型,創建欄位的時候可以指定欄位類型為 ROWID。ROWID 還是 Oracle 表中行的唯一標識,通過 ROWID,Oracle 可以快速定位表中行數據的具體物理存儲位置。
2.1、利用 ROWID 查詢數據
有人把 ROWID 也稱之為偽列,但它們在語法上有個明顯的區別就是——ROWID 可以加表別名限定。這點從 ROWID 的用途來看也很好理解。
通過 ROWID 查詢表中某一行數據的示例:
SELECT t.staff_id,t.staff_name,t.birthday FROM demo.t_staff t WHERE t.ROWID='AAAMpVAAEAAAABeAAA';
結果:
STAFF_ID STAFF_NAME BIRTHDAY
----------- -------------------------------------------------- -----------
1 小明 1988-05-08
通過 ROWID 查詢表中重覆數據的示例:
SELECT COUNT(1) res FROM demo.t_staff t WHERE t.ROWID>(SELECT MAX(n.ROWID) FROM demo.t_staff n WHERE n.staff_id=t.staff_id); -- res: 0
通過 ROWID 來分頁查詢的示例(對員工表分頁,一頁 3 條數據,查第 1 頁):
SELECT t4.staff_name,t4.dept_code,t4.gender,t4.birthday
FROM demo.t_staff t4
WHERE t4.ROWID IN(
SELECT t3.rid FROM(
SELECT t2.rid,ROWNUM rn FROM(
SELECT t1.ROWID rid,t1.birthday FROM demo.t_staff t1 ORDER BY t1.birthday
) t2 WHERE ROWNUM <= (1*3)
) t3 WHERE t3.rn >= ((1-1)*3+1)
) ORDER BY t4.birthday;
結果:
STAFF_NAME DEPT_CODE GENDER BIRTHDAY
-------------------------------------------------- -------------------------------------------------- ------ -----------
小薩 010102 1 1986-03-07
小明 010101 1 1988-05-08
李陽 010101 1 1989-01-14
2.2、利用 ROWID 更新數據
在 PL/SQL Developer 中,查詢某個表的時候,只要在查詢欄位列表中包含 ROWID,即可直接修改已經查出來的數據。寫法示例:
SELECT t.ROWID,t.* FROM demo.t_staff t; -- 執行查詢之後,數據窗格上面會出現一個小鎖,點一下就可以直接修改數據了
3、NULL 值
在 Oracle 中 NULL 是一個很特殊的值,任何類型的值都可以是 NULL。NULL 是未知的,它可以是任何類型的值,也可以不依賴於任何類型而單獨存在(字面量 NULL),任何沒有 NOT NULL 約束或主鍵約束的列都有可能出現 NULL 值。
3.1、NULL 與空字元串
NULL 的判斷和比較運算規則:NULL 與任何值(包括 NULL 自身)既不相等也不不想等,換句話說它與任何值的比較結果都是未知的。很多人都說 NULL 與空字元串是等價的,其實是 Oracle 把它們做了等價處理,但 Oracle 並不建議把它們當成是一樣的,至少在某些系統函數內部它們就不一樣,而且以後可能還會變。事實上 NULL 可能是任何類型,也可能沒有類型,而空字元串一定是字元類型,這是它們的本質區別。示例:
WITH
t1 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL=NULL),t2 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL<>NULL),
t3 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL=' '),t4 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL<>' '),
t5 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL=''),t6 AS(SELECT COUNT(1) res FROM DUAL WHERE NULL<>''),
t7 AS(SELECT COUNT(1) res FROM DUAL WHERE ''=''),t8 AS(SELECT COUNT(1) res FROM DUAL WHERE ''<>''),
t9 AS(SELECT COUNT(1) res FROM DUAL WHERE ''=' '),t10 AS(SELECT COUNT(1) res FROM DUAL WHERE ''<>' ')
SELECT t1.res,t2.res,t3.res,t4.res,t5.res,t6.res,t7.res,t8.res,t9.res,t10.res FROM t1,t2,t3,t4,t5,t6,t7,t8,t9,t10;
結果(下麵的結果進一步證明瞭 NULL != Any value & NULL !!= Any value
和 '' <=> NULL
):
RES RES RES RES RES RES RES RES RES RES
---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
0 0 0 0 0 0 0 0 0 0
事實上除了不能用 =
和 <>
來比較 NULL,也不能用 >
、<
等其它比較運算符來比較 NULL。Oracle 提供了 IS NULL
和 IS NOT NULL
來判別某個數據是否為 NULL 或不為 NULL。示例:
SELECT COUNT(1) res FROM DUAL WHERE NULL IS NULL; -- res:1
SELECT COUNT(1) res FROM DUAL WHERE 0 IS NULL; -- res:0
SELECT COUNT(1) res FROM DUAL WHERE '' IS NULL; -- res:1
SELECT COUNT(1) res FROM DUAL WHERE NULL IS ''; -- 報錯(missing NULL keyword)
SELECT COUNT(1) res FROM DUAL WHERE NULL IS NOT NULL; -- res:0
SELECT COUNT(1) res FROM DUAL WHERE 0 IS NOT NULL; -- res:1
SELECT COUNT(1) res FROM DUAL WHERE '' IS NOT NULL; -- res:0
SELECT COUNT(1) res FROM DUAL WHERE NULL IS NOT ''; -- 報錯(missing NULL keyword)
IS NULL
和 IS NOT NULL
是不可分割的整體,不能寫成 IS ''
或 IS NOT ''
,如上兩組案例的最後 4 個也能證明這一點。
NULL 的算術和邏輯運算規則:NULL 在做一些算術運算時,如 +
、-
、*
、/
等,結果一定還是 NULL。如果是連接操作符 ||
,則會直接忽略 NULL。示例:
SELECT 1 + NULL res FROM DUAL; -- res:NULL
SELECT 1 - NULL res FROM DUAL; -- res:NULL
SELECT 1 * NULL res FROM DUAL; -- res:NULL
SELECT 1 / NULL res FROM DUAL; -- res:NULL
SELECT '1' || NULL res FROM DUAL; -- res:'1'
如果把上面 5 條語句中的 NULL 替換成 '',結果不變。這也印證了在做邏輯運算時,Oracle 把 '' 做了與 NULL 等效的處理。
3.2、NULL 與函數
Oracle 中的很多函數都對 NULL 做了特殊處理,如聚合函數、DECODE 函數等。有些函數會直接忽略 NULL,而另一些函數則會對 NULL 做特殊處理,下麵我們來看 4 個 NULL 與函數的示例:
LTRIM 函數
SELECT LTRIM('_ID','_') res FROM DUAL; -- res:ID
SELECT LTRIM('_ID',NULL) res FROM DUAL; -- res:NULL
REPLACE 函數
SELECT REPLACE('_ID','_') res FROM DUAL; -- res:ID
SELECT REPLACE('_ID',NULL) res FROM DUAL; -- res:'_ID'
DECODE 函數
語法:DECODE(expr, val1, res1, val2, res2, ...[, defval])
描述:如果第一個參數與第 2n(n>=1) 個參數相等,則返回第 2n+1 個參數,如果第一個參數與所有第 2n 個參數都不相等,有預設參數(當參數列表長度為 2n 時,最後一個參數就是預設參數)就返回預設參數,沒有預設參數就返回 NULL。
示例:
SELECT DECODE(1,1,'A',2,'B','C') res FROM dual; -- res:'A'
SELECT DECODE(2,1,'A',2,'B','C') res FROM dual; -- res:'B'
SELECT DECODE(3,1,'A',2,'B','C') res FROM dual; -- res:'C'
SELECT DECODE(3,1,'A',2,'B') res FROM dual; -- res:NULL
SELECT DECODE(NULL,1,'A',2,'B') res FROM dual; -- res:NULL
SELECT DECODE(NULL,1,'A',NULL,'B') res FROM dual; -- res:'B'
DECODE 函數很強大,用起來也很方便,但它是 Oracle 的方言,我們還是要儘可能少用。其實 DECODE 函數能實現的任何功能都能用 CASE 函數或 SQL 的 IF 語句來實現,只是書寫起來相對繁瑣。
CASE 函數
語法:
CASE [ expression ]
WHEN condition_1 THEN result_1
WHEN condition_2 THEN result_2
...
WHEN condition_n THEN result_n
ELSE res
END
示例(為方便示例間對比和節省篇幅,避免篇幅過長不方便閱讀,下述 4 個示例均縮寫成一行,實際開發中一般有斷行更好閱讀):
SELECT CASE 1 WHEN 1 THEN 'A' WHEN 2 THEN 'B' ELSE 'C' END res FROM DUAL; -- res: 'A'
SELECT CASE 2 WHEN 1 THEN 'A' WHEN 2 THEN 'B' ELSE 'C' END res FROM DUAL; -- res: 'B'
SELECT CASE NULL WHEN NULL THEN 'A' ELSE 'B' END res FROM DUAL; -- res:'B'
SELECT CASE WHEN NULL IS NULL THEN 'NULL' ELSE 'NOT NULL' END res FROM DUAL; -- res: 'NULL'
為了能更靈活的對 NULL 進行處理,Oracle 提供了 NVL、NVL2、NULLIF、COALESCE 等函數來專門處理 NULL。下麵來看看這 4 個函數的示例:
NVL 函數
語法:NVL(expr1, expr2)
描述:常用於某個值可能是 NULL,而查詢結果中又不能出現空值的轉換需求。如果第一個參數為 NULL,則返回第二個參數,否則返回第一個參數。
示例:
SELECT NVL(NULL,'b') res FROM DUAL; -- res:b
SELECT NVL('a','b') res FROM DUAL; -- res:a
SELECT NVL(NULL,2) res FROM DUAL; -- res:2
SELECT NVL(1,2) res FROM DUAL; -- res:1
NVL2 函數
語法:NVL2(expr1, expr2, expr3)
描述:常用於無論某個值是否為 NULL,都需要轉換的需求。如果第一個參數值為 NULL,則返回第三個參數,否則返回第二個參數。
示例:
SELECT NVL2(NULL,'b','c') res FROM DUAL; -- res:c
SELECT NVL2('a','b','c') res FROM DUAL; -- res:b
SELECT NVL2(NULL,2,3) res FROM DUAL; -- res:3
SELECT NVL2(1,2,3) res FROM DUAL; -- res:2
NULLIF 函數
語法:NULLIF(expr1, expr2)
描述:如果第一個參數與第二個參數相等,則返回 NULL,否則返回第一個參數。此函數要求第一個參數與第二個參數類型相容,且第一個參數不能是 NULL,限制比較多,且可用 CASE 函數或 DECODE 函數代替,因此實際開發中用的不多。
示例:
SELECT NULLIF('a','a') res FROM DUAL; -- res:NULL
SELECT NULLIF('a','b') res FROM DUAL; -- res:a
SELECT NULLIF(1,1) res FROM DUAL; -- res:NULL
SELECT NULLIF(1,2) res FROM DUAL; -- res:1
COALESCE 函數
語法:COALESCE(expr1, expr2, ..., exprn)
描述:此函數可接受兩個及以上的參數,從左向右找到第一個不為 NULL 的參數並返回,若所有參數均為 NULL,則返回 NULL。此函數要求參數列表數據類型相容,且有短路計算功能,找到非 NULL 的參數後便不再繼續。
案例:
SELECT COALESCE(NULL,NULL,'a') res FROM DUAL; -- res:a
SELECT COALESCE('','','a') res FROM DUAL; -- res:a
SELECT COALESCE(NULL,0,1,2) res FROM DUAL; -- res:0
SELECT COALESCE(NULL,NULL,NULL) res FROM DUAL; -- res:NULL
3.3、NULL 與索引
眾所周知,索引就是商業資料庫中為加速對錶中數據行的檢索而創建的一種存儲結構。Oracle 中的索引有很多種,其中最基本的單列索引和複合索引不會存儲索引列全為 NULL 的行。證明示例:
-- 先給 demo.t_staff.hire_date 列創建一個索引
CREATE INDEX idx_hire_date ON demo.t_staff(hire_date);
-- 然後查詢一下索引中的數據行數,由於 hire_date 列全都為 NULL,結果是 0
SELECT t.table_owner,t.table_name,t.index_name,t.num_rows FROM SYS.USER_INDEXES t WHERE t.index_name='IDX_HIRE_DATE';
-- 將女員工的 hire_date 全都設置為 2016-06-01
UPDATE demo.t_staff t SET t.hire_date=TO_DATE('2016-06-01','yyyy-mm-dd') WHERE t.gender=0;
COMMIT;
-- 重建 IDX_HIRE_DATE 索引
ALTER INDEX demo.idx_hire_date REBUILD;
-- 再來查詢索引中的數據行數,結果是 5
SELECT t.table_owner,t.table_name,t.index_name,t.num_rows FROM SYS.USER_INDEXES t WHERE t.index_name='IDX_HIRE_DATE';
這裡將介紹一個利用 NULL 和基於函數的索引來優化查詢的典型案例:假如人事部想要一個能方便給新員工指派導師的功能。稍微分析下需求就會發現,公司大多數員工都是正式員工,試用期的員工只占很小的一部分。而人事專員在實際使用過程中經常查詢的恰恰是那少數新員工,正式員工則很少會查詢。這時就可以只對新員工所屬行建立索引,一方面節省了維護索引的開銷,降低了索引的存儲空間,另一方麵包含行少的索引檢索也更快。示例:
-- 給員工表添加一個記錄試用狀態的欄位 prob_status
ALTER TABLE demo.t_staff ADD(prob_status NUMBER(1));
COMMENT ON COLUMN demo.t_staff.prob_status IS '試用狀態{0已轉正/1試用中}';
-- 根據上文的證明的結論,試用中的員工狀態全部轉為 NULL 了,自然就進不了索引了
CREATE INDEX idx_prob_status ON demo.t_staff(DECODE(prob_status,1,1,NULL));
-- 再來查詢試用期員工時,就會走索引了,大家可以自行確認索引行數和分析下句的執行計劃
SELECT * FROM demo.t_staff t WHERE DECODE(t.prob_status,1,1,NULL)=1;
3.4、NULL 與 SQL
當 DDL 遇上 NULL:因前文.Net程式員學用Oracle系列(6):表、欄位、註釋、約束、索引中已經介紹過基本的 DDL 語法了,也都比較簡單,本節就不再贅述。但有關操作非空約束和預設值的 DDL 語句,卻有很多與 NULL 相關的細節問題仍需註意,這裡將直接給出 5 個實驗結果,有興趣的讀者朋友們可自行驗證。
- 創建表的時候,如果同時給欄位設置非空約束和預設值,那麼 NULL/NOT NULL 必須放在 DEFAULT 後面。
- 創建欄位的時候,如果同時設置預設值,那麼表中現有行的該列會自動更新為預設值,否則全部為 NULL。
- 修改表欄位的時候,如果同時修改非空約束和預設值,那麼 NULL/NOT NULL 必須在 DEFAULT 後面。
- 添加或修改預設值的時候,不會影響表中現有行的值。
- 添加或修改非空約束的時候,如果欄位原本是 NOT NULL 約束,無論表中是否有數據,都可以直接改為 NULL 約束。如果欄位原本是 NULL 約束,且該列不包含 NULL 值,仍然可以修改為 NOT NULL 約束;倘若該列已經存在 NULL 值,那就不能再修改為 NOT NULL 約束了。
當子查詢遇上 NULL:有些子查詢結果集中包含 NULL 時,就可能會導致外部查詢的“結果集異常”,如 IN/NOT IN 中的非相關子查詢。
IN 非相關子查詢示例:
WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE t1.cname IN(SELECT t2.cname FROM t2);
結果:
CID CNAME
---------- -----
1 Trump
NOT IN 非相關子查詢示例:
WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Hillary' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE t1.cname NOT IN(SELECT t2.cname FROM t2);
結果:
CID CNAME
---------- -----
因為 NULL 既不等於任何值,也不不等於任何值,所以上述兩個單列非相關子查詢的結果集,都比“預期”少了一行記錄。多列子查詢的規律相同,分析方法也是:NULL != Any value & NULL !!= Any value
。下麵再補充幾個多列子查詢的示例,即使你還不太明白,但相信你根據我給出的分析方法,再對比這幾條語句和結果也能自己悟出其中的奧妙。
示例:
WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE (t1.cid,t1.cname) IN(SELECT t2.cid,t2.cname FROM t2);
結果:
CID CNAME
---------- -----
1 Trump
示例:
WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Hillary' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE (t1.cid,t1.cname) NOT IN(SELECT t2.cid,t2.cname FROM t2);
結果:
CID CNAME
---------- -----
1 Trump
示例:
WITH
t1 AS(SELECT 1 cid,'Trump' cname FROM DUAL UNION ALL SELECT 2,NULL FROM DUAL),
t2 AS(SELECT 1 cid,'Hillary' cname FROM DUAL UNION ALL SELECT 3,NULL FROM DUAL)
SELECT cid,cname FROM t1 WHERE (t1.cid,t1.cname) NOT IN(SELECT t2.cid,t2.cname FROM t2);
結果:
CID CNAME
---------- -----
1 Trump
2
如果你看了上面 3 個示例之後,結果還是不理解的話,那就看看下麵 4 條語句的條件分析說明,看明白之後再去悟上面的 3 個示例吧。已經理解了的讀者可以跳過本節了!
-- 該句的 WHERE 條件相當於 t.dept_code = '010101'
SELECT COUNT(1) res FROM demo.t_staff t WHERE t.dept_code IN('010101'); -- res:5
-- 該句的 WHERE 條件相當於 t.dept_code = '010101' OR t.dept_code = NULL
SELECT COUNT(1) res FROM demo.t_staff t WHERE t.dept_code IN('010101',NULL); -- res:5
-- 該句的 WHERE 條件相當於 t.dept_code != '010101'
SELECT COUNT(1) res FROM demo.t_staff t WHERE t.dept_code NOT IN('010101'); -- res:11
-- 該句的 WHERE 條件相當於 t.dept_code != '010101' AND t.dept_code != NULL
SELECT COUNT(1) res FROM demo.t_staff t WHERE t.dept_code NOT IN('010101',NULL); -- res:0
當集合查詢和分組查詢遇上 NULL:在.Net程式員學用Oracle系列(14):子查詢、集合查詢第 2 節中有講到 INTERSECT、UNION、MINUS 三個操作符的處理結果是不包含重覆行的,如果某列中含有 NULL,其它列與別的行相同則認為是重覆行。示例:
SELECT 1 cid,'Trump' cname FROM DUAL
UNION
SELECT 2 cid,NULL cname FROM DUAL
UNION
SELECT 2 cid,NULL cname FROM DUAL;
結果:
CID CNAME
---------- -----
1 Trump
2
在分組查詢中也認為 NULL 和 NULL 是相等的。示例:
WITH t AS(
SELECT 1 cid,'Trump' cname FROM DUAL
UNION ALL
SELECT 2 cid,NULL cname FROM DUAL
UNION ALL
SELECT 2 cid,NULL cname FROM DUAL
)
SELECT t.cname,COUNT(1) cnt FROM t GROUP BY t.cname ORDER BY cnt;
結果:
CNAME CNT
----- ----------
Trump 1
2
當排序遇上 NULL:排序子句 ORDER BY 預設是升序(ASC),這時候排序列值為 NULL 的行會排在最後,而如果指定了降序(DESC),NULL 則排在最前面。可以這麼來記這個規律——排序時 NULL 最大。當然,在 Oracle 中可以顯示的指定 NULL 的排序規則。在升序排列時需要 NULL 排在最前面,可以指定 NULLS FIRST
,在降序排列時需要 NULL 排在最後面,可以指定 NULLS LAST
。示例:
SELECT 1 cid,'Trump' cname FROM DUAL
UNION ALL
SELECT 2,'Hillary' FROM DUAL
UNION ALL
SELECT 3,NULL FROM DUAL
ORDER BY 2 NULLS FIRST;
結果:
CID CNAME
---------- -------
3
2 Hillary
1 Trump
當 UPDATE 遇上 NULL:在.Net程式員學用Oracle系列(12):增刪改查中已詳述,本節不再贅述。
4、總結
本文主要講述了 Oracle 中較為常用、又極其特殊的 3 個對象。其中 DUAL 和 ROWID 使用起來比較簡單;NULL 則不然,不僅它本身特殊,很多常見的語句、操作符、函數等遇上它也變得特殊,個中的細節實在太多,限於篇幅和本人的精力,本文也只介紹了相對常見的一些情況,更多奧妙還有待大家在學習和工作中去發現。
本文鏈接:http://www.cnblogs.com/hanzongze/p/Oracle-Dual-Null.html
版權聲明:本文為博客園博主 韓宗澤 原創,作者保留署名權!歡迎通過轉載、演繹或其它傳播方式來使用本文,但必須在明顯位置給出作者署名和本文鏈接!本人初寫博客,水平有限,若有不當之處,敬請批評指正,謝謝!