JOB/DBMS2010. 6. 30. 10:37

DOWNLOAD :http://www.oracle.com/technology/software/tech/oci/instantclient/htdocs/winsoft.html

 

1. instantclient-basic  , instantclient-SQL*Plus , SDK 등 다운로드

 

2. 적당한 위에 압축 풀기

  C:\instantclient_10_2

 

3. tnsnames.ora 생성

  C:\instantclient_10_2\network\ 에 생성.

 

ORADB =
  (DESCRIPTION =
    (ADDRESS_LIST =
      (ADDRESS = (PROTOCOL = TCP)(HOST = 211.42.87.150)(PORT = 1521))
    )
    (CONNECT_DATA =
      (SERVICE_NAME = NONGUP)
    )
  )

 

4. 환경변수 설정

* PATH  : C:\instantclient_10_2 추가.

* ORACLE_HOME : C:\instantclient_10_2 신규 추가.

* TNS_ADMIN : C:\instantclient_10_2\network 신규추가.

 

5. 레지스트리 추가

   orainstan_reg

  

   KOREAN_KOREA.KO16KSC5601

 

   * 서버쪽 NLS_LANG 과 같게 셋팅.

Posted by webfeel
JOB/DBMS2010. 3. 19. 17:49

웹 개발자들을 대상으로 교육을 하다보면, 게시판에 대한 빠른 검색방법에 대하여 많은 질문을 받습니다. 아마도 웹 개발자들이 처음으로 구축하게 되는 예제가 게시판이어서 그런것 같습니다.

먼저, 게시판의 종류는 답글(질의응답)과 댓글(리플) 없는 자유게시판에서부터 답글이 있는 계층형(질의응답) 게시판, 댓글이 포함되는 게시판, 요즘에는 댓글도 계층형으로 구성되는 게시판도 있습니다.

여기서는 가장 간단한 자유 게시판을 예로 들어, 쿼리와 인덱스 작성에 대하여 알아보겠습니다.

실습을 위해 테이블을 다음과 같이 생성하였습니다.

create table freeboard
(no number,
subject varchar2(100),
content varchar2(4000));

게시판의 일련 번호 부여를 위해 시퀀스를 작성했습니다.

create sequence freeboard_no_seq
start with 1
increment by 1;

게시판의 성능 테스트를 위해서 10만건의 게시물을 입력하였습니다.

begin
  for i in 1..100000 loop
      insert into freeboard values(freeboard_no_seq.nextval, i||'번째 게시물 제목', lpad('*', 500, '*'));
      commit;
  end loop;
end;
/

쿼리의 성능 측정을 위해 AUTOTRACE를 사용하기로 하였습니다. 먼저, plustrace 롤을 만들어 사용자에게 부여하고, 실행계획이 저장될 plan_table을 생성하였습니다.

connect / as sysdba
@C:\oracle\product\10.2.0\db_1\sqlplus\admin\plustrce.sql
grant plustrace to scott;

connect scott/tiger
@C:\oracle\product\10.2.0\db_1\rdbms\admin\utlxplan.sql

set autotrace traceonly

<인덱스가 없는 상태에서 5만번째 게시물을 검색해봅니다.>

select no, subject, content from freeboard
where no=50000;

주의 깊게 보아야 할 내용은 다음과 같습니다.

Execution Plan

-------------------------------------------------------------------------------
| Id  | Operation         | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |           |     1 |   528 |  1708   (1)| 00:00:21 |
|*  1 |  TABLE ACCESS FULL| FREEBOARD |     1 |   528 |  1708   (1)| 00:00:21 |
-------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
        165  recursive calls
          0  db block gets
       7769  consistent gets
          0  physical reads
          0  redo size
       1058  bytes sent via SQL*Net to client
        385  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          4  sorts (memory)
          0  sorts (disk)
          1  rows processed

=> no 컬럼에 인덱스가 존재하지 않기 때문에 오라클의 옵티마이저는 Full Table Scan 방식을 선택하였으며, 읽어온 블럭의 갯수는 7769(=7769+0)개 입니다.

<이번엔 페이지당 표시되는 게시물을 20개로 가정하고, 게시판의 첫번째 페이지(글번호, 제목)를 검색해봅니다.>

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select no, subject
            from freeboard
            order by no desc) t1
      where rownum <= 1*20) t2
where rn >= (1-1)*20+1;

