PostgreSQLFlexQueryStoreRuntime — Query Store Runtime
Aggregated execution statistics per normalized query, per collection interval
(default 15 min). Sourced from pg_stat_statements via Azure Query Store. Every
execution is counted — no data is lost between collection windows.
Kusto query
let server = "my-pg-prd-1";
AzureDiagnostics
| where Category == "PostgreSQLFlexQueryStoreRuntime" and LogicalServerName_s == server
| project Queryid_str_s, Dbid_d, Userid_d, Query_type_s, Is_system_query_b,
Start_time_t, End_time_t,
Calls_d, Rows_d, Total_time_d, Mean_time_d, Min_time_d, Max_time_d, Stddev_time_d,
Shared_blks_hit_d, Shared_blks_read_d, Shared_blks_dirtied_d, Shared_blks_written_d,
Local_blks_hit_d, Local_blks_read_d, Local_blks_dirtied_d, Local_blks_written_d,
Temp_blks_read_d, Temp_blks_written_d, Blk_read_time_d, Blk_write_time_d,
AdditionalFields
Fields
Query identity
Queryid_d(number) — query fingerprint (numeric; may lose precision — preferQueryid_str_s).Queryid_str_s(string) — same fingerprint as a string (use this for joins/exact match).Dbid_d(number) — database OID.Userid_d(number) — role OID that ran the query.Query_type_s(string) — statement type:select,insert,update,delete, or empty for utility commands.Is_system_query_b(boolean) — whether it's an internal/system query.AdditionalFields(dynamic) — extra context such asSearch_path.
Collection window
Start_time_t/End_time_t(datetime) — bounds of the aggregation interval.Runtime_stats_entry_id_d(number) — internal ID of the stats bucket inazure_sys.
Execution counters & timing (cumulative over the window)
Calls_d(number) — number of executions.Rows_d(number) — total rows returned/affected.Total_time_d(number, ms) — total execution time.Mean_time_d/Min_time_d/Max_time_d/Stddev_time_d(number, ms) — per-call time distribution.
Buffer / block I/O
Shared_blks_hit_d/Shared_blks_read_d/Shared_blks_dirtied_d/Shared_blks_written_d(number) — shared buffer cache hits, disk reads, dirtied and written blocks.Local_blks_hit_d/Local_blks_read_d/Local_blks_dirtied_d/Local_blks_written_d(number) — same for local (temp-table) buffers.Temp_blks_read_d/Temp_blks_written_d(number) — temp file blocks read/written (spills).Blk_read_time_d/Blk_write_time_d(number, ms) — time spent reading/writing blocks (needstrack_io_timing).
For a deep explanation of the buffer I/O fields see Postgres — Buffer I/O explained.
Use cases — troubleshooting
- Find the most expensive queries — rank by
Total_time_d(overall load) orMean_time_d(per-call slowness). - High CPU investigation — top queries by
Total_time_d/Calls_ddriving CPU. - Cache efficiency / I/O-bound queries — low
Shared_blks_hit_dvs highShared_blks_read_d, or highBlk_read_time_d. - Temp-file spills (work_mem too low) — non-zero
Temp_blks_read_d/Temp_blks_written_d. - Write amplification — high
Shared_blks_dirtied_d/Shared_blks_written_d. - Plan/perf regressions over time — compare
Mean_time_d/Stddev_time_dfor the sameQueryid_str_sacross windows. - Chatty queries — high
Calls_dwith low per-call cost (N+1 patterns). - Result-set blowups — unexpectedly large
Rows_d.
Q&A
Q&A
Q: What does an empty Query_type_s mean?
Query_type_s classifies the statement as select, insert, update, or delete. An empty value means the statement is a utility command — anything outside those four DML categories: CREATE, ALTER, DROP, TRUNCATE, VACUUM, ANALYZE, COPY, SET, SHOW, BEGIN, COMMIT, ROLLBACK, EXPLAIN, etc. Query Store tracks these too (they have execution time and I/O cost), but they don't map to a DML type so the field is left blank.
Q: What is the "stats bucket" behind Runtime_stats_entry_id_d?
Query Store does not keep one row per execution. Instead it aggregates all executions of the same query fingerprint within a fixed time window (default 15 minutes, configurable via pg_qs.interval_length_minutes). That aggregated row is the stats bucket: one row per (Queryid, Dbid, Userid, time-window). Runtime_stats_entry_id_d is its internal primary key in query_store.runtime_stats in the azure_sys schema on the server. Not useful for direct analysis.
Q: What exactly does Calls_d count? And what does Rows_d count?
Calls_d is the number of times the Postgres query executor completed a full execution of that query pattern within the window — one parse → plan → execute cycle per call. It does not count individual syscalls, sub-queries, or I/O operations underneath.
Rows_d is the total rows processed at the output boundary of the executor across all calls:
| Type | What Rows_d counts |
|---|---|
SELECT | Rows returned to the client |
INSERT | Rows inserted |
UPDATE | Rows updated |
DELETE | Rows deleted |
| Utility | Usually 0 |
A SELECT that scans 1 million rows to return 5 has Rows_d = 5 — the scan cost shows up in Shared_blks_read_d instead.
Q: What is Stddev_time_d?
Population standard deviation of per-execution duration (ms) across all Calls_d in the window: sqrt(sum((t_i − mean)²) / n).
| Mean | Stddev | Interpretation |
|---|---|---|
| low | low | Fast and consistent — healthy |
| high | low | Slow but consistent — likely a plan or index problem |
| any | high | Unpredictable — sometimes fast, sometimes slow |
| high | >> mean | Bimodal: the query occasionally takes dramatically longer |
A high stddev relative to mean points to: intermittent lock waits, parameter-dependent plan choices, cache cold-start effects, or data skew. Cross-reference with PostgreSQLFlexQueryStoreWaitStats for the same Queryid_str_s and window to see what the outlier executions were waiting on.
Q: Why are some Queryid values negative?
The query ID is a 64-bit hash of the normalized query parse tree (literals replaced with $1, $2… then hashed). Hash functions distribute values uniformly across all 64-bit patterns. When stored as a signed 64-bit integer, any hash whose most significant bit is 1 reads as negative — about half of all IDs. The sign is an artifact of signed integer interpretation, not a semantic distinction.
64-bit hash output:
0xxxxxxx xxxxxxxx … → positive when read as int64
1xxxxxxx xxxxxxxx … → negative when read as int64 (MSB = 1)
The normalization step makes the ID stable: WHERE id = 42 and WHERE id = 99 produce the same Queryid because both normalize to WHERE id = $1.
Queryid_d (float column) has only 53 bits of mantissa — a full 64-bit integer silently loses precision when cast to double, so two distinct queries can collide. Always use Queryid_str_s for joins and filtering.