zepinos
21k
2015-06-23 12:18:35
41
58034

페이징(Paging)에 대한 이해 - (2) ROW NUMBER 을 이용한 게시물 가져오기.




흔히 웹 개발을 처음 하는 개발자가 처음 겪게 되는 난관은 바로 게시판의 페이징(Paging)이 아닐까 합니다. 특히 DBMS 와 연동해서 개발하는 경우가 많은데, 각 DBMS 마다 페이징 방법도 많고 성능 등 신경써야 할 부분이 많이 때문에 어떤 자료를 보고 따라해야 문제가 없을지 판단하기 막막할 때가 있습니다.


그래서 페이징에 대한 원리를 파악해서 가장 기본적인 페이징 구현을 할 수 있는 방법을 알려드리고자 합니다.


이 글은 초보자를 위한 글이므로, 페이징 정도는 우습다는 분은 패스해주시길...^^;;;



 

 

이번에는 DBMS 에서 ROW NUMBER(오라클의 rownum, SQL Server 의 ROW_NUMBER(), MySQL 의 변수를 이용한 번호 등)을 이용한 게시물 가져오기에 대해서 설명하겠습니다.


ROW NUMBER 는 쉽게 말해 출력되는 결과물에 순차적인 번호를 달아주는 기능입니다. 필요할 때 언제든지 번호를 달아달라고 시스템에 요청을 하면 사용자에게 보내줄 데이터에 번호를 달아주게 되죠. 문제는 이 번호를 달 때 정렬이 되어 있지 않다면 번호를 엉뚱하게 달 수도 있고, 번호를 단다는 것 자체가 시스템에 부하를 줘서 정말 많은 게시물을 가지고 있는 경우 특정 영역의 게시물들을 가져오는데 그 반응이 느려진다는데 있습니다.


DBMS 는 보통 인덱스(Index) 라는 것을 가지고 있습니다. 그래서 1 억 개 이상의 데이터를 가지고 있더라도 인덱스를 이용하면 원하는 자료를 순식간에 가져올 수 있습니다. 하지만, 게시판에서는 특정 번호를 가지고 데이터를 불러올 수 없습니다. 실시간으로 계속 자료가 쌓이다보면 해당 페이지에 보일 게시물의 내용이 지속적으로 바뀌기 때문입니다. 변동이 좀 덜하거나 하면 해당 페이지에 속한 게시물이 어떤 것인지 따로 기입한 뒤 그 정보를 통해 게시물을 가져오면 속도에 대한 문제가 발생하지 않지만, 추가 뿐만 아니라 삭제의 경우 이런 페이징을 어렵게 만드는 요인입니다. 그래서, 화면에 출력되어야 할 순서대로 정렬을 한 뒤 가상의 번호인 ROW NUMBER 을 앞에 달아줘서 원하는 영역의 자료만 가져올 수 있는 방식을 취합니다.


그런데, 일반적인 게시판은 아주 단순한 특징을 하나 가지고 있습니다. 사람들은 일반적으로 페이지 번호가 3 페이지 이상인 것을 거의 읽지 않는다는 것입니다. 즉, 페이지 번호가 낮은 것을 훨씬 더 많이 조회하고 있고, 그 비율이 제 경험상 5 페이지 이하의 페이지 조회율이 95% 이상이라고 생각하고 있습니다. 그러므로 총 페이지수가 1000 페이지인 많은 글이 있는 게시판이라도 1000 페이지를 읽는데 5 초가 걸리더라도 1 페이지를 읽는데 0.01 초 밖에 걸리지 않는다면 사용자들은 그렇게 느리다고 느끼지 않는다는 것입니다. 1 페이지나 1000 페이지나 모두 2 초씩 동일하게 소비되는 것보다 더 빠르고 좋은 게시판으로 사람들은 생각한다는 것이죠. 그래서 이런 특성을 이용한 게시물 가져오기 방식을 많이 쓰게 됩니다.


 

 

그럼 테스트 데이터 구조를 정의하겠습니다. 쿼리는 오라클로 하겠습니다. 먼저, board 라는 테이블이 존재하고, id, name, content, createdate 라는 필드가 존재한다고 하겠습니다. 여기서 createdate 는 date 형의 필드이고, id 는 sequence 형태의 number 형, name 은 varchar2, content 는 crob 의 형이라고 가정하겠습니다.


일반적으로 게시판을 검색할 때에는 최근에 작성한 순서대로 게시물을 조회합니다. 그럼 다음과 같은 쿼리를 작성하게 될겁니다.



select id, name, content, createdate

from board

order by createdate



이렇게 하면 최신 순서대로 모든 게시물이 다 출력이 되겠죠. 예전처럼 ResultSet 을 직접 받아와서 처리하면 DBMS 에서 커서를 가지고 대기하다가 데이터를 요청할 때 전송하지만, 요즘 많이 쓰는 MyBatis 등은 데이터를 즉시 받아와서 resultType 이나 resultMap 에 정의된 대로 데이터를 bean 에 저장을 해버립니다. 그래서 모두 받아오면 문제가 발생하겠죠.

그래서 원하는 범위의 게시물을 가져와야 합니다.


