1. 程式人生 > >神奇的 SQL 之子查詢,細節滿滿 !

神奇的 SQL 之子查詢,細節滿滿 !

前言

  開心一刻

    有一天,麻雀遇見一隻烏鴉。

    麻雀問:你是啥子鳥喲 ?

    烏鴉說:我是鳳凰。

    麻雀說:哪有你龜兒子這麼黢黑的鳳凰 ?

    烏鴉說:你懂個剷剷,老子是燒鍋爐的鳳凰。

子查詢

  講子查詢之前,我們先來看看檢視,何謂檢視 ? 檢視是基於 SQL 語句的結果集的視覺化的表,包含行和列,就像一個真實的表,但只是一張虛擬表,我們可以將其視作為一張普通的表;檢視只供資料查詢,不能進行資料更改,也不能儲存資料,查詢資料來源於我們的實體表;說的簡單點,檢視就是複雜 SELECT 語句的一個代號,為查詢提供便利。檢視總是顯示最近的資料,每當我們查詢檢視時,資料庫引擎通過使用 SQL 語句來重建資料。

  那何謂子查詢,它與檢視又有何關係 ? 檢視是持久化的 SELECT 語句,而子查詢就是將定義檢視的 SELECT 語句直接用於 FROM 子句當中,它是個一次性的檢視,在 SELECT 語句執行完之後就會消失。光說概念,可能還是不太好理解,我們來看下檢視與子查詢的具體示例,通過示例我們就能更好的理解了

  假設我們有如下表

