<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>TunA</title>
    <link>https://hrjeong.tistory.com/</link>
    <description>database consultant  </description>
    <language>ko</language>
    <pubDate>Fri, 17 Jul 2026 05:45:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>정희락_</managingEditor>
    <image>
      <title>TunA</title>
      <url>https://tistory1.daumcdn.net/tistory/513722/attach/e4fee27d74ba4ab791dd627558c756b2</url>
      <link>https://hrjeong.tistory.com</link>
    </image>
    <item>
      <title>쿼리 변환을 통한 Top-N 쿼리 성능 개선 사례</title>
      <link>https://hrjeong.tistory.com/380</link>
      <description>&lt;p style=&quot;text-align: justify;&quot;&gt;쿼리 변환을 통해 Top-N 쿼리의 성능을 개선한 사례를 살펴보자.&lt;/p&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;아래와 같이 테이블을 생성하자.

&lt;pre class=&quot;brush:sql&quot;&gt;-- 1
DROP TABLE t1 PURGE;
DROP TABLE t2 PURGE;

CREATE TABLE t1 AS SELECT ROWNUM AS c1, ROWNUM AS c2 FROM XMLTABLE ('1 to 100000');
CREATE TABLE t2 AS SELECT * FROM t1;

ALTER TABLE t2 ADD CONSTRAINT t2_pk PRIMARY KEY (c1);
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;

아래는 t1, t2 테이블을 아우터 조인한 Top-N 쿼리다. t1, t2 테이블이 해시 조인되어 418개의 블록 I/O가 발생했다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 2
SELECT *
  FROM (SELECT   *
            FROM t1 a
            LEFT OUTER
            JOIN t2 b
              ON b.c1 = a.c2
           WHERE a.c1 &amp;gt; 0
        ORDER BY a.c1) a
 WHERE ROWNUM &amp;lt;= 10;

--------------------------------------------------------------------
| Id  | Operation               | Name | Starts | A-Rows | Buffers |
--------------------------------------------------------------------
|   0 | SELECT STATEMENT        |      |      1 |     10 |     418 |
|*  1 |  COUNT STOPKEY          |      |      1 |     10 |     418 |
|   2 |   VIEW                  |      |      1 |     10 |     418 |
|*  3 |    SORT ORDER BY STOPKEY|      |      1 |     10 |     418 |
|*  4 |     HASH JOIN OUTER     |      |      1 |    100K|     418 |
|*  5 |      TABLE ACCESS FULL  | T1   |      1 |    100K|     209 |
|   6 |      TABLE ACCESS FULL  | T2   |      1 |    100K|     209 |
--------------------------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;

2번 쿼리는 아우터 조인한 t2 테이블의 c1 칼럼이 PK이므로 아래 쿼리처럼 Top-N 처리 후 t2 테이블을 조인해도 결과가 동일하다. t2 테이블의 NL 조인되어 블록 I/O가 229개로 감소했다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 3
SELECT *
  FROM (SELECT   *
            FROM t1 a
           WHERE a.c1 &amp;gt; 0
        ORDER BY a.c1) a
  LEFT OUTER
  JOIN t2 b
    ON b.c1 = a.c2
 WHERE ROWNUM &amp;lt;= 10;

---------------------------------------------------------------------------
| Id  | Operation                     | Name  | Starts | A-Rows | Buffers |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |       |      1 |     10 |     229 |
|*  1 |  COUNT STOPKEY                |       |      1 |     10 |     229 |
|   2 |   NESTED LOOPS OUTER          |       |      1 |     10 |     229 |
|   3 |    VIEW                       |       |      1 |     10 |     209 |
|   4 |     SORT ORDER BY             |       |      1 |     10 |     209 |
|*  5 |      TABLE ACCESS FULL        | T1    |      1 |    100K|     209 |
|   6 |    TABLE ACCESS BY INDEX ROWID| T2    |     10 |     10 |      20 |
|*  7 |     INDEX UNIQUE SCAN         | T2_PK |     10 |     10 |      10 |
---------------------------------------------------------------------------
&lt;/pre&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;

아래와 같이 t2 테이블에 대한 동적 조건이 존재한다면 쿼리를 3번 쿼리처럼 변경할 수 없다. 동적 조건과 함께&amp;nbsp;아우터 조인까지&amp;nbsp;동적으로 처리하는 방식이 성능 측면에서 가장 효율적이지만 개발 난이도가 높아지는 단점이 있다.&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 4
SELECT *
  FROM (SELECT   *
            FROM t1 a
            LEFT OUTER
            JOIN t2 b
              ON b.c1 = a.c2
           WHERE a.c1 &amp;gt; 0
             AND b.c2 &amp;gt; 0 -- 동적 조건
        ORDER BY a.c1) a
 WHERE ROWNUM &amp;lt;= 10;

--------------------------------------------------------------------
| Id  | Operation               | Name | Starts | A-Rows | Buffers |
--------------------------------------------------------------------
|   0 | SELECT STATEMENT        |      |      1 |     10 |     418 |
|*  1 |  COUNT STOPKEY          |      |      1 |     10 |     418 |
|   2 |   VIEW                  |      |      1 |     10 |     418 |
|*  3 |    SORT ORDER BY STOPKEY|      |      1 |     10 |     418 |
|*  4 |     HASH JOIN           |      |      1 |    100K|     418 |
|*  5 |      TABLE ACCESS FULL  | T1   |      1 |    100K|     209 |
|*  6 |      TABLE ACCESS FULL  | T2   |      1 |    100K|     209 |
--------------------------------------------------------------------
&lt;/pre&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;

아래와 같이 쿼리를 변경해보자. 인라인 뷰의 SELECT 목록을 t1 테이블만 조회했고, Top-N 처리 후에 t2 테이블을 아우터 조인했다. 중복 조인으로 인해 블록 I/O가 5번 쿼리보다 20개 증가했다. 전체 블록 I/O에 비해 크지 않은 수치로 볼 수 있다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 5
SELECT *
  FROM (SELECT   a.*      -- 변경
            FROM t1 a
            LEFT OUTER
            JOIN t2 b
              ON b.c1 = a.c2
           WHERE a.c1 &amp;gt; 0
             AND b.c2 &amp;gt; 0 -- 동적 조건
        ORDER BY a.c1) a
  LEFT OUTER
  JOIN t2 b
    ON b.c1 = a.c2
 WHERE ROWNUM &amp;lt;= 10;

---------------------------------------------------------------------------
| Id  | Operation                     | Name  | Starts | A-Rows | Buffers |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |       |      1 |     10 |     438 |
|*  1 |  COUNT STOPKEY                |       |      1 |     10 |     438 |
|   2 |   NESTED LOOPS OUTER          |       |      1 |     10 |     438 |
|   3 |    VIEW                       |       |      1 |     10 |     418 |
|   4 |     SORT ORDER BY             |       |      1 |     10 |     418 |
|*  5 |      HASH JOIN                |       |      1 |    100K|     418 |
|*  6 |       TABLE ACCESS FULL       | T1    |      1 |    100K|     209 |
|*  7 |       TABLE ACCESS FULL       | T2    |      1 |    100K|     209 |
|   8 |    TABLE ACCESS BY INDEX ROWID| T2    |     10 |     10 |      20 |
|*  9 |     INDEX UNIQUE SCAN         | T2_PK |     10 |     10 |      10 |
---------------------------------------------------------------------------
&lt;/pre&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;아래 쿼리는 동적 조건이 비활성화된 경우를 가정하여 동적 조건을 주석 처리했다. 쿼리 변환에 의해 인라인 뷰 내에서 t2 테이블의 조인이 제거되었다.&amp;nbsp;블록 I/O가 3번 쿼리와 동일하게 229개만 발생한다. 쿼리 변환을 통해 동적 조건이 비활성화된 경우&amp;nbsp;블록 I/O를 감소시키는 개선 방안이다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 6
SELECT *
  FROM (SELECT   a.*      -- 변경
            FROM t1 a
            LEFT OUTER
            JOIN t2 b
              ON b.c1 = a.c2
           WHERE a.c1 &amp;gt; 0
          -- AND b.c2 &amp;gt; 0 -- 동적 조건
        ORDER BY a.c1) a
  LEFT OUTER
  JOIN t2 b
    ON b.c1 = a.c2
 WHERE ROWNUM &amp;lt;= 10;

---------------------------------------------------------------------------
| Id  | Operation                     | Name  | Starts | A-Rows | Buffers |
---------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |       |      1 |     10 |     229 |
|*  1 |  COUNT STOPKEY                |       |      1 |     10 |     229 |
|   2 |   NESTED LOOPS OUTER          |       |      1 |     10 |     229 |
|   3 |    VIEW                       |       |      1 |     10 |     209 |
|   4 |     SORT ORDER BY             |       |      1 |     10 |     209 |
|*  5 |      TABLE ACCESS FULL        | T1    |      1 |    100K|     209 |
|   6 |    TABLE ACCESS BY INDEX ROWID| T2    |     10 |     10 |      20 |
|*  7 |     INDEX UNIQUE SCAN         | T2_PK |     10 |     10 |      10 |
---------------------------------------------------------------------------
&lt;/pre&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;

스칼라 서브 쿼리도 동일한 기법을 활용할 수 있다. 아래 쿼리는 동적 조건에 스칼라 서브 쿼리의 결과를 사용했다. 101,000개의 블록 I/O가 발생했다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 7
SELECT a.*
  FROM (SELECT   a.*
               , (SELECT MAX (x.c2) FROM t2 x WHERE x.c1 = a.c2) AS xc2
            FROM t1 a
        ORDER BY a.c1) a
 WHERE ROWNUM &amp;lt;= 10
   AND a.xc2 &amp;gt; 0 -- 동적 조건
;

