From 6a2f77784fb2bfb96290f99282a9b85b4cfe23c1 Mon Sep 17 00:00:00 2001 From: Wazir Ahmed Date: Fri, 6 Feb 2026 12:19:30 +0530 Subject: [PATCH] doc: Document existing `stats` tables and `/mcp/stats` tools & implementation Signed-off-by: Wazir Ahmed --- doc/MCP/MCP_Stats_Implementation_Plan.md | 1147 +++++++++++++ doc/MCP/MCP_Stats_Tools_Spec.md | 1357 ++++++++++++++++ doc/Stats_Tables.md | 1857 ++++++++++++++++++++++ 3 files changed, 4361 insertions(+) create mode 100644 doc/MCP/MCP_Stats_Implementation_Plan.md create mode 100644 doc/MCP/MCP_Stats_Tools_Spec.md create mode 100644 doc/Stats_Tables.md diff --git a/doc/MCP/MCP_Stats_Implementation_Plan.md b/doc/MCP/MCP_Stats_Implementation_Plan.md new file mode 100644 index 000000000..439a6303c --- /dev/null +++ b/doc/MCP/MCP_Stats_Implementation_Plan.md @@ -0,0 +1,1147 @@ +# ProxySQL Stats MCP Tools - Implementation Guide + +This document provides implementation guidance for the `/stats` endpoint tools, including database access patterns, table mappings, SQL queries, data flow documentation, and design rationale. + +## Table of Contents + +- [1. Database Access Patterns](#1-database-access-patterns) +- [2. Table-to-Tool Mapping](#2-table-to-tool-mapping) +- [3. Data Flow Patterns](#3-data-flow-patterns) +- [4. Interval-to-Table Resolution](#4-interval-to-table-resolution) +- [5. Tool Implementation Details](#5-tool-implementation-details) +- [6. Helper Functions](#6-helper-functions) +- [7. Error Handling Patterns](#7-error-handling-patterns) +- [8. Testing Strategies](#8-testing-strategies) + +--- + +## 1. Database Access Patterns + +### 1.1 Database Types + +ProxySQL maintains several SQLite databases: + +| Database | Variable | Purpose | Schema Prefix | +|---|---|---|---| +| `admindb` | `GloAdmin->admindb` | Configuration and admin interface | (none) | +| `statsdb` | `GloAdmin->statsdb` | In-memory real-time statistics | `stats.` | +| `statsdb_disk` | `GloAdmin->statsdb_disk` | Persistent historical statistics | `stats_history.` | +| `statsdb_mem` | Internal | Internal metrics collection | N/A (not directly accessible) | + +### 1.2 Access Rules + +**Real-time stats tables:** Access through `GloAdmin->admindb` with the `stats.` schema prefix. + +```cpp +// Example: Query stats_mysql_connection_pool +GloAdmin->admindb->execute_statement( + "SELECT * FROM stats.stats_mysql_connection_pool", + &error, &cols, &affected_rows, &resultset +); +``` + +**Historical data tables:** Access through `GloAdmin->statsdb_disk` directly (no prefix needed as it's the default schema). + +```cpp +// Example: Query mysql_connections history (direct access - preferred) +GloAdmin->statsdb_disk->execute_statement( + "SELECT * FROM mysql_connections WHERE timestamp > ?", + &error, &cols, &affected_rows, &resultset +); +``` + +Alternatively, historical tables can be accessed through `GloAdmin->admindb` using the `stats_history.` prefix, as `statsdb_disk` is attached to both databases: + +```cpp +// Example: Query mysql_connections history (via admindb with prefix) +GloAdmin->admindb->execute_statement( + "SELECT * FROM stats_history.mysql_connections WHERE timestamp > ?", + &error, &cols, &affected_rows, &resultset +); +``` + +Direct access via `statsdb_disk` is preferred for performance. + +**Never use `GloAdmin->statsdb` directly** — it's for internal ProxySQL use only. + +### 1.3 Query Execution Pattern + +```cpp +json Stats_Tool_Handler::execute_query(const std::string& sql, SQLite3DB* db) { + SQLite3_result* resultset = NULL; + char* error = NULL; + int cols = 0; + int affected_rows = 0; + + int rc = db->execute_statement(sql.c_str(), &error, &cols, &affected_rows, &resultset); + + if (rc != SQLITE_OK) { + std::string err_msg = error ? error : "Query execution failed"; + if (error) free(error); + return create_error_response(err_msg); + } + + json rows = resultset_to_json(resultset, cols); + delete resultset; + + return rows; +} +``` + +--- + +## 2. Table-to-Tool Mapping + +### 2.1 Live Data Tools + +| Tool | MySQL Tables | PostgreSQL Tables | +|---|---|---| +| `show_status` | `stats.stats_mysql_global`, `stats.stats_memory_metrics` | `stats.stats_pgsql_global`, `stats.stats_memory_metrics` | +| `show_processlist` | `stats.stats_mysql_processlist` | `stats.stats_pgsql_processlist` | +| `show_queries` | `stats.stats_mysql_query_digest` | `stats.stats_pgsql_query_digest` | +| `show_commands` | `stats.stats_mysql_commands_counters` | `stats.stats_pgsql_commands_counters` | +| `show_connections` | `stats.stats_mysql_connection_pool`, `stats.stats_mysql_free_connections` | `stats.stats_pgsql_connection_pool`, `stats.stats_pgsql_free_connections` | +| `show_errors` | `stats.stats_mysql_errors` | `stats.stats_pgsql_errors` (uses `sqlstate` instead of `errno`) | +| `show_users` | `stats.stats_mysql_users` | `stats.stats_pgsql_users` | +| `show_client_cache` | `stats.stats_mysql_client_host_cache` | `stats.stats_pgsql_client_host_cache` | +| `show_query_rules` | `stats.stats_mysql_query_rules` | `stats.stats_pgsql_query_rules` | +| `show_prepared_statements` | `stats.stats_mysql_prepared_statements_info` | `stats.stats_pgsql_prepared_statements_info` | +| `show_gtid` | `stats.stats_mysql_gtid_executed` | N/A | +| `show_cluster` | `stats.stats_proxysql_servers_status`, `stats.stats_proxysql_servers_metrics`, `stats.stats_proxysql_servers_checksums`, `stats.stats_proxysql_servers_clients_status` | Same (shared) | + +### 2.2 Historical Data Tools + +| Tool | MySQL Tables | PostgreSQL Tables | +|---|---|---| +| `show_system_history` | `system_cpu`, `system_cpu_hour`, `system_memory`, `system_memory_hour` | Same (shared) | +| `show_query_cache_history` | `mysql_query_cache`, `mysql_query_cache_hour` | N/A | +| `show_connection_history` | `mysql_connections`, `mysql_connections_hour`, `myhgm_connections`, `myhgm_connections_hour`, `history_stats_mysql_connection_pool` | N/A | +| `show_query_history` | `history_mysql_query_digest` | `history_pgsql_query_digest` | + +### 2.3 Utility Tools + +| Tool | MySQL Tables | PostgreSQL Tables | +|---|---|---| +| `flush_query_log` | `stats.stats_mysql_query_events`, `history_mysql_query_events` | N/A | +| `show_query_log` | `stats.stats_mysql_query_events`, `history_mysql_query_events` | N/A | +| `flush_queries` | `history_mysql_query_digest` | `history_pgsql_query_digest` | + +### 2.4 Column Naming: MySQL vs PostgreSQL + +ProxySQL uses different column names for the same concept between MySQL and PostgreSQL: + +| Concept | MySQL Column | PostgreSQL Column | API Field | +|---------|--------------|-------------------|-----------| +| Database/Schema | `schemaname` | `database` | `database` | +| Error Code | `errno` | `sqlstate` | `errno`/`sqlstate` | +| Process DB | `db` | `database` | `database` | + +**Implementation Note:** The history table `history_pgsql_query_digest` uses `schemaname` (matching MySQL convention) rather than `database`, creating an inconsistency with the live `stats_pgsql_query_digest` table. Implementation must handle this when building queries for PostgreSQL. + +--- + +## 3. Data Flow Patterns + +### 3.1 Query Events Flow + +Query events use a circular buffer that must be explicitly flushed to tables. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Query Execution │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ MySQL_Logger::log_request() │ +│ Creates MySQL_Event, adds to circular buffer │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Circular Buffer (MyLogCB) │ +│ Size controlled by eventslog_table_memory_size │ +│ Events accumulate until flushed │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + DUMP TO MEMORY DUMP TO DISK DUMP TO BOTH + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ stats_mysql_ │ │ history_mysql_ │ │ Both │ +│ query_events │ │ query_events │ │ tables │ +│ (in-memory, │ │ (on-disk, │ │ │ +│ capped size) │ │ append-only) │ │ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +**Implementation for `flush_query_log`:** + +The tool triggers the appropriate admin command based on destination: + +```cpp +json Stats_Tool_Handler::handle_flush_query_log(const json& arguments) { + std::string destination = arguments.value("destination", "memory"); + + std::string command; + if (destination == "memory") { + command = "DUMP EVENTSLOG FROM BUFFER TO MEMORY"; + } else if (destination == "disk") { + command = "DUMP EVENTSLOG FROM BUFFER TO DISK"; + } else if (destination == "both") { + command = "DUMP EVENTSLOG FROM BUFFER TO BOTH"; + } + + // Execute via GloMyLogger->processEvents() with appropriate database pointers + int events_flushed = GloMyLogger->processEvents( + destination == "disk" ? nullptr : GloAdmin->statsdb, + destination == "memory" ? nullptr : GloAdmin->statsdb_disk + ); + + json result; + result["events_flushed"] = events_flushed; + result["destination"] = destination; + return create_success_response(result); +} +``` + +### 3.2 Query Digest Flow + +Query digest statistics are maintained in an in-memory hash map, not SQLite. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Query Completes │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Query_Processor::update_query_digest() │ +│ Updates digest_umap (hash map in memory) │ +│ Aggregates: count_star, sum_time, min/max, rows │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌─────────────────────┴─────────────────────┐ + │ │ + SELECT query SAVE TO DISK + (non-destructive) (destructive) + │ │ + ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────────┐ +│ get_query_digests_v2() │ │ FlushDigestTableToDisk() │ +│ - Swap map with empty │ │ - get_query_digests_reset() │ +│ - Serialize to SQLite │ │ - Atomic swap (empties map) │ +│ - Merge back │ │ - Write to history table │ +│ - Data preserved │ │ - Delete swapped data │ +└─────────────────────────┘ │ - Map starts fresh │ + └─────────────────────────────┘ +``` + +**Key Implementation Notes:** + +1. **Reading live data (`show_queries`):** Non-destructive. ProxySQL handles the swap-serialize-merge internally when you query `stats_mysql_query_digest`. + +2. **Saving to history (`flush_queries`):** Destructive. The live map is emptied. Call `FlushDigestTableToDisk()`: + +```cpp +json Stats_Tool_Handler::handle_flush_queries(const json& arguments) { + std::string db_type = arguments.value("db_type", "mysql"); + + int digests_saved; + if (db_type == "mysql") { + digests_saved = GloAdmin->FlushDigestTableToDisk(GloAdmin->statsdb_disk); + } else { + digests_saved = GloAdmin->FlushDigestTableToDisk(GloAdmin->statsdb_disk); + } + + json result; + result["db_type"] = db_type; + result["digests_saved"] = digests_saved; + result["dump_time"] = time(NULL); + return create_success_response(result); +} +``` + +### 3.3 Historical Tables Flow + +Historical tables are populated by periodic timers and aggregated into hourly tables. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Admin Thread Timer Check │ +│ (every poll cycle) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ *_timetoget(curtime) returns true? │ +│ (checks if interval has elapsed) │ +└─────────────────────────────────────────────────────────────────┘ + │ yes + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Collect current metrics │ +│ (e.g., system_cpu from times()) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ INSERT INTO raw table │ +│ (e.g., system_cpu) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Check if hourly aggregation needed │ +│ (current time >= last_hour_entry + 3600) │ +└─────────────────────────────────────────────────────────────────┘ + │ yes + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ INSERT INTO *_hour SELECT ... GROUP BY │ +│ (aggregation: SUM/AVG/MAX depending on column) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ DELETE old data │ +│ - Raw: older than 7 days │ +│ - Hourly: older than 365 days │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. Interval-to-Table Resolution + +Historical tools accept user-friendly interval parameters and automatically select the appropriate table. + +### 4.1 Interval Mapping + +| User Interval | Seconds | Table Type | Rationale | +|---|---|---|---| +| `30m` | 1800 | Raw | Fine-grained, small dataset | +| `1h` | 3600 | Raw | Fine-grained, small dataset | +| `2h` | 7200 | Raw | Fine-grained, moderate dataset | +| `4h` | 14400 | Raw | Raw data still manageable | +| `6h` | 21600 | Raw | Raw data still manageable | +| `12h` | 43200 | Raw | Boundary: last interval using raw | +| `1d` | 86400 | Hourly | Raw would have ~1440 rows, hourly has 24 | +| `3d` | 259200 | Hourly | Hourly aggregation more efficient | +| `7d` | 604800 | Hourly | Raw data may not exist (7-day retention) | +| `30d` | 2592000 | Hourly | Raw data doesn't exist this far back | +| `90d` | 7776000 | Hourly | Raw data doesn't exist this far back | + +### 4.2 Implementation + +```cpp +struct IntervalConfig { + int seconds; + bool use_hourly; +}; + +std::map interval_map = { + {"30m", {1800, false}}, + {"1h", {3600, false}}, + {"2h", {7200, false}}, + {"4h", {14400, false}}, + {"6h", {21600, false}}, + {"12h", {43200, false}}, + {"1d", {86400, true}}, + {"3d", {259200, true}}, + {"7d", {604800, true}}, + {"30d", {2592000, true}}, + {"90d", {7776000, true}} +}; + +std::string get_table_name(const std::string& base_table, const std::string& interval) { + auto it = interval_map.find(interval); + if (it == interval_map.end()) { + return base_table; // Default to raw + } + + if (it->second.use_hourly) { + return base_table + "_hour"; + } + return base_table; +} + +std::string build_time_range_query(const std::string& table, int seconds) { + time_t now = time(NULL); + time_t start = now - seconds; + + return "SELECT * FROM " + table + + " WHERE timestamp BETWEEN " + std::to_string(start) + + " AND " + std::to_string(now) + + " ORDER BY timestamp"; +} +``` + +--- + +## 5. Tool Implementation Details + +### 5.1 show_status + +**Source Tables:** +- MySQL: `stats.stats_mysql_global`, `stats.stats_memory_metrics` +- PostgreSQL: `stats.stats_pgsql_global`, `stats.stats_memory_metrics` + +**Category Mapping:** + +```cpp +std::map> category_prefixes = { + {"connections", {"Client_Connections_", "Server_Connections_", "Active_Transactions"}}, + {"queries", {"Questions", "Slow_queries", "GTID_", "Queries_", "Query_Processor_", "Backend_query_time_"}}, + {"commands", {"Com_"}}, + {"pool_ops", {"ConnPool_", "MyHGM_"}}, + {"monitor", {"MySQL_Monitor_", "PgSQL_Monitor_"}}, + {"query_cache", {"Query_Cache_"}}, + {"prepared_stmts", {"Stmt_"}}, + {"security", {"automatic_detected_sql_injection", "ai_", "mysql_whitelisted_"}}, + {"memory", {"_buffers_bytes", "_internal_bytes", "SQLite3_memory_bytes", "ConnPool_memory_bytes", + "jemalloc_", "Auth_memory", "query_digest_memory", "query_rules_memory", + "prepare_statement_", "firewall_", "stack_memory_"}}, + {"errors", {"generated_error_packets", "Access_Denied_", "client_host_error_", "mysql_unexpected_"}}, + {"logger", {"MySQL_Logger_"}}, + {"system", {"ProxySQL_Uptime", "MySQL_Thread_Workers", "PgSQL_Thread_Workers", + "Servers_table_version", "mysql_listener_paused", "pgsql_listener_paused", "OpenSSL_"}}, + {"mirror", {"Mirror_"}} +}; +``` + +**SQL Query:** + +```sql +-- For category filter +SELECT Variable_Name, Variable_Value +FROM stats.stats_mysql_global +WHERE Variable_Name LIKE 'Client_Connections_%' + OR Variable_Name LIKE 'Server_Connections_%' + OR Variable_Name = 'Active_Transactions'; + +-- For variable_name filter (using LIKE) +SELECT Variable_Name, Variable_Value +FROM stats.stats_mysql_global +WHERE Variable_Name LIKE ?; + +-- Also query memory_metrics for 'memory' category +SELECT Variable_Name, Variable_Value +FROM stats.stats_memory_metrics; +``` + +**Description Lookup:** + +Maintain a static map of variable descriptions: + +```cpp +std::map variable_descriptions = { + {"Client_Connections_connected", "Currently connected clients"}, + {"Client_Connections_created", "Total client connections ever created"}, + {"Questions", "Total queries processed"}, + // ... etc +}; +``` + +### 5.2 show_processlist + +**Source Tables:** +- MySQL: `stats.stats_mysql_processlist` +- PostgreSQL: `stats.stats_pgsql_processlist` + +**SQL Query:** + +```sql +SELECT ThreadID, SessionID, user, db, cli_host, cli_port, + hostgroup, l_srv_host, l_srv_port, srv_host, srv_port, + command, time_ms, info, status_flags, extended_info +FROM stats.stats_mysql_processlist +WHERE (user = ? OR ? IS NULL) + AND (hostgroup = ? OR ? IS NULL) + AND (time_ms >= ? OR ? IS NULL) +ORDER BY time_ms DESC +LIMIT ? OFFSET ?; +``` + +**Note:** The `l_srv_host` and `l_srv_port` columns represent the local ProxySQL interface, while `srv_host` and `srv_port` represent the backend server. + +**Summary Aggregation:** + +```cpp +json build_summary(const json& sessions) { + std::map by_user, by_hostgroup, by_command; + + for (const auto& session : sessions) { + by_user[session["user"].get()]++; + by_hostgroup[std::to_string(session["hostgroup"].get())]++; + by_command[session["command"].get()]++; + } + + json summary; + summary["by_user"] = by_user; + summary["by_hostgroup"] = by_hostgroup; + summary["by_command"] = by_command; + return summary; +} +``` + +### 5.3 show_queries + +**Source Tables:** +- MySQL: `stats.stats_mysql_query_digest` (uses `schemaname` column) +- PostgreSQL: `stats.stats_pgsql_query_digest` (uses `database` column) + +**SQL Query (MySQL):** + +```sql +SELECT hostgroup, schemaname AS database, username, client_address, digest, + digest_text, count_star, first_seen, last_seen, + sum_time AS sum_time_us, min_time AS min_time_us, max_time AS max_time_us, + sum_rows_affected, sum_rows_sent +FROM stats.stats_mysql_query_digest +WHERE (count_star >= ? OR ? IS NULL) + AND (hostgroup = ? OR ? IS NULL) + AND (username = ? OR ? IS NULL) + AND (schemaname = ? OR ? IS NULL) -- database parameter maps to schemaname column + AND (digest = ? OR ? IS NULL) + AND (sum_time / count_star >= ? OR ? IS NULL) +ORDER BY count_star DESC +LIMIT ? OFFSET ?; +``` + +**SQL Query (PostgreSQL):** + +```sql +SELECT hostgroup, database, username, client_address, digest, + digest_text, count_star, first_seen, last_seen, + sum_time AS sum_time_us, min_time AS min_time_us, max_time AS max_time_us, + sum_rows_affected, sum_rows_sent +FROM stats.stats_pgsql_query_digest +WHERE (count_star >= ? OR ? IS NULL) + AND (hostgroup = ? OR ? IS NULL) + AND (username = ? OR ? IS NULL) + AND (database = ? OR ? IS NULL) -- database parameter maps to database column + AND (digest = ? OR ? IS NULL) + AND (sum_time / count_star >= ? OR ? IS NULL) +ORDER BY count_star DESC +LIMIT ? OFFSET ?; +``` + +**Calculated Fields:** + +```cpp +for (auto& query : queries) { + int count = query["count_star"].get(); + int sum_time = query["sum_time_us"].get(); + query["avg_time_us"] = count > 0 ? sum_time / count : 0; +} +``` + +### 5.4 show_commands + +**Source Tables:** +- MySQL: `stats.stats_mysql_commands_counters` +- PostgreSQL: `stats.stats_pgsql_commands_counters` + +**SQL Query:** + +```sql +SELECT Command, Total_Time_us, Total_cnt, + cnt_100us, cnt_500us, cnt_1ms, cnt_5ms, cnt_10ms, cnt_50ms, + cnt_100ms, cnt_500ms, cnt_1s, cnt_5s, cnt_10s, cnt_INFs +FROM stats.stats_mysql_commands_counters +WHERE Command = ? OR ? IS NULL; +``` + +**Percentile Calculation:** + +See [Section 6.1](#61-percentile-calculation-from-histograms). + +### 5.5 show_connections + +**Source Tables:** +- MySQL: `stats.stats_mysql_connection_pool`, `stats.stats_mysql_free_connections` +- PostgreSQL: `stats.stats_pgsql_connection_pool`, `stats.stats_pgsql_free_connections` + +**SQL Query (main):** + +```sql +SELECT hostgroup, srv_host, srv_port, status, + ConnUsed, ConnFree, ConnOK, ConnERR, MaxConnUsed, + Queries, Queries_GTID_sync, Bytes_data_sent, Bytes_data_recv, Latency_us +FROM stats.stats_mysql_connection_pool +WHERE (hostgroup = ? OR ? IS NULL) + AND (status = ? OR ? IS NULL) +ORDER BY hostgroup, srv_host, srv_port; +``` + +**SQL Query (detail - MySQL):** + +```sql +SELECT fd, hostgroup, srv_host, srv_port, user, schema AS database, + init_connect, time_zone, sql_mode, autocommit, idle_ms +FROM stats.stats_mysql_free_connections +WHERE (hostgroup = ? OR ? IS NULL); +``` + +**SQL Query (detail - PostgreSQL):** + +```sql +SELECT fd, hostgroup, srv_host, srv_port, user, database, + init_connect, time_zone, sql_mode, idle_ms +FROM stats.stats_pgsql_free_connections +WHERE (hostgroup = ? OR ? IS NULL); +``` + +**PostgreSQL Notes:** +- The `stats_pgsql_free_connections` table uses `database` column (MySQL uses `schema`) +- The `stats_pgsql_free_connections` table does not have the `autocommit` column +- The `stats_pgsql_connection_pool` table does not have the `Queries_GTID_sync` column + +**Calculated Fields:** + +```cpp +for (auto& server : servers) { + int used = server["conn_used"].get(); + int free = server["conn_free"].get(); + int total = used + free; + + server["utilization_pct"] = total > 0 ? (double)used / total * 100 : 0; + + int ok = server["conn_ok"].get(); + int err = server["conn_err"].get(); + int total_conns = ok + err; + + server["error_rate"] = total_conns > 0 ? (double)err / total_conns : 0; +} +``` + +### 5.6 show_errors + +**Source Tables:** +- MySQL: `stats.stats_mysql_errors` (uses `schemaname` column, `errno` for error codes) +- PostgreSQL: `stats.stats_pgsql_errors` (uses `database` column, `sqlstate` for error codes) + +**SQL Query (MySQL):** + +```sql +SELECT hostgroup, hostname, port, username, client_address, + schemaname AS database, errno, count_star, first_seen, last_seen, last_error +FROM stats.stats_mysql_errors +WHERE (count_star >= ? OR ? IS NULL) + AND (errno = ? OR ? IS NULL) + AND (username = ? OR ? IS NULL) + AND (schemaname = ? OR ? IS NULL) -- database parameter maps to schemaname column +ORDER BY count_star DESC +LIMIT ? OFFSET ?; +``` + +**SQL Query (PostgreSQL):** + +```sql +SELECT hostgroup, hostname, port, username, client_address, + database, sqlstate, count_star, first_seen, last_seen, last_error +FROM stats.stats_pgsql_errors +WHERE (count_star >= ? OR ? IS NULL) + AND (sqlstate = ? OR ? IS NULL) + AND (username = ? OR ? IS NULL) + AND (database = ? OR ? IS NULL) -- database parameter maps to database column +ORDER BY count_star DESC +LIMIT ? OFFSET ?; +``` + +**Note:** The tool normalizes to `database` field name in responses for consistency across both databases. Error codes use `errno` for MySQL and `sqlstate` for PostgreSQL as these are fundamentally different concepts. + +**Calculated Fields:** + +```cpp +for (auto& error : errors) { + int count = error["count_star"].get(); + int first = error["first_seen"].get(); + int last = error["last_seen"].get(); + + double hours = (last - first) / 3600.0; + error["frequency_per_hour"] = hours > 0 ? count / hours : count; +} +``` + +### 5.7 show_cluster + +**Source Tables (shared):** +- `stats.stats_proxysql_servers_status` +- `stats.stats_proxysql_servers_metrics` +- `stats.stats_proxysql_servers_checksums` + +**SQL Queries:** + +```sql +-- Node status +SELECT hostname, port, weight, master, global_version, + check_age_us, ping_time_us, checks_OK, checks_ERR +FROM stats.stats_proxysql_servers_status +WHERE hostname = ? OR ? IS NULL; + +-- Node metrics +SELECT hostname, port, weight, response_time_ms, Uptime_s, + last_check_ms, Queries, Client_Connections_connected, Client_Connections_created +FROM stats.stats_proxysql_servers_metrics; + +-- Configuration checksums +SELECT hostname, port, name, version, epoch, checksum, + changed_at, updated_at, diff_check +FROM stats.stats_proxysql_servers_checksums; +``` + +**Health Calculation:** + +```cpp +std::string calculate_cluster_health(const json& nodes) { + int total = nodes.size(); + int healthy = 0; + + for (const auto& node : nodes) { + int ok = node["checks_ok"].get(); + int err = node["checks_err"].get(); + double success_rate = (ok + err) > 0 ? (double)ok / (ok + err) : 0; + + if (success_rate >= 0.95) healthy++; + } + + if (healthy == total) return "healthy"; + if (healthy >= total / 2) return "degraded"; + return "unhealthy"; +} +``` + +### 5.8 show_connection_history + +**Source Tables:** +- Global: `mysql_connections`, `mysql_connections_hour`, `myhgm_connections`, `myhgm_connections_hour` +- Per-server: `history_stats_mysql_connection_pool` + +**SQL Queries:** + +```sql +-- Global connections (raw) +SELECT timestamp, Client_Connections_aborted, Client_Connections_connected, + Client_Connections_created, Server_Connections_aborted, Server_Connections_connected, + Server_Connections_created, ConnPool_get_conn_failure, ConnPool_get_conn_immediate, + ConnPool_get_conn_success, Questions, Slow_queries, GTID_consistent_queries +FROM mysql_connections +WHERE timestamp BETWEEN ? AND ? +ORDER BY timestamp; + +-- Global connections (hourly) +SELECT timestamp, Client_Connections_aborted, Client_Connections_connected, + Client_Connections_created, Server_Connections_aborted, Server_Connections_connected, + Server_Connections_created, ConnPool_get_conn_failure, ConnPool_get_conn_immediate, + ConnPool_get_conn_success, Questions, Slow_queries, GTID_consistent_queries +FROM mysql_connections_hour +WHERE timestamp BETWEEN ? AND ? +ORDER BY timestamp; + +-- MyHGM connections (raw) +SELECT timestamp, MyHGM_myconnpoll_destroy, MyHGM_myconnpoll_get, + MyHGM_myconnpoll_get_ok, MyHGM_myconnpoll_push, MyHGM_myconnpoll_reset +FROM myhgm_connections +WHERE timestamp BETWEEN ? AND ? +ORDER BY timestamp; + +-- Per-server history +SELECT timestamp, hostgroup, srv_host, srv_port, status, + ConnUsed, ConnFree, ConnOK, ConnERR, MaxConnUsed, + Queries, Queries_GTID_sync, Bytes_data_sent, Bytes_data_recv, Latency_us +FROM history_stats_mysql_connection_pool +WHERE timestamp BETWEEN ? AND ? + AND (hostgroup = ? OR ? IS NULL) +ORDER BY timestamp, hostgroup, srv_host; +``` + +### 5.9 show_query_history + +**Source Tables:** +- MySQL: `history_mysql_query_digest` (uses `schemaname` column) +- PostgreSQL: `history_pgsql_query_digest` (uses `schemaname` column) + +**Note:** Both MySQL and PostgreSQL history tables use `schemaname` column. This differs from the live `stats_pgsql_query_digest` table which uses `database`. The tool normalizes to `database` in responses. + +**SQL Query (MySQL):** + +```sql +SELECT dump_time, hostgroup, schemaname AS database, username, client_address, + digest, digest_text, count_star, first_seen, last_seen, + sum_time AS sum_time_us, min_time AS min_time_us, max_time AS max_time_us, + sum_rows_affected, sum_rows_sent +FROM history_mysql_query_digest +WHERE (dump_time = ? OR ? IS NULL) + AND (dump_time >= ? OR ? IS NULL) + AND (dump_time <= ? OR ? IS NULL) + AND (digest = ? OR ? IS NULL) + AND (username = ? OR ? IS NULL) + AND (schemaname = ? OR ? IS NULL) -- database parameter maps to schemaname column +ORDER BY dump_time DESC, count_star DESC +LIMIT ? OFFSET ?; +``` + +**SQL Query (PostgreSQL):** + +```sql +-- Note: history_pgsql_query_digest uses 'schemaname' (unlike live stats_pgsql_query_digest which uses 'database') +SELECT dump_time, hostgroup, schemaname AS database, username, client_address, + digest, digest_text, count_star, first_seen, last_seen, + sum_time AS sum_time_us, min_time AS min_time_us, max_time AS max_time_us, + sum_rows_affected, sum_rows_sent +FROM history_pgsql_query_digest +WHERE (dump_time = ? OR ? IS NULL) + AND (dump_time >= ? OR ? IS NULL) + AND (dump_time <= ? OR ? IS NULL) + AND (digest = ? OR ? IS NULL) + AND (username = ? OR ? IS NULL) + AND (schemaname = ? OR ? IS NULL) -- database parameter maps to schemaname column +ORDER BY dump_time DESC, count_star DESC +LIMIT ? OFFSET ?; +``` + +**Grouping by Snapshot:** + +```cpp +json group_by_snapshot(SQLite3_result* resultset) { + std::map snapshots; + + for (each row in resultset) { + int dump_time = atoi(row->fields[0]); + if (snapshots.find(dump_time) == snapshots.end()) { + snapshots[dump_time] = json::array(); + } + snapshots[dump_time].push_back(row_to_json(row)); + } + + json result = json::array(); + for (const auto& [dump_time, queries] : snapshots) { + json snapshot; + snapshot["dump_time"] = dump_time; + snapshot["queries"] = queries; + result.push_back(snapshot); + } + return result; +} +``` + +### 5.10 show_query_log + +**Source Tables:** +- Memory: `stats.stats_mysql_query_events` +- Disk: `history_mysql_query_events` + +**Note:** This tool is MySQL-only. The `id` column is used internally for row management and is not exposed in the response. + +**SQL Query:** + +```sql +SELECT thread_id, username, schemaname AS database, start_time, end_time, + query_digest, query, server, client, event_type, hid, + extra_info, affected_rows, last_insert_id, rows_sent, + client_stmt_id, gtid, errno, error +FROM stats.stats_mysql_query_events -- or history_mysql_query_events for disk +WHERE (username = ? OR ? IS NULL) + AND (schemaname = ? OR ? IS NULL) -- database parameter maps to schemaname column + AND (query_digest = ? OR ? IS NULL) + AND (server = ? OR ? IS NULL) + AND (errno = ? OR ? IS NULL) + AND (errno != 0 OR ? = 0) -- errors_only filter + AND (start_time >= ? OR ? IS NULL) + AND (start_time <= ? OR ? IS NULL) +ORDER BY start_time DESC +LIMIT ? OFFSET ?; +``` + +--- + +## 6. Helper Functions + +### 6.1 Percentile Calculation from Histograms + +The `stats_mysql_commands_counters` table provides latency histograms. To calculate percentiles: + +```cpp +struct HistogramBucket { + int threshold_us; + int count; +}; + +std::vector bucket_thresholds = { + 100, 500, 1000, 5000, 10000, 50000, 100000, 500000, 1000000, 5000000, 10000000, INT_MAX +}; + +int calculate_percentile(const std::vector& bucket_counts, double percentile) { + int total = 0; + for (int count : bucket_counts) { + total += count; + } + + int target = total * percentile; + int cumulative = 0; + + for (size_t i = 0; i < bucket_counts.size(); i++) { + cumulative += bucket_counts[i]; + if (cumulative >= target) { + return bucket_thresholds[i]; + } + } + + return bucket_thresholds.back(); +} + +json calculate_percentiles(SQLite3_row* row) { + std::vector counts = { + atoi(row->fields[3]), // cnt_100us + atoi(row->fields[4]), // cnt_500us + atoi(row->fields[5]), // cnt_1ms + atoi(row->fields[6]), // cnt_5ms + atoi(row->fields[7]), // cnt_10ms + atoi(row->fields[8]), // cnt_50ms + atoi(row->fields[9]), // cnt_100ms + atoi(row->fields[10]), // cnt_500ms + atoi(row->fields[11]), // cnt_1s + atoi(row->fields[12]), // cnt_5s + atoi(row->fields[13]), // cnt_10s + atoi(row->fields[14]) // cnt_INFs + }; + + json percentiles; + percentiles["p50_us"] = calculate_percentile(counts, 0.50); + percentiles["p90_us"] = calculate_percentile(counts, 0.90); + percentiles["p95_us"] = calculate_percentile(counts, 0.95); + percentiles["p99_us"] = calculate_percentile(counts, 0.99); + + return percentiles; +} +``` + +### 6.2 SQLite Result to JSON Conversion + +```cpp +json resultset_to_json(SQLite3_result* resultset, int cols) { + json rows = json::array(); + + if (!resultset || resultset->rows_count == 0) { + return rows; + } + + for (size_t i = 0; i < resultset->rows_count; i++) { + SQLite3_row* row = resultset->rows[i]; + json obj; + + for (int j = 0; j < cols; j++) { + const char* field = row->fields[j]; + const char* column = resultset->column_definition[j]->name; + + if (field == nullptr) { + obj[column] = nullptr; + } else if (is_numeric(field)) { + // Try to parse as integer first, then as double + char* endptr; + long long ll = strtoll(field, &endptr, 10); + if (*endptr == '\0') { + obj[column] = ll; + } else { + obj[column] = std::stod(field); + } + } else { + obj[column] = field; + } + } + rows.push_back(obj); + } + + return rows; +} + +bool is_numeric(const char* str) { + if (str == nullptr || *str == '\0') return false; + + char* endptr; + strtod(str, &endptr); + return *endptr == '\0'; +} +``` + +### 6.3 Time Range Builder + +```cpp +std::pair get_time_range(const std::string& interval) { + auto it = interval_map.find(interval); + if (it == interval_map.end()) { + throw std::invalid_argument("Invalid interval: " + interval); + } + + time_t now = time(NULL); + time_t start = now - it->second.seconds; + + return {start, now}; +} +``` + +--- + +## 7. Error Handling Patterns + +### 7.1 Standard Error Response + +```cpp +json create_error_response(const std::string& message) { + json response; + response["success"] = false; + response["error"] = message; + return response; +} + +json create_success_response(const json& result) { + json response; + response["success"] = true; + response["result"] = result; + return response; +} +``` + +### 7.2 Common Error Scenarios + +**Database Query Failure:** + +```cpp +if (rc != SQLITE_OK) { + std::string err_msg = error ? error : "Query execution failed"; + if (error) free(error); + return create_error_response(err_msg); +} +``` + +**Invalid Parameters:** + +```cpp +if (!arguments.contains("required_param")) { + return create_error_response("Missing required parameter: required_param"); +} + +std::string value = arguments["param"]; +if (!is_valid_value(value)) { + return create_error_response("Invalid value for parameter 'param': " + value); +} +``` + +**PostgreSQL Not Supported:** + +```cpp +std::string db_type = arguments.value("db_type", "mysql"); +if (db_type == "pgsql") { + return create_error_response("PostgreSQL is not supported for this tool. Historical connection data is only available for MySQL."); +} +``` + +**Empty Result Set:** + +```cpp +if (!resultset || resultset->rows_count == 0) { + json result; + result["message"] = "No data found"; + result["data"] = json::array(); + return create_success_response(result); +} +``` + +--- + +## 8. Testing Strategies + +### 8.1 Unit Tests + +Test each handler function independently: + +```cpp +TEST(StatsToolHandler, ShowStatus) { + Stats_Tool_Handler handler(GloMCPH); + handler.init(); + + json args; + args["db_type"] = "mysql"; + args["category"] = "connections"; + + json response = handler.execute_tool("show_status", args); + + ASSERT_TRUE(response["success"].get()); + ASSERT_TRUE(response["result"].contains("variables")); + ASSERT_GT(response["result"]["variables"].size(), 0); +} + +TEST(StatsToolHandler, ShowStatusWithVariableFilter) { + Stats_Tool_Handler handler(GloMCPH); + handler.init(); + + json args; + args["db_type"] = "mysql"; + args["variable_name"] = "Client_Connections_%"; + + json response = handler.execute_tool("show_status", args); + + ASSERT_TRUE(response["success"].get()); + for (const auto& var : response["result"]["variables"]) { + std::string name = var["variable_name"].get(); + ASSERT_TRUE(name.find("Client_Connections_") == 0); + } +} +``` + +### 8.2 Integration Tests + +Test with actual ProxySQL instance: + +```bash +# Start ProxySQL with test configuration +proxysql -f -c test_proxysql.cnf & + +# Generate some traffic +mysql -h 127.0.0.1 -P6033 -utest -ptest -e "SELECT 1" & + +# Test MCP endpoint +curl -X POST http://localhost:6071/mcp/stats \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer test-token" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "show_queries", + "arguments": {"db_type": "mysql", "limit": 10} + }, + "id": 1 + }' + +# Verify response structure +# ... +``` + +### 8.3 Test Data Setup + +```sql +-- Populate test data via admin interface +-- (Note: Most stats tables are read-only and populated by ProxySQL internally) + +-- For testing historical tables, wait for timer-based collection +-- or manually trigger collection via internal mechanisms + +-- For testing query events, generate queries and then flush +SELECT 1; +SELECT 2; +-- Admin: DUMP EVENTSLOG FROM BUFFER TO MEMORY; +``` + +### 8.4 Edge Cases to Test + +1. **Empty tables** — Ensure graceful handling when no data exists +2. **Large result sets** — Test with limit parameter, verify truncation +3. **Invalid parameters** — Test error responses for bad input +4. **PostgreSQL fallback** — Test error messages for unsupported PostgreSQL operations +5. **Time range boundaries** — Test historical queries at retention boundaries (7 days, 365 days) +6. **Concurrent access** — Test behavior under concurrent tool calls diff --git a/doc/MCP/MCP_Stats_Tools_Spec.md b/doc/MCP/MCP_Stats_Tools_Spec.md new file mode 100644 index 000000000..34bef2cf4 --- /dev/null +++ b/doc/MCP/MCP_Stats_Tools_Spec.md @@ -0,0 +1,1357 @@ +# ProxySQL Stats MCP Tools - Specification + +This document specifies the MCP tools available on the `/stats` endpoint for monitoring and analyzing ProxySQL statistics. + +## Table of Contents + +- [1. Overview](#1-overview) +- [2. Response Format Convention](#2-response-format-convention) +- [3. Tool Categories](#3-tool-categories) +- [4. Live Data Tools](#4-live-data-tools) + - [4.1 show_status](#41-show_status) + - [4.2 show_processlist](#42-show_processlist) + - [4.3 show_queries](#43-show_queries) + - [4.4 show_commands](#44-show_commands) + - [4.5 show_connections](#45-show_connections) + - [4.6 show_errors](#46-show_errors) + - [4.7 show_users](#47-show_users) + - [4.8 show_client_cache](#48-show_client_cache) + - [4.9 show_query_rules](#49-show_query_rules) + - [4.10 show_prepared_statements](#410-show_prepared_statements) + - [4.11 show_gtid](#411-show_gtid) + - [4.12 show_cluster](#412-show_cluster) +- [5. Historical Data Tools](#5-historical-data-tools) + - [5.1 show_system_history](#51-show_system_history) + - [5.2 show_query_cache_history](#52-show_query_cache_history) + - [5.3 show_connection_history](#53-show_connection_history) + - [5.4 show_query_history](#54-show_query_history) +- [6. Utility Tools](#6-utility-tools) + - [6.1 flush_query_log](#61-flush_query_log) + - [6.2 show_query_log](#62-show_query_log) + - [6.3 flush_queries](#63-flush_queries) + +--- + +## 1. Overview + +The `/stats` endpoint provides tools for monitoring ProxySQL performance, health, and operational metrics. Tools are organized into three categories: + +- **Live Data Tools** - Real-time statistics and current state +- **Historical Data Tools** - Time-series data for trend analysis +- **Utility Tools** - Data management operations (flush, sync) + +All tools support both MySQL and PostgreSQL where applicable, controlled by a `db_type` parameter. + +--- + +## 2. Response Format Convention + +All tools follow the MCP JSON-RPC response format with success/error wrappers. + +**Success Response:** +```json +{ + "success": true, + "result": { + // Tool-specific result data + } +} +``` + +**Error Response:** +```json +{ + "success": false, + "error": "Error message describing what went wrong" +} +``` + +--- + +## 2.1 Time Field Conventions + +All tools follow consistent naming conventions for time-related fields: + +| Suffix | Unit | Example Fields | +|--------|------|----------------| +| `_us` | Microseconds | `sum_time_us`, `min_time_us`, `max_time_us`, `latency_us`, `ping_time_us` | +| `_ms` | Milliseconds | `time_ms`, `idle_ms` | +| (none) | Unix timestamp (seconds since epoch) | `first_seen`, `last_seen`, `timestamp`, `dump_time` | + +**Notes:** +- Response field names may differ from database column names for clarity and consistency +- All duration/latency measurements use microseconds (`_us`) unless milliseconds provide more natural values (e.g., session duration) +- All timestamps are Unix epoch timestamps in seconds + +--- + +## 3. Tool Categories + +### Live Data Tools (12 tools) + +| Tool | Description | +|---|---| +| `show_status` | Global status variables and metrics | +| `show_processlist` | Currently active sessions | +| `show_queries` | Query digest performance statistics | +| `show_commands` | Command execution counters with latency histograms | +| `show_connections` | Backend connection pool metrics | +| `show_errors` | Error tracking and frequency | +| `show_users` | User connection statistics | +| `show_client_cache` | Client host error cache | +| `show_query_rules` | Query rule hit counts | +| `show_prepared_statements` | Prepared statement information | +| `show_gtid` | GTID replication status (MySQL only) | +| `show_cluster` | ProxySQL cluster node health | + +### Historical Data Tools (4 tools) + +| Tool | Description | +|---|---| +| `show_system_history` | CPU and memory trends over time | +| `show_query_cache_history` | Query cache performance trends | +| `show_connection_history` | Connection metrics history | +| `show_query_history` | Query digest snapshots over time | + +### Utility Tools (3 tools) + +| Tool | Description | +|---|---| +| `flush_query_log` | Flush query events from buffer to tables | +| `show_query_log` | View query event audit log | +| `flush_queries` | Save query digest statistics to disk | + +--- + +## 4. Live Data Tools + +### 4.1 show_status + +Returns global status variables and metrics from ProxySQL. Similar to MySQL's `SHOW STATUS` command. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `category` | string | No | (none) | Filter by category: `connections`, `queries`, `commands`, `pool_ops`, `monitor`, `query_cache`, `prepared_stmts`, `security`, `memory`, `errors`, `logger`, `system`, `mirror` | +| `variable_name` | string | No | (none) | Filter by variable name pattern (supports SQL LIKE with `%` wildcards) | + +**Note:** If neither `category` nor `variable_name` is provided, returns all variables grouped by category. + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "variables": [ + { + "variable_name": "Client_Connections_connected", + "value": "245", + "description": "Currently connected clients" + }, + { + "variable_name": "Client_Connections_created", + "value": "15432", + "description": "Total client connections ever created" + }, + { + "variable_name": "Questions", + "value": "156789", + "description": "Total queries processed" + } + ] + } +} +``` + +**PostgreSQL Notes:** PostgreSQL has ~59 variables compared to MySQL's ~129. Some variable names differ (e.g., `pgsql_backend_buffers_bytes` instead of `mysql_backend_buffers_bytes`). + +--- + +### 4.2 show_processlist + +Shows all currently active sessions being processed by ProxySQL. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `username` | string | No | (none) | Filter by username | +| `hostgroup` | int | No | (none) | Filter by hostgroup ID | +| `min_time_ms` | int | No | (none) | Only show sessions running longer than N milliseconds | +| `limit` | int | No | `100` | Maximum number of sessions to return | +| `offset` | int | No | `0` | Skip first N results | + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "total_sessions": 156, + "sessions": [ + { + "session_id": 12345, + "thread_id": 67, + "user": "app_user", + "database": "production_db", + "client_host": "10.0.1.50", + "client_port": 54321, + "hostgroup": 10, + "backend_host": "db-server-01", + "backend_port": 3306, + "command": "Query", + "time_ms": 234, + "info": "SELECT * FROM orders WHERE status = 'pending' LIMIT 1000" + } + ], + "summary": { + "by_user": { + "app_user": 120, + "admin_user": 25, + "report_user": 11 + }, + "by_hostgroup": { + "10": 100, + "20": 45, + "30": 11 + }, + "by_command": { + "Query": 145, + "Sleep": 8, + "Connect": 3 + } + } + } +} +``` + +**PostgreSQL Notes:** PostgreSQL processlist includes additional columns `backend_pid` and `backend_state`. + +--- + +### 4.3 show_queries + +Returns aggregated query performance statistics by digest pattern. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `sort_by` | `"count"` \| `"avg_time"` \| `"sum_time"` \| `"max_time"` \| `"rows_sent"` | No | `"count"` | Sort order | +| `limit` | int | No | `100` | Maximum number of results | +| `offset` | int | No | `0` | Skip first N results | +| `min_count` | int | No | (none) | Only show queries executed at least N times | +| `min_time_us` | int | No | (none) | Only show queries with avg time >= N microseconds | +| `database` | string | No | (none) | Filter by database name | +| `username` | string | No | (none) | Filter by username | +| `hostgroup` | int | No | (none) | Filter by hostgroup ID | +| `digest` | string | No | (none) | Filter by specific query digest | + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "total_digests": 1234, + "queries": [ + { + "digest": "0x3A2B4C5D6E7F8A9B", + "digest_text": "SELECT * FROM orders WHERE status = ?", + "hostgroup": 10, + "database": "production_db", + "username": "app_user", + "client_address": "10.0.1.50", + "count_star": 15234, + "first_seen": 1737440000, + "last_seen": 1737446400, + "sum_time_us": 45678900, + "min_time_us": 1200, + "max_time_us": 234500, + "avg_time_us": 2998, + "sum_rows_affected": 0, + "sum_rows_sent": 15234000 + } + ], + "summary": { + "total_queries": 156789, + "total_time_us": 987654321 + } + } +} +``` + +**PostgreSQL Notes:** MySQL uses the `schemaname` column while PostgreSQL uses the `database` column internally. The tool normalizes both to the `database` field in responses for consistency. + +--- + +### 4.4 show_commands + +Returns command execution statistics with latency distribution histograms. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `command` | string | No | (none) | Filter by specific command (SELECT, INSERT, etc.) | +| `limit` | int | No | `100` | Maximum number of commands to return | +| `offset` | int | No | `0` | Skip first N results | + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "commands": [ + { + "command": "SELECT", + "total_count": 98765, + "total_time_us": 234567890, + "avg_time_us": 2375, + "latency_histogram": { + "cnt_100us": 1000, + "cnt_500us": 5000, + "cnt_1ms": 15000, + "cnt_5ms": 40000, + "cnt_10ms": 25000, + "cnt_50ms": 10000, + "cnt_100ms": 2500, + "cnt_500ms": 200, + "cnt_1s": 50, + "cnt_5s": 10, + "cnt_10s": 5, + "cnt_INFs": 0 + }, + "percentiles": { + "p50_us": 6500, + "p90_us": 35000, + "p95_us": 75000, + "p99_us": 120000 + } + }, + { + "command": "INSERT", + "total_count": 45678, + "total_time_us": 123456789, + "avg_time_us": 2702, + "latency_histogram": { + "cnt_100us": 500, + "cnt_500us": 2000, + "cnt_1ms": 8000, + "cnt_5ms": 20000, + "cnt_10ms": 10000, + "cnt_50ms": 4000, + "cnt_100ms": 1000, + "cnt_500ms": 150, + "cnt_1s": 25, + "cnt_5s": 3, + "cnt_10s": 0, + "cnt_INFs": 0 + }, + "percentiles": { + "p50_us": 5200, + "p90_us": 28000, + "p95_us": 55000, + "p99_us": 95000 + } + } + ], + "summary": { + "total_commands": 245000, + "total_time_us": 567890123 + } + } +} +``` + +--- + +### 4.5 show_connections + +Returns backend connection pool metrics per server. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `hostgroup` | int | No | (none) | Filter by hostgroup ID | +| `server` | string | No | (none) | Filter by server (format: `host:port`) | +| `status` | `"ONLINE"` \| `"SHUNNED"` \| `"OFFLINE_SOFT"` \| `"OFFLINE_HARD"` | No | (none) | Filter by server status | +| `detail` | bool | No | `false` | Include free connection details | + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "servers": [ + { + "hostgroup": 10, + "srv_host": "db-server-01", + "srv_port": 3306, + "status": "ONLINE", + "conn_used": 45, + "conn_free": 55, + "conn_ok": 123456, + "conn_err": 23, + "max_conn_used": 100, + "queries": 456789, + "queries_gtid_sync": 450000, + "bytes_data_sent": 56789012, + "bytes_data_recv": 1234567890, + "latency_us": 1500, + "utilization_pct": 45.0, + "error_rate": 0.00019 + } + ], + "summary": { + "total_servers": 5, + "online_servers": 4, + "total_conn_used": 180, + "total_conn_free": 320, + "total_queries": 2345678, + "overall_utilization_pct": 36.0, + "by_status": { + "ONLINE": 4, + "SHUNNED": 1, + "OFFLINE_SOFT": 0, + "OFFLINE_HARD": 0 + } + } + } +} +``` + +**Response with `detail=true`:** + +When `detail=true`, includes an additional `free_connections` array showing individual idle connections: + +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "servers": [...], + "free_connections": [ + { + "fd": 123, + "hostgroup": 10, + "srv_host": "db-server-01", + "srv_port": 3306, + "user": "app_user", + "schema": "production_db", + "init_connect": "SET time_zone='UTC'", + "time_zone": "UTC", + "sql_mode": "STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION", + "autocommit": "1", + "idle_ms": 15000 + } + ], + "summary": {...} + } +} +``` + +**PostgreSQL Notes:** PostgreSQL does not have the `queries_gtid_sync` column. + +--- + +### 4.6 show_errors + +Returns error tracking statistics with frequency analysis. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `errno` | int | No | (none) | Filter by error number (MySQL) | +| `sqlstate` | string | No | (none) | Filter by SQLSTATE (PostgreSQL) | +| `username` | string | No | (none) | Filter by username | +| `database` | string | No | (none) | Filter by database name | +| `hostgroup` | int | No | (none) | Filter by hostgroup ID | +| `min_count` | int | No | (none) | Only show errors with count >= N | +| `sort_by` | `"count"` \| `"first_seen"` \| `"last_seen"` | No | `"count"` | Sort order | +| `limit` | int | No | `100` | Maximum number of results | +| `offset` | int | No | `0` | Skip first N results | + +**Response (MySQL):** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "total_error_types": 12, + "total_error_count": 234, + "errors": [ + { + "hostgroup": 10, + "hostname": "db-server-01", + "port": 3306, + "username": "app_user", + "client_address": "10.0.1.50", + "database": "production_db", + "errno": 1062, + "count_star": 45, + "first_seen": 1737440000, + "last_seen": 1737446400, + "last_error": "Duplicate entry '123' for key 'PRIMARY'", + "frequency_per_hour": 7.5 + } + ], + "summary": { + "by_errno": { + "1062": 45, + "1045": 12, + "2003": 5 + }, + "by_hostgroup": { + "10": 42, + "20": 20 + } + } + } +} +``` + +**Response (PostgreSQL):** + +PostgreSQL uses `sqlstate` instead of `errno`: + +```json +{ + "success": true, + "result": { + "db_type": "pgsql", + "total_error_types": 8, + "total_error_count": 156, + "errors": [ + { + "hostgroup": 10, + "hostname": "pg-server-01", + "port": 5432, + "username": "app_user", + "client_address": "10.0.1.50", + "database": "production_db", + "sqlstate": "23505", + "count_star": 32, + "first_seen": 1737440000, + "last_seen": 1737446400, + "last_error": "duplicate key value violates unique constraint", + "frequency_per_hour": 5.3 + } + ], + "summary": { + "by_sqlstate": { + "23505": 32, + "28P01": 8, + "08006": 3 + }, + "by_hostgroup": { + "10": 30, + "20": 13 + } + } + } +} +``` + +--- + +### 4.7 show_users + +Returns connection statistics per user. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `username` | string | No | (none) | Filter by specific username | +| `limit` | int | No | `100` | Maximum number of users to return | +| `offset` | int | No | `0` | Skip first N results | + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "users": [ + { + "username": "app_user", + "frontend_connections": 120, + "frontend_max_connections": 200, + "utilization_pct": 60.0, + "status": "normal" + }, + { + "username": "admin_user", + "frontend_connections": 5, + "frontend_max_connections": 10, + "utilization_pct": 50.0, + "status": "normal" + }, + { + "username": "batch_user", + "frontend_connections": 48, + "frontend_max_connections": 50, + "utilization_pct": 96.0, + "status": "near_limit" + } + ], + "summary": { + "total_users": 15, + "total_connections": 245, + "total_capacity": 500, + "overall_utilization_pct": 49.0 + } + } +} +``` + +**Status Values:** +- `normal` - Below 80% utilization +- `near_limit` - Between 80% and 100% utilization +- `at_limit` - At 100% utilization + +--- + +### 4.8 show_client_cache + +Returns client host error cache for connection throttling analysis. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `client_address` | string | No | (none) | Filter by specific client IP | +| `min_error_count` | int | No | (none) | Only show hosts with error count >= N | +| `limit` | int | No | `100` | Maximum number of hosts to return | +| `offset` | int | No | `0` | Skip first N results | + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "hosts": [ + { + "client_address": "10.0.1.50", + "error_count": 15, + "last_updated": 1737446400 + }, + { + "client_address": "10.0.1.51", + "error_count": 3, + "last_updated": 1737445000 + } + ], + "summary": { + "total_hosts": 12, + "total_errors": 45 + } + } +} +``` + +--- + +### 4.9 show_query_rules + +Returns query rule hit statistics. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `rule_id` | int | No | (none) | Filter by specific rule ID | +| `min_hits` | int | No | (none) | Only show rules with hits >= N | +| `include_zero_hits` | bool | No | `false` | Include rules with zero hits | +| `limit` | int | No | `100` | Maximum number of rules to return | +| `offset` | int | No | `0` | Skip first N results | + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "total_rules": 50, + "rules": [ + { + "rule_id": 1, + "hits": 45678 + }, + { + "rule_id": 2, + "hits": 23456 + }, + { + "rule_id": 5, + "hits": 12345 + } + ], + "summary": { + "total_hits": 234567, + "rules_with_hits": 35, + "rules_without_hits": 15 + } + } +} +``` + +--- + +### 4.10 show_prepared_statements + +Returns prepared statement information. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `username` | string | No | (none) | Filter by username | +| `database` | string | No | (none) | Filter by database name | + +**Response (MySQL):** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "total_statements": 45, + "statements": [ + { + "global_stmt_id": 1, + "database": "production_db", + "username": "app_user", + "digest": "0x3A2B4C5D6E7F8A9B", + "ref_count_client": 10, + "ref_count_server": 5, + "num_columns": 8, + "num_params": 2, + "query": "SELECT * FROM orders WHERE id = ? AND status = ?" + } + ] + } +} +``` + +**Response (PostgreSQL):** + +PostgreSQL uses `num_param_types` instead of `num_columns`/`num_params`: + +```json +{ + "success": true, + "result": { + "db_type": "pgsql", + "total_statements": 32, + "statements": [ + { + "global_stmt_id": 1, + "database": "production_db", + "username": "app_user", + "digest": "0x3A2B4C5D6E7F8A9B", + "ref_count_client": 8, + "ref_count_server": 4, + "num_param_types": 2, + "query": "SELECT * FROM orders WHERE id = $1 AND status = $2" + } + ] + } +} +``` + +--- + +### 4.11 show_gtid + +Returns GTID (Global Transaction ID) replication information. + +**Note:** This tool is MySQL-only. GTID is a MySQL-specific replication feature. There is no `db_type` parameter. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `hostname` | string | No | (none) | Filter by backend server hostname | +| `port` | int | No | (none) | Filter by backend server port | + +**Response:** +```json +{ + "success": true, + "result": { + "servers": [ + { + "hostname": "db-server-01", + "port": 3306, + "gtid_executed": "3E11FA47-71CA-11E1-9E33-C80AA9429562:1-12345:23456-56789", + "events": 678901 + }, + { + "hostname": "db-server-02", + "port": 3306, + "gtid_executed": "3E11FA47-71CA-11E1-9E33-C80AA9429562:1-12340", + "events": 678500 + } + ], + "summary": { + "total_servers": 3, + "total_events": 2036703 + } + } +} +``` + +--- + +### 4.12 show_cluster + +Returns ProxySQL cluster node health, synchronization status, and configuration checksums. + +**Note:** This tool does not have a `db_type` parameter as cluster tables are shared. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `hostname` | string | No | (none) | Filter by specific node hostname | +| `include_checksums` | bool | No | `true` | Include configuration checksums | + +**Response:** +```json +{ + "success": true, + "result": { + "cluster_health": "healthy", + "total_nodes": 3, + "online_nodes": 3, + "master_node": "proxysql-01:6032", + "nodes": [ + { + "hostname": "proxysql-01", + "port": 6032, + "weight": 1000, + "master": true, + "global_version": 100, + "check_age_us": 50000, + "ping_time_us": 1234, + "checks_ok": 9998, + "checks_err": 2, + "check_success_rate": 0.9998, + "uptime_s": 8640000, + "queries": 567890, + "client_connections": 245 + }, + { + "hostname": "proxysql-02", + "port": 6032, + "weight": 1000, + "master": false, + "global_version": 100, + "check_age_us": 48000, + "ping_time_us": 1456, + "checks_ok": 9995, + "checks_err": 5, + "check_success_rate": 0.9995, + "uptime_s": 8640000, + "queries": 534567, + "client_connections": 230 + } + ], + "checksums": [ + { + "hostname": "proxysql-01", + "port": 6032, + "name": "mysql_servers", + "version": 75, + "epoch": 1737446400, + "checksum": "0x1A2B3C4D5E6F", + "changed_at": 1737440000, + "updated_at": 1737446400, + "diff_check": 0 + }, + { + "hostname": "proxysql-02", + "port": 6032, + "name": "mysql_servers", + "version": 75, + "epoch": 1737446400, + "checksum": "0x1A2B3C4D5E6F", + "changed_at": 1737440000, + "updated_at": 1737446400, + "diff_check": 0 + } + ], + "summary": { + "config_in_sync": true, + "nodes_in_sync": 3, + "nodes_out_of_sync": 0, + "total_queries_all_nodes": 1703456, + "total_client_connections": 735, + "avg_ping_time_us": 1350 + } + } +} +``` + +**Cluster Health Values:** +- `healthy` - All nodes online and in sync +- `degraded` - Some nodes offline or out of sync +- `unhealthy` - Majority of nodes have issues + +--- + +## 5. Historical Data Tools + +### 5.1 show_system_history + +Returns historical CPU and memory usage trends. + +**Note:** This tool does not have a `db_type` parameter as system metrics are shared. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `metric` | `"cpu"` \| `"memory"` \| `"all"` | No | `"all"` | Which metrics to return | +| `interval` | `"30m"` \| `"1h"` \| `"2h"` \| `"4h"` \| `"6h"` \| `"12h"` \| `"1d"` \| `"3d"` \| `"7d"` \| `"30d"` \| `"90d"` | No | `"1h"` | How far back to look | + +**Table Selection:** Intervals ≤12h use raw tables; intervals ≥1d use hourly aggregated tables. + +**Response:** +```json +{ + "success": true, + "result": { + "interval": "1h", + "resolution": "raw", + "cpu": [ + { + "timestamp": 1737446400, + "tms_utime": 12345, + "tms_stime": 5678 + }, + { + "timestamp": 1737446460, + "tms_utime": 12400, + "tms_stime": 5700 + } + ], + "memory": [ + { + "timestamp": 1737446400, + "allocated": 536870912, + "resident": 398458880, + "active": 304087040, + "mapped": 524288000, + "metadata": 10485760, + "retained": 52428800 + }, + { + "timestamp": 1737446460, + "allocated": 540000000, + "resident": 400000000, + "active": 306000000, + "mapped": 525000000, + "metadata": 10500000, + "retained": 52500000 + } + ] + } +} +``` + +**Note:** Memory metrics require jemalloc. If ProxySQL was built without jemalloc (`NOJEM`), the `memory` array will be empty. + +--- + +### 5.2 show_query_cache_history + +Returns historical query cache performance metrics. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `interval` | `"30m"` \| `"1h"` \| `"2h"` \| `"4h"` \| `"6h"` \| `"12h"` \| `"1d"` \| `"3d"` \| `"7d"` \| `"30d"` \| `"90d"` | No | `"1h"` | How far back to look | + +**PostgreSQL Support:** Historical query cache data is only available for MySQL. When `db_type="pgsql"`, this tool returns an error. + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "interval": "1h", + "resolution": "raw", + "data": [ + { + "timestamp": 1737446400, + "count_GET": 45678, + "count_GET_OK": 38234, + "count_SET": 12345, + "bytes_IN": 45678901, + "bytes_OUT": 234567890, + "entries_purged": 123, + "entries_in_cache": 4567, + "memory_bytes": 52428800, + "hit_rate": 0.837 + } + ] + } +} +``` + +--- + +### 5.3 show_connection_history + +Returns historical connection metrics at global or per-server level. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `interval` | `"30m"` \| `"1h"` \| `"2h"` \| `"4h"` \| `"6h"` \| `"12h"` \| `"1d"` \| `"3d"` \| `"7d"` \| `"30d"` \| `"90d"` | No | `"1h"` | How far back to look | +| `scope` | `"global"` \| `"per_server"` \| `"all"` | No | `"global"` | Level of detail | +| `hostgroup` | int | No | (none) | Filter per_server by hostgroup | +| `server` | string | No | (none) | Filter per_server by server (format: `host:port`) | + +**PostgreSQL Support:** Historical connection data is only available for MySQL. When `db_type="pgsql"`, this tool returns an error. + +**Response with `scope="global"`:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "interval": "1h", + "resolution": "raw", + "scope": "global", + "global": { + "connections": [ + { + "timestamp": 1737446400, + "Client_Connections_aborted": 5, + "Client_Connections_connected": 245, + "Client_Connections_created": 1500, + "Server_Connections_aborted": 2, + "Server_Connections_connected": 120, + "Server_Connections_created": 800, + "ConnPool_get_conn_failure": 3, + "ConnPool_get_conn_immediate": 500, + "ConnPool_get_conn_success": 1200, + "Questions": 15678, + "Slow_queries": 12, + "GTID_consistent_queries": 100 + } + ], + "myhgm": [ + { + "timestamp": 1737446400, + "MyHGM_myconnpoll_destroy": 50, + "MyHGM_myconnpoll_get": 1200, + "MyHGM_myconnpoll_get_ok": 1197, + "MyHGM_myconnpoll_push": 1150, + "MyHGM_myconnpoll_reset": 10 + } + ] + } + } +} +``` + +**Response with `scope="per_server"`:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "interval": "1h", + "resolution": "raw", + "scope": "per_server", + "per_server": [ + { + "timestamp": 1737446400, + "hostgroup": 10, + "srv_host": "db-server-01", + "srv_port": 3306, + "status": "ONLINE", + "conn_used": 45, + "conn_free": 55, + "conn_ok": 123456, + "conn_err": 23, + "max_conn_used": 100, + "queries": 5678, + "queries_gtid_sync": 5500, + "bytes_data_sent": 1234567, + "bytes_data_recv": 7654321, + "latency_us": 1500 + } + ] + } +} +``` + +**Response with `scope="all"`:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "interval": "1h", + "resolution": "raw", + "scope": "all", + "global": { + "connections": [...], + "myhgm": [...] + }, + "per_server": [...] + } +} +``` + +--- + +### 5.4 show_query_history + +Returns historical query digest snapshots for trend analysis. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | +| `dump_time` | int | No | (none) | Filter by specific snapshot timestamp | +| `start_time` | int | No | (none) | Start of time range (Unix timestamp) | +| `end_time` | int | No | (none) | End of time range (Unix timestamp) | +| `digest` | string | No | (none) | Filter by specific query digest | +| `username` | string | No | (none) | Filter by username | +| `database` | string | No | (none) | Filter by database name | +| `limit` | int | No | `100` | Maximum results per snapshot | +| `offset` | int | No | `0` | Skip first N results | + +**Note:** Query history is only populated when `SAVE MYSQL DIGEST TO DISK` (or the equivalent for PostgreSQL) has been executed, either manually or via the periodic timer. + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "snapshots": [ + { + "dump_time": 1737446400, + "queries": [ + { + "hostgroup": 10, + "database": "production_db", + "username": "app_user", + "client_address": "10.0.1.50", + "digest": "0x3A2B4C5D6E7F8A9B", + "digest_text": "SELECT * FROM orders WHERE status = ?", + "count_star": 5000, + "first_seen": 1737442800, + "last_seen": 1737446399, + "sum_time_us": 15000000, + "min_time_us": 1200, + "max_time_us": 50000, + "sum_rows_affected": 0, + "sum_rows_sent": 5000000 + } + ] + }, + { + "dump_time": 1737450000, + "queries": [ + { + "hostgroup": 10, + "database": "production_db", + "username": "app_user", + "client_address": "10.0.1.50", + "digest": "0x3A2B4C5D6E7F8A9B", + "digest_text": "SELECT * FROM orders WHERE status = ?", + "count_star": 6200, + "first_seen": 1737446400, + "last_seen": 1737449999, + "sum_time_us": 18600000, + "min_time_us": 1100, + "max_time_us": 55000, + "sum_rows_affected": 0, + "sum_rows_sent": 6200000 + } + ] + } + ], + "summary": { + "total_snapshots": 2, + "earliest_snapshot": 1737446400, + "latest_snapshot": 1737450000 + } + } +} +``` + +--- + +## 6. Utility Tools + +### 6.1 flush_query_log + +Flushes query events from the circular buffer into queryable tables. + +**Note:** This tool is MySQL-only. Query event logging is not available for PostgreSQL. There is no `db_type` parameter. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `destination` | `"memory"` \| `"disk"` \| `"both"` | No | `"memory"` | Where to flush events | + +**Response:** +```json +{ + "success": true, + "result": { + "events_flushed": 1234, + "destination": "memory" + } +} +``` + +**Note:** This operation drains the circular buffer. Events are removed from the buffer after flushing. + +--- + +### 6.2 show_query_log + +Returns individual query execution events from the audit log. + +**Note:** This tool is MySQL-only. Query event logging is not available for PostgreSQL. There is no `db_type` parameter. Query events must be flushed before they become visible. Use `flush_query_log` first. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `source` | `"memory"` \| `"disk"` | No | `"memory"` | Which table to read from | +| `username` | string | No | (none) | Filter by username | +| `database` | string | No | (none) | Filter by database name | +| `query_digest` | string | No | (none) | Filter by digest hash | +| `server` | string | No | (none) | Filter by backend server | +| `errno` | int | No | (none) | Filter by error number | +| `errors_only` | bool | No | `false` | Only show queries with errors | +| `start_time` | int | No | (none) | Start of time range (Unix timestamp) | +| `end_time` | int | No | (none) | End of time range (Unix timestamp) | +| `limit` | int | No | `100` | Maximum results | +| `offset` | int | No | `0` | Skip first N results | + +**Response:** +```json +{ + "success": true, + "result": { + "source": "memory", + "total_events": 1234, + "events": [ + { + "thread_id": 67, + "username": "app_user", + "database": "production_db", + "start_time": 1737446400, + "end_time": 1737446401, + "query_digest": "0x3A2B4C5D6E7F8A9B", + "query": "SELECT * FROM orders WHERE id = 12345", + "server": "db-server-01:3306", + "client": "10.0.1.50:54321", + "event_type": 1, + "hostgroup": 10, + "affected_rows": 0, + "rows_sent": 1, + "errno": 0, + "error": null + } + ], + "summary": { + "total_errors": 5, + "time_range": { + "earliest": 1737440000, + "latest": 1737446400 + } + } + } +} +``` + +--- + +### 6.3 flush_queries + +Saves current query digest statistics to disk and resets the in-memory counters. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|---|---|---|---|---| +| `db_type` | `"mysql"` \| `"pgsql"` | No | `"mysql"` | Database type | + +**Response:** +```json +{ + "success": true, + "result": { + "db_type": "mysql", + "digests_saved": 1234, + "dump_time": 1737446400 + } +} +``` + +**Important:** This operation resets the live query digest statistics. After flushing: +- `show_queries` will return empty results until new queries accumulate +- The saved data can be viewed with `show_query_history` + +--- + +## Appendix: PostgreSQL Support Summary + +| Tool | PostgreSQL Support | +|---|---| +| `show_status` | Full | +| `show_processlist` | Full | +| `show_queries` | Full | +| `show_commands` | Full | +| `show_connections` | Full | +| `show_errors` | Full | +| `show_users` | Full | +| `show_client_cache` | Full | +| `show_query_rules` | Full | +| `show_prepared_statements` | Full | +| `show_gtid` | Not applicable (MySQL only) | +| `show_cluster` | N/A (shared tables) | +| `show_system_history` | N/A (shared tables) | +| `show_query_cache_history` | Not supported (MySQL only data) | +| `show_connection_history` | Not supported (MySQL only data) | +| `show_query_history` | Full | +| `flush_query_log` | Not supported (MySQL only) | +| `show_query_log` | Not supported (MySQL only) | +| `flush_queries` | Full | diff --git a/doc/Stats_Tables.md b/doc/Stats_Tables.md new file mode 100644 index 000000000..64ade5fa0 --- /dev/null +++ b/doc/Stats_Tables.md @@ -0,0 +1,1857 @@ +# ProxySQL Statistics and Metrics Tables + +This document provides a comprehensive analysis of ProxySQL statistics and metrics table schemas. ProxySQL exposes statistics through its admin interface (default port 6032) backed by in-memory SQLite tables. + +## Table of Contents + +- [1. MySQL Statistics Tables](#1-mysql-statistics-tables) +- [2. PostgreSQL Statistics Tables](#2-postgresql-statistics-tables) +- [3. Cluster Statistics Tables](#3-cluster-statistics-tables) +- [4. Historical Statistics Tables](#4-historical-statistics-tables) +- [5. System Statistics Tables](#5-system-statistics-tables) +- [6. Table Reset Pattern](#6-table-reset-pattern) +- [7. Database Architecture](#7-database-architecture) +- [8. Data Characteristics and Example Queries](#8-data-characteristics-and-example-queries) +- [9. Common Notes](#9-common-notes) +- [10. File References](#10-file-references) + +## 1. MySQL Statistics Tables + +### 1.1 stats_mysql_query_rules + +Tracks hit counts for query rules. + +```sql +CREATE TABLE stats_mysql_query_rules ( + rule_id INTEGER PRIMARY KEY, + hits INT NOT NULL +) +``` + +**Columns:** +- `rule_id` - Reference to the query rule in `mysql_query_rules` +- `hits` - Number of times this rule was matched + +### 1.2 stats_mysql_users + +Monitors frontend user connections and limits. + +```sql +CREATE TABLE stats_mysql_users ( + username VARCHAR PRIMARY KEY, + frontend_connections INT NOT NULL, + frontend_max_connections INT NOT NULL +) +``` + +**Columns:** +- `username` - MySQL username +- `frontend_connections` - Current number of active frontend connections +- `frontend_max_connections` - Maximum allowed connections (from `mysql_users` config) + +### 1.3 stats_mysql_commands_counters + +Aggregates command execution statistics with latency distribution. + +```sql +CREATE TABLE stats_mysql_commands_counters ( + Command VARCHAR NOT NULL PRIMARY KEY, + Total_Time_us INT NOT NULL, + Total_cnt INT NOT NULL, + cnt_100us INT NOT NULL, + cnt_500us INT NOT NULL, + cnt_1ms INT NOT NULL, + cnt_5ms INT NOT NULL, + cnt_10ms INT NOT NULL, + cnt_50ms INT NOT NULL, + cnt_100ms INT NOT NULL, + cnt_500ms INT NOT NULL, + cnt_1s INT NOT NULL, + cnt_5s INT NOT NULL, + cnt_10s INT NOT NULL, + cnt_INFs +) +``` + +**Columns:** +- `Command` - MySQL command type (SELECT, INSERT, etc.) +- `Total_Time_us` - Total execution time in microseconds +- `Total_cnt` - Total number of executions +- `cnt_100us` - Count of queries with latency ≤100μs +- `cnt_500us` - Count of queries with latency ≤500μs +- `cnt_1ms` - Count of queries with latency ≤1ms +- `cnt_5ms` - Count of queries with latency ≤5ms +- `cnt_10ms` - Count of queries with latency ≤10ms +- `cnt_50ms` - Count of queries with latency ≤50ms +- `cnt_100ms` - Count of queries with latency ≤100ms +- `cnt_500ms` - Count of queries with latency ≤500ms +- `cnt_1s` - Count of queries with latency ≤1s +- `cnt_5s` - Count of queries with latency ≤5s +- `cnt_10s` - Count of queries with latency ≤10s +- `cnt_INFs` - Count of queries with unknown/undefined latency + +### 1.4 stats_mysql_processlist + +Shows active MySQL sessions, similar to MySQL's `PROCESSLIST`. + +```sql +CREATE TABLE stats_mysql_processlist ( + ThreadID INT NOT NULL, + SessionID INTEGER PRIMARY KEY, + user VARCHAR, + db VARCHAR, + cli_host VARCHAR, + cli_port INT, + hostgroup INT, + l_srv_host VARCHAR, + l_srv_port INT, + srv_host VARCHAR, + srv_port INT, + command VARCHAR, + time_ms INT NOT NULL, + info VARCHAR, + status_flags INT, + extended_info VARCHAR +) +``` + +**Columns:** +- `ThreadID` - Internal thread identifier +- `SessionID` - Unique session identifier (primary key) +- `user` - Username +- `db` - Database/schema name +- `cli_host` - Client host address +- `cli_port` - Client port +- `hostgroup` - Target hostgroup +- `l_srv_host` - Local (proxysql) server hostname +- `l_srv_port` - Local server port +- `srv_host` - Backend MySQL server hostname +- `srv_port` - Backend MySQL server port +- `command` - Current command type +- `time_ms` - Query execution time in milliseconds +- `info` - Query text or status message +- `status_flags` - Internal status flags +- `extended_info` - Extended session information (JSON) + +### 1.5 stats_mysql_connection_pool + +Backend connection pool metrics per server. + +```sql +CREATE TABLE stats_mysql_connection_pool ( + hostgroup INT, + srv_host VARCHAR, + srv_port INT, + status VARCHAR, + ConnUsed INT, + ConnFree INT, + ConnOK INT, + ConnERR INT, + MaxConnUsed INT, + Queries INT, + Queries_GTID_sync INT, + Bytes_data_sent INT, + Bytes_data_recv INT, + Latency_us INT +) +``` + +**Columns:** +- `hostgroup` - Hostgroup ID +- `srv_host` - Backend server hostname +- `srv_port` - Backend server port +- `status` - Server status (ONLINE, SHUNNED, OFFLINE_SOFT, OFFLINE_HARD) +- `ConnUsed` - Currently used connections +- `ConnFree` - Free/available connections in pool +- `ConnOK` - Total successful connections +- `ConnERR` - Total connection errors +- `MaxConnUsed` - Maximum concurrent connections used +- `Queries` - Total queries sent to this server +- `Queries_GTID_sync` - Queries synchronized with GTID +- `Bytes_data_sent` - Total bytes sent to server +- `Bytes_data_recv` - Total bytes received from server +- `Latency_us` - Ping latency in microseconds + +**stats_mysql_connection_pool_reset** - Identical schema to `stats_mysql_connection_pool` but allows resetting stats. + +### 1.6 stats_mysql_free_connections + +Details about idle connections available in the pool. + +```sql +CREATE TABLE stats_mysql_free_connections ( + fd INT NOT NULL, + hostgroup INT NOT NULL, + srv_host VARCHAR NOT NULL, + srv_port INT NOT NULL, + user VARCHAR NOT NULL, + schema VARCHAR, + init_connect VARCHAR, + time_zone VARCHAR, + sql_mode VARCHAR, + autocommit VARCHAR, + idle_ms INT, + statistics VARCHAR, + mysql_info VARCHAR +) +``` + +**Columns:** +- `fd` - File descriptor for the connection +- `hostgroup` - Hostgroup ID +- `srv_host` - Backend server hostname +- `srv_port` - Backend server port +- `user` - Backend username +- `schema` - Default schema +- `init_connect` - INIT_CONNECT string +- `time_zone` - Timezone setting +- `sql_mode` - SQL mode +- `autocommit` - Autocommit status +- `idle_ms` - Idle time in milliseconds +- `statistics` - Connection statistics (JSON) +- `mysql_info` - Additional MySQL-specific info (JSON) + +### 1.7 stats_mysql_query_digest + +Aggregated query performance statistics. + +```sql +CREATE TABLE stats_mysql_query_digest ( + hostgroup INT, + schemaname VARCHAR NOT NULL, + username VARCHAR NOT NULL, + client_address VARCHAR NOT NULL, + digest VARCHAR NOT NULL, + digest_text VARCHAR NOT NULL, + count_star INTEGER NOT NULL, + first_seen INTEGER NOT NULL, + last_seen INTEGER NOT NULL, + sum_time INTEGER NOT NULL, + min_time INTEGER NOT NULL, + max_time INTEGER NOT NULL, + sum_rows_affected INTEGER NOT NULL, + sum_rows_sent INTEGER NOT NULL, + PRIMARY KEY(hostgroup, schemaname, username, client_address, digest) +) +``` + +**Columns:** +- `hostgroup` - Hostgroup where query was executed +- `schemaname` - Database/schema name +- `username` - Username who executed query +- `client_address` - Client IP address +- `digest` - Query digest hash +- `digest_text` - Representative query text (normalized) +- `count_star` - Total executions +- `first_seen` - Unix timestamp of first execution +- `last_seen` - Unix timestamp of last execution +- `sum_time` - Total execution time in microseconds +- `min_time` - Minimum execution time in microseconds +- `max_time` - Maximum execution time in microseconds +- `sum_rows_affected` - Total rows affected +- `sum_rows_sent` - Total rows sent to client + +**stats_mysql_query_digest_reset** - Identical schema to `stats_mysql_query_digest` for resetting stats. + +### 1.8 stats_mysql_global + +Global ProxySQL metrics. + +```sql +CREATE TABLE stats_mysql_global ( + Variable_Name VARCHAR NOT NULL PRIMARY KEY, + Variable_Value VARCHAR NOT NULL +) +``` + +**Complete Variable List:** + +This table uses a key-value format where each row is a different metric. The variables are populated from multiple sources in the codebase (see [Source References](#source-references) below). + +**System / Uptime** + +| Variable Name | Description | Type | +|---|---|---| +| `ProxySQL_Uptime` | Seconds since ProxySQL started | Gauge | + +**Client Connection Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Active_Transactions` | Currently active transactions | Gauge | +| `Client_Connections_aborted` | Total aborted client connections | Counter | +| `Client_Connections_connected` | Currently connected clients | Gauge | +| `Client_Connections_connected_prim_pass` | Clients connected via primary password | Gauge | +| `Client_Connections_connected_addl_pass` | Clients connected via additional password | Gauge | +| `Client_Connections_created` | Total client connections ever created | Counter | +| `Client_Connections_sha2cached` | Connections using SHA2 cached authentication | Counter | +| `Client_Connections_non_idle` | Non-idle client connections (requires `IDLE_THREADS`) | Gauge | +| `Client_Connections_hostgroup_locked` | Connections locked to a specific hostgroup | Gauge | + +**Server Connection Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Server_Connections_aborted` | Total aborted server connections | Counter | +| `Server_Connections_connected` | Currently connected backend servers | Gauge | +| `Server_Connections_created` | Total server connections ever created | Counter | +| `Server_Connections_delayed` | Connections delayed due to throttling | Counter | + +**Memory Buffer Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `mysql_backend_buffers_bytes` | Memory used by backend connection buffers | Gauge | +| `mysql_frontend_buffers_bytes` | Memory used by frontend connection buffers | Gauge | +| `mysql_session_internal_bytes` | Memory used by internal session structures | Gauge | + +**Command Counters (Com_\*)** + +| Variable Name | Description | Type | +|---|---|---| +| `Com_autocommit` | Autocommit statements executed | Counter | +| `Com_autocommit_filtered` | Autocommit statements filtered | Counter | +| `Com_commit` | COMMIT statements executed | Counter | +| `Com_commit_filtered` | COMMIT statements filtered | Counter | +| `Com_rollback` | ROLLBACK statements executed | Counter | +| `Com_rollback_filtered` | ROLLBACK statements filtered | Counter | +| `Com_backend_change_user` | Backend CHANGE USER commands | Counter | +| `Com_backend_init_db` | Backend INIT_DB commands | Counter | +| `Com_backend_set_names` | Backend SET NAMES commands | Counter | +| `Com_frontend_init_db` | Frontend INIT_DB commands | Counter | +| `Com_frontend_set_names` | Frontend SET NAMES commands | Counter | +| `Com_frontend_use_db` | Frontend USE DB commands | Counter | + +**Prepared Statement Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `Com_backend_stmt_prepare` | Backend PREPARE statements | Counter | +| `Com_backend_stmt_execute` | Backend EXECUTE statements | Counter | +| `Com_backend_stmt_close` | Backend CLOSE statements | Counter | +| `Com_frontend_stmt_prepare` | Frontend PREPARE statements | Counter | +| `Com_frontend_stmt_execute` | Frontend EXECUTE statements | Counter | +| `Com_frontend_stmt_close` | Frontend CLOSE statements | Counter | + +**Query Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Questions` | Total queries processed | Counter | +| `Slow_queries` | Number of slow queries | Counter | +| `GTID_consistent_queries` | Queries requiring GTID consistency | Counter | +| `GTID_session_collected` | GTID sessions collected | Counter | +| `Queries_backends_bytes_recv` | Bytes received from backends | Counter | +| `Queries_backends_bytes_sent` | Bytes sent to backends | Counter | +| `Queries_frontends_bytes_recv` | Bytes received from frontends | Counter | +| `Queries_frontends_bytes_sent` | Bytes sent to frontends | Counter | +| `Query_Processor_time_nsec` | Time spent in query processor (nanoseconds) | Counter | +| `Backend_query_time_nsec` | Time spent executing on backends (nanoseconds) | Counter | + +**Connection Pool Operation Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `ConnPool_get_conn_latency_awareness` | Connections obtained via latency-aware routing | Counter | +| `ConnPool_get_conn_immediate` | Connections obtained immediately from pool | Counter | +| `ConnPool_get_conn_success` | Successful connection pool gets | Counter | +| `ConnPool_get_conn_failure` | Failed connection pool gets | Counter | +| `MyHGM_myconnpoll_get` | Total connection pool get operations | Counter | +| `MyHGM_myconnpoll_get_ok` | Successful pool get operations | Counter | +| `MyHGM_myconnpoll_push` | Connections returned to pool | Counter | +| `MyHGM_myconnpoll_destroy` | Connections destroyed from pool | Counter | +| `MyHGM_myconnpoll_reset` | Connection pool resets | Counter | + +**Backend Health Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `mysql_killed_backend_connections` | Backend connections killed | Counter | +| `mysql_killed_backend_queries` | Backend queries killed | Counter | +| `backend_lagging_during_query` | Queries affected by backend lag | Counter | +| `backend_offline_during_query` | Queries affected by backend going offline | Counter | +| `get_aws_aurora_replicas_skipped_during_query` | Aurora replicas skipped during query | Counter | +| `max_connect_timeouts` | Maximum connect timeouts reached | Counter | + +**Hostgroup Locking Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `hostgroup_locked_set_cmds` | SET commands on locked hostgroup sessions | Counter | +| `hostgroup_locked_queries` | Queries on locked hostgroup sessions | Counter | + +**Unexpected Frontend Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `mysql_unexpected_frontend_com_quit` | Unexpected COM_QUIT from frontend | Counter | +| `mysql_unexpected_frontend_com_ping` | Unexpected COM_PING from frontend | Counter | +| `mysql_unexpected_frontend_packets` | Unexpected packets from frontend | Counter | + +**Max Lag Throttling Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `queries_with_max_lag_ms` | Queries subject to max lag check | Counter | +| `queries_with_max_lag_ms__delayed` | Queries delayed due to max lag | Counter | +| `queries_with_max_lag_ms__total_wait_time_us` | Total wait time from max lag delays (μs) | Counter | + +**Security Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `automatic_detected_sql_injection` | Automatically detected SQL injection attempts | Counter | +| `mysql_whitelisted_sqli_fingerprint` | Whitelisted SQL injection fingerprints | Counter | +| `ai_detected_anomalies` | AI-detected query anomalies | Counter | +| `ai_blocked_queries` | Queries blocked by AI detection | Counter | + +**Error and Miscellaneous Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `generated_error_packets` | Error packets generated by ProxySQL | Counter | +| `client_host_error_killed_connections` | Connections killed due to client host errors | Counter | +| `mysql_set_wait_timeout_commands` | SET wait_timeout commands processed | Counter | +| `mysql_timeout_terminated_connections` | Connections terminated due to timeout | Counter | + +**Mirror Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Mirror_concurrency` | Current number of mirror sessions | Gauge | +| `Mirror_queue_length` | Current mirror queue length | Gauge | + +**Miscellaneous Status** + +| Variable Name | Description | Type | +|---|---|---| +| `Selects_for_update__autocommit0` | SELECT FOR UPDATE with autocommit=0 | Counter | +| `Servers_table_version` | Current version of the servers table | Gauge | +| `MySQL_Thread_Workers` | Number of MySQL thread workers | Gauge | +| `new_req_conns_count` | New request connections count from query processor | Counter | +| `mysql_listener_paused` | Whether the MySQL listener is paused (0/1) | Gauge | +| `OpenSSL_Version_Num` | OpenSSL version number | Gauge | + +**Access Denied Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `Access_Denied_Wrong_Password` | Access denied due to wrong password | Counter | +| `Access_Denied_Max_Connections` | Access denied due to max connections | Counter | +| `Access_Denied_Max_User_Connections` | Access denied due to max user connections | Counter | + +**MySQL Monitor Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `MySQL_Monitor_Workers` | Number of monitor worker threads | Gauge | +| `MySQL_Monitor_Workers_Aux` | Number of auxiliary monitor workers | Gauge | +| `MySQL_Monitor_Workers_Started` | Number of monitor workers started | Counter | +| `MySQL_Monitor_connect_check_OK` | Successful monitor connect checks | Counter | +| `MySQL_Monitor_connect_check_ERR` | Failed monitor connect checks | Counter | +| `MySQL_Monitor_ping_check_OK` | Successful monitor ping checks | Counter | +| `MySQL_Monitor_ping_check_ERR` | Failed monitor ping checks | Counter | +| `MySQL_Monitor_read_only_check_OK` | Successful read-only checks | Counter | +| `MySQL_Monitor_read_only_check_ERR` | Failed read-only checks | Counter | +| `MySQL_Monitor_replication_lag_check_OK` | Successful replication lag checks | Counter | +| `MySQL_Monitor_replication_lag_check_ERR` | Failed replication lag checks | Counter | +| `MySQL_Monitor_dns_cache_queried` | DNS cache queries | Counter | +| `MySQL_Monitor_dns_cache_lookup_success` | Successful DNS cache lookups | Counter | +| `MySQL_Monitor_dns_cache_record_updated` | DNS cache records updated | Counter | + +**Memory Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `SQLite3_memory_bytes` | SQLite3 internal memory usage | Gauge | +| `ConnPool_memory_bytes` | Connection pool memory usage | Gauge | + +**Prepared Statement Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Stmt_Client_Active_Total` | Total active client-side prepared statements | Gauge | +| `Stmt_Client_Active_Unique` | Unique active client-side prepared statements | Gauge | +| `Stmt_Server_Active_Total` | Total active server-side prepared statements | Gauge | +| `Stmt_Server_Active_Unique` | Unique active server-side prepared statements | Gauge | +| `Stmt_Max_Stmt_id` | Maximum prepared statement ID assigned | Gauge | +| `Stmt_Cached` | Cached prepared statements | Gauge | + +**Query Cache Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Query_Cache_Memory_bytes` | Memory used by query cache | Gauge | +| `Query_Cache_count_GET` | Query cache GET operations | Counter | +| `Query_Cache_count_GET_OK` | Successful query cache GETs | Counter | +| `Query_Cache_count_SET` | Query cache SET operations | Counter | +| `Query_Cache_bytes_IN` | Bytes written into query cache | Counter | +| `Query_Cache_bytes_OUT` | Bytes read from query cache | Counter | +| `Query_Cache_Purged` | Query cache entries purged | Counter | +| `Query_Cache_Entries` | Current query cache entries | Gauge | + +**MySQL Logger Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `MySQL_Logger_memoryCopyCount` | Number of memory copy operations | Counter | +| `MySQL_Logger_diskCopyCount` | Number of disk copy operations | Counter | +| `MySQL_Logger_getAllEventsCallsCount` | Number of getAllEvents calls | Counter | +| `MySQL_Logger_getAllEventsEventsCount` | Total events from getAllEvents | Counter | +| `MySQL_Logger_totalMemoryCopyTimeMicros` | Total memory copy time (μs) | Counter | +| `MySQL_Logger_totalDiskCopyTimeMicros` | Total disk copy time (μs) | Counter | +| `MySQL_Logger_totalGetAllEventsTimeMicros` | Total getAllEvents time (μs) | Counter | +| `MySQL_Logger_totalEventsCopiedToMemory` | Events copied to memory | Counter | +| `MySQL_Logger_totalEventsCopiedToDisk` | Events copied to disk | Counter | +| `MySQL_Logger_circularBufferEventsAddedCount` | Events added to circular buffer | Counter | +| `MySQL_Logger_circularBufferEventsDroppedCount` | Events dropped from circular buffer | Counter | +| `MySQL_Logger_circularBufferEventsSize` | Current circular buffer size | Gauge | + +**Source References** {#source-references} + +Variables are aggregated from these code locations: +- `MySQL_Threads_Handler::SQL3_GlobalStatus()` in `lib/MySQL_Thread.cpp` +- `MySQL_HostGroups_Manager::SQL3_Get_ConnPool_Stats()` in `lib/MySQL_HostGroups_Manager.cpp` +- `ProxySQL_Admin::stats___mysql_global()` in `lib/ProxySQL_Admin_Stats.cpp` +- `Query_Cache::SQL3_getStats()` in `lib/Query_Cache.cpp` +- `MySQL_Logger::getAllMetrics()` in `lib/MySQL_Logger.cpp` + +### 1.9 stats_memory_metrics + +Memory usage statistics. + +```sql +CREATE TABLE stats_memory_metrics ( + Variable_Name VARCHAR NOT NULL PRIMARY KEY, + Variable_Value VARCHAR NOT NULL +) +``` + +**Complete Variable List:** + +This table uses a key-value format. Variables are populated from `lib/ProxySQL_Admin_Stats.cpp`. + +**Core Memory** + +| Variable Name | Description | Conditional | +|---|---|---| +| `SQLite3_memory_bytes` | SQLite3 internal memory usage | Always | + +**jemalloc Memory (requires jemalloc build)** + +| Variable Name | Description | Conditional | +|---|---|---| +| `jemalloc_resident` | Resident memory reported by jemalloc | Build without `NOJEM` | +| `jemalloc_active` | Active memory reported by jemalloc | Build without `NOJEM` | +| `jemalloc_allocated` | Allocated memory reported by jemalloc | Build without `NOJEM` | +| `jemalloc_mapped` | Mapped memory reported by jemalloc | Build without `NOJEM` | +| `jemalloc_metadata` | Metadata memory reported by jemalloc | Build without `NOJEM` | +| `jemalloc_retained` | Retained memory reported by jemalloc | Build without `NOJEM` | + +**Module Memory** + +| Variable Name | Description | Conditional | +|---|---|---| +| `Auth_memory` | MySQL authentication module memory | `GloMyAuth` active | +| `mysql_query_digest_memory` | MySQL query digest memory | `GloMyQPro` active | +| `mysql_query_rules_memory` | MySQL query rules memory | `GloMyQPro` active | +| `pgsql_query_digest_memory` | PostgreSQL query digest memory | `GloPgQPro` active | +| `pgsql_query_rules_memory` | PostgreSQL query rules memory | `GloPgQPro` active | + +**Prepared Statement Memory** + +| Variable Name | Description | Conditional | +|---|---|---| +| `prepare_statement_metadata_memory` | Prepared statement metadata memory | `GloMyStmt` active | +| `prepare_statement_backend_memory` | Prepared statement backend memory | `GloMyStmt` active | + +**Firewall Memory** + +| Variable Name | Description | Conditional | +|---|---|---| +| `mysql_firewall_users_table` | Firewall users table memory | `GloMyQPro` active | +| `mysql_firewall_users_config` | Firewall users config memory | `GloMyQPro` active | +| `mysql_firewall_rules_table` | Firewall rules table memory | `GloMyQPro` active | +| `mysql_firewall_rules_config` | Firewall rules config memory | `GloMyQPro` active | + +**Stack Memory** + +| Variable Name | Description | Conditional | +|---|---|---| +| `stack_memory_mysql_threads` | Stack memory for MySQL threads | Always | +| `stack_memory_admin_threads` | Stack memory for admin threads | Always | +| `stack_memory_cluster_threads` | Stack memory for cluster threads | Always | + +### 1.10 stats_mysql_gtid_executed + +GTID (Global Transaction ID) information. + +```sql +CREATE TABLE stats_mysql_gtid_executed ( + hostname VARCHAR NOT NULL, + port INT NOT NULL DEFAULT 3306, + gtid_executed VARCHAR, + events INT NOT NULL +) +``` + +**Columns:** +- `hostname` - Backend server hostname +- `port` - Backend server port +- `gtid_executed` - GTID set string +- `events` - Number of GTID events + +### 1.11 stats_mysql_errors + +MySQL error tracking. + +```sql +CREATE TABLE stats_mysql_errors ( + hostgroup INT NOT NULL, + hostname VARCHAR NOT NULL, + port INT NOT NULL, + username VARCHAR NOT NULL, + client_address VARCHAR NOT NULL, + schemaname VARCHAR NOT NULL, + errno INT NOT NULL, + count_star INTEGER NOT NULL, + first_seen INTEGER NOT NULL, + last_seen INTEGER NOT NULL, + last_error VARCHAR NOT NULL DEFAULT '', + PRIMARY KEY (hostgroup, hostname, port, username, schemaname, errno) +) +``` + +**Columns:** +- `hostgroup` - Hostgroup ID +- `hostname` - Backend server hostname +- `port` - Backend server port +- `username` - Username +- `client_address` - Client IP address +- `schemaname` - Database/schema name +- `errno` - MySQL error number +- `count_star` - Error count +- `first_seen` - Unix timestamp of first occurrence +- `last_seen` - Unix timestamp of last occurrence +- `last_error` - Error message + +**stats_mysql_errors_reset** - Identical schema to `stats_mysql_errors` for resetting stats. + +### 1.12 stats_mysql_client_host_cache + +Client host error tracking for connection throttling. + +```sql +CREATE TABLE stats_mysql_client_host_cache ( + client_address VARCHAR NOT NULL, + error_count INT NOT NULL, + last_updated BIGINT NOT NULL +) +``` + +**Columns:** +- `client_address` - Client IP address +- `error_count` - Number of recent errors from this host +- `last_updated` - Unix timestamp of last update + +**stats_mysql_client_host_cache_reset** - Identical schema to `stats_mysql_client_host_cache` for resetting stats. + +### 1.13 stats_mysql_prepared_statements_info + +Prepared statement statistics. + +```sql +CREATE TABLE stats_mysql_prepared_statements_info ( + global_stmt_id INT NOT NULL, + schemaname VARCHAR NOT NULL, + username VARCHAR NOT NULL, + digest VARCHAR NOT NULL, + ref_count_client INT NOT NULL, + ref_count_server INT NOT NULL, + num_columns INT NOT NULL, + num_params INT NOT NULL, + query VARCHAR NOT NULL +) +``` + +**Columns:** +- `global_stmt_id` - Global statement ID +- `schemaname` - Database schema +- `username` - Username +- `digest` - Query digest +- `ref_count_client` - Reference count from client +- `ref_count_server` - Reference count from server +- `num_columns` - Number of result columns +- `num_params` - Number of parameters +- `query` - Query text + +### 1.14 stats_mysql_query_events + +Query event log (when query logging is enabled). + +```sql +CREATE TABLE stats_mysql_query_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + thread_id INTEGER, + username TEXT, + schemaname TEXT, + start_time INTEGER, + end_time INTEGER, + query_digest TEXT, + query TEXT, + server TEXT, + client TEXT, + event_type INTEGER, + hid INTEGER, + extra_info TEXT, + affected_rows INTEGER, + last_insert_id INTEGER, + rows_sent INTEGER, + client_stmt_id INTEGER, + gtid TEXT, + errno INT, + error TEXT +) +``` + +**Columns:** +- `id` - Auto-increment ID +- `thread_id` - Thread ID +- `username` - Username +- `schemaname` - Database schema +- `start_time` - Start timestamp +- `end_time` - End timestamp +- `query_digest` - Query digest +- `query` - Full query text +- `server` - Server address +- `client` - Client address +- `event_type` - Event type +- `hid` - Hostgroup ID +- `extra_info` - Extra information +- `affected_rows` - Rows affected +- `last_insert_id` - Last insert ID +- `rows_sent` - Rows sent +- `client_stmt_id` - Client statement ID +- `gtid` - GTID information +- `errno` - Error number +- `error` - Error message + +## 2. PostgreSQL Statistics Tables + +All PostgreSQL stats tables mirror their MySQL counterparts with appropriate name changes (e.g., `schemaname` instead of `db`, `sqlstate` instead of `errno`). + +### 2.1 stats_pgsql_query_rules + +```sql +CREATE TABLE stats_pgsql_query_rules ( + rule_id INTEGER PRIMARY KEY, + hits INT NOT NULL +) +``` + +### 2.2 stats_pgsql_users + +```sql +CREATE TABLE stats_pgsql_users ( + username VARCHAR PRIMARY KEY, + frontend_connections INT NOT NULL, + frontend_max_connections INT NOT NULL +) +``` + +### 2.3 stats_pgsql_commands_counters + +```sql +CREATE TABLE stats_pgsql_commands_counters ( + Command VARCHAR NOT NULL PRIMARY KEY, + Total_Time_us INT NOT NULL, + Total_cnt INT NOT NULL, + cnt_100us INT NOT NULL, + cnt_500us INT NOT NULL, + cnt_1ms INT NOT NULL, + cnt_5ms INT NOT NULL, + cnt_10ms INT NOT NULL, + cnt_50ms INT NOT NULL, + cnt_100ms INT NOT NULL, + cnt_500ms INT NOT NULL, + cnt_1s INT NOT NULL, + cnt_5s INT NOT NULL, + cnt_10s INT NOT NULL, + cnt_INFs +) +``` + +### 2.4 stats_pgsql_processlist + +```sql +CREATE TABLE stats_pgsql_processlist ( + ThreadID INT NOT NULL, + SessionID INTEGER PRIMARY KEY, + user VARCHAR, + database VARCHAR, + cli_host VARCHAR, + cli_port INT, + hostgroup INT, + l_srv_host VARCHAR, + l_srv_port INT, + srv_host VARCHAR, + srv_port INT, + backend_pid INT, + backend_state VARCHAR, + command VARCHAR, + time_ms INT NOT NULL, + info VARCHAR, + status_flags INT, + extended_info VARCHAR +) +``` + +**Note:** This table includes `database`, `backend_pid`, and `backend_state` columns specific to PostgreSQL. + +### 2.5 stats_pgsql_stat_activity + +A PostgreSQL-compatible **VIEW** (not a table) that mirrors `pg_stat_activity`. + +```sql +CREATE VIEW stats_pgsql_stat_activity AS +SELECT + ThreadID AS thread_id, + database AS datname, + SessionID AS pid, + user AS usename, + cli_host AS client_addr, + cli_port AS client_port, + hostgroup, + l_srv_host, + l_srv_port, + srv_host, + srv_port, + backend_pid, + backend_state AS state, + command, + time_ms AS duration_ms, + info as query, + status_flags, + extended_info +FROM stats_pgsql_processlist +``` + +### 2.6 stats_pgsql_connection_pool + +```sql +CREATE TABLE stats_pgsql_connection_pool ( + hostgroup INT, + srv_host VARCHAR, + srv_port INT, + status VARCHAR, + ConnUsed INT, + ConnFree INT, + ConnOK INT, + ConnERR INT, + MaxConnUsed INT, + Queries INT, + Bytes_data_sent INT, + Bytes_data_recv INT, + Latency_us INT +) +``` + +### 2.7 stats_pgsql_free_connections + +```sql +CREATE TABLE stats_pgsql_free_connections ( + fd INT NOT NULL, + hostgroup INT NOT NULL, + srv_host VARCHAR NOT NULL, + srv_port INT NOT NULL, + user VARCHAR NOT NULL, + database VARCHAR, + init_connect VARCHAR, + time_zone VARCHAR, + sql_mode VARCHAR, + idle_ms INT, + statistics VARCHAR, + pgsql_info VARCHAR +) +``` + +### 2.8 stats_pgsql_query_digest + +```sql +CREATE TABLE stats_pgsql_query_digest ( + hostgroup INT, + database VARCHAR NOT NULL, + username VARCHAR NOT NULL, + client_address VARCHAR NOT NULL, + digest VARCHAR NOT NULL, + digest_text VARCHAR NOT NULL, + count_star INTEGER NOT NULL, + first_seen INTEGER NOT NULL, + last_seen INTEGER NOT NULL, + sum_time INTEGER NOT NULL, + min_time INTEGER NOT NULL, + max_time INTEGER NOT NULL, + sum_rows_affected INTEGER NOT NULL, + sum_rows_sent INTEGER NOT NULL, + PRIMARY KEY(hostgroup, database, username, client_address, digest) +) +``` + +**Note:** This table uses `database` column, unlike MySQL which uses `schemaname`. However, the historical table `history_pgsql_query_digest` uses `schemaname` for consistency with MySQL history tables. + +### 2.9 stats_pgsql_prepared_statements_info + +```sql +CREATE TABLE stats_pgsql_prepared_statements_info ( + global_stmt_id INT NOT NULL, + database VARCHAR NOT NULL, + username VARCHAR NOT NULL, + digest VARCHAR NOT NULL, + ref_count_client INT NOT NULL, + ref_count_server INT NOT NULL, + num_param_types INT NOT NULL, + query VARCHAR NOT NULL +) +``` + +### 2.10 stats_pgsql_global + +```sql +CREATE TABLE stats_pgsql_global ( + Variable_Name VARCHAR NOT NULL PRIMARY KEY, + Variable_Value VARCHAR NOT NULL +) +``` + +**Complete Variable List:** + +This table uses a key-value format, similar to `stats_mysql_global`. Variables are populated from `PgSQL_Threads_Handler::SQL3_GlobalStatus()` in `lib/PgSQL_Thread.cpp` and other sources. + +> **Note:** The PgSQL global stats are less mature than MySQL. Many counter/gauge arrays present in the MySQL path are commented out in the PgSQL code. The active variables are listed below. + +**System / Uptime** + +| Variable Name | Description | Type | +|---|---|---| +| `ProxySQL_Uptime` | Seconds since ProxySQL started | Gauge | + +**Client Connection Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Active_Transactions` | Currently active transactions | Gauge | +| `Client_Connections_aborted` | Total aborted client connections | Counter | +| `Client_Connections_connected` | Currently connected clients | Gauge | +| `Client_Connections_created` | Total client connections ever created | Counter | +| `Client_Connections_non_idle` | Non-idle client connections (requires `IDLE_THREADS`) | Gauge | + +**Server Connection Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Server_Connections_aborted` | Total aborted server connections | Counter | +| `Server_Connections_connected` | Currently connected backend servers | Gauge | +| `Server_Connections_created` | Total server connections ever created | Counter | +| `Server_Connections_delayed` | Connections delayed due to throttling | Counter | + +**Memory Buffer Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `pgsql_backend_buffers_bytes` | Memory used by backend connection buffers | Gauge | +| `pgsql_frontend_buffers_bytes` | Memory used by frontend connection buffers | Gauge | +| `pgsql_session_internal_bytes` | Memory used by internal session structures | Gauge | + +**Transaction Command Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `Commit` | COMMIT statements executed | Counter | +| `Commit_filtered` | COMMIT statements filtered | Counter | +| `Rollback` | ROLLBACK statements executed | Counter | +| `Rollback_filtered` | ROLLBACK statements filtered | Counter | + +**Backend Command Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `Backend_reset_connection` | Backend connection resets | Counter | +| `Backend_set_client_encoding` | Backend SET client_encoding commands | Counter | +| `Frontend_set_client_encoding` | Frontend SET client_encoding commands | Counter | + +**Mirror Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Mirror_concurrency` | Current number of mirror sessions | Gauge | +| `Mirror_queue_length` | Current mirror queue length | Gauge | + +**Miscellaneous Status** + +| Variable Name | Description | Type | +|---|---|---| +| `Selects_for_update__autocommit0` | SELECT FOR UPDATE with autocommit=0 | Counter | +| `Servers_table_version` | Current version of the servers table | Gauge | +| `PgSQL_Thread_Workers` | Number of PgSQL thread workers | Gauge | +| `pgsql_listener_paused` | Whether the PgSQL listener is paused (0/1) | Gauge | + +**Access Denied Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `Access_Denied_Wrong_Password` | Access denied due to wrong password | Counter | +| `Access_Denied_Max_Connections` | Access denied due to max connections | Counter | +| `Access_Denied_Max_User_Connections` | Access denied due to max user connections | Counter | + +**PgSQL Monitor Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `PgSQL_Monitor_connect_check_OK` | Successful monitor connect checks | Counter | +| `PgSQL_Monitor_connect_check_ERR` | Failed monitor connect checks | Counter | +| `PgSQL_Monitor_ping_check_OK` | Successful monitor ping checks | Counter | +| `PgSQL_Monitor_ping_check_ERR` | Failed monitor ping checks | Counter | +| `PgSQL_Monitor_read_only_check_OK` | Successful read-only checks | Counter | +| `PgSQL_Monitor_read_only_check_ERR` | Failed read-only checks | Counter | +| `PgSQL_Monitor_ssl_connections_OK` | Successful SSL monitor connections | Counter | +| `PgSQL_Monitor_non_ssl_connections_OK` | Successful non-SSL monitor connections | Counter | + +**Connection Pool Operation Counters** + +| Variable Name | Description | Type | +|---|---|---| +| `PgHGM_pgconnpoll_get` | Total connection pool get operations | Counter | +| `PgHGM_pgconnpoll_get_ok` | Successful pool get operations | Counter | +| `PgHGM_pgconnpoll_push` | Connections returned to pool | Counter | +| `PgHGM_pgconnpoll_destroy` | Connections destroyed from pool | Counter | +| `PgHGM_pgconnpoll_reset` | Connection pool resets | Counter | + +**Memory Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `SQLite3_memory_bytes` | SQLite3 internal memory usage | Gauge | +| `ConnPool_memory_bytes` | Connection pool memory usage | Gauge | + +**Prepared Statement Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Stmt_Client_Active_Total` | Total active client-side prepared statements | Gauge | +| `Stmt_Client_Active_Unique` | Unique active client-side prepared statements | Gauge | +| `Stmt_Server_Active_Total` | Total active server-side prepared statements | Gauge | +| `Stmt_Server_Active_Unique` | Unique active server-side prepared statements | Gauge | +| `Stmt_Max_Stmt_id` | Maximum prepared statement ID assigned | Gauge | +| `Stmt_Cached` | Cached prepared statements | Gauge | + +**Query Cache Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `Query_Cache_Memory_bytes` | Memory used by query cache | Gauge | +| `Query_Cache_count_GET` | Query cache GET operations | Counter | +| `Query_Cache_count_GET_OK` | Successful query cache GETs | Counter | +| `Query_Cache_count_SET` | Query cache SET operations | Counter | +| `Query_Cache_bytes_IN` | Bytes written into query cache | Counter | +| `Query_Cache_bytes_OUT` | Bytes read from query cache | Counter | +| `Query_Cache_Purged` | Query cache entries purged | Counter | +| `Query_Cache_Entries` | Current query cache entries | Gauge | + +**Query Processor Metrics** + +| Variable Name | Description | Type | +|---|---|---| +| `new_req_conns_count` | New request connections count from query processor | Counter | + +**Source References** + +Variables are aggregated from these code locations: +- `PgSQL_Threads_Handler::SQL3_GlobalStatus()` in `lib/PgSQL_Thread.cpp` +- `PgSQL_HostGroups_Manager::SQL3_Get_ConnPool_Stats()` in `lib/PgSQL_HostGroups_Manager.cpp` +- `ProxySQL_Admin::stats___pgsql_global()` in `lib/ProxySQL_Admin_Stats.cpp` +- `Query_Cache::SQL3_getStats()` in `lib/Query_Cache.cpp` + +### 2.11 stats_pgsql_errors + +```sql +CREATE TABLE stats_pgsql_errors ( + hostgroup INT NOT NULL, + hostname VARCHAR NOT NULL, + port INT NOT NULL, + username VARCHAR NOT NULL, + client_address VARCHAR NOT NULL, + database VARCHAR NOT NULL, + sqlstate VARCHAR NOT NULL, + count_star INTEGER NOT NULL, + first_seen INTEGER NOT NULL, + last_seen INTEGER NOT NULL, + last_error VARCHAR NOT NULL DEFAULT '', + PRIMARY KEY (hostgroup, hostname, port, username, database, sqlstate) +) +``` + +### 2.12 stats_pgsql_client_host_cache + +```sql +CREATE TABLE stats_pgsql_client_host_cache ( + client_address VARCHAR NOT NULL, + error_count INT NOT NULL, + last_updated BIGINT NOT NULL +) +``` + +### 2.13 MySQL vs PostgreSQL Column Naming + +ProxySQL uses different column names for the same concept between MySQL and PostgreSQL tables: + +| Concept | MySQL Column | PostgreSQL Column | Notes | +|---------|--------------|-------------------|-------| +| Database/Schema | `schemaname` | `database` | Different naming convention | +| Error Code | `errno` | `sqlstate` | MySQL uses numeric codes, PostgreSQL uses 5-char SQLSTATE | +| Backend Info | `mysql_info` | `pgsql_info` | In free_connections tables | +| Process DB | `db` | `database` | In processlist tables | + +**Note on History Tables:** The `history_pgsql_query_digest` table uses `schemaname` (matching MySQL convention) rather than `database`, creating an inconsistency with the live `stats_pgsql_query_digest` table. + +## 3. Cluster Statistics Tables + +Tables for monitoring ProxySQL cluster nodes. + +### 3.1 stats_proxysql_servers_clients_status + +```sql +CREATE TABLE stats_proxysql_servers_clients_status ( + uuid VARCHAR NOT NULL, + hostname VARCHAR NOT NULL, + port INT NOT NULL, + admin_mysql_ifaces VARCHAR NOT NULL, + last_seen_at INT NOT NULL, + PRIMARY KEY (uuid, hostname, port) +) +``` + +### 3.2 stats_proxysql_servers_status + +```sql +CREATE TABLE stats_proxysql_servers_status ( + hostname VARCHAR NOT NULL, + port INT NOT NULL DEFAULT 6032, + weight INT CHECK (weight >= 0) NOT NULL DEFAULT 0, + master VARCHAR NOT NULL, + global_version INT NOT NULL, + check_age_us INT NOT NULL, + ping_time_us INT NOT NULL, + checks_OK INT NOT NULL, + checks_ERR INT NOT NULL, + PRIMARY KEY (hostname, port) +) +``` + +### 3.3 stats_proxysql_servers_metrics + +```sql +CREATE TABLE stats_proxysql_servers_metrics ( + hostname VARCHAR NOT NULL, + port INT NOT NULL DEFAULT 6032, + weight INT CHECK (weight >= 0) NOT NULL DEFAULT 0, + comment VARCHAR NOT NULL DEFAULT '', + response_time_ms INT NOT NULL, + Uptime_s INT NOT NULL, + last_check_ms INT NOT NULL, + Queries INT NOT NULL, + Client_Connections_connected INT NOT NULL, + Client_Connections_created INT NOT NULL, + PRIMARY KEY (hostname, port) +) +``` + +### 3.4 stats_proxysql_servers_checksums + +```sql +CREATE TABLE stats_proxysql_servers_checksums ( + hostname VARCHAR NOT NULL, + port INT NOT NULL DEFAULT 6032, + name VARCHAR NOT NULL, + version INT NOT NULL, + epoch INT NOT NULL, + checksum VARCHAR NOT NULL, + changed_at INT NOT NULL, + updated_at INT NOT NULL, + diff_check INT NOT NULL, + PRIMARY KEY (hostname, port, name) +) +``` + +### 3.5 stats_proxysql_message_metrics + +Message/metric tracking for ProxySQL internal use. + +```sql +CREATE TABLE stats_proxysql_message_metrics ( + message_id VARCHAR NOT NULL, + filename VARCHAR NOT NULL, + line INT CHECK (line >= 0) NOT NULL DEFAULT 0, + func VARCHAR NOT NULL, + count_star INTEGER NOT NULL, + first_seen INTEGER NOT NULL, + last_seen INTEGER NOT NULL, + PRIMARY KEY (filename, line, func) +) +``` + +## 4. Historical Statistics Tables + +Tables stored in the persistent statsdb_disk database with time-series data. + +### 4.1 Connection Metrics + +#### 4.1.1 mysql_connections + +Connection statistics by timestamp. + +```sql +CREATE TABLE mysql_connections ( + timestamp INT NOT NULL, + Client_Connections_aborted INT NOT NULL, + Client_Connections_connected INT NOT NULL, + Client_Connections_created INT NOT NULL, + Server_Connections_aborted INT NOT NULL, + Server_Connections_connected INT NOT NULL, + Server_Connections_created INT NOT NULL, + ConnPool_get_conn_failure INT NOT NULL, + ConnPool_get_conn_immediate INT NOT NULL, + ConnPool_get_conn_success INT NOT NULL, + Questions INT NOT NULL, + Slow_queries INT NOT NULL, + GTID_consistent_queries INT NOT NULL, + PRIMARY KEY (timestamp) +) +``` + +#### 4.1.2 mysql_connections_hour + +Hourly aggregated connection metrics. + +#### 4.1.3 mysql_connections_day + +Daily aggregated connection metrics. + +### 4.2 Connection Pool History + +#### 4.2.1 history_stats_mysql_connection_pool + +Historical connection pool metrics by timestamp. + +```sql +CREATE TABLE history_stats_mysql_connection_pool ( + timestamp INT NOT NULL, + hostgroup INT, + srv_host VARCHAR, + srv_port INT, + status VARCHAR, + ConnUsed INT, + ConnFree INT, + ConnOK INT, + ConnERR INT, + MaxConnUsed INT, + Queries INT, + Queries_GTID_sync INT, + Bytes_data_sent INT, + Bytes_data_recv INT, + Latency_us INT, + PRIMARY KEY (timestamp, hostgroup, srv_host, srv_port) +) +``` + +### 4.3 MyHGM (MySQL Host Group Manager) Metrics + +#### 4.3.1 myhgm_connections + +MyHGM internal connection metrics. + +```sql +CREATE TABLE myhgm_connections ( + timestamp INT NOT NULL, + MyHGM_myconnpoll_destroy INT NOT NULL, + MyHGM_myconnpoll_get INT NOT NULL, + MyHGM_myconnpoll_get_ok INT NOT NULL, + MyHGM_myconnpoll_push INT NOT NULL, + MyHGM_myconnpoll_reset INT NOT NULL, + PRIMARY KEY (timestamp) +) +``` + +### 4.4 MySQL Status Variables History + +#### 4.4.1 history_mysql_status_variables + +Time-series data for MySQL status variables. + +```sql +CREATE TABLE history_mysql_status_variables ( + timestamp INT NOT NULL, + variable_id INT NOT NULL, + variable_value VARCHAR NOT NULL, + PRIMARY KEY (timestamp, variable_id) +) +``` + +#### 4.4.2 history_mysql_status_variables_lookup + +Mapping table for variable names to IDs. + +```sql +CREATE TABLE history_mysql_status_variables_lookup ( + variable_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + variable_name VARCHAR NOT NULL, + UNIQUE (variable_name) +) +``` + +#### 4.4.3 history_pgsql_status_variables + +PostgreSQL status variables history. + +#### 4.4.4 history_pgsql_status_variables_lookup + +PostgreSQL variable name to ID mapping. + +### 4.5 Query Digest History + +#### 4.5.1 history_mysql_query_digest + +Historical query digest snapshots. + +```sql +CREATE TABLE history_mysql_query_digest ( + dump_time INT, + hostgroup INT, + schemaname VARCHAR NOT NULL, + username VARCHAR NOT NULL, + client_address VARCHAR NOT NULL, + digest VARCHAR NOT NULL, + digest_text VARCHAR NOT NULL, + count_star INTEGER NOT NULL, + first_seen INTEGER NOT NULL, + last_seen INTEGER NOT NULL, + sum_time INTEGER NOT NULL, + min_time INTEGER NOT NULL, + max_time INTEGER NOT NULL, + sum_rows_affected INTEGER NOT NULL, + sum_rows_sent INTEGER NOT NULL +) +``` + +#### 4.5.2 history_pgsql_query_digest + +PostgreSQL query digest history. + +### 4.6 Query Events History + +#### 4.6.1 history_mysql_query_events + +Historical query event log. + +```sql +CREATE TABLE history_mysql_query_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + thread_id INTEGER, + username TEXT, + schemaname TEXT, + start_time INTEGER, + end_time INTEGER, + query_digest TEXT, + query TEXT, + server TEXT, + client TEXT, + event_type INTEGER, + hid INTEGER, + extra_info TEXT, + affected_rows INTEGER, + last_insert_id INTEGER, + rows_sent INTEGER, + client_stmt_id INTEGER, + gtid TEXT, + errno INT, + error TEXT +) +``` + +## 5. System Statistics Tables + +### 5.1 CPU Metrics + +#### 5.1.1 system_cpu + +CPU usage statistics. + +```sql +CREATE TABLE system_cpu ( + timestamp INT NOT NULL, + tms_utime INT NOT NULL, + tms_stime INT NOT NULL, + PRIMARY KEY (timestamp) +) +``` + +#### 5.1.2 system_cpu_hour + +Hourly aggregated CPU metrics. + +#### 5.1.3 system_cpu_day + +Daily aggregated CPU metrics. + +### 5.2 Memory Metrics + +#### 5.2.1 system_memory + +Memory usage statistics (requires jemalloc, excluded with NOJEM). + +```sql +CREATE TABLE system_memory ( + timestamp INT NOT NULL, + allocated INT NOT NULL, + resident INT NOT NULL, + active INT NOT NULL, + mapped INT NOT NULL, + metadata INT NOT NULL, + retained INT NOT NULL, + PRIMARY KEY (timestamp) +) +``` + +#### 5.2.2 system_memory_hour + +Hourly aggregated memory metrics. + +#### 5.2.3 system_memory_day + +Daily aggregated memory metrics. + +### 5.3 Query Cache Metrics + +#### 5.3.1 mysql_query_cache + +Query cache statistics. + +```sql +CREATE TABLE mysql_query_cache ( + timestamp INT NOT NULL, + count_GET INT NOT NULL, + count_GET_OK INT NOT NULL, + count_SET INT NOT NULL, + bytes_IN INT NOT NULL, + bytes_OUT INT NOT NULL, + Entries_Purged INT NOT NULL, + Entries_In_Cache INT NOT NULL, + Memory_Bytes INT NOT NULL, + PRIMARY KEY (timestamp) +) +``` + +#### 5.3.2 mysql_query_cache_hour + +Hourly aggregated query cache metrics. + +#### 5.3.3 mysql_query_cache_day + +Daily aggregated query cache metrics. + +## 6. Table Reset Pattern + +Many stats tables have a `*_reset` counterpart: + +- `stats_mysql_connection_pool_reset` +- `stats_mysql_query_digest_reset` +- `stats_mysql_errors_reset` +- `stats_mysql_client_host_cache_reset` +- `stats_pgsql_connection_pool_reset` +- `stats_pgsql_query_digest_reset` +- `stats_pgsql_errors_reset` +- `stats_pgsql_client_host_cache_reset` +- `stats_proxysql_message_metrics_reset` + +**Purpose:** Allow preserving current stats before resetting. Reset is typically done by: +1. Reading from the regular table +2. Inserting into the `_reset` table +3. Resetting the regular table counters + +## 7. Database Architecture + +### 7.1 stats (In-Memory Database) + +Contains real-time statistics tables prefixed with `stats_*`. This database is attached to the main admin database as the `stats` schema. + +### 7.2 statsdb_disk (Persistent Database) + +Contains historical tables with time-series data. Attached as `stats_history` schema. Includes: +- Hourly aggregated tables (suffixed with `_hour`) +- Daily aggregated tables (suffixed with `_day`) +- Full-resolution history tables (prefixed with `history_`) + +### 7.3 statsdb_mem (Internal In-Memory) + +Internal statistics database used by ProxySQL for metrics collection and aggregation. + +## 8. Data Characteristics and Example Queries + +ProxySQL stats tables fall into a few distinct categories based on how they store data. + +### 8.1 Key-Value Tables + +These tables store metrics as `(Variable_Name, Variable_Value)` rows, where each row is a different metric. + +| Table | Description | +|---|---| +| `stats_mysql_global` | ~129 variables covering connections, queries, monitors, caches, and more. Mix of counters (cumulative since startup) and gauges (current value). | +| `stats_pgsql_global` | ~59 variables, same structure as the MySQL counterpart. | +| `stats_memory_metrics` | ~21 variables tracking memory usage across modules. All values in bytes. | + +**Example - Reading individual metrics:** +```sql +SELECT Variable_Value FROM stats_mysql_global +WHERE Variable_Name = 'Client_Connections_connected'; +``` + +**Example - Pivoting multiple metrics into a single row:** +```sql +SELECT + MAX(CASE WHEN Variable_Name = 'Client_Connections_connected' THEN Variable_Value END) AS connected, + MAX(CASE WHEN Variable_Name = 'Client_Connections_created' THEN Variable_Value END) AS created, + MAX(CASE WHEN Variable_Name = 'Questions' THEN Variable_Value END) AS questions +FROM stats_mysql_global; +``` + +### 8.2 Per-Entity Tables + +Each row represents one instance of an entity (a backend server, a query pattern, a user, etc.), with numeric columns sharing the same meaning across rows. + +| Table | Each Row Represents | +|---|---| +| `stats_mysql_connection_pool` | A backend server in a hostgroup | +| `stats_mysql_query_digest` | A unique query pattern (by digest) | +| `stats_mysql_commands_counters` | A MySQL command type | +| `stats_mysql_users` | A frontend user | +| `stats_mysql_errors` | An error type per server/user combination | +| `stats_mysql_query_rules` | A query rule | +| `stats_mysql_client_host_cache` | A client host | +| `stats_mysql_prepared_statements_info` | A prepared statement | + +**Example - Connection pool summary by hostgroup:** +```sql +SELECT + hostgroup, + SUM(ConnUsed) AS total_used, + SUM(ConnFree) AS total_free, + SUM(Queries) AS total_queries, + AVG(Latency_us) AS avg_latency_us +FROM stats_mysql_connection_pool +GROUP BY hostgroup; +``` + +**Example - Top 10 slowest query digests:** +```sql +SELECT digest_text, count_star, + sum_time / count_star AS avg_time_us, + max_time AS max_time_us +FROM stats_mysql_query_digest +ORDER BY sum_time DESC +LIMIT 10; +``` + +**Example - Error distribution by hostgroup:** +```sql +SELECT hostgroup, errno, SUM(count_star) AS total_errors +FROM stats_mysql_errors +GROUP BY hostgroup, errno +ORDER BY total_errors DESC; +``` + +### 8.3 Snapshot / Event Tables + +These tables capture live state or individual events. Their contents change constantly and are primarily useful for real-time debugging. + +| Table | Each Row Represents | +|---|---| +| `stats_mysql_processlist` | A currently active session | +| `stats_mysql_query_events` | An individual query event (log entry) | +| `stats_mysql_free_connections` | A currently idle connection in the pool | + +### 8.4 Historical / Time-Series Tables + +Stored in the persistent `statsdb_disk` database, these tables record timestamped snapshots at different granularities for trend analysis. + +| Table | Granularity | +|---|---| +| `mysql_connections` | Per-minute | +| `mysql_connections_hour` | Hourly | +| `mysql_connections_day` | Daily | +| `history_stats_mysql_connection_pool` | Per-minute, per-server | +| `system_cpu` / `system_cpu_hour` / `system_cpu_day` | Per-minute / Hourly / Daily | +| `system_memory` / `system_memory_hour` / `system_memory_day` | Per-minute / Hourly / Daily | +| `mysql_query_cache` / `mysql_query_cache_hour` / `mysql_query_cache_day` | Per-minute / Hourly / Daily | + +**Example - Connection trends over the last hour:** +```sql +SELECT timestamp, Questions, Client_Connections_connected +FROM mysql_connections +WHERE timestamp > strftime('%s','now','-1 hour'); +``` + +### 8.5 Data Collection Triggers + +Different tables are populated through different mechanisms: + +#### Timer-Based Collection + +Historical time-series tables are populated by periodic timers in the admin thread. Each metric type has its own configurable interval: + +| Admin Variable | Controls | Default | Valid Range | +|---|---|---|---| +| `admin-stats_mysql_connections` | `mysql_connections`, `myhgm_connections` tables | 60s | 0-300s | +| `admin-stats_system_cpu` | `system_cpu` table | 60s | 0-600s | +| `admin-stats_system_memory` | `system_memory` table (requires jemalloc) | 60s | 0-600s | +| `admin-stats_mysql_query_cache` | `mysql_query_cache` table | 60s | 0-300s | +| `admin-stats_mysql_query_digest_to_disk` | `history_mysql_query_digest` table | 0 (disabled) | 0+ seconds | +| `admin-stats_mysql_connection_pool` | `history_stats_mysql_connection_pool` table | 60s | 0-300s | + +Setting a variable to `0` disables collection for that metric type. + +**Allowed Discrete Intervals:** Values are rounded to the nearest allowed interval: 0, 1, 5, 10, 30, 60, 120, 300, or 600 seconds. + +#### Per-Query Updates + +These tables are updated in real-time as queries execute: + +| Table | Update Trigger | +|---|---| +| `stats_mysql_query_digest` | After each query completes, stats are aggregated into the in-memory digest map | +| `stats_mysql_commands_counters` | After each command completes | +| `stats_mysql_query_rules` | When a query rule is matched | +| `stats_mysql_errors` | When an error occurs | + +#### On-Demand / Manual Collection + +| Table | Trigger | +|---|---| +| `stats_mysql_query_events` | `DUMP EVENTSLOG FROM BUFFER TO MEMORY` command | +| `history_mysql_query_events` | `DUMP EVENTSLOG FROM BUFFER TO DISK` command | +| `history_mysql_query_digest` | `SAVE MYSQL DIGEST TO DISK` command or periodic timer | + +### 8.6 Retention Policies + +Historical tables in `statsdb_disk` have automatic retention management: + +| Table Type | Retention Period | Cleanup Trigger | +|---|---|---| +| Raw tables (`mysql_connections`, `system_cpu`, etc.) | **7 days** | On each new data insertion | +| Hourly aggregated tables (`*_hour`) | **365 days** | On each new data insertion | +| Daily aggregated tables (`*_day`) | Not populated | N/A | + +**Note:** The `*_day` tables are defined in the schema but are **never populated** by the current codebase. Only raw and hourly granularities contain data. + +Retention cleanup happens automatically after each data insertion. For example, after inserting a new row into `system_cpu`, rows older than 7 days are deleted, and then the hourly aggregation is checked and performed if needed. + +### 8.7 Aggregation Methods for Hourly Tables + +When raw data is aggregated into `_hour` tables, different columns use different aggregation functions based on their metric type: + +#### `system_cpu_hour` + +| Column | Aggregation | Reasoning | +|---|---|---| +| `tms_utime` | **SUM** | CPU time is cumulative; sum gives total ticks in the hour | +| `tms_stime` | **SUM** | Same as above | + +#### `system_memory_hour` + +| Column | Aggregation | Reasoning | +|---|---|---| +| `allocated` | **AVG** | Memory is a point-in-time gauge; average gives representative value | +| `resident` | **AVG** | Same as above | +| `active` | **AVG** | Same as above | +| `mapped` | **AVG** | Same as above | +| `metadata` | **AVG** | Same as above | +| `retained` | **AVG** | Same as above | + +#### `mysql_connections_hour` + +| Column | Aggregation | Reasoning | +|---|---|---| +| `Client_Connections_aborted` | **MAX** | Counter - MAX captures end-of-hour cumulative value | +| `Client_Connections_connected` | **AVG** | Gauge - AVG gives average connections during the hour | +| `Client_Connections_created` | **MAX** | Counter | +| `Server_Connections_aborted` | **MAX** | Counter | +| `Server_Connections_connected` | **AVG** | Gauge | +| `Server_Connections_created` | **MAX** | Counter | +| `ConnPool_get_conn_failure` | **MAX** | Counter | +| `ConnPool_get_conn_immediate` | **MAX** | Counter | +| `ConnPool_get_conn_success` | **MAX** | Counter | +| `Questions` | **MAX** | Counter | +| `Slow_queries` | **MAX** | Counter | +| `GTID_consistent_queries` | **MAX** | Counter | + +#### `mysql_query_cache_hour` + +| Column | Aggregation | Reasoning | +|---|---|---| +| `count_GET` | **MAX** | Counter | +| `count_GET_OK` | **MAX** | Counter | +| `count_SET` | **MAX** | Counter | +| `bytes_IN` | **MAX** | Counter | +| `bytes_OUT` | **MAX** | Counter | +| `Entries_Purged` | **MAX** | Counter | +| `Entries_In_Cache` | **AVG** | Gauge | +| `Memory_Bytes` | **AVG** | Gauge | + +#### `myhgm_connections_hour` + +| Column | Aggregation | Reasoning | +|---|---|---| +| `MyHGM_myconnpoll_destroy` | **MAX** | Counter | +| `MyHGM_myconnpoll_get` | **MAX** | Counter | +| `MyHGM_myconnpoll_get_ok` | **MAX** | Counter | +| `MyHGM_myconnpoll_push` | **MAX** | Counter | +| `MyHGM_myconnpoll_reset` | **MAX** | Counter | + +**Aggregation Timing:** Hourly aggregation is performed lazily. Each time a new raw row is inserted, the system checks if the current time is at least 3600 seconds past the last hourly entry. If so, it runs the aggregation INSERT for the completed hour(s). + +### 8.8 Buffer and Flush Mechanics + +Some tables require explicit flush operations to make data visible. + +#### Query Events Buffer + +Query events are first collected into a **circular buffer** in memory, not directly into SQLite tables. + +**Data Flow:** +``` +Query executes + ↓ +MySQL_Logger::log_request() adds event to circular buffer (MyLogCB) + ↓ +Buffer accumulates events (size controlled by mysql_eventslog_table_memory_size) + ↓ +Manual DUMP command drains buffer to SQLite tables +``` + +**Flush Commands:** + +| Command | Destination | +|---|---| +| `DUMP EVENTSLOG FROM BUFFER TO MEMORY` | `stats_mysql_query_events` (in-memory) | +| `DUMP EVENTSLOG FROM BUFFER TO DISK` | `history_mysql_query_events` (on-disk) | +| `DUMP EVENTSLOG FROM BUFFER TO BOTH` | Both tables | + +**Important:** The `get_all_events()` operation **drains** the buffer. Once flushed, events are removed from the buffer. There is no way to peek at buffer contents without draining. + +**Retention Differences:** +- `stats_mysql_query_events`: Capped to `eventslog_table_memory_size` entries; oldest rows evicted when full +- `history_mysql_query_events`: Append-only, no automatic eviction + +#### Query Digest Snapshot + +Query digest statistics are maintained in an in-memory hash map (`digest_umap`), not in SQLite. The map is updated in real-time as queries complete. + +**Reading Live Data (Non-Destructive):** + +When you `SELECT FROM stats_mysql_query_digest`, ProxySQL: +1. Briefly swaps the live map with an empty map +2. Serializes the swapped data to an in-memory SQLite table +3. Merges any new entries accumulated during serialization back into the map +4. Executes your query against the SQLite table + +This is a **non-destructive** read — the data is preserved. + +**Saving to History (Destructive):** + +When you run `SAVE MYSQL DIGEST TO DISK` (or the periodic timer triggers): +1. The live map is atomically swapped with an empty map +2. The swapped data is written to `history_mysql_query_digest` with a `dump_time` column +3. The swapped data is deleted — **the live map is now empty** +4. New queries immediately start accumulating in the fresh empty map + +**Snapshot Characteristics:** +- Each `SAVE` produces a batch of rows with the same `dump_time` +- Each snapshot represents stats accumulated **since the previous save** +- The same digest can appear in multiple snapshots with different counts +- `first_seen`/`last_seen` timestamps are converted from monotonic to wall-clock time + +### 8.9 PostgreSQL Table Coverage + +Not all tables have PostgreSQL equivalents. Here is the coverage matrix: + +#### Live Statistics Tables + +| MySQL Table | PostgreSQL Equivalent | Notes | +|---|---|---| +| `stats_mysql_global` | `stats_pgsql_global` | PostgreSQL has ~59 variables vs MySQL's ~129 | +| `stats_mysql_processlist` | `stats_pgsql_processlist` | Includes additional `backend_pid`, `backend_state` columns | +| `stats_mysql_query_digest` | `stats_pgsql_query_digest` | Uses `database` instead of `schemaname` | +| `stats_mysql_connection_pool` | `stats_pgsql_connection_pool` | No `Queries_GTID_sync` column | +| `stats_mysql_free_connections` | `stats_pgsql_free_connections` | Uses `database` and `pgsql_info` columns | +| `stats_mysql_commands_counters` | `stats_pgsql_commands_counters` | Full equivalent | +| `stats_mysql_users` | `stats_pgsql_users` | Full equivalent | +| `stats_mysql_errors` | `stats_pgsql_errors` | Uses `sqlstate` instead of `errno` | +| `stats_mysql_query_rules` | `stats_pgsql_query_rules` | Full equivalent | +| `stats_mysql_client_host_cache` | `stats_pgsql_client_host_cache` | Full equivalent | +| `stats_mysql_prepared_statements_info` | `stats_pgsql_prepared_statements_info` | Uses `database`, `num_param_types` columns | +| `stats_mysql_gtid_executed` | — | **MySQL only** (GTID is MySQL-specific) | +| `stats_mysql_query_events` | — | **MySQL only** | + +#### Historical Tables + +| MySQL Table | PostgreSQL Equivalent | Notes | +|---|---|---| +| `history_mysql_query_digest` | `history_pgsql_query_digest` | Full equivalent | +| `history_mysql_query_events` | — | **MySQL only** | +| `mysql_connections` | — | **MySQL only** | +| `mysql_connections_hour` | — | **MySQL only** | +| `myhgm_connections` | — | **MySQL only** | +| `myhgm_connections_hour` | — | **MySQL only** | +| `history_stats_mysql_connection_pool` | — | **MySQL only** | +| `mysql_query_cache` | — | **MySQL only** | +| `mysql_query_cache_hour` | — | **MySQL only** | +| `history_mysql_status_variables` | `history_pgsql_status_variables` | Full equivalent | + +#### Shared Tables (Database-Agnostic) + +These tables are shared between MySQL and PostgreSQL as they track ProxySQL itself: + +- `stats_proxysql_servers_status` +- `stats_proxysql_servers_metrics` +- `stats_proxysql_servers_checksums` +- `stats_proxysql_servers_clients_status` +- `stats_memory_metrics` +- `system_cpu` / `system_cpu_hour` +- `system_memory` / `system_memory_hour` + +## 9. Common Notes + +1. **Schema Versioning:** Tables have version-specific definitions; current versions are aliased without version suffixes. + +2. **Reset Capability:** Most counters support reset via `*_reset` tables. + +3. **Time Units:** + - `*_us` = microseconds + - `*_ms` = milliseconds + - `*_s` = seconds + - Unqualified timestamps are Unix epoch (seconds) + +4. **Primary Keys:** Most tables use composite keys for efficient data aggregation. + +5. **Nullable Columns:** Some columns may be `NULL` depending on configuration (e.g., `status`, `port`, `hostgroup`). + +6. **Views:** Some tables like `stats_pgsql_stat_activity` are actually views and cannot be dropped or modified directly. + +7. **Performance Considerations:** Historical tables can grow large; ProxySQL manages retention through hourly/daily aggregation tables. + +## 10. File References + +Schema definitions are located in: +- `include/ProxySQL_Admin_Tables_Definitions.h` - Stats table definitions +- `include/ProxySQL_Statistics.hpp` - Historical/Time-series table definitions +- `lib/ProxySQL_Statistics.cpp` - Statistics initialization and management +- `lib/Admin_Bootstrap.cpp` - Stats table registration +- `lib/ProxySQL_Admin_Stats.cpp` - Stats table population logic