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
JOB2008. 12. 25. 23:39
Apache 설치
     
** 체크사항 **
Apache 2.x 수정
server/mpm/prefork/prefork.c 파일에서
#define DEFAULT_SERVER_LIMIT 256 부분을 찾아서
#define DEFAULT_SERVER_LIMIT 1280 으로 수정

server/mpm/worker/worker.c 파일에서
#define DEFAULT_SERVER_LIMIT 16 부분을 찾아서
#define DEFAULT_SERVER_LIMIT 20 으로 수정

Apache 1.3.x 수정 
src/include/httpd.h
#ifndef HARD_SERVER_LIMIT
#ifdef WIN32
#define HARD_SERVER_LIMIT 1024
#elif defined(NETWARE)
#define HARD_SERVER_LIMIT 2048
#else
#define HARD_SERVER_LIMIT 1256   # 256을 1256 으로 수정



설치 Version : httpd-2.2.11.tar.gz

prompt >./configure --prefix=/usr/local/apache2 \
--enable-rewrite \
--enable-so \
--enable-mods-shared=most \
--with-mpm=worker

prompt>make

prompt>make install


PHP 설치
** 필요 라이브러리 설치 **

* FREETYPE 설치 (freetype-2.3.6.tar.gz)
    http://freetype.sourceforge.net/download.html
   prompt>tar xvf freetype-2.3.6.tar.gz
   prompt>./configure
   prompt>make
   prompt>make install

* ZLIB 설치 (zlib-1.2.3.tar.gz)
   http://www.zlib.net/
   prompt>tar xvf zlib-1.2.3.tar.gz
   prompt>./configure
   prompt>make
   prompt>make install

* JPEG 설치 (jpegsrc.v6b.tar.gz)
   ftp://ftp.uu.net/graphics/jpeg/
   prompt>tar xvf jpegsrc.v6b.tar.gz
   prompt>./configure --enable-shared --enable-static
   prompt>make
   prompt>mkdir /usr/local/man
   prompt>mkdir /usr/local/man/man1
   prompt>make install

* GIF 설치 (libungif-4.1.0.tar.gz)
   ftp://sunsite.unc.edu/pub/Linux/libs/graphics/
   prompt>tar xvf libungif-4.1.0.tar.gz
   prompt>./configure
   prompt>make
   prompt>make install

* LIBPNG 설치 (libpng-1.2.34.tar.gz)
   http://www.libpng.org/pub/png/libpng.html
  prompt>tar xvf libpng-1.2.34.tar.gz
  prompt>cd libpng-1.2.34
  prompt>./configure
  prompt>make
  prompt>make install

* LIBICONV 설치 (libiconv-1.12.tar.gz)
  http://www.gnu.org/software/libiconv/
  prompt>cd libiconv-1.12.tar.gz
  prompt>./configure
  prompt>make 
  prompt>make install

* GD 설치 (gd-2.0.35.tar.gz)
  http://www.libgd.org/Downloads
  prompt>tar xvf gd-2.0.35.tar.gz
  prompt>cd gd-2.0.35
  prompt>./configure
  prompt>make
  prompt>make install

* LIBXML2 설치 (libxml2-2.6.30.tar.gz)
   ftp://ftp.gnome.org/pub/GNOME/sources/libxml2/2.6/
  prompt>tar xvf libxml2-2.6.30.tar.gz
  prompt>./configure
  prompt>make
  prompt>make install


prompt>./configure \
--prefix=/usr/local/php5 \
--with-apxs2=/usr/local/apache2/bin/apxs \
--with-mysql=/usr/local/mysql \
--with-gd \
--with-iconv=/usr/local/bin/iconv \
--disable-debug \
--with-libxml-dir=/usr/local/ \
--disable-ipv6

prompt>make
prompt>make install

       httpd.conf 수정
      
LoadModule php5_module        modules/libphp5.so

      
AddType application/x-httpd-php .php .phtml .html .inc
         AddType application/x-httpd-php-source .phps

Mysql 설치


설치 Version : mysql-5.1.30.tar.gz

