跳到主要內容

客戶服務

Vertica 健康檢查

在您的 Vertica 環境執行一支資料蒐集腳本,匯出 40 份 CSV,由艾維智據此產出完整的健康檢查報告。 腳本只讀取系統檢視表,不會修改任何資料;執行時間通常在一到數分鐘內。

  1. STEP 1

    取得腳本並執行

    以 dbadmin 身分在資料庫主機執行,逐項匯出系統檢視表資料。

  2. STEP 2

    匯出 CSV

    產生 40 份 CSV 與執行記錄,自動打包成單一 tar.gz。

  3. STEP 3

    交付檢查報告

    艾維智依資料撰寫檢查報告,含效能分析與改善建議。

01 執行步驟

腳本只使用 v_* 系統檢視表與 vsql, 不需安裝額外套件,也不會寫入資料庫。建議以 dbadmin 身分執行, 其他帳號需具備 v_monitor、v_catalog 檢視表的讀取權限。

步驟 1 建立腳本檔

在資料庫主機上建立 V_HCheck.sh:先到本頁 04 完整腳本按「複製」, 再用編輯器貼上存檔,最後確認語法無誤。

bash
# 1. 建立檔案:貼上「04 完整腳本」的內容後存檔
vi V_HCheck.sh

# 2. 加上執行權限
chmod +x V_HCheck.sh

# 3. 檢查語法(沒有輸出代表正確)
bash -n V_HCheck.sh

步驟 2 執行腳本

建議以 dbadmin 身分執行;密碼請用檔案傳入,不要留在指令列。

bash
# 1. 以互動方式輸入密碼寫入暫存檔(不會留在指令列或 history)
umask 077
read -rsp '密碼: ' VHPASS && printf '%s' "$VHPASS" > ./pw.txt && unset VHPASS

# 2. 執行(-h 主機、-d 資料庫名稱請換成實際值)
./V_HCheck.sh -h 10.1.2.3 -p 5433 -U dbadmin -d YOUR_DB --pwfile ./pw.txt -D 30

# 3. 執行結束後刪除暫存檔
rm -f ./pw.txt

步驟 3 回傳打包檔

腳本結束時會在輸出目錄的上一層產生 VCheck_<host>_<日期>.tar.gz, 將該檔回傳即可,內容已包含全部 CSV 與執行記錄。

bash
ls -lh VCheck_*.tar.gz
# 例:VCheck_10.1.2.3_20261122_1130.tar.gz
# 將此檔以安全的方式(郵件、SFTP 或我們提供的上傳連結)回傳即可

02 參數說明

參數說明預設值
-h, --hostVertica 節點 IP 或主機名稱必填
-p, --portvsql 連接埠5433
-U, --user連線使用者,建議 dbadmindbadmin
-d, --database資料庫名稱必填
-w, --password密碼直接寫在指令列(會留在 shell history,不建議)—
--pwfile從檔案第一行讀取密碼(建議做法)—
-D, --days觀察窗口天數,需在 Data Collector 保留期內30
-o, --outdir輸出目錄./VCheck_<host>_<YYYYmmdd_HHMM>
--ssl連線啟用 SSL(等同 -s require)關閉
-V / -H顯示版本 / 顯示說明—

03 輸出檔案

每一項檢查各自輸出一個 CSV,單一查詢失敗不會中斷其他項目;失敗原因寫在 _errors.log,並在 _check_status.csv 標記。

text
VCheck_10.1.2.3_20261122_1130/
├── 00_run_meta.txt          執行環境與連線參數(不含密碼)
├── 00_view_columns.csv      系統檢視表欄位清單(版本差異比對用)
├── 01_version.csv
├── 02_nodes.csv
├── ...                      共 40 份 CSV
├── 37_config_params.csv
├── _check_status.csv        每項檢查的狀態與筆數
├── _csv_integrity.csv       欄位數一致性檢查
└── _errors.log              失敗項目的錯誤訊息

VCheck_10.1.2.3_20261122_1130.tar.gz   ← 回傳這個檔案
檔案用途
00_run_meta.txt執行環境、連線參數、開始與結束時間、成功與失敗項數(不含密碼)
_check_status.csv每一項檢查的狀態、資料筆數、耗時與異常列數
_csv_integrity.csvCSV 欄位數一致性檢查,用於確認匯出資料沒有被換行或逗號破壞
_errors.log失敗項目的資料庫錯誤訊息,便於判斷是權限或版本差異

04 完整腳本

版本 2.0、728 行。可直接複製後貼到主機上使用; 腳本不會寫入或修改資料庫內容。

版本 v2.0行數 728大小 27.5 KB更新 2026-11-10
V_HCheck.sh
#!/usr/bin/env bash
# ==============================================================================
#  V_HCheck.sh  --  Vertica Health Check Data Collector
#
#  用途:在 Vertica 環境以 dbadmin 執行,匯出一組 CSV,供製作「Vertica 檢查報告」。
#  版本:2.0  (2026-11)
#  相容:Vertica 10.x / 11.x / 12.x / 23.x / 24.x / 25.x
#  需求:bash >= 3.2、vsql(client 即可)、awk、tar
#
#  範例:
#    ./V_HCheck.sh -h 10.1.2.3 -p 5433 -U dbadmin -d ACHI_DB
#    ./V_HCheck.sh -h 10.1.2.3 -d ACHI_DB -w 'MyPass' -D 30 -o ./out_20261122
#    ./V_HCheck.sh -d ACHI_DB --pwfile ~/.vcheck.pw
#
#  說明:
#    * 每一項檢查獨立執行;單一查詢失敗(例如該版本沒有某個 view)不會中斷,
#      失敗原因會寫進 _errors.log,最後在 _check_status.csv 列出。
#    * 建議一律使用 dbadmin 執行,否則部分 v_monitor 檢視表會因權限不足而空白。
#    * 執行完會自動打包成 VCheck_<host>_<日期>.tar.gz,把該檔回傳即可。
# ==============================================================================

