크롤링한 가게 데이터들을 추가하고 나서 인덱스를 작업하지 않았다. (4만건 정도)
추가로 distange_range라는 값을 추가로 주어야했다.
ex.) 가게는 250m/500m/750m 단위로 검색할 수 있게 해둔 상황. sorting을 위해
현재 위치와 100m 떨어져있으면 "distance_range": "250",
현재 위치와 450m 떨어져있으면 "distance_range": "500"
이런 식으로 값을 추가로 보내야 했다.
쿼리를 수정하니 원래 1~2초 단위로 실행되던 api가 15초 이상이 걸렸다.
return stores.stream()
.map(store -> {
int distance = store.getDistance();
int distanceRange = distance <= 250 ? 250
: distance <= 500 ? 500
: distance <= 750 ? 750
: 0;
-> 속도에 큰 변화가 없었다.(여전히 10초 대)
long totalStart = System.currentTimeMillis();
long start = System.currentTimeMillis();
List<String> storeIds = findStoreService.메서드명1(변수...);
System.out.println("메서드명1: " + (System.currentTimeMillis() - start) + "ms");
start = System.currentTimeMillis();
List<StoreDistanceDTO> storeDetails = findStoreService.메서드명2(변수...);
System.out.println("메서드명2: " + (System.currentTimeMillis() - start) + "ms");
System.out.println("Total time: " + (System.currentTimeMillis() - totalStart) + "ms");
이런 식으로 실제 시간을 측정하는 로그를 추가했다.
이 방법으로 서비스 단에서 객체 변환이 아닌 DB 조회가 오래 걸리는 것임을 확인했다. (객체 변환에서는 2ms 정도밖에 소요되지 않았다.)
-- 원래 사용하던 CTE절
WITH menu_counts AS (
SELECT store_id, COUNT(*) AS cnt
FROM menu
WHERE represent = 'Y' -- 전체 menu 테이블 스캔!
GROUP BY store_id
)
-- 개선한 CTE 절
WITH menu_counts AS (
SELECT store_id, COUNT(*) AS cnt
FROM menu
WHERE represent = 'Y'
AND store_id IN (N개) -- 해당하는 N개만 조회
GROUP BY store_id
)
LEFT JOIN
(SELECT st.store_id, f.type1, f.food_type_id
FROM store st
JOIN food_type f ON st.category = f.type2
WHERE st.store_id IN (N개) -- store 테이블 또 조회
) f ON s.store_id = f.store_id
LEFT JOIN food_type f ON s.category = f.type2 -- 직접 JOIN
-> 2초대로 줄었다.
TABLE FULL SCAN이 일어나 데이터 조회에 많은 시간이 소요될 수 있다.가게를 조회할 때 마다, 메뉴/방송/가게/음식 타입 테이블을 전부 조회한다.
-- 인덱스가 있는지 확인하고 없으면 만든다.
CREATE INDEX IF NOT EXISTS idx_store_id ON store(store_id);
-- 인덱스가 있는지만 확인한다.
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'store';
-> 실행시간이 30ms 대로 줄었다.
다른 메서드에서도 병목현상으로 3초 정도로 실행되고 있어서 쿼리를 개선했다.
hh24:mi 부분에서 에러가 났었던 적이 있었다.SELECT DISTINCT bh.store_id
FROM business_hours bh
WHERE ...
AND EXISTS (
SELECT 1 FROM business_hours bh2 -- 1번째 스캔
WHERE bh2.store_id = bh.store_id
AND bh2.business_type = 'O0001'
...
)
AND EXISTS (
SELECT 1 FROM business_hours bh3, bh4 -- 2번째, 3번째 스캔
WHERE bh3.store_id = bh.store_id
AND bh4.store_id = bh.store_id
...
)
WHERE bh2.${currentDay} IS NOT NULL -- currentDay = 'mon', 'tue' 등
WHERE bh2.${currentDay}::text NOT LIKE '%휴무%'
WITH valid_stores AS (
-- 폐업하지 않은 store만 먼저 필터링
),
dinner_hours AS (
SELECT
store_id,
MAX(CASE WHEN business_type = 'O0001' THEN ${currentDay} END) as open_time,
MAX(CASE WHEN business_type = 'C0001' THEN ${currentDay} END) as close_time
FROM business_hours
WHERE store_id IN (SELECT store_id FROM valid_stores)
AND business_type IN ('O0001', 'C0001')
AND ${currentDay} IS NOT NULL
AND ${currentDay} NOT IN ('휴무', '정기휴무', '')
GROUP BY store_id -- 핵심: 한 번에 집계!
)
MAX(CASE WHEN business_type = 'O0001' THEN ${currentDay} END) as open_time,
MAX(CASE WHEN business_type = 'C0001' THEN ${currentDay} END) as close_time
## 개선 전:
-- O0001 찾기 위해 테이블 스캔
SELECT ... WHERE business_type = 'O0001'
-- C0001 찾기 위해 또 테이블 스캔
SELECT ... WHERE business_type = 'C0001'
### 개선 후:
-- 한 번에 가져와서 메모리에서 분리
CASE WHEN business_type = 'O0001' THEN ...
CASE WHEN business_type = 'C0001' THEN ...
## 개선 전 :
EXISTS (
SELECT 1 FROM business_hours bh2
WHERE ... AND bh2.${currentDay} NOT LIKE '%휴무%'
)
-- 각 store마다 반복 검사!
## 개선 후 :
WHERE business_type IN ('O0001', 'C0001')
AND ${currentDay} IS NOT NULL
AND ${currentDay} NOT IN ('휴무', '정기휴무', '')
AND ${currentDay}::text NOT LIKE '%휴무%'
-- 한 번만 검사!
ST_DistanceSphereWHERE ST_DistanceSphere(location, ST_GeomFromText(...)) <= 750
GIST 인덱스는 공간 범위 검색에 최적화되어 있다.ST_DistanceSphere는 함수 실행 결과를 비교한다.ST_DWithin으로 공간 인덱스 직접 활용WHERE ST_DWithin(
s.location::geography,
ST_SetSRID(ST_MakePoint(#{currentLong}, #{currentLat}), 4326)::geography,
750
)
GIST (Generalized Search Tree):
MBR(Minimum Bounding Region) (최소 경계 영역)이라고 한다.## 원래 (geometry):
sqlST_DistanceSphere(location, ST_GeomFromText(...))
-- geometry 타입, 거리는 "도" 단위 → 미터로 변환 필요
## 개선 (geography):
sqlST_DWithin(s.location::geography, ...::geography, 750)
-- geography 타입, 거리가 바로 "미터" 단위
-- geometry: 1도 ≈ 111km (위도에 따라 다름)
WHERE ST_Distance(location, point) <= 0.0067 -- 750m를 도로 변환?
-- geography: 직관적!
WHERE ST_DWithin(location, point, 750) -- 750미터
ServBay에서 PostGIS 사용하기: PostgreSQL에 공간 기능 추가
[Postgresql] Gist 인덱스란?