【AI大数据工程师特训笔记】第06讲:子查询&视图
目录
第一章 子查询基本概念
1.1 什么是子查询
子查询 就是在一个 SQL 语句中嵌套另一个 SELECT 语句。执行时,数据库会先执行内层的子查询,将结果返回给外层主查询使用。
零基础解释:就像你查东西要先查一个中间结果。比如“查询交易额高于平均交易额的客户”,你需要先算出“平均交易额”这个中间值,然后再用它去比较。这个中间值就可以通过子查询获得。
1.2 为什么需要子查询
-
数据无法直接获取:需要的数据不能直接拿到,必须通过查询计算后才能得到
-
分步思考:把复杂问题拆解成多个查询步骤,逻辑更清晰
-
动态条件:条件值不是固定的,而是依赖于表中其他数据
1.3 子查询分类(按返回形态)
|
类别 |
返回内容 |
常用运算符 |
记忆示例 |
|---|---|---|---|
|
标量子查询 |
1 行 1 列(单个值) |
|
|
|
多行子查询 |
N 行 1 列(一列值列表) |
|
|
|
多列子查询 |
N 行 N 列(多列组合) |
|
|
|
存在性子查询 |
布尔值(是否至少一行) |
|
|
|
派生表子查询 |
多行多列(临时表) |
|
|
1.3.1 按使用位置分类
|
位置 |
用途 |
子查询类型 |
|---|---|---|
|
|
过滤行 |
标量、多行、存在性 |
|
|
生成临时表(派生表) |
派生表 |
|
|
计算列(标量值) |
标量子查询 |
|
|
分组后过滤 |
标量子查询 |
1.3.2 记忆口诀
“标多存派”——标量、多行、存在性、派生表,四种子查询类型
第二章 证券行业案例:数据表设计
为了演示子查询和视图,我们创建一套证券交易业务的数据模型。
-- 创建证券业务 schema
CREATE SCHEMA IF NOT EXISTS securities;
-- 1. 客户表 (clients)
CREATE TABLE securities.clients (
client_id INTEGER PRIMARY KEY, -- 客户编号
full_name VARCHAR(50) NOT NULL, -- 客户姓名
id_card VARCHAR(18) UNIQUE, -- 身份证号
phone VARCHAR(20), -- 联系电话
risk_level VARCHAR(10) DEFAULT '平衡型', -- 风险等级:保守型/平衡型/进取型
open_date DATE DEFAULT CURRENT_DATE
);
-- 2. 证券公司/营业部表 (brokerages)
CREATE TABLE securities.brokerages (
brokerage_id INTEGER PRIMARY KEY, -- 券商编号
brokerage_name VARCHAR(50) NOT NULL, -- 券商名称
city VARCHAR(30) -- 所在城市
);
-- 3. 证券账户表 (accounts)
CREATE TABLE securities.accounts (
account_id INTEGER PRIMARY KEY, -- 资金账户号
client_id INTEGER NOT NULL, -- 所属客户编号
brokerage_id INTEGER NOT NULL, -- 开户券商编号
account_type VARCHAR(20) DEFAULT '普通账户', -- 普通账户/两融账户
balance NUMERIC(12,2) DEFAULT 0.00, -- 资金余额(元)
frozen_amount NUMERIC(12,2) DEFAULT 0.00, -- 冻结金额
status VARCHAR(10) DEFAULT '正常', -- 正常/冻结/销户
FOREIGN KEY (client_id) REFERENCES securities.clients(client_id),
FOREIGN KEY (brokerage_id) REFERENCES securities.brokerages(brokerage_id)
);
-- 4. 股票信息表 (stocks)
CREATE TABLE securities.stocks (
stock_code VARCHAR(10) PRIMARY KEY, -- 股票代码(如 '000001')
stock_name VARCHAR(30) NOT NULL, -- 股票名称
industry VARCHAR(30), -- 所属行业
list_date DATE -- 上市日期
);
-- 5. 交易记录表 (transactions)
CREATE TABLE securities.transactions (
trans_id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL, -- 交易账户
stock_code VARCHAR(10) NOT NULL, -- 股票代码
trans_type VARCHAR(4) NOT NULL, -- 买入/卖出
quantity INTEGER NOT NULL, -- 成交数量(股)
price NUMERIC(10,2) NOT NULL, -- 成交价格(元/股)
trans_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
commission NUMERIC(10,2), -- 佣金
FOREIGN KEY (account_id) REFERENCES securities.accounts(account_id),
FOREIGN KEY (stock_code) REFERENCES securities.stocks(stock_code)
);
-- 6. 每日持仓快照表(用于演示聚合子查询)
CREATE TABLE securities.daily_positions (
id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL,
stock_code VARCHAR(10) NOT NULL,
hold_quantity INTEGER NOT NULL, -- 持仓数量
snapshot_date DATE DEFAULT CURRENT_DATE,
FOREIGN KEY (account_id) REFERENCES securities.accounts(account_id)
);
-- 插入券商数据
INSERT INTO securities.brokerages VALUES (1, '中信证券', '北京');
INSERT INTO securities.brokerages VALUES (2, '华泰证券', '上海');
INSERT INTO securities.brokerages VALUES (3, '国泰君安', '深圳');
-- 插入客户数据
INSERT INTO securities.clients VALUES (101, '张三', '1101011XXXX001011234', '13800001111', '进取型', '2020-01-10');
INSERT INTO securities.clients VALUES (102, '李四', '310101XXXX001023j456', '13912345678', '平衡型', '2021-03-15');
INSERT INTO securities.clients VALUES (103, '王芳', '440301XXXX0010h36789', '13687654321', '保守型', '2019-11-20');
INSERT INTO securities.clients VALUES (104, '赵雷', '510101XXXX00104j8888', '15900000000', '进取型', '2023-05-01');
INSERT INTO securities.clients VALUES (105, '孙梅', '4201061XXXX001105555', '17711112222', '平衡型', '2022-08-08');
-- 插入证券账户
INSERT INTO securities.accounts VALUES (1001, 101, 1, '普通账户', 500000.00, 0, '正常');
INSERT INTO securities.accounts VALUES (1002, 101, 1, '两融账户', 200000.00, 0, '正常');
INSERT INTO securities.accounts VALUES (1003, 102, 2, '普通账户', 80000.00, 0, '正常');
INSERT INTO securities.accounts VALUES (1004, 103, 3, '普通账户', 2000000.00, 0, '正常');
INSERT INTO securities.accounts VALUES (1005, 103, 3, '两融账户', 500000.00, 0, '正常');
INSERT INTO securities.accounts VALUES (1006, 104, 2, '普通账户', 10000.00, 0, '冻结');
-- 客户105(孙梅)无账户,用于演示外连接
-- 插入股票信息
INSERT INTO securities.stocks VALUES ('000001', '平安银行', '银行', '1991-04-03');
INSERT INTO securities.stocks VALUES ('600519', '贵州茅台', '白酒', '2001-08-27');
INSERT INTO securities.stocks VALUES ('000858', '五粮液', '白酒', '1998-04-27');
INSERT INTO securities.stocks VALUES ('300750', '宁德时代', '新能源', '2018-06-11');
-- 插入交易记录
INSERT INTO securities.transactions VALUES (5001, 1001, '600519', '买入', 100, 1800.00, '2025-01-10 09:30:00', 50.00);
INSERT INTO securities.transactions VALUES (5002, 1001, '600519', '买入', 100, 1850.00, '2025-01-15 14:20:00', 50.00);
INSERT INTO securities.transactions VALUES (5003, 1003, '000001', '买入', 1000, 12.50, '2025-02-01 11:00:00', 10.00);
INSERT INTO securities.transactions VALUES (5004, 1004, '300750', '买入', 500, 200.00, '2025-02-10 16:00:00', 100.00);
INSERT INTO securities.transactions VALUES (5005, 1004, '300750', '卖出', 200, 210.00, '2025-02-14 19:30:00', 42.00);
INSERT INTO securities.transactions VALUES (5006, 1005, '000858', '买入', 300, 150.00, '2025-02-18 10:00:00', 30.00);
INSERT INTO securities.transactions VALUES (5007, 1001, '000001', '买入', 2000, 11.80, '2025-03-01 13:00:00', 20.00);
-- 插入每日持仓快照
INSERT INTO securities.daily_positions VALUES (1, 1001, '600519', 200, '2025-03-31');
INSERT INTO securities.daily_positions VALUES (2, 1001, '000001', 2000, '2025-03-31');
INSERT INTO securities.daily_positions VALUES (3, 1003, '000001', 1000, '2025-03-31');
INSERT INTO securities.daily_positions VALUES (4, 1004, '300750', 300, '2025-03-31');
INSERT INTO securities.daily_positions VALUES (5, 1005, '000858', 300, '2025-03-31');
第三章 子查询实战案例(证券业务)
3.1 标量子查询(单个值)
场景:查询交易金额高于平均交易金额的交易记录。
-- 查询单笔成交金额超过所有交易平均金额的交易
SELECT trans_id
,account_id
,stock_code
,quantity * price AS turnover
FROM securities.transactions
WHERE quantity * price > (SELECT AVG(quantity * price) FROM securities.transactions);
3.2 多行子查询 + IN
场景:查询在“中信证券”开户的客户姓名。
SELECT full_name
FROM securities.clients
WHERE client_id IN (
SELECT client_id
FROM securities.accounts
WHERE brokerage_id = (SELECT brokerage_id FROM securities.brokerages WHERE brokerage_name = '中信证券')
);
3.3 存在性子查询 EXISTS
场景:查询发生过交易(买入或卖出)的所有客户。
SELECT c.full_name
FROM securities.clients c
WHERE EXISTS (
SELECT 1
FROM securities.accounts a
JOIN securities.transactions t ON a.account_id = t.account_id
WHERE a.client_id = c.client_id
);
3.4 相关子查询
场景:查询交易金额高于自己账户平均交易金额的交易记录(相关子查询)。
SELECT t1.trans_id
,t1.account_id
,t1.stock_code
,t1.quantity * t1.price AS turnover
FROM securities.transactions t1
WHERE t1.quantity * t1.price > (
SELECT AVG(t2.quantity * t2.price)
FROM securities.transactions t2
WHERE t2.account_id = t1.account_id -- 关联到外层账户
);
3.5 ALL / ANY 子查询
场景:查询交易金额高于任意一笔贵州茅台(600519)交易的交易记录(即比茅台的最小成交额高即可)。
SELECT trans_id
,account_id
,quantity * price AS turnover
FROM securities.transactions
WHERE quantity * price > ANY (
SELECT quantity * price
FROM securities.transactions
WHERE stock_code = '600519'
);
3.6 派生表(FROM 子查询)
场景:查询每个账户的总交易金额,并只显示总交易金额高于 50000 的账户。
SELECT account_id
,total_turnover
FROM (
SELECT account_id
,SUM(quantity * price) AS total_turnover
FROM securities.transactions
GROUP BY account_id
) AS account_summary
WHERE total_turnover > 50000;
3.7 HAVING + 子查询
场景:查询平均交易金额高于全公司平均交易金额的账户。
SELECT account_id
,AVG(quantity * price) AS avg_turnover
FROM securities.transactions
GROUP BY account_id
HAVING AVG(quantity * price) > (SELECT AVG(quantity * price) FROM securities.transactions);
3.8 LATERAL 子查询(扩展内容)
LATERAL 子查询允许子查询引用它同一 FROM 子句中出现在它前面的表的列,相当于为外层查询的每一行都执行一次子查询。它的核心价值在于解决“派生表无法引用外层列”的限制:例如要查询“每个客户最近的一笔交易”,普通子查询很难做到,但用 LATERAL 可以先取客户,然后对每个客户执行 ORDER BY trans_time LIMIT 1 的子查询,从而为每个客户动态计算独立结果。简单理解:LATERAL 让子查询不再是静态的独立查询,而是能跟随主查询每一行的“随行查询”。
场景:为每个客户查询其最近的一次交易。
SELECT c.full_name
,latest.*
FROM securities.clients c
LEFT JOIN LATERAL (
SELECT t.trans_id
,t.stock_code
,t.quantity * t.price AS turnover
FROM securities.accounts a
inner JOIN securities.transactions t
ON a.account_id = t.account_id
where a.client_id = c.client_id
ORDER BY t.trans_time DESC
LIMIT 1
) latest ON true;
3.9 递归 CTE (WITH RECURSIVE) 扩展内容
递归 CTE 常用于遍历树形结构(如组织架构、产品分类)。在证券行业中,可以用于查找“同一控制人下的所有账户”。
-- 假设有一个控制关系表(非实际业务,仅演示语法)
-- 查找某一客户控制的所有关联账户
WITH RECURSIVE related_accounts AS (
-- 起点
SELECT a.account_id, a.client_id
FROM securities.accounts a
WHERE a.client_id = 101
UNION
-- 递归部分:通过某种关系继续寻找(此处为简化示例)
SELECT a.account_id, a.client_id
FROM securities.accounts a
JOIN related_accounts r ON a.client_id = r.client_id -- 实际需要业务关系字段
)
SELECT * FROM related_accounts;
第四章 WITH (CTE) 临时命名查询
4.1 CTE 是什么
CTE (Common Table Expression) 官方名称为“公共表表达式”,俗称 “WITH 虚拟视图” 或 “一次性的临时表”。
-
作用:把复杂的
SELECT片段起个名字,后面直接当表用 -
生命周期:只存在当前查询,运行结束自动消失
-
优点:
-
逻辑分层,易读易维护
-
可以递归(遍历树/图)
-
避免重复嵌套,提高可读性
-
4.2 基本语法
WITH 临时表名 AS (
SELECT 列1, 列2, ...
FROM 表名
WHERE 条件
)
SELECT * FROM 临时表名;
执行顺序:先执行 WITH 里面的 SELECT,再执行外面的 SELECT。
4.3 单 CTE 示例
场景:使用 CTE 查询交易金额高于平均值的交易。
WITH avg_turnover AS (
SELECT AVG(quantity * price) AS avg_val
FROM securities.transactions
)
SELECT trans_id
,account_id
,quantity * price AS turnover
FROM securities.transactions, avg_turnover
WHERE quantity * price > avg_turnover.avg_val;
4.4 多 CTE 示例(链式引用)
场景:先计算每个账户的交易总额,再计算每个账户与账户平均值的差额。
WITH account_total AS (
SELECT account_id
,SUM(quantity * price) AS total_turnover
FROM securities.transactions
GROUP BY account_id
),
overall_avg AS (
SELECT AVG(total_turnover) AS avg_turnover
FROM account_total
)
SELECT account_id
,total_turnover
,total_turnover - avg_turnover AS diff
FROM account_total, overall_avg
ORDER BY diff DESC;
4.5 CTE vs 派生表 vs 临时表
|
维度 |
CTE (WITH) |
派生表 (FROM 子查询) |
临时表 (TEMP TABLE) |
|---|---|---|---|
|
生命周期 |
当前语句 |
当前语句 |
会话/事务 |
|
是否存真实数据 |
❌ 不存储 |
❌ 不存储 |
✅ 存储 |
|
可递归 |
✅ 支持 |
❌ 不支持 |
❌ 不支持 |
|
多次引用 |
⚠️ 每次重新计算 |
— |
✅ 可复用 |
|
适用场景 |
分层逻辑、递归 |
简单子查询 |
中间结果复用 |
性能贴士:在 PostgreSQL 中,同一 SQL 里多次引用同一 CTE,会重新计算多次。大数据量时建议改用临时表或物化视图。
第五章 视图
5.1 什么是视图
视图(View) 是一条保存下来的 SELECT 语句。可以把它理解为给复杂查询起一个永久的名字,之后可以像普通表一样查询。
-
不存储真实数据,只存储查询定义(逻辑表)
-
视图的数据来源于基表(创建视图时依赖的表)
-
对视图执行 DML 操作(增删改)实际上会影响基表(受限于视图规则)
5.2 为什么需要视图
-
简化复杂 SQL:把多表关联封装成视图,用户直接
SELECT * FROM 视图 -
统一口径:确保所有人用同一个统计规则
-
屏蔽敏感字段:隐藏工资、身份证号等敏感列
-
兼容旧程序:底层表结构调整后,通过视图保持原接口不变
5.3 视图 vs CTE vs 临时表
|
维度 |
普通视图 |
CTE (WITH) |
临时表 |
|---|---|---|---|
|
生命周期 |
永久 |
当前语句 |
会话/事务 |
|
是否存真实数据 |
❌ |
❌ |
✅ |
|
可更新(DML) |
✅(简单视图) |
❌ |
✅ |
|
递归能力 |
❌ |
✅ |
❌ |
口诀:“封装用视图,计算用临时,分层/树用 WITH”
5.4 创建视图(基础语法)
CREATE VIEW 视图名 AS
SELECT 列1, 列2, ...
FROM 表名
WHERE 条件;
5.5 证券行业视图示例
示例 1:简化复杂查询
场景:创建视图封装“账户-客户-券商”四表关联,后续直接使用。
CREATE VIEW securities.v_account_full AS
SELECT a.account_id
,c.full_name AS client_name
,c.risk_level
,b.brokerage_name
,a.balance
,a.status
FROM securities.accounts a
inner JOIN securities.clients c
ON a.client_id = c.client_id
inner JOIN securities.brokerages b
ON a.brokerage_id = b.brokerage_id;
-- 使用视图:像普通表一样查询
SELECT *
FROM securities.v_account_full
WHERE balance > 100000;
示例 2:屏蔽敏感字段
场景:对外提供客户信息视图,隐藏身份证号、手机号。
CREATE VIEW securities.v_clients_public AS
SELECT client_id
,full_name
,risk_level
,open_date
FROM securities.clients;
示例 3:统一统计口径
场景:创建部门/券商维度的统计报表视图。
CREATE VIEW securities.v_brokerage_stats AS
SELECT b.brokerage_name
,COUNT(DISTINCT a.client_id) AS client_count
,COUNT(a.account_id) AS account_count
,SUM(a.balance) AS total_balance
,AVG(a.balance) AS avg_balance
FROM securities.brokerages b
LEFT JOIN securities.accounts a
ON b.brokerage_id = a.brokerage_id
GROUP BY b.brokerage_id, b.brokerage_name;
5.6 修改和删除视图
-- 替换视图(覆盖定义)
CREATE OR REPLACE VIEW securities.v_clients_public AS
SELECT client_id
,full_name
,risk_level -- 去掉了 open_date
FROM securities.clients;
-- 删除视图
DROP VIEW IF EXISTS securities.v_clients_public;
5.7 可更新视图与 CHECK OPTION
简单视图(基于单表,不包含聚合、DISTINCT、GROUP BY 等)支持 DML 操作。
-- 重建视图,包含所有 NOT NULL 字段
CREATE OR REPLACE VIEW securities.v_active_accounts AS
SELECT
account_id,
client_id,
brokerage_id, -- ✅ 加上这个
account_type, -- ✅ 如果有默认值,可加可不加
balance,
status
FROM securities.accounts
WHERE status = '正常'
WITH CHECK OPTION; -- 防止插入/更新后不满足视图条件
-- 插入时需要提供 brokerage_id
INSERT INTO securities.v_active_accounts
(account_id, client_id, brokerage_id, balance, status)
VALUES (2001, 101, 1, 10000, '正常'); -- brokerage_id=1 对应中信证券
-- 以下插入会失败,因为 status='冻结' 不满足视图条件(被 CHECK OPTION 拦截)
INSERT INTO securities.v_active_accounts (account_id, client_id, balance, status)
VALUES (2002, 102,2, 5000, '冻结');
注意:通过视图插入数据,会将数据插入到基表中。
5.8 物化视图(性能优化)
物化视图(Materialized View) 会真正存储数据,适合报表类场景,但需要手动刷新。
-- 创建物化视图
CREATE MATERIALIZED VIEW securities.mv_daily_position_summary AS
SELECT snapshot_date
,stock_code
,SUM(hold_quantity) AS total_hold
,COUNT(DISTINCT account_id) AS holder_count
FROM securities.daily_positions
GROUP BY snapshot_date, stock_code;
-- 刷新物化视图(全量刷新)
REFRESH MATERIALIZED VIEW securities.mv_daily_position_summary;
-- 使用物化视图
SELECT * FROM securities.mv_daily_position_summary;
注意:物化视图刷新前数据不会变,适合对实时性要求不高的报表场景。PostgreSQL 还支持
REFRESH MATERIALIZED VIEW CONCURRENTLY并发刷新,但需要唯一索引。
第六章 临时表(TEMP TABLE)
6.1 为什么需要临时表
-
中间结果存储:多步复杂报表,避免反复扫描大表
-
会话隔离:不同连接互不干扰,天然“线程私有”
-
自动清理:会话或事务结束自动回收
-
降低锁竞争:将热点操作转移到临时表,减少主表锁等待
6.2 生命周期与可见性
|
模式 |
关键字 |
生命周期 |
可见范围 |
|---|---|---|---|
|
会话级(默认) |
|
会话结束 |
仅当前会话 |
|
事务级 |
|
事务结束 |
仅当前事务 |
6.3 基本语法
-- 最简形式
CREATE TEMP TABLE tmp_stock_list (stock_code VARCHAR(10), hold_quantity INTEGER);
-- 带约束和索引
CREATE TEMP TABLE tmp_trade_summary (
account_id INTEGER PRIMARY KEY,
total_amount NUMERIC(12,2) NOT NULL,
trade_count INTEGER
);
CREATE INDEX tmp_idx_account ON tmp_trade_summary(account_id);
-- 从查询结果直接创建临时表
SELECT account_id
,SUM(quantity * price) AS total_turnover
INTO TEMP TABLE tmp_account_turnover
FROM securities.transactions
GROUP BY account_id;
-- 查询临时结果表数据
select * from tmp_account_turnover;
6.4 事务级临时表
BEGIN;
CREATE TEMP TABLE tmp_tx (id INT) ON COMMIT DROP;
-- 在事务内使用临时表
INSERT INTO tmp_tx VALUES (1);
COMMIT; -- 事务结束后临时表自动删除
6.5 查看和删除临时表
-- 查看当前会话所有临时表
SELECT schemaname
,tablename
FROM pg_tables
WHERE schemaname LIKE 'pg_temp%';
-- 手动删除
DROP TABLE IF EXISTS tmp_stock_list;
6.6 性能最佳实践
-
命名规范:统一前缀
tmp_,一眼识别 -
连接池友好:使用
ON COMMIT DROP或手动DROP,避免临时表在长连接中累积 -
大数据量写入前:可设置
ALTER TABLE tmp SET UNLOGGED;减少 WAL 日志压力 -
手动分析:临时表不会被
AUTOVACUUM扫描,频繁更新后应手动ANALYZE tmp_table -
内存配置:合理设置
temp_buffers(默认 8MB,可调大减少磁盘 I/O)
第七章 总结
7.1 子查询口诀
“单行用比较,多行用集合”
“标多存派,位置不同”
|
子查询类型 |
返回形态 |
常用位置 |
典型运算符 |
|---|---|---|---|
|
标量子查询 |
1 值 |
WHERE/SELECT/HAVING |
|
|
多行子查询 |
1 列多行 |
WHERE |
|
|
存在性子查询 |
布尔值 |
WHERE |
|
|
派生表 |
多行多列 |
FROM |
— |
7.2 CTE 与视图选择指南
|
需求 |
推荐方案 |
|---|---|
|
复杂查询需要分层阅读 |
|
|
需要递归遍历(树形结构) |
|
|
需要永久封装、复用 |
普通视图 |
|
需要屏蔽敏感字段 |
普通视图 |
|
需要加速报表查询 |
物化视图 |
|
需要存储中间结果、多步骤复用 |
临时表 |
更多推荐

所有评论(0)