PostgreSQL SELECT
PostgreSQL 使用 SELECT 查询数据:
SELECT 字段
FROM 表
WHERE 条件;
“查”不仅是读取数据,还包括:
- 字段选择与计算
- 条件筛选
- 排序和分页
- 去重
- 分组统计
- 多表关联
- 子查询与 CTE
- 窗口函数
- JSONB、数组查询
- 行锁
- 执行计划和索引优化
完整语法见 PostgreSQL 18 SELECT。
下面继续使用两张核心表。
股票基本信息:
market.security
├── security_id
├── exchange_code
├── code
├── name
├── board
├── industry
├── is_st
├── is_active
├── extra
├── created_at
└── updated_at
日线行情:
market.daily_quote
├── security_id
├── trade_date
├── open_price
├── high_price
├── low_price
├── close_price
├── pre_close
├── volume
├── amount
├── change_rate
├── turnover
└── created_at
其中:
PRIMARY KEY (security_id, trade_date)
SELECT *
FROM market.security;
* 表示全部字段。
调试时可以使用,但正式代码建议明确写出字段:
SELECT
security_id,
exchange_code,
code,
name,
industry
FROM market.security;
明确字段的优点:
- 减少网络传输
- 避免读取大文本和 JSONB
- 表结构变化时结果更稳定
- 更容易使用覆盖索引
- Python 返回结构更清晰
SELECT code
FROM market.security;
查询多个字段:
SELECT
code,
name,
industry
FROM market.security;
使用 AS 设置结果字段名:
SELECT
code AS stock_code,
name AS stock_name,
industry AS stock_industry
FROM market.security;
计算字段也可以设置别名:
SELECT
security_id,
trade_date,
close_price,
close_price - pre_close AS change_amount
FROM market.daily_quote;
AS 可以省略:
SELECT code stock_code
FROM market.security;
但建议保留 AS,可读性更好。
SELECT
s.code,
s.name,
s.industry
FROM market.security AS s;
多表查询时,别名非常重要:
s.code
q.close_price
定义别名后,应使用别名引用表:
FROM market.security AS s
后面使用:
s.code
而不是:
market.security.code
SQL 的书写顺序和逻辑执行顺序不同。
书写顺序:
SELECT
FROM
WHERE
GROUP BY
HAVING
ORDER BY
LIMIT
逻辑上可以理解为:
WITH
↓
FROM / JOIN
↓
WHERE
↓
GROUP BY
↓
HAVING
↓
SELECT
↓
DISTINCT
↓
UNION / INTERSECT / EXCEPT
↓
ORDER BY
↓
LIMIT / OFFSET
↓
行锁
因此下面通常不能直接使用别名:
SELECT
close_price - pre_close AS change_amount
FROM market.daily_quote
WHERE change_amount > 0;
因为执行 WHERE 时,change_amount 还没有在 SELECT 阶段产生。
应写成:
SELECT
close_price - pre_close AS change_amount
FROM market.daily_quote
WHERE close_price - pre_close > 0;
或者使用子查询:
SELECT *
FROM (
SELECT
security_id,
trade_date,
close_price - pre_close AS change_amount
FROM market.daily_quote
) AS q
WHERE q.change_amount > 0;
SELECT *
FROM market.security
WHERE code = '600519';
股票代码是字符串,必须保留引号和前导零:
WHERE code = '000001'
不能写成:
WHERE code = 000001
标准 SQL 写法:
WHERE industry <> '银行'
PostgreSQL 也支持:
WHERE industry != '银行'
推荐使用标准写法 <>。
SELECT *
FROM market.daily_quote
WHERE close_price > 100;
支持:
= 等于
<> 不等于
> 大于
>= 大于等于
< 小于
<= 小于等于
所有条件同时成立:
SELECT *
FROM market.security
WHERE exchange_code = 'SSE'
AND is_active = true;
任意一个条件成立:
SELECT *
FROM market.security
WHERE code LIKE '00%'
OR code LIKE '60%';
SELECT *
FROM market.security
WHERE NOT is_st;
推荐使用括号明确复杂逻辑:
SELECT *
FROM market.security
WHERE is_active = true
AND (
code LIKE '00%'
OR code LIKE '60%'
);
否则下面的语句:
WHERE is_active = true
AND code LIKE '00%'
OR code LIKE '60%'
等价于:
WHERE (
is_active = true
AND code LIKE '00%'
)
OR code LIKE '60%'
可能把非正常状态的 60 开头股票也查询出来。
NULL 表示未知值,不能使用普通等号比较。
错误:
WHERE industry = NULL
正确:
WHERE industry IS NULL
查询非空:
WHERE industry IS NOT NULL
SELECT *
FROM market.security
WHERE industry IS DISTINCT FROM '银行';
它会返回:
- 行业不是银行的记录
- 行业为
NULL的记录
而:
WHERE industry <> '银行'
不会返回 industry IS NULL 的记录。
IS DISTINCT FROM 和 IS NOT DISTINCT FROM 可以把 NULL 当作普通值进行安全比较。PostgreSQL 比较运算
WHERE is_active IS TRUE
WHERE is_active IS FALSE
WHERE is_active IS NULL
如果布尔字段允许 NULL,下面两种写法含义不同:
WHERE NOT is_active
只匹配 false。
WHERE is_active IS NOT TRUE
匹配:
false
NULL
SELECT *
FROM market.daily_quote
WHERE close_price BETWEEN 10 AND 20;
等价于:
WHERE close_price >= 10
AND close_price <= 20
BETWEEN 包含两个边界。
SELECT *
FROM market.daily_quote
WHERE trade_date BETWEEN
DATE '2026-01-01'
AND DATE '2026-12-31';
对于时间戳,更推荐左闭右开:
SELECT *
FROM market.stock_tick
WHERE recorded_at >= TIMESTAMPTZ '2026-09-14 00:00:00+08'
AND recorded_at < TIMESTAMPTZ '2026-09-15 00:00:00+08';
这样不会遗漏带小时、分钟和微秒的数据。
SELECT *
FROM market.security
WHERE code IN (
'600519',
'000001',
'601318'
);
等价于:
WHERE code = '600519'
OR code = '000001'
OR code = '601318'
反向条件:
WHERE code NOT IN (
'600519',
'000001'
)
如果集合中包含 NULL:
WHERE security_id NOT IN (1, 2, NULL)
可能一行也匹配不到。
子查询排除场景更推荐使用:
SELECT *
FROM market.security AS s
WHERE NOT EXISTS (
SELECT 1
FROM market.watchlist AS w
WHERE w.security_id = s.security_id
);
SELECT *
FROM market.security
WHERE code LIKE '60%';
通配符:
| 符号 | 含义 |
|---|---|
% |
任意数量字符 |
_ |
任意一个字符 |
例如:
WHERE name LIKE '%科技%'
查询名称中包含“科技”的股票。
WHERE code LIKE '60____'
表示以 60 开头,后面正好四个字符。
ILIKE 是 PostgreSQL 提供的大小写不敏感匹配:
SELECT *
FROM users
WHERE username ILIKE 'martin%';
中文本身没有大小写区别,因此中文查询通常不需要 ILIKE。
SELECT *
FROM market.security
WHERE code ~ '^(00|60)[0-9]{4}$';
常用操作符:
| 操作符 | 含义 |
|---|---|
~ |
区分大小写匹配 |
~* |
不区分大小写匹配 |
!~ |
区分大小写不匹配 |
!~* |
不区分大小写不匹配 |
查询名称以 ST 或 *ST 开头:
SELECT *
FROM market.security
WHERE name ~ '^\*?ST';
SELECT
code,
name,
CASE
WHEN code LIKE '60%' THEN '沪市主板'
WHEN code LIKE '00%' THEN '深市主板'
WHEN code LIKE '30%' THEN '创业板'
WHEN code LIKE '68%' THEN '科创板'
ELSE '其他'
END AS board_name
FROM market.security;
返回第一个非空值:
SELECT
code,
name,
coalesce(industry, '未分类') AS industry
FROM market.security;
计算时处理空值:
SELECT
coalesce(sum(amount), 0) AS total_amount
FROM market.daily_quote
WHERE trade_date = DATE '1990-01-01';
两个值相同时返回 NULL:
SELECT
amount / nullif(volume, 0) AS average_price
FROM market.daily_quote;
如果 volume = 0,nullif(volume, 0) 返回 NULL,可以避免除零错误。
SELECT
greatest(open_price, close_price) AS body_high,
least(open_price, close_price) AS body_low
FROM market.daily_quote;
升序:
SELECT
code,
name
FROM market.security
ORDER BY code ASC;
降序:
SELECT
security_id,
trade_date,
close_price
FROM market.daily_quote
WHERE security_id = 100
ORDER BY trade_date DESC;
多字段排序:
SELECT
code,
name,
industry
FROM market.security
ORDER BY
industry ASC,
code ASC;
只有明确使用 ORDER BY,结果顺序才有保证。没有 ORDER BY 时,PostgreSQL 可以按照它认为最快的顺序返回数据。PostgreSQL 排序
ORDER BY industry ASC NULLS LAST
ORDER BY industry DESC NULLS FIRST
PostgreSQL 默认把 NULL 视为大于普通非空值,因此通常:
ASC → NULLS LAST
DESC → NULLS FIRST
建议需要稳定结果时明确写出。
分页时不能只按可能重复的字段排序:
ORDER BY trade_date DESC
应补充唯一字段:
ORDER BY
trade_date DESC,
security_id DESC
这样即使同一日期有多条记录,顺序也保持确定。
SELECT *
FROM market.security
ORDER BY security_id
LIMIT 10;
跳过前20行,再取10行:
SELECT *
FROM market.security
ORDER BY security_id
LIMIT 10
OFFSET 20;
等价的标准 SQL 写法:
SELECT *
FROM market.security
ORDER BY security_id
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;
LIMIT 必须配合确定性的 ORDER BY,否则不同查询可能返回不同子集。PostgreSQL LIMIT 与 OFFSET
查询涨幅最高的前10名,并保留与第10名并列的数据:
SELECT
security_id,
change_rate
FROM market.daily_quote
WHERE trade_date = DATE '2026-09-14'
ORDER BY change_rate DESC
FETCH FIRST 10 ROWS WITH TIES;
如果第10名有多个相同涨跌幅,实际返回可能超过10行。
下面的深分页:
SELECT *
FROM market.daily_quote
ORDER BY trade_date DESC, security_id DESC
LIMIT 50
OFFSET 1000000;
数据库仍然需要找到并跳过前面大量记录,通常越往后越慢。
更适合使用游标分页,也叫 Keyset Pagination。
第一页:
SELECT *
FROM market.daily_quote
ORDER BY trade_date DESC, security_id DESC
LIMIT 50;
假设最后一行是:
trade_date = 2026-09-01
security_id = 1200
下一页:
SELECT *
FROM market.daily_quote
WHERE (
trade_date,
security_id
) < (
DATE '2026-09-01',
1200
)
ORDER BY trade_date DESC, security_id DESC
LIMIT 50;
对应索引:
CREATE INDEX idx_daily_quote_trade_security
ON market.daily_quote (trade_date, security_id);
Keyset 分页的优点:
- 深分页速度更稳定
- 不需要跳过大量记录
- 并发插入时更不容易重复或遗漏
缺点是不能直接任意跳转到第10000页。
查询所有行业:
SELECT DISTINCT industry
FROM market.security
WHERE industry IS NOT NULL
ORDER BY industry;
多字段去重:
SELECT DISTINCT
exchange_code,
industry
FROM market.security;
DISTINCT 针对整个查询结果组合去重,不是只针对第一个字段。
不要用 DISTINCT 掩盖错误的多表关联。如果结果意外重复,应先检查表关系和关联条件。
查询每只股票最新一条行情:
SELECT DISTINCT ON (q.security_id)
q.security_id,
q.trade_date,
q.close_price,
q.volume
FROM market.daily_quote AS q
ORDER BY
q.security_id,
q.trade_date DESC;
执行逻辑:
- 按
security_id分组; - 根据
ORDER BY决定每组第一行; - 每只股票保留第一行。
DISTINCT ON 中的字段必须对应 ORDER BY 最左侧字段:
DISTINCT ON (security_id)
ORDER BY security_id, trade_date DESC
没有明确排序时,“保留哪一行”是不确定的。
常用聚合函数:
| 函数 | 作用 |
|---|---|
count(*) |
统计行数 |
count(column) |
统计非空值数量 |
sum() |
求和 |
avg() |
平均值 |
max() |
最大值 |
min() |
最小值 |
string_agg() |
字符串聚合 |
array_agg() |
数组聚合 |
jsonb_agg() |
JSONB数组聚合 |
SELECT count(*)
FROM market.security;
统计非空行业:
SELECT count(industry)
FROM market.security;
两者不同:
count(*) 统计所有行
count(industry) 只统计 industry 非空的行
SELECT
max(close_price) AS highest_close,
min(close_price) AS lowest_close,
avg(close_price) AS average_close,
sum(volume) AS total_volume,
sum(amount) AS total_amount
FROM market.daily_quote
WHERE security_id = 100;
除 count 外,大多数聚合函数在没有输入行时返回 NULL,而不是零:
SELECT coalesce(sum(amount), 0)
FROM market.daily_quote
WHERE trade_date = DATE '1900-01-01';
统计每个行业的股票数量:
SELECT
industry,
count(*) AS stock_count
FROM market.security
WHERE industry IS NOT NULL
GROUP BY industry
ORDER BY stock_count DESC;
按交易日统计成交额:
SELECT
trade_date,
sum(amount) AS total_amount,
sum(volume) AS total_volume,
avg(change_rate) AS average_change_rate
FROM market.daily_quote
GROUP BY trade_date
ORDER BY trade_date DESC;
使用 GROUP BY 后,SELECT 中的普通字段必须:
- 出现在
GROUP BY中; - 或者放在聚合函数内;
- 或者能够由分组键函数依赖确定。
错误:
SELECT
trade_date,
security_id,
sum(amount)
FROM market.daily_quote
GROUP BY trade_date;
一个交易日对应多只股票,数据库无法确定应该返回哪个 security_id。
WHERE 过滤分组前的明细行:
SELECT
trade_date,
sum(amount) AS total_amount
FROM market.daily_quote
WHERE amount > 0
GROUP BY trade_date;
HAVING 过滤分组后的聚合结果:
SELECT
trade_date,
sum(amount) AS total_amount
FROM market.daily_quote
GROUP BY trade_date
HAVING sum(amount) > 100000000000
ORDER BY trade_date DESC;
区别:
WHERE → 过滤原始记录
HAVING → 过滤分组结果
能放在 WHERE 的条件不要无意义地放在 HAVING,因为尽早过滤通常能减少参与聚合的数据。
统计每天上涨、下跌和平盘数量:
SELECT
trade_date,
count(*) FILTER (
WHERE change_rate > 0
) AS rising_count,
count(*) FILTER (
WHERE change_rate < 0
) AS falling_count,
count(*) FILTER (
WHERE change_rate = 0
) AS flat_count,
count(*) AS total_count
FROM market.daily_quote
GROUP BY trade_date
ORDER BY trade_date DESC;
这很适合计算市场情绪中的:
- 上涨家数
- 下跌家数
- 平盘家数
- 涨跌比
计算上涨比例:
SELECT
trade_date,
count(*) FILTER (WHERE change_rate > 0)::numeric
/ nullif(count(*), 0) AS rising_ratio
FROM market.daily_quote
GROUP BY trade_date;
这里把计数转换为 numeric,避免整数除法截断。
查询股票名称和行情:
SELECT
s.code,
s.name,
q.trade_date,
q.close_price,
q.volume
FROM market.security AS s
INNER JOIN market.daily_quote AS q
ON q.security_id = s.security_id
WHERE s.code = '600519'
ORDER BY q.trade_date DESC;
INNER JOIN 只返回两边都匹配的记录。
INNER 可以省略:
JOIN market.daily_quote AS q
查询所有股票,以及指定交易日的行情:
SELECT
s.code,
s.name,
q.trade_date,
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'
ORDER BY s.code;
即使某只股票当天没有行情,也会返回股票信息,只是行情字段为 NULL。
正确:
LEFT JOIN market.daily_quote AS q
ON q.security_id = s.security_id
AND q.trade_date = DATE '2026-09-14'
如果写成:
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
返回右表全部记录:
SELECT *
FROM table_a AS a
RIGHT JOIN table_b AS b
ON b.id = a.id;
通常可以交换表顺序,改写成更容易理解的 LEFT JOIN。
返回两边全部数据:
SELECT *
FROM table_a AS a
FULL JOIN table_b AS b
ON b.id = a.id;
适合:
- 两个数据源差异核对
- 找出双方缺失数据
- 数据迁移检查
笛卡尔积:
SELECT *
FROM table_a
CROSS JOIN table_b;
如果 A 有100行、B有200行,结果有:
100 × 200 = 20,000 行
多表查询漏写关联条件,可能意外产生巨大的笛卡尔积。
NATURAL JOIN 会自动使用两张表中所有同名字段关联。以后新增同名字段时,查询含义可能悄悄改变。
推荐明确写:
JOIN ... ON ...
或者:
JOIN ... USING (security_id)
PostgreSQL 对各类连接的行为有完整说明。表表达式与 JOIN
查询有行情数据的股票:
SELECT
s.security_id,
s.code,
s.name
FROM market.security AS s
WHERE EXISTS (
SELECT 1
FROM market.daily_quote AS q
WHERE q.security_id = s.security_id
);
查询没有行情数据的股票:
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
);
SELECT 1 表示只关心记录是否存在,不需要子查询返回实际字段。
查询每只股票最后交易日:
SELECT
s.code,
s.name,
(
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;
标量子查询必须返回:
- 零行:结果为
NULL - 一行:正常使用
- 多行:报错
需要返回多行时,不能把子查询直接当成单个字段。
查询每只股票最新一条行情:
SELECT
s.code,
s.name,
latest.trade_date,
latest.close_price,
latest.volume
FROM market.security AS s
LEFT JOIN LATERAL (
SELECT
q.trade_date,
q.close_price,
q.volume
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
ORDER BY s.code;
LATERAL 允许右侧子查询引用左侧当前行:
q.security_id = s.security_id
它很适合:
- 每个用户最新一条订单
- 每只股票最近一条行情
- 每个分类排名前几条记录
- 每个父记录的局部查询
CTE 可以把复杂查询拆成多个可读步骤:
WITH active_security AS (
SELECT
security_id,
code,
name
FROM market.security
WHERE is_active = true
),
latest_trade_date AS (
SELECT max(trade_date) AS trade_date
FROM market.daily_quote
)
SELECT
s.code,
s.name,
q.close_price,
q.change_rate
FROM active_security AS s
CROSS JOIN latest_trade_date AS d
JOIN market.daily_quote AS q
ON q.security_id = s.security_id
AND q.trade_date = d.trade_date
ORDER BY q.change_rate DESC;
CTE 的主要价值:
- 拆解复杂逻辑
- 减少重复子查询
- 提高可读性
- 便于逐步调试
- 支持递归查询
PostgreSQL 18 中,普通无副作用且只使用一次的 CTE,优化器可能将它合并到主查询中。因此不要再简单认为“CTE 一定会物化”。
需要明确物化:
WITH data AS MATERIALIZED (
SELECT ...
)
SELECT ...
FROM data;
允许合并优化:
WITH data AS NOT MATERIALIZED (
SELECT ...
)
SELECT ...
FROM data;
只有在执行计划证明有必要时,才应主动控制物化行为。
合并结果并去重:
SELECT security_id
FROM market.watchlist_a
UNION
SELECT security_id
FROM market.watchlist_b;
合并结果但保留重复:
SELECT security_id
FROM market.watchlist_a
UNION ALL
SELECT security_id
FROM market.watchlist_b;
如果业务不需要去重,优先使用 UNION ALL,避免额外去重开销。
查询两边共有的数据:
SELECT security_id
FROM market.watchlist_a
INTERSECT
SELECT security_id
FROM market.watchlist_b;
查询第一边有、第二边没有的数据:
SELECT security_id
FROM market.watchlist_a
EXCEPT
SELECT security_id
FROM market.watchlist_b;
两边必须具有:
- 相同字段数量
- 对应字段类型兼容
最终排序应写在集合运算之后。
聚合函数会把多行合并为一行;窗口函数在保留明细行的同时进行跨行计算。
基本结构:
函数() OVER (
PARTITION BY 分组字段
ORDER BY 排序字段
ROWS ...
)
SELECT
security_id,
trade_date,
close_price,
row_number() OVER (
PARTITION BY security_id
ORDER BY trade_date DESC
) AS row_num
FROM market.daily_quote;
每只股票内部从最新日期开始编号。
窗口函数不能直接放在同级 WHERE 中:
-- 错误思路
WHERE row_number() OVER (...) = 1
应使用 CTE:
WITH ranked AS (
SELECT
q.*,
row_number() OVER (
PARTITION BY security_id
ORDER BY trade_date DESC
) AS row_num
FROM market.daily_quote AS q
)
SELECT
security_id,
trade_date,
close_price,
volume
FROM ranked
WHERE row_num = 1;
SELECT
security_id,
trade_date,
change_rate,
rank() OVER (
PARTITION BY trade_date
ORDER BY change_rate DESC
) AS market_rank
FROM market.daily_quote;
区别:
| 函数 | 并列后是否跳号 |
|---|---|
row_number() |
不并列,每行唯一编号 |
rank() |
并列,后面跳号 |
dense_rank() |
并列,后面不跳号 |
SELECT
security_id,
trade_date,
close_price,
lag(close_price) OVER (
PARTITION BY security_id
ORDER BY trade_date
) AS previous_close
FROM market.daily_quote;
计算涨跌幅:
WITH quote_change AS (
SELECT
security_id,
trade_date,
close_price,
lag(close_price) OVER (
PARTITION BY security_id
ORDER BY trade_date
) AS previous_close
FROM market.daily_quote
)
SELECT
security_id,
trade_date,
close_price,
previous_close,
close_price / nullif(previous_close, 0) - 1
AS change_rate
FROM quote_change;
lead(close_price) OVER (
PARTITION BY security_id
ORDER BY trade_date
)
SELECT
security_id,
trade_date,
close_price,
avg(close_price) OVER (
PARTITION BY security_id
ORDER BY trade_date
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
) AS ma5,
avg(close_price) OVER (
PARTITION BY security_id
ORDER BY trade_date
ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
) AS ma20,
avg(close_price) OVER (
PARTITION BY security_id
ORDER BY trade_date
ROWS BETWEEN 59 PRECEDING AND CURRENT ROW
) AS ma60
FROM market.daily_quote
ORDER BY security_id, trade_date;
ROWS BETWEEN 59 PRECEDING AND CURRENT ROW 表示:
当前行 + 前59行 = 60条交易记录
前59个交易日不足时,PostgreSQL 会使用当前已有记录计算平均值。如果要求必须满60日,可以同时计算窗口记录数,再进行判断。
WITH indicators AS (
SELECT
security_id,
trade_date,
close_price,
count(*) OVER (
PARTITION BY security_id
ORDER BY trade_date
ROWS BETWEEN 59 PRECEDING AND CURRENT ROW
) AS sample_count,
avg(close_price) OVER (
PARTITION BY security_id
ORDER BY trade_date
ROWS BETWEEN 59 PRECEDING AND CURRENT ROW
) AS ma60
FROM market.daily_quote
)
SELECT
security_id,
trade_date,
close_price,
CASE
WHEN sample_count = 60 THEN ma60
ELSE NULL
END AS ma60
FROM indicators;
当前日期:
SELECT CURRENT_DATE;
当前时间:
SELECT now();
最近7天:
SELECT *
FROM market.daily_quote
WHERE trade_date >= CURRENT_DATE - 7;
最近30天时间戳:
SELECT *
FROM market.stock_tick
WHERE recorded_at >= now() - interval '30 days';
按月分组:
SELECT
date_trunc('month', created_at) AS month,
count(*) AS row_count
FROM market.security
GROUP BY date_trunc('month', created_at)
ORDER BY month;
提取年月:
SELECT
extract(year FROM trade_date) AS year,
extract(month FROM trade_date) AS month
FROM market.daily_quote;
不推荐:
WHERE date_trunc('year', created_at)
= TIMESTAMPTZ '2026-01-01 00:00:00+00'
它对字段执行函数,普通 created_at 索引可能难以直接利用。
推荐:
WHERE created_at >= TIMESTAMPTZ '2026-01-01 00:00:00+00'
AND created_at < TIMESTAMPTZ '2027-01-01 00:00:00+00'
假设 extra 内容:
{
"province": "贵州",
"company_type": "国企",
"concepts": ["白酒", "消费"]
}
SELECT extra->'concepts'
FROM market.security;
-> 返回 JSONB。
获取文本:
SELECT extra->>'province'
FROM market.security;
->> 返回 text。
SELECT *
FROM market.security
WHERE extra->>'province' = '贵州';
包含查询:
SELECT *
FROM market.security
WHERE extra @> '{"company_type": "国企"}'::jsonb;
判断键是否存在:
SELECT *
FROM market.security
WHERE extra ? 'province';
对应 GIN 索引:
CREATE INDEX idx_security_extra_gin
ON market.security
USING gin (extra);
如果 province 是稳定并且频繁查询的属性,更适合设计成普通字段,而不是长期藏在 JSONB 中。
假设:
concepts text[]
查询包含“机器人”:
SELECT *
FROM market.security
WHERE '机器人' = ANY(concepts);
数组包含指定元素:
SELECT *
FROM market.security
WHERE concepts @> ARRAY['机器人'];
同时包含多个题材:
WHERE concepts @> ARRAY['机器人', '人工智能'];
任意一个重叠:
WHERE concepts && ARRAY['机器人', '人工智能'];
对应 GIN 索引:
CREATE INDEX idx_security_concepts_gin
ON market.security
USING gin (concepts);
如果题材还包含分类、来源、更新时间等信息,应使用题材表和多对多关联表,而不是数组。
普通 SELECT 不会锁住记录阻止其他事务修改。
如果查询后需要基于当前值继续修改,可以使用:
BEGIN;
SELECT *
FROM account
WHERE account_id = 1
FOR UPDATE;
UPDATE account
SET balance = balance - 100
WHERE account_id = 1;
COMMIT;
常用模式:
FOR UPDATE
FOR NO KEY UPDATE
FOR SHARE
FOR KEY SHARE
不等待锁:
FOR UPDATE NOWAIT
跳过已被其他事务锁定的任务:
SELECT *
FROM task
WHERE status = 'pending'
ORDER BY task_id
FOR UPDATE SKIP LOCKED
LIMIT 10;
SKIP LOCKED 适合任务队列,不适合普通报表,因为它可能有意跳过部分数据。
查看数据库准备如何执行查询:
EXPLAIN
SELECT *
FROM market.security
WHERE exchange_code = 'SSE'
AND code = '600519';
执行并显示真实统计:
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM market.security
WHERE exchange_code = 'SSE'
AND code = '600519';
常见节点:
| 节点 | 含义 |
|---|---|
Seq Scan |
顺序扫描表 |
Index Scan |
使用索引并回表 |
Index Only Scan |
尽量只读取索引 |
Bitmap Index Scan |
位图索引扫描 |
Bitmap Heap Scan |
按位图访问表页 |
Nested Loop |
嵌套循环连接 |
Hash Join |
哈希连接 |
Merge Join |
归并连接 |
Sort |
排序 |
Aggregate |
聚合 |
WindowAgg |
窗口函数 |
重点查看:
costrowsactual rowsactual timeloopsBuffers- 是否出现磁盘排序
- 预估行数与实际行数是否相差很大
EXPLAIN ANALYZE 会真正执行语句。用于 SELECT 时一般只是读取数据,但查询中如果调用具有副作用的函数,仍需谨慎。PostgreSQL EXPLAIN
WHERE exchange_code = ?
AND code = ?
对应:
UNIQUE (exchange_code, code)
WHERE security_id = ?
ORDER BY trade_date DESC
对应:
PRIMARY KEY (security_id, trade_date)
B-tree 可以反向扫描,因此通常不必仅为了 trade_date DESC 重复建立相同字段的降序索引。
WHERE trade_date = ?
对应:
CREATE INDEX idx_daily_quote_trade_date
ON market.daily_quote (trade_date);
ORDER BY trade_date DESC, security_id DESC
对应:
CREATE INDEX idx_daily_quote_trade_security
ON market.daily_quote (trade_date, security_id);
WHERE extra @> ...
通常考虑 GIN。
PostgreSQL 提供 B-tree、Hash、GiST、SP-GiST、GIN 和 BRIN 等索引类型。PostgreSQL 索引类型
读取了不需要的大文本、JSONB 或二进制字段。
误以为数据库会永久按主键或插入顺序返回。
页码越大,跳过的数据越多。
WHERE lower(name) = 'abc'
普通 name 索引未必能直接使用。可以考虑表达式索引:
CREATE INDEX idx_security_lower_name
ON market.security (lower(name));
WHERE name LIKE '%科技%'
普通 B-tree 通常难以用于这种任意位置包含查询,可根据需求评估 pg_trgm 和 GIN/GiST。
WHERE code::integer = 1
既破坏前导零语义,也可能影响普通代码索引使用。
不要用 DISTINCT 暂时遮住,应检查关联关系。
右表条件放进 WHERE 后,可能失去外连接效果。
PostgreSQL 的精确全表 count(*) 通常需要扫描整张表或覆盖全部记录的索引,其工作量与数据规模相关。
先查询100只股票,再为每只股票单独查询行情,会产生101次数据库请求。
应考虑:
JOINLATERAL- 批量
IN - SQLAlchemy 预加载
import psycopg
sql = """
SELECT
security_id,
exchange_code,
code,
name,
industry
FROM market.security
WHERE exchange_code = %s
AND code = %s
"""
with psycopg.connect(
"postgresql://postgres:password@localhost:5432/stock"
) as connection:
with connection.cursor() as cursor:
cursor.execute(
sql,
("SSE", "600519"),
)
row = cursor.fetchone()
if row is None:
print("没有找到数据")
else:
print(row)
查询多行:
cursor.execute(
"""
SELECT code, name
FROM market.security
WHERE exchange_code = %s
ORDER BY code
""",
("SSE",),
)
rows = cursor.fetchall()
单参数元组必须保留逗号:
("SSE",)
对于大量数据,可以分批读取:
while rows := cursor.fetchmany(1000):
for row in rows:
print(row)
或者使用服务端游标:
with connection.cursor(name="security_cursor") as cursor:
cursor.execute(
"""
SELECT *
FROM market.daily_quote
ORDER BY security_id, trade_date
"""
)
cursor.itersize = 1000
for row in cursor:
print(row)
不推荐:
sql = f"""
SELECT *
FROM market.security
WHERE code = '{code}'
"""
推荐参数化:
cursor.execute(
"""
SELECT *
FROM market.security
WHERE code = %s
""",
(code,),
)
参数只能替代数据值,不能直接替代表名和字段名。
动态排序字段应使用白名单:
allowed_columns = {
"code": "code",
"name": "name",
"created_at": "created_at",
}
order_column = allowed_columns.get(user_input)
if order_column is None:
raise ValueError("非法排序字段")
不能把用户输入直接拼接到 ORDER BY。
基本查询:
from sqlalchemy import select
statement = (
select(Security)
.where(
Security.exchange_code == "SSE",
Security.code == "600519",
)
)
security = session.execute(
statement
).scalar_one_or_none()
查询指定字段:
statement = (
select(
Security.code,
Security.name,
Security.industry,
)
.where(Security.is_active.is_(True))
.order_by(Security.code)
)
rows = session.execute(statement).all()
关联查询:
statement = (
select(
Security.code,
Security.name,
DailyQuote.trade_date,
DailyQuote.close_price,
)
.join(
DailyQuote,
DailyQuote.security_id == Security.security_id,
)
.where(Security.code == "600519")
.order_by(DailyQuote.trade_date.desc())
)
分页:
statement = (
select(Security)
.where(Security.is_active.is_(True))
.order_by(Security.security_id)
.limit(50)
)
查询每只正常股票的最新行情,并计算5日、20日和60日均线:
WITH indicators 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
FROM market.daily_quote AS q
),
latest AS (
SELECT
i.*,
row_number() OVER (
PARTITION BY i.security_id
ORDER BY i.trade_date DESC
) AS row_num
FROM indicators AS i
)
SELECT
s.exchange_code,
s.code,
s.name,
s.industry,
l.trade_date,
l.close_price,
l.volume,
l.change_rate,
round(l.ma5, 4) AS ma5,
round(l.ma20, 4) AS ma20,
CASE
WHEN l.ma60_count = 60
THEN round(l.ma60, 4)
ELSE NULL
END AS ma60
FROM latest AS l
JOIN market.security AS s
ON s.security_id = l.security_id
WHERE l.row_num = 1
AND s.is_active IS TRUE
AND s.is_st IS FALSE
ORDER BY
l.change_rate DESC NULLS LAST,
s.code;
这条查询综合使用了:
- CTE
- 窗口函数
- 移动平均
- 每组最新一行
- 多表关联
- 布尔筛选
- 条件表达式
- 空值排序
执行查询前后检查:
- 是否只查询需要的字段?
- 股票代码是否按字符串处理?
NULL是否使用IS NULL?AND和OR是否加了清晰括号?- 日期和时间范围是否使用了正确边界?
- 分页是否有稳定的
ORDER BY? - 深分页是否应该改为 Keyset?
LEFT JOIN的右表条件是否应放在ON?- 关联条件是否会产生一对多重复?
- 是否错误地使用
DISTINCT掩盖重复? WHERE和HAVING是否使用正确?count(*)与count(column)是否区分?- 聚合无数据时是否处理了
NULL? - 窗口函数是否需要明确
ROWS范围? - 是否出现 N+1 查询?
- 参数是否使用参数化绑定?
- 筛选、关联、排序字段是否有合理索引?
- 是否用
EXPLAIN (ANALYZE, BUFFERS)验证? - 查询是否一次返回了过多数据?
“查”的完整思路是:
明确需要什么结果
↓
确定数据来自哪些表
↓
JOIN 形成数据来源
↓
WHERE 过滤明细
↓
GROUP BY 聚合
↓
HAVING 过滤分组
↓
SELECT 计算输出字段
↓
DISTINCT 去重
↓
ORDER BY 确定顺序
↓
LIMIT / Keyset 控制数量
↓
EXPLAIN 验证性能
核心原则:
查询不是“把数据全部拿出来再处理”,而是尽量让 PostgreSQL 在最靠近数据的地方完成筛选、关联、聚合和排序,只把真正需要的结果返回给应用程序。