=> 서브 쿼리를 사용한 이유는 게시물이 삭제되면, 글번호가 연속적이지 않기 때문입니다.

Execution Plan
--------------------------------------------------------------------------------------
| Id  | Operation                | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |           |    20 |  1560 |  1707   (1)| 00:00:21 |
|*  1 |  VIEW                    |           |    20 |  1560 |  1707   (1)| 00:00:21 |
|*  2 |   COUNT STOPKEY          |           |       |       |            |          |
|   3 |    VIEW                  |           |   100K|  6347K|  1707   (1)| 00:00:21 |
|*  4 |     SORT ORDER BY STOPKEY|           |   100K|  2636K|  1707   (1)| 00:00:21 |
|   5 |      TABLE ACCESS FULL   | FREEBOARD |   100K|  2636K|  1707   (1)| 00:00:21 |
--------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
       7750  consistent gets
          0  physical reads
          0  redo size
       1251  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> Full Table Scan 작업 후, 정렬 작업이 수행되었으며, 7750(=7750+0)개 블럭을 읽었습니다.

<마지막 페이지(5000번째) 검색>

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select no, subject
            from freeboard
            order by no desc) t1
      where rownum <= 5000*20) t2
where rn >= (5000-1)*20+1;

Execution Plan
----------------------------------------------------------------------------------------------
| Id  | Operation                | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |           |   100K|  7617K|       |  2479   (2)| 00:00:30 |
|*  1 |  VIEW                    |           |   100K|  7617K|       |  2479   (2)| 00:00:30 |
|*  2 |   COUNT STOPKEY          |           |       |       |       |            |          |
|   3 |    VIEW                  |           |   100K|  6347K|       |  2479   (2)| 00:00:30 |
|*  4 |     SORT ORDER BY STOPKEY|           |   100K|  2636K|  7096K|  2479   (2)| 00:00:30 |
|   5 |      TABLE ACCESS FULL   | FREEBOARD |   100K|  2636K|       |  1707   (1)| 00:00:21 |
----------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
       7750  consistent gets
          0  physical reads
          0  redo size
       1143  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> 마찬가지로 Full Table Scan 작업 후, 정렬 작업이 수행되었으며, 7750(=7750+0)개 블럭을 읽었습니다.

게시판의 게시물은 일련 번호에 따라 내림차순으로 출력되어야 하므로 내림차순 인덱스를 구성합니다.

create unique index freeboard_no_idx on freeboard(no desc);

일련 번호에 대하여 기본키 제약조건을 설정합니다.

alter table freeboard
add constraint freeboard_no_pk primary key(no);

테이블 통계와 인덱스 통계를 수집합니다.

exec dbms_stats.gather_table_stats('scott', 'freeboard');

exec dbms_stats.gather_index_stats('scott', 'freeboard_no_idx');

<인덱스가 있는 상태에서 5만번째 게시물을 검색해봅니다.>

select no, subject, content from freeboard
where no=50000;

Execution Plan

-----------------------------------------------------------------------------------------------
| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                 |     1 |   528 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| FREEBOARD       |     1 |   528 |     2   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | FREEBOARD_NO_PK |     1 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
          3  consistent gets
          0  physical reads
          0  redo size
        969  bytes sent via SQL*Net to client
        374  bytes received via SQL*Net from client
          1  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

=> 인덱스를 이용하여 검색하기 때문에 3(=3+0)개 블럭을 읽었습니다.

<페이지당 표시되는 게시물을 20개로 가정하고, 첫번째 페이지(글번호, 제목) 검색>

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select no, subject
            from freeboard
            order by no desc) t1
      where rownum <= 1*20) t2
where rn >= (1-1)*20+1;

Execution Plan
--------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |                 |    20 |  1560 |     4   (0)| 00:00:01 |
|*  1 |  VIEW                          |                 |    20 |  1560 |     4   (0)| 00:00:01 |
|*  2 |   COUNT STOPKEY                |                 |       |       |            |          |
|   3 |    VIEW                        |                 |    20 |  1300 |     4   (0)| 00:00:01 |
|   4 |     TABLE ACCESS BY INDEX ROWID| FREEBOARD       |   100K|  2636K|     4   (0)| 00:00:01 |
|   5 |      INDEX FULL SCAN DESCENDING| FREEBOARD_NO_PK |    20 |       |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
          9  consistent gets
          8  physical reads
          0  redo size
       1251  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> 인덱스를 내림차순으로 구성하였기 때문에 정렬작업이 수행되지 않고 17(=9+8)개 블럭을 읽었습니다.