총 게시물 수는 25 개, 한 페이지에 보여줄 게시물 수는 10 개, 현재 페이지는 3 페이지라고 가정하겠습니다. 그럼 가져와야할 게시물의 범위는? 딱 봐도 21 ~ 25입니다. 하지만, 아주 다행스럽게도 SQL 의 범위 검색은 범위가 벗어나도 있는데까지만 가져오기 때문에 21 ~ 30 사이의 게시물을 가져오라고 시켜도 알아서 21 ~ 25 까지의 게시물만 가져옵니다. 그래서 프로그램에서는 다음과 같이 시작 게시물 번호와 끝 게시물 번호를 계산해서 쿼리를 만들면 됩니다. 특별히 계산식의 해설은 하지 않겠습니다.



int totalCount = 25;

int countPage = 10;


int page = 3;


int startCount = (page - 1) * countPage + 1;  // 21 이 되겠죠

int endCount = page * countPage;  // 30 이 될 겁니다.



이렇게 만들어진 startCount 와 endCount 을 이용해서 DBMS 에서 원하는 게시물을 조회하는쿼리를 만들어보겠습니다. 위에서 언급했듯이 오라클의 쿼리입니다. 오라클은 ROW NUMBER 을 rownum 이라는 것으로 제공해줍니다. 사용의 편의를 위해 별칭(alias, as 을 이용해서 붙임)을 사용합니다.

 

먼저 페이지 번호를 붙여보겠습니다.


select rownum as rnum, id, name, content, createdate

from board

order by createdate


이렇게 해주면...순서대로 출력이 안되는걸 보실 수 있습니다. rownum 은 order by 가 이루어지기 전에 이루어진다는 걸 아실 수 있습니다. 그래서 아래와 같이 번호를 부여해야 합니다. 서브쿼리를 쓰는 거죠.


select rownum as rnum, A.id, A.name, A.content, A.createdate

from (

    select id, name, content, createdate

    from board

    order by createdate) A


이렇게 출력하면 순서대로 출력됩니다. 그럼 우리가 원하는 21 ~ 30 까지의 게시물을 가져오는 쿼리로 확장해보겠습니다.


select rownum as rnum, A.id, A.name, A.content, A.createdate

from (

    select id, name, content, createdate

    from board

    order by createdate) A

where rownum between 21 and 30

-- where rownum >= 21 and rownum <= 30


숫자 범위를 이용해 값을 가져오는 가장 대표적인 방법은 between 정도가 되겠네요. 아주 쉽게 21 ~ 30(실제로는 21 ~ 25) 의 게시물을 가져왔습니다.


 

 끝일까요?


아쉽게도 아닙니다. DBMS 의 내부 동작 원리 상 위와 같이 페이지 내의 게시물을 가져오면 위에서 언급한 "모든 페이지 내 게시물을 가져올 때 2 초가 걸리는" 페이지가 될 수 있습니다. 아니,어쩌면 모든 페이지를 5 초 걸려서 가져올 겁니다. 

 

왜냐하면, 저렇게 범위 검색을 하게 되면 모든 테이블 내용을 정렬한 뒤 1 부터 번호를 쭉 달아서 끝번호까지 번호를 부여한 뒤 21 ~ 30 까지의 데이터를 가져오기 때문입니다. 예제에서는 게시물이 많이 없기 때문에 충분히 빠르지만, 실제 수백, 수천 건의 데이터가 들어있으면 속도 저하가 눈에 보입니다.

 

그럼 어떻게 해야 할까요?

 

해결책 중 하나는 다음과 같은 방법입니다.

 

select X.rnum, X.id, X.name, X.content, X.createdate

from ( 

    select rownum as rnum, A.id, A.name, A.content, A.createdate

    from (

        select id, name, content, createdate

        from board

        order by createdate) A

    where rownum <= 30) X

where X.rnum >= 21


좀 달라졌죠? 번거롭게 한 번 더 둘러싼 뒤 게시물을 가져옵니다. 무슨 차이가 있을까요? DBMS 는 ROW NUMBER 을 부여하다가 위와 같이 첫번째 조건을 만족하게 되면 최적화를 통해 그 아래 데이터에 대한 정보 수집을 중지합니다. 그래서 30 개까지만 임시 테이블에 저장해둔 뒤 번호를 부여하고 나머지 값들은 버립니다. 그 뒤에 30개 안에서 앞의 20 개를 버리고 21 개째부터 나머지(=30)을 가져오기 때문에 속도가 빠릅니다.


즉, 총 게시물 수가 천만개라면, 정렬 후 천만개 모두를 번호를 매기면서 21 보다 같거나 크고 30 보다 작거나 같은 것을 찾는 것과 정렬 후 천만개 중 30 개까지 번호를 매긴 뒤 그걸 따로 떼서 21 보다 같거나 큰 것만 따로 떼서 사용자에게 제공하는 것의 차이가 발생하기 때문에 속도 차이가 눈에 띄게 나타납니다.


 

 

그래서, 만약 현재 자신이 사용중인 페이지 쿼리가 between 을 쓰고 있거나 한 번의 서브쿼리을 이용한 범위 검색을 쓰고 있다면, 두 번의 서브쿼리 형태로 바꾸시길 바랍니다.


추가로, 저번 글에서 말씀드렸던 부분인데...하나의 쿼리에서 총 게시물 수를 ROW_NUMBER() 등을 통해서 위의 페이지 쿼리와 동시에 쓰는 분들이 있습니다. 위와 비슷한 이유로 속도가 느려집니다.


