PostgreSQL 多表查询
多表查询的核心,是根据表与表之间的关系,把不同表中的行组合成新的结果集。
例如:
股票基本信息 + 每日行情 + 公司资料 + 题材概念
PostgreSQL 主要通过以下方式实现:
JOIN:横向组合字段- 子查询:使用另一个查询的结果
EXISTS:判断关联记录是否存在LATERAL:为左表每行执行关联子查询UNION:纵向合并多组结果- CTE:拆分复杂查询步骤
官方教程见 PostgreSQL 18 多表连接。
本文使用五张表:
erDiagram
SECURITY ||--o| SECURITY_PROFILE : "一对一"
SECURITY ||--o{ DAILY_QUOTE : "一对多"
SECURITY ||--o{ SECURITY_CONCEPT : "关联"
CONCEPT ||--o{ SECURITY_CONCEPT : "关联"
SECURITY {
bigint security_id PK
varchar code
text name
}
SECURITY_PROFILE {
bigint security_id PK,FK
text primary_business
}
DAILY_QUOTE {
bigint security_id PK,FK
date trade_date PK
numeric close_price
}
CONCEPT {
bigint concept_id PK
text name
}
SECURITY_CONCEPT {
bigint security_id PK,FK
bigint concept_id PK,FK
}
关系类型:
| 关系 | 示例 |
|---|---|
| 一对一 | 一只股票对应一份公司资料 |
| 一对多 | 一只股票对应多天行情 |
| 多对多 | 一只股票属于多个题材,一个题材包含多只股票 |
SELECT
表1.字段,
表2.字段
FROM 表1
JOIN 表2
ON 表1.关联字段 = 表2.关联字段;
查询股票及其行情:
SELECT
s.code,
s.name,
q.trade_date,
q.close_price
FROM market.security AS s
JOIN market.daily_quote AS q
ON q.security_id = s.security_id;
这里:
s:股票基本信息q:日线行情security_id:两张表的关联字段- 一只股票有多条行情,所以股票名称会在结果中重复出现
多表查询中,建议始终使用表别名和限定字段名:
s.code
q.close_price
避免直接写:
code
close_price
这样即使以后多张表出现同名字段,SQL 仍然清晰。
| JOIN 类型 | 返回结果 |
|---|---|
INNER JOIN |
只返回左右两边匹配的记录 |
LEFT JOIN |
返回左表全部记录,以及匹配的右表记录 |
RIGHT JOIN |
返回右表全部记录,以及匹配的左表记录 |
FULL JOIN |
返回两边全部记录 |
CROSS JOIN |
返回左右表所有组合 |
Self Join |
同一张表与自己关联 |
SELECT
s.code,
s.name,
q.trade_date,
q.close_price,
q.change_rate
FROM market.security AS s
INNER JOIN market.daily_quote AS q
ON q.security_id = s.security_id
WHERE q.trade_date = DATE '2026-09-14'
ORDER BY q.change_rate DESC;
INNER JOIN 只返回:
股票表中存在
并且
行情表中也存在
如果某只股票在指定交易日没有行情,它不会出现在结果中。
INNER 可以省略:
JOIN market.daily_quote AS q
等价于:
INNER JOIN market.daily_quote AS q
查询全部正常股票,以及指定日期的行情:
SELECT
s.code,
s.name,
q.trade_date,
q.close_price,
q.change_rate
FROM market.security AS s
LEFT JOIN market.daily_quote AS q
ON q.security_id = s.security_id
AND q.trade_date = DATE '2026-09-14'
WHERE s.is_active IS TRUE
ORDER BY s.code;
结果包括:
- 当天有行情的股票
- 当天没有行情的股票
没有行情的股票对应字段为:
q.trade_date = NULL
q.close_price = NULL
q.change_rate = NULL
LEFT JOIN 适合:
- 查询所有股票,包括无行情股票
- 查询所有用户,包括无订单用户
- 查询所有题材,包括没有成分股的题材
- 查找缺失数据
下面两条语句看起来相似,结果却不同。
SELECT
s.code,
s.name,
q.close_price
FROM market.security AS s
LEFT JOIN market.daily_quote AS q
ON q.security_id = s.security_id
AND q.trade_date = DATE '2026-09-14';
日期条件位于 ON,表示:
只关联指定日期的行情,但仍保留左表中的所有股票。
SELECT
s.code,
s.name,
q.close_price
FROM market.security AS s
LEFT JOIN market.daily_quote AS q
ON q.security_id = s.security_id
WHERE q.trade_date = DATE '2026-09-14';
没有行情的股票,其 q.trade_date 为 NULL,会被 WHERE 过滤掉。
结果实际上接近:
INNER JOIN
原则:
- 表之间如何匹配:放在
ON - 最终结果如何筛选:放在
WHERE LEFT JOIN中需要保留左表未匹配行时,右表条件通常放在ON
PostgreSQL 官方文档明确说明,外连接中条件放在 ON 和 WHERE 会产生不同结果。JOIN 表达式
查询指定日期没有行情的股票:
SELECT
s.security_id,
s.code,
s.name
FROM market.security AS s
LEFT JOIN market.daily_quote AS q
ON q.security_id = s.security_id
AND q.trade_date = DATE '2026-09-14'
WHERE q.security_id IS NULL;
之所以检查:
q.security_id IS NULL
是因为该字段在真实行情记录中定义为 NOT NULL。
SELECT
s.security_id,
s.code,
s.name
FROM market.security AS s
WHERE NOT EXISTS (
SELECT 1
FROM market.daily_quote AS q
WHERE q.security_id = s.security_id
AND q.trade_date = DATE '2026-09-14'
);
NOT EXISTS 更直接地表达:
不存在满足条件的关联行情。
在只需要判断存在与否、不需要返回右表字段时,通常优先考虑 EXISTS 或 NOT EXISTS。
SELECT
s.code,
s.name,
i.name AS imported_name
FROM market.security AS s
RIGHT JOIN staging.security_import AS i
ON i.exchange_code = s.exchange_code
AND i.code = s.code;
它保留右表 security_import 的全部记录。
多数情况下可以交换表顺序,改写为更容易理解的 LEFT JOIN:
SELECT
s.code,
s.name,
i.name AS imported_name
FROM staging.security_import AS i
LEFT JOIN market.security AS s
ON s.exchange_code = i.exchange_code
AND s.code = i.code;
实际项目中 LEFT JOIN 通常比 RIGHT JOIN 更常用。
FULL JOIN 返回:
- 两边都匹配的数据
- 只存在于左表的数据
- 只存在于右表的数据
适合核对两个数据源。
SELECT
coalesce(s.exchange_code, i.exchange_code) AS exchange_code,
coalesce(s.code, i.code) AS code,
s.name AS database_name,
i.name AS imported_name,
CASE
WHEN s.security_id IS NULL
THEN '仅导入表存在'
WHEN i.code IS NULL
THEN '仅正式表存在'
WHEN s.name IS DISTINCT FROM i.name
THEN '数据不一致'
ELSE '数据一致'
END AS compare_status
FROM market.security AS s
FULL JOIN staging.security_import AS i
ON i.exchange_code = s.exchange_code
AND i.code = s.code
ORDER BY
exchange_code,
code;
这里使用:
IS DISTINCT FROM
可以正确比较包含 NULL 的字段。
使用 FULL JOIN 核对数据前,应保证两边业务键唯一。否则一边存在重复代码时,结果可能成倍增加。
SELECT *
FROM table_a
CROSS JOIN table_b;
如果:
table_a 有 100 行
table_b 有 200 行
结果为:
100 × 200 = 20,000 行
这就是笛卡尔积。
假设交易日历表:
market.trading_calendar
├── trade_date
└── is_open
生成“所有正常股票 × 所有交易日”:
SELECT
s.security_id,
s.code,
c.trade_date
FROM market.security AS s
CROSS JOIN market.trading_calendar AS c
WHERE s.is_active IS TRUE
AND c.is_open IS TRUE;
继续关联行情,查找缺失数据:
SELECT
s.code,
c.trade_date
FROM market.security AS s
CROSS JOIN market.trading_calendar AS c
LEFT JOIN market.daily_quote AS q
ON q.security_id = s.security_id
AND q.trade_date = c.trade_date
WHERE s.is_active IS TRUE
AND c.is_open IS TRUE
AND q.security_id IS NULL
ORDER BY
c.trade_date,
s.code;
交叉连接必须评估结果规模:
5000只股票 × 2500个交易日 = 1250万行
比较两个交易日的行情:
SELECT
s.code,
s.name,
previous.close_price AS previous_close,
current.close_price AS current_close,
current.close_price / nullif(previous.close_price, 0) - 1
AS calculated_change_rate
FROM market.daily_quote AS current
JOIN market.daily_quote AS previous
ON previous.security_id = current.security_id
AND previous.trade_date = DATE '2026-09-11'
JOIN market.security AS s
ON s.security_id = current.security_id
WHERE current.trade_date = DATE '2026-09-14'
ORDER BY calculated_change_rate DESC;
虽然 current 和 previous 都来自 daily_quote,但别名使它们成为两个不同的查询角色。
如果要自动寻找每条记录的上一个交易日,窗口函数 lag() 通常比自连接更合适。
最明确、最灵活:
JOIN market.daily_quote AS q
ON q.security_id = s.security_id
多字段业务键:
JOIN staging.security_import AS i
ON i.exchange_code = s.exchange_code
AND i.code = s.code
当两张表中的关联字段名称相同时:
SELECT
security_id,
s.code,
q.trade_date,
q.close_price
FROM market.security AS s
JOIN market.daily_quote AS q
USING (security_id);
它等价于:
ON s.security_id = q.security_id
区别是 USING 在结果中只保留一个 security_id。
多字段:
JOIN another_table
USING (exchange_code, code)
SELECT *
FROM market.security
NATURAL JOIN market.daily_quote;
NATURAL JOIN 会自动使用两张表中所有同名字段关联。
如果以后两张表都增加:
created_at
updated_at
查询关联条件可能在没有修改 SQL 的情况下改变。
如果两张表没有同名字段,NATURAL JOIN 甚至会表现得像 CROSS JOIN。因此生产代码应明确使用 ON 或 USING。
查询股票、公司资料和指定日期行情:
SELECT
s.code,
s.name,
s.industry,
p.company_feature,
p.primary_business,
q.trade_date,
q.close_price,
q.change_rate
FROM market.security AS s
LEFT JOIN market.security_profile AS p
ON p.security_id = s.security_id
LEFT JOIN market.daily_quote AS q
ON q.security_id = s.security_id
AND q.trade_date = DATE '2026-09-14'
WHERE s.is_active IS TRUE
ORDER BY s.code;
这里:
security是主表security_profile是可选的一对一资料daily_quote是指定日期的一条行情- 两个
LEFT JOIN保证资料或行情缺失时仍保留股票
股票与题材通过中间表关联:
security
↓
security_concept
↓
concept
查询“机器人”题材股票:
SELECT
s.security_id,
s.code,
s.name,
c.name AS concept_name
FROM market.security AS s
JOIN market.security_concept AS sc
ON sc.security_id = s.security_id
JOIN market.concept AS c
ON c.concept_id = sc.concept_id
WHERE c.name = '机器人'
AND s.is_active IS TRUE
ORDER BY s.code;
中间表负责保存:
security_id + concept_id
不要把题材保存成:
机器人,人工智能,算力
这种逗号分隔字符串不适合关系查询。
SELECT
s.code,
s.name,
c.name AS concept_name
FROM market.security AS s
JOIN market.security_concept AS sc
ON sc.security_id = s.security_id
JOIN market.concept AS c
ON c.concept_id = sc.concept_id
WHERE s.exchange_code = 'SSE'
AND s.code = '600519'
ORDER BY c.name;
SELECT
s.security_id,
s.code,
s.name,
string_agg(
c.name,
'、'
ORDER BY c.name
) AS concepts
FROM market.security AS s
LEFT JOIN market.security_concept AS sc
ON sc.security_id = s.security_id
LEFT JOIN market.concept AS c
ON c.concept_id = sc.concept_id
WHERE s.exchange_code = 'SSE'
AND s.code = '600519'
GROUP BY
s.security_id,
s.code,
s.name;
返回类似:
600519 | 贵州茅台 | 国企、白酒、消费
聚合为数组:
coalesce(
array_agg(
c.name
ORDER BY c.name
) FILTER (
WHERE c.concept_id IS NOT NULL
),
ARRAY[]::text[]
) AS concepts
查询同时属于“机器人”和“人工智能”的股票:
SELECT
s.security_id,
s.code,
s.name
FROM market.security AS s
JOIN market.security_concept AS sc
ON sc.security_id = s.security_id
JOIN market.concept AS c
ON c.concept_id = sc.concept_id
WHERE c.name IN ('机器人', '人工智能')
GROUP BY
s.security_id,
s.code,
s.name
HAVING count(DISTINCT c.name) = 2;
如果查询三个指定题材,应相应改为:
HAVING count(DISTINCT c.name) = 3
查询属于“机器人”题材的股票,但不需要返回题材字段:
SELECT
s.security_id,
s.code,
s.name
FROM market.security AS s
WHERE EXISTS (
SELECT 1
FROM market.security_concept AS sc
JOIN market.concept AS c
ON c.concept_id = sc.concept_id
WHERE sc.security_id = s.security_id
AND c.name = '机器人'
);
使用普通 JOIN 时,一只股票匹配多个关联记录,就会出现多行;EXISTS 只回答“有没有”,不会因为右表有多条记录而重复左表。
不要为了消除这种重复就机械地添加:
SELECT DISTINCT
应先判断业务真正需要的是:
- 返回关联明细:使用
JOIN - 判断关联是否存在:使用
EXISTS
WITH latest_quote AS (
SELECT DISTINCT ON (q.security_id)
q.security_id,
q.trade_date,
q.close_price,
q.change_rate,
q.volume
FROM market.daily_quote AS q
ORDER BY
q.security_id,
q.trade_date DESC
)
SELECT
s.code,
s.name,
l.trade_date,
l.close_price,
l.change_rate,
l.volume
FROM market.security AS s
LEFT JOIN latest_quote AS l
ON l.security_id = s.security_id
WHERE s.is_active IS TRUE
ORDER BY s.code;
WITH ranked_quote AS (
SELECT
q.*,
row_number() OVER (
PARTITION BY q.security_id
ORDER BY q.trade_date DESC
) AS row_num
FROM market.daily_quote AS q
)
SELECT
s.code,
s.name,
q.trade_date,
q.close_price,
q.change_rate
FROM market.security AS s
LEFT JOIN ranked_quote AS q
ON q.security_id = s.security_id
AND q.row_num = 1
ORDER BY s.code;
注意 q.row_num = 1 放在 ON 中,才能保留没有行情的股票。
SELECT
s.code,
s.name,
latest.trade_date,
latest.close_price,
latest.change_rate
FROM market.security AS s
LEFT JOIN LATERAL (
SELECT
q.trade_date,
q.close_price,
q.change_rate
FROM market.daily_quote AS q
WHERE q.security_id = s.security_id
ORDER BY q.trade_date DESC
LIMIT 1
) AS latest ON true
WHERE s.is_active IS TRUE
ORDER BY s.code;
LATERAL 允许右侧子查询引用左侧当前股票:
q.security_id = s.security_id
它适合:
- 每只股票最新一条行情
- 每个用户最后一笔订单
- 每个分类排名前3的商品
- 每个父记录的局部 Top N
SELECT
s.code,
s.name,
recent.trade_date,
recent.close_price
FROM market.security AS s
LEFT JOIN LATERAL (
SELECT
q.trade_date,
q.close_price
FROM market.daily_quote AS q
WHERE q.security_id = s.security_id
ORDER BY q.trade_date DESC
LIMIT 3
) AS recent ON true
WHERE s.is_active IS TRUE
ORDER BY
s.code,
recent.trade_date DESC;
这是多表查询最常见、也最隐蔽的错误。
假设一只股票有:
100条行情
5个题材
直接连接:
FROM security AS s
JOIN daily_quote AS q
ON q.security_id = s.security_id
JOIN security_concept AS sc
ON sc.security_id = s.security_id
结果可能产生:
100 × 5 = 500行
如果再执行:
sum(q.amount)
成交额可能被重复计算5次。
SELECT
s.code,
sum(q.amount) AS total_amount,
count(sc.concept_id) AS concept_count
FROM market.security AS s
JOIN market.daily_quote AS q
ON q.security_id = s.security_id
JOIN market.security_concept AS sc
ON sc.security_id = s.security_id
GROUP BY s.code;
两个一对多关系互相相乘。
WITH quote_summary AS (
SELECT
security_id,
sum(amount) AS total_amount,
count(*) AS quote_count
FROM market.daily_quote
GROUP BY security_id
),
concept_summary AS (
SELECT
security_id,
count(*) AS concept_count
FROM market.security_concept
GROUP BY security_id
)
SELECT
s.code,
s.name,
coalesce(q.total_amount, 0) AS total_amount,
coalesce(q.quote_count, 0) AS quote_count,
coalesce(c.concept_count, 0) AS concept_count
FROM market.security AS s
LEFT JOIN quote_summary AS q
ON q.security_id = s.security_id
LEFT JOIN concept_summary AS c
ON c.security_id = s.security_id
ORDER BY s.code;
核心原则:
连接前先确认每个查询结果的粒度。需要“一只股票一行”,就先把行情和题材分别聚合到“一只股票一行”,再进行连接。
统计每只股票的行情条数:
SELECT
s.code,
s.name,
count(q.security_id) AS quote_count
FROM market.security AS s
LEFT JOIN market.daily_quote AS q
ON q.security_id = s.security_id
GROUP BY
s.security_id,
s.code,
s.name;
这里必须使用:
count(q.security_id)
如果使用:
count(*)
即使股票没有行情,LEFT JOIN 仍会产生一条左表保留行,所以可能得到:
quote_count = 1
而 count(q.security_id) 只统计右表非空匹配记录,没有行情时正确返回零。
适合:
- 需要返回右表字段
- 需要返回关联明细
- 需要聚合关联记录
SELECT
s.code,
q.trade_date,
q.close_price
FROM market.security AS s
JOIN market.daily_quote AS q
ON q.security_id = s.security_id;
适合:
- 只判断右表是否存在
- 不希望左表因多条匹配而重复
WHERE EXISTS (...)
适合返回单个值:
SELECT
s.code,
(
SELECT max(q.trade_date)
FROM market.daily_quote AS q
WHERE q.security_id = s.security_id
) AS last_trade_date
FROM market.security AS s;
标量子查询如果返回多行,会报错。
适合为左表每行查询:
- 最新一条
- 前N条
- 单独聚合结果
选择方式应以查询语义和执行计划为准,不要简单认为 JOIN 或子查询一定更快。
| 操作 | 组合方向 | 作用 |
|---|---|---|
JOIN |
横向 | 把不同表的字段组合在一行 |
UNION |
纵向 | 把多个查询结果上下合并 |
code | name | close_price
第一组股票
第二组股票
第三组股票
示例:
SELECT
code,
name,
'沪市主板' AS source
FROM market.security
WHERE code LIKE '60%'
UNION ALL
SELECT
code,
name,
'深市主板' AS source
FROM market.security
WHERE code LIKE '00%';
UNION 会去重:
UNION
UNION ALL 保留重复:
UNION ALL
如果不需要去重,优先使用 UNION ALL,避免额外的排序或哈希去重开销。
两边查询必须满足:
- 字段数量相同
- 对应字段数据类型兼容
普通关联:
ON a.industry = b.industry
如果两边都是 NULL,也不会被认为匹配,因为:
NULL = NULL
结果是未知,而不是 TRUE。
如果业务明确要求两个 NULL 匹配,可以使用:
ON a.industry IS NOT DISTINCT FROM b.industry
但主外键关系通常应该:
- 使用稳定ID
- 字段类型一致
- 外键列尽量
NOT NULL
不要随意使用:
ON coalesce(a.key, 0) = coalesce(b.key, 0)
这可能把原本毫无关系的空值错误地关联到一起。
推荐:
security.security_id bigint
daily_quote.security_id bigint
security_concept.security_id bigint
不推荐:
一个表 bigint
一个表 varchar
然后查询时转换:
ON s.security_id::text = q.security_id
问题包括:
- 类型语义混乱
- 可能影响索引使用
- 每次查询都需要转换
- 更容易出现非法值
如果当前代码字段使用 char(6),另一张表使用 varchar(6),不要长期依赖:
ON btrim(a.code) = btrim(b.code)
更好的方式是清洗数据并统一表结构。
旧式写法:
SELECT
s.code,
q.close_price
FROM
market.security AS s,
market.daily_quote AS q
WHERE q.security_id = s.security_id;
虽然结果可以与 INNER JOIN 相同,但关联条件和筛选条件混杂。
推荐:
SELECT
s.code,
q.close_price
FROM market.security AS s
JOIN market.daily_quote AS q
ON q.security_id = s.security_id;
如果旧式写法漏掉关联条件,就会意外产生笛卡尔积。显式 JOIN ... ON 更容易阅读和审查。
PRIMARY KEY (security_id)
根据交易所和代码查询:
UNIQUE (exchange_code, code)
PRIMARY KEY (security_id, trade_date)
支持:
WHERE security_id = ?
ORDER BY trade_date DESC
按交易日查询全市场:
CREATE INDEX idx_daily_quote_trade_security
ON market.daily_quote (trade_date, security_id);
主键:
PRIMARY KEY (security_id, concept_id)
支持查询一只股票有哪些题材。
反向查询某个题材有哪些股票:
CREATE INDEX idx_security_concept_concept_security
ON market.security_concept (
concept_id,
security_id
);
UNIQUE (name)
支持根据题材名称定位 concept_id。
外键不会自动为引用方创建普通索引。因此应根据关联方向,为子表外键建立合适索引。
PostgreSQL 执行计划中常见:
| 算法 | 常见适用情况 |
|---|---|
Nested Loop |
外层结果较少,内层关联字段有索引 |
Hash Join |
较大数据集的等值连接 |
Merge Join |
两边已排序或能有效排序的连接 |
例如:
Nested Loop
-> Index Scan
-> Index Scan
或者:
Hash Join
-> Seq Scan
-> Hash
-> Seq Scan
查询中写的是逻辑关系:
JOIN ... ON ...
具体采用什么算法,由优化器根据以下信息选择:
- 表大小
- 过滤选择性
- 索引
- 字段统计信息
- 内存参数
- 预计成本
通常不应强制指定 JOIN 算法,应先确保表结构、统计信息、索引和 SQL 语义正确。
EXPLAIN
SELECT
s.code,
s.name,
q.trade_date,
q.close_price
FROM market.security AS s
JOIN market.daily_quote AS q
ON q.security_id = s.security_id
WHERE s.exchange_code = 'SSE'
AND q.trade_date = DATE '2026-09-14';
执行并查看真实统计:
EXPLAIN (
ANALYZE,
BUFFERS,
VERBOSE
)
SELECT
s.code,
s.name,
q.trade_date,
q.close_price
FROM market.security AS s
JOIN market.daily_quote AS q
ON q.security_id = s.security_id
WHERE s.exchange_code = 'SSE'
AND q.trade_date = DATE '2026-09-14';
重点查看:
- 使用了哪种 JOIN 算法
- 是否使用目标索引
estimated rows与actual rows是否接近loops是否过大- 是否扫描大量无效行
- 是否产生大排序
- 是否出现临时磁盘文件
- Buffer 命中和读取情况
- JOIN 后行数是否异常放大
EXPLAIN ANALYZE 会真正执行查询。PostgreSQL EXPLAIN
- 主表使用主键或唯一键关联。
- 关联字段类型保持一致。
- 高频关联方向建立合适索引。
- 只查询需要的字段,避免多表
SELECT *。 - 尽早过滤不需要的数据。
- 先确认每张表和子查询的数据粒度。
- 多个一对多表不要直接同时聚合。
- 只判断存在时使用
EXISTS。 - 每组最新或前N条可以使用
LATERAL。 - 不用
DISTINCT掩盖错误关联。 - 避免在关联字段上执行类型转换和函数。
- 避免不必要的
CROSS JOIN。 - 用
EXPLAIN (ANALYZE, BUFFERS)验证。 - 保持统计信息及时更新:
ANALYZE market.security;
ANALYZE market.daily_quote;
ANALYZE market.security_concept;
import psycopg
sql = """
SELECT
s.code,
s.name,
q.trade_date,
q.close_price,
q.change_rate
FROM market.security AS s
JOIN market.daily_quote AS q
ON q.security_id = s.security_id
WHERE s.exchange_code = %s
AND s.code = %s
AND q.trade_date >= %s
AND q.trade_date < %s
ORDER BY q.trade_date DESC
"""
params = (
"SSE",
"600519",
"2026-01-01",
"2027-01-01",
)
with psycopg.connect(
"postgresql://postgres:password@localhost:5432/stock"
) as connection:
with connection.cursor() as cursor:
cursor.execute(sql, params)
for row in cursor:
print(row)
不能拼接用户输入:
## 不推荐
sql = f"""
SELECT ...
WHERE s.code = '{code}'
"""
必须使用参数化查询。
from sqlalchemy import select
statement = (
select(
Security.code,
Security.name,
DailyQuote.trade_date,
DailyQuote.close_price,
DailyQuote.change_rate,
)
.join(
DailyQuote,
DailyQuote.security_id == Security.security_id,
)
.where(
Security.exchange_code == "SSE",
Security.code == "600519",
)
.order_by(
DailyQuote.trade_date.desc(),
)
)
rows = session.execute(statement).all()
左连接:
statement = (
select(
Security.code,
Security.name,
DailyQuote.close_price,
)
.outerjoin(
DailyQuote,
(DailyQuote.security_id == Security.security_id)
& (DailyQuote.trade_date == trade_date),
)
)
注意日期条件应放在 outerjoin() 的关联条件中,才能保留当天无行情的股票。
WITH quote_metrics AS (
SELECT
q.security_id,
q.trade_date,
q.close_price,
q.volume,
q.change_rate,
avg(q.close_price) OVER (
PARTITION BY q.security_id
ORDER BY q.trade_date
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
) AS ma5,
avg(q.close_price) OVER (
PARTITION BY q.security_id
ORDER BY q.trade_date
ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
) AS ma20,
avg(q.close_price) OVER (
PARTITION BY q.security_id
ORDER BY q.trade_date
ROWS BETWEEN 59 PRECEDING AND CURRENT ROW
) AS ma60,
count(*) OVER (
PARTITION BY q.security_id
ORDER BY q.trade_date
ROWS BETWEEN 59 PRECEDING AND CURRENT ROW
) AS ma60_count,
row_number() OVER (
PARTITION BY q.security_id
ORDER BY q.trade_date DESC
) AS row_num
FROM market.daily_quote AS q
),
concept_summary AS (
SELECT
sc.security_id,
string_agg(
c.name,
'、'
ORDER BY c.name
) AS concepts
FROM market.security_concept AS sc
JOIN market.concept AS c
ON c.concept_id = sc.concept_id
GROUP BY sc.security_id
)
SELECT
s.exchange_code,
s.code,
s.name,
s.board,
s.industry,
q.trade_date,
q.close_price,
q.volume,
q.change_rate,
round(q.ma5, 4) AS ma5,
round(q.ma20, 4) AS ma20,
CASE
WHEN q.ma60_count = 60
THEN round(q.ma60, 4)
ELSE NULL
END AS ma60,
coalesce(c.concepts, '') AS concepts
FROM market.security AS s
JOIN quote_metrics AS q
ON q.security_id = s.security_id
AND q.row_num = 1
LEFT JOIN concept_summary AS c
ON c.security_id = s.security_id
WHERE s.is_active IS TRUE
AND s.is_st IS FALSE
ORDER BY
q.change_rate DESC NULLS LAST,
s.code;
这条查询先分别把数据整理到正确粒度:
quote_metrics → 每只股票选最新一行
concept_summary → 每只股票聚合为一行
security → 每只股票一行
然后再连接,因此不会产生行情与题材相乘造成的重复统计。
执行前检查:
- 每张表的一行代表什么?
- 表之间是一对一、一对多还是多对多?
- 关联字段是否为主键、唯一键或外键?
- 关联字段类型是否完全一致?
- 是否需要
INNER JOIN还是LEFT JOIN? - 右表筛选条件应该放在
ON还是WHERE? - 是否需要保留没有关联记录的左表数据?
- 一个左表行会匹配多少个右表行?
- 多个一对多关系是否会相乘?
- 是否应该先聚合再关联?
- 只是判断存在时是否应该使用
EXISTS? - 是否误用了
NOT IN? - 是否使用了危险的
NATURAL JOIN? - 是否意外产生
CROSS JOIN? - 是否用
DISTINCT掩盖关联错误? - 外键引用方是否有合适索引?
- 是否只返回需要的字段?
EXPLAIN预估行数和实际行数是否接近?- JOIN 后结果行数是否符合业务预期?
核心原则:
多表查询最重要的不是记住多少种 JOIN,而是先明确每张表的数据粒度,再明确“哪些行应该匹配、未匹配的行是否保留”。粒度正确,关联才不会重复;关系明确,结果才不会失真。