set -u

SCRIPT_VER="2.0"
SELF="$(basename "$0")"

# ---------------------------- 預設參數 ---------------------------------------
HOST=""
PORT="5433"
DBUSER="dbadmin"
DB=""
PASS=""
PASS_SRC=""
PWFILE=""
DAYS="30"
OUTDIR=""
SSL=""

usage() {
cat <<EOF
用法: $SELF -h <host> [-p port] [-U user] -d <database> [-w 密碼 | --pwfile 檔] [-D 天數] [-o 目錄]

  -h, --host        Vertica 節點 IP / 主機名(必填)
  -p, --port        vsql 連接埠,預設 5433
  -U, --user        使用者,預設 dbadmin(請一律使用 dbadmin)
  -d, --database    資料庫名稱(必填)
  -w, --password    密碼;未提供則互動輸入(建議用互動輸入或 --pwfile)
      --pwfile      從檔案第一行讀取密碼
  -D, --days        觀察窗口天數,預設 30
  -o, --outdir      輸出目錄,預設 ./VCheck_<host>_<YYYYmmdd_HHMM>
      --ssl         連線啟用 SSL(等同 -s require)
  -V, --version     顯示版本
  -H, --help        顯示說明

範例:
  $SELF -h 10.1.2.3 -d ACHI_DB
  $SELF -h 10.1.2.3 -d ACHI_DB --pwfile ./pw.txt -D 30 -o ./out
EOF
}

# ---------------------------- 參數解析 ---------------------------------------
while [ $# -gt 0 ]; do
  case "$1" in
    -h|--host)     HOST="$2"; shift 2 ;;
    -p|--port)     PORT="$2"; shift 2 ;;
    -U|--user)     DBUSER="$2"; shift 2 ;;
    -d|--database) DB="$2"; shift 2 ;;
    -w|--password) PASS="$2"; PASS_SRC="command line (-w)"; shift 2 ;;
    --pwfile)      PWFILE="$2"; shift 2 ;;
    -D|--days)     DAYS="$2"; shift 2 ;;
    -o|--outdir)   OUTDIR="$2"; shift 2 ;;
    --ssl)         SSL="-s require"; shift ;;
    -V|--version)  echo "$SELF version $SCRIPT_VER"; exit 0 ;;
    -H|--help)     usage; exit 0 ;;
    *) echo "未知參數: $1" >&2; usage; exit 2 ;;
  esac
done

if [ -z "$HOST" ] || [ -z "$DB" ]; then
  echo "錯誤:-h/-d 為必填" >&2; usage; exit 2
fi

case "$DAYS" in
  ''|*[!0-9]*) echo "錯誤:-D 必須是整數天數" >&2; exit 2 ;;
esac

# 密碼來源
if [ -n "$PWFILE" ]; then
  if [ ! -r "$PWFILE" ]; then echo "錯誤:無法讀取 $PWFILE" >&2; exit 2; fi
  PASS="$(head -n 1 "$PWFILE")"
  PASS_SRC="pwfile ($PWFILE)"
elif [ -z "$PASS" ]; then
  if [ -n "${VCHECK_PASSWORD:-}" ]; then
    PASS="$VCHECK_PASSWORD"; PASS_SRC="環境變數 VCHECK_PASSWORD"
  else
    printf 'Vertica 密碼 (%s@%s): ' "$DBUSER" "$HOST" >&2
    stty -echo 2>/dev/null
    read -r PASS
    stty echo 2>/dev/null
    printf '\n' >&2
    PASS_SRC="interactive"
  fi
fi

if [ -z "$OUTDIR" ]; then
  OUTDIR="./VCheck_${HOST}_$(date +%Y%m%d_%H%M)"
fi
mkdir -p "$OUTDIR" || { echo "錯誤:無法建立輸出目錄 $OUTDIR" >&2; exit 1; }

# 前置檢查:找出 vsql(PATH 沒有時退回 /opt/vertica/bin)
if [ -n "${VSQL:-}" ] && [ -x "${VSQL}" ]; then
  VSQL_BIN="$VSQL"
elif command -v vsql >/dev/null 2>&1; then
  VSQL_BIN="$(command -v vsql)"
elif [ -x /opt/vertica/bin/vsql ]; then
  VSQL_BIN="/opt/vertica/bin/vsql"
else
  echo "錯誤:找不到 vsql。請確認已安裝 Vertica client 並在 PATH 中(或設定 VSQL 環境變數指向 vsql 路徑)。" >&2
  exit 1
fi

LOG="$OUTDIR/_errors.log"
STATUS="$OUTDIR/_check_status.csv"
META="$OUTDIR/00_run_meta.txt"