습관적으로 위와 같이 페이징을 할 때에는 전체 게시물 수를 얻는 쿼리 따로, 그리고 페이지 게시물을 가져오는 쿼리를 따로 쓰고, ROW NUMBER 을 이용할 때에는 between 형태를 이용하지 않도록 주의하시면 페이징을 90% 마스터 하시는 겁니다.


물론, content 나 name 은 like 검색을 통해 전후방 일치 검색을 하게 되는데 게시물이 매우 많아지면 어쩔 수 없이 느립니다. 이 때에는 검색엔진의 힘을 빌리는게 가장 좋은데, 검색엔진은 보통 결과로 위에서 설정한 id 와 같은 pk 나 uq 값을 페이징 처리까지 해서 결과로 제공합니다. 그러므로 더 쉬운 게시판 페이징을 할 수 있습니다.


 

 

마지막으로, 위와 같은 쿼리를 할 때 또 한가지 주의점을 알려드립니다. 위에서 createdate 는 일부러 날짜형으로 지정을 했습니다. 그런데, 프로그램에서는 이를 변환해서 String 형태로 받는 경우가 더 많습니다. 그래서 DBMS 에 쿼리로 질의를 할 때 변환 함수를 써서 변환을 하는데 이걸 언제하는지 고민하지 않고 개발하는 분들이 계십니다. 아래와 같이 처리하는 거죠.


select X.rnum, X.id, X.name, X.content, X.createdate

from ( 

    select rownum as rnum, A.id, A.name, A.content, A.createdate

    from (

        select id, name, content, to_char(createdate, 'yyyy-MM-dd') as createdate

        from board

        order by createdate) A

    where rownum <= 30) X

where X.rnum >= 21


하지만, 이렇게 할 경우 모든 게시물에 대한 날짜 정보를 변환을 한 뒤에 번호를 붙여주게 됩니다. 그래서 이런 변환 함수는 해줄 수 있는 가장 마지막 단계에서 해주는게 좋습니다.


select X.rnum, X.id, X.name, X.content,  to_char(X.createdate, 'yyyy-MM-dd') as createdate

from ( 

    select rownum as rnum, A.id, A.name, A.content, A.createdate

    from (

        select id, name, content, createdate

        from board

        order by createdate) A

    where rownum <= 30) X

where X.rnum >= 21

 



다음에 시간이 좀 남으면 3 편도 연재해볼까 생각합니다. 요즘은 거의 쓸 일이 없는 SQL Server 의 예전 방식인 TOP 을 이용하는 방식이나 MySQL 의 LIMIT 정도가 되겠네요.


초보 분들에게 많은 도움이 되었으면 좋겠네요.

 

 

이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;


위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.

