|
|
아주 좋은 질문입니다. 이건 SQL 튜닝 실무에서 자주 마주치는 문제로, "SQL 안에 SQL을 넣는" 상황의 이스케이프(escape) 문제입니다. 단계별로 정리해드리겠습니다.
## 문제 상황 정확히 이해하기
**첨부 스크립트의 구조:**
```sql
WITH src AS (
SELECT ' ' || UPPER(REGEXP_REPLACE(:sql_text, '[[:space:]]+', ' ')) || ' ' AS t
FROM DUAL
)
```
여기서 `:sql_text`에 분석하고 싶은 SQL을 문자열로 넘겨야 합니다.
**예를 들어 분석하고 싶은 SQL:**
```sql
SELECT * FROM employees WHERE name = 'KIM' AND dept_id = 10
```
**문제점:**
이 SQL을 그대로 `:sql_text`에 넣으려면 문자열로 감싸야 하는데, 그 안에 이미 작은따옴표(`'KIM'`)가 있어서 충돌합니다.
## 해결 방법들
이스케이프 처리를 위한 여러 가지 방법이 있습니다. 사용 환경에 따라 선택하세요.
### 방법 1: 바인드 변수 사용 (SQL*Plus, SQL Developer 등)
**가장 안전하고 표준적인 방법**입니다. 바인드 변수를 사용하면 이스케이프 자체를 신경 쓰지 않아도 됩니다.
**SQL*Plus 또는 Tibero 클라이언트 (tbSQL):**
```sql
VARIABLE sql_text VARCHAR2(4000);
BEGIN
:sql_text := 'SELECT * FROM employees WHERE name = ''KIM'' AND dept_id = 10';
END;
/
WITH src AS (
SELECT ' ' || UPPER(REGEXP_REPLACE(:sql_text, '[[:space:]]+', ' ')) || ' ' AS t
FROM DUAL
)
```
**핵심:** PL/SQL 블록 안에서 대입할 때는 작은따옴표 두 개(`''`)로 이스케이프합니다.
### 방법 2: Oracle 12c+ / Tibero의 대체 인용 (Alternative Quoting)
**가장 편리한 방법**입니다. 문자열 안의 따옴표를 이스케이프할 필요가 없어집니다.
```sql
VARIABLE sql_text VARCHAR2(4000);
BEGIN
:sql_text := q'[SELECT * FROM employees WHERE name = 'KIM' AND dept_id = 10]';
END;
/
```
**문법 설명:**
- `q'[...]'` : 대괄호 사이의 모든 것을 있는 그대로 문자열로 취급
- 사용 가능한 구분자: `[]`, `{}`, `()`, `<>`, 또는 `!` `#` 같은 단일 문자
**예시:**
```sql
q'[문자열 안에 '작은따옴표'가 있어도 OK]'
q'{안녕하세요 'World'}'
q'(SELECT * FROM t WHERE x = 'A')'
q'!문자열!'
q'#SELECT 'hello' FROM dual#'
```
**주의:** 문자열 내부에 종료 구분자가 없어야 합니다. 예를 들어 `q'[abc]def]'` 는 첫 `]'`에서 끝나버립니다.
**Tibero 지원 여부 확인 필요:** Tibero도 Oracle 호환성을 목표로 하므로 이 문법을 지원하지만, 정확한 지원 여부는 버전에 따라 다를 수 있습니다.
### 방법 3: 홑따옴표 두 번 (기본 방법)
가장 원시적이지만 어디서든 동작하는 방법입니다.
**원본 SQL:**
```sql
SELECT * FROM employees WHERE name = 'KIM'
```
**이스케이프 후:**
```sql
VARIABLE sql_text VARCHAR2(4000);
BEGIN
:sql_text := 'SELECT * FROM employees WHERE name = ''KIM''';
END;
/
```
**규칙:**
- 원본의 `'` → 두 개의 `''` 로 변환
**단점:** SQL이 복잡하면 이스케이프하기 번거롭고 실수하기 쉬움.
### 방법 4: DBeaver, Toad, SQL Developer 등의 GUI 도구
이전 대화에서 DBeaver를 언급하셨는데, 이런 GUI 도구는 대부분 **바인드 변수 입력 팝업**을 제공합니다.
**DBeaver의 경우:**
1. `:sql_text` 가 포함된 스크립트를 실행
2. 팝업창이 뜨며 값 입력 요청
3. **팝업에는 이스케이프 없이 SQL을 그대로 붙여넣기 가능**
```
[Bind Parameters 팝업]
sql_text: SELECT * FROM employees WHERE name = 'KIM'
```
이 방법이 가장 편리합니다. GUI 도구가 이스케이프를 자동 처리합니다.
**확실하지 않음:** GUI 도구별로 바인드 변수 처리 방식이 조금씩 다를 수 있습니다.
### 방법 5: CTE로 인라인 정의 (스크립트 자체 수정)
첨부 스크립트를 살짝 수정해서 매번 바인드 변수 없이 SQL을 넣는 방법입니다.
**원본:**
```sql
WITH src AS (
SELECT ' ' || UPPER(REGEXP_REPLACE(:sql_text, ...)) || ' ' AS t
FROM DUAL
)
```
**수정 (스크립트에 직접 SQL 삽입):**
```sql
WITH src AS (
SELECT ' ' || UPPER(REGEXP_REPLACE(
q'[SELECT * FROM employees WHERE name = 'KIM' AND dept_id = 10]',
'[[:space:]]+', ' '
)) || ' ' AS t
FROM DUAL
),
```
`:sql_text` 자리에 `q'[...]'` 로 감싼 SQL을 직접 넣습니다.
**장점:** 바인드 변수 없이 한 번에 실행 가능
**단점:** 매번 스크립트를 수정해야 함
## 실전 사용 시나리오
이전 대화에서 Tibero SQL 튜닝을 진행하신다고 하셨으니, 실전 예시를 드리겠습니다.
### 시나리오 1: SQL Developer에서 실행
**단계별 진행:**
1. SQL Developer 열기
2. 아래 스크립트를 스크립트 창에 붙여넣기:
```sql
VARIABLE sql_text VARCHAR2(4000);
BEGIN
:sql_text := q'[
SELECT e.name, e.salary, d.dept_name
FROM employees e, departments d
WHERE e.dept_id = d.id
AND e.hire_date >= TO_DATE('2020-01-01', 'YYYY-MM-DD')
AND e.status = 'ACTIVE'
ORDER BY e.salary DESC
]';
END;
/
-- STEP 1 스크립트 실행
WITH src AS (
SELECT ' ' || UPPER(REGEXP_REPLACE(:sql_text, '[[:space:]]+', ' ')) || ' ' AS t
FROM DUAL
),
rules AS (
-- ... 이하 원본 스크립트 그대로 ...
)
SELECT ... ;
```
3. `F5` 또는 스크립트 실행 (개별 문장 실행 아닌 스크립트 전체 실행)
**주의:** SQL Developer에서는 `F9`(현재 문장)이 아닌 `F5`(스크립트 실행) 사용.
### 시나리오 2: 명령줄 (tbSQL, SQL*Plus)
```sql
SQL> VARIABLE sql_text VARCHAR2(4000);
SQL> BEGIN
2 :sql_text := q'[SELECT * FROM employees WHERE dept_id = 10 AND status = 'ACTIVE']';
3 END;
4 /
SQL> @sql_tuning_check.sql
```
### 시나리오 3: DBeaver 등 GUI
바인드 변수 팝업 활용:
1. 원본 스크립트 그대로 실행
2. 팝업이 뜨면 SQL 문자열 입력 (이스케이프 불필요)
## 문자열 길이 제한 주의
Oracle/Tibero의 `VARCHAR2` 최대 길이는:
- **표준 SQL**: 4000자
- **PL/SQL 변수**: 32767자
- **12c+의 EXTENDED VARCHAR2**: 32767자 (`MAX_STRING_SIZE=EXTENDED` 설정 필요)
**확실하지 않음:** Tibero의 정확한 제한은 버전에 따라 다를 수 있습니다.
**긴 SQL 처리 방법:**
```sql
VARIABLE sql_text VARCHAR2(32767);
BEGIN
:sql_text := q'[
... 매우 긴 SQL ...
]';
END;
/
```
만약 이 방법으로도 부족하면 `CLOB` 사용을 고려해야 합니다.
## 스크립트 개선 제안
첨부하신 스크립트를 실무에서 편하게 쓰시려면, 다음처럼 개선하시는 것을 추천드립니다.
### 개선 1: 대체 인용 예시 주석 추가
```sql
/* =====================================================================
STEP 1. SQL 텍스트 정적 분석 (실행 이력 없어도 사용 가능)
---------------------------------------------------------------------
사용법:
VARIABLE sql_text VARCHAR2(4000);
BEGIN
:sql_text := q'[여기에 분석할 SQL 입력]';
END;
/
--------------------------------------------------------------------- */
```
### 개선 2: 여러 SQL을 한 번에 분석하는 버전
```sql
CREATE OR REPLACE PROCEDURE analyze_sql (
p_sql_text IN VARCHAR2
) AS
BEGIN
FOR r IN (
WITH src AS (
SELECT ' ' || UPPER(REGEXP_REPLACE(p_sql_text, '[[:space:]]+', ' ')) || ' ' AS t
FROM DUAL
),
rules AS (
-- ... 원본 rules 정의 ...
)
SELECT r.no, r.lvl, r.item, r.advice
FROM src s, rules r
WHERE REGEXP_LIKE(s.t, r.pat)
ORDER BY DECODE(r.lvl, '높음', 1, '중간', 2, '낮음', 3, 4), r.no
) LOOP
DBMS_OUTPUT.PUT_LINE(r.no || ' [' || r.lvl || '] ' || r.item || ': ' || r.advice);
END LOOP;
END;
/
-- 사용 예:
BEGIN
analyze_sql(q'[SELECT * FROM employees WHERE name = 'KIM']');
END;
/
```
**락 관련 안내:** `CREATE OR REPLACE PROCEDURE`는 DDL이지만, 새 프로시저를 만들거나 정의만 교체하는 것이므로 데이터에 락이 걸리지 않습니다. 다만 같은 이름의 프로시저를 동시에 실행 중이면 일시적으로 라이브러리 캐시 락이 걸릴 수 있습니다.
### 개선 3: 파일 기반 입력 (매우 긴 SQL용)
```sql
-- 임시 테이블에 SQL 저장
CREATE GLOBAL TEMPORARY TABLE tmp_sql_input (
sql_text CLOB
) ON COMMIT PRESERVE ROWS;
-- SQL 입력
INSERT INTO tmp_sql_input VALUES (
q'[
... 매우 긴 SQL ...
]'
);
-- 분석
WITH src AS (
SELECT ' ' || UPPER(REGEXP_REPLACE(
(SELECT DBMS_LOB.SUBSTR(sql_text, 4000, 1) FROM tmp_sql_input),
'[[:space:]]+', ' '
)) || ' ' AS t
FROM DUAL
),
-- ... 나머지 원본 스크립트 ...
```
**확실하지 않음:** Tibero의 CLOB 처리 세부사항은 Oracle과 다를 수 있으니 테스트 권장.
## 대체 인용의 다양한 예시
`q'[...]'` 사용의 실전 예시들입니다.
```sql
-- 대괄호
q'[SELECT 'a' FROM dual]'
-- 중괄호
q'{SELECT 'a' FROM dual}'
-- 소괄호
q'(SELECT 'a' FROM dual)'
-- 꺾쇠괄호
q'<select 'a' from dual>'</select 'a' from dual>
-- 임의의 문자 (대칭 불필요)
q'!SELECT 'a' FROM dual!'
q'#SELECT 'a' FROM dual#'
q'@SELECT 'a' FROM dual@'
```
**팁:** SQL 내용에 특정 문자가 있으면 다른 구분자를 선택하세요.
```sql
-- SQL에 대괄호가 있는 경우 → 중괄호 사용
q'{SELECT col[1] FROM some_array_table}'
-- SQL에 소괄호가 많은 경우 → # 사용
q'#SELECT COUNT(*) FROM (SELECT * FROM t)#'
```
## 실전 검증: 여러분의 튜닝 스크립트로 테스트
이전 대화에서 SQL 튜닝을 하신다고 하셨으니, 실제로 테스트할 수 있는 예시를 드리겠습니다.
**테스트 SQL (튜닝 포인트가 여러 개 있는 나쁜 예):**
```sql
VARIABLE sql_text VARCHAR2(4000);
BEGIN
:sql_text := q'[
SELECT *
FROM employees e
WHERE UPPER(e.name) LIKE '%KIM%'
AND e.salary + 1000 > 50000
AND e.dept_id NOT IN (10, 20)
OR e.status IS NULL
ORDER BY e.hire_date
]';
END;
/
-- STEP 1 실행
WITH src AS (
SELECT ' ' || UPPER(REGEXP_REPLACE(:sql_text, '[[:space:]]+', ' ')) || ' ' AS t
FROM DUAL
),
rules AS (
-- ... 원본 rules 정의 그대로 ...
)
SELECT r.no, r.lvl, r.item, r.advice
FROM src s, rules r
WHERE REGEXP_LIKE(s.t, r.pat)
ORDER BY DECODE(r.lvl, '높음', 1, '중간', 2, '낮음', 3, 4), r.no;
```
**예상 결과:** LIKE 앞 %, 컬럼 함수, 컬럼 연산, NOT IN, OR, IS NULL 등 여러 튜닝 포인트가 검출되어야 합니다.
## Tibero 특이사항 (참고)
첨부 스크립트가 Tibero 기준이라 하셨으니 몇 가지 참고사항입니다.
**확실하지 않음:** 다음 사항들은 Tibero 버전에 따라 다를 수 있습니다.
1. **`q'[...]'` 문법 지원**: Tibero는 Oracle 호환성을 목표로 하므로 대부분 지원하지만, 정확한 버전 지원은 확인 필요
2. **`VARIABLE` 바인드**: tbSQL이 SQL*Plus와 유사한 문법을 지원하는 것으로 알고 있음
3. **V$ 뷰**: 스크립트 주석에도 나와있듯 Tibero의 V$ 뷰는 Oracle과 컬럼명/단위가 다를 수 있음
**Tibero 사용 시 권장:**
- 실행 전 `DESC V$SQLAREA` 등으로 컬럼 확인
- 문법 오류가 나면 Tibero 공식 문서 참조
## 정리
**질문의 답:**
`:sql_text` 파라미터에 SQL 문을 넘길 때 사용할 수 있는 방법들:
**추천 순서:**
1. **GUI 도구 사용** (DBeaver, SQL Developer): 팝업에 그대로 입력, 이스케이프 자동 처리
2. **대체 인용 (`q'[...]'`)**: 이스케이프 불필요, 가장 편리
3. **바인드 변수 + 홑따옴표 두 번 (`''`)**: 전통적 방법
4. **인라인 삽입**: 스크립트를 수정해서 SQL을 직접 삽입
**가장 실용적인 조합:**
```sql
VARIABLE sql_text VARCHAR2(4000);
BEGIN
:sql_text := q'[여기에 분석할 SQL을 자유롭게 넣기]';
END;
/
-- 첨부 스크립트 실행
```
## 이전 대화 맥락과 연결
이전 대화에서 Tibero SQL 튜닝을 진행하시는 것을 알고 있습니다. 이 스크립트는 유용한 도구이지만, 몇 가지 개선 여지가 있습니다.
**개선 제안:**
1. **PROCEDURE로 만들기**: 매번 바인드 변수 설정 없이 함수 호출로 사용
2. **결과를 로그 테이블에 저장**: 튜닝 이력 관리
3. **더 많은 검출 규칙 추가**: 힌트, 계층 쿼리, 재귀 등
## 추가 질문
관심 있으시면 다음도 다뤄볼 수 있습니다.
1. **스크립트를 PROCEDURE로 재작성**하여 사용 편의성 향상
2. **여러 SQL을 배치로 분석**하는 방법
3. **더 정교한 튜닝 포인트 검출 규칙** 추가 (예: 명시적 조인 순서, 힌트 유효성)
4. **Tibero vs Oracle의 V$ 뷰 차이점**
또한 **실제로 이 스크립트를 사용하시다가 특정 SQL에서 문제가 발생**한다면, 그 SQL과 함께 알려주시면 함께 확인해드리겠습니다.
먼저 위의 대체 인용 (`q'[...]'`) 방식으로 시도해보시고, 결과를 알려주세요. Tibero에서 정상 동작하는지 확인이 필요합니다.