<마지막 페이지(5000번째) 검색>

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select no, subject
            from freeboard
            order by no desc) t1
      where rownum <= 5000*20) t2
where rn >= (5000-1)*20+1;

Execution Plan
----------------------------------------------------------------------------------------------
| Id  | Operation                | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |           |   100K|  7617K|       |  2479   (2)| 00:00:30 |
|*  1 |  VIEW                    |           |   100K|  7617K|       |  2479   (2)| 00:00:30 |
|*  2 |   COUNT STOPKEY          |           |       |       |       |            |          |
|   3 |    VIEW                  |           |   100K|  6347K|       |  2479   (2)| 00:00:30 |
|*  4 |     SORT ORDER BY STOPKEY|           |   100K|  2636K|  7096K|  2479   (2)| 00:00:30 |
|   5 |      TABLE ACCESS FULL   | FREEBOARD |   100K|  2636K|       |  1707   (1)| 00:00:21 |
----------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
       7750  consistent gets
          0  physical reads
          0  redo size
       1143  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> 인덱스가 있지만, 오라클의 옵티마이저는 테이블 전체를 읽고 정렬을 수행하는 방법을 선택했습니다. 총 7750(=7750+0)개 블럭을 읽었습니다.

그렇다면, Hint를 주어 정렬 작업을 수행하지 않고 인덱스를 사용한 경우, 쿼리의 성능을 살펴보겠습니다.

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select /*+ index(freeboard freeboard_no_idx) */ no, subject
            from freeboard) t1
      where rownum <= 5000*20) t2
where rn >= (5000-1)*20+1;

Execution Plan
--------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                  |   100K|  7617K|  7932   (1)| 00:01:36 |
|*  1 |  VIEW                         |                  |   100K|  7617K|  7932   (1)| 00:01:36 |
|*  2 |   COUNT STOPKEY               |                  |       |       |            |          |
|   3 |    TABLE ACCESS BY INDEX ROWID| FREEBOARD        |   100K|  2636K|  7932   (1)| 00:01:36 |
|   4 |     INDEX FULL SCAN           | FREEBOARD_NO_IDX |   100K|       |   227   (2)| 00:00:03 |
--------------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       7920  consistent gets
          0  physical reads
          0  redo size
       1143  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> 오라클 옵티마이저의 선택이 옳았습니다. 오히려, 더 많은 블럭(7920개)을 읽는군요.

어쨌든, 게시판에서 게시물을 글번호에 대한 Equal 조건으로 검색하는 경우, 오름차순이나 내림차순 인덱스와 관련없이
인덱스만 존재하면 검색속도는 충분히 보장 받을 수 있습니다. 그러나, 게시판을 최근 페이지 단위로 검색해야 하는 경우에는 인덱스를 내림차순으로 구성하여 정렬 작업을 피하는 편이 유리합니다. 물론, 오라클의 버전에 따라 오름차순 인덱스를 구성해도 옵티마이저가 인덱스를 역순으로 사용하여 정렬 작업을 회피하기도 하기 때문에 항상 실행계획을 확인하는 습관을 들이는것이 좋을 것 같습니다.

 

 

 

 

 

 

 

====================================================================

 

먼저, 내림차순으로 구성되어 있는 인덱스를 이용하여 5000번 페이지의 첫번째 글번호를 읽고,

마찬가지로 인덱스를 이용하여 20건을 읽어 보았습니다.

select /*+ index(freeboard freeboard_no_idx) */ no, subject
from freeboard
where no<=(select /*+ index(freeboard freeboard_no_idx) */ min(no)

                  from freeboard
                  where rownum <= (5000-1)*20+1)
and rownum<=20;

-------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                  |    20 |   540 |   456   (2)| 00:00:06 |
|*  1 |  COUNT STOPKEY               |                  |       |       |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID| FREEBOARD        |    20 |   540 |     3   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN          | FREEBOARD_NO_IDX |    45 |       |     2   (0)| 00:00:01 |
|   4 |     SORT AGGREGATE           |                  |     1 |     5 |            |          |
|*  5 |      COUNT STOPKEY           |                  |       |       |            |          |
|   6 |       INDEX FULL SCAN        | FREEBOARD_NO_IDX |   100K|   488K|   227   (2)| 00:00:03 |
-------------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
        231  consistent gets
          0  physical reads
          0  redo size
       1143  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         20  rows processed