: > "$LOG"
printf 'file,status,rows,elapsed_sec,note,bad_rows\n' > "$STATUS"
: > "$OUTDIR/.integrity.tmp"

# ---------------------------- 連線參數 ---------------------------------------
CONN=(-h "$HOST" -p "$PORT" -U "$DBUSER" -d "$DB")
[ -n "$PASS" ] && CONN+=(-w "$PASS")
[ -n "$SSL" ]  && CONN+=($SSL)

VSQL_OPTS=(-A -F "$(printf '\001')" -q -X -P footer=off)

OK_CNT=0
FAIL_CNT=0
FAIL_LIST=""
BAD_LIST=""
START_TS="$(date '+%Y-%m-%d %H:%M:%S')"
START_EPOCH="$(date +%s)"

# 文字欄位淨化:把換行換成空白(避免單筆資料被拆成多列)。
# 逗號不需要處理:vsql 以 \001 分隔輸出,再由 awk 轉成標準 CSV(含引號轉義)。
# 用法: $(sancol message)
sancol() {
  printf "REPLACE(REPLACE(COALESCE(%s::VARCHAR,''), CHR(10), ' '), CHR(13), ' ')" "$1"
}

# ---------------------------- 核心:執行單一檢查 -----------------------------
# run_check <輸出檔名(不含.csv)> <說明> [oneline]   ← SQL 由 stdin 傳入
#   oneline:適用回傳多行文字的函式(如 GET_COMPLIANCE_STATUS),合併為單列
run_check() {
  local name="$1"; local title="$2"; local mode="${3:-}"
  local f="$OUTDIR/$name.csv"
  local raw="$OUTDIR/.raw.$name"
  local t0 t1 rows rc bad hdr

  t0="$(date +%s)"
  printf '\n---- [%s] %s\n' "$name" "$title" >> "$LOG"

  "$VSQL_BIN" "${CONN[@]}" "${VSQL_OPTS[@]}" -o "$raw" 2>> "$LOG"
  rc=$?

  if [ -f "$raw" ]; then
    # 1) 去掉 vsql footer、空行   2) 檢查每列欄位數(\001 分隔,值內不會出現)
    read -r rows bad hdr <<EOF
$(awk 'BEGIN{FS="\001"}
       /^\([0-9]+ rows?\)$/{next}
       NF==0{next}
       { if(!seen){h=NF; seen=1; rows=1; next }
         rows++; if (NF!=h) b++ }
       END{ printf "%d %d %d", rows+0, b+0, h+0 }' "$raw")
EOF

    if [ "$mode" = "oneline" ]; then
      awk 'BEGIN{FS="\001"}
           /^\([0-9]+ rows?\)$/{next} NF==0{next}
           { if(!seen){print; seen=1; next}
             printf "%s | ", $0 }' "$raw" > "$f.ol"
      # 收尾:補換行
      { cat "$f.ol"; echo; } > "$f"; rm -f "$f.ol"
      rows=1; bad=0
    else
      # \001 → RFC4180 CSV:內含逗號/引號的欄位加引號並將 " 覆寫為 ""
      awk 'BEGIN{FS="\001"; OFS=","}
           /^\([0-9]+ rows?\)$/{next} NF==0{next}
           { out=""
             for(i=1;i<=NF;i++){ v=$i
               if (v ~ /[,"]/) { gsub(/"/,"\"\"",v); v="\"" v "\"" }
               out = out (i>1 ? OFS : "") v }
             print out }' "$raw" > "$f"
    fi
    rm -f "$raw"
  else
    : > "$f"; rows=0; bad=0
  fi

  t1="$(date +%s)"
  if [ $rc -eq 0 ] && [ -s "$f" ]; then
    [ "$rows" -lt 0 ] && rows=0
    OK_CNT=$((OK_CNT+1))
    printf '%s,OK,%s,%s,,%s\n' "$name" "$rows" "$((t1-t0))" "$bad" >> "$STATUS"
    printf '  [ OK ] %-38s %8s rows %3ss  cols=%s%s\n' "$name" "$rows" "$((t1-t0))" "$hdr" \
           "$( [ "${bad:-0}" -gt 0 ] && echo "  ⚠ 欄位數異常 ${bad} 列" )"
    [ "${bad:-0}" -gt 0 ] && { BAD_LIST="$BAD_LIST $name"; echo "$name,$hdr,$rows,$bad" >> "$OUTDIR/.integrity.tmp"; }
  else
    # 失敗:清空輸出,避免半截資料被誤用;原因見 _errors.log
    : > "$f"
    FAIL_CNT=$((FAIL_CNT+1))
    FAIL_LIST="$FAIL_LIST $name"
    printf '%s,FAIL,0,%s,see _errors.log,0\n' "$name" "$((t1-t0))" >> "$STATUS"
    printf '  [FAIL] %-38s %8s       %ss  (見 _errors.log)\n' "$name" "-" "$((t1-t0))"
  fi
}

# ---------------------------- 執行環境紀錄 -----------------------------------
{
  echo "=== Vertica Health Check run meta ==="
  echo "script_version : $SCRIPT_VER"
  echo "run_at         : $START_TS"
  echo "run_by_host    : $(hostname 2>/dev/null)"
  echo "run_by_user    : $(whoami 2>/dev/null)"
  echo "target_host    : $HOST:$PORT"
  echo "database       : $DB"
  echo "db_user        : $DBUSER"
  echo "password_src   : ${PASS_SRC:-none}"
  echo "window_days    : $DAYS"
  echo "outdir         : $(cd "$OUTDIR" && pwd)"
  echo "vsql_version   : $("$VSQL_BIN" --version 2>&1 | head -n 1)"
  echo "os             : $(uname -srm 2>/dev/null)"
} > "$META"

echo "=============================================================="
echo " Vertica Health Check  v$SCRIPT_VER"
echo " 目標: $DBUSER@$HOST:$PORT / DB=$DB   窗口: 最近 $DAYS 天"
echo " 輸出: $(cd "$OUTDIR" && pwd)"
echo "=============================================================="

# ==============================================================================
# 00. Pre-flight:關鍵檢視表欄位清單(若後續查詢失敗,可從此檔查出正確欄名)
# ==============================================================================
# 系統檢視表不會出現在 v_catalog.view_columns,改以 SELECT * ... LIMIT 0 取得實際欄名。
# 用途:若後續某項檢查失敗,從這個檔就能查出該版本的正確欄位名。
VIEW_LIST="v_catalog.nodes v_catalog.tables v_catalog.projection_columns v_catalog.projections
v_catalog.license_audits v_catalog.constraint_columns v_monitor.system v_monitor.disk_storage
v_monitor.cpu_usage v_monitor.memory_usage v_monitor.io_usage v_monitor.network_usage
v_monitor.query_requests v_monitor.query_events v_monitor.error_messages
v_monitor.resource_rejections v_monitor.resource_rejection_details
v_monitor.resource_pool_status v_monitor.projection_refreshes v_monitor.projection_storage
v_monitor.delete_vectors v_monitor.partition_status v_monitor.storage_containers
v_monitor.tuning_recommendations v_monitor.configuration_parameters"

{
  printf 'view_name,columns\n'
  for v in $VIEW_LIST; do
    cols="$("$VSQL_BIN" "${CONN[@]}" -A -F '|' -q -X -P footer=off \
              -c "SELECT * FROM $v LIMIT 0" 2>> "$LOG" | head -n 1)"
    if [ -n "$cols" ]; then
      printf '%s,"%s"\n' "$v" "$cols"
    else
      printf '%s,"<<NOT FOUND OR NO PERMISSION>>"\n' "$v"
    fi
  done
} > "$OUTDIR/00_view_columns.csv"
printf '00_view_columns,OK,%s,0,,\n' "$(($(wc -l < "$OUTDIR/00_view_columns.csv") - 1))" >> "$STATUS"
printf '  [ OK ] %-38s %8s rows\n' "00_view_columns" "$(($(wc -l < "$OUTDIR/00_view_columns.csv") - 1))"
OK_CNT=$((OK_CNT+1))

# ==============================================================================
# A. 版本與授權
# ==============================================================================
run_check 01_version "A1 版本資訊" <<SQL
SELECT version() AS vertica_version;
SQL

run_check 02_nodes "A1/B1 節點清單與狀態" <<SQL
SELECT * FROM v_catalog.nodes ORDER BY node_name;
SQL

run_check 03_compliance "A2 授權合規狀態 (GET_COMPLIANCE_STATUS)" oneline <<SQL
SELECT GET_COMPLIANCE_STATUS() AS compliance_status;
SQL

run_check 04_license_audits "A3 授權用量歷史 (license_audits)" <<SQL
SELECT * FROM v_catalog.license_audits ORDER BY audit_start_timestamp;
SQL

# ==============================================================================
# B. 叢集健康度
# ==============================================================================
run_check 05_ksafety_epoch "B2 K-Safety 與 epoch (v_monitor.system)" <<SQL
SELECT * FROM v_monitor.system;
SQL

run_check 06_disk_storage "B3 各節點儲存空間 (disk_storage)" <<SQL
SELECT * FROM v_monitor.disk_storage ORDER BY node_name, storage_path;
SQL

# ==============================================================================
# C. 主機資源(時序 + 聚合)
# ==============================================================================
run_check 07_cpu_timeseries "C1 CPU 時序" <<SQL
SELECT node_name, end_time, average_cpu_usage_percent
FROM   v_monitor.cpu_usage
WHERE  end_time >= now() - INTERVAL '${DAYS} days'
ORDER  BY node_name, end_time;
SQL

run_check 08_cpu_summary "C1 CPU 彙總 (min/avg/p95/max)" <<SQL
WITH base AS (
  SELECT node_name,
         average_cpu_usage_percent AS v,
         PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY average_cpu_usage_percent)
           OVER (PARTITION BY node_name) AS p95
  FROM   v_monitor.cpu_usage
  WHERE  end_time >= now() - INTERVAL '${DAYS} days'
)
SELECT node_name,
       COUNT(*)              AS samples,
       ROUND(MIN(v),2)       AS min_cpu_pct,
       ROUND(AVG(v),2)       AS avg_cpu_pct,
       ROUND(MAX(p95),2)     AS p95_cpu_pct,
       ROUND(MAX(v),2)       AS max_cpu_pct
