postgreSQL 12-2 vacuum-主流程
記錄自學postgreSQL原始碼的過程, 非常感謝大神的分享
大量借鑑了大神的部落格內容,拜謝。
本人屬於初學者,理解能力,程式碼閱讀能力也有限,如果由錯誤的地方請多多諒解,歡迎討論。
原始碼如下
/* * Primary entry point for manual VACUUM and ANALYZE commands * 手工執行VACUUM/ANALYZE命令時的主入口 * * This is mainly a preparation wrapper for the real operations that will * happen in vacuum(). * 這是vacuum()函式的包裝器(wrapper) */ void ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) { VacuumParams params; bool verbose = false; bool skip_locked = false; bool analyze = false; bool freeze = false; bool full = false; bool disable_page_skipping = false; ListCell *lc; /* Set default value */ //@lguan //設定預設值 params.index_cleanup = VACOPT_TERNARY_DEFAULT; params.truncate = VACOPT_TERNARY_DEFAULT; /* Parse options list */ //解析選項鍊表 foreach(lc, vacstmt->options) { DefElem *opt = (DefElem *) lfirst(lc); /* Parse common options for VACUUM and ANALYZE */ //解析VACUUM和ANALYZE的常用選項 if (strcmp(opt->defname, "verbose") == 0) verbose = defGetBoolean(opt); else if (strcmp(opt->defname, "skip_locked") == 0) skip_locked = defGetBoolean(opt); else if (!vacstmt->is_vacuumcmd) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("unrecognized ANALYZE option \"%s\"", opt->defname), parser_errposition(pstate, opt->location))); /* Parse options available on VACUUM */ //解析VACUUM的可用引數 else if (strcmp(opt->defname, "analyze") == 0) analyze = defGetBoolean(opt); else if (strcmp(opt->defname, "freeze") == 0) freeze = defGetBoolean(opt); else if (strcmp(opt->defname, "full") == 0) full = defGetBoolean(opt); else if (strcmp(opt->defname, "disable_page_skipping") == 0) disable_page_skipping = defGetBoolean(opt); else if (strcmp(opt->defname, "index_cleanup") == 0) params.index_cleanup = get_vacopt_ternary_value(opt); else if (strcmp(opt->defname, "truncate") == 0) params.truncate = get_vacopt_ternary_value(opt); else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("unrecognized VACUUM option \"%s\"", opt->defname), parser_errposition(pstate, opt->location))); } /* Set vacuum options */ //設定vacuum選項 params.options = (vacstmt->is_vacuumcmd ? VACOPT_VACUUM : VACOPT_ANALYZE) | (verbose ? VACOPT_VERBOSE : 0) | (skip_locked ? VACOPT_SKIP_LOCKED : 0) | (analyze ? VACOPT_ANALYZE : 0) | (freeze ? VACOPT_FREEZE : 0) | (full ? VACOPT_FULL : 0) | (disable_page_skipping ? VACOPT_DISABLE_PAGE_SKIPPING : 0); /* sanity checks on options */ //安全測試 Assert(params.options & (VACOPT_VACUUM | VACOPT_ANALYZE)); Assert((params.options & VACOPT_VACUUM) || !(params.options & (VACOPT_FULL | VACOPT_FREEZE))); Assert(!(params.options & VACOPT_SKIPTOAST)); /* * Make sure VACOPT_ANALYZE is specified if any column lists are present. * 如出現欄位列表,則確保指定了VACOPT_ANALYZE選項 * @lguan * 當清理選項指定了列欄位,那麼要確保VACOPT_ANALYZE必須指定 */ if (!(params.options & VACOPT_ANALYZE)) { ListCell *lc; foreach(lc, vacstmt->rels) { VacuumRelation *vrel = lfirst_node(VacuumRelation, lc); if (vrel->va_cols != NIL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("ANALYZE option must be specified when a column list is provided"))); } } /* * All freeze ages are zero if the FREEZE option is given; otherwise pass * them as -1 which means to use the default values. * 如指定了FREEZE選項則設定所有freeze ages為0. * 否則的話,傳遞-1(即使用預設值). */ if (params.options & VACOPT_FREEZE) { params.freeze_min_age = 0; params.freeze_table_age = 0; params.multixact_freeze_min_age = 0; params.multixact_freeze_table_age = 0; } else { params.freeze_min_age = -1; params.freeze_table_age = -1; params.multixact_freeze_min_age = -1; params.multixact_freeze_table_age = -1; } /* user-invoked vacuum is never "for wraparound" */ //使用者呼叫的vacuum永遠不會是wraparound params.is_wraparound = false; /* user-invoked vacuum never uses this parameter */ //使用者呼叫vacuum永遠不會使用該引數 params.log_min_duration = -1; /* Now go through the common routine */ //呼叫vacuum vacuum(vacstmt->rels, ¶ms, NULL, isTopLevel); }
建立測試用例
postgres=# create table t_index_test (id int,v1 char(8),v2 varchar(16)); CREATE TABLE postgres=# alter table t_index_test add constraint pk_t_index_test primary key(id); ALTER TABLE postgres=# insert into t_index_test select generate_series(1,10), concat('v1-', generate_series(1,10)::varchar), concat('v2-', generate_series(1,10)::varchar); INSERT 0 10 postgres=# select * from t_index_test; id | v1 | v2 ----+----------+------- 1 | v1-1 | v2-1 2 | v1-2 | v2-2 3 | v1-3 | v2-3 4 | v1-4 | v2-4 5 | v1-5 | v2-5 6 | v1-6 | v2-6 7 | v1-7 | v2-7 8 | v1-8 | v2-8 9 | v1-9 | v2-9 10 | v1-10 | v2-10 (10 rows) postgres=# select pg_backend_pid(); pg_backend_pid ---------------- 16227 (1 row) postgres=# VACUUM (VERBOSE, ANALYZE) t_index_test;
gdb 斷點,檢視函式呼叫棧
(gdb) b ExecVacuum Breakpoint 1 at 0x562e5d12cab0: file vacuum.c, line 89. (gdb) c Continuing. Breakpoint 1, ExecVacuum (pstate=pstate@entry=0x562e5f32e1c0, vacstmt=vacstmt@entry=0x562e5f253970, isTopLevel=isTopLevel@entry=true) at vacuum.c:89 89 { (gdb) bt #0 ExecVacuum (pstate=pstate@entry=0x562e5f32e1c0, vacstmt=vacstmt@entry=0x562e5f253970, isTopLevel=isTopLevel@entry=true) at vacuum.c:89 #1 0x0000562e5d29408b in standard_ProcessUtility (pstmt=0x562e5f253c90, queryString=0x562e5f252d70 "VACUUM (VERBOSE, ANALYZE) t_index_test;", context=<optimized out>, params=0x0, queryEnv=0x0, dest=0x562e5f253d70, completionTag=0x7ffc6ec73380 "") at utility.c:670 #2 0x0000562e5d290ec4 in PortalRunUtility (portal=0x562e5f2bacd0, pstmt=0x562e5f253c90, isTopLevel=<optimized out>, setHoldSnapshot=<optimized out>, dest=0x562e5f253d70, completionTag=0x7ffc6ec73380 "") at pquery.c:1175 #3 0x0000562e5d291980 in PortalRunMulti (portal=portal@entry=0x562e5f2bacd0, isTopLevel=isTopLevel@entry=true, setHoldSnapshot=setHoldSnapshot@entry=false, dest=dest@entry=0x562e5f253d70, altdest=altdest@entry=0x562e5f253d70, completionTag=completionTag@entry=0x7ffc6ec73380 "") at pquery.c:1321 #4 0x0000562e5d29255a in PortalRun (portal=portal@entry=0x562e5f2bacd0, count=count@entry=9223372036854775807, isTopLevel=isTopLevel@entry=true, run_once=run_once@entry=true, dest=dest@entry=0x562e5f253d70, altdest=altdest@entry=0x562e5f253d70, completionTag=0x7ffc6ec73380 "") at pquery.c:796 #5 0x0000562e5d28e685 in exec_simple_query (query_string=0x562e5f252d70 "VACUUM (VERBOSE, ANALYZE) t_index_test;") at postgres.c:1215 #6 0x0000562e5d28feab in PostgresMain (argc=<optimized out>, argv=argv@entry=0x562e5f27ee18, dbname=<optimized out>, username=<optimized out>) at postgres.c:4247 #7 0x0000562e5d218bdf in BackendRun (port=0x562e5f277310, port=0x562e5f277310) at postmaster.c:4437 #8 BackendStartup (port=0x562e5f277310) at postmaster.c:4128 #9 ServerLoop () at postmaster.c:1704 #10 0x0000562e5d219b30 in PostmasterMain (argc=3, argv=0x562e5f24ea40) at postmaster.c:1377 #11 0x0000562e5cf9add8 in main (argc=3, argv=0x562e5f24ea40) at main.c:228
除錯觀察
(gdb) n 101 params.truncate = VACOPT_TERNARY_DEFAULT; (gdb) p *pstate $1 = {parentParseState = 0x0, p_sourcetext = 0x562e5f252d70 "VACUUM (VERBOSE, ANALYZE) t_index_test;", p_rtable = 0x0, p_joinexprs = 0x0, p_joinlist = 0x0, p_namespace = 0x0, p_lateral_active = false, p_ctenamespace = 0x0, p_future_ctes = 0x0, p_parent_cte = 0x0, p_target_relation = 0x0, p_target_rangetblentry = 0x0, p_is_insert = false, p_windowdefs = 0x0, p_expr_kind = EXPR_KIND_NONE, p_next_resno = 1, p_multiassign_exprs = 0x0, p_locking_clause = 0x0, p_locked_from_parent = false, p_resolve_unknowns = true, p_queryEnv = 0x0, p_hasAggs = false, p_hasWindowFuncs = false, p_hasTargetSRFs = false, p_hasSubLinks = false, p_hasModifyingCTE = false, p_last_srf = 0x0, p_pre_columnref_hook = 0x0, p_post_columnref_hook = 0x0, p_paramref_hook = 0x0, p_coerce_param_hook = 0x0, p_ref_hook_state = 0x0} (gdb) p *vacstmt $2 = {type = T_VacuumStmt, options = 0x562e5f2537e0, rels = 0x562e5f253940, is_vacuumcmd = true} (gdb) p vacstmt->options $3 = (List *) 0x562e5f2537e0 (gdb) p vacstmt->options->next There is no member named next.
結構體
/* ---------------------- * Vacuum and Analyze Statements * Vacuum和Analyze宣告 * * Even though these are nominally two statements, it's convenient to use * just one node type for both. * 雖然在這裡有兩種不同的宣告,但只需要使用統一的Node型別即可. * @lguan * 意思是Vacuum和Analyze使用同一個宣告,只是用NodeTag來區分? * ---------------------- */ typedef struct VacuumStmt { NodeTag type; List *options; /* list of DefElem nodes */ //VacuumRelation連結串列,如為NIL-->所有Relation. List *rels; /* list of VacuumRelation, or NIL for all */ //true則表示VACCUM,false表示ANALYZE bool is_vacuumcmd; /* true for VACUUM, false for ANALYZE */ } VacuumStmt;
其中options即為使用者輸入的option,在本例中為“VERBOSE”和“ANALYZE”
option為一個連結串列,這時PG中通用的一個連結串列結構體
typedef struct List { NodeTag type; /* T_List, T_IntList, or T_OidList */ int length; ListCell *head; ListCell *tail; } List; struct ListCell { union { void *ptr_value; int int_value; Oid oid_value; } data; ListCell *next; };
繼續除錯
(gdb) n 104 foreach(lc, vacstmt->options) (gdb) n 106 DefElem *opt = (DefElem *) lfirst(lc); (gdb) n 109 if (strcmp(opt->defname, "verbose") == 0) (gdb) p *opt $4 = {type = T_DefElem, defnamespace = 0x0, defname = 0x562e5f253758 "verbose", arg = 0x0, defaction = DEFELEM_UNSPEC, location = 8}
可以看到為第一個引數“verbose”,這裡應該是前面的解析程式碼中做了轉換,大寫轉換成小寫。
看一下foreach和lfirst宏
#define foreach(cell, l) \ for ((cell) = list_head(l); (cell) != NULL; (cell) = lnext(cell))
#define lfirst(lc) ((lc)->data.ptr_value)
可以看到option這個連結串列中的節點指標所指向的位置是一個DefElem的結構體,由於封裝原因,所以在連結串列中是一個void型別的指標,可以指向任意的型別,所以可以猜測在前面的呼叫函式中,在構建VacuumStmt結構體時應該已經option連結串列中的節點實際資料設定成了DefElem,(這裡只是個人猜測,這一程式碼值得深入連線以下,因為PG出現大量的這種型別的寫法,具有模擬c++類,封裝的實現的意思)。
看以下DefElem結構體
/* * DefElem - a generic "name = value" option definition * @lguan * DefElem - 通用的“name=value”的選項的定義,就是說該選項的名字即為該選項的值 * * In some contexts the name can be qualified. Also, certain SQL commands * allow a SET/ADD/DROP action to be attached to option settings, so it's * convenient to carry a field for that too. (Note: currently, it is our * practice that the grammar allows namespace and action only in statements * where they are relevant; C code can just ignore those fields in other * statements.) * @lguan * 在某些語境下,名稱可以是限定的。 * 此外,某些 SQL 命令允許將 SET/ADD/DROP 操作附加到選項設定,因此攜帶欄位也很方便。 */ typedef enum DefElemAction { DEFELEM_UNSPEC, /* no action given */ DEFELEM_SET, DEFELEM_ADD, DEFELEM_DROP } DefElemAction; typedef struct DefElem { NodeTag type; char *defnamespace; /* NULL if unqualified name */ char *defname; Node *arg; /* a (Value *) or a (TypeName *) */ DefElemAction defaction; /* unspecified action, or SET/ADD/DROP */ int location; /* token location, or -1 if unknown */ } DefElem;
繼續除錯
(gdb) n 110 verbose = defGetBoolean(opt); (gdb) n 106 DefElem *opt = (DefElem *) lfirst(lc); (gdb) p verbose $5 = true (gdb) n 109 if (strcmp(opt->defname, "verbose") == 0) (gdb) p *opt $6 = {type = T_DefElem, defnamespace = 0x0, defname = 0x562e5d566c01 "analyze", arg = 0x0, defaction = DEFELEM_UNSPEC, location = 17} (gdb) n 111 else if (strcmp(opt->defname, "skip_locked") == 0) (gdb) n 113 else if (!vacstmt->is_vacuumcmd) (gdb) n 120 else if (strcmp(opt->defname, "analyze") == 0) (gdb) n 121 analyze = defGetBoolean(opt); (gdb) n
下個引數和之前的一樣的邏輯,掠過
繼續除錯
(gdb) n 140 params.options = (gdb) p analyze $7 = <optimized out> (gdb) n 158 if (!(params.options & VACOPT_ANALYZE)) (gdb) p analyze $8 = <optimized out> (gdb) p params $9 = {options = 7, freeze_min_age = 0, freeze_table_age = 1598457328, multixact_freeze_min_age = 22062, multixact_freeze_table_age = 1598458104, is_wraparound = 46, log_min_duration = 208, index_cleanup = VACOPT_TERNARY_DEFAULT, truncate = VACOPT_TERNARY_DEFAULT} (gdb) n 177 if (params.options & VACOPT_FREEZE) (gdb) n 189 params.multixact_freeze_table_age = -1; (gdb) n 193 params.is_wraparound = false; (gdb) n 196 params.log_min_duration = -1; (gdb) n 199 vacuum(vacstmt->rels, ¶ms, NULL, isTopLevel); (gdb) p params $10 = {options = 7, freeze_min_age = -1, freeze_table_age = -1, multixact_freeze_min_age = -1, multixact_freeze_table_age = -1, is_wraparound = false, log_min_duration = -1, index_cleanup = VACOPT_TERNARY_DEFAULT, truncate = VACOPT_TERNARY_DEFAULT} (gdb)
後面就是在設定結構體VacuumParams的值了
typedef enum VacuumOption { VACOPT_VACUUM = 1 << 0, /* do VACUUM */ VACOPT_ANALYZE = 1 << 1, /* do ANALYZE */ VACOPT_VERBOSE = 1 << 2, /* print progress info */ VACOPT_FREEZE = 1 << 3, /* FREEZE option */ VACOPT_FULL = 1 << 4, /* FULL (non-concurrent) vacuum */ VACOPT_SKIP_LOCKED = 1 << 5, /* skip if cannot get lock */ VACOPT_SKIPTOAST = 1 << 6, /* don't process the TOAST table, if any */ VACOPT_DISABLE_PAGE_SKIPPING = 1 << 7 /* don't skip any pages */ } VacuumOption; /* * A ternary value used by vacuum parameters. * vacuum的引數的三元值 * * DEFAULT value is used to determine the value based on other * configurations, e.g. reloptions. * 預設值代表該引數是基於其他的配置的,比如說:對應關係(表)的選項 */ typedef enum VacOptTernaryValue { VACOPT_TERNARY_DEFAULT = 0, VACOPT_TERNARY_DISABLED, VACOPT_TERNARY_ENABLED, } VacOptTernaryValue; /* * Parameters customizing behavior of VACUUM and ANALYZE. * 客戶端呼叫VACUUM/ANALYZE時的定製化引數 * * Note that at least one of VACOPT_VACUUM and VACOPT_ANALYZE must be set * in options. * 注意至少VACOPT_VACUUM/VACOPT_ANALYZE在選項中設定. */ typedef struct VacuumParams { int options; /* bitmask of VacuumOption VacuumOption的位掩碼*/ //最小freeze age,-1表示使用預設 int freeze_min_age; /* min freeze age, -1 to use default */ //當freeze age 達到 freeze_table_age 時需要掃描整個table int freeze_table_age; /* age at which to scan whole table */ //最小的multixact freeze age,-1表示預設 int multixact_freeze_min_age; /* min multixact freeze age, -1 to * use default */ //當min multixact freeze age 達到 multixact_freeze_table_age 時需要掃描整個table int multixact_freeze_table_age; /* multixact age at which to scan * whole table */ //是否強制wraparound? bool is_wraparound; /* force a for-wraparound vacuum */ //以毫秒為單位的最小執行閾值 int log_min_duration; /* minimum execution threshold in ms at * which verbose logs are activated, -1 * to use default */ //進行索引vacuum和清理,預設值由索引對應的表的選項來決定 VacOptTernaryValue index_cleanup; /* Do index vacuum and cleanup, * default value depends on reloptions */ //在末尾截斷空頁,預設值由對應的表的選項決定 VacOptTernaryValue truncate; /* Truncate empty pages at the end, * default value depends on reloptions */ } VacuumParams;
這裡用了一個區域性變數params作為引數,呼叫vacuum主實現函式。
在本例中沒有進入
if (!(params.options & VACOPT_ANALYZE)) { ListCell *lc; foreach(lc, vacstmt->rels) { VacuumRelation *vrel = lfirst_node(VacuumRelation, lc); if (vrel->va_cols != NIL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("ANALYZE option must be specified when a column list is provided"))); } }
所以沒有觀察到vacstmt->rels這個連結串列的結構,因此,再除錯一次,去掉ANALYZE選項。
結構體
/* * Info about a single target table of VACUUM/ANALYZE. * VACUUM/ANALYZE命令的目標表資訊. * * If the OID field is set, it always identifies the table to process. * Then the relation field can be NULL; if it isn't, it's used only to report * failure to open/lock the relation. * 如設定了OID欄位,該值通常是將要處理的資料表的oid值,那麼關係欄位可以為NULL。 * 如果不是,則僅用於報告未能開啟/鎖定關係。 */ typedef struct VacuumRelation { NodeTag type; RangeVar *relation; /* table name to process, or NULL */ Oid oid; /* table's OID; InvalidOid if not looked up */ //表的列欄位名,如果是NIL表示VACUUM所有列 List *va_cols; /* list of column names, or NIL for all */ } VacuumRelation;
然後根據gdb結果
(gdb) p *vrel $6 = {type = T_VacuumRelation, relation = 0x562e5f253758, oid = 0, va_cols = 0x0}
oid欄位為0,不知道是不是註釋有誤的原因?????
看一下結構體
/* * RangeVar - range variable, used in FROM clauses * RangeVar - 範圍變數,用於 FROM 子句 * * Also used to represent table names in utility statements; there, the alias * field is not used, and inh tells whether to apply the operation * recursively to child tables. In some contexts it is also useful to carry * a TEMP table indication here. * 也用於表示實用程式語句中的表名。這種情況下,沒有使用alias欄位,並且 inh 用於表示是否將操作遞迴地應用於子表。 * 在某些情況下,在此處攜帶 TEMP 表指示也很有用。???? */ typedef struct RangeVar { NodeTag type; char *catalogname; /* the catalog (database) name, or NULL */ char *schemaname; /* the schema name, or NULL */ char *relname; /* the relation/sequence name */ bool inh; /* expand rel by inheritance? recursively act * on children? */ char relpersistence; /* see RELPERSISTENCE_* in pg_class.h */ Alias *alias; /* table alias & optional column aliases */ int location; /* token location, or -1 if unknown */ } RangeVar;
(gdb) p *(RangeVar *)(vrel->relation) $10 = {type = T_RangeVar, catalogname = 0x0, schemaname = 0x0, relname = 0x562e5f253738 "t_index_test", inh = true, relpersistence = 112 'p', alias = 0x0, location = 15} (gdb)
基本除錯完成。
總結如下,
結構體VacuumStmt是vacuum的主結構體,其中的option連結串列是用於輸入引數的連結串列集合,該連結串列中的節點型別應該是DefElem結構體,rels連結串列是使用者想要進行vacuum的表的集合,該連結串列的節點型別為VacuumRelation結構體。
而VacuumParams結構體中儲存的是進行vacuum操作時的一些配置引數,其中的option和上面的option不是一樣,這裡的option是透過解析上面的option,再透過VacuumOption這個結構體,生成的一個位掩碼,來代表再上面的option(也就是使用者輸入的vacuum選項)
再次感謝
大神這種分享知識的精神也正符合PG開源社群的宗旨。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70012752/viewspace-2884353/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- PostgreSQL 原始碼解讀(125)- MVCC#9(vacuum-主流程)SQL原始碼MVCC#
- PG 12-2 B-Tree 索引 分析 分裂 level = 1索引
- 2.modbus主流程分析
- Glide 系列-2:主流程原始碼分析(4.8.0)IDE原始碼
- react fiber 主流程及功能模組梳理React
- 主流程式語言的介紹及特點
- 主流程式語言的“介紹”與“選擇”
- 那些主流程式語言的知識,C語言(Ⅰ)C語言
- 主流程式語言的優勢與劣勢對比
- 三大主流程式語言Python為啥這麼牛?Python
- 智慧對話機器人如何設計產品主流程框架?機器人框架
- PostgreSQL DBA(45) - Hypothetical Indexes in PostgreSQLSQLIndex
- Python 是增長最快的主流程式語言 | StackOverflow 重磅調查Python
- PostgreSQLSQL
- PostgreSQL:WITHSQL
- PostgreSQL DBA(133) - Extension(postgresql_anonymizer)SQL
- PostgreSQL DBA(181) - Using PostgreSQL as a Data WarehouseSQL
- PostgreSQL:Redhat 8.5 + PostgreSQL 14.5 安裝SQLRedhat
- PostgreSQL-PostgreSQL中的public(九)SQL
- 搞懂分散式技術4:ZAB協議概述與選主流程詳解分散式協議
- Java 10更新彙總,新的編譯器通吃主流程式語言Java編譯
- postgresql關於postgresql.auto.conf和postgresql.conf的區別SQL
- 【PostgreSQL 】PostgreSQL 15對distinct的優化SQL優化
- PostgreSQL:COPYSQL
- Hacking PostgreSQLSQL
- PostgreSQL核心SQL
- PostgreSQL:表SQL
- PostgreSQL:INDEXSQLIndex
- PostgreSQL:鎖SQL
- PostgreSQL:RULESQL
- PostgreSQL:EXPLAINSQLAI
- PostgreSQL DBA(142) - PG 12(Monitoring PostgreSQL VACUUM processes)SQL
- PostgreSQL TPROC-C基準測試:PostgreSQL 12與PostgreSQL 13效能對比SQL
- postgreSQL學習(一):在Linux下安裝postgreSQLSQLLinux
- PostgreSQL DBA(143) - pgAdmin(Monitoring PostgreSQL VACUUM processes#2)SQL
- PostgreSQL 速查、備忘手冊 | PostgreSQL Quick Find and TutorialSQLUI
- POSTGRESQL postgresql 升級的需求來自哪裡SQL
- 【pgupgrade】Postgresql10升級到Postgresql13SQL