결과에서 보면 가장 마지막 페이지를 읽기 위해 231개의 블럭을 읽게 되었군요.

좋은 아이디어 입니다.

기환씨 열심히 공부하고 계시죠. 파이팅입니다. 이상입니다.

Posted by webfeel
JOB/DBMS2010. 3. 19. 17:44

계층형 게시판을 위한 두번째 알고리즘에 대해서 알아봅니다.

첫번째 계층형 게시판에서는 같은 레벨에 답글을 다는 경우, 가장 최근 답글이 동일한 레벨에서 가장 위에 표시되었습니다.
이번에는 가장 최근 답글이 동일한 레벨에서 마지막에 표시되도록 합니다.

설명을 위해 테이블에는 다음과 같은 컬럼들만 있다고 가정합니다. 실제로는 더 많겠죠.

No : 글번호(Primary Key)
Title : 글제목
Grp : 같은 주제를 갖는 게시물의 고유번호. 부모글과 부모글로부터 파생된 모든 자식글은 같은 번호를 갖습니다.
Depth : 같은 그룹내 게시물의 순서

게시판에 첫번째 글이 올라오면, 테이블에는 다음과 같은 정보가 저장됩니다.
=================================
No, Title            , Grp, Depth
=================================
1, '안녕하세요'     ,   1, 'A'
=================================
새 글에서 No는 시퀀스로부터 받아온 값, Grp는 No와 동일한 값, Depth는 'A'입니다.

두번째 글과 세번째 글이 올라옵니다.
=================================
No, Title            , Grp, Depth
=================================
3, '모임이 있습니다',   3, 'A'
2, '날씨가 맑습니다',   2, 'A'
1, '안녕하세요     ',   1, 'A'
=================================

첫번째 글에 답글이 올라옵니다.
=================================
No, Title            , Grp, Depth
=================================
3, '모임이 있습니다',   3, 'A'
2, '날씨가 맑습니다',   2, 'A'
1, '안녕하세요     ',   1, 'A'
4, '  반가워요     ',   1, 'AA' 
=================================
답글의 경우, Grp는 부모글의 Grp 값, Depth는 부모글의 Depth에 'A'를 추가합니다.
첫번째 글의 답글에 답글이 올라옵니다. 즉, 네번째 글의 답글이 올라옵니다.
=================================
No, Title            , Grp, Depth
=================================
3, '모임이 있습니다',   3, 'A'
2, '날씨가 맑습니다',   2, 'A'
1, '안녕하세요     ',   1, 'A'
4, '  반가워요     ',   1, 'AA' 
5, '    감사합니다 ',   1, 'AAA'
=================================

첫번째 글의 두번째 답글이 올라옵니다.
=================================
No, Title            , Grp, Depth
=================================
3, '모임이 있습니다',   3, 'A'
2, '날씨가 맑습니다',   2, 'A'
1, '안녕하세요     ',   1, 'A'
4, '  반가워요     ',   1, 'AA' 
5, '    감사합니다 ',   1, 'AAA'
6, '  환영합니다   ',   1, 'AB' 
=================================

여기서, 두번째 답글이 첫번째 답글의 아래에 표시되려면, 동일한 그룹 번호에 있으면서, 부모의 Depth 문자열로 시작되고,
문자열의 길이가 부모의 Depth 문자열보다 하나가 큰 문자열 중 최대값을 찾아 Depth로 사용합니다.

여섯번째 게시물에 답글이 올라옵니다.
=================================
No, Title            , Grp, Depth
=================================
3, '모임이 있습니다',   3, 'A'
2, '날씨가 맑습니다',   2, 'A'
1, '안녕하세요     ',   1, 'A'
4, '  반가워요     ',   1, 'AA' 
5, '    감사합니다 ',   1, 'AAA'
6, '  환영합니다   ',   1, 'AB' 
7, '    감사합니다 ',   1, 'ABA'
=================================

