무거운 SQL은 cost 문턱 대신 DB 상태를 아는 OwlDB가 판정한다

AI가 쓴 SQL이 운영 DB를 오래 붙잡으면 같은 DB를 쓰는 주문 화면과 정산 배치가 함께 느려집니다. 이를 막는 흔한 방법은 실행 전에 EXPLAIN을 보고 cost가 일정 값을 넘는 SQL을 돌려보내는 것입니다. 그런데 cost는 옵티마이저가 통계로 짐작한 값이라, 통계가 낡거나 DB가 바쁘면 실제 실행 시간과 어긋납니다. 문턱을 어디에 두어도 멀쩡한 조회를 막거나 무거운 조회를 통과시킵니다.
그래서 무거운 SQL의 판정은 DB의 지금 상태를 아는 쪽에 둡니다. 직접 연결(관리자가 접속 정보를 입력한 연결)에서는 실행 계획을 판정 없이 모델에게 보여 주고, 실행이 시작된 뒤의 피해를 시간 예산과 동시 실행 수로 묶습니다. OwlDB로 연결한 DB에서는 OwlDB가 실행 전에 판정하고, 실행 중에 자원 상한을 걸고, 세션을 감시합니다. 막을 때는 기준을 넘긴 계획 노드를 함께 돌려주어 모델이 다음 라운드에서 SQL을 고칩니다. 이 글은 판정을 어디에 두는지, 거절을 어떤 모양으로 돌려주는지를 설명합니다.
cost는 시간이 아니라 통계로 짐작한 상대값이다
cost 문턱은 장점이 분명합니다. 구현이 단순하고, 실행하기 전에 막습니다. 문제는 cost가 재는 것입니다. PostgreSQL의 cost는 페이지 하나를 순서대로 읽는 비용을 1로 놓은 상대 단위이고, 엔진마다 단위가 다릅니다. 같은 엔진 안에서도 cost는 실제 시간과 세 가지 이유로 어긋납니다.
- 통계가 낡으면 행 수를 잘못 짐작합니다. 마지막 수집 뒤 행이 크게 늘어난 테이블은 그 테이블을 읽는 모든 조회의 cost가 실제보다 낮게 나옵니다.
- 조건의 선택도(조건을 통과하는 행의 비율)를 잘못 짐작하면, 가벼워 보이는 조인이 실행 중에 행을 크게 불립니다.
- 실행 시간은 SQL만으로 정해지지 않습니다. 같은 SQL도 그 순간 무거운 배치가 돌고 있으면 몇 배 느려집니다.
세 가지 모두 SQL 문장 밖에 있는 정보입니다. 통계를 언제 수집했는지, 지금 어떤 세션이 돌고 있는지를 모르면 cost 숫자 하나로는 무거운 SQL과 가벼운 SQL을 가를 수 없습니다.
직접 연결은 실행 계획을 보여 주고, 피해는 실행 중 상한으로 묶는다
직접 연결에서 OwlAI가 보는 것은 연결 하나입니다. DB의 현재 부하도, 통계가 얼마나 낡았는지도 모릅니다. 그래서 실행 계획 조회 도구는 판정하지 않습니다. 실행 계획을 그대로 모델에게 보여 주고, 계획이 틀린 SQL이거나 계획을 내지 못한 경우에만 실패로 돌려줍니다. EXPLAIN ANALYZE는 SQL을 실제로 실행하므로 이 도구에서 받지 않습니다. 시스템 프롬프트는 계획에 전체 스캔이나 카테시안 조인이 보이거나 예상 행 수가 크게 불어나면, 실행하지 말고 SQL을 고쳐 쓰라고 모델에게 지시합니다.
판정이 없는 대신 실행이 시작된 뒤의 피해에는 상한이 걸립니다. 값은 모두 설치 설정이고, 모델이 도구 인자로 늘릴 수 없습니다.
| 상한 | 설정 키 | 기본값 |
|---|---|---|
| SQL 한 번의 시간 예산 | timeouts.dbQuerySeconds |
600초 |
| 프로세스 하나에서 DB 하나에 동시에 도는 실행 수 | dbConnection.perDbConcurrency |
4 |
| 한 번에 받는 행 수 | dbConnection.maxRowLimit |
10,000행 |
| 한 번에 받는 셀의 누적 크기 | dbConnection.resultMaxBytes |
16MiB |
동시 실행 상한을 넘는 호출은 그 DB의 대기열에서 기다립니다. 모델이 무거운 SQL을 여러 개 한꺼번에 보내도, 채팅 요청을 받는 프로세스나 작업을 도는 워커 하나가 한 DB에 동시에 보내는 실행은 네 개를 넘지 않습니다. 시간 예산이 다하면 감시 스레드가 엔진의 취소 호출을 보내고, 취소가 듣지 않을 때를 대비해 DB 서버 쪽 시간 상한도 걸려 있습니다(「읽기 전용과 쿼리 취소는 DB 서버에 닿아야 한다」).
이 상한은 모두 실행이 시작된 뒤에 움직입니다. 무거운 조회는 예산이 다할 때까지 DB 자원을 쓰고, 취소를 받은 엔진이 실제로 언제 멈추는지는 엔진이 정합니다. AI 조회가 다른 업무와 자원을 다투지 않는 DB라면 이것으로 충분합니다. 운영 업무와 같은 DB라면 실행 전에 거르는 판정이 필요하고, 그 판정에는 SQL 밖의 정보가 필요합니다.
OwlDB는 실행 전에 판정하고, 기준을 넘긴 계획 노드를 짚는다
OwlDB는 DB를 관리하는 쪽이라 연결 하나로는 알 수 없는 것을 이미 압니다. 테이블마다 통계를 언제 수집했는지, 지금 도는 세션이 무엇인지, 노드마다 역할과 부하가 어떤지입니다. OwlDB 연결은 OwlDB가 관리하는 DB 목록에서 골라 만든 연결이고, 이 연결의 SQL은 OwlDB가 실행합니다(「OwlDB 연결은 접속 정보 대신 DB 식별자만 든다」). 무거운 SQL을 다루는 세 단계를 OwlDB에 둔 이유입니다.
OwlDB 연결에서 모델이 실행 계획 조회 도구를 부르면, 실행 계획과 함께 판정 블록이 옵니다. 블록에는 네 가지가 있습니다.
- 실행해도 되는지
- 판정에 쓴 기준
- 이유
- 기준을 넘게 만든 계획 노드
OwlAI는 이 블록을 고치지 않고 도구 결과에 그대로 싣습니다. 모델이 실행 계획을 보지 않고 바로 SQL 실행 도구를 불러도 OwlDB는 같은 판정을 거칩니다. 기준을 넘으면 실행하지 않고 QUERY_COST_EXCEEDED와 같은 판정 블록을 돌려줍니다.
판정 블록에서 쓸모가 큰 칸은 계획 노드입니다. "너무 무겁다"만 받은 모델은 무엇을 고칠지 몰라 SQL을 통째로 다시 쓰거나 답을 포기합니다. "주문 이력 테이블 전체 스캔"을 받은 모델은 기간 조건을 더하거나 인덱스가 걸린 컬럼으로 거릅니다. 두 테이블의 조인 조건이 빠져 생긴 카테시안 조인이라면, 그 노드를 받은 모델이 빠진 조건을 채웁니다.
판정 기준은 OwlDB 한 곳에만 있습니다. OwlAI가 따로 문턱을 두면 OwlAI의 문턱을 지난 SQL이 OwlDB에서 다시 막히거나 그 반대가 생깁니다. 한 곳에서 통과한 SQL이 다음 곳에서 막히면 모델은 무엇을 고쳐야 할지 모르고, 같은 SQL을 조금씩 바꿔 다시 보내는 라운드만 늘어납니다.
판정이 빗나간 조회는 실행 중에 멈춘다
실행 전 판정도 실행 계획을 보고 하는 짐작입니다. 통계를 아무리 잘 관리해도 선택도를 틀리게 짐작하는 조건은 남습니다. 그래서 OwlDB는 판정을 통과한 조회에도 두 가지를 더 겁니다.
첫째, 실행 중 자원 상한입니다. AI 조회의 세션에는 CPU, 입출력, 메모리, 시간 네 가지에 상한이 걸립니다. 계획으로는 가벼워 보였는데 실행 중에 행이 크게 불어나는 조인이 여기서 걸립니다.
둘째, 세션 감시입니다. OwlDB는 1초마다 AI 조회의 세션을 보고, 예산을 넘긴 세션을 강제로 종료합니다. 직접 연결에서 OwlAI가 보내는 것은 쿼리 취소 요청이고, 멈추는 시점은 엔진이 정합니다. OwlDB는 DB 세션 자체를 끊으므로, 화면에서는 멈췄는데 DB에서는 계속 도는 조회가 남지 않습니다.
모델에게는 SQL로 고칠 수 있는 거절만 간다
OwlDB의 거절은 모델이 SQL을 고쳐서 풀 수 있는지에 따라 두 갈래로 나뉩니다.
QUERY_COST_EXCEEDED는 SQL로 풀 수 있는 거절입니다. 판정 블록이 도구 결과로 모델에게 가고, 모델은 같은 라운드의 다른 도구 결과와 함께 그것을 읽습니다. 다음 라운드에서 모델은 조건을 좁히거나, 집계를 GROUP BY로 DB에 맡기거나, 빠진 조인 조건을 채운 SQL을 냅니다. 사용자가 받는 답은 그만큼 한 라운드 늦게 나옵니다.
읽을 수 있는 복제 노드가 없거나 DB가 바쁜 경우는 SQL의 문제가 아닙니다. 이 사유는 모델에게 보이지 않고, OwlAI가 잠시 뒤 같은 요청을 다시 보냅니다(「AI 조회를 실행할 노드는 등록할 때가 아니라 실행할 때 고른다」). 인프라 사유를 받은 모델은 그것을 자기 SQL의 문제로 읽고 멀쩡한 조건을 지우거나 조인을 풀어, 실행은 되지만 답이 틀린 SQL을 만들기 때문입니다. 행 상한에 걸린 결과는 거절이 아니라 "더 있음" 표시와 함께 잘린 결과로 옵니다. 두 경로가 같은 결과 모양을 쓰는 이유는 「SQL 실행기가 둘이어도 판정과 결과 형식은 한 벌만 둔다」에서 다룹니다.
설계에서 고른 것
cost 문턱 대신 OwlDB의 판정을 골랐습니다. cost 문턱은 단순하고 실행 전에 막지만, 통계의 신선도와 지금의 부하를 모르는 숫자 하나로는 무거운 SQL을 가를 수 없습니다. 판정은 그 정보를 가진 OwlDB에 두었습니다. 대가로 직접 연결에는 실행 전 판정이 없고, 무거운 조회는 시간 예산이 다할 때까지 DB 자원을 씁니다. 동시 실행 상한은 한 번에 도는 그런 조회의 수를 프로세스마다 네 개로 묶습니다.
기준은 OwlDB 한 곳에만 둡니다. 두 곳이 각자 문턱을 들면 따로 보면 안전해 보이지만, 모델은 서로 맞지 않는 거절을 받습니다. 대신 기준을 바꾸려면 OwlDB에서 바꿔야 하고, OwlAI 쪽에서 연결마다 조정할 수는 없습니다.
막기만 하는 대신 이유와 계획 노드를 돌려줍니다. 사용자가 원하는 것은 답이고, 막힌 조회는 그 답으로 가는 한 라운드입니다. 무엇이 무거웠는지를 짚어 주면 모델이 고칠 곳이 정해집니다. 그 대가로 답은 라운드 하나만큼 늦습니다. SQL과 상관없는 사유는 반대로 모델에게 감추고 OwlAI가 다시 요청합니다.
AI 조회가 운영 업무와 같은 DB를 쓰고 그 DB를 OwlDB가 관리한다면, OwlDB로 연결해 판정과 세션 감시를 OwlDB에 맡기는 편이 맞습니다. 분석용 복제본처럼 AI 조회가 다른 업무와 자원을 다투지 않는 DB라면, 직접 연결의 시간 예산과 동시 실행 상한으로 충분합니다.
어떤 문제를 해결하나요?
DB 연결과 OwlDB의 다른 글

엔지니어링 노트 · DB 연결과 OwlDB
배스천에는 계정 대신 DB 포워딩만 여는 키 한 줄을 넣는다
운영 DB가 사설망 안에 있고 그 앞에 배스천 호스트(사설망으로 들어가는 SSH 서버)가 있으면, OwlAI는 배스천을 거치는 SSH 터널로 DB에 붙습니다.

엔지니어링 노트 · DB 연결과 OwlDB
인증서 오류는 검증을 끄지 않고, 빠진 CA와 틀린 이름을 고쳐서 푼다
기업망에 설치한 OwlAI가 클라우드 LLM이나 고객 DB에 TLS로 붙을 때, 인증서 오류가 나는 경우는 크게 둘입니다.

엔지니어링 노트 · DB 연결과 OwlDB
RDS 목록은 액세스 키 대신 60분 뒤 만료되는 역할로 읽는다
OwlAI SaaS에서 고객의 Amazon RDS·Aurora를 연결하려면 엔드포인트·포트·엔진과 함께 DB가 공개 서브넷에 있는지, 어느 VPC·보안 그룹에 속하는지 같은 망 정보가 필요합니다.