FROM   base
GROUP  BY node_name
ORDER  BY node_name;
SQL

run_check 09_memory_timeseries "C2 Memory 時序" <<SQL
SELECT node_name, end_time, average_memory_usage_percent
FROM   v_monitor.memory_usage
WHERE  end_time >= now() - INTERVAL '${DAYS} days'
ORDER  BY node_name, end_time;
SQL

run_check 10_memory_summary "C2 Memory 彙總 (min/avg/p95/max)" <<SQL
WITH base AS (
  SELECT node_name,
         average_memory_usage_percent AS v,
         PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY average_memory_usage_percent)
           OVER (PARTITION BY node_name) AS p95
  FROM   v_monitor.memory_usage
  WHERE  end_time >= now() - INTERVAL '${DAYS} days'
)
SELECT node_name,
       COUNT(*)           AS samples,
       ROUND(MIN(v),2)    AS min_mem_pct,
       ROUND(AVG(v),2)    AS avg_mem_pct,
       ROUND(MAX(p95),2)  AS p95_mem_pct,
       ROUND(MAX(v),2)    AS max_mem_pct
FROM   base
GROUP  BY node_name
ORDER  BY node_name;
SQL

run_check 11_io_timeseries "C3 磁碟 I/O 時序" <<SQL
SELECT node_name, end_time, read_kbytes_per_sec, written_kbytes_per_sec
FROM   v_monitor.io_usage
WHERE  end_time >= now() - INTERVAL '${DAYS} days'
ORDER  BY node_name, end_time;
SQL