네번째 게시물에 두번째 답글이 올라옵니다.
=================================
No, Title            , Grp, Depth
=================================
3, '모임이 있습니다',   3, 'A'
2, '날씨가 맑습니다',   2, 'A'
1, '안녕하세요     ',   1, 'A'
4, '  반가워요     ',   1, 'AA' 
5, '    감사합니다 ',   1, 'AAA'
8, '    저도 방가  ',   1, 'AAB'
6, '  환영합니다   ',   1, 'AB' 
7, '    감사합니다 ',   1, 'ABA'
=================================

검색 할 때는 Grp를 내림차순으로 Depth를 오름차 순으로 정렬하면 됩니다.
인덱스는 기본키와 Grp+Depth의 복합 인덱스를 설정하면 됩니다.

실제 오라클에서 테이블을 구성하고 검색해봅니다.

drop table qnaboard;

create table qnaboard
(no number,
title varchar2(100),
contents varchar2(4000),
writer varchar2(20),
wdate date,
grp number,
depth varchar2(1000));

drop sequence qnaboard_no_seq;

create sequence qnaboard_no_seq
start with 1
increment by 1;

create index qnaboard_no_idx on qnaboard(no) reverse;

alter table qnaboard
add constraint qnaboard_no_pk primary key (no);

문자열 처리를 위한 도우미 함수입니다. 문자열을 입력하면 마지막 문자를 다음 문자로 교체합니다.
예를 들어, 'AAABB'를 입력하면, 'AAABC'를 리턴합니다.

create or replace function next_depth(str varchar2)
return varchar2
as
v_str1 varchar2(32767);
v_str2 varchar2(32767);
begin
v_str1 := substr(str, 1, length(str)-1);
        v_str2 := substr(str, -1, 1);
return v_str1||chr(ascii(v_str2)+1);
end;
/

create index qnaboard_grp_seq on qnaboard(grp desc, depth asc);

alter session set nls_date_format='yyyy-mm-dd hh24:mi:ss';

첫번째 게시물 입력 - 새 글 입력
insert into qnaboard
values(qnaboard_no_seq.nextval, '안녕하세요', null, '길동', sysdate, qnaboard_no_seq.currval, 'A');

두번째 게시물 입력 - 새 글 입력
insert into qnaboard
values(qnaboard_no_seq.nextval, '날씨가 맑습니다', null, '철수', sysdate, qnaboard_no_seq.currval, 'A');

세번째 게시물 입력 - 새 글 입력
insert into qnaboard
values(qnaboard_no_seq.nextval, '모임이 있습니다', null, '영희', sysdate, qnaboard_no_seq.currval, 'A');

네번째 게시물 입력 - 첫번째 게시물의 답글
답글이 올라오는 경우에는 답글에 부여 할 depth를 결정해야 합니다.
이를 위해서, 부모글의 depth인 'A'로 다음과 같은 쿼리를 실행합니다.
select next_depth(max(depth))
from qnaboard
where grp=1
and depth like 'A_';

위의 쿼리가 null이면 답글에 부여 할 depth는 부모글의 depth에 'A'를 붙인 문자열이 됩니다.
insert into qnaboard
values(qnaboard_no_seq.nextval, '반가워요', null, '만수', sysdate, 1, 'AA');

다섯번째 게시물 입력 - 네번째 게시물의 답글
네번째 게시물의 depth는 'AA'입니다.
select next_depth(max(depth))
from qnaboard
where grp=1
and depth like 'AA_';

위의 쿼리가 null이므로 답글에 부여 할 depth는 부모글의 depth에 'A'를 붙인 'AAA'가 됩니다.
insert into qnaboard
values(qnaboard_no_seq.nextval, '감사합니다', null, '길동', sysdate, 1, 'AAA');

여섯번째 게시물 입력 - 첫번째 게시물의 두번째 답글
첫번째 게시물의 depth는 'A'입니다.
select next_depth(max(depth))
from qnaboard
where grp=1
and depth like 'A_';

위의 쿼리 리턴값 'AB'가 답글에 부여 할 depth가 됩니다.
insert into qnaboard
values(qnaboard_no_seq.nextval, '환영합니다', null, '찬호', sysdate, 1, 'AB');

일곱번째 게시물 입력 - 여섯번째 게시물의 답글
여섯번째 게시물의 depth는 'AB'입니다.
select next_depth(max(depth))
from qnaboard
where grp=1
and depth like 'AB_';