계정생성
prompt>useradd mysql

prompt>./configure --prefix=/usr/local/mysql \
--with-mysqld-user=mysql

prompt>make

prompt>make install

초기설정 및 권한변경

/usr/local/mysql 디렉토리 소유자 변경
prompt>chown -R mysql.mysql /usr/local/mysql
prompt>cd /usr/local/mysql/bin
prompt>./mysql_install_db --user=mysql


시작
prompt>./mysqld_safe --user=mysql &

종료
prompt>./mysqladmin -u root -p shutdown

PATH변수 추가하기
prompt>
vi .bash_profile

PATH=$PATH:$HOME/bin ==> PATH=$PATH:$HOME/bin:/usr/local/mysql/bin 추가




FreeTDS 설치 (MSSQL,SYBASE...연결시)

/usr/local/src# wget http://ibiblio.org/pub/Linux/ALPHA/freetds/stable/freetds-stable.tgz

/usr/local/src# tar xvfpz freetds-stable.tgz

/usr/local/src/freetds# ./configure
--prefix=/usr/local/freetds
--with-tdsver=8.0
--disable-odbc
--disable-debug
--enable-dbmfix
--enable-msdblib

/usr/local/src/freetds# make
/usr/local/src/freetds# make install
configure 단계에서 'Directory /usr/local/freetds is not a FreeTDS installation directory' 라는 메시지가 나올경우
# cp /usr/local/src/freetds/include/tds.h /usr/local/freetds/include/
또는
# cp /usr/local/src/freetds/src/tds/.libs/tds.h /usr/local/freetds/include/
tds.h 파일을 설치된 디렉토리에 복사한다.


freetds.conf 파일에 client charset=EUC-KR을 추가하여 한글깨짐 현상을 막을 수 있습니다..

[global]
client charset = EUC-KR
PHP 설치시 추가
--with-mssql=/usr/local/freetds
--with-sybase=/usr/local/freetds

'JOB' 카테고리의 다른 글

  (0) 2012.12.10
Posted by webfeel
2008. 6. 23. 11:06

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

JOB/PHP2008. 6. 20. 15:30

$date_1 = "2008-05-29"; //시작일
$date_2 = "2008-06-10"; //종료일

$d1_data = explode("-",$date_1);
$d2_data = explode("-",$date_2);

$data1_str = " " . "$d1_data[1]" . "/" . "$d1_data[2]" . "/" . "$d1_data[0]";
$data2_str = " " . "$d2_data[1]" . "/" . "$d2_data[2]" . "/" . "$d2_data[0]";

$d1_strtotime = strtotime($data1_str);
$d2_strtotime = strtotime($data2_str);



while($d1_strtotime != $d2_strtotime){
                                    
$d1_strtotime = strtotime("+1 day",$d1_strtotime);
                                        
print $sing=date("Y-m-d",$d1_strtotime) . "<br>";

}

//RESULT
2008-05-30
2008-05-31
2008-06-01
2008-06-02
2008-06-03
2008-06-04
2008-06-05
2008-06-06
2008-06-07
2008-06-08
2008-06-09
2008-06-10


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

$date_1 = "2008-05-29";
$date_2 = "2008-06-02";

$d1_data = explode("-",$date_1);
$d2_data = explode("-",$date_2);

$data1_str = " " . "$d1_data[1]" . "/" . "$d1_data[2]" . "/" . "$d1_data[0]";
$data2_str = " " . "$d2_data[1]" . "/" . "$d2_data[2]" . "/" . "$d2_data[0]";

$d1_strtotime = strtotime($data1_str);
$d2_strtotime = strtotime($data2_str);

$d1_strtotime = strtotime("-1 day",$d1_strtotime);

 

while($d1_strtotime != $d2_strtotime){
                                    
 $d1_strtotime = strtotime("+1 day",$d1_strtotime);
                                        
echo $sing=date("Y-m-d",$d1_strtotime) . "<br>";


}
//RESULT
2008-05-29
2008-05-30
2008-05-31
2008-06-01
2008-06-02

Posted by webfeel