run_check 12_io_summary "C3 磁碟 I/O 彙總" <<SQL
SELECT node_name,
       ROUND(MIN(read_kbytes_per_sec),2)    AS min_read_kbps,
       ROUND(AVG(read_kbytes_per_sec),2)    AS avg_read_kbps,
       ROUND(MAX(read_kbytes_per_sec),2)    AS max_read_kbps,
       ROUND(MIN(written_kbytes_per_sec),2) AS min_written_kbps,
       ROUND(AVG(written_kbytes_per_sec),2) AS avg_written_kbps,
       ROUND(MAX(written_kbytes_per_sec),2) AS max_written_kbps
FROM   v_monitor.io_usage
WHERE  end_time >= now() - INTERVAL '${DAYS} days'
GROUP  BY node_name
ORDER  BY node_name;
SQL

run_check 13_network_timeseries "C4 網路 I/O 時序" <<SQL
SELECT node_name, end_time, tx_kbytes_per_sec, rx_kbytes_per_sec
FROM   v_monitor.network_usage
WHERE  end_time >= now() - INTERVAL '${DAYS} days'
ORDER  BY node_name, end_time;
SQL

run_check 14_network_summary "C4 網路 I/O 彙總" <<SQL
SELECT node_name,
       ROUND(MIN(tx_kbytes_per_sec),2) AS min_tx_kbps,
       ROUND(AVG(tx_kbytes_per_sec),2) AS avg_tx_kbps,
       ROUND(MAX(tx_kbytes_per_sec),2) AS max_tx_kbps,
       ROUND(MIN(rx_kbytes_per_sec),2) AS min_rx_kbps,
       ROUND(AVG(rx_kbytes_per_sec),2) AS avg_rx_kbps,
       ROUND(MAX(rx_kbytes_per_sec),2) AS max_rx_kbps
FROM   v_monitor.network_usage
WHERE  end_time >= now() - INTERVAL '${DAYS} days'
GROUP  BY node_name
ORDER  BY node_name;
SQL

# ==============================================================================
# D. 資源池與拒絕
# ==============================================================================
run_check 15_resource_pool_status "D1 資源池狀態 (resource_pool_status)" <<SQL
SELECT * FROM v_monitor.resource_pool_status
ORDER  BY node_name, pool_name;
SQL

run_check 16_resource_rejections "D2 資源拒絕彙總 (resource_rejections)" <<SQL
SELECT * FROM v_monitor.resource_rejections
WHERE  last_rejected_timestamp >= now() - INTERVAL '${DAYS} days'
ORDER  BY last_rejected_timestamp DESC;
SQL

run_check 17_resource_rejection_details "D3 資源拒絕明細 (resource_rejection_details)" <<SQL
SELECT * FROM v_monitor.resource_rejection_details;
SQL

run_check 18_top_memory_queries "D4 最耗記憶體查詢 Top 200" <<SQL
SELECT node_name, user_name, session_id, request_id, request_type,
       request_duration_ms,
       memory_acquired_mb,
       start_timestamp, end_timestamp,
       $(sancol "LEFT(request,300)") AS query_text
FROM   v_monitor.query_requests
WHERE  start_timestamp >= now() - INTERVAL '${DAYS} days'
  AND  memory_acquired_mb IS NOT NULL
ORDER  BY memory_acquired_mb DESC
LIMIT  200;
SQL

run_check 19_rejection_window_queries "D4 資源拒絕時段前後 15 分鐘的查詢" <<SQL
SELECT q.node_name, r.pool_name, r.last_rejected_timestamp,
       q.user_name, q.session_id, q.request_id,
       q.request_duration_ms, q.memory_acquired_mb, q.start_timestamp,
       $(sancol "LEFT(q.request,300)") AS query_text
FROM   v_monitor.query_requests q
JOIN   v_monitor.resource_rejections r
       ON r.node_name = q.node_name
WHERE  ABS(DATEDIFF('minute', r.last_rejected_timestamp, q.start_timestamp)) <= 15
ORDER  BY q.memory_acquired_mb DESC
LIMIT  100;
SQL