위의 쿼리가 null이므로 답글에 부여 할 depth는 부모글의 depth에 'A'를 붙인 'ABA'가 됩니다.
insert into qnaboard
values(qnaboard_no_seq.nextval, '감사합니다', null, '길동', sysdate, 1, 'ABA');

여덟번째 게시물 입력 - 네번째 게시물의 답글
네번째 게시물의 depth는 'AA'입니다.
select next_depth(max(depth))
from qnaboard
where grp=1
and depth like 'AA_';

위의 쿼리 리턴값 'AAB'가 답글에 부여 할 depth가 됩니다.
insert into qnaboard
values(qnaboard_no_seq.nextval, '저도 방가', null, '병헌', sysdate, 1, 'AAB');

게시판을 검색하는 경우, grp로 내림차순, seq로 오름차순으로 정렬하면 됩니다.
select 
case when length(depth)-1=0 then no
            when length(depth)-1>0 then null end no
, rpad('+', length(depth)-1, '-')||title title
, writer
, wdate
from qnaboard
order by grp desc, depth;

        NO TITLE                WRITER               WDATE
---------- -------------------- -------------------- -------------------
         3 모임이 있습니다      영희                 2007-09-15 21:51:26
         2 날씨가 맑습니다      철수                 2007-09-15 21:51:03
         1 안녕하세요           길동                 2007-09-15 21:48:23
           +반가워요            만수                 2007-09-15 21:53:41
           +-감사합니다         길동                 2007-09-15 21:55:57
           +-저도 방가          병헌                 2007-09-15 22:03:27
           +환영합니다          찬호                 2007-09-15 21:57:55
           +-감사합니다         길동                 2007-09-15 21:59:49

Posted by webfeel
JOB/DBMS2010. 3. 19. 16:15

웹 개발자들을 대상으로 교육을 하다보면, 게시판에 대한 빠른 검색방법에 대하여 많은 질문을 받습니다. 아마도 웹 개발자들이 처음으로 구축하게 되는 예제가 게시판이어서 그런것 같습니다.

먼저, 게시판의 종류는 답글(질의응답)과 댓글(리플) 없는 자유게시판에서부터 답글이 있는 계층형(질의응답) 게시판, 댓글이 포함되는 게시판, 요즘에는 댓글도 계층형으로 구성되는 게시판도 있습니다.

여기서는 가장 간단한 자유 게시판을 예로 들어, 쿼리와 인덱스 작성에 대하여 알아보겠습니다.

실습을 위해 테이블을 다음과 같이 생성하였습니다.

create table freeboard
(no number,
subject varchar2(100),
content varchar2(4000));

게시판의 일련 번호 부여를 위해 시퀀스를 작성했습니다.

create sequence freeboard_no_seq
start with 1
increment by 1;

게시판의 성능 테스트를 위해서 10만건의 게시물을 입력하였습니다.

begin
  for i in 1..100000 loop
      insert into freeboard values(freeboard_no_seq.nextval, i||'번째 게시물 제목', lpad('*', 500, '*'));
      commit;
  end loop;
end;
/

쿼리의 성능 측정을 위해 AUTOTRACE를 사용하기로 하였습니다. 먼저, plustrace 롤을 만들어 사용자에게 부여하고, 실행계획이 저장될 plan_table을 생성하였습니다.

connect / as sysdba
@C:\oracle\product\10.2.0\db_1\sqlplus\admin\plustrce.sql
grant plustrace to scott;

connect scott/tiger
@C:\oracle\product\10.2.0\db_1\rdbms\admin\utlxplan.sql

set autotrace traceonly

<인덱스가 없는 상태에서 5만번째 게시물을 검색해봅니다.>

select no, subject, content from freeboard
where no=50000;

주의 깊게 보아야 할 내용은 다음과 같습니다.

Execution Plan

-------------------------------------------------------------------------------
| Id  | Operation         | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |           |     1 |   528 |  1708   (1)| 00:00:21 |
|*  1 |  TABLE ACCESS FULL| FREEBOARD |     1 |   528 |  1708   (1)| 00:00:21 |
-------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
        165  recursive calls
          0  db block gets
       7769  consistent gets
          0  physical reads
          0  redo size
       1058  bytes sent via SQL*Net to client
        385  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          4  sorts (memory)
          0  sorts (disk)
          1  rows processed

=> no 컬럼에 인덱스가 존재하지 않기 때문에 오라클의 옵티마이저는 Full Table Scan 방식을 선택하였으며, 읽어온 블럭의 갯수는 7769(=7769+0)개 입니다.