--------------------------------------------------------------------------
| Id  | Operation                    | Name  | Starts | A-Rows | Buffers |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |      1 |     10 |     101K|
|   1 |  SORT AGGREGATE              |       |    100K|    100K|     101K|
|   2 |   TABLE ACCESS BY INDEX ROWID| T2    |    100K|    100K|     101K|
|*  3 |    INDEX UNIQUE SCAN         | T2_PK |    100K|    100K|    1462 |
|*  4 |  COUNT STOPKEY               |       |      1 |     10 |     101K|
|*  5 |   VIEW                       |       |      1 |     10 |     101K|
|   6 |    SORT ORDER BY             |       |      1 |     10 |     101K|
|   7 |     TABLE ACCESS FULL        | T1    |      1 |    100K|     209 |
--------------------------------------------------------------------------
&lt;/pre&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;아래와 같이&amp;nbsp;쿼리를 변경해보자. 메인 쿼리의 SELECT 목록에서 t1 테이블만 조회했고, 스칼라 서브 쿼리를 중복 기술했다. 중복 스칼라 서브 쿼리로 인해 블록 I/O가 7번 쿼리보다 20개 증가했다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 8
SELECT a.c1 -- 변경
     , a.c2 -- 변경
     , (SELECT MAX (x.c2) FROM t2 x WHERE x.c1 = a.c2) AS xc2
  FROM (SELECT   a.*
               , (SELECT MAX (x.c2) FROM t2 x WHERE x.c1 = a.c2) AS xc2
            FROM t1 a
        ORDER BY a.c1) a
 WHERE ROWNUM &amp;lt;= 10
   AND a.xc2 &amp;gt; 0 -- 동적 조건
;

----------------------------------------------------------------------------
| Id  | Operation                      | Name  | Starts | A-Rows | Buffers |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |       |      1 |     10 |     101K|
|   1 |  SORT AGGREGATE                |       |     10 |     10 |      20 |
|   2 |   TABLE ACCESS BY INDEX ROWID  | T2    |     10 |     10 |      20 |
|*  3 |    INDEX UNIQUE SCAN           | T2_PK |     10 |     10 |      10 |
|*  4 |  COUNT STOPKEY                 |       |      1 |     10 |     101K|
|*  5 |   VIEW                         |       |      1 |     10 |     101K|
|   6 |    SORT AGGREGATE              |       |    100K|    100K|     101K|
|   7 |     TABLE ACCESS BY INDEX ROWID| T2    |    100K|    100K|     101K|
|*  8 |      INDEX UNIQUE SCAN         | T2_PK |    100K|    100K|    1462 |
|   9 |    SORT ORDER BY               |       |      1 |     10 |     101K|
|  10 |     TABLE ACCESS FULL          | T1    |      1 |    100K|     209 |
----------------------------------------------------------------------------
&lt;/pre&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;

아래 쿼리는 동적 조건을 주석 처리했다. 아우터 조인과 마찬가지로&amp;nbsp;쿼리 변환에 의해 스칼라 서브 쿼리가 제거되어 블록 I/O가 209개만 발생한다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 9
SELECT a.c1 -- 변경
     , a.c2 -- 변경
     , (SELECT MAX (x.c2) FROM t2 x WHERE x.c1 = a.c2) AS c2x
  FROM (SELECT   a.*
               , (SELECT MAX (x.c2) FROM t2 x WHERE x.c1 = a.c2) AS xc2
            FROM t1 a
        ORDER BY a.c1) a
 WHERE ROWNUM &amp;lt;= 10
-- AND a.xc2 &amp;gt; 0 -- 동적 조건
;

--------------------------------------------------------------------------
| Id  | Operation                    | Name  | Starts | A-Rows | Buffers |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |      1 |     10 |     209 |
|   1 |  SORT AGGREGATE              |       |     10 |     10 |      20 |
|   2 |   TABLE ACCESS BY INDEX ROWID| T2    |     10 |     10 |      20 |
|*  3 |    INDEX UNIQUE SCAN         | T2_PK |     10 |     10 |      10 |
|*  4 |  COUNT STOPKEY               |       |      1 |     10 |     209 |
|   5 |   VIEW                       |       |      1 |     10 |     209 |
|*  6 |    SORT ORDER BY STOPKEY     |       |      1 |     10 |     209 |
|   7 |     TABLE ACCESS FULL        | T1    |      1 |    100K|     209 |
--------------------------------------------------------------------------&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Oracle/Tuning</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/380</guid>
      <comments>https://hrjeong.tistory.com/380#entry380comment</comments>
      <pubDate>Wed, 16 Sep 2020 16:41:11 +0900</pubDate>
    </item>
    <item>
      <title>여부 속성으로 인한 성능 저하 사례 #2</title>
      <link>https://hrjeong.tistory.com/379</link>
      <description>&lt;p&gt;여부 속성으로 인한 두 번째 성능 저하 사례를 살펴보자.&lt;/p&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;p&gt;테스트를 위해 아래의 테이블을 생성하자. t1 테이블을 고객별 상품 조회 이력 테이블로 가정하자.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 1-1
DROP TABLE t1 PURGE;

CREATE TABLE t1 AS
SELECT 1 AS id
     , ROWNUM AS cd
     , DATE '2050-01-01' - NUMTODSINTERVAL (ROWNUM, 'MINUTE') AS dt
     , 'N' AS del_yn
  FROM XMLTABLE ('1 to 144000');

ALTER TABLE t1 ADD CONSTRAINT t1_pk PRIMARY KEY (id, cd, dt);

-- 1-2
UPDATE t1
   SET del_yn = 'Y'
 WHERE id = 1
   AND dt &amp;lt; DATE '2049-12-31' - 30;

COMMIT;
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;

고객이&amp;nbsp;2050-01-01에 로그인하면 아래 쿼리가 수행된다. 30일 이전의 상품 조회 이력을 논리적으로 삭제하는 처리다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 2
UPDATE t1
   SET del_yn = 'Y'
 WHERE id = 1
   AND dt &amp;lt; DATE '2050-01-01' - 30;
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;

UPDATE 문을 SELECT 문으로 변경하여 실행계획을 확인해보면 갱신 대상이 10만건 이상인 것을 확인할 수 있다.&lt;sup class=&quot;footnote&quot;&gt;&lt;a href=&quot;#footnote_379_1&quot; id=&quot;footnote_link_379_1&quot; onmouseover=&quot;tistoryFootnote.show(this, 379, 1)&quot; onmouseout=&quot;tistoryFootnote.hide(379, 1)&quot; style=&quot;color:#f9650d; font-family: Verdana, Sans-serif; display: inline;&quot;&gt;&lt;span style=&quot;display: none;&quot;&gt;[각주:&lt;/span&gt;1&lt;span style=&quot;display: none;&quot;&gt;]&lt;/span&gt;&lt;/a&gt;&lt;/sup&gt;
&lt;pre class=&quot;brush:sql&quot;&gt;-- 3
SELECT del_yn
  FROM t1
 WHERE id = 1
   AND dt &amp;lt; DATE '2050-01-01' - 30;

----------------------------------------------------------------
| Id  | Operation                   | Name  | A-Rows | Buffers |
----------------------------------------------------------------
|   0 | SELECT STATEMENT            |       |    100K|    1045 |
|   1 |  TABLE ACCESS BY INDEX ROWID| T1    |    100K|    1045 |
|*  2 |   INDEX RANGE SCAN          | T1_PK |    100K|     623 |
----------------------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;

업무적으로 과거 데이터는 이미 논리 삭제가 된 상태이므로 아래와 같이 del_yn = 'N' 조건을 추가하여 갱신 대상을 줄일 수 있다.&lt;sup class=&quot;footnote&quot;&gt;&lt;a href=&quot;#footnote_379_2&quot; id=&quot;footnote_link_379_2&quot; onmouseover=&quot;tistoryFootnote.show(this, 379, 2)&quot; onmouseout=&quot;tistoryFootnote.hide(379, 2)&quot; style=&quot;color:#f9650d; font-family: Verdana, Sans-serif; display: inline;&quot;&gt;&lt;span style=&quot;display: none;&quot;&gt;[각주:&lt;/span&gt;2&lt;span style=&quot;display: none;&quot;&gt;]&lt;/span&gt;&lt;/a&gt;&lt;/sup&gt;&amp;nbsp;불필요한 갱신을 방지함으로써 로깅 부하가 감소했지만 블록 I/O는 여전히 높다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 4
SELECT del_yn
  FROM t1
 WHERE id = 1
   AND dt &amp;lt; DATE '2050-01-01' - 30
   AND del_yn = 'N' -- 추가
;

----------------------------------------------------------------
| Id  | Operation                   | Name  | A-Rows | Buffers |
----------------------------------------------------------------
|   0 | SELECT STATEMENT            |       |   1440 |     848 |
|*  1 |  TABLE ACCESS BY INDEX ROWID| T1    |   1440 |     848 |
|*  2 |   INDEX RANGE SCAN          | T1_PK |    100K|     524 |
----------------------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;

아래와 같이 인덱스를 생성하자. NDV과 무관하게 등호(=)로 입력된 del_yn 칼럼이 범위로 입력된 dt 칼럼보다 앞쪽에 위치해야 한다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 5
CREATE INDEX t1_x1 ON t1 (id, del_yn, dt);

인덱스가 생성되었습니다.
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;

쿼리를 다시 수행하면 블록 I/O가 9로 감소한 것을 확인할 수 있다.&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 6
SELECT del_yn
  FROM t1
 WHERE id = 1
   AND dt &amp;lt; DATE '2050-01-01' - 30
   AND del_yn = 'N';

----------------------------------------------------
| Id  | Operation        | Name  |A-Rows | Buffers |
----------------------------------------------------
|   0 | SELECT STATEMENT |       |  1440 |       9 |
|*  1 |  INDEX RANGE SCAN| T1_X1 |  1440 |       9 |
----------------------------------------------------
&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;ol class=&quot;footnotes&quot;&gt;
    &lt;li id=&quot;footnote_379_1&quot;&gt;일전에 언급한 시한폭탄 유형이다. &lt;a href=&quot;#footnote_link_379_1&quot;&gt;[본문으로]&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;footnote_379_2&quot;&gt;1분에 1개의 상품을 조회하고 1일에 1회 로그인한다고 가정하면 갱신 대상이 최대 1440건이다. &lt;a href=&quot;#footnote_link_379_2&quot;&gt;[본문으로]&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;</description>
      <category>Oracle/Tuning</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/379</guid>
      <comments>https://hrjeong.tistory.com/379#entry379comment</comments>
      <pubDate>Thu, 9 Jul 2020 10:27:38 +0900</pubDate>
    </item>
    <item>
      <title>여부 속성으로 인한 성능 저하 사례 #1</title>
      <link>https://hrjeong.tistory.com/378</link>
      <description>&lt;p&gt;여부 속성으로 인한 성능 저하 사례를 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;테스트를 위해 아래의 테이블을 생성하자. t1을 전문 전송 테이블로 가정하자.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 1
DROP TABLE t1 PURGE;

CREATE TABLE t1 AS
SELECT ROWNUM AS id
     , LPAD ('X', 4000, 'X') AS doc
     , CASE WHEN ROWNUM &amp;gt; 9990 THEN 'N' ELSE 'Y' END AS if_yn
  FROM XMLTABLE ('1 to 10000');

ALTER TABLE t1 ADD CONSTRAINT t1_pk PRIMARY KEY (id);

EXEC DBMS_STATS.GATHER_TABLE_STATS ('SCOTT', 'T1', method_opt =&amp;gt; 'FOR ALL COLUMNS SIZE SKEWONLY');
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;

전문 전송은 보통 아래의 철차로 수행된다. if_yn이 N인 미전송 전문을 전송하고, 전송이 완료되면 if_yn 값을 Y로 갱신하는 방식이다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 2
BEGIN
    FOR f1 IN (SELECT * FROM t1 WHERE if_yn = 'N' ORDER BY id)
    LOOP
        -- 전송
        UPDATE t1 SET if_yn = 'Y' WHERE c1 = f1.c1;
    END LOOP;
END;
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;

아래는 CURSOR FOR LOOP 문의 쿼리의 실행 계획이다. 인덱스가 없으므로 t1 테이블을 전체 스캔한다. 전문 데이터를 삭제하지 않는다면 테이블 크기가 선형적으로 증가하므로 블록 I/O도 선형적으로 증가한다.&lt;sup class=&quot;footnote&quot;&gt;&lt;a href=&quot;#footnote_378_1&quot; id=&quot;footnote_link_378_1&quot; onmouseover=&quot;tistoryFootnote.show(this, 378, 1)&quot; onmouseout=&quot;tistoryFootnote.hide(378, 1)&quot; style=&quot;color:#f9650d; font-family: Verdana, Sans-serif; display: inline;&quot;&gt;&lt;span style=&quot;display: none;&quot;&gt;[각주:&lt;/span&gt;1&lt;span style=&quot;display: none;&quot;&gt;]&lt;/span&gt;&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 3
SELECT * FROM t1 WHERE if_yn = 'N' ORDER BY id;

------------------------------------------------------
| Id  | Operation          | Name | A-Rows | Buffers |
------------------------------------------------------
|   0 | SELECT STATEMENT   |      |     10 |   10012 |
|   1 |  SORT ORDER BY     |      |     10 |   10012 |
|*  2 |   TABLE ACCESS FULL| T1   |     10 |   10012 |
------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter(&quot;IF_YN&quot;='N')
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;

아래는 if_yn 칼럼의 통계 정보다. num_distinct가 2이므로 인덱스 생성 대상이 아닌 칼럼으로 오판할 수 있다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 4
SELECT num_distinct, histogram FROM user_tab_columns WHERE table_name = 'T1' AND column_name = 'IF_YN';

NUM_DISTINCT HISTOGRAM
------------ ---------
           2 FREQUENCY

1개의 행이 선택되었습니다.
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;

실제 데이터를 조회해보면 if_yn이 N인 행은 10건에 불과한 것을 확인할 수 있다. 전문 전송의 특성에 따라 미전송 건은 항상 적을 수 밖에 없다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 5
SELECT if_yn, COUNT (*) AS cnt FROM t1 GROUP BY if_yn;

IF_YN  CNT
----- ----
Y     9990
N       10

2 행이 선택되었습니다.
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;

아래와 같이 if_yn 칼럼에 인덱스를 생성하자.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 6
CREATE INDEX t1_x1 ON t1 (if_yn);

인덱스가 생성되었습니다.
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;

쿼리를 다시 수행하면 쿼리가&amp;nbsp;인덱스 스캔으로 수행된다. 블록 I/O가 10,012에서 12로 감소했다. 전문 전송이 1분 간격으로 수행된다면 일별&amp;nbsp;14,400,000개의 블록 I/O를 감소시킬 수 있다.&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 7
SELECT * FROM t1 WHERE if_yn = 'N' ORDER BY id;

-----------------------------------------------------------------
| Id  | Operation                    | Name  | A-Rows | Buffers |
-----------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |     10 |      12 |
|   1 |  SORT ORDER BY               |       |     10 |      12 |
|   2 |   TABLE ACCESS BY INDEX ROWID| T1    |     10 |      12 |
|*  3 |    INDEX RANGE SCAN          | T1_X1 |     10 |       2 |
-----------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - access(&quot;IF_YN&quot;='N')
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;추가로 if_yn 칼럼에 히스토그램이 생성하면 인덱스 생성으로 인한 부작용을 방지할 수 있다. 아래 쿼리는 if_yn를 Y로 조회했다. 히스토그램으로 인해 인덱스를 스캔하지 않고 테이블을 전체 스캔한 것을 확인할 수 있다.&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 8
SELECT * FROM t1 WHERE if_yn = 'Y';

-----------------------------------------------------
| Id  | Operation         | Name | A-Rows | Buffers |
-----------------------------------------------------
|   0 | SELECT STATEMENT  |      |   9990 |   10011 |
|*  1 |  TABLE ACCESS FULL| T1   |   9990 |   10011 |
-----------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter(&quot;IF_YN&quot;='Y')
&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
  &lt;ol class=&quot;footnotes&quot;&gt;
    &lt;li id=&quot;footnote_378_1&quot;&gt;이런 류의 성능 저하를 시한폭탄에 비유하곤 한다. &lt;a href=&quot;#footnote_link_378_1&quot;&gt;[본문으로]&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;</description>
      <category>Oracle/Tuning</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/378</guid>
      <comments>https://hrjeong.tistory.com/378#entry378comment</comments>
      <pubDate>Wed, 8 Jul 2020 10:24:40 +0900</pubDate>
    </item>
    <item>
      <title>MERGE 문의 동시성</title>
      <link>https://hrjeong.tistory.com/377</link>
      <description>&lt;p&gt;MERGE 문은 WHEN 절의 기술 순서와 무관하게 UPDATE, INSERT 순서로 동작한다. 이때 ON 절은 UPDATE 문의 WHERE 절과 유사하게 동작한다.&amp;nbsp;이로 인해 예상치 못한 에러가 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;테스트를 위해 아래의 테이블을 생성하자.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 1
DROP TABLE t1 PURGE;
CREATE TABLE t1 (c1 VARCHAR2(1), c2 NUMBER);
ALTER TABLE t1 ADD CONSTRAINT t1_pk PRIMARY KEY (c1);
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;

S1 세션에서 아래의 MERGE 문을 수행하자. 최초 건은 INSERT 문을 수행하고, 이후 동일 건에 대해 UPDATE 문을 수행하려는 의도로 작성된 MERGE 문이다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 2: S1
MERGE
 INTO t1
USING DUAL
   ON (c1 = 'A')
 WHEN MATCHED THEN
      UPDATE SET c2 = c2 + 1
 WHEN NOT MATCHED THEN
      INSERT VALUES ('A', 1);

1 행이 병합되었습니다.
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;


S2 세션에서 동일한 MERGE 문을 수행하면 블로킹이 발생한다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 3: S2
MERGE
 INTO t1
USING DUAL
   ON (c1 = 'A')
 WHEN MATCHED THEN
      UPDATE SET c2 = c2 + 1
 WHEN NOT MATCHED THEN
      INSERT VALUES ('A', 1);

-- 블로킹
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;


S1 세션에서 커밋을 수행하자.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 4: S1
COMMIT;

커밋이 완료되었습니다.
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;



S2 세션은 UPDATE 문이 수행되지 않고 ORA-00001 에러가 발생한다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 5: S2
ORA-00001: 무결성 제약 조건(SCOTT.T1_PK)에 위배됩니다
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;


예제의 MERGE 문은 아래 코드와 유사하게 동작한다. UPDATE 문의 WHERE 절은 consistent read로 동작한다.&amp;nbsp;S2 세션의 MERGE 문은 UPDATE 대상이 없어 INSERT 문이 수행했지만,&amp;nbsp;S1 세션이 동일 PK로 INSERT 문이 수행하고 있어 블로킹이 발생한 것이다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 6
BEGIN
    UPDATE t1 SET c2 = c2 + 1 WHERE c1 = 'A';

    IF SQL%ROWCOUNT = 0 THEN
        INSERT INTO t1 VALUES ('A', 1);
    END IF;
END;
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;


의도대로 구문을 동작시키려면 아래 코드를 사용해야 한다. S1 세션에서 아래의&amp;nbsp;코드를 수행하자.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 7: S1
BEGIN
    INSERT INTO t1 VALUES ('B', 1);
EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
        UPDATE t1 SET c2 = c2 + 1 WHERE c1 = 'B';
END;
/

PL/SQL 처리가 정상적으로 완료되었습니다.
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;


S1 세션에서 동일한 코드를 수행하면 블로킹이 발생한다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 8: S2
BEGIN
    INSERT INTO t1 VALUES ('B', 1);
EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
        UPDATE t1 SET c2 = c2 + 1 WHERE c1 = 'B';
END;
/

-- 블로킹
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;


S1 세션에서 커밋을 수행하자.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 9: S1
COMMIT;

커밋이 완료되었습니다.
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p&gt;


S2&amp;nbsp;세션은&amp;nbsp;에러가 발생하지 않고 UPDATE 문이 수행된다. 에러는 발생하지 않지만 UPDATE 문으로 인해 동시성 저하는 여전히 발생할 수 있다는 점에 주의해야 한다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 10: S2

PL/SQL 처리가 정상적으로 완료되었습니다.

SELECT * FROM t1;

C1 C2
-- --
A   1
B   2

2 행이 선택되었습니다.
&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Oracle/SQL</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/377</guid>
      <comments>https://hrjeong.tistory.com/377#entry377comment</comments>
      <pubDate>Mon, 6 Jul 2020 08:45:17 +0900</pubDate>
    </item>
    <item>
      <title>뷰 병합이 동작하지 않는 사례</title>
      <link>https://hrjeong.tistory.com/376</link>
      <description>&lt;p style=&quot;text-align: justify;&quot;&gt;뷰 병합이 동작하지 않아 쿼리&amp;nbsp;성능이&amp;nbsp;저하된 사례를 살펴보자.&lt;/p&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;테스트를 위해 아래와 같이 테이블과 뷰를 생성하자. 
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 1
DROP TABLE t1 PURGE;
DROP TABLE t2 PURGE;
DROP TABLE t3 PURGE;

CREATE TABLE t1 AS SELECT ROWNUM AS c1, ROWNUM AS c2 FROM XMLTABLE ('1 to 10000');
CREATE TABLE t2 AS SELECT * FROM t1;
CREATE TABLE t3 AS SELECT * FROM t1;

CREATE UNIQUE INDEX t1_u1 ON t1 (c2);
CREATE UNIQUE INDEX t2_u1 ON t2 (c1);
CREATE UNIQUE INDEX t3_u1 ON t3 (c1);

CREATE OR REPLACE VIEW v1 AS SELECT * FROM t2;
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래 쿼리는 테이블과 뷰를 조인한다. v1 뷰가 병합된 것을 확인할 수 있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 2
SELECT *
  FROM t1 a, v1 b
 WHERE a.c2 = 1
   AND b.c1 = a.c1;

-----------------------------------------------------------------------------------
| Id  | Operation                    | Name  | Starts | E-Rows | A-Rows | Buffers |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |      1 |        |      1 |       6 |
|   1 |  NESTED LOOPS                |       |      1 |      1 |      1 |       6 |
|   2 |   TABLE ACCESS BY INDEX ROWID| T1    |      1 |      1 |      1 |       3 |
|*  3 |    INDEX UNIQUE SCAN         | T1_U1 |      1 |      1 |      1 |       2 |
|   4 |   TABLE ACCESS BY INDEX ROWID| T2    |      1 |      1 |      1 |       3 |
|*  5 |    INDEX UNIQUE SCAN         | T2_U1 |      1 |      1 |      1 |       2 |
-----------------------------------------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래와 같이 뷰에 스칼라 서브 쿼리와 스칼라 서브 쿼리의 결과를 사용한 필터 조건을 추가해보자.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 3
CREATE OR REPLACE VIEW v1
AS
SELECT *
  FROM (SELECT a.c1
             , (SELECT x.c2 FROM t3 x WHERE x.c1 = a.c2) AS c2 --&amp;gt; ADD
          FROM t2 a)
 WHERE c2 = 1 --&amp;gt; ADD
;
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

쿼리를 다시 수행해보면 v1 뷰가 병합되지 않은 것을 확인할 수 있다. 스칼라 서브 쿼리가 10,000번 수행되어 블록 I/O가 10,175개로 증가했다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 4
SELECT /*+ MERGE(B) */
       *
  FROM t1 a, v1 b
 WHERE a.c2 = 1
   AND b.c1 = a.c1;

-----------------------------------------------------------------------------------
| Id  | Operation                    | Name  | Starts | E-Rows | A-Rows | Buffers |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |      1 |        |      1 |   10175 |
|   1 |  TABLE ACCESS BY INDEX ROWID | T3    |  10000 |      1 |  10000 |   10142 |
|*  2 |   INDEX UNIQUE SCAN          | T3_U1 |  10000 |      1 |  10000 |     142 |
|   3 |  NESTED LOOPS                |       |      1 |      1 |      1 |   10175 |
|   4 |   TABLE ACCESS BY INDEX ROWID| T1    |      1 |      1 |      1 |       3 |
|*  5 |    INDEX UNIQUE SCAN         | T1_U1 |      1 |      1 |      1 |       2 |
|*  6 |   VIEW                       |       |      1 |      1 |      1 |   10172 |
|   7 |    TABLE ACCESS FULL         | T2    |      1 |  10000 |  10000 |      30 |
-----------------------------------------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

힌트를 추가해도 JPPD가 동작하지 않는 것을 확인할 수 있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 5
SELECT /*+ LEADING(A) USE_NL(B) PUSH_PRED(B) */
       *
  FROM t1 a, v1 b
 WHERE a.c2 = 1
   AND b.c1 = a.c1;

-----------------------------------------------------------------------------------
| Id  | Operation                    | Name  | Starts | E-Rows | A-Rows | Buffers |
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |       |      1 |        |      1 |   10175 |
|   1 |  TABLE ACCESS BY INDEX ROWID | T3    |  10000 |      1 |  10000 |   10142 |
|*  2 |   INDEX UNIQUE SCAN          | T3_U1 |  10000 |      1 |  10000 |     142 |
|   3 |  NESTED LOOPS                |       |      1 |      1 |      1 |   10175 |
|   4 |   TABLE ACCESS BY INDEX ROWID| T1    |      1 |      1 |      1 |       3 |
|*  5 |    INDEX UNIQUE SCAN         | T1_U1 |      1 |      1 |      1 |       2 |
|*  6 |   VIEW                       |       |      1 |      1 |      1 |   10172 |
|   7 |    TABLE ACCESS FULL         | T2    |      1 |  10000 |  10000 |      30 |
-----------------------------------------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

조인을 아우터 조인으로 변경하면&amp;nbsp;JPPD가 동작하지만, 뷰를 사용하는 쿼리가 다수인 경우 근본적인 해법이 될 수 없다.&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 6
SELECT /*+ LEADING(A) USE_NL(B) PUSH_PRED(B) NO_OUTER_JOIN_TO_INNER */
       *
  FROM t1 a, v1 b
 WHERE a.c2 = 1
   AND b.c1(+) = a.c1
   AND b.c1 IS NOT NULL;

-------------------------------------------------------------------------------------
| Id  | Operation                      | Name  | Starts | E-Rows | A-Rows | Buffers |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |       |      1 |        |      1 |       9 |
|   1 |  TABLE ACCESS BY INDEX ROWID   | T3    |      1 |      1 |      1 |       3 |
|*  2 |   INDEX UNIQUE SCAN            | T3_U1 |      1 |      1 |      1 |       2 |
|*  3 |  FILTER                        |       |      1 |        |      1 |       9 |
|   4 |   NESTED LOOPS OUTER           |       |      1 |      1 |      1 |       9 |
|   5 |    TABLE ACCESS BY INDEX ROWID | T1    |      1 |      1 |      1 |       3 |
|*  6 |     INDEX UNIQUE SCAN          | T1_U1 |      1 |      1 |      1 |       2 |
|*  7 |    VIEW PUSHED PREDICATE       |       |      1 |      1 |      1 |       6 |
|   8 |     TABLE ACCESS BY INDEX ROWID| T2    |      1 |      1 |      1 |       3 |
|*  9 |      INDEX UNIQUE SCAN         | T2_U1 |      1 |      1 |      1 |       2 |
-------------------------------------------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

해당 현상은 스칼라 서브 쿼리의 결과를 필터 조건으로 사용했기 때문이다. t3 테이블의 c1 열이 고유하므로 뷰의 스칼라 서브 쿼리를 조인으로 변경할 수 있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 7
CREATE OR REPLACE VIEW v1
AS
SELECT a.c1, b.c2
  FROM t2 a, t3 b
 WHERE b.c1(+) = a.c2
   AND b.c2(+) = 1;
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

v1 뷰가 병합되어 블록 I/O가 9개로 감소한 것을 확인할 수 있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 8
SELECT *
  FROM t1 a, v1 b
 WHERE a.c2 = 1
   AND b.c1 = a.c1;

------------------------------------------------------------------------------------
| Id  | Operation                     | Name  | Starts | E-Rows | A-Rows | Buffers |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |       |      1 |        |      1 |       9 |
|   1 |  NESTED LOOPS OUTER           |       |      1 |      1 |      1 |       9 |
|   2 |   NESTED LOOPS                |       |      1 |      1 |      1 |       6 |
|   3 |    TABLE ACCESS BY INDEX ROWID| T1    |      1 |      1 |      1 |       3 |
|*  4 |     INDEX UNIQUE SCAN         | T1_U1 |      1 |      1 |      1 |       2 |
|   5 |    TABLE ACCESS BY INDEX ROWID| T2    |      1 |      1 |      1 |       3 |
|*  6 |     INDEX UNIQUE SCAN         | T2_U1 |      1 |      1 |      1 |       2 |
|*  7 |   TABLE ACCESS BY INDEX ROWID | T3    |      1 |      1 |      1 |       3 |
|*  8 |    INDEX UNIQUE SCAN          | T3_U1 |      1 |      1 |      1 |       2 |
------------------------------------------------------------------------------------
&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Oracle/Tuning</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/376</guid>
      <comments>https://hrjeong.tistory.com/376#entry376comment</comments>
      <pubDate>Tue, 7 Apr 2020 16:40:47 +0900</pubDate>
    </item>
    <item>
      <title>조인에 따른 사용자 함수의 동작</title>
      <link>https://hrjeong.tistory.com/375</link>
      <description>&lt;p style=&quot;text-align: justify;&quot;&gt;조인에 따른 사용자 함수의 동작을 살펴보자.&lt;/p&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;p&gt;테스트를 위해 아래와 같이 테이블과 함수를 생성하자.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 1
DROP TABLE t1 PURGE;
DROP TABLE t2 PURGE;

CREATE TABLE t1 AS SELECT ROWNUM AS c1 FROM XMLTABLE ('1 to 10');
CREATE TABLE t2 AS SELECT DECODE (c1, 1, 1, c1 + 10) AS c1 FROM t1;

CREATE OR REPLACE FUNCTION f1 (i_c1 IN NUMBER, i_second IN NUMBER)
    RETURN NUMBER
IS
BEGIN
    DBMS_LOCK.SLEEP (i_second);
    RETURN 1;
END;
/
&lt;/pre&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래 쿼리는 t2 테이블을 NL 조인하여 99개의 블록 I/O가 발생했다. f1 함수는 조인이 성공한 1회만 수행되어 쿼리 수행에 0.1초가 소요되었다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 2
SELECT /*+ LEADING(A) USE_NL(B) */
       *
  FROM t1 a, t2 b
 WHERE b.c1 = a.c1
   AND f1 (b.c1, 0.1) = 1;

----------------------------------------------------------------------------
| Id  | Operation          | Name | Starts | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |      1 |      1 |00:00:00.11 |      99 |
|   1 |  NESTED LOOPS      |      |      1 |      1 |00:00:00.11 |      99 |
|   2 |   TABLE ACCESS FULL| T1   |      1 |     10 |00:00:00.01 |       9 |
|*  3 |   TABLE ACCESS FULL| T2   |     10 |      1 |00:00:00.11 |      90 |
----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter((&quot;B&quot;.&quot;C1&quot;=&quot;A&quot;.&quot;C1&quot; AND &quot;F1&quot;(&quot;B&quot;.&quot;C1&quot;,.1)=1))
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래 쿼리는 해시 조인으로 수행되어 블록 I/O가 21개로 감소했지만, f1 함수가 10회 수행되어 쿼리 수행에 1.1초가 소요되었다. 조인 대상을 각각 액세스하는 해시 조인의 특성으로 인한&amp;nbsp;현상이다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 3
SELECT /*+ LEADING(A) USE_HASH(B) */
       *
  FROM t1 a, t2 b
 WHERE b.c1 = a.c1
   AND f1 (b.c1, 0.1) = 1;

----------------------------------------------------------------------------
| Id  | Operation          | Name | Starts | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |      1 |      1 |00:00:01.10 |      21 |
|*  1 |  HASH JOIN         |      |      1 |      1 |00:00:01.10 |      21 |
|   2 |   TABLE ACCESS FULL| T1   |      1 |     10 |00:00:00.01 |       9 |
|*  3 |   TABLE ACCESS FULL| T2   |      1 |      1 |00:00:01.09 |       9 |
----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access(&quot;B&quot;.&quot;C1&quot;=&quot;A&quot;.&quot;C1&quot;)
   3 - filter(&quot;F1&quot;(&quot;B&quot;.&quot;C1&quot;,.1)=1)
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래 쿼리처럼 조인 후 함수를&amp;nbsp;수행하면 함수가 1회만 수행되어 수행 시간이 0.1초로 감소하는 것을 확인할 수 있다. 조인 부하와 함수 부하의 비율에 따라 적용 여부를 판단해야 한다.&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 4
SELECT *
  FROM (SELECT /*+ NO_MERGE LEADING(A) USE_HASH(B) */
               a.c1 AS ac1, b.c1 AS bc1
          FROM t1 a, t2 b
         WHERE b.c1 = a.c1)
 WHERE f1 (bc1, 0.1) = 1;

-----------------------------------------------------------------------------
| Id  | Operation           | Name | Starts | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |      1 |      1 |00:00:00.11 |      21 |
|*  1 |  VIEW               |      |      1 |      1 |00:00:00.11 |      21 |
|*  2 |   HASH JOIN         |      |      1 |      1 |00:00:00.01 |      21 |
|   3 |    TABLE ACCESS FULL| T1   |      1 |     10 |00:00:00.01 |       9 |
|   4 |    TABLE ACCESS FULL| T2   |      1 |     10 |00:00:00.01 |       9 |
-----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter(&quot;F1&quot;(&quot;BC1&quot;,.1)=1)
   2 - access(&quot;B&quot;.&quot;C1&quot;=&quot;A&quot;.&quot;C1&quot;)
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

쿼리가 복잡한 경우 아래처럼 함수가 사용된 조건을 조인의 필터 조건으로 변경할 수도 있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 5
SELECT /*+ LEADING(A) USE_HASH(B) */
       *
  FROM t1 a, t2 b
 WHERE b.c1 = a.c1
   AND f1 (b.c1, 0.1) + a.c1 = 1 + a.c1;

----------------------------------------------------------------------------
| Id  | Operation          | Name | Starts | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |      |      1 |      1 |00:00:00.11 |      21 |
|*  1 |  HASH JOIN         |      |      1 |      1 |00:00:00.11 |      21 |
|   2 |   TABLE ACCESS FULL| T1   |      1 |     10 |00:00:00.01 |       9 |
|   3 |   TABLE ACCESS FULL| T2   |      1 |     10 |00:00:00.01 |       9 |
----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access(&quot;B&quot;.&quot;C1&quot;=&quot;A&quot;.&quot;C1&quot;)
       filter(&quot;F1&quot;(&quot;B&quot;.&quot;C1&quot;,.1)+&quot;A&quot;.&quot;C1&quot;=1+&quot;A&quot;.&quot;C1&quot;)
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;함수 인수의 NDV가 적다면 캐싱 효과를 위해 서브 쿼리를 사용할&amp;nbsp;수도 있다. 서브 쿼리는 조인 후 필터 방식으로 수행된다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 6
SELECT /*+ LEADING(A) USE_HASH(B) */
       *
  FROM t1 a, t2 b
 WHERE b.c1 = a.c1
   AND (SELECT f1 (b.c1, 0.1) FROM DUAL) = 1;

-----------------------------------------------------------------------------
| Id  | Operation           | Name | Starts | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |      |      1 |      1 |00:00:00.11 |      21 |
|*  1 |  FILTER             |      |      1 |      1 |00:00:00.11 |      21 |
|*  2 |   HASH JOIN         |      |      1 |      1 |00:00:00.01 |      21 |
|   3 |    TABLE ACCESS FULL| T1   |      1 |     10 |00:00:00.01 |       9 |
|   4 |    TABLE ACCESS FULL| T2   |      1 |     10 |00:00:00.01 |       9 |
|   5 |   FAST DUAL         |      |      1 |      1 |00:00:00.01 |       0 |
-----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter(=1)
   2 - access(&quot;B&quot;.&quot;C1&quot;=&quot;A&quot;.&quot;C1&quot;)
&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Oracle/Tuning</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/375</guid>
      <comments>https://hrjeong.tistory.com/375#entry375comment</comments>
      <pubDate>Sat, 21 Mar 2020 09:54:20 +0900</pubDate>
    </item>
    <item>
      <title>월 기준 이력 조회</title>
      <link>https://hrjeong.tistory.com/374</link>
      <description>&lt;p style=&quot;text-align: justify;&quot;&gt;점 이력 데이터를 지정한 월 기준으로 조회하는 쿼리의 성능 개선 사례를 살해보자.&lt;/p&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;테스트를 위해 아래와 같이 데이터를 생성하자. t1은 이력 테이블, t2는 캘린더 테이블이다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 1
DROP TABLE t1 PURGE;
DROP TABLE t2 PURGE;

CREATE TABLE t1 (cd VARCHAR2(1), dt DATE, val NUMBER);

INSERT INTO t1 VALUES ('A', DATE '2050-01-01', 1);
INSERT INTO t1 VALUES ('A', DATE '2050-02-01', 2);
INSERT INTO t1 VALUES ('A', DATE '2050-03-01', 3);
INSERT INTO t1 VALUES ('A', DATE '2050-04-01', 1);
INSERT INTO t1 VALUES ('A', DATE '2050-05-01', 2);
INSERT INTO t1 VALUES ('A', DATE '2050-06-05', 3);
INSERT INTO t1 VALUES ('A', DATE '2050-06-10', 1);
INSERT INTO t1 VALUES ('A', DATE '2050-07-05', 2);
INSERT INTO t1 VALUES ('A', DATE '2050-07-10', 3);
COMMIT;

CREATE TABLE t2 AS SELECT DATE '2050-01-01' + ROWNUM - 1 AS dt FROM XMLTABLE ('1 to 365');
&lt;/pre&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;

아래는 개선 전 쿼리다. 비효율적인 소트 머지 조인과 소트 부하로 인해 이력 건수가 많은 경우 쿼리 성능이 급격히 저하될 수 있다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 2
SELECT dt, cd, val, adt
  FROM (SELECT b.dt, a.cd, a.val, a.dt AS adt
             , ROW_NUMBER () OVER (PARTITION BY a.cd, b.dt ORDER BY a.dt DESC) AS rn
          FROM t1 a, t2 b
         WHERE b.dt &amp;gt;= a.dt
           AND b.dt BETWEEN DATE '2050-07-01' AND DATE '2050-07-31')
 WHERE rn = 1;

DT         CD VAL ADT
---------- -- --- ----------
2050-07-01 A    1 2050-06-10
2050-07-02 A    1 2050-06-10
2050-07-03 A    1 2050-06-10
2050-07-04 A    1 2050-06-10
2050-07-05 A    2 2050-07-05
2050-07-06 A    2 2050-07-05
2050-07-07 A    2 2050-07-05
2050-07-08 A    2 2050-07-05
2050-07-09 A    2 2050-07-05
2050-07-10 A    3 2050-07-10
2050-07-11 A    3 2050-07-10
2050-07-12 A    3 2050-07-10
2050-07-13 A    3 2050-07-10
2050-07-14 A    3 2050-07-10
2050-07-15 A    3 2050-07-10
2050-07-16 A    3 2050-07-10
2050-07-17 A    3 2050-07-10
2050-07-18 A    3 2050-07-10
2050-07-19 A    3 2050-07-10
2050-07-20 A    3 2050-07-10
2050-07-21 A    3 2050-07-10
2050-07-22 A    3 2050-07-10
2050-07-23 A    3 2050-07-10
2050-07-24 A    3 2050-07-10
2050-07-25 A    3 2050-07-10
2050-07-26 A    3 2050-07-10
2050-07-27 A    3 2050-07-10
2050-07-28 A    3 2050-07-10
2050-07-29 A    3 2050-07-10
2050-07-30 A    3 2050-07-10
2050-07-31 A    3 2050-07-10

31 행이 선택되었습니다.