# ==============================================================================
# E. 查詢效能
# ==============================================================================
run_check 20_query_duration_buckets "E1 查詢回應時間分布" <<SQL
SELECT duration_bucket,
       COUNT(*) AS query_count,
       ROUND(CAST(100.0 * COUNT(*) / SUM(COUNT(*)) OVER () AS FLOAT), 2) AS pct
FROM (
  SELECT CASE WHEN request_duration_ms < 1000 THEN '1) < 1 sec'
              WHEN request_duration_ms < 2000 THEN '2) 1 - 2 sec'
              WHEN request_duration_ms < 3000 THEN '3) 2 - 3 sec'
              WHEN request_duration_ms < 4000 THEN '4) 3 - 4 sec'
              WHEN request_duration_ms < 5000 THEN '5) 4 - 5 sec'
              ELSE '6) > 5 sec' END AS duration_bucket
  FROM   v_monitor.query_requests
  WHERE  start_timestamp >= now() - INTERVAL '${DAYS} days'
    AND  request_duration_ms IS NOT NULL
) b
GROUP  BY duration_bucket
ORDER  BY duration_bucket;
SQL

run_check 20b_query_window "E1 查詢資料涵蓋範圍(實際可取得的起訖時間)" <<SQL
SELECT MIN(start_timestamp) AS first_query_ts,
       MAX(start_timestamp) AS last_query_ts,
       COUNT(*)             AS query_cnt,
       MIN(request_duration_ms) AS min_ms,
       MAX(request_duration_ms) AS max_ms
FROM   v_monitor.query_requests
WHERE  start_timestamp >= now() - INTERVAL '${DAYS} days';
SQL

run_check 21_top_slow_queries "E2 最慢查詢 Top 500" <<SQL
SELECT node_name, user_name, session_id, request_id, request_type,
       request_duration_ms, memory_acquired_mb,
       start_timestamp, end_timestamp, error_count,
       $(sancol "LEFT(request,500)") AS query_text
FROM   v_monitor.query_requests
WHERE  start_timestamp >= now() - INTERVAL '${DAYS} days'
  AND  request_duration_ms IS NOT NULL
ORDER  BY request_duration_ms DESC
LIMIT  500;
SQL

run_check 22_failed_queries "E3 失敗查詢明細" <<SQL
SELECT node_name, user_name, session_id, request_id, request_type,
       request_duration_ms, memory_acquired_mb, error_count,
       start_timestamp, $(sancol "LEFT(request,500)") AS query_text
FROM   v_monitor.query_requests
WHERE  start_timestamp >= now() - INTERVAL '${DAYS} days'
  AND  NOT success
ORDER  BY start_timestamp DESC
LIMIT  2000;
SQL

run_check 23_query_events_summary "E4 查詢事件統計 (event_type)" <<SQL
SELECT event_type, COUNT(*) AS event_count
FROM   v_monitor.query_events
WHERE  event_timestamp >= now() - INTERVAL '${DAYS} days'
GROUP  BY event_type
ORDER  BY event_count DESC;
SQL

run_check 24_query_events_detail "E4 查詢事件明細" <<SQL
SELECT event_timestamp, node_name, session_id, transaction_id,
       event_type, $(sancol "LEFT(event_description,300)") AS event_description
FROM   v_monitor.query_events
WHERE  event_timestamp >= now() - INTERVAL '${DAYS} days'
ORDER  BY event_timestamp DESC
LIMIT  50000;
SQL

run_check 25_query_load_by_user "E5 各使用者/請求型態的負載" <<SQL
SELECT user_name, request_type,
       COUNT(*)                                   AS request_cnt,
       ROUND(SUM(request_duration_ms)/1000.0, 1)  AS total_sec,
       ROUND(AVG(request_duration_ms),1)          AS avg_ms,
       ROUND(MAX(memory_acquired_mb),1)           AS max_mem_mb
FROM   v_monitor.query_requests
WHERE  start_timestamp >= now() - INTERVAL '${DAYS} days'
GROUP  BY user_name, request_type
ORDER  BY request_cnt DESC;
SQL

# ==============================================================================
# F. 錯誤訊息
# ==============================================================================
run_check 26_error_severity "F1 錯誤訊息依嚴重度統計" <<SQL
SELECT error_level,
       COUNT(*)                     AS msg_cnt,
       COUNT(DISTINCT node_name)    AS node_cnt,
       MIN(event_timestamp)         AS first_ts,
       MAX(event_timestamp)         AS last_ts
FROM   v_monitor.error_messages
WHERE  event_timestamp >= now() - INTERVAL '${DAYS} days'
GROUP  BY error_level
ORDER  BY msg_cnt DESC;
SQL

run_check 27_error_message_groups "F2 錯誤訊息正規化分群 Top 100" <<SQL
SELECT message_pattern,
       COUNT(*)                  AS msg_cnt,
       COUNT(DISTINCT node_name) AS node_cnt,
       MIN(event_timestamp)      AS first_ts,
       MAX(event_timestamp)      AS last_ts