<이번엔 페이지당 표시되는 게시물을 20개로 가정하고, 게시판의 첫번째 페이지(글번호, 제목)를 검색해봅니다.>

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select no, subject
            from freeboard
            order by no desc) t1
      where rownum <= 1*20) t2
where rn >= (1-1)*20+1;

=> 서브 쿼리를 사용한 이유는 게시물이 삭제되면, 글번호가 연속적이지 않기 때문입니다.

Execution Plan
--------------------------------------------------------------------------------------
| Id  | Operation                | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |           |    20 |  1560 |  1707   (1)| 00:00:21 |
|*  1 |  VIEW                    |           |    20 |  1560 |  1707   (1)| 00:00:21 |
|*  2 |   COUNT STOPKEY          |           |       |       |            |          |
|   3 |    VIEW                  |           |   100K|  6347K|  1707   (1)| 00:00:21 |
|*  4 |     SORT ORDER BY STOPKEY|           |   100K|  2636K|  1707   (1)| 00:00:21 |
|   5 |      TABLE ACCESS FULL   | FREEBOARD |   100K|  2636K|  1707   (1)| 00:00:21 |
--------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
       7750  consistent gets
          0  physical reads
          0  redo size
       1251  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> Full Table Scan 작업 후, 정렬 작업이 수행되었으며, 7750(=7750+0)개 블럭을 읽었습니다.

<마지막 페이지(5000번째) 검색>

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select no, subject
            from freeboard
            order by no desc) t1
      where rownum <= 5000*20) t2
where rn >= (5000-1)*20+1;

Execution Plan
----------------------------------------------------------------------------------------------
| Id  | Operation                | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |           |   100K|  7617K|       |  2479   (2)| 00:00:30 |
|*  1 |  VIEW                    |           |   100K|  7617K|       |  2479   (2)| 00:00:30 |
|*  2 |   COUNT STOPKEY          |           |       |       |       |            |          |
|   3 |    VIEW                  |           |   100K|  6347K|       |  2479   (2)| 00:00:30 |
|*  4 |     SORT ORDER BY STOPKEY|           |   100K|  2636K|  7096K|  2479   (2)| 00:00:30 |
|   5 |      TABLE ACCESS FULL   | FREEBOARD |   100K|  2636K|       |  1707   (1)| 00:00:21 |
----------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
       7750  consistent gets
          0  physical reads
          0  redo size
       1143  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> 마찬가지로 Full Table Scan 작업 후, 정렬 작업이 수행되었으며, 7750(=7750+0)개 블럭을 읽었습니다.

게시판의 게시물은 일련 번호에 따라 내림차순으로 출력되어야 하므로 내림차순 인덱스를 구성합니다.

create unique index freeboard_no_idx on freeboard(no desc);

일련 번호에 대하여 기본키 제약조건을 설정합니다.

alter table freeboard
add constraint freeboard_no_pk primary key(no);

테이블 통계와 인덱스 통계를 수집합니다.

exec dbms_stats.gather_table_stats('scott', 'freeboard');

exec dbms_stats.gather_index_stats('scott', 'freeboard_no_idx');

<인덱스가 있는 상태에서 5만번째 게시물을 검색해봅니다.>

select no, subject, content from freeboard
where no=50000;

Execution Plan

-----------------------------------------------------------------------------------------------
| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                 |     1 |   528 |     2   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| FREEBOARD       |     1 |   528 |     2   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | FREEBOARD_NO_PK |     1 |       |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
          3  consistent gets
          0  physical reads
          0  redo size
        969  bytes sent via SQL*Net to client
        374  bytes received via SQL*Net from client
          1  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

=> 인덱스를 이용하여 검색하기 때문에 3(=3+0)개 블럭을 읽었습니다.

<페이지당 표시되는 게시물을 20개로 가정하고, 첫번째 페이지(글번호, 제목) 검색>

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select no, subject
            from freeboard
            order by no desc) t1
      where rownum <= 1*20) t2
where rn >= (1-1)*20+1;

