PL/pgSQL存储过程CTE使用时persnr列名歧义错误排查
问题概述
编写PL/pgSQL存储过程实现UPSERT逻辑(存在匹配数据则更新,无匹配则插入),采用CTE结合UPDATE+INSERT的结构,但运行时抛出ERROR: column reference "persnr" is ambiguous错误,尝试添加表前缀引用后仍未解决。
相关代码与环境
存储过程代码
CREATE PROCEDURE CPM_SP_KST_UMBUCHUNG( _scenario_plan_0 varchar(30), session_usr varchar(30) ) LANGUAGE plpgsql AS $$ -- SP DECLARE --Variable Declaration _scenario_plan_1 character varying; _scenario_plan_2 character varying; BEGIN --SP _scenario_plan_1 = (SELECT COD_SCENARIO_SUCC FROM SCENARIO WHERE COD_SCENARIO = _scenario_plan_0); _scenario_plan_2 = (SELECT COD_SCENARIO_SUCC FROM SCENARIO WHERE COD_SCENARIO = (SELECT COD_SCENARIO_SUCC FROM SCENARIO WHERE COD_SCENARIO = _scenario_plan_0)); BEGIN -- STATUS N WITH cte_update_rest AS ( UPDATE AW_001_000001_000001 HR_SET SET COD_DEST1 = V.COD_DEST1_NEU ,COD_DEST2 = V.COD_DEST2_NEU ,COD_DEST3 = V.COD_DEST3_NEU ,COD_AZIENDA = V.COD_AZIENDA_NEU ,IMPORTO = CAST(V.IMPORTO as NUMERIC)/CAST(V.HR_ANTEIL as NUMERIC) * CAST(V.KST_ANTEIL as NUMERIC) ,ANTEIL = 100 * CAST(V.HR_ANTEIL as NUMERIC) * CAST(V.KST_ANTEIL as NUMERIC) ,ZEIT = CAST(V.ZEIT as NUMERIC)/CAST(V.HR_ANTEIL as NUMERIC) * CAST(V.KST_ANTEIL as NUMERIC) ,PROVENIENZA = 'CPM_SP_KST_UMBUCHUNG' ,USERUPD = session_usr ,DATEUPD = NOW() FROM V_KST_AW_BASIS V WHERE V.HR_SCE in (_scenario_plan_0, _scenario_plan_1, _scenario_plan_2) AND V.KST_SCE = _scenario_plan_0 AND V.STATUS = 'N' AND HR_SET.COD_SCENARIO = V.HR_SCE AND HR_SET.COD_AZIENDA = V.COD_AZIENDA AND HR_SET.PERSNR = V.PERSNR AND HR_SET.COD_PERIODO = V.COD_PERIODO AND HR_SET.COD_SCENARIO = V.HR_SCE AND HR_SET.FUNKTION = V.FUNKTION AND HR_SET.LOHNART = V.LOHNART AND HR_SET.COD_CONTO = V.COD_CONTO AND HR_SET.COD_VALUTA = V.COD_VALUTA AND HR_SET.COD_CATEGORIA = V.COD_CATEGORIA AND HR_SET.BUCHUNG = V.BUCHUNG AND HR_SET.TARIF = V.TARIF AND HR_SET.EN_VERSION = V.EN_VERSION AND HR_SET.COD_DEST1 = V.COD_DEST1 AND HR_SET.COD_DEST2 = V.COD_DEST2 AND HR_SET.COD_DEST3 = V.COD_DEST3 RETURNING * ) INSERT INTO AW_001_000001_000001 ( OID ,PERSNR ,COD_AZIENDA ,COD_SCENARIO ,COD_PERIODO ,FUNKTION ,LOHNART ,COD_CONTO ,COD_DEST1 ,COD_DEST2 ,COD_DEST3 ,COD_VALUTA ,IMPORTO ,ANTEIL ,ZEIT ,BUCHUNG ,COD_CATEGORIA ,TARIF ,EN_VERSION ,PROVENIENZA ,USERUPD ,DATEUPD ) (SELECT uuid_generate_v4() as OID ,cte.PERSNR ,cte.COD_AZIENDA_NEU as COD_AZIENDA ,cte.COD_SCENARIO ,cte.COD_PERIODO ,cte.FUNKTION ,cte.LOHNART ,cte.COD_CONTO ,cte.COD_DEST1_NEU ,cte.COD_DEST2_NEU ,cte.COD_DEST3_NEU ,cte.COD_VALUTA ,CAST(cte.IMPORTO as NUMERIC)/CAST(cte.cte.HR_ANTEIL as NUMERIC) * CAST(cte.KST_ANTEIL as NUMERIC) as IMPORTO ,100 * CAST(cte.HR_ANTEIL as NUMERIC) * CAST(cte.KST_ANTEIL as NUMERIC) as ANTEIL --das muss noch angepasst werden damit die Berechnung stimmt ,CAST(cte.ZEIT as NUMERIC)/CAST(cte.HR_ANTEIL as NUMERIC) * CAST(cte.KST_ANTEIL as NUMERIC) as ZEIT ,cte.BUCHUNG ,cte.COD_CATEGORIA ,cte.TARIF ,cte.EN_VERSION ,'CPM_SP_KST_UMBUCHUNG' as PROVENIENZA ,session_usr as USERUPD ,NOW() as DATEUPD FROM cte_update_rest cte WHERE cte.HR_SCE in (_scenario_plan_0, _scenario_plan_1, _scenario_plan_2) AND cte.KST_SCE = _scenario_plan_0 AND cte.STATUS = 'N' ) ; END; -- STATUS N BEGIN -- STATUS U SELECT 'U' as PLATZHALTER; END; -- STATUS U BEGIN -- STATUS L SELECT 'L' as PLATZHALTER; END; -- STATUS L END; --SP $$ -- SP
视图代码
CREATE VIEW V_KST_AW_BASIS AS SELECT KST_SET.PERSNR ,HR_SET.COD_AZIENDA AS COD_AZIENDA ,KST_SET.COD_AZIENDA_NEU ,HR_SET.COD_SCENARIO AS HR_SCE ,KST_SET.COD_SCENARIO AS KST_SCE ,HR_SET.COD_PERIODO ,HR_SET.FUNKTION ,HR_SET.LOHNART ,HR_SET.COD_CONTO ,HR_SET.COD_DEST1 as COD_DEST1 ,HR_SET.COD_DEST2 as COD_DEST2 ,HR_SET.COD_DEST3 as COD_DEST3 ,KST_SET.COD_DEST1_NEU as COD_DEST1_NEU ,KST_SET.COD_DEST2_NEU as COD_DEST2_NEU ,KST_SET.COD_DEST3_NEU as COD_DEST3_NEU ,HR_SET.COD_VALUTA ,HR_SET.IMPORTO ,CAST(HR_SET.ANTEIL AS NUMERIC)/100 as HR_ANTEIL ,KST_SET.ANTEIL_NEU AS KST_ANTEIL ,HR_SET.ZEIT ,HR_SET.BUCHUNG ,HR_SET.COD_CATEGORIA ,HR_SET.TARIF ,HR_SET.EN_VERSION ,KST_SET.STATUS FROM AW_001_000001_000001 HR_SET RIGHT JOIN AW_001_000004_000001 KST_SET ON HR_SET.PERSNR = KST_SET.PERSNR AND HR_SET.COD_AZIENDA = KST_SET.COD_AZIENDA AND HR_SET.COD_DEST1 = KST_SET.COD_DEST1 AND HR_SET.COD_DEST2 = KST_SET.COD_DEST2 AND HR_SET.COD_DEST3 = KST_SET.COD_DEST3 WHERE KST_SET.STATUS is not null
环境信息
PostgreSQL 13.10 (Ubuntu 13.10-1.pgdg20.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0, 64-bit
错误原因分析
CTE的RETURNING * 导致列来源混淆
CTE中UPDATE ... RETURNING *仅返回被更新表AW_001_000001_000001的所有列,但后续INSERT语句中引用的COD_AZIENDA_NEU、HR_SCE、KST_SCE等字段属于视图V_KST_AW_BASIS,并非被更新表的列。当代码试图从CTE中获取这些不存在的列时,PostgreSQL会尝试匹配同名列,而PERSNR在被更新表和视图中都存在,导致无法确定引用的是哪一个,触发歧义错误。代码笔误加剧解析混乱
INSERT语句的SELECT部分存在明显笔误:CAST(cte.cte.HR_ANTEIL as NUMERIC),重复的cte.前缀让PostgreSQL解析列时出现混乱,进一步触发列歧义判断。视图与更新表的列重叠
视图V_KST_AW_BASIS的PERSNR来自KST_SET表,而被更新表AW_001_000001_000001也有同名列,当CTE返回更新表的PERSNR,而代码逻辑中隐含试图引用视图的PERSNR时,就会出现冲突。
修复方案
1. 明确CTE返回的列,包含视图所需字段
将RETURNING *改为明确返回后续INSERT需要的字段,包括视图中的必要字段(通过UPDATE的FROM子句中的视图别名引用):
WITH cte_update_rest AS ( UPDATE AW_001_000001_000001 HR_SET SET COD_DEST1 = V.COD_DEST1_NEU ,COD_DEST2 = V.COD_DEST2_NEU ,COD_DEST3 = V.COD_DEST3_NEU ,COD_AZIENDA = V.COD_AZIENDA_NEU ,IMPORTO = CAST(V.IMPORTO as NUMERIC)/CAST(V.HR_ANTEIL as NUMERIC) * CAST(V.KST_ANTEIL as NUMERIC) ,ANTEIL = 100 * CAST(V.HR_ANTEIL as NUMERIC) * CAST(V.KST_ANTEIL as NUMERIC) ,ZEIT = CAST(V.ZEIT as NUMERIC)/CAST(V.HR_ANTEIL as NUMERIC) * CAST(V.KST_ANTEIL as NUMERIC) ,PROVENIENZA = 'CPM_SP_KST_UMBUCHUNG' ,USERUPD = session_usr ,DATEUPD = NOW() FROM V_KST_AW_BASIS V WHERE V.HR_SCE in (_scenario_plan_0, _scenario_plan_1, _scenario_plan_2) AND V.KST_SCE = _scenario_plan_0 AND V.STATUS = 'N' AND HR_SET.COD_SCENARIO = V.HR_SCE AND HR_SET.COD_AZIENDA = V.COD_AZIENDA AND HR_SET.PERSNR = V.PERSNR AND HR_SET.COD_PERIODO = V.COD_PERIODO AND HR_SET.FUNKTION = V.FUNKTION AND HR_SET.LOHNART = V.LOHNART AND HR_SET.COD_CONTO = V.COD_CONTO AND HR_SET.COD_VALUTA = V.COD_VALUTA AND HR_SET.COD_CATEGORIA = V.COD_CATEGORIA AND HR_SET.BUCHUNG = V.BUCHUNG AND HR_SET.TARIF = V.TARIF AND HR_SET.EN_VERSION = V.EN_VERSION AND HR_SET.COD_DEST1 = V.COD_DEST1 AND HR_SET.COD_DEST2 = V.COD_DEST2 AND HR_SET.COD_DEST3 = V.COD_DEST3 -- 明确返回需要的字段,包括视图中的必要内容 RETURNING HR_SET.PERSNR, V.COD_AZIENDA_NEU, HR_SET.COD_SCENARIO, HR_SET.COD_PERIODO, HR_SET.FUNKTION, HR_SET.LOHNART, HR_SET.COD_CONTO, V.COD_DEST1_NEU, V.COD_DEST2_NEU, V.COD_DEST3_NEU, HR_SET.COD_VALUTA, HR_SET.IMPORTO, V.HR_ANTEIL, V.KST_ANTEIL, HR_SET.ZEIT, HR_SET.BUCHUNG, HR_SET.COD_CATEGORIA, HR_SET.TARIF, HR_SET.EN_VERSION, V.HR_SCE, V.KST_SCE, V.STATUS )
2. 修正INSERT语句中的笔误
将CAST(cte.cte.HR_ANTEIL as NUMERIC)修改为CAST(cte.HR_ANTEIL as NUMERIC)。
3. 确保所有列引用明确
调整INSERT的SELECT语句,确保所有列都来自CTE明确返回的字段:
INSERT INTO AW_001_000001_000001 ( OID, PERSNR, COD_AZIENDA, COD_SCENARIO, COD_PERIODO, FUNKTION, LOHNART, COD_CONTO, COD_DEST1, COD_DEST2, COD_DEST3, COD_VALUTA, IMPORTO, ANTEIL, ZEIT, BUCHUNG, COD_CATEGORIA, TARIF, EN_VERSION, PROVENIENZA, USERUPD, DATEUPD ) SELECT uuid_generate_v4() as OID, cte.PERSNR, cte.COD_AZIENDA_NEU as COD_AZIENDA, cte.COD_SCENARIO, cte.COD_PERIODO, cte.FUNKTION, cte.LOHNART, cte.COD_CONTO, cte.COD_DEST1_NEU, cte.COD_DEST2_NEU, cte.COD_DEST3_NEU, cte.COD_VALUTA, CAST(cte.IMPORTO as NUMERIC)/CAST(cte.HR_ANTEIL as NUMERIC) * CAST(cte.KST_ANTEIL as NUMERIC) as IMPORTO, 100 * CAST(cte.HR_ANTEIL as NUMERIC) * CAST(cte.KST_ANTEIL as NUMERIC) as ANTEIL, CAST(cte.ZEIT as NUMERIC)/CAST(cte.HR_ANTEIL as NUMERIC) * CAST(cte.KST_ANTEIL as NUMERIC) as ZEIT, cte.BUCHUNG, cte.COD_CATEGORIA, cte.TARIF, cte.EN_VERSION, 'CPM_SP_KST_UMBUCHUNG' as PROVENIENZA, session_usr as USERUPD, NOW() as DATEUPD FROM cte_update_rest cte WHERE cte.HR_SCE in (_scenario_plan_0, _scenario_plan_1, _scenario_plan_2) AND cte.KST_SCE = _scenario_plan_0 AND cte.STATUS = 'N'
额外优化建议
- 优化
_scenario_plan_1和_scenario_plan_2的获取逻辑,减少嵌套查询:SELECT COD_SCENARIO_SUCC, (SELECT COD_SCENARIO_SUCC FROM SCENARIO WHERE COD_SCENARIO = s.COD_SCENARIO_SUCC) INTO _scenario_plan_1, _scenario_plan_2 FROM SCENARIO s WHERE COD_SCENARIO = _scenario_plan_0; - 考虑使用PostgreSQL原生的
INSERT ... ON CONFLICT语法实现UPSERT,该语法更简洁高效,但需要为目标