29
36
  • 댓글 41

  • 버미버미
    85
    2015-06-23 13:54:40
    잘 봣습니다~~^^
  • zepinos
    21k
    2015-06-23 18:55:18

    fx 님//성능 테스트는...오라클에서 주로 했고, 그것도 좀 예전 일이라 요즘 DBMS 의 내부가 바뀌었으면 저 내용이 맞지 않을 수 있습니다.


    하지만, 제가 위에서 말씀드렸다시피 order by 보다 rownum 이 우선적으로 영향을 받기 때문에 서브쿼리에서 rownum 을 만들어서 가져오면 순서대로 가져오지 않을텐데요...


    속도 부분도 마찬가지구요.


    지금 오라클 장비가 전혀 없어서 테스트는 진행하지 않고 제 지식만으로 적은 글이긴 합니다만, 최소 oracle 10g 이하 버전에서는 between 은 분명 두 번의 서브쿼리를 이용한 방식보단 느립니다. 제가 주로 했던 건 억 단위 건수라(구조는 간단하지만) 체감이 바로 될 정도였으니까요.


    물론 정형화된 래핑(MyBatis 의 <sql> 등을 이용해서)을 통해서 쿼리를 한 군데서만 작성하면 count 와 게시물 조회까지 적용되도록 만들 수도 있습니다. 대신 function 부분이 좀 걸리죠. 이게 실행계획에 따라서 알아서 최종 결과에만 function 이 적용될 때도 있지만, 실행계획이 100 % 의도대로 되지 않기 때문에 습관적으로 최외각에 적어주는게 사고위험(?)을 벗어나는 가장 좋은 습관이라 생각합니다.



    간단하게 찾아보니 다음 같은 링크가 보이네요. 오라클 장비가 있으시면 직접 테스트해서 결과를 알려주시면 더 고맙겠습니다. 어쨌든 이 글에선 기본적인 습관에 관한 문제이기 때문에 게시물 수가 많이 않으면 뭘 써도 좋으나 습관적으로 이렇게 쓰는게 좋을 것이라는 가이드 같은 거니까요.


    http://blog.naver.com/giragi/46357666

    http://erim1005.blogspot.kr/2012/10/blog-post.html

  • fx
    1k
    2015-06-23 19:00:29

    죄송합니다.

    제가 틀렀습니다.


  • zepinos
    21k
    2015-06-24 09:56:29

    댓글을 다 지우셨네요. ^^;;; 다른 분들도 내용을 보고 검증도 하고 그러면서 공부할 기회라고 생각했었는데 말입니다. 아쉽네요.


    저도 오라클 튜닝을 업으로 삼는 친구(군대에서 오라클을 배운 놈이라 저보다 경력이 살짝 깁니다)가 있어서 아주 오래 전에 페이징에 대해서 연구할 때 오라클의 동작 원리 등을 조언받을 수 있긴 했지만 그래도 맨땅의 헤딩(?)이 제가 올린 글의 내용에 대한 근간이 되거든요.

  • smilehoho
    382
    2015-06-24 11:02:02

    oracle 12c 사용하고 있습니다.

    기술문서를 보다가 운 좋게 페이징에 유용하게 사용할 수 있는 기능을 발견하여 사용 중입니다. 아직까지는 성능면에서 만족하고 있는데, 데이터가 많아 지면 어떨지 모르겠습니다.


    OFFSET (#{PAGE} - 1) * #{FETCH} ROWS FETCH NEXT #{FETCH} ROWS ONLY

    mybatis 소스입니다.


    선배님의 의견이 듣고 싶습니다.

  • 아야나미
    2k
    2015-06-24 11:08:39

    egov도 그렇고 여태 between으로 사용해왔는데

    한번 사용해봐야겠습니다. 감사합니다.

    해당 내용 제 블로그에 링크 남기겠습니다 :)

    ------------------------------------------------------------

    3.0 부터인지는 모르겠지만 egov도 샘플 생성해보면

    LIMIT #{recordCountPerPage} OFFSET #{firstIndex}

    식으로 변경되었습니다.(오라클이 아니라서 그런가;)


  • 샤프심
    207
    2015-06-24 11:09:40

    좋은 글 감사합니다.

    얼마전 면접볼때 페이징 처리 질문에 벙쪘던..기억이..

    다시한번 리마인드 시켜주네요~

  • zepinos
    21k
    2015-06-24 13:00:04

    smilehoho 님 // 위에서 살짝 언급했듯이...제 지식이 오라클 10g 정도에서 멈추어 있는게 사실입니다. 말씀하신 offset fetch 의 경우 페이징에서 더 좋은 효율을 보여주지 않을까 생각됩니다. 구문도 훨씬 간단해지구요. MS SQL Server 2012 부터도 제공되는 기능 같네요.


    참고로, MySQL 의 LIMIT 가 위에서 언급한 ROW NUMBER 방식보다 더 빠릅니다. smilehoho 님께서 언급하신 offset fetch 가 LIMIT 와 같은 방식이 아닐까 추측해봅니다.

  • LichKing
    16k
    2015-06-24 15:09:43

    확인차 한가지만 여쭤보겠습니다.

    비트윈이나 서브쿼리한번 더 쓴거나 어쨋든 제일처음 천만건가지고오는건 동일한데 비트윈은 1000만까지, 즉 모든로우에 로우넘을 부여한다음에 20보다크고 30보다작은걸 가져오는방식인건가요???

  • zepinos
    21k
    2015-06-24 15:19:49

    LichKing 님 // 서브 쿼리 한 번 써서 거기서 rownum 을 채번하고 거기서 between 쓰면 그렇게 되는 것으로 압니다.


    rownum 채번한 걸 다시 서브쿼리 써서 범위 검색을 해야 범위의 max 값까지만 채번하고 뒷부분을 채번하지 않고(정확히는 order by 을 하다가 중지한다고 들었습니다. 저도 오래 전에 들은거라 기억이 가물가물) 작업을 진행한다고 들었습니다.


    실제 천만개 정도의 row 을 dummy 로 쌓아놓고 한 번만 서브쿼리 써서 between 을 해보시면 확인 가능할 겁니다. 위에서 fx 님도 해당 테스트를 진행하시고 댓글 지우신 것 같구요.


    오라클 12c 부터는 offset fetch 을 지원하기 때문에 이 기능을 사용하는 것도 권하고 싶은데...문제는 12c 쓰는 곳이 워낙 드물테니...아직도 9i 쓰는 곳도 허다하게 많아서요. 생각난 김에 튜너 일도 하는 친구놈(지금은 오라클ERP TA 에 중점을...)에게 전화 걸어보니 12c 는 자기도 구경 안해봤다고 그러네요.

    워낙 비용이 비싸서...

  • zepinos
    21k
    2015-06-25 10:07:20

    어제 오랫만에 SQL Server 쪽 문서를 살펴보니 SQL Server 2012 에 도입된 offset fetch 는 성능이 실망스러운 수준이라고 그러더군요.


    오라클 쪽에서 성능 검증을 못해봤기 때문에 offset fetch 에 대해서 성능이 검증되면 다시 그에 대한 글을 남겨놓겠습니다.

  • 하마
    8k
    2015-08-09 11:42:12

    글 중간에 

    "물론, content 나 name 은 like 검색을 통해 전후방 일치 검색을 하게 되는데 게시물이 매우 많아지면 어쩔 수 없이 느립니다. 이 때에는 검색엔진의 힘을 빌리는게 가장 좋은데, 검색엔진은 보통 결과로 위에서 설정한 id 와 같은 pk 나 uq 값을 페이징 처리까지 해서 결과로 제공합니다. 그러므로 더 쉬운 게시판 페이징을 할 수 있습니다." 

    이런 문장이 나오는데, 페이징 초보자로서 이해가 잘 안되네요.  

    - "content 나 name 를 통해서 문장 내용을 검색한후에 검색된 결과를 페이징할경우, 즉 날짜기반 정렬이 아닐경우, 검색엔진을 이용하는것도 고려하자" 인가요? 

    좋은글 잘 읽었습니다. 감사합니다.


  • zepinos
    21k
    2015-08-09 22:12:38

    하마 님// 그게 아니라, RDBMS 에서의 인덱스 처리 방법의 문제입니다.


    SQL 에서 like 검색의 특징인데, 본문 검색일 경우 전방, 후방, 전후방 일치 검색 중 전후방일치 검색을 합니다. 하지만 RDBMS 는 전방 일치 검색만 인덱스에서 조회를 합니다.


    즉,


    where content like '검색%'          -- 전방 일치

    where content like '%검색'          -- 후방 일치

    where content like '%검색%'       -- 전후방 일치


    이 경우 첫번째 방식만 인덱스를 이용하기 때문에 속도가 빠를 뿐, 나머지 두 방식은 텍스트 전체를 뒤져서 검색을 하기 때문에 속도가 눈에 확 뛸 정도로 느려집니다. (물론 자료량이 많지 않다면 바로 체감 못할 수도 있겠지만요)


    검색엔진들은 content 필드의 값을 형태소 분리를 통해 키워드를 추출해서 일치검색을 하거나 낱자 검색 형태를 취한다던가 하는 형태로 본문 내용 그 자체를 분해해서 이를 빨리 찾아주는 형태입니다. 그래서 RDBMS 와는 처리 방식이 다르기 때문에 본문 검색에서 훨씬 유리합니다. RDBMS 는 필드의 변형을 통한 검색에는 매우 취약합니다. 아니, 원래는 그런걸 고려하지 않습니다.


    정렬의 문제가 아니라, 정렬 이전에 검색 자체의 속도가 느려지기 때문에 본문의 like 검색은 양이 많아질 경우 게시판에서 지원을 안하거나 검색엔진을 별도로 도입하는 쪽으로 가는게 좋습니다.


  • 하마
    8k
    2015-08-09 23:44:27

    흠  내용은 이해가 가는데 페이징이나 게시판 만들어본적이없어서 그런지 저 내용이 페이징이랑 어떤연관성 있는지를 유추하기 힘드네요..


    위에 형광색 칠한 질문처럼  본문검색을 빠르게 해서 나온 ' 결과 로우' 를 남바링해서 페이징하자가 아닌가보죠;;

  • zepinos
    21k
    2015-08-10 00:01:44

    저 내용은 페이징과 상관이 없기 때무에 유추하기 힘든 겁니다.


    색칠하신 내용은 페이징과 관계없는, RDBMS 에서의 기본적인 내용입니다. 아주 기본적인 내용인데, 페이징 공부를 이제 시작하시는 분들은 이런 기본적인 내용도 잘 모르는 경우가 있기 때문에 주의 차원에서 올린 내용입니다.


    물론 본문 내용 자체를 like 로 후방/전후방 일치 검색을 하면 목록 자체를 뽑아오는게 매우 느리기 때문에 페이징 처리 자체도 느려지는게 맞습니다.
  • return true
    3k
    2017-09-08 16:51:31

    2년뒤에 발견했네요

    좋은글입니다.

  • bytrustu
    256
    2018-07-28 13:54:38

    감사합니다

    페이징처리 학습중이었는데

    검색을 해도 해당 소스에 대한 자세한 설명이 부족해서 제대로 이해를 못하고 있었는데

    이번기회에 제대로 이해하고 가게 되네요 

    :)

  • BR-Sung
    468
    2018-08-02 17:37:04

    참 이해가 잘 되도록 써주셨습니다.

    감사합니다.

    그런데, 서브쿼리 하나와 between을 사용하는 구문에서

    between 1 and 10 은 되지만, 앞의 숫자가 1이상일 때에는 어떤 결과도 출력되지 않습니다.

    혹시 잘못된게 아닐까요?


    서브쿼리 두번 사용하는 구문은 정말 많은 도움이 되었고, 이해도 잘 되었습니다.

    보신다면 답변 부탁드립니다.

  • zepinos
    21k
    2018-08-02 17:46:41

    between 비교를 빼고 한 번 출력해보세요. 실제 rownum 이 그 사이에 존재해야 출력됩니다.

  • BR-Sung
    468
    2018-08-02 17:55:59

    between 비교를 빼면 1부터 모든 레코드가 출력 됩니다.

    제가 테스트한 테이블의 총 레코드 수는 1044개이구요.

    between 외 어떤 조건도 추가하지 않았습니다.

    분명 between 1 and x 로는 출력이 되는데요.

    between 1이 아닌 값은 출력 자체가 안되네요.

    말씀하신 rownum이 그 사이에 존재하구요.

    직접 한 번 테스트해주실 수 있을까요?

    왜 이러는지 너무 궁금하네요.


    제가 테스트한 3가지 쿼리 입니다.

    ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

    select rownum as rnum, A.name, A.address

    from (select name, address

          from person

          order by name asc) A

    where rownum between 11 and 20;  < -- 출력 안됨. (0개 레코드)


    ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

    select rownum as rnum, A.name, A.address

    from (select name, address

          from person

          order by name asc) A

    where rownum between 1 and 10;  < -- 출력 됨. (10개 레코드)


    ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

    select rownum as rnum, A.name, A.address

    from (select name, address

          from person

          order by name asc) A

    where rownum between 1 and 30;  < -- 출력 됨. (30개 레코드)


  • zepinos
    21k
    2018-08-02 18:10:12

    아쉽게도 제가 지금 오라클 장비가 하나도 없어요...T.T


    정확하게 하려면 rownum as rnum 나오는 걸 다시 subquery 로 해서 한 번 더 외각에서 between 해야합니다. 그러면 두 번의 비교와 동일해져요. between 으로 하는건 실무에서 제가 직접 구현해보질 않아서(남이 써놓은 것만 봤어요)...현재는 확답을 드리기 어렵겠네요. 죄송합니다.
  • BR-Sung
    468
    2018-08-02 18:13:09

    답변 감사합니다.

    서브쿼리 두 번 사용해서 하는건 매번 보기만 봤는데

    왜 그렇게 사용하는지 모르고 복사 붙여넣기로 사용 했었거든요.

    설명을 잘 해주셔서, 이해가 되니까 눈 감고도 쿼리 작성할 수 있게 되었네요.

    감사드립니다.

    좋은 하루 보내세요.

  • a-z
    5
    2018-08-31 16:26:49 작성 2018-08-31 16:27:05 수정됨

    BR-Sung 님 

    혹시 이 문제 해결 하셨나요? 같은 문제로 고민중인데 1만 가능하고 2부터는 값이 안나오네요..

  • a-z
    5
    2018-08-31 16:42:39

    select *

    from (select rownum as rnum, B.*

    from board B)A

    where a.rnum between 21 and 30; 


    로 해결했습니다.

  • 크림붕어빵
    305
    2018-10-28 19:03:18

    안녕하세요.
    현재 오라클로 페이징을 하고 있는 학생입니다.

    올려주신 이 코드에서 서브 쿼리가 끝나고 A나 X를 입력하시는 이유가 뭔가요?

    테이블명인지 아닌지 잘 모르겠네요.

    select X.rnum, X.id, X.name, X.content, X.createdate
    
    from ( 
    
        select rownum as rnum, A.id, A.name, A.content, A.createdate
    
        from (
    
            select id, name, content, createdate
    
            from board
    
            order by createdate) A
    
        where rownum <= 30) X
    
    where X.rnum >= 21
  • zepinos
    21k
    2018-10-28 19:24:27

    alfhsk 님 // 괄호 안 쿼리의 결과가 가상의 테이블에 저장이 됩니다. 그 가상 테이블을 A 와 X 로 이름을 붙여준 것입니다(별명, alias).

  • 크림붕어빵
    305
    2018-10-28 20:38:20

    zepinos님


    답변 감사합니다.

    그런데 질문 하나만 더 해도 될까요?


    예제를 보고 따라하면서

    where문에 count를 써서 모든 값에 번호를 매긴다음 2부터 끝까지 출력하게 하려고 했는데

    어디서 잘못된 건지 모르겠습니다.

    select * 
    from (
    	select rownum, POST_WRITER, post_password, post_title,post_contents,post_date,post_num 
    	from (
    		select * 
    		from board_tbl 
    		order by post_num asc)
    		
    	where (
    		select count(post_num) 
    		from board_tbl)<=rownum) 
    where rownum>=2
    

  • zepinos
    21k
    2018-10-28 20:50:58

    의도하는 바가 뭔지 모르겠지만, 일단 rownum 은 출력순서대로 번호를 자동으로 부여하기 때문에 서브쿼리의 select 에 모두 rownum 이 있기 때문에 as 을 이용해 별칭을 줘서 외부에서 사용하는게 좋습니다. 그리고 count() 는 집계함수니까 집계합수를 저렇게 쓰는선 말이 안될 것 같은데요...


    정확히 의도하는 바가 뭔질 모르겠네요.

  • 크림붕어빵
    305
    2018-10-28 21:04:45 작성 2018-10-28 21:14:02 수정됨

    zepinos 님


    오라클을 배우고 사용한지 얼마 안 돼서 뭐가 되고 뭐가 안 되는지 잘 모르겠네요..

    의도하는 바는 테이블의 모들 튜플에 번호를 부여해서 rownum 2부터 출력하는 거였는데

    제가 글을 이상하게 썻나 보네요.


  • zepinos
    21k
    2018-10-28 21:46:57

    테이블의 튜플이라는게 레코드와 동일한 거라면 그냥 rownum >= 2 라고 하면 됩니다.

  • Twibap
    146
    2019-01-03 01:26:39

    아직 초보라 "여러개 쿼리를 사용하는게 한번 사용하는것 보다 빠를 수 있다" 정도로 이해했습니다.

    쿼리를 최대한 적게 쓰는게 좋을거라 생각해서 개발 속도가 잘 안나왔는데 일단 만들고 튜닝은 나중에 ^^;;;

  • GODisGood
    2
    2019-10-29 00:18:32

    정말 최곱니다!!!!! ㅜㅜ 오늘 paging 때문에 야근까지 하면서 삽질하고 왔는데 ㅜㅜ


    몇가지 질문하고 싶은 것이 있습니다 !!


    저도 작성자님처럼 서브쿼리와 rownum을 사용해서 범위에 있는 데이터를 가져오는데 성공했습니다! 문제는 DBMS TOOL 에서만요..ㅜㅜ Toad 를 사용하는데 TOAD에선 제가 원하는 결과가 정확히 출력되는데 


    mybatis를 사용하는데.. 


    select 생략

      from (생략)

    where rnum between @{start} and @{end} 


    이런식으로 수행하면 엉뚱한 방식으로 값을 가져옵니다...ㅜㅜㅜ


    왜 그런 걸까요,,


  • 마우스
    163
    2020-01-14 15:43:12

    강좌 감사합니다.

    궁금한게 있는데 마지막에 X로 한번 더 감쌀때


    select * from
    (select rownum R, A.* from (
    select bid,bname,btitle,bcontent,bdate from mvc_board order by bdate) A)
    where R between 1 and 4;

    그냥 이런식으로 감싸도 되지 않나요??

  • zepinos
    21k
    2020-01-14 15:50:59

    님 // 두 번 감싼 형태가 동일하기 때문에 부등호로 한 번씩 처리하는 것과 내부적으로 동일하게 동작할 겁니다. 댓글 초반부에 내용이 있었는데, 댓글 중에 지워진게 있어서 내용이 이어지질 않아서 이해가 어려울 수 있겠네요. 명확히 하기 위해 부등호로 최대값(여기서는 4)으로 목록을 자르고 다시 외부에서 최소값(여기서는 1)으로 목록을 다시 자르는게 더 명확하게 이해할 수 있다고 생각됩니다.

  • 마우스
    163
    2020-01-14 16:00:48

    zepinos 님//

    빠른 답변 감사드립니다.

    내부적으로 동일하게 나오지 지금은 제 방식대로 사용해면서 더 공부해서 익히도록 하겠습니다.

  • 홀리씻
    12
    2020-09-16 03:48:26 작성 2020-09-16 03:49:00 수정됨

    와 지금도 활동하고 계시네용 오래전 게시물이라 다른 글 먼저 훑어보고 보고 고민하다가 들어왔는데 많이 배우고가네요 감사합니다!! (나 닉네임 왜 이러짘ㅋㅋ)

  • zepinos
    21k
    2020-09-16 12:01:25

     홀리씻 님 // 아직 현역 개발자랍니다. ^^;;; 사십중반 아재개-_-발...

  • 매운만두맥주
    1k
    2020-12-04 17:06:12

    좋은 자료 정말 감사합니다. 초보 입장에서 정말 귀중한 자료네요 ㅠㅠ

    궁금한 점이 있는데 여쭤봐도 될까요??


    게시글 30개 이하 (page3 이하)의 게시물 중 

    (1) 최신 글 기준, 인덱스가 30 이하인 범위를 먼저 검색.

    (2) 선택된 범위 중 page 1,2,3에 해당하는 부분을 (page를 인자로 받아)검색


    즉, 성능 향상의 관건은 처음 검색할 범위 (<=30)를 정해둔다는 것에서 비롯되는 걸로 이해했는데

    그렇다면

    (2)에 해당하는 부분은  

    between (page -1 )*10 and page*10 이런식으로 between을 써서 범위를 지정해주면 될까요?



    두 번째로 만약 30 이상이 되는 글을 검색하는 것도 염두해 두어

    처음 Controller 부분에서

    1. page <= 3일 때는 zepinos님이 말씀하신 것처럼 쿼리를 작성하고

    2. page >3일 때는 일반적인 쿼리작성 방법대로 SQL문을 짜면 되는건가요??

  • zepinos
    21k
    2020-12-04 17:28:57

    질문 내용이 잘 이해가 안가서 정확한 답을 해드리기는 힘들겠네요.


    1. 30개 이내의 글 중에 검색 내용이 있는 것만 보여주는 방식인가요? 일부 게시판들을 보면 검색을 하면 특정 범위의 결과만 보여주고, 이전보기/다음보기 같은 버튼을 두어서 다음 영역의 검색결과를 보여주는 식으로 처리하던데...그렇게 하면 Full Text Search 라고 하더라도 정해진 범위에서만 처리하기 때문에 극단적으로 느려지는 경우는 드물 것 같습니다. 다만, 사용자가 예전 내용 포함해서 한 번에 보길 원하는 경우에는 매우 불편해지겠죠. 반대로 최신 글의 것만 보길 원하는 사용자가 많다면 유용할 것 같습니다.

    2, 이를 위해 30개의 글 중에서 검색 결과의 내용을 다시 페이징 한다면 서브 쿼리로 결과에 대한 between 와 같은 제약조건을 추가로 걸어야 하겠죠. 다만, 이런 경우 페이징마다 조회하는 것보단 처음에 전체 목록을 한 번에 가져와서 그 값들을 재사용하는게 나을 수 있을지도 모르겠네요.


    결국 원하는 조건이 뭔지에 따라 효율적인 구현방법은 천차만별인데, 사실 지금 뭘 원하시는지 이해가 잘 안되서...따로 글로 자세히 설명하시던지 해야 할 것 같아요.

  • 매운만두맥주
    1k
    2020-12-04 18:16:52

    아.. 죄송합니다. 제가 설명을  잘 못했네요..

    제 질문을 설명드리기 위해서, 위 글에 대해서 제가 이해한 것을 말씀드리면.


    "그런데, 일반적인 게시판은 아주 단순한 특징을 하나 가지고 있습니다. 사람들은 일반적으로 페이지 번호가 3 페이지 이상인 것을 거의 읽지 않는다는 것입니다"

    라고 하셨는데

    위 같은 문제를 해결하기 위해서,


    총 page (totalPage) 중 '1,2,3' 에 해당하는 부분, 즉 1~30 까지의 게시물들만 우선적으로 추려,

    그것을 기준으로 검색하는 방법론을 제시한 것으로 이해했습니다.


    예를 들면, 

    1 2 3 4 5 6 7 8 9 10 다음 

     이라는 페이지가 나열되어 있을 때

    page <= 3인 경우에 한하여,   

    1.  1~30까지의 게시물만 우선적으로 추린 후 (where rownum <= 30) 

    2.  거기서 다시 각 page의 범위를 다시 지정하는 것입니다.


    제 첫 번째 질문은 

    위 글에서는 그냥 숫자로 page=3일 경우만 고려해서 where X.rnum  >= 21 로 써 놓으셨는데

    실제 쿼리를 짤 때는 1,2,3 등의 page가 요청될 것을 고려해서

    where rnum between ((page-1)*10 +1) and page*10 이런식으로 짜면 되지 않느냐는 질문이었습니다.


    두 번째 질문은 

    page가 1~3인 경우를 제외하고 4 이상인 경우에는 (위에서는 page 1~3인 경우만 특정하여 SQL문을 작성하였으므로)

    그럼 컨트롤러에서 page >=4 인 경우를 따로 지정하여

    일반적인 페이지 탐색 SQL문을 써 놓으면 되는지 여쭤본 것이었습니다.


    제가 시작한지 얼마 안 돼서 설명을 잘 못 했습니다..

  • zepinos
    21k
    2020-12-04 18:49:02

    일단, 웹의 특성은...비연결성입니다. 사용자가 어떤 요청을 하면 서버는 그에 대한 처리를 한 뒤 결과를 돌려줍니다. 이게 request 와 response 입니다. 이 행위 후 두 기기 간 연결은 "끊어집니다". 그래서 미리 30 개의 결과를 뽑은 뒤 10 개밤 사용자에게 보내고, 다음 요청 시 그 뒤의 20 개를 사용한다는건...일반적인 방법으로는 불가능합니다. 처음부터 30 개를 다 보낸 뒤 사용자 브라우져에서 이를 조작해서 사용하던가, 아니면 서버 측에서 클라이언트를 특정할 수 있는 방법을 이용해서(쿠키 혹은 세션 등) 이미 저장하고 있던 데이터를 보내주던가 해야 합니다. 특히 후자의 경우는 서버 쪽에서 사용자의 정보를 임시 저장해야 하기 때문에 한 번에 하나의 서버와 통신하는 클라이언트와 달리 불특정 다수의 클라이언트와 통신하는 서버 입장에선 부하나 용량 등 고려 요소가 많습니다. 그래서 이런 식으로 처리하는 경우는 드뭅니다.


    본문에서 이야기하는, 페이지 번호가 빠를 수록 부하가 덜하다는 것은 미리 데이터를 뽑아놓고 재사용한다는 의미가 아닙니다. 요청을 DB 에 보낼 때, 페이징을 하는 범위에 따라서 DB 가 가지는 부하가 달라진다는 의미입니다.


    예를 들어보겠습니다. 게시물이 100 개가 있고, 불특정하게...내용 중에 "한국" 이라는 단어가 30개가 있다고 가정해보겠습니다. 그리고, 이 100 개의 게시물은 등록일 기준으로 정렬이 된다고 하구요.

    그럼, 1 페이지에 10 개의 게시물이 있다면...내부에서 어떻게 동작을 할까요? 먼저 등록일 기준으로 정렬을 해야겠죠. 그래서 100 개의 게시물은 인덱스를 이용하든 가상의 테이블에 정렬을 하든...먼저 정렬을 수행합니다. 여기까진 페이징의 속도 차이와는 연관이 없습니다.

    그 다음이 중요합니다. 첫번째 게시글의 본문을 뒤져 "한국" 이라는 단어가 있는지 확인합니다. 단어가 없다면 다음 게시글을 찾겠지만, 단어가 존재한다면 임시 테이블에 해당 게시물을 담아둡니다. 임시테이블에 1 개의 게시글이 담기겠죠. 이렇게 우리가 원하는 1 페이지 만큼(between 1 and 10 을 했든 rownum 을 써서 했든) 다 담기면...더 이상의 찾기는 의미가 없기 때문에 여기서 멈추고 결과를 돌려줍니다. 이렇게 DBMS 는 동작합니다. 만약 처음부터 "한국" 을 찾았는데 13 개의 글을 차례대로 찾아보니 10 개째가 찾아졌다면, DBMS 는 13 개의 글을 뒤져본게 됩니다.

    그럼 2페이지의 10 개의 게시물은 어떨까요? 위에서 언급했듯이 서버는 다음 요청에서 이전 요청의 내용을 알지 못합니다. 그렇기 때문에 정렬을 먼저 수행한 뒤...1 페이지가 어디서 끝났는지 알 수 없기 때문에(2페이지 요청할 때 데이터가 갱신될 수 있기 때문에 사실 요청 때마다 새로 해야 합니다) 임시 테이블이 첫번째부터 데이터를 채워나가야 합니다. 그렇기 때문에 첫페이지 때 찾은 13 개에 추가로 다음 10 개가 나온 만큼 더 본문에서 단어를 찾아야 합니다.


    이렇듯, 페이지가 늘어날수록 검색해야 하는 범위가 커집니다. 이유는 이전 요청 시의 결과를 유지하지 않고 있다는 점과 조건을 만족하면 중간에 처리를 멈춰버리기 때문입니다. 페이지가 늘어날수록 1페이지~원하는페이지까지 뒤져야 하기 때문에 점점 뒤져야 하는 양이 많아지기 때문에 속도가 느려지는건 당연한 것이 됩니다.


    이해가 되셨을지 궁금하네요.

  • 로그인을 하시면 댓글을 등록할 수 있습니다.