CREATE TABLE t_customer_credit (
    id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
    login_name VARCHAR(50) NOT NULL COMMENT '登入名',
    credit_type TINYINT(1) NOT NULL COMMENT '額度型別,1:自由資金,2:凍結資金,3:優惠',
    amount DECIMAL(22,6) NOT NULL DEFAULT '0.00000' COMMENT '額度值',
    create_by VARCHAR(50) NOT NULL COMMENT '建立者',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '建立時間',
    update_by VARCHAR(50) NOT NULL COMMENT '修改者',
  PRIMARY KEY (id)
);
INSERT INTO `t_customer_credit` VALUES (1, 'zhangsan', 1, 550.000000, 'system', '2019-7-7 11:30:09', '2019-7-8 20:21:05', 'system');
INSERT INTO `t_customer_credit` VALUES (2, 'zhangsan', 2, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');
INSERT INTO `t_customer_credit` VALUES (3, 'zhangsan', 3, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');
INSERT INTO `t_customer_credit` VALUES (4, 'lisi', 1, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');
INSERT INTO `t_customer_credit` VALUES (5, 'lisi', 2, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');
INSERT INTO `t_customer_credit` VALUES (6, 'lisi', 3, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');
View Code

  以及如下 3 個檢視

-- 自由資金
DROP VIEW IF EXISTS view_free;
CREATE VIEW view_free(login_name, freeAmount) AS 
SELECT login_name, amount
FROM t_customer_credit
WHERE credit_type = 1;

-- 凍結資金
DROP VIEW IF EXISTS view_freeze;
CREATE VIEW view_freeze(login_name, freezeAmount) AS 
SELECT login_name, amount
FROM t_customer_credit
WHERE credit_type = 2;

-- 優惠
DROP VIEW IF EXISTS view_promotion;
CREATE VIEW view_promotion(login_name, promotionAmount) AS 
SELECT login_name, amount
FROM t_customer_credit
WHERE credit_type = 3;
View Code

  那麼我們可以用如下 SQL 來顯示使用者的三個額度

SELECT v1.login_name, v1.free_amount, v2.freeze_amount, v3.promotion_amount FROM
view_free v1 
LEFT JOIN view_freeze v2 ON v1.login_name = v2.login_name
LEFT JOIN view_promotion v3 ON v1.login_name = v3.login_name;

  換成子查詢的方式,SQL 如下

SELECT free.login_name, free.freeAmount, freeze.freezeAmount, promotion.promotionAmount
FROM 
(
    SELECT login_name, amount freeAmount
    FROM t_customer_credit
    WHERE credit_type = 1
) free 
LEFT JOIN
(
    SELECT login_name, amount freezeAmount
    FROM t_customer_credit
    WHERE credit_type = 2
) freeze ON free.login_name = freeze.login_name
LEFT JOIN
(
    SELECT login_name, amount promotionAmount
    FROM t_customer_credit
    WHERE credit_type = 3
) promotion ON free.login_name = promotion.login_name;
View Code

  注意 SQL 的執行順序,子查詢作為內層查詢會首先執行;原則上子查詢必須設定名稱,所以我們儘量從處理內容的角度出發為子查詢設定一個恰當的名稱

普通子查詢

  上面講到的子查詢就是普通子查詢,非要給個定義的話,就是返回多行結果的子查詢。這個在實際應用中還是用的非常多的,這個相信大家都比較熟悉,不做過多的說明,只舉個簡單例子

  假設我們有商品表:t_commodity

DROP TABLE IF EXISTS t_commodity;
CREATE TABLE t_commodity (
    id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
    serial_number VARCHAR(32) NOT NULL COMMENT '編號',
    name VARCHAR(50) NOT NULL COMMENT '名稱',
    category VARCHAR(50) NOT NULL COMMENT '類別',
    sell_unit_price DECIMAL(22,6) NOT NULL COMMENT '出售單價',
    purchase_unit_price DECIMAL(22,6) NOT NULL COMMENT '進貨單價',
    create_time DATETIME NOT NULL COMMENT '建立時間',
    update_time DATETIME NOT NULL COMMENT '更新時間',
    primary key(id)
) COMMENT '商品表';

-- 初始資料
INSERT INTO t_commodity(serial_number, name, category, sell_unit_price, purchase_unit_price, create_time, update_time)
VALUES
('0001', 'T恤衫', '衣服', '80', '20', NOW(), NOW()),
('0002', '羽絨服', '衣服', '280.5', '100', NOW(), NOW()),
('0003', '休閒褲', '褲子', '50', '15.5', NOW(), NOW()),
('0004', '運動短褲', '褲子', '30', '10', NOW(), NOW()),
('0005', '菜刀', '廚具', '35', '10', NOW(), NOW()),
('0006', '鍋鏟', '廚具', '15', '6.5', NOW(), NOW()),
('0007', '鍋', '廚具', '60', '20', NOW(), NOW()),
('0008', '電飯煲', '廚具', '240', '70', NOW(), NOW()),
('0009', '打孔器', '辦公用品', '50', '10.5', NOW(), NOW()),
('0010', '檔案架', '辦公用品', '35', '13', NOW(), NOW()),
('0011', '辦公桌', '辦公用品', '280', '120', NOW(), NOW()),
('0012', '辦公椅', '辦公用品', '256', '100', NOW(), NOW());
View Code

  現在我們要實現如下要求:統計出各個類別下商品的數量,我們可以寫出如下 SQL

-- 最容易想到的 GROUP BY
SELECT category, COUNT(*) cnt FROM t_commodity
GROUP BY category

-- 子查詢實現,這裡貌似多此一舉,權且當做一個示例
-- 普通子查詢一般應用於多表之間的查詢,比如 學生表、課程表、選課表 之間的一些查詢
SELECT * FROM
(
    SELECT category, COUNT(*) cnt FROM t_commodity
    GROUP BY category
) cs;
View Code

標量子查詢

  普通子查詢一般是返回多行結果(偶爾也會只返回 1 行,有時也會查不到結果);當返回結果是 1 行 1 列時,該子查詢被稱作標量子查詢,標量子查詢有個特殊的限制,必須而且只能返回 1 行 1 列的結果。

  說的簡單點:標量子查詢就是返回單一值的子查詢。由於返回值是單一值,所以標量子查詢可以用在 = 或 <> 這樣需要單一值的比較運算子之中,這也正是其優勢所在。我們來看一些簡單的例子,還是以 t_commodity 為例,假設我們有如下需求,我們該如何實現它

    1、查詢出售單價高於平均出售單價的商品

    2、查詢所有商品資訊,並在每個商品的資訊中加入平均出售單價、平均進貨單價

    3、按照商品類別分類,查詢出平均出售單價高於全部商品的平均出售單價的商品類別(類別名、類別平均出售單價)

  查詢 1

    第一感覺,我們也許會寫出如下的 SQL

-- 錯誤的 SQL
SELECT * FROM t_commodity
WHERE sell_unit_price > AVG(sell_unit_price);

    實際上這個 SQL 執行會報錯,WHERE 子句中不能使用聚合函式。那這個查詢要怎麼寫了,此時標量子查詢就派上用場了,SQL 如下

-- 查詢出售單價高於平均出售單價的商品
SELECT * FROM t_commodity
WHERE sell_unit_price > (
    SELECT AVG(sell_unit_price) 
    FROM t_commodity
);

  查詢 2

    這個 SQL 應該比較容易想到,SELECT 子句中加入 平均出售單價、平均進貨單價 列即可,如下

-- 查詢所有商品資訊,並在每個商品的資訊中加入平均出售單價、平均進貨單價
SELECT *,
    (SELECT AVG(sell_unit_price) FROM t_commodity) avg_sell_price,
    (SELECT AVG(purchase_unit_price) FROM t_commodity) avg_purchase_price
FROM t_commodity;

  查詢 3

    先以類別進行分組,然後取分組後各個類別的平均出售價格,與全部商品的平均出售價格比較,過濾出滿足條件的類別,SQL 如下

-- 按照商品類別分類,查詢出平均出售單價高於全部商品的平均出售單價的商品類別(類別名、類別平均出售單價)
SELECT category, AVG(sell_unit_price) category_avg_sell_price
FROM t_commodity
GROUP BY category
HAVING AVG(sell_unit_price) > (
    SELECT AVG(sell_unit_price) 
    FROM t_commodity
)

  使用標量子查詢時,我們需要注意一點:我們要明確的知道該子查詢返回的結果就是單一值,絕對不能返回多行結果。不然執行會報錯

關聯子查詢

  關聯子查詢是指一個包含對錶的引用的子查詢,該表也顯示在外部查詢中。通俗一點來講,就是子查詢引用到了主查詢的資料資料。在關聯子查詢中,對於外部查詢返回的每一行資料,內部查詢都要執行一次。另外,在關聯子查詢中是資訊流是雙向的,外部查詢的每行資料傳遞一個值給子查詢,然後子查詢為每一行資料執行一次並返回它的記錄。然後,外部查詢根據返回的記錄做出決策。光看概念,晦澀難懂,我們結合具體的例子來看關聯子查詢

  還是以商品表:t_commodity  為例,如何選取出各商品類別中高於該類別平均出售價格的商品,可能大家還沒明白這個需求,那麼我們具體點

    所有商品的類別、出售價格如下

    各類別及類別平均出售價格如下

     我們得到的正確結果應該是

  這個 SQL 我們要如何寫? 像這樣

-- 錯誤的 SQL
SELECT * FROM t_commodity
WHERE sell_unit_price > (
    SELECT AVG(sell_unit_price) FROM t_commodity
    GROUP BY category
)

  是肯定不行的,那正確的開啟方式應該是怎麼樣的了,此時需要關聯子查詢上場了,SQL如下

SELECT * FROM t_commodity t1
WHERE sell_unit_price > (
    SELECT AVG(sell_unit_price) FROM t_commodity t2
    WHERE t1.category = t2.category
    GROUP BY category
)

   子查詢中的 WHERE 子句(WHERE t1.category = t2.category) 至關重要,它的作用是在同一商品類別中對各商品的出售單價與平均單價進行比較。在對錶中某一部分記錄的集合進行比較時,就可以使用關聯子查詢,當出現 “限定” 或 “限制” 這樣的詞彙時,通常會使用關聯子查詢。

  在關聯子查詢中,對於外部查詢返回的每一行資料,內部查詢都要執行一次,DBMS 內部的執行結果類似如下

總結

  1、SQL 執行順序

(8) SELECT  (9) DISTINCT (11) <TOP_specification> <select_list>
(1)  FROM <left_table>
(3) <join_type> JOIN <right_table>
(2) ON <join_condition>
(4) WHERE <where_condition>
(5) GROUP BY <group_by_list>
(6) WITH {CUBE | ROLLUP}
(7) HAVING <having_condition>
(10) ORDER BY <order_by_list>

  2、書寫位置

    子查詢可以在 SELECT、INSERT、UPDATE 和 DELETE 語句中,同 =、<、>、>=、<=、IN、BETWEEN 等運算子一起使用,使用起來也是非常靈活的;標量子查詢出現的位置就更靈活了,並不僅僅侷限於 WHERE 子句中,通常任何可以使用單一值的位置都可以使用,SELECE 子句、GROUP BY 子句、HAVING 子句、ORDER BY 子句,也就是說,能夠使用常量或者列名的地方,都可以使用標量子查詢。

  3、效率問題

    子查詢的效率一直都是一個比較頭疼的問題,加合適的索引能改善效率,但也只是侷限於很少的情況;如果資料量大,對效能要求又高,能不用子查詢就儘量別用子查詢,儘量用其他的方式替代,很多情況下,子查詢可以用關聯查詢來替代

參考

  《SQL基礎教程》

  《SQL進階教程》