Execution Plan
--------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |                 |    20 |  1560 |     4   (0)| 00:00:01 |
|*  1 |  VIEW                          |                 |    20 |  1560 |     4   (0)| 00:00:01 |
|*  2 |   COUNT STOPKEY                |                 |       |       |            |          |
|   3 |    VIEW                        |                 |    20 |  1300 |     4   (0)| 00:00:01 |
|   4 |     TABLE ACCESS BY INDEX ROWID| FREEBOARD       |   100K|  2636K|     4   (0)| 00:00:01 |
|   5 |      INDEX FULL SCAN DESCENDING| FREEBOARD_NO_PK |    20 |       |     2   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
          9  consistent gets
          8  physical reads
          0  redo size
       1251  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> 인덱스를 내림차순으로 구성하였기 때문에 정렬작업이 수행되지 않고 17(=9+8)개 블럭을 읽었습니다.

<마지막 페이지(5000번째) 검색>

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select no, subject
            from freeboard
            order by no desc) t1
      where rownum <= 5000*20) t2
where rn >= (5000-1)*20+1;

Execution Plan
----------------------------------------------------------------------------------------------
| Id  | Operation                | Name      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |           |   100K|  7617K|       |  2479   (2)| 00:00:30 |
|*  1 |  VIEW                    |           |   100K|  7617K|       |  2479   (2)| 00:00:30 |
|*  2 |   COUNT STOPKEY          |           |       |       |       |            |          |
|   3 |    VIEW                  |           |   100K|  6347K|       |  2479   (2)| 00:00:30 |
|*  4 |     SORT ORDER BY STOPKEY|           |   100K|  2636K|  7096K|  2479   (2)| 00:00:30 |
|   5 |      TABLE ACCESS FULL   | FREEBOARD |   100K|  2636K|       |  1707   (1)| 00:00:21 |
----------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
       7750  consistent gets
          0  physical reads
          0  redo size
       1143  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> 인덱스가 있지만, 오라클의 옵티마이저는 테이블 전체를 읽고 정렬을 수행하는 방법을 선택했습니다. 총 7750(=7750+0)개 블럭을 읽었습니다.

그렇다면, Hint를 주어 정렬 작업을 수행하지 않고 인덱스를 사용한 경우, 쿼리의 성능을 살펴보겠습니다.

select t2.no, t2.subject
from (select rownum rn, t1.no, t1.subject
      from (select /*+ index(freeboard freeboard_no_idx) */ no, subject
            from freeboard) t1
      where rownum <= 5000*20) t2
where rn >= (5000-1)*20+1;

Execution Plan
--------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                  |   100K|  7617K|  7932   (1)| 00:01:36 |
|*  1 |  VIEW                         |                  |   100K|  7617K|  7932   (1)| 00:01:36 |
|*  2 |   COUNT STOPKEY               |                  |       |       |            |          |
|   3 |    TABLE ACCESS BY INDEX ROWID| FREEBOARD        |   100K|  2636K|  7932   (1)| 00:01:36 |
|   4 |     INDEX FULL SCAN           | FREEBOARD_NO_IDX |   100K|       |   227   (2)| 00:00:03 |
--------------------------------------------------------------------------------------------------

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       7920  consistent gets
          0  physical reads
          0  redo size
       1143  bytes sent via SQL*Net to client
        396  bytes received via SQL*Net from client
          3  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         20  rows processed

=> 오라클 옵티마이저의 선택이 옳았습니다. 오히려, 더 많은 블럭(7920개)을 읽는군요.

어쨌든, 게시판에서 게시물을 글번호에 대한 Equal 조건으로 검색하는 경우, 오름차순이나 내림차순 인덱스와 관련없이
인덱스만 존재하면 검색속도는 충분히 보장 받을 수 있습니다. 그러나, 게시판을 최근 페이지 단위로 검색해야 하는 경우에는 인덱스를 내림차순으로 구성하여 정렬 작업을 피하는 편이 유리합니다. 물론, 오라클의 버전에 따라 오름차순 인덱스를 구성해도 옵티마이저가 인덱스를 역순으로 사용하여 정렬 작업을 회피하기도 하기 때문에 항상 실행계획을 확인하는 습관을 들이는것이 좋을 것 같습니다.

Posted by webfeel
JOB/DBMS2010. 3. 19. 15:51

 

 

SELECT a.table_name, b.column_name, a.index_name, a.order
FROM user_indexes a, user_ind_columns b where a.index_name=b.index_name AND
a.table_name in('CONTENT_BOARD');

Posted by webfeel
2008. 6. 23. 11:06

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.