환자 검색·내원 조회를 재 보고 고친다 — 5초 두 곳이 각각 441ms·1ms 가 됐다

"검색 및 선택하면 시간이 너무 오래걸려" — 체감이 아니라 숫자였다.
진단에 시간을 재는 줄을 먼저 넣었다. 어디가 느린지 숫자 없이 고치면 엉뚱한 곳을 만진다.

## 실측 (--db-patient)

  성명 검색     5,025ms → 441ms
  ListVisits    5,000ms 내외 → 1~45ms
  문맥 읽기     557ms (그대로 — 병목이 아니었다)

## 검색 — 보이지도 않는 값에 5초를 쓰고 있었다

최종내원일시와 재원여부를 파생표 두 개로 붙였는데 둘 다 P_ComInf 를 <b>통째로 GROUP BY</b>
한 뒤에야 조인된다. 조건에 맞는 환자가 몇 명이든 내원 테이블 전체를 집계했다.

그리고 최종내원일시는 <b>목록 열에 없다</b> — 차트번호·성명·주민번호·휴대전화·비고뿐이다.
아무도 보지 않는 값을 위해 5초를 쓰고 있었다. 뺐다.

재원여부는 비고 배지에 쓰므로 남기되, 행 상한을 먼저 걸고 <b>돌아온 환자에 대해서만</b>
따로 읽는다(FillInpatient). 왕복이 하나 늘지만 집계 범위가 전체 → 최대 200명으로 줄어든다.
차트번호는 바인드로 넘긴다 — 이어 붙이면 레거시의 주입 구멍을 되살린다.

## 내원 조회 — 원인을 한 번 잘못 짚었다

마스터 셋(M_DepMst·M_UidMst·M_InsMst)을 ROW_NUMBER 파생표로 접은 것이 문제라고 보고
상관 스칼라(MAX ... KEEP DENSE_RANK FIRST)로 바꿨다. 결과는 같고 정렬이 사라지는데
<b>시간은 그대로 5초였다.</b>

진짜 원인은 WHERE 였다 — <c>TRIM(A.ComChtNum) = :c</c> 는 컬럼에 함수를 씌워
인덱스를 못 타고 P_ComInf 전건을 스캔한다. 내원이 한 건인 환자도 5초가 걸린 이유다.

LIKE 접두로 인덱스 범위 스캔을 살리고, 그것만으로는 '123' 이 '1234' 까지 잡으므로
TRIM 등호를 함께 둬서 넘친 행을 걸러 낸다. 5,000ms → 1ms.

측정을 안 했으면 마스터 조인만 고치고 "고쳤다"고 보고했을 것이다.
그 변경도 유지한다(정렬이 사라지는 것은 이득이다) — 다만 그것이 원인은 아니었다.

## 게이트

- --db-patient ①~㉑ 전건 통과(판정 23건 유지 — 결과가 달라지지 않았다)
- dotnet test 336/336 · --edit-smoke 실패 0
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-19 11:23:49 +09:00
co-authored by Claude Opus 5
parent 4bdd583adb
commit 222d1987d5
2 changed files with 96 additions and 32 deletions
+12 -2
View File
@@ -1893,15 +1893,18 @@ public static class DbSmoke
{
var store = new PatientVisitStore(ConfigService.Current.ConnectionString);
// 네 갈래 SQL 이 전부 도는지 — 결과가 0건이어도 쿼리는 성공해야 한다
// 네 갈래 SQL 이 전부 도는지 — 결과가 0건이어도 쿼리는 성공해야 한다.
// <b>시간을 함께 잰다.</b> "느리다"는 체감이고, 어디가 느린지는 숫자로만 안다.
foreach (var kind in new[]
{
PatientSearchKind.ChartNumber, PatientSearchKind.Name,
PatientSearchKind.ResidentNumber, PatientSearchKind.MobilePhone,
})
{
var watch = System.Diagnostics.Stopwatch.StartNew();
var found = store.Search(kind, term);
lines.Add($" {kind,-16} {found.Count}건");
watch.Stop();
lines.Add($" {kind,-16} {found.Count,4}건 {watch.ElapsedMilliseconds,6}ms");
}
Check("① 검색 네 갈래가 예외 없이 돈다", true);
@@ -1928,7 +1931,10 @@ public static class DbSmoke
foreach (var candidate in people)
{
probed++;
var vw = System.Diagnostics.Stopwatch.StartNew();
visits = store.ListVisits(candidate.ChtNum);
vw.Stop();
lines.Add($" ListVisits {vw.ElapsedMilliseconds}ms → {visits.Count}건");
if (visits.Count > 0)
{
break;
@@ -1964,7 +1970,11 @@ public static class DbSmoke
{
var target = visits[0].ComNum;
var contexts = new PatientContextStore(ConfigService.Current.ConnectionString);
// 환자를 고른 뒤 사람이 기다리는 구간이다 — 문맥 5행 + 시각 + 복호화까지 붙는다
var loadWatch = System.Diagnostics.Stopwatch.StartNew();
var context = contexts.Load(target);
loadWatch.Stop();
lines.Add($" 문맥 읽기 {loadWatch.ElapsedMilliseconds}ms (환자를 고른 뒤 기다리는 구간)");
Check("⑧ 문맥 5행 조회가 예외 없이 돈다", context is not null,
$"내원 {target:F0} 을 읽지 못했다(목록에는 있는데 다시 읽으면 없다)");