Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
b9caf5ce
Commit
b9caf5ce
authored
Aug 17, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
merge: 热修矩阵 500 —— 补回 JOIN patients + 三层防线加固
parents
3aa69952
a0a86b94
Pipeline
#3576
failed in 0 seconds
Changes
3
Pipelines
1
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
83 additions
and
5 deletions
+83
-5
apps/pac-service/scripts/bench-matrix.ts
+22
-3
apps/pac-service/src/modules/plan/reason-temperature.sql.ts
+6
-0
apps/pac-service/tests/plan-label.spec.ts
+55
-2
No files found.
apps/pac-service/scripts/bench-matrix.ts
View file @
b9caf5ce
...
...
@@ -26,8 +26,19 @@ function inline(q: { sql: string; values: unknown[] }): string {
});
}
function
matrixSql
(
hostId
:
string
,
tenantId
:
string
,
clinicId
:
string
):
string
{
const
scope
=
{
hostId
,
tenantId
,
clinicIds
:
[],
sourceUnits
:
[]
}
as
never
;
/**
* ⚠️ **sourceUnits 必须按真实值传** —— 它非空时 `poolBaseSql` 会拼 `AND p.source_unit IN (...)`,
* 走的是另一条 SQL 分支。2026-08-17 这里硬编 `[]` 导致本地永远测不到那一支,
* 把删掉 `JOIN patients p` 的改动一路放行到线上,矩阵直接 500
* (`missing FROM-clause entry for table "p"`)。⛔ 别再退回硬编。
*/
function
matrixSql
(
hostId
:
string
,
tenantId
:
string
,
clinicId
:
string
,
sourceUnits
:
string
[],
):
string
{
const
scope
=
{
hostId
,
tenantId
,
clinicIds
:
[],
sourceUnits
}
as
never
;
const
inner
=
inline
(
planLabelAnchorsSql
(
poolBaseSql
(
scope
,
clinicId
)));
// 外层与 `CohortAttributesService.matrix` 同构(温度 CASE 简化成三档,量的是同一条 join 路径)
return
`EXPLAIN (ANALYZE, BUFFERS)
...
...
@@ -73,13 +84,21 @@ async function snapshot(sqlWithExplain: string) {
FROM followup_plans WHERE status='active' AND assignee_user_id IS NULL
GROUP BY 1,2,3 HAVING count(*) >= 10 ORDER BY 4 DESC LIMIT 5`
;
// 真实存在的品牌命名空间 —— 拿来跑「sourceUnits 非空」那一支
const
units
=
await
prisma
.
$queryRaw
<
Array
<
{
source_unit
:
string
}
>>
`
SELECT DISTINCT source_unit FROM patients WHERE source_unit IS NOT NULL LIMIT 3`
;
const
unitList
=
units
.
map
((
u
)
=>
u
.
source_unit
);
console
.
log
(
` (另跑一遍 sourceUnits=
${
JSON
.
stringify
(
unitList
)}
那一支)\n`
);
console
.
log
(
`初选矩阵基准 · 每档取
${
ROUNDS
}
轮最快值\n`
);
console
.
log
(
' plan 数 最快耗时 磁盘读 JIT(首段)'
);
console
.
log
(
' '
+
'─'
.
repeat
(
46
));
const
out
:
Array
<
Record
<
string
,
number
>>
=
[];
const
snaps
:
string
[]
=
[];
for
(
const
c
of
clinics
)
{
const
sql
=
matrixSql
(
c
.
host_id
,
c
.
tenant_id
,
c
.
target_clinic_id
);
const
sql
=
matrixSql
(
c
.
host_id
,
c
.
tenant_id
,
c
.
target_clinic_id
,
[]);
// 🔴 同一诊所再跑一遍**带品牌过滤**的那一支 —— 只测一支等于没测
await
run
(
matrixSql
(
c
.
host_id
,
c
.
tenant_id
,
c
.
target_clinic_id
,
unitList
));
let
best
=
{
ms
:
Infinity
,
reads
:
0
,
jit
:
0
};
for
(
let
r
=
0
;
r
<
ROUNDS
;
r
++
)
{
const
x
=
await
run
(
sql
);
...
...
apps/pac-service/src/modules/plan/reason-temperature.sql.ts
View file @
b9caf5ce
...
...
@@ -155,6 +155,12 @@ export function planLabelAnchorsSql(planFilter: Prisma.Sql): Prisma.Sql {
${
BOUNDS
.
warm
}
AS warm_until,
${
BOUNDS
.
anchor
}
AS anchor_at
FROM followup_plans fp
-- 🔴 p 必须留着 —— poolBaseSql 在 scope.sourceUnits 非空时会拼
-- AND p.source_unit IN (...)(多品牌隔离)。2026-08-17 线上 500 就是删了它:
-- missing FROM-clause entry for table "p"。
-- ⚠️ 本地基准脚本当时硬编了 sourceUnits=[],永远走不到那个分支 —— 测了个假场景。
-- ⛔ 改这里之前先确认 poolBaseSql 还引用了哪些别名。
JOIN patients p ON p.id = fp.patient_id
JOIN plan_reasons pr ON pr.plan_id = fp.id
CROSS JOIN LATERAL unnest(pr.potential_labels) AS lab(lbl)
${
LAST_VISIT_JOIN
}
...
...
apps/pac-service/tests/plan-label.spec.ts
View file @
b9caf5ce
import
{
refreshLabelsSql
,
countMissingLabelsSql
,
LABEL_REFRESH_BATCH
}
from
'../src/modules/plan/plan-label.sql'
;
import
{
planLabelAnchorsSql
}
from
'../src/modules/plan/reason-temperature.sql'
;
import
{
poolBaseSql
}
from
'../src/modules/plan/cohort-filter'
;
import
{
Prisma
}
from
'@prisma/client'
;
/**
...
...
@@ -22,8 +23,16 @@ describe('矩阵查询不再碰 patient_facts', () => {
expect
(
sql
).
not
.
toMatch
(
/jsonb_array_elements_text/
);
});
it
(
'⛔ 不再 JOIN patients —— 年龄已烘进标签,那张表原本要被全表扫'
,
()
=>
{
expect
(
sql
).
not
.
toMatch
(
/JOIN patients/
);
/**
* ⚠️ 这里原来断言的是「⛔ 不再 JOIN patients」—— **那条断言本身是错的**,
* 它把 2026-08-17 那个线上 500 固化成了"期望行为":`poolBaseSql` 在
* sourceUnits 非空时要引用 `p.source_unit`,删了 join 就 `missing FROM-clause`。
* ⇒ 现在改成断言**真正变了的那件事**:查询里不再出现按年龄推标签的 CASE
* (年龄已烘进 potential_labels),而 patients 这张表因为品牌隔离仍要在场。
*/
it
(
'不再在查询里按年龄推标签 —— 年龄已烘进 potential_labels'
,
()
=>
{
expect
(
sql
).
not
.
toMatch
(
/date_part
\(
'year'/
);
expect
(
sql
).
not
.
toMatch
(
/birth_date/
);
});
it
(
'改用预存的 potential_labels'
,
()
=>
{
...
...
@@ -36,6 +45,50 @@ describe('矩阵查询不再碰 patient_facts', () => {
});
});
/**
* 🔴 2026-08-17 线上 500 的回归守卫。
*
* 当时把 `JOIN patients p` 从矩阵查询里删掉了(标签预计算后"用不到年龄"),
* 但 `poolBaseSql` 在 **scope.sourceUnits 非空**时会拼 `AND p.source_unit IN (...)`
* ⇒ PG 报 `missing FROM-clause entry for table "p"`,矩阵整页 500。
*
* ⚠️ 本地基准脚本当时硬编 `sourceUnits: []`,永远走不到那一支 —— **测了个假场景**。
* ⇒ 这里不连库也能守住:把两种 scope 拼出来的 SQL 做**别名闭合检查**。
*/
describe
(
'poolBaseSql 用到的别名,查询里必须都在 FROM 里'
,
()
=>
{
const
scope
=
(
sourceUnits
:
string
[])
=>
({
hostId
:
'h'
,
tenantId
:
't'
,
clinicIds
:
[],
sourceUnits
})
as
never
;
it
(
'🔴 sourceUnits 非空时会引用 p. —— 那么查询必须 JOIN patients p'
,
()
=>
{
const
filter
=
poolBaseSql
(
scope
([
'瑞尔'
]),
'c1'
);
expect
(
filter
.
sql
).
toMatch
(
/p
\.
source_unit/
);
// 前提成立:它确实引用了 p
const
q
=
planLabelAnchorsSql
(
filter
).
sql
;
expect
(
q
).
toMatch
(
/JOIN patients p
\b
/
);
});
it
(
'sourceUnits 为空时也不能少 —— ⛔ 别按"这一支用不到"就删'
,
()
=>
{
const
q
=
planLabelAnchorsSql
(
poolBaseSql
(
scope
([]),
'c1'
)).
sql
;
expect
(
q
).
toMatch
(
/JOIN patients p
\b
/
);
});
it
(
'⭐ 别名闭合:过滤片段里出现的每个表别名,都要能在 FROM/JOIN 里找到'
,
()
=>
{
for
(
const
units
of
[[],
[
'瑞尔'
,
'瑞泰'
]])
{
const
q
=
planLabelAnchorsSql
(
poolBaseSql
(
scope
(
units
),
'c1'
)).
sql
;
const
used
=
new
Set
([...
q
.
matchAll
(
/
\b([
a-z
]{1,4})\.[
a-z_
]
+/g
)].
map
((
m
)
=>
m
[
1
]));
const
declared
=
new
Set
([
...[...
q
.
matchAll
(
/
\b(?:
FROM|JOIN
)\s
+
[
a-z_
]
+
\s
+
([
a-z
]{1,4})\b
/g
)].
map
((
m
)
=>
m
[
1
]),
...[...
q
.
matchAll
(
/AS
\s
+
([
a-z
]{1,4})\(
/g
)].
map
((
m
)
=>
m
[
1
]),
]);
for
(
const
a
of
used
)
{
if
(
a
===
'max'
||
a
===
'now'
)
continue
;
// 函数名不是别名
expect
({
units
,
alias
:
a
,
declared
:
[...
declared
]
}).
toEqual
(
expect
.
objectContaining
({
alias
:
expect
.
stringMatching
(
new
RegExp
(
`^(
${[...
declared
].
join
(
'|'
)}
)$`
))
}),
);
}
}
});
});
describe
(
'刷新 SQL 的几条结构前提'
,
()
=>
{
it
(
'🔴 带 id 游标 —— 否则全量重算每次取同一批,原地打转且不报错'
,
()
=>
{
const
first
=
text
(
refreshLabelsSql
({
afterId
:
null
,
onlyMissing
:
false
}));
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment