2010년 3월 11일 목요일

Estimation the Size of a Join

1. 용어 정리

T(R) - R relation 의 총 튜플 수

T(S) - S relation 의 총 튜플 수

 

V(relation,attribute) - relation 에서 해당 attribute 의 distinct value 값의 개수 ex) V(R,a)

 

R ⋈ S - R 과 S 를 자연조인

 

Optimizer 가 Join Cost 계산을 위해서는 조인시에 생기는 결과 집합의 크기를 산출해야 한다.

해당 정보는 Join Order 계산에 사용하게 된다.

R, S 가 Join R(X,Y) ⋈ S(Y,Z) 한다고 가정하자.

Y 값으로 R, S 가 Join 된다고 가정하였을때, Y 값이 R 과 S 둘다 관련이 있다고 해당 정보만을 가지고는 확인할수가 없다.

 

몇가지 가정을 해보자..

1. 두 릴레이션이 Y 값에 대한 disjoing set 이라면,  T(R ⋈ S) = 0 이다.

2. S 릴레이션의 키가 Y 이고 R 의 외래키와 대응된다면,

   정확히 S 의 각 튜플 하나는 R 의 각 튜플들과 Join 하게 된다. 따라서 T(R ⋈ S) = T(R) 이다.

3. 거의 모든 R 과 S 의 Y 값들이 같은 값을 가진다면, T(R ⋈ S) = T(R)T(S) 이다.

 

 

위의 경우 외에도 여러가지 상황이 존재하지만, 일반적인 경우를 가정하여, 간단히 가정을 해보자.

1. Containment of Value Sets

    만일 R 과 S 가 attribute Y 를 가지고 있고, V(R,Y) ≦ V(S,Y) 라면, 모든 R 의 Y 값들은 S 의 Y 값들에 포함된다.

 

2. Preservation of Value Sets

    만일 R 의 attirbure A 가 S 에 포함되지 않을때, V(R ⋈ S, A) = V(R,A) 이다.

 

사실 1번 가정은 아마도 모순일수 있다. 하지만, Y 가 S 의 Key 이고, R 의 외래키에 대응된다면, 조건을 만족한다. 또한 거의 많은 다른 케이스에서도 거의 1번의 조건을 만족한다.

왜냐하면, 직관적으로 S 가 많은 Y의 값들을 가지고 있고,  R 도 Y 값을 가지고 있다면, R 의 Y 값은 S 에도 존재할 가능성이 충분이 있다고, 기대 가능하기 때문이다.

 

2번가정 또한 아마도 모순일수 있다. 하지만, R ⋈ S 의 join attribute 들이 S 의 key 값이고, R 의 외래키와 대응된다면 2번의 조건을 만족한다.

사실상, R 이 "dangling tuples ( join 조건에 불필요한 튜플들 ) " 를 가지고 있다면, R 의 튜플들은 S 의 튜플과 join 되지 않게 된다. 하지만 dangling tuples 를 가지고 있더라도 2번의 가정은 여전히 유효하다.

 

위 2개의 가정을 기반으로하여  R(X,Y) ⋈ S(Y,Z) 의 join 크기를 추정할수 있다.

 

r 을 R 에 속하는 튜플이라고하고, s 는 S 에 속하는 튜플이라고 하자.

V(R,Y) ≥ V(S,Y) 라고 가정하면, 1번의 가정에 의해서 s 의 Y 값은 정확이 한개의 값이 R 에 나타나게 된다. 따라서, r 은 s의 1/V(R,Y) 의 같은 Y 값들을 가질수 있게 된다.

이와 유사하게, V(R,Y) < V(S,Y) 라면,  r 의 Y 값들은 S 에 나타나게 되고, 가능성은 1/V(S,Y) 이다.

 

일반적으로 Y가 일치할 확률은, 1/max(V(R,Y),V(S,Y)) 이며, Join Cardinarity 는

 

T(R ⋈ S) = T(R)T(S) / max(V(R,Y),V(S,Y))

 

위의 공식은 T(R ⋈ S) 에서 두 릴레이션간 일치하는 tuples 들의 수를 계산하게 된다.

 

 

위에서와 같이 기본적으로 Join 의 크기를 정확히 측정하는것은 한계가 있다.

위의 가정이 일반적으로 맞는 가정이라고 해서, 경험상 이렇게 추정하는것이 어느정도 맞으리라고 기대하게 되는 경험적인 산출방법이기 때문에, 넓은 Case에 있어서 정확한 조인 크기를 산출하지 못한다.

이를 보완하기 위해서 상용 DBMS 의 Join 크기 산출은 위의 공식과 거의 일치하지만,

히스토그램 같은 통계치를 사용하여 Join 크기 산출의 오차를 수정하는 노력이 이루어지고 있다.  

 

 

 

* 위의 글은 DATABASE SYSTEM The Complete Book Second Edition ( Jeffrey D. Ullman )

   의 내용을 을 참고하였습니다. *

2010년 3월 9일 화요일

Left Deep Tree

Left-deep trees

Filed under: Execution plans — Jonathan Lewis @ 11:42 pm UTC Jan 24,2007

In response to an article on hinting, one reader asked me to describe what I meant by the terms “left-deep” and “bushy” when talking about optimization strategies.

To demonstrate the differences, I created a couple of stored outlines in a 9.2 database and used the OEM Outline Manager to generate the pictures that I’ve pasted below. To read a tree, walk around it in an anti-clockwise direction, ignoring nodes on the way down and ”operating” them on the way back up. (But don’t operate them as you go “under” them, only as you go up, passing them to their right).

Left Deep

Walk down to the bottom left corner: we start with a tablescan of t4, then use an index into t3, joining the tables with a nested loop. We scan table t2 to join it with a merge join, then use an index into t1 to join it using a nested loop.

left_deep.jpg

Bushy

Again we head for the bottom-left corner, but in this case we start with a tablescan of t2 and use an index into t1 to join it with a nested loop join. Then we leave the join result set in suspension, and walk on round the tree to do a tablescan of t4, using an index into t3 to join it using a nested loop. This gives us a second we join result set, which we join to the first result set using a hash join.
bushy.jpg

As you can see, the typical left-deep tree will maximise the depth of the tree, with nothing but little twiglets branching to the right as you build it. The typical bushy tree will be stubbier with branchng bits sticking out all over the place.

 

출처 : http://jonathanlewis.wordpress.com/2007/01/24/left-deep-trees/ 

쿼리변환시 OUTER JOIN

1. 스키마 생성

create table t1(c1 number,c2 number);
create table t2(c1 number,c2 number);

 

insert into t1 values(1,1);
insert into t1 values(2,1);
insert into t1 values(3,1);
insert into t1 values(4,1);
insert into t1 values(5,1);


insert into t2 values(1,1);
insert into t2 values(2,2);
insert into t2 values(3,2);

 

SQL> select * from t1;

 C1    C2
---------- ----------
  1     1
  2     1
  3     1
  4     1
  5     1

 

SQL> select * from t2;

 C1    C2
---------- ----------
  1     1
  2     2
  3     2

 

 

옵티마이져는 기본적으로 Push Selection 을 선호한다. ( 조인연산 및 Cost 가 발생하는 집합연산을 하기전에 미리 Selection 연산을 수행하면 결과집합의 수를 줄일수 있기때문. )

 

OUTER JOIN 도 이러한 기법을 사용할수 있다.

하지만 자연조인 방식대로 사용한다면 쿼리 변환시 결과 집합이 달라질수 있는 오류를 범할수 있다.

예를 들어보자.

 

위의 스키마를 이용하여 LEFT OUTER JOIN 을 수행한다.

select * from T1 LEFT OUTER JOIN T2 ON T1.c1 = T2.c1;

 

 

 C1    C2       C1  C2
---------- ----------

  1     1          1   1
  2     1          2   2
  3     1          3   2
  5     1
  4     1

 

 T1.C1 = T2.C1 의 조건으로 수행하였기 때문에 오른쪽 릴레이션에 일치하는 값이 없더라도 NULL 로 결과집합에 포함되며. 왼쪽 릴레이션 또한 나머지 튜플들을 결과집합에 포함시키는걸 확인할 수 있다.

 

그럼 여기서 T2 테이블 ( RIGHT ) 에 조건을 추가해보자

select * from T1 LEFT OUTER JOIN T2 ON T1.c1 = T2.c1 where T2.c1 = 2;

 

 

 C1    C2       C1  C2
---------- ----------

  2     1           2   2

 

T1 과 T2 가 JOIN 후에 Where 조건절에 대한 Selection 이 일어난 것을 볼수 있다.

 

만일 T1, T2 가 매우 큰 릴레이션이고, Where 조건절이 매우 낮은 Selectivity 를 가지는 경우라면,

Join 하기전에 미리 Selection 연산을 수행하는것이 효율적이라고 생각할수 있을것이다.

 

Push Selection 기법을 사용하여 쿼리를 변환해보자.

1. select * from T1 LEFT OUTER JOIN T2 ON t1.c1 = t2.c1 AND T2.c1 = 2;

2. select * from T1 LEFT OUTER JOIN (select * from T2 where c1 = 2) T2 ON T1.c1 = T2.c1;

 

Where 조건절을 Join 조건절로 Push 하였다.

T2 릴레이션이 조건절에 대한 Selection 수행하여 릴레이션의 크기를 줄이고 T1 과 Join 하는것으로 보인다.

 

그럼 결과도 같을까?

SQL> select * from T1 LEFT OUTER JOIN T2 ON t1.c1 = T2.c1 AND T2.c1 = 2;

 C1    C2       C1  C2
---------- ----------
  2     1          2   2
  5     1
  4     1
  3     1
  1     1

 

SQL> select * from T1 LEFT OUTER JOIN (select * from T2 where c1 = 2) T2 ON T1.c1 = T2.c1;

 

 C1    C2       C1  C2
---------- ----------

  2     1          2   2
  5     1
  4     1
  3     1
  1     1

 

아.. 결과가 바뀌었다. 결과가 바뀔수 있는 쿼리변환은 의미가 없다.

왜 결과가 바뀌었을까? 이유는 간단하다.

 

OUTER JOIN 의 특성을 고려하지 않고 Push Selection 연산을 수행하였기 때문이다.

OUTER JOIN ON 절에 조건절을 추가하면 해당 조건절에 대해서 조인하기전에 이미 필터링이 일어난 집합을 가지고 JOIN 을 수행하게된다.

 

하지만 Where 절에 조건절을 추가하게되면 JOIN 이 완료된 집합에 대해서 조건절을 체크하게 되는데,

이경우 OUTER JOIN 의 특성을 잃어 버리게된다.

 

Push Selection 을 보장받고 싶다면 다음과 같이 변환한다.

SQL> select * from T1 LEFT OUTER JOIN T2 ON T1.c1 = T2.c1 AND T2.c1 = 2 where T2.c1 = 2;

 

 C1    C2       C1  C2
---------- ----------

  2     1           2   2

Push Selection 연산시 where 절의 조건을 삭제하지 않고 남겨 두어야 동일한 결과를 나타낼 수 있다.  

 

따라서 OUTER JOIN 에 대해서 Push Selection 하는것은 경우에 따라서 신중히 고려하여 수행하여야 한다.

어차피 똑똑한 옵티마이져가 알아서 할테지만 ...

 

2010년 3월 8일 월요일

동적 프로그래밍 ( Dynamic Programming )

저자: 김대곤


얼마 전에 완역된 Introduction to Algorithms(개정판)에도 Dynamic Programming은 고급 설계 및 분석 기법에 등장한다. 이번 기사에서는 Dynamic Programming의 핵심 아이디어와, 장점과 단점을 살펴보고, 또한 어떤 상황에 적용할 수 있는지 살펴보자. 그리고 나서 정말 고급(?)한 기술인지 스스로 판단해 보자.

먼저 다음 문제를 생각해 보자. 한 저택에는 30 개의 방이 있으며, 5 * 6 형태로 배열되어 있다(그림 1). 각 방에는 일정한 금액이 놓여져 있다.




[ 그림 1 ]


Start 지점에서 End 까지 가는 길들 중에서 가장 많은 금액을 모을 수 있는 길을 찾는 것이다. 단 위로 또는 왼쪽으로만 이동할 수 있다.

“그래, 간단한데. 가능한 모든 길을 전부 비교해 보면 되지.” 그런데, 문제는 계산이 그렇게 간단하지 않다는데 있다. 먼저, 가능한 길의 갯수는 126개이다. 각 길은 8 개의 방으로 구성되어 있다. 별로 계산하고 싶지 않은 숫자다. 사람 뿐 아니라 컴퓨터도 똑같은 생각을 가지고 있다.

이야기를 더 진행하기 전에 몇 가지 용어를 정의하도록 하자.

- MONEY(i,j) : (i,j)방에 있는 금액.
- UP(i,j) : (i,j)방 위에 있는 방, 즉 (i-1,j) 방.
- LEFT(i,j) : (i,j)방 위에 있는 왼쪽 방, 즉 (i,j-1) 방.
- COST(i,j,k,l) : (i,j)방에서 (m,n)방으로 갈 때 얻을 수 있는 최대 금액.
- PATH(i,j,k,l) : 최대 금액을 얻을 수 있는 (i,j)방에서 (m,n)방으로 가는 경로.

Start 지점에 있다고 가정해 보자. LEFT(5,6)로 가야하는지, UP(5,6)로 가야하는지를 제외하고는 무엇이든지 알아낼 수 있다면, 어떠한 정보가 필요한가? 만약, COST(LEFT(5,6))와 COST(UP(5,6))를 알고 있다면, 어느 길로 갈 것인가? 좀 더 직설적으로 말해서, UP(5,6) 입구에 COST(UP(5,6))=401, LEFT(5,6) 입구에 COST(LEFT(5,6))=391 이라고 써 놓았다면, 어디로 가겠는가? 당연히 UP(5,6)로 갈 것이다. 이제 문제는 각 COST(i,j)를 구하는 것으로 볼 수 있다. COST(i,j)는 다음과 같이 정의된다.

- COST(i,j) = MONEY(i,j), if i=1 && j=1
- COST(i,j) = MONEY(i,j)+COST(i,j-1), if i=1 && j>1
- COST(i,j) = MONEY(i,j)+COST(i-1,j), if i>1 && j=1
- COST(i,j) = MONEY(i,j)+MAX(COST(i-1,j),COST(i,j-1)), if i>1 && j>1

사실은 여기까지는 Induction를 이용한 알고리즘 설계이다. 이것을 그대로 옮기면 다음과 같은 프로그램을 작성할 수 있다.


public static int getRecursiveCost(int i, int j) {
        // Boundary case.
        if ( i == 0 && j == 0 ) {
            return rooms[i][j];
        }
       
        // Reach bottom
        if ( i == 0 ) {
            return rooms[i][j] + getRecursiveCost(i,j-1);
        }
       
        // Reach leftest.
        if ( j == 0 ) {
            return rooms[i][j] + getRecursiveCost(i-1,j);
        }
       
        int right = getRecursiveCost(i-1,j);
        int down  = getRecursiveCost(i,j-1);
       
        if ( right > down ) {
            return rooms[i][j] + right;
        }
       
        return rooms[i][j] + down;
       
    }


실행가능한 프로그램은 NaiveRecursion.java로 사이트에서 다운로드 가능하다. 이름에서도 알 수 있듯이 위의 프로그램은 효율적인 프로그램은 아니다. 그 이유는 이 재귀 프로그램의 재귀 트리(Recursion Tree)를 그려보면 쉽게 알 수 있다.




[ 그림 2 ]


같은 함수가 여러 번 호출된 것을 쉽게 알 수 있다. 예를 들면 COST(3,5)는 3번 호출되었다. 이것은 Dynamic Programming를 사용해야 하는 하나의 표지가 된다. 즉, Recursion Tree에서 반복이 있는 경우에 Dynamic Programming를 사용한다. 하지만 조금의 코드 수정으로도 Dynamic Programming과 같은 효과를 얻을 수 있다. 이러한 방법을 Memoization, Caching, 또는 Tabulation이라고 부른다. 이름에서도 알 수 있듯이, 한 번 계산한 값을 저장해 놓고, 필요하면 가져다 쓰는 것이다.


public static int getMemoizedCost(int i, int j) {
        if ( costs[i][j] != 0 ) {
            return costs[i][j];
        } 
       
        // Boundary case.
        if ( i == 0 && j == 0 ) {
            costs[i][j] = rooms[i][j];
            return costs[i][j];
        }
       
        // Reach bottom
        if ( i == 0 ) {
            costs[i][j] = rooms[i][j] + getMemoizedCost(i,j-1);
            return costs[i][j];
        }
       
        // Reach leftest.
        if ( j == 0 ) {
            costs[i][j] = rooms[i][j] + getMemoizedCost(i-1,j);
            return costs[i][j];
        }
       
        int right = getMemoizedCost(i-1,j);
        int down  = getMemoizedCost(i,j-1);
       
        if ( right > down ) {
            costs[i][j] = rooms[i][j] + right;
            return costs[i][j];
        }
       
        costs[i][j] = rooms[i][j] + down;
        return costs[i][j];
       
    }