FROM (
  SELECT node_name, event_timestamp,
         LEFT(
           REGEXP_REPLACE(
             REGEXP_REPLACE(
               REGEXP_REPLACE(
                 REPLACE(REPLACE(LEFT(message, 200), CHR(10), ' '), CHR(13), ' '),
               '[A-Za-z0-9_]{8}-[A-Za-z0-9_]{4}-[A-Za-z0-9_]{4}-[A-Za-z0-9_]{4}-[A-Za-z0-9_]{12}', '<GUID>'),
             '''[^'']*''', '''?'''),
           '[0-9]+', '#'),
         180) AS message_pattern
  FROM   v_monitor.error_messages
  WHERE  event_timestamp >= now() - INTERVAL '${DAYS} days'
    AND  error_level IN ('ERROR','FATAL','PANIC')
    AND  message NOT LIKE 'A Mergeout operation is already in progress on%'
    AND  message NOT LIKE 'No projections found%'
) t
GROUP  BY message_pattern
ORDER  BY msg_cnt DESC
LIMIT  100;
SQL

run_check 28_error_objects "F3 錯誤訊息關聯物件 (schema.table)" <<SQL
SELECT REGEXP_SUBSTR(message, '[A-Za-z0-9_]+[.][A-Za-z0-9_]+') AS object_name,
       COUNT(*)             AS msg_cnt,
       MIN(event_timestamp) AS first_ts,
       MAX(event_timestamp) AS last_ts
FROM   v_monitor.error_messages
WHERE  event_timestamp >= now() - INTERVAL '${DAYS} days'
  AND  error_level IN ('ERROR','FATAL')
  AND  REGEXP_SUBSTR(message, '[A-Za-z0-9_]+[.][A-Za-z0-9_]+') IS NOT NULL
GROUP  BY object_name
ORDER  BY msg_cnt DESC
LIMIT  100;
SQL

# ==============================================================================
# G. 資料庫物件維護
# ==============================================================================
run_check 29_tables_without_pk "G1 無主鍵的表(依 schema 統計 + 總計)" <<SQL
WITH nopk AS (
  SELECT DISTINCT t.table_id, t.table_schema
  FROM   v_catalog.tables t
  WHERE  t.is_system_table = 'f'
    AND  NOT EXISTS (
           SELECT 1 FROM v_catalog.constraint_columns cc
           WHERE  cc.table_id = t.table_id
             AND  cc.constraint_type = 'p')
)
SELECT table_schema, COUNT(*) AS table_cnt
FROM   nopk GROUP BY table_schema
UNION ALL
SELECT '*** TOTAL ***' AS table_schema, COUNT(*) AS table_cnt FROM nopk
ORDER  BY 1;
SQL

run_check 30_projections_last_refresh "G2 逾 3 個月未 refresh 的投影" <<SQL
SELECT p.projection_schema,
       p.projection_name,
       p.anchor_table_name,
       p.is_up_to_date,
       p.has_statistics,
       MAX(pr.refresh_start) AS last_refresh_start
FROM   v_catalog.projections p
LEFT   JOIN v_monitor.projection_refreshes pr
       ON pr.projection_id = p.projection_id
GROUP  BY p.projection_schema, p.projection_name, p.anchor_table_name,
          p.is_up_to_date, p.has_statistics
HAVING MAX(pr.refresh_start) IS NULL
    OR MAX(pr.refresh_start) < now() - INTERVAL '3 months'
ORDER  BY CASE WHEN last_refresh_start IS NULL THEN 0 ELSE 1 END, last_refresh_start;
SQL

run_check 31_projection_stats_gaps "G3 統計資料 NONE/ROWCOUNT/逾期 的投影欄位" <<SQL
SELECT p.projection_schema, p.projection_name, p.anchor_table_name,
       pc.statistics_type,
       COUNT(*) AS column_cnt,
       SUM(CASE WHEN pc.statistics_updated_timestamp IS NULL
                  OR pc.statistics_updated_timestamp < now() - INTERVAL '${DAYS} days'
                THEN 1 ELSE 0 END) AS stale_column_cnt
FROM   v_catalog.projection_columns pc
JOIN   v_catalog.projections p ON p.projection_id = pc.projection_id
WHERE  pc.statistics_type IN ('NONE','ROWCOUNT')
GROUP  BY p.projection_schema, p.projection_name, p.anchor_table_name, pc.statistics_type
ORDER  BY stale_column_cnt DESC, column_cnt DESC
LIMIT  1000;
SQL

run_check 31b_projection_uptodate "G4 投影 up-to-date 狀態統計" <<SQL
SELECT is_up_to_date, is_segmented, COUNT(*) AS projection_cnt
FROM   v_catalog.projections
GROUP  BY is_up_to_date, is_segmented
ORDER  BY projection_cnt DESC;
SQL

run_check 32_delete_vectors "G5 未 purge 的 delete vector" <<SQL
SELECT * FROM v_monitor.delete_vectors;
SQL

run_check 33_projection_storage "G6 投影儲存空間" <<SQL
SELECT * FROM v_monitor.projection_storage;
SQL

run_check 34_partition_status "G7 分割區狀態" <<SQL
SELECT * FROM v_monitor.partition_status;
SQL

run_check 35_storage_containers "G7 ROS 容器分布" <<SQL
SELECT * FROM v_monitor.storage_containers;
SQL

run_check 36_tuning_recommendations "G8 調校建議(含 resegment)" <<SQL
SELECT * FROM v_monitor.tuning_recommendations
ORDER  BY tuning_cost DESC;
SQL

