上周被拉去查一个库存批次接口的问题,反馈是同一段代码、同一批数据,有时候查得到有时候查不到。第一反应是连接池复用连接的时候状态没清干净,顺着这个思路查下去,发现还真跟"连接状态"有关系,只是不在连接池那层,在 SQL 自己身上。
那条 SQL 的 WHERE 里塞了两个函数:f_set_batch 把当前要查的批次号记到会话里,f_get_batch 再把这个值读出来去过滤。写的人大概是想先 set 再 get,可实测下来,这个顺序压根靠不住。
环境是 KES V009R001C010:
css
ksql -h 127.0.0.1 -p 54321 -U app_user -d app_db
搭场景:一张库存表,两个函数
用普通用户连上去,建表插数据:
sql
create table app_schema.t_seq_stock (
stock_id integer primary key,
batch_no integer not null,
sku_no varchar(20) not null,
qty integer not null
);
insert into app_schema.t_seq_stock(stock_id, batch_no, sku_no, qty) values
(1, 10, 'SKU-A01', 100),
(2, 10, 'SKU-A02', 50),
(3, 20, 'SKU-B01', 200),
(4, 30, 'SKU-C01', 80);
再建两个函数复刻接口里"先记住状态,再拿状态去查"的写法:f_set_batch 往会话里写批次号,f_get_batch 从会话里读出来:
sql
create or replace function app_schema.f_set_batch(p_batch integer)
returns integer language plpgsql as $$
begin
perform set_config('app.current_batch', p_batch::text, false);
return 1;
end;
$$;
create or replace function app_schema.f_get_batch()
returns integer language plpgsql as $$
begin
return current_setting('app.current_batch', true)::integer;
end;
$$;
set_config 写进去的值,只在当前这个数据库连接的生命周期里有效,换一个连接就没了------这个特性正是这个坑的核心。
新连上去就跑,直接查空
新开一个连接,模拟接口刚从连接池拿到一个从没用过的连接,直接跑这条查询:
csharp
select
s.stock_id, s.batch_no, s.sku_no, s.qty
from app_schema.t_seq_stock s
where s.batch_no = app_schema.f_get_batch()
and app_schema.f_set_batch(10) = 1;

0 行。这条 SQL 写的人大概是这么想的:先靠 f_set_batch(10) 把批次定成 10,再靠 f_get_batch() 读出这个 10 去过滤 batch_no。可它俩顺序反了------f_get_batch() 写在前面,先执行,这时候 f_set_batch 压根还没被调用过,会话里这个变量是空的,batch_no = 空 自然查不出东西。
同一个连接里,正确顺序和错误顺序放一起看
不断开连接,先手动跑一遍"顺序对"的版本:
csharp
select
s.stock_id, s.batch_no, s.sku_no, s.qty
from app_schema.t_seq_stock s
where app_schema.f_set_batch(10) = 1
and s.batch_no = app_schema.f_get_batch();
紧接着在同一个连接里,把顺序倒回去,再跑一遍最开始那条"顺序错"的版本:
csharp
select
s.stock_id, s.batch_no, s.sku_no, s.qty
from app_schema.t_seq_stock s
where s.batch_no = app_schema.f_get_batch()
and app_schema.f_set_batch(10) = 1;

第一条查出 2 行,没问题,f_set_batch 先执行,批次定成了 10,f_get_batch 读到的也是 10。奇怪的是第二条------刚才在新连接里明明是 0 行的"错误顺序"写法,这次也查出了 2 行。
这就是这个坑最容易骗人的地方。会话里的变量在第一条查询执行完之后已经被设成了 10,没有任何东西会把它清空,第二条查询虽然把 f_get_batch() 写在前面,但它读到的是上一条查询留下的旧值,不是这条查询自己该有的状态。查出结果不代表这条 SQL 写对了,只是运气好,前面刚好有人帮它把变量填上了。
断开重连,"错误顺序"立刻现原形
把这个连接断掉,重新连一次,跑同一条"错误顺序"的 SQL:
css
ksql -h 127.0.0.1 -p 54321 -U app_user -d app_db
vbnet
set search_path to app_schema, public;
select
s.stock_id, s.batch_no, s.sku_no, s.qty
from app_schema.t_seq_stock s
where s.batch_no = app_schema.f_get_batch()
and app_schema.f_set_batch(10) = 1;

又是 0 行。跟最开始一模一样。这就证明了上一步那 2 行是巧合,不是这条 SQL 本身可靠------连接池里的连接是被复用的,一个连接可能被 A 请求用过、留了状态,紧接着被 B 请求拿去跑这条"顺序错"的 SQL,B 能不能查到东西完全看运气,取决于前一个用这个连接的人有没有刚好设置过同一个批次。这才是接口"有时候查得到有时候查不到"的真实原因,跟数据、跟缓存都没关系。
执行计划能看出这两个条件是绑在一起判断的
对"顺序对"的那条 SQL 跑一下执行计划:
csharp
explain analyze
select
s.stock_id, s.batch_no, s.sku_no, s.qty
from app_schema.t_seq_stock s
where app_schema.f_set_batch(10) = 1
and s.batch_no = app_schema.f_get_batch();

Filter: ((f_set_batch(10) = 1) AND (batch_no = f_get_batch())),两个条件被合并成一个 Filter 表达式一起判断,顺序跟 SQL 里写的一致。这只能说明这次跑出来的计划没有把条件顺序倒过来,不代表 KES 在任何场景下都保证严格按书写顺序执行------SQL 是声明式语言,WHERE 里的条件顺序从来不是数据库对外承诺的东西,优化器有没有权利调整、什么情况下会调整,不能只看这一次的执行计划就下死结论。真正靠得住的判断依据还是前面那几步的连接实验,不是这张执行计划。
改法:状态设置和查询彻底分开
把 f_set_batch 挪到查询外面,业务代码里先显式调用一次,再单独发起一条只读的查询:
csharp
select app_schema.f_set_batch(10);
select
s.stock_id, s.batch_no, s.sku_no, s.qty
from app_schema.t_seq_stock s
where s.batch_no = app_schema.f_get_batch();

2 行,不管这个连接之前有没有被别人用过、留没留状态,这两条语句拆开之后结果都是稳定的。真要往下追,更彻底的办法是这个批次号本来就不用靠会话变量传递,接口拿到参数直接 where batch_no = 10 传进去就完了,set_config/current_setting 这种会话状态方案本身也只是个过渡手段,能不用就不用。
顺手记几条
- WHERE 子句里不要放会修改数据、修改会话状态、写日志这类有副作用的函数,判断标准很简单:这个函数除了返回值还顺便干了别的事,就不该出现在 WHERE 里。
- 确实需要"先处理状态再查询",把这两步拆成应用代码里两条独立的语句,别指望数据库替你保证执行顺序。
- 排查这类"结果时对时错"的问题,思路就是这篇走的路线:同一个连接反复跑 vs 断开重连跑,对比结果是否一致,比瞪着 SQL 文本猜有用得多。
- 纯读取、不修改任何状态的函数,KES 里可以声明成
IMMUTABLE或STABLE,一是给优化器一个准确的信号,二是给后面看代码的人一个明确提示:这个函数没有副作用,可以放心用。