여러 번 호출 되지만 재계산하지 않고 계산값을 사용한다. 나머지 코드는 입력과 무관한 작은 상수이고, 호출되는 인자의 값 범위는 1~5, 1~6 이므로 5*6의 시간이 걸린다. 즉, n*m 의 방이 있는 경우, O(n*m) 걸린다. 단순한 재귀 프로그램에 비해 엄청난 발전이다. 지수(exponential) 함수에서 제곱승으로 시간이 단축된 것이다.

Dynamic Programming으로 넘어가기 전에 Dynamic Programming를 적용할 수 있는 또 다른 기준에 대해서 알아보자. 만약 PATH(5,6,1,1)이 (3,5)방을 지나간다고 가정하자. 그러면 PATH(5,6,1,1)은 단순히 PATH(5,6,3,5)와 PATH(3,5,1,1)를 연결한 것이다. 증명은 간단하다. 만약 방(5,6)에서 방(3,5)로 가는 더 좋은 경로가 있다면, 그 경로와 PATH(3,5,1,1)를 연결하면, 방(5,6)에서 방(1,1)로 가는 더 좋은 경로를 얻게 되며, 이것은 처음에 택한 경로가 PATH(5,6,1,1)이라는 가정과 모순된다. 이러한 성질을 Optimal Substructure라고 하며, Dynamic Programming를 적용할 수 있는지에 대한 또 다른 아이디어를 제공한다.

우리가 걸어온 단계 중에서 가장 힘든 부분은 재귀식을 도출하는 것이다. 그 이후에는 단순한 수정에 불과하다. Dynamic Programming이란 Memoization이 필요하지 않도록 단순히 계산 순서를 조정하는 것이다. Memoization의 단점은 추가적인 메모리 사용과 재귀 프로그램이 만들어 내는 추가적인 부담이다. 장점은 간단함에 있다. Dynamic Programming의 장점은 Memoization의 단점이 없다는 것이고, 단점은 Memoization의 장점이 없다는 것이다. 즉, 머리 좀 싸매야 한다는 것.

위의 예제에 Dynamic Programming를 적용하면, COST(i,j) 이전에 COST(i-1,j)와 COST(i,j-1)를 계산하는 것이다. 이 예제에서는 계산 순서가 쉽게 보이지만, 모든 문제가 이처럼 쉽지는 않다. 마지막으로 이러한 순서를 만족하는 프로그램을 제공하는 것으로 기사를 마치도록 하자.


public static void main(String[] args) {
        // Read input data
        read();
       
        // Compute
        for ( int i=0 ; i < 5 ; i++ ) {
            for ( int j=0 ; j < 6 ; j++ ) {
                //costs[i][j] = getMemoizedCost(i,j);
                if ( i == 0 && j == 0 ) {
                    rooms[i][j] = rooms[i][j];
                } else if ( i == 0 ) {
                    rooms[i][j] = rooms[i][j] + rooms[i][j-1];
                } else if ( j == 0 ) {
                    rooms[i][j] = rooms[i][j] + rooms[i-1][j];
                } else {
                    if ( rooms[i-1][j] > rooms[i][j-1] ) {
                        rooms[i][j] = rooms[i][j] + rooms[i-1][j];
                    } else {
                        rooms[i][j] = rooms[i][j] + rooms[i][j-1];
                    }
                }
            }
        }
       
        printMatrix(rooms);
    }


* 예제에 사용한 문제의 경우, 가능한 길의 갯수는 126개이다. 이것도 Dynamic Programming를 이용하여 쉽게 구할 수 있다. 만약 성공한다면, 여러분은 Dynamic Programming을 이해하고 사용한 것이라 할 수 있다.

CNF/DNF

CNF/DNF

정규화 방법을 SQL문 단위로 설정할 수 있게 하는 힌트이다.

  • CNF: Conjunctive normal form으로 정규화

  • DNF: Disjunctive normal form으로 정규화

WHERE절에서 AND, OR와 함께 술어들을 사용하고 AND와 OR의 묶음으로만 정규화되어 있지 않은 경우 두 가지 정규화 방법이 있다. OR들의 술어 묶음을 AND로 묶는 것을 CNF로 정규화한다고 하고, AND들의 술어 묶음을 OR로 묶는 것을 DNF로 정규화한다고 한다.