# ==============================================================================
# H. 環境快照
# ==============================================================================
run_check 37_config_params "H1 關鍵設定參數" <<SQL
SELECT node_name, parameter_name,
       $(sancol current_value) AS current_value,
       $(sancol database_value) AS database_value,
       $(sancol default_value) AS default_value,
       current_level, restart_level, is_mismatch,
       $(sancol groups) AS groups
FROM   v_monitor.configuration_parameters
ORDER  BY parameter_name, node_name;
SQL

# ==============================================================================
# 收尾:統計、打包
# ==============================================================================
END_EPOCH="$(date +%s)"
ELAPSED=$(( END_EPOCH - START_EPOCH ))

{
  echo ""
  echo "=== result ==="
  echo "finished_at    : $(date '+%Y-%m-%d %H:%M:%S')"
  echo "elapsed_sec    : $ELAPSED"
  echo "checks_ok      : $OK_CNT"
  echo "checks_failed  : $FAIL_CNT"
  echo "failed_list    :$FAIL_LIST"
} >> "$META"

echo "--------------------------------------------------------------"
echo " 完成:OK=$OK_CNT  FAIL=$FAIL_CNT  耗時 ${ELAPSED}s"
if [ -n "$FAIL_LIST" ]; then
  echo " 失敗項目:$FAIL_LIST"
  echo " 失敗原因請看:${LOG}(多為權限或該版本無此檢視表)"
fi

# ------------------------------------------------------------------
# CSV 完整性檢查:每列欄位數必須與表頭一致(抓出夾帶換行/逗號的漏網之魚)
# ------------------------------------------------------------------
INTEG="$OUTDIR/_csv_integrity.csv"
printf 'file,header_cols,total_rows,bad_rows\n' > "$INTEG"
cat "$OUTDIR/.integrity.tmp" >> "$INTEG" 2>/dev/null
rm -f "$OUTDIR/.integrity.tmp"
BAD_CNT="$(printf '%s' "$BAD_LIST" | wc -w)"
if [ "${BAD_CNT:-0}" -gt 0 ]; then
  echo " 注意:有 ${BAD_CNT} 個 CSV 出現欄位數不一致的列:${BAD_LIST}"
  echo "       (多半是文字欄位夾帶換行,請連同 _errors.log 一併回報)"
fi

# 打包(放在輸出目錄的上一層,檔名帶時間避免同日覆蓋)
ARCHIVE="$(cd "$OUTDIR/.." && pwd)/VCheck_${HOST}_$(date +%Y%m%d_%H%M).tar.gz"
if tar --exclude='.raw.*' -czf "$ARCHIVE" -C "$(dirname "$OUTDIR")" "$(basename "$OUTDIR")" 2>/dev/null; then
  echo " 打包完成:$ARCHIVE"
  echo "archive        : $ARCHIVE" >> "$META"
else
  echo " 打包失敗,請手動壓縮目錄:$(cd "$OUTDIR" && pwd)"
fi
echo "--------------------------------------------------------------"
echo "請將上面那個 .tar.gz 回傳即可(內含全部 CSV 與 _errors.log)"

# 全部失敗 → 回傳非 0,方便排程/自動化偵測
[ "$OK_CNT" -eq 0 ] && exit 1
exit 0

05 常見問題

出現「找不到 vsql」怎麼辦?
在 Vertica 伺服器上 vsql 位於 /opt/vertica/bin/vsql。若不在 PATH 中,可執行 export PATH=/opt/vertica/bin:$PATH,或直接指定 export VSQL=/opt/vertica/bin/vsql 後再執行腳本。
部分項目顯示 FAIL,會影響報告嗎?
不會中斷,其他項目仍會完成。失敗原因多為權限不足或該版本沒有對應的檢視表,詳情寫在 _errors.log。請一併回傳該檔,我們會判斷是權限問題或版本差異,必要時調整查詢後重跑該項目。
連線被拒絕(Connection refused)怎麼排查?
依序確認:資料庫是否啟動、連接埠是否為 5433(或自訂的 vsql 埠)、主機防火牆是否放行、以及是否需要在叢集中指定其他節點。可先用 vsql -h <host> -p 5433 -U dbadmin -d <db> 測試手動連線。
密碼包含特殊字元,用 -w 會出錯
請改用 --pwfile,將密碼單獨寫在檔案第一行(不要有引號或多餘空白),並以 chmod 600 限制權限。使用完畢請刪除該檔案。
為什麼資料只有幾天?
時序資料來自 Data Collector,各資料表保留期不同(常見網路資料僅保留數天)。腳本會輸出實際可取得的起訖時間,報告中會照實標示涵蓋範圍。若需要完整 30 天,請先延長對應 retention 後再執行。
可以在非資料庫主機執行嗎?
可以,只要該主機裝有 vsql 且能連到資料庫。不過部分作業系統層面的數字(CPU、記憶體、磁碟)是以資料庫回報的節點資料為準,與執行腳本的主機無關。
腳本會修改資料庫或影響效能嗎?
不會。全部查詢皆為系統檢視表的 SELECT,不寫入資料、不改設定、不下 DDL。唯讀查詢在資料量大時仍會佔用少量資源,建議避開尖峰時段執行。

需要協助?

若執行過程遇到權限、連線或版本相容問題,或希望先看一份報告範例, 歡迎與我們聯繫,我們可以協助遠端執行與判讀。

聯絡我們