1편에서 250개의 쿼리를 1개로 줄이는 최적화를 진행했습니다.
-- 250개 좌표에 대해 LATERAL JOIN으로 한 번에 조회
WITH target_coords AS (
SELECT * FROM (VALUES ('11010', 37.5, 127.0), ...)
AS t(region_code, target_lat, target_lng)
)
SELECT DISTINCT ON (tc.region_code, hr.target_year)
tc.region_code,
hr.target_year,
hr.score
FROM target_coords tc
CROSS JOIN LATERAL (
SELECT target_year, score
FROM hazard_results
WHERE risk_type = %s
AND target_year BETWEEN %s AND %s -- 예: 2025 ~ 2100
ORDER BY (거리 계산) ASC
LIMIT 1
) hr
ORDER BY tc.region_code, hr.target_year
성능은 극적으로 개선되었지만... 새로운 문제가 발생했습니다.
{
"regionScores": {
"11010": { "2025": 33.1 }, // ❌ 2025년만!
"11020": { "2025": 30.9 }, // ❌ 2025년만!
"11030": { "2025": 33.5 } // ❌ 2025년만!
}
}
{
"regionScores": {
"11010": {
"2025": 33.1,
"2030": 35.2,
"2035": 37.5,
// ... 중략
"2100": 55.8
}
}
}
FROM target_coords tc -- 250개 행
CROSS JOIN LATERAL (
SELECT target_year, score
FROM hazard_results
WHERE risk_type = 'extreme_heat'
AND target_year BETWEEN '2025' AND '2100' -- 🔴 여기가 문제
ORDER BY (거리 계산) ASC
LIMIT 1 -- 🔴 각 지역당 1개만!
) hr
LATERAL JOIN의 동작:
1. target_coords의 각 행(지역)마다 서브쿼리 실행
2. BETWEEN '2025' AND '2100' 범위 내에서
3. 거리 기준으로 정렬해서
4. LIMIT 1로 가장 가까운 데이터 1개만 선택
문제점:
현재 쿼리:
"각 지역마다, 2025~2100년 중에서 가장 가까운 데이터 1개만 줘"
→ 결과: 지역당 1개 (대부분 2025년)
원하는 동작:
"각 지역마다, 각 연도별로 가장 가까운 데이터를 줘"
→ 결과: 지역당 16개 (연도별로)
WITH target_coords AS (
-- 250개 지역 좌표
SELECT * FROM (VALUES
('11010', 37.5, 127.0),
('11020', 37.6, 127.1),
-- ... 250개
) AS t(region_code, target_lat, target_lng)
),
target_years AS (
-- 16개 연도 (NEW!)
SELECT * FROM (VALUES
('2025'), ('2030'), ('2035'), ('2040'),
('2045'), ('2050'), ('2055'), ('2060'),
('2065'), ('2070'), ('2075'), ('2080'),
('2085'), ('2090'), ('2095'), ('2100')
) AS y(year)
)
SELECT DISTINCT ON (tc.region_code, ty.year)
tc.region_code,
ty.year as target_year,
hr.score
FROM target_coords tc
CROSS JOIN target_years ty -- 🟢 연도도 CROSS JOIN!
CROSS JOIN LATERAL (
SELECT score
FROM hazard_results
WHERE risk_type = %s
AND target_year = ty.year -- 🟢 특정 연도만 조회
ORDER BY (
POW(latitude - tc.target_lat::numeric, 2) +
POW(longitude - tc.target_lng::numeric, 2)
) ASC
LIMIT 1 -- 🟢 해당 연도의 최근접 데이터 1개
) hr
ORDER BY tc.region_code, ty.year
Before:
┌─────────┐
│ 지역 250개 │ → LATERAL → 각 지역당 1개 데이터
└─────────┘
After:
┌─────────┐ ┌────────┐
│ 지역 250개 │ × │ 연도 16개 │ → 250 × 16 = 4,000개 조합
└─────────┘ └────────┘
↓
각 (지역, 연도) 조합마다 LATERAL 실행
↓
4,000개 데이터 (지역당 16개 연도)
query_region_batch = f"""
WITH target_coords AS (
SELECT * FROM (VALUES {coords_clause})
AS t(region_code, target_lat, target_lng)
)
SELECT DISTINCT ON (tc.region_code, hr.target_year)
tc.region_code,
hr.target_year,
hr.{score_col} as score
FROM target_coords tc
CROSS JOIN LATERAL (
SELECT target_year, {score_col}
FROM hazard_results
WHERE risk_type = %s
AND target_year BETWEEN %s AND %s
ORDER BY (거리 계산) ASC
LIMIT 1
) hr
ORDER BY tc.region_code, hr.target_year
"""
# 고정된 연도 범위 생성
fixed_years = list(range(2025, 2101, 5)) # [2025, 2030, ..., 2100]
query_region_batch = f"""
WITH target_coords AS (
SELECT * FROM (VALUES {coords_clause})
AS t(region_code, target_lat, target_lng)
),
target_years AS (
SELECT * FROM (VALUES {','.join("('" + str(y) + "')" for y in fixed_years)})
AS y(year)
)
SELECT DISTINCT ON (tc.region_code, ty.year)
tc.region_code,
ty.year as target_year,
hr.{score_col} as score
FROM target_coords tc
CROSS JOIN target_years ty
CROSS JOIN LATERAL (
SELECT {score_col}
FROM hazard_results
WHERE risk_type = %s
AND target_year = ty.year
ORDER BY (
POW(latitude - tc.target_lat::numeric, 2) +
POW(longitude - tc.target_lng::numeric, 2)
) ASC
LIMIT 1
) hr
ORDER BY tc.region_code, ty.year
"""
psycopg2.errors.UndefinedFunction: operator does not exist: character varying = integer
LINE 9: AND target_year IN (2025,2030,2035,...)
^
HINT: No operator matches the given name and argument types.
You might need to add explicit type casts.
target_year 컬럼: VARCHAR (문자열)2025 (정수)# Before (정수)
AND target_year IN ({','.join(str(y) for y in fixed_years)})
# 결과: AND target_year IN (2025,2030,2035,...)
# After (문자열로 감싸기)
AND target_year IN ({','.join("'" + str(y) + "'" for y in fixed_years)})
# 결과: AND target_year IN ('2025','2030','2035',...)
| 방식 | 쿼리 수 | 결과 데이터 | 실행 시간 | 완성도 |
|---|---|---|---|---|
| 원본 (Python Loop) | 250개 | 250 × 16 = 4,000개 | ~30초 | ❌ 타임아웃 |
| 1차 최적화 (LATERAL) | 1개 | 250개 (❌) | ~1초 | ❌ 데이터 부족 |
| 2차 최적화 (연도 CROSS JOIN) | 1개 | 4,000개 (✅) | ~2초 | ✅ 완벽 |
-- ❌ 잘못된 생각: "LATERAL이 알아서 연도별로 반복하겠지"
CROSS JOIN LATERAL (
WHERE target_year BETWEEN '2025' AND '2100'
LIMIT 1
)
-- ✅ 올바른 방법: "반복할 것을 명시적으로 CROSS JOIN"
CROSS JOIN target_years ty
CROSS JOIN LATERAL (
WHERE target_year = ty.year
LIMIT 1
)
-- LIMIT 1의 의미:
-- "각 LATERAL 실행마다 1개만 반환"
-- ≠ "전체 결과 중 1개만 반환"
-- 지역 250개 × LATERAL LIMIT 1 = 250개 결과
-- 지역 250개 × 연도 16개 × LATERAL LIMIT 1 = 4,000개 결과
SELECT DISTINCT ON (tc.region_code, ty.year)
-- (지역, 연도) 조합마다 첫 번째 행만 선택
-- 이미 LATERAL에서 LIMIT 1을 했으므로 중복 방지용
{
"regionScores": {
"11010": {
"2025": 33.1,
"2030": 35.4,
"2035": 38.2,
"2040": 41.5,
"2045": 44.8,
"2050": 48.1,
"2055": 51.6,
"2060": 54.9,
"2065": 58.3,
"2070": 61.8,
"2075": 65.2,
"2080": 68.7,
"2085": 72.1,
"2090": 75.6,
"2095": 79.0,
"2100": 82.5
},
"11020": { /* 16개 연도 */ },
// ... 248개 지역 더
},
"siteAALs": {
"uuid1": { /* 16개 연도 */ },
"uuid2": { /* 16개 연도 */ }
}
}
# 단순히 쿼리가 성공했다고 끝이 아니라
region_rows = db.execute_query(...)
# 결과 개수를 확인
expected_count = len(REGION_COORD_MAP) * len(fixed_years)
actual_count = len(region_rows)
assert actual_count == expected_count, \
f"Expected {expected_count}, got {actual_count}"
LATERAL JOIN을 사용할 때는:
250개 쿼리를 1개로 줄이는 것도 중요하지만,
올바른 결과를 반환하는 것이 더 중요합니다.