예를 들어 WHERE I1 = 1 AND ( I3 < 1 OR ( I3 = 3 AND I2 = 1 ) )의 WHERE 절은 다음과 같이 두 가지 형태로 정규화될 수 있다.

  1. CNF로 정규화 한 경우

    ( I1 = 1 ) AND ( I3 < 1 OR I3 = 3 ) AND ( I3 < 1 OR I2 = 1 )

  2. DNF로 정규화 한 경우

    ( I1 = 1 AND I3 < 1 ) OR ( I1 = 1 AND I3 = 3 AND I2 = 1 )

알티베이스 최적화 과정에서 대부분 정규화는 CNF 방식을 취하며 경우에 따라 CNF 변환 비용이 매우 크다거나, DNF로 변환하는 것이 인덱스 스캔에 더 유리하여 접근하는 레코드 수가 현저하게 감소하는 경우 DNF로 변환한다.

AND, OR를 매우 복잡하게 사용한 질의의 경우 CNF, DNF로 고려해 유리한 방법으로 정규화하도록 힌트를 주면 좋다.

예제

정규화 방법과 각각의 정규화에 따른 실행 계획이 어떻게 생성되는지 예를 들어 설명한다.

테이블 T1에 IDX1 (I1, I2, I3)의 인덱스가 존재한다고 가정한다.




2010년 3월 2일 화요일

다른 session 에 10046 trace 걸기

1. 해당 Session ID 와 Serial Number 검색
SELECT  
         s.status "Status", s.serial# "Serial#",
         s.username "DB User", s.osuser "Client User",
        p.program "O.S. Program",
s.sid "session ID"
    FROM v$session s, v$process p, sys.v_$sess_io si
   WHERE s.paddr = p.addr(+)
     AND si.sid(+) = s.sid
     AND (s.username IS NOT NULL)
     AND (NVL (s.osuser, 'x') <> 'SYSTEM')
     AND (s.TYPE <> 'BACKGROUND')
ORDER BY 1;


Status    Serial# DB User   Client User  O.S. Program   session ID
-------- ---------- ------------------------------ ------------------------------ ------------------------------------------------ ----------
ACTIVE  3 SYS   oracle10  oracle@ljh8324-skku (TNS V1-V3)  159

-- 특정 user의 세션정보 확인 --

select sid , serial# from V$session where username='V50';
 

2. 해당 세션에 Trace 를 건다. 

begin
dbms_monitor.session_trace_enable (
session_id => 159
,serial_num => 3
  ,waits => TRUE
,binds => TRUE );
end;
/

3. 수행이 끝나면 Trace 를 해제한다.

begin
dbms_monitor.session_trace_disable (
session_id => 159
,serial_num => 3
);
end;
/

4. tkprof 를 이용하여 변환.

cd $ORACLE_HOME/../admin/orcl10/udump/
tkprof xxx.trc report.prf sys=no

vi report.prf


2010년 3월 1일 월요일

union, difference, intersection of sets, bags

SQL> select * from t1;

 C1    C2
----------
  1     2
  3     4
  1     2
  1     2

SQL> select * from t2;

 C1    C2
----------
  1     2
  3     4
  3     4

t
appears n times in T1 and m times T2


1. bag union T1 T2  = ( Oracle union all )
- tuple t appears n + m times

2. bag intersection T1 
∩ T2 ( Oracle not support ?)
- t appears min(n,m) times

3. bag difference T1 - T2 ( 교환법칙 성립 안됨  T1 - T2  not equal T2 - T1 ) ( Oracle not support ?)
- t appears max(0,n-m) times

4. set union T1 T2  ( Oracle union )
- tuple t appears n + m times and Duplicate Elimination  

5. set difference T1 -T2  ( Oracle minus )
 - t appears max(0,n-m) times and key counting and Elimination  

6. set intersection T1 T2 ( Oracle intersect )
- t appears min(n,m) times and Duplicate Elimination


Oracle 수행결과

SQL> select * from t1 union all select * from t2;

 C1    C2
---------- ----------
  1     2
  3     4
  1     2
  1     2
  1     2
  3     4
  3     4


SQL> select * from t1 union select * from t2;

 C1    C2
---------- ----------
  1     2
  3     4


SQL> select * from t1 intersect select * from t2;

 C1    C2
---------- ----------
  1     2
  3     4


select * from t2 intersect select * from t1;

 C1    C2
---------- ----------
  1     2
  3     4


select * from t2 minus select * from t1;

no rows selected

select * from t1 minus select * from t2;

no rows selected