--------------------------------------------------------------------------------
| Id  | Operation                | Name | Starts | A-Rows | Buffers | Used-Mem |
--------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |      |      1 |     31 |      16 |          |
|*  1 |  VIEW                    |      |      1 |     31 |      16 |          |
|*  2 |   WINDOW SORT PUSHED RANK|      |      1 |     31 |      16 |20480  (0)| -- !
|   3 |    MERGE JOIN            |      |      1 |    266 |      16 |          |
|   4 |     SORT JOIN            |      |      1 |      9 |       7 | 2048  (0)|
|*  5 |      TABLE ACCESS FULL   | T1   |      1 |      9 |       7 |          |
|*  6 |     SORT JOIN            |      |      9 |    266 |       9 | 2048  (0)| -- !
|*  7 |      TABLE ACCESS FULL   | T2   |      1 |     31 |       9 |          |
--------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter(&quot;RN&quot;=1)
   2 - filter(ROW_NUMBER() OVER ( PARTITION BY &quot;A&quot;.&quot;CD&quot;,&quot;B&quot;.&quot;DT&quot; ORDER BY INTERNAL_FUNCTION(&quot;A&quot;.&quot;DT&quot;) DESC
              )&amp;lt;=1)
   5 - filter(&quot;A&quot;.&quot;DT&quot;&amp;lt;=TO_DATE(' 2050-07-31 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
   6 - access(&quot;B&quot;.&quot;DT&quot;&amp;gt;=&quot;A&quot;.&quot;DT&quot;)
       filter(&quot;B&quot;.&quot;DT&quot;&amp;gt;=&quot;A&quot;.&quot;DT&quot;)
   7 - filter((&quot;B&quot;.&quot;DT&quot;&amp;gt;=TO_DATE(' 2050-07-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND &quot;B&quot;.&quot;DT&quot;&amp;lt;=TO_DATE('
              2050-07-31 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))
&lt;/pre&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;

아래 쿼리는 LAG 함수로 종료일을 생성하여 t2 테이블을 조인했다. 조인과 소트 부하가 개선되었지만 소트 영역을 b.dt &amp;gt;= a.dt 조건으로 스캔한 후 b.dt &amp;lt;  a.ldt 조건으로 필터링하는 비효율이 남아 있다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 3
SELECT b.dt, a.cd, a.val, a.dt AS adt, ldt
  FROM (SELECT cd, dt, val
             , LAG (dt, 1, DATE '2050-07-31' + 1) OVER (PARTITION BY cd ORDER BY dt DESC) AS ldt
          FROM t1
         WHERE dt &amp;lt;= DATE '2050-07-31') a
     , t2 b
 WHERE a.ldt &amp;gt;= DATE '2050-07-01'
   AND b.dt &amp;gt;= a.dt
   AND b.dt &amp;lt;  a.ldt
   AND b.dt BETWEEN DATE '2050-07-01' AND DATE '2050-07-31';

DT         CD VAL ADT        LDT
---------- -- --- ---------- ----------
2050-07-01 A    1 2050-06-10 2050-07-05
2050-07-02 A    1 2050-06-10 2050-07-05
2050-07-03 A    1 2050-06-10 2050-07-05
2050-07-04 A    1 2050-06-10 2050-07-05
2050-07-05 A    2 2050-07-05 2050-07-10
2050-07-06 A    2 2050-07-05 2050-07-10
2050-07-07 A    2 2050-07-05 2050-07-10
2050-07-08 A    2 2050-07-05 2050-07-10
2050-07-09 A    2 2050-07-05 2050-07-10
2050-07-10 A    3 2050-07-10 2050-08-01
2050-07-11 A    3 2050-07-10 2050-08-01
2050-07-12 A    3 2050-07-10 2050-08-01
2050-07-13 A    3 2050-07-10 2050-08-01
2050-07-14 A    3 2050-07-10 2050-08-01
2050-07-15 A    3 2050-07-10 2050-08-01
2050-07-16 A    3 2050-07-10 2050-08-01
2050-07-17 A    3 2050-07-10 2050-08-01
2050-07-18 A    3 2050-07-10 2050-08-01
2050-07-19 A    3 2050-07-10 2050-08-01
2050-07-20 A    3 2050-07-10 2050-08-01
2050-07-21 A    3 2050-07-10 2050-08-01
2050-07-22 A    3 2050-07-10 2050-08-01
2050-07-23 A    3 2050-07-10 2050-08-01
2050-07-24 A    3 2050-07-10 2050-08-01
2050-07-25 A    3 2050-07-10 2050-08-01
2050-07-26 A    3 2050-07-10 2050-08-01
2050-07-27 A    3 2050-07-10 2050-08-01
2050-07-28 A    3 2050-07-10 2050-08-01
2050-07-29 A    3 2050-07-10 2050-08-01
2050-07-30 A    3 2050-07-10 2050-08-01
2050-07-31 A    3 2050-07-10 2050-08-01

31 행이 선택되었습니다.

-----------------------------------------------------------------------------
| Id  | Operation             | Name | Starts | A-Rows | Buffers | Used-Mem |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |      |      1 |     31 |      16 |          |
|   1 |  MERGE JOIN           |      |      1 |     31 |      16 |          |
|   2 |   SORT JOIN           |      |      1 |      3 |       7 | 2048  (0)| -- !
|*  3 |    VIEW               |      |      1 |      3 |       7 |          |
|   4 |     WINDOW SORT       |      |      1 |      9 |       7 | 2048  (0)|
|*  5 |      TABLE ACCESS FULL| T1   |      1 |      9 |       7 |          |
|*  6 |   FILTER              |      |      3 |     31 |       9 |          |
|*  7 |    SORT JOIN          |      |      3 |     78 |       9 | 2048  (0)| -- !
|*  8 |     TABLE ACCESS FULL | T2   |      1 |     31 |       9 |          |
-----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter((&quot;A&quot;.&quot;DT&quot;&amp;lt;=TO_DATE(' 2050-07-31 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND &quot;A&quot;.&quot;LDT&quot;&amp;gt;TO_DATE('
              2050-07-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))
   5 - filter(&quot;DT&quot;&amp;lt;=TO_DATE(' 2050-07-31 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
   6 - filter(&quot;B&quot;.&quot;DT&quot;&amp;lt;&quot;A&quot;.&quot;LDT&quot;)
   7 - access(&quot;B&quot;.&quot;DT&quot;&amp;gt;=&quot;A&quot;.&quot;DT&quot;)
       filter(&quot;B&quot;.&quot;DT&quot;&amp;gt;=&quot;A&quot;.&quot;DT&quot;)
   8 - filter((&quot;B&quot;.&quot;DT&quot;&amp;gt;=TO_DATE(' 2050-07-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND &quot;B&quot;.&quot;DT&quot;&amp;lt;=TO_DATE('
              2050-07-31 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))
&lt;/pre&gt;&lt;div style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/div&gt;

아래 쿼리는 이력의 일자수로 행을 복제하는 방식을 사용했다. 3번 쿼리의 비효율을 개선할 수 있다.
&lt;pre class=&quot;brush:sql&quot;&gt;-- 4
SELECT a.bdt + b.c1 - 1 AS dt, a.cd, a.val, a.dt AS adt, a.bdt, a.edt, a.edt - a.bdt AS cnt
  FROM (SELECT cd, dt, val
             , GREATEST (dt, DATE '2050-07-01') AS bdt
             , LAG (dt, 1, DATE '2050-07-31' + 1) OVER (PARTITION BY cd ORDER BY dt DESC) AS edt
          FROM t1
         WHERE dt &amp;lt;= DATE '2050-07-31') a
     , (SELECT ROWNUM AS c1
          FROM XMLTABLE ('1 to 31')
         WHERE ROWNUM &amp;lt;= DATE '2050-07-31' - DATE '2050-07-01' + 1) b
 WHERE a.edt &amp;gt;= DATE '2050-07-01'
   AND b.c1 &amp;lt;= a.edt - a.bdt;

DT         CD VAL ADT        BDT        EDT        CNT
---------- -- --- ---------- ---------- ---------- ---
2050-07-31 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-30 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-29 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-28 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-27 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-26 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-25 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-24 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-23 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-22 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-21 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-20 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-19 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-18 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-17 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-16 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-15 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-14 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-13 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-12 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-11 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-10 A    3 2050-07-10 2050-07-10 2050-08-01  22
2050-07-09 A    2 2050-07-05 2050-07-05 2050-07-10   5
2050-07-08 A    2 2050-07-05 2050-07-05 2050-07-10   5
2050-07-07 A    2 2050-07-05 2050-07-05 2050-07-10   5
2050-07-06 A    2 2050-07-05 2050-07-05 2050-07-10   5
2050-07-05 A    2 2050-07-05 2050-07-05 2050-07-10   5
2050-07-04 A    1 2050-06-10 2050-07-01 2050-07-05   4
2050-07-03 A    1 2050-06-10 2050-07-01 2050-07-05   4
2050-07-02 A    1 2050-06-10 2050-07-01 2050-07-05   4
2050-07-01 A    1 2050-06-10 2050-07-01 2050-07-05   4

31 행이 선택되었습니다.

--------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name                  | Starts | A-Rows | Buffers | Used-Mem |
--------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |                       |      1 |     31 |       7 |          |
|   1 |  MERGE JOIN                           |                       |      1 |     31 |       7 |          |
|   2 |   SORT JOIN                           |                       |      1 |      3 |       7 | 2048  (0)| -- !
|*  3 |    VIEW                               |                       |      1 |      3 |       7 |          |
|   4 |     WINDOW SORT                       |                       |      1 |      9 |       7 | 2048  (0)|
|*  5 |      TABLE ACCESS FULL                | T1                    |      1 |      9 |       7 |          |
|*  6 |   SORT JOIN                           |                       |      3 |     31 |       0 | 2048  (0)| -- !
|   7 |    VIEW                               |                       |      1 |     31 |       0 |          |
|*  8 |     COUNT STOPKEY                     |                       |      1 |     31 |       0 |          |
|   9 |      COLLECTION ITERATOR PICKLER FETCH| XQSEQUENCEFROMXMLTYPE |      1 |     31 |       0 |          |
--------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter(&quot;A&quot;.&quot;EDT&quot;&amp;gt;=TO_DATE(' 2050-07-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
   5 - filter(&quot;DT&quot;&amp;lt;=TO_DATE(' 2050-07-31 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))
   6 - access(INTERNAL_FUNCTION(&quot;B&quot;.&quot;C1&quot;)&amp;lt;=&quot;A&quot;.&quot;EDT&quot;-&quot;A&quot;.&quot;BDT&quot;)
       filter(INTERNAL_FUNCTION(&quot;B&quot;.&quot;C1&quot;)&amp;lt;=&quot;A&quot;.&quot;EDT&quot;-&quot;A&quot;.&quot;BDT&quot;)
   8 - filter(ROWNUM&amp;lt;=31)
&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Oracle/Tuning</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/374</guid>
      <comments>https://hrjeong.tistory.com/374#entry374comment</comments>
      <pubDate>Mon, 16 Mar 2020 11:03:36 +0900</pubDate>
    </item>
    <item>
      <title>특정 행의 계층 경로 조회</title>
      <link>https://hrjeong.tistory.com/373</link>
      <description>&lt;p style=&quot;text-align: justify;&quot;&gt;특정 행의 계층 경로를 조회해보자.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot;&gt;아래 쿼리는 전체 계층을 순전개하여 필요한 행을 필터링했다. 테이블 사이즈가 큰 경우 쿼리의 성능이 저하될 수 있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 1
SELECT     SUBSTR (SYS_CONNECT_BY_PATH (ename, '&amp;gt;'), 2) AS path
      FROM emp
     WHERE empno = 7788
START WITH mgr IS NULL
CONNECT BY mgr = PRIOR empno;

PATH
----------------
KING&amp;gt;JONES&amp;gt;SCOTT

1개의 행이 선택되었습니다.

---------------------------------------------------------
| Id  | Operation                                | Name |
---------------------------------------------------------
|   0 | SELECT STATEMENT                         |      |
|*  1 |  FILTER                                  |      |
|*  2 |   CONNECT BY NO FILTERING WITH START-WITH|      |
|   3 |    TABLE ACCESS FULL                     | EMP  |
---------------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래 쿼리는 특정 행의 계층을 역전개하고, LISTAGG 함수로 계층 경로를 생성했다. 최소한의 계층 전개로 계층 경로를 조회할 수 있다.&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 2
SELECT     LISTAGG (ename, '&amp;gt;') WITHIN GROUP (ORDER BY LEVEL DESC) AS path
      FROM emp
START WITH empno = 7788
CONNECT BY empno = PRIOR mgr;

PATH
----------------
KING&amp;gt;JONES&amp;gt;SCOTT

1개의 행이 선택되었습니다.

-------------------------------------------------
| Id  | Operation                      | Name   |
-------------------------------------------------
|   0 | SELECT STATEMENT               |        |
|   1 |  SORT GROUP BY                 |        |
|*  2 |   CONNECT BY WITH FILTERING    |        |
|   3 |    TABLE ACCESS BY INDEX ROWID | EMP    |
|*  4 |     INDEX UNIQUE SCAN          | PK_EMP |
|   5 |    NESTED LOOPS                |        |
|   6 |     CONNECT BY PUMP            |        |
|   7 |     TABLE ACCESS BY INDEX ROWID| EMP    |
|*  8 |      INDEX UNIQUE SCAN         | PK_EMP |
-------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;[2020-03-04]&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;아래는&amp;nbsp;마농님이 작성하신 쿼리다. 집계를 수행하지 않으므로 2번 쿼리에 비해 효율적이다.&lt;/div&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 3
SELECT     REVERSE (SUBSTR (SYS_CONNECT_BY_PATH (REVERSE (ename), '&amp;gt;'), 2)) AS path
      FROM emp
     WHERE CONNECT_BY_ISLEAF = 1
START WITH empno = 7788
CONNECT BY empno = PRIOR mgr;

PATH
----------------
KING&amp;gt;JONES&amp;gt;SCOTT

1개의 행이 선택되었습니다.

-------------------------------------------------
| Id  | Operation                      | Name   |
-------------------------------------------------
|   0 | SELECT STATEMENT               |        |
|*  1 |  FILTER                        |        |
|*  2 |   CONNECT BY WITH FILTERING    |        |
|   3 |    TABLE ACCESS BY INDEX ROWID | EMP    |
|*  4 |     INDEX UNIQUE SCAN          | PK_EMP |
|   5 |    NESTED LOOPS                |        |
|   6 |     CONNECT BY PUMP            |        |
|   7 |     TABLE ACCESS BY INDEX ROWID| EMP    |
|*  8 |      INDEX UNIQUE SCAN         | PK_EMP |
-------------------------------------------------
&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Oracle/SQL</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/373</guid>
      <comments>https://hrjeong.tistory.com/373#entry373comment</comments>
      <pubDate>Tue, 3 Mar 2020 22:35:02 +0900</pubDate>
    </item>
    <item>
      <title>INDEX 힌트의 세 가지 방식</title>
      <link>https://hrjeong.tistory.com/372</link>
      <description>&lt;p style=&quot;text-align: left;&quot;&gt;INDEX 힌트는 세 가지 방식으로 사용할 수 있다. 방식에 따른 장단점을 살펴보자.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot;&gt;테스트를 위해 아래와 같이 테이블을 생성하자.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 1
DROP TABLE t1 PURGE;
CREATE TABLE t1 AS SELECT ROWNUM AS c1, ROWNUM AS c2, ROWNUM AS c3, ROWNUM AS c4 FROM XMLTABLE ('1 to 10000');

CREATE INDEX t1_x1 ON t1 (c1);
CREATE INDEX t1_x2 ON t1 (c2, c3);
CREATE INDEX t1_x3 ON t1 (c1, c2, c3);
CREATE INDEX t1_x4 ON t1 (c1, c2, c4);
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래는 온라인 문서의 인덱스 구문이다. 테이블만 기술하면 전체 인덱스, 인덱스를 기술하면 기술한 인덱스 중에서 최적의 인덱스를 선택한다. 전자를 테이블 기술 방식, 후자를 인덱스 기술 방식이라고 하자.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;/*+ INDEX ([@queryblock] tablespec [indexspec [indexspec]...]) */
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래 쿼리는 INDEX 힌트에 테이블만 기술했다. 조건에 따라 최적의 인덱스가 선택된다. Outline Data를 보면 온라인 문서와 달리 테이블 (칼럼, [칼럼]...) 형식의 칼럼 기술 방식이 사용된 것을 확인할 수 있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 2-1
SELECT /*+ INDEX(T1) */ * FROM t1 WHERE c1 = 1;

-----------------------------------------------------
| Id  | Operation                           | Name  |
-----------------------------------------------------
|   0 | SELECT STATEMENT                    |       |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  2 |   INDEX RANGE SCAN                  | T1_X1 |
-----------------------------------------------------

Outline Data
-------------
  /*+
      INDEX_RS_ASC(@&quot;SEL$1&quot; &quot;T1&quot;@&quot;SEL$1&quot; (&quot;T1&quot;.&quot;C1&quot;))
  */

-- 2-2
SELECT /*+ INDEX(T1) */ * FROM t1 WHERE c1 = 1 AND c2 = 1;

-----------------------------------------------------
| Id  | Operation                           | Name  |
-----------------------------------------------------
|   0 | SELECT STATEMENT                    |       |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  2 |   INDEX RANGE SCAN                  | T1_X3 |
-----------------------------------------------------

Outline Data
-------------
  /*+
      INDEX_RS_ASC(@&quot;SEL$1&quot; &quot;T1&quot;@&quot;SEL$1&quot; (&quot;T1&quot;.&quot;C1&quot; &quot;T1&quot;.&quot;C2&quot; &quot;T1&quot;.&quot;C3&quot;))
  */
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래 쿼리는 INDEX 힌트를 칼럼 기술 방식으로 사용했다. 칼럼 순서와&amp;nbsp;선두 칼럼이 일치하는 인덱스 중에서 최적의 인덱스가 선택된다. 3-4번 쿼리는&amp;nbsp;선두 칼럼이 일치하는 인덱스가 없어&amp;nbsp;힌트가 무시되었다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 3-1
SELECT /*+ INDEX(T1 (C1)) */ * FROM t1 WHERE c1 = 1;

-----------------------------------------------------
| Id  | Operation                           | Name  |
-----------------------------------------------------
|   0 | SELECT STATEMENT                    |       |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  2 |   INDEX RANGE SCAN                  | T1_X1 |
-----------------------------------------------------

-- 3-2
SELECT /*+ INDEX(T1 (C1 C2)) */ * FROM t1 WHERE c1 = 1;

-----------------------------------------------------
| Id  | Operation                           | Name  |
-----------------------------------------------------
|   0 | SELECT STATEMENT                    |       |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  2 |   INDEX RANGE SCAN                  | T1_X3 |
-----------------------------------------------------

-- 3-3
SELECT /*+ INDEX(T1 (C1 C2 C4)) */ * FROM t1 WHERE c1 = 1;

-----------------------------------------------------
| Id  | Operation                           | Name  |
-----------------------------------------------------
|   0 | SELECT STATEMENT                    |       |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  2 |   INDEX RANGE SCAN                  | T1_X4 |
-----------------------------------------------------

-- 3-4
SELECT /*+ INDEX(T1 (C1 C2 C3 C4)) */ * FROM t1 WHERE c1 = 1;

-----------------------------------------------------
| Id  | Operation                           | Name  |
-----------------------------------------------------
|   0 | SELECT STATEMENT                    |       |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  2 |   INDEX RANGE SCAN                  | T1_X1 |
-----------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래 쿼리는 INDEX 힌트를 세 가지 방식으로 사용했다. 세 쿼리 모두 t1_x2 인덱스를 사용했다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 4
SELECT /*+ INDEX(T1)       */ * FROM t1 WHERE c2 = 1;
SELECT /*+ INDEX(T1 T1_X2) */ * FROM t1 WHERE c2 = 1;
SELECT /*+ INDEX(T1 (C2))  */ * FROM t1 WHERE c2 = 1;

-----------------------------------------------------
| Id  | Operation                           | Name  |
-----------------------------------------------------
|   0 | SELECT STATEMENT                    |       |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  2 |   INDEX RANGE SCAN                  | T1_X2 |
-----------------------------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

t1_x2 인덱스를 삭제하고 쿼리를 다시 수행해보자. 5-1번 쿼리는 t1_x3 인덱스를 FULL SCAN하고, 5-2, 5-3번 쿼리는 힌트가 무시되어 테이블을 FULL SCAN한다. 테이블 기술 방식은 최적의 인덱스가 없는 경우 쿼리의&amp;nbsp;성능이 저하될 수&amp;nbsp;있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 5-1
DROP INDEX t1_x2;

SELECT /*+ INDEX(T1) */ * FROM t1 WHERE c2 = 1;

-----------------------------------------------------
| Id  | Operation                           | Name  |
-----------------------------------------------------
|   0 | SELECT STATEMENT                    |       |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  2 |   INDEX FULL SCAN                   | T1_X3 | -- !
-----------------------------------------------------

-- 5-2
SELECT /*+ INDEX(T1 T1_X2) */ * FROM t1 WHERE c2 = 1;

----------------------------------
| Id  | Operation         | Name |
----------------------------------
|   0 | SELECT STATEMENT  |      |
|*  1 |  TABLE ACCESS FULL| T1   |
----------------------------------

-- 5-3
SELECT /*+ INDEX(T1 (C2)) */ * FROM t1 WHERE c2 = 1;

----------------------------------
| Id  | Operation         | Name |
----------------------------------
|   0 | SELECT STATEMENT  |      |
|*  1 |  TABLE ACCESS FULL| T1   |
----------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;
t1_x2 인덱스를 c3, c2 순서로 생성하고 쿼리를 다시 수행해보자. 6-1, 6-2번 쿼리는 t1_x2 인덱스를 FULL SCAN하고, 6-3번 쿼리는 힌트가 무시되어 테이블을 FULL SCAN한다. 인덱스 기술 방식도 인덱스가 변경되는 경우 쿼리의 성능이 저하될 수 있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 6-1
CREATE INDEX t1_x2 ON t1 (c3, c2);

SELECT /*+ INDEX(T1) */ * FROM t1 WHERE c2 = 1;

---------------------------------------------
| Id  | Operation                   | Name  |
---------------------------------------------
|   0 | SELECT STATEMENT            |       |
|   1 |  TABLE ACCESS BY INDEX ROWID| T1    |
|*  2 |   INDEX FULL SCAN           | T1_X2 | -- !
---------------------------------------------

-- 6-2
SELECT /*+ INDEX(T1 T1_X2) */ * FROM t1 WHERE c2 = 1;

---------------------------------------------
| Id  | Operation                   | Name  |
---------------------------------------------
|   0 | SELECT STATEMENT            |       |
|   1 |  TABLE ACCESS BY INDEX ROWID| T1    |
|*  2 |   INDEX FULL SCAN           | T1_X2 | -- !
---------------------------------------------

-- 6-3
SELECT /*+ INDEX(T1 (C2)) */ * FROM t1 WHERE c2 = 1;

----------------------------------
| Id  | Operation         | Name |
----------------------------------
|   0 | SELECT STATEMENT  |      |
|*  1 |  TABLE ACCESS FULL| T1   |
----------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;지금까지&amp;nbsp;살펴본데로 INDEX 힌트는 칼럼 기술 방식으로 사용하는 편이 가장 바람직하다. 인덱스 변경을 감안하여 스캔 효율을 만족하는 범위에서 최소의 칼럼을 기술하도록 하자.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Oracle/Tuning</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/372</guid>
      <comments>https://hrjeong.tistory.com/372#entry372comment</comments>
      <pubDate>Sun, 23 Feb 2020 11:34:12 +0900</pubDate>
    </item>
    <item>
      <title>사용자 정의 함수와 FBI</title>
      <link>https://hrjeong.tistory.com/371</link>
      <description>&lt;p style=&quot;text-align: justify;&quot;&gt;사용자 정의 함수의 성능을 개선하기 위해 FBI(Function Based Index)를 사용하는 경우가 있다. 사용자 정의 함수를 사용한 FBI의 동작을 살펴보자.&lt;/p&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;테스트를 위해 아래와 같이&amp;nbsp;테이블과 함수를 생성하자. t1_x1 인덱스는 f1 함수를 사용한 FBI다.&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 1
DROP TABLE t1 PURGE;
CREATE TABLE t1 AS SELECT ROWNUM AS c1, ROWNUM AS c2 FROM XMLTABLE ('1 to 2');

CREATE OR REPLACE FUNCTION f1 (i_val IN NUMBER)
    RETURN NUMBER DETERMINISTIC
IS
BEGIN
    DBMS_OUTPUT.PUT_LINE ('X');
    RETURN i_val + 1;
END;
/

CREATE INDEX t1_x1 ON t1 (c1, f1 (c1));
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래의&amp;nbsp;2-1, 2-2번 쿼리는 t1_x1 인덱스를 사용했고, f1 함수가 수행되지 않았다. 2-2번 쿼리에서 인덱스로 테이블을 액세스한 경우&amp;nbsp;인덱스에 저장된 값을 사용한다는 것을 알 수 있다. t1_x1 인덱스를 사용하지 않은 3번 쿼리는 f1 함수가 수행된다.&amp;nbsp;&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 2-1
SET SERVEROUTPUT ON

SELECT c1, f1 (c1) AS cf1 FROM t1 WHERE c1 = 1;

C1 C1F
-- ---
 1   2

1개의 행이 선택되었습니다.

----------------------------------
| Id  | Operation        | Name  |
----------------------------------
|   0 | SELECT STATEMENT |       |
|*  1 |  INDEX RANGE SCAN| T1_X1 |
----------------------------------

-- 2-2
SELECT c1, c2, f1 (c1) AS cf1 FROM t1 WHERE c1 = 1;

C1 C2 CF1
-- -- ---
 1  1   2

1개의 행이 선택되었습니다.

-----------------------------------------------------
| Id  | Operation                           | Name  |
-----------------------------------------------------
|   0 | SELECT STATEMENT                    |       |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |
|*  2 |   INDEX RANGE SCAN                  | T1_X1 |
-----------------------------------------------------

-- 2-3
SELECT c1, f1 (c1) AS cf1 FROM t1;

C1 CF1
-- ---
 1   2
 2   3

2 행이 선택되었습니다.

X
X

----------------------------------
| Id  | Operation         | Name |
----------------------------------
|   0 | SELECT STATEMENT  |      |
|   1 |  TABLE ACCESS FULL| T1   |
----------------------------------
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

FBI의 표현식은 테이블에 가상 칼럼(SYS_NC00003$)으로 생성된다. 가상 칼럼은 물리적으로 저장되지 않는다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 3
SELECT column_name, data_default, hidden_column, virtual_column FROM user_tab_cols WHERE table_name = 'T1';

COLUMN_NAME  DATA_DEFAULT      HIDDEN_COLUMN VIRTUAL_COLUMN
------------ ----------------- ------------- --------------
C1                             NO            NO
C2                             NO            NO
SYS_NC00003$ &quot;TUNA&quot;.&quot;F1&quot;(&quot;C1&quot;) YES           YES

3 행이 선택되었습니다.
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

블록 덤프를 수행하기 위해 아래와 같이 쿼리를 수행하자. 테이블은 7번 파일, 403번 블록, 인덱스는 7번 파일, 411번 블록에 저장되어 있다.
&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 4-1
SELECT object_id FROM user_objects WHERE object_name  = 'T1_X1';

OBJECT_ID
---------
   277654

1개의 행이 선택되었습니다.

-- 4-2
SELECT c1
     , sys_nc00003$
     , DBMS_ROWID.ROWID_RELATIVE_FNO (ROWID) AS tfno
     , DBMS_ROWID.ROWID_BLOCK_NUMBER (ROWID) AS tbno
     , DBMS_ROWID.ROWID_RELATIVE_FNO (SYS_OP_LBID (277654, 'L', ROWID)) AS ifno
     , DBMS_ROWID.ROWID_BLOCK_NUMBER (SYS_OP_LBID (277654, 'L', ROWID)) AS ibno
  FROM t1;

C1 SYS_NC00003$ TFNO TBNO IFNO IBNO
-- ------------ ---- ---- ---- ----
 1            2    7  403    7  411
 2            3    7  403    7  411

2 행이 선택되었습니다.

X
X
&lt;/pre&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;p style=&quot;text-align: justify;&quot;&gt;

아래와 같이 블록 덤프를 수행하자. t1 테이블은&amp;nbsp;f1 (c1) 표현식의 값이&amp;nbsp;저장되지 않고,&amp;nbsp;t1_x1 인덱스는 표현식의 결과가 저장된 것을 확인할&amp;nbsp;수 있다. 복호화 함수로 FBI를 생성하면 보안 이슈가 발생할 수 있는 점에 주의할 필요가 있겠다.&lt;/p&gt;&lt;pre class=&quot;brush:sql&quot;&gt;-- 5-1
ALTER SYSTEM DUMP DATAFILE 7 BLOCK 403;

tab 0, row 0, @0x1f77
tl: 9 fb: --H-FL-- lb: 0x0  cc: 2
col  0: [ 2]  c1 02 --&amp;gt; 1
col  1: [ 2]  c1 02 --&amp;gt; 1
tab 0, row 1, @0x1f6e
tl: 9 fb: --H-FL-- lb: 0x0  cc: 2
col  0: [ 2]  c1 03 --&amp;gt; 2
col  1: [ 2]  c1 03 --&amp;gt; 2

-- 5-2
ALTER SYSTEM DUMP DATAFILE 7 BLOCK 411;

row#0[8021] flag: -------, lock: 0, len=15
col 0; len 2; (2):  c1 02             --&amp;gt; 1 : c1
col 1; len 2; (2):  c1 03             --&amp;gt; 2 : f1 (c1)
col 2; len 6; (6):  01 c0 01 93 00 00 --&amp;gt; ROWID
row#1[8006] flag: -------, lock: 0, len=15
col 0; len 2; (2):  c1 03             --&amp;gt; 2 : c1
col 1; len 2; (2):  c1 04             --&amp;gt; 3 : f1 (c1)
col 2; len 6; (6):  01 c0 01 93 00 01 --&amp;gt; ROWID
&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Oracle/Administration</category>
      <author>정희락_</author>
      <guid isPermaLink="true">https://hrjeong.tistory.com/371</guid>
      <comments>https://hrjeong.tistory.com/371#entry371comment</comments>
      <pubDate>Sat, 22 Feb 2020 13:23:43 +0900</pubDate>
    </item>
  </channel>
</rss>