티스토리 뷰
게시판 페이징시 쉽게 쓰일수 있는 limit X, Y 시 X 값이 커질수록 응답속도가 늦어진다는 특성이 있다
이 점을 개선하고자 X 값이 과도하게 늘어나는 상황을 없애는 방법으로 속도에서 개선을 얻은 구현이다.
개선점이 더 필요하지만 그래도 그럭저럭 쓸만한 게시판이다.
/*
# 사용준비
1. 본 파일을 http://localhost/index.php 에 위치시킨다
2. 코드에서 "디비정보" 를 문자열 검색해서 디비정보를 알맞게 입력해준다
3. http://localhost/index.php?mode=truncate 주소창에 입력
# 글쓰기
http://localhost/?mode=insert
를 요청하면 1만건씩 입력하도록 되어있다.
# 글 목록보기
http://localhost/?mode=select&amount=30&page=1
한페이지당 30건씩 해서 1페이지를 조회한다는거다
주소에 &fast=1 를 붙이면 자료의 양과 비례하지 않는 속도를 내준다. 하지만 페이지에 삭제된 자료가 포함되있다면 그것을 제외한 만큼의 수만 보여준다
주소에 &fast=1 를 붙이지 않으면 삭제된 자료에 의해 생기는 구멍이 나지 않고 페이지마다 일정한 수를 보여준다. 하지만 아래쪽 페이지로 갈수록 자료의 양에 비례하는 속도를 낸다
# 글제거
http://localhost/?mode=delete&seq=54210981
를 요청하면 54210981 번 글을 지움
# 대략적인 성능
54210982 건의 게시물이 있고 1000개 분할 segment 로 데이터가 기록되있을 때 테스트 결과
테스트기기: 삼성SSD840프로, 16GB RAM, Xeon E3-1231V3, Windows 10
http://localhost/?mode=select&amount=20&page=2710400&fast= 마지막페이지 요청시 전체소요시간: 0.0059590339660645
http://localhost/?mode=select&amount=20&page=2710400 마지막페이지 요청시 전체소요시간: 0.36519384384155
http://localhost/?mode=select&amount=20&page=1&fast= 첫페이지 요청시 전체소요시간: 0.005479097366333
http://localhost/?mode=select&amount=20&page=1 첫페이지 요청시 전체소요시간: 0.014885902404785
*/
# 디비정보
$db_host = "localhost";
$db_user = "";
$db_password = "";
$db_database = "test";
# 글 목록 요청시 1회에 가지고올 segment 목록의 양의 한계치
$max_amount_for_segment_request_a_time = 10000;
# 하나의 segment 에 들어갈 자료의 수
$max_count_of_row_for_a_segment = 1000;
function get_time() {
list($usec, $sec) = explode(" ", microtime());
return ((float)$usec + (float)$sec);
}
function get_max_segment_no() {
global $mysqli;
$result = $mysqli->query("select max(segment_no) as segment_no from board_segments;");
$max_segment = 0;
while ($row = $result->fetch_object()){
if(gettype($row->segment_no) !== "NULL") {
$max_segment = $row->segment_no;
}
}
return $max_segment;
}
function get_article_count_for_segment_no($segment_no) {
global $mysqli;
$result = $mysqli->query("select article_cnt from board_segments where segment_no = '".$segment_no."';");
$article_cnt = 0;
while ($row = $result->fetch_object()){
if(gettype($row->article_cnt) !== "NULL") {
$article_cnt = $row->article_cnt;
}
}
return $article_cnt;
}
if(isset($_GET["mode"])) {
$start = get_time();
set_time_limit(0);
header("Content-type: text/plain; charset=utf-8");
$mysqli = new mysqli($db_host, $db_user, $db_password, $db_database);
if($_GET["mode"] === "truncate") {
# 테이블 초기화
$mysqli->query("DROP TABLE IF EXISTS board_body;");
$mysqli->query("DROP TABLE IF EXISTS board_segments;");
$query ="
CREATE TABLE IF NOT EXISTS `board_body` (
`seq_number` int(11) NOT NULL AUTO_INCREMENT,
`segment_no` int(11) NOT NULL DEFAULT '0',
`article_title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`seq_number`),
KEY `segment_no` (`segment_no`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1 ;";
$mysqli->query($query);
$query="CREATE TABLE IF NOT EXISTS `board_segments` (
`segment_no` int(11) NOT NULL,
`article_cnt` int(11) NOT NULL,
PRIMARY KEY (`segment_no`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;";
$mysqli->query($query);
}
if($_GET["mode"] === "select") {
# 조회하기
if(!isset($_GET['page']) || !isset($_GET['amount'])) {
exit;
}
# 요청한 페이지의 번호
$request_page_no = intval($_GET["page"]);
# 한페이지에 보여줄 자료의 수
$amount_to_show_a_page = intval($_GET["amount"]);
# dutie는 앞으로 가져와야할 자료의 총 수를 dutie에 넣어주며 이것은 충족될때마다 충족된만큼 빼주며 진행되게된다
$dutie = $amount_to_show_a_page;
# valueMaxK 에는 현재 가장 최신의 segment 의 번호가 들어가있다.
$valueMaxK = get_max_segment_no();
# 요청한 페이지의 가장 첫게시물이 될 자료가 있을 위치를 의미한다
# 만약 이 값이 0이면 전체 자료중 가장 꼭대기부터 가져오면 된다는거고
# 만약 이 값이 10이면 전체 자료중 가장 꼭대기부터 10개를 지나쳐온 다음부터 시작되는 자료를 가져오면 된다는 것이다.
$row_count_skip = ($request_page_no-1) * $amount_to_show_a_page;
# 페이지마다 균일한 갯수가 나오는것을 포기하는 대신 매우 빠른 속도를 얻고자 할때 사용하는 옵션
$fast_mode = isset($_GET["fast"]);
# 1회에 가져올 segment 의 수
# 대략적인 값이다
# 대략적이여도 작동에 무방하다
$seg_page = 1;
$seg_page_ = intval(ceil($row_count_skip / $max_count_of_row_for_a_segment));
if($seg_page_ > $seg_page) {
$seg_page = $seg_page_;
if($seg_page > $max_amount_for_segment_request_a_time) {
$seg_page = $max_amount_for_segment_request_a_time;
}
}
# segment 를 담기위한 LOOP 의 작동을 제어하기 위해 사용된다
# working 이 False 가 되면 모든 자료를 찾았거나 다 뒤져봤지만 자료를 끝내 다 충족시키지 못했다는 의미이다.
$working = true;
# accumulate_count는 segment 를 훑고 내려가면서 자료의 양을 누적시킨다
# 이 누적값은 요청한 페이지의 가장 첫게시물이 될 자료가 포함된 segment 를 찾기위해서 사용된다
$accumulate_count = 0;
# segment_list는 자료가 포함된 segment 를 담기위해 사용되며 여기에 담긴만큼 DB에 자료를 요청한다
$segment_list = [];
# lookup_segment 는 로직과는 무관 (시간측정 등을 위한 코드)
$lookup_segment = [];
while ($working && !$fast_mode) {
# 로직과는 무관 (시간측정 등을 위한 코드)
$start_sub = get_time();
$query = "select * from board_segments where segment_no <= ".$valueMaxK." order by segment_no desc limit ".$seg_page."";
$result = $mysqli->query($query);
# segment 의 목록을 가져온다. 예를 들어 valueMaxK=5, seg_page=3 이라면
# res['result'] 는 list이며 총 3행이 들어가게되고
# 0번째의 segment_no 의 값은 5이다
# 1번째의 segment_no 의 값은 4이다
# 2번째의 segment_no 의 값은 3이다
# 만약 segment_no 가 4인것이 없다면
# 0번째의 segment_no 의 값은 5이다
# 1번째의 segment_no 의 값은 3이다
# 2번째의 segment_no 의 값은 2이다
# 가 될것이다
# --------------------------
if($result->num_rows === 0) {
# 더 이상 segment 가 존재하지 않는다는 의미다.
# 첫번째 WHILE 에서 여기에 들어온다면 게시판에 자료가 아예 없다는 의미로 받아도 좋다
# 첫번째 WHILE 이 아니라면 찾다 찾다 바닥까지 왔지만 결국 한페이지에 보여줘야할 자료의 양을 충족시키지 못했다 라는 의미로 받으면 된다.
# 그래서 그냥 루프를 종료한다.
$working = false;
} else {
# WHILE 이 돌때마다 일정 범위(seg_page)만큼의 segmnt 목록을 가져온다.
# res['result'] 는 list 중 가장 작은 segment_no 에서 1을 뺀값을 valueMaxK 에 넣는다.
# 이는 다음 WHILE 에서 segmnt 를 가져올 것을 위해 미리 준비해둔 값이다.
# 다음 WHILE 이 실행되지 않는다면 valueMaxK 값은 사용되지 않게된다
$result->data_seek($result->num_rows-1);
$row=$result->fetch_object();
$valueMaxK = $row->segment_no-1;
$result->data_seek(0);
while($row = $result->fetch_object()) {
if($working) {
# 훑고 내려온 자료의 양을 누적시킨다
# 이 누적값은 요청한 페이지의 가장 첫게시물이 될 자료가 포함된 segment 를 찾기위해서 사용된다
$accumulate_count += intval($row->article_cnt);
# > 로 비교한것이 핵심인 꽤 중요한 부분이다.
# row_count_skip 는 전체 자료중 건너 뛸 자료의 수 이다.
# 즉 이 수만큼을 제외하고나서 부터의 자료들이 현재 요청한 페이지에 포함될 자료가 된다는 의미다.
# 따라서 현재 accumulate_count 에 포함된 자료의 수는 row_count_skip보다 커야만 현재 segment 에서 가져올 것이 존재한다는 의미이다
# 만약 그렇지 않다면 아직 요청한 페이지에 포함될 자료가 존재하는 segment 를 찾지 못했다는 의미이다.
if ($accumulate_count > $row_count_skip) {
# 여기에 들어와야 비로소 페이지에 포함될 자료가 존재하는 segment 를찾은거다.
# 현재 segment 에서 가져올 위치를 계산한다.
if (sizeof($segment_list) == 0) {
# 아직 segment_list 에 추가된 segment 가 없다.
# 이는 요청한 페이지의 가장 첫게시물이 될 자료를 현재 segment 에서 찾았다는것이고, 그 시작점을 계산해서 segmnt_set['start_row'] 에 넣는다
$row->start_row = intval($row->article_cnt) - ($accumulate_count - $row_count_skip);
}
else {
# 여기에 왔다는건 이미 FOR 루프를 한회 돌고나서 그때 자료의 량을 충분히 받지 못해서 다시한번 들어온거다.
# 이때는 segment 의 가장 처음인 0부터를 범위로써 지정한다
$row->start_row = 0;
}
# available_to_get 의 값의 의미는 현재 segment 에서 가져올 수 있는 자료의 수이다.
# 현재 segment에서 시작점으로부터 바닥까지 포함되는 자료의 총 자료의 수이다.
$available_to_get = intval($row->article_cnt) - $row->start_row;
# 앞으로 가져와야할 자료의 총 수를 의미하는 dutie 를 segmnt_set['get_rows'] 에 대입한다
# segmnt_set['get_rows'] 의 의미는 현재 segment 에서 가져오기로 약속할 수이다.
$row->get_rows = $dutie;
# 이렇게 현재 segment 에서 가져오기로 약속한 수가 실질적으로 유효한지를 비교한다.
# available_to_get 는 현재 segment 에서 가져올수 있는 자료의 수를 의미한다.
# available_to_get 값은 가져와야할 수보다 크거나 같을 수도 있고, 작을수도 있다.
# 작지 않다면 이번 segment 에서 가져올 데이터가 충족된다는의미이고
# 작다면 부족하다는 뜻이다.
# 나머지 모자르는 부분은 다음 segment 에서 가져와야 한다
if ($row->get_rows > $available_to_get) {
# 여기 들어왔다는건 available_to_get 로는 부족하다는 뜻이다.
# 일단 이번 segment 에서 가져올수 있는 양만큼은 가져오고 부족한 양은 다음 segment 에서 채우도록 한다
$row->get_rows = $available_to_get;
}
# 앞으로 가져와야할 자료의 총 수를 의미하는 dutie 에서 현재 가져오기로 확정된 수만큼을 빼준다
$dutie -= $row->get_rows;
# 이렇게 해서 만든 segmnt_set 를 segment_list 에 추가해준다
# segmnt_set 이란 결국 본 segment 에서 가져올 구체적 범위라고 보면 된다.
# 이렇게 모은 segment_list 에 있는 범위들을 DB에서 조회하게될것이다.
$segment_list[] = $row;
if ($dutie <= 0) {
# 더 이상 가져올 자료가 없다는것이다
# 루프를 종료한다
$working = false;
}
}
}
}
}
# 로직과는 무관 (시간측정 등을 위한 코드)
$end_sub = get_time();
$lookup_segment[] = ['duration'=>$end_sub - $start_sub, 'num_rows'=>$result->num_rows, 'query'=>$query];
}
# 로직과는 무관 (시간측정 등을 위한 코드)
print "segment 탐색을 위해 사용된 쿼리 목록과 각각의 소요시간\n";
$total_value = 0;
foreach($lookup_segment as $value) {
$total_value+=$value['duration'];
}
print_r($lookup_segment);
print "segment 탐색 전체소요시간: ".$total_value;
print "\n";
# lookup_segment 는 로직과는 무관 (시간측정 등을 위한 코드)
$lookup_segment = [];
# 게시물을 담기위해 사용될 배열
$page_result = [];
if($fast_mode) {
# 로직과는 무관 (시간측정 등을 위한 코드)
$start_sub = get_time();
# fast_mode 를 사용할때 페이지에 포함시킬 자료의 seq_number 준비하기
$seq_list = [];
if($fast_mode) {
$all_count = $mysqli->query("SELECT count(*) as cnt FROM board_body")->fetch_object()->cnt;
$start_number = $all_count - $row_count_skip;
for($i=0;$i<$amount_to_show_a_page;$i++) {
$seq_list[] = $start_number-$i;
}
}
$query = "SELECT * FROM board_body WHERE seq_number in (".join(",", $seq_list).")";
$result = $mysqli->query($query);
while($row = $result->fetch_object()) {
$page_result[] = $row;
}
# 로직과는 무관 (시간측정 등을 위한 코드)
$end_sub = get_time();
$lookup_segment[] = ['duration'=>$end_sub - $start_sub, 'query'=>$query];
} else {
foreach($segment_list as $key=>$list) {
# 로직과는 무관 (시간측정 등을 위한 코드)
$start_sub = get_time();
$query = "select * from board_body where segment_no=".$list->segment_no." order by seq_number desc limit ".$list->start_row.", ".$list->get_rows."";
$result = $mysqli->query($query);
while($row = $result->fetch_object()) {
$page_result[] = $row;
}
# 로직과는 무관 (시간측정 등을 위한 코드)
$end_sub = get_time();
$lookup_segment[] = ['duration'=>$end_sub - $start_sub, 'query'=>$query];
}
}
print "******************************\n";
print "가져온 게시물\n\n";
foreach($page_result as $key=>$list) {
print $list->seq_number."\n";
}
print "******************************\n\n";
# 로직과는 무관 (시간측정 등을 위한 코드)
print "실제 자료를 가져오기 위해 사용된 쿼리 목록과 각각의 소요시간\n";
$total_value = 0;
foreach($lookup_segment as $value) {
$total_value+=$value['duration'];
}
print_r($lookup_segment);
print "실제 자료가져오기 전체시간: ".$total_value;
print "\n";
print "\n";
}
if($_GET["mode"] === "insert") {
# 글 입력
# 입력할 글의 갯수
$amount = 10000;
# WRITE LOCK 차지하기
# 동시적인 데이터 접근으로 인해 데이터의 무결성이 파괴되는것을 막는다.
# 한 접속이 READ 중일때는 다른 접속들이 READ 하는것과는 경합이 발생하지 않는다. 그러나 다른접속들이 WRITE 하는것은 READ 접속이 끝나야 이루어진다
# WRITE 가 이루어지고있을때는 WRITE 락을 차지한 접속외에 다른접속들은 READ/WRITE 모두 대기하게된다
$mysqli->query("LOCK TABLES board_body WRITE, board_segments WRITE");
for($i=0;$i<$amount;$i++) {
# 현재 가장 큰 segment 번호를 가져옴
$max_segment = get_max_segment_no();
# 현재 가장 큰 segment 번호에 해댕하는 자료의 갯수를 가져옴
$article_count_in_max_segment = get_article_count_for_segment_no($max_segment);
# segment 번호가 하나도존재하지않거나 이미 현재 가장 큰 segment 번호에 할당될수 있는 자료량이 초과되었을경우에는
# segment 번호를 1 더해서 새로운 segment 를 만든다
# 그게 아니라면 현재 segment 에 1을 추가한다
if($article_count_in_max_segment >= $max_count_of_row_for_a_segment || $max_segment === 0) {
$max_segment++;
$mysqli->query("INSERT INTO board_segments (segment_no, article_cnt) VALUES (\"".($max_segment)."\", \""."1"."\");");
} else {
$mysqli->query("update board_segments set article_cnt = article_cnt+1 where segment_no =\"".$max_segment."\";");
}
# 행 추가
$mysqli->query("INSERT INTO board_body (segment_no) VALUES (\"".($max_segment)."\");");
}
# UNLOCK
$mysqli->query("UNLOCK TABLES");
}
if($_GET["mode"] === "delete") {
if(isset($_GET["seq"])) {
$mysqli->query("LOCK TABLES board_body WRITE, board_segments WRITE");
$seq_number = $_GET["seq"];
$result = $mysqli->query("select * from board_body where seq_number = '".$seq_number."';");
$row = $result->fetch_object();
# 글 지우고
$mysqli->query("delete from board_body where seq_number = '".$seq_number."';");
# 글이 포함되있던 segment 의 article_cnt 를 -1 해줌
# 그리고 글이 포함되있던 segment 의 article_cnt 가 0이 되면 segment 를 제거해줌
$mysqli->query("update board_segments set article_cnt= article_cnt-1 where segment_no = '".$row->segment_no."';");
$result = $mysqli->query("select * from board_segments where segment_no = '".$row->segment_no."';");
$row = $result->fetch_object();
if($row->article_cnt <= 0) {
$mysqli->query("delete from board_segments where segment_no = '".$row->segment_no."';");
}
$mysqli->query("UNLOCK TABLES");
}
}
print "=========================\n";
$end = get_time();
$time = $end - $start;
print "전체소요시간: ".$time."\n";
print "=========================\n";
}
'컴퓨터 사용 방법' 카테고리의 다른 글
BigInteger 자바스크립트(Javascript) 에서 아주 매우 큰수 처리하기 (0) | 2018.04.01 |
---|---|
파이썬(Python)으로 크롤링하는 매우 간단, 쉬운 방법 (0) | 2018.04.01 |
개선된 게시판 페이지 알고리즘 (0) | 2018.02.23 |
아무것도 모르는 상태에서 코딩(프로그래밍) 경험해보기 #6 - 화면에 글씨를 담은 박스 만들고 움직이게하기 (0) | 2018.02.14 |
아무것도 모르는 상태에서 코딩(프로그래밍) 경험해보기 #5 - 화면에 글씨를 담은 박스 만들기 (0) | 2018.02.13 |
파이썬으로 사진과 파일정리 하기 (0) | 2017.10.08 |
댓글