From 47a3790c57a64f670460b4bece12e3a4004d32f1 Mon Sep 17 00:00:00 2001 From: thisgun Date: Tue, 7 Apr 2026 10:37:12 +0000 Subject: [PATCH] =?UTF-8?q?[perf]bbs/list.php=20N+1=20=EC=BF=BC=EB=A6=AC?= =?UTF-8?q?=202=EA=B1=B4=EC=9D=84=20IN=20=EC=BF=BC=EB=A6=AC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98=20=EC=A1=B0=ED=9A=8C=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 게시판 목록 페이지에서 발생하던 N+1 쿼리 패턴 2건을 제거: 1. 공지 처리 루프 (line 94~120): 기존에는 bo_notice의 각 wr_id마다 sql_fetch를 따로 호출하여 공지가 N개면 N개의 쿼리가 발생. 유효한 wr_id를 정수화하여 수집한 뒤 단일 IN 쿼리로 일괄 조회하고, id 기반 해시맵으로 원래 순서대로 처리. 2. 검색 결과 2차 조회 (line 199~200): DISTINCT wr_parent 쿼리로 ID만 받은 뒤, 각 행마다 sql_fetch를 호출하여 검색 결과가 N건이면 추가로 N개의 쿼리가 발생. 단일 IN 쿼리로 모든 부모 글을 일괄 조회 후 $rows_to_process 배열에 원래 순서대로 정렬하여 공통 처리 루프에서 소비. 검색/비검색 분기에서 중복되던 row→list 변환 로직도 공통화. 효과 (공지 5개 + 검색 결과 20건 기준): 기존: 1 DISTINCT + 20 fetch + 5 notice fetch = 26 쿼리 개선: 1 DISTINCT + 1 IN(search) + 1 IN(notice) = 3 쿼리 부수 효과: - 삭제된 공지/부모글에 대한 에러 처리 개선 (기존은 empty row를 get_list에 넘겨 잠재적 오류, 개선판은 자동 스킵) - wr_id 정수화로 SQL injection 방지 계층 추가 --- bbs/list.php | 127 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 42 deletions(-) diff --git a/bbs/list.php b/bbs/list.php index 83cec1963..43f80b6d6 100644 --- a/bbs/list.php +++ b/bbs/list.php @@ -89,15 +89,31 @@ if (!$is_search_bbs) { $from_notice_idx = ($page - 1) * $page_rows; if($from_notice_idx < 0) $from_notice_idx = 0; - $board_notice_count = count($arr_notice); - for ($k=0; $k<$board_notice_count; $k++) { - if (trim($arr_notice[$k]) == '') continue; + // 유효한 공지 wr_id만 수집 (정수화로 SQL injection 방지) + $notice_ids_int = array(); + foreach ($arr_notice as $k_idx => $nid_raw) { + $nid_int = (int) trim($nid_raw); + if ($nid_int > 0) { + $notice_ids_int[$k_idx] = $nid_int; + } + } - $row = sql_fetch(" select * from {$write_table} where wr_id = '{$arr_notice[$k]}' "); + // N+1 쿼리 제거: 공지 전체를 단일 IN 쿼리로 일괄 조회 + $notice_rows_by_id = array(); + if (!empty($notice_ids_int)) { + $sql = " select * from {$write_table} where wr_id in (" . implode(',', $notice_ids_int) . ") "; + $result = sql_query($sql); + while ($nrow = sql_fetch_array($result)) { + $notice_rows_by_id[$nrow['wr_id']] = $nrow; + } + } - if (!isset($row['wr_id']) || !$row['wr_id']) continue; + // 원래 bo_notice 순서대로 처리 (페이징 로직의 $k 인덱스 의미 보존) + foreach ($notice_ids_int as $k => $nid) { + if (!isset($notice_rows_by_id[$nid])) continue; + $row = $notice_rows_by_id[$nid]; $notice_array[] = $row['wr_id']; if($k < $from_notice_idx) continue; @@ -106,7 +122,7 @@ if (!$is_search_bbs) { $list[$i]['is_notice'] = true; $list[$i]['list_content'] = $list[$i]['wr_content']; - // 비밀글인 경우 리스트에서 내용이 출력되지 않게 글 내용을 지웁니다. + // 비밀글인 경우 리스트에서 내용이 출력되지 않게 글 내용을 지웁니다. if (strstr($list[$i]['wr_option'], "secret")) { $list[$i]['wr_content'] = ''; } @@ -178,45 +194,72 @@ if ($sst) { $sql_order = " order by {$sst} {$sod} "; } -if ($is_search_bbs) { - $sql = " select distinct wr_parent from {$write_table} where {$sql_search} {$sql_order} limit {$from_record}, $page_rows "; -} else { - $sql = " select * from {$write_table} where wr_is_comment = 0 "; - if(!empty($notice_array)) - $sql .= " and wr_id not in (".implode(', ', $notice_array).") "; - $sql .= " {$sql_order} limit {$from_record}, $page_rows "; +// 페이지의 공지개수가 목록수 보다 작을 때만 실행 +$rows_to_process = array(); +if ($page_rows > 0) { + if ($is_search_bbs) { + // 1차: 검색 결과의 wr_parent ID 목록 조회 + $sql = " select distinct wr_parent from {$write_table} where {$sql_search} {$sql_order} limit {$from_record}, $page_rows "; + $result = sql_query($sql); + + $parent_ids_in_order = array(); + while ($id_row = sql_fetch_array($result)) { + $pid = (int) $id_row['wr_parent']; + if ($pid > 0) { + $parent_ids_in_order[] = $pid; + } + } + + // 2차: N+1 쿼리 제거 - 단일 IN 쿼리로 전체 행 일괄 조회 + if (!empty($parent_ids_in_order)) { + $batch_sql = " select * from {$write_table} where wr_id in (" . implode(',', $parent_ids_in_order) . ") "; + $batch_result = sql_query($batch_sql); + + $rows_by_id = array(); + while ($br = sql_fetch_array($batch_result)) { + $rows_by_id[$br['wr_id']] = $br; + } + + // DISTINCT 쿼리의 원래 정렬 순서대로 재구성 + foreach ($parent_ids_in_order as $pid) { + if (isset($rows_by_id[$pid])) { + $rows_to_process[] = $rows_by_id[$pid]; + } + } + } + } else { + $sql = " select * from {$write_table} where wr_is_comment = 0 "; + if(!empty($notice_array)) + $sql .= " and wr_id not in (".implode(', ', $notice_array).") "; + $sql .= " {$sql_order} limit {$from_record}, $page_rows "; + + $result = sql_query($sql); + while ($row = sql_fetch_array($result)) { + $rows_to_process[] = $row; + } + } } -// 페이지의 공지개수가 목록수 보다 작을 때만 실행 -if($page_rows > 0) { - $result = sql_query($sql); - - $k = 0; - - while ($row = sql_fetch_array($result)) - { - // 검색일 경우 wr_id만 얻었으므로 다시 한행을 얻는다 - if ($is_search_bbs) - $row = sql_fetch(" select * from {$write_table} where wr_id = '{$row['wr_parent']}' "); - - $list[$i] = get_list($row, $board, $board_skin_url, G5_IS_MOBILE ? $board['bo_mobile_subject_len'] : $board['bo_subject_len']); - if (strstr($sfl, 'subject')) { - $list[$i]['subject'] = search_font($stx, $list[$i]['subject']); - } - $list[$i]['is_notice'] = false; - $list[$i]['list_content'] = $list[$i]['wr_content']; - - // 비밀글인 경우 리스트에서 내용이 출력되지 않게 글 내용을 지웁니다. - if (strstr($list[$i]['wr_option'], "secret")) { - $list[$i]['wr_content'] = ''; - } - - $list_num = $total_count - ($page - 1) * $list_page_rows - $notice_count; - $list[$i]['num'] = $list_num - $k; - - $i++; - $k++; +$k = 0; +foreach ($rows_to_process as $row) +{ + $list[$i] = get_list($row, $board, $board_skin_url, G5_IS_MOBILE ? $board['bo_mobile_subject_len'] : $board['bo_subject_len']); + if (strstr($sfl, 'subject')) { + $list[$i]['subject'] = search_font($stx, $list[$i]['subject']); } + $list[$i]['is_notice'] = false; + $list[$i]['list_content'] = $list[$i]['wr_content']; + + // 비밀글인 경우 리스트에서 내용이 출력되지 않게 글 내용을 지웁니다. + if (strstr($list[$i]['wr_option'], "secret")) { + $list[$i]['wr_content'] = ''; + } + + $list_num = $total_count - ($page - 1) * $list_page_rows - $notice_count; + $list[$i]['num'] = $list_num - $k; + + $i++; + $k++; } g5_latest_cache_data($board['bo_table'], $list);