Skip to content

Commit

Permalink
Add cb psql query menu.
Browse files Browse the repository at this point in the history
This is an initial phase of supporting a builtin menu of useful queries
when using `cb psql`. Currently, it only supports predefined queries. To
access this menu, simply enter `:menu` into psql and it will present the
available query options.

Example:

```
Example Team/example-cluster/postgres=> :menu
Cache
  1 – Show cache and index hit rates
Connection Management
  2 – Show connection count by state
  3 – Show connection count by user and application
Extensions
  4 – Show available extensions
  5 – Show installed extensions
Indexes
  6 – Show duplicate indexes
  7 – Show list of indexes
  8 – Show unused indexes
Locks
  9 – Show blocking queries
Query Performance
  10 – Show queries consuming the most system time
  11 – Show queries running over 1 minute
  12 – Show slowest average queries
Size Information
  13 – Show database sizes
  14 – Show table sizes

Type choice and press <Enter> (q to quit):

```
  • Loading branch information
abrightwell committed Jan 26, 2024
1 parent 1e40e1e commit 6223176
Show file tree
Hide file tree
Showing 21 changed files with 698 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- `cb psql` now provides a builtin menu of commonly useful queries.

## [3.4.4] - 2024-01-23
### Fixed
Expand Down
2 changes: 2 additions & 0 deletions src/cb/psql.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "./action"
require "./dirs"
require "./query_menu/*"

module CB
class Psql < APIAction
Expand Down Expand Up @@ -100,6 +101,7 @@ module CB
File.open(psqlrc.path, "a") do |f|
f.puts "\\set ON_ERROR_ROLLBACK interactive"
f.puts "\\set PROMPT1 '#{psqlpromptname}/%[%033[33;1m%]%x%x%x%[%033[0m%]%[%033[1m%]%/%[%033[0m%]%R%# '"
f.puts QueryMenu::Menu.new.render(cluster: c)
end

psqlrc.path.to_s
Expand Down
32 changes: 32 additions & 0 deletions src/cb/query_menu/menu.psql.ecr
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<%- option : Int32 = 1 %>
<%- @queries.each do |category, queries| -%>
\echo <%= "#{category.colorize.bold}" %>
<%- queries.each do |query| -%>
\echo ' <%= "#{option} \u2013 #{query.label}" %>'
<%- option += 1 -%>
<%- end -%>
<%- end -%>
\echo
\prompt 'Type choice and press <%= "<Enter>".colorize.bold %> (<%= "q".colorize.bold %> to quit): ' choice
\echo

<%- option = 1 %>
SELECT CASE
<%- @queries.each_value do |queries| -%>
<%- queries.each do |query| %>
WHEN :'choice'::text = '<%= option %>' THEN
'\i `echo <%= query.path %>`'
'\echo'
'\i <%= @path %>'
<%- option += 1 -%>
<%- end -%>
<%- end -%>
WHEN :'choice'::text = 'q'
THEN '\echo Quitting!'
ELSE
'\echo <%= "Error:".colorize.red.bold %> Unknown option! Try again.'
'\echo'
'\i <%= @path %>'
END AS action \gset

:action
98 changes: 98 additions & 0 deletions src/cb/query_menu/queries.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
require "./query"

module CB::QueryMenu
# Cache
@[Metadata(label: "Show cache and index hit rates", category: "Cache")]
class CacheHitRates < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/cache_hit_rates.sql")
end

#
# Connection Management
#

@[Metadata(label: "Show connection count by state", category: "Connection Management")]
class ConnectionManagementCountByStates < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/connection_management_count_by_state.sql")
end

@[Metadata(label: "Show connection count by user and application", category: "Connection Management")]
class ConnectionManagementCountByUser < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/connection_management_count_by_user_and_application.sql")
end

#
# Extensions Queries
#

@[Metadata(label: "Show available extensions", category: "Extensions")]
class AvailableExtensions < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/extensions_available.sql")
end

@[Metadata(label: "Show installed extensions", category: "Extensions")]
class InstalledExtensions < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/extensions_installed.sql")
end

#
# Index Queries
#

@[Metadata(label: "Show duplicate indexes", category: "Indexes")]
class IndexesDuplicates < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/indexes_duplicates.sql")
end

@[Metadata(label: "Show list of indexes", category: "Indexes")]
class IndexesList < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/indexes_list.sql")
end

@[Metadata(label: "Show unused indexes", category: "Indexes")]
class IndexesUnused < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/indexes_unused.sql")
end

#
# Locks Queries
#

@[Metadata(label: "Show blocking queries", category: "Locks")]
class LocksBlockingQueries < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/locks_blocking_queries.sql")
end

#
# Query Performance Queries
#

@[Metadata(label: "Show queries consuming the most system time", category: "Query Performance")]
class MostConsumingQueries < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/query_performance_most_consuming_system_time.sql")
end

@[Metadata(label: "Show queries running over 1 minute", category: "Query Performance")]
class OverOneMinuteQueries < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/query_performance_over_one_minute.sql")
end

@[Metadata(label: "Show slowest average queries", category: "Query Performance")]
class SlowestAverageQueries < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/query_performance_slowest_average.sql")
end

#
# Size Information Queries
#

@[Metadata(label: "Show database sizes", category: "Size Information")]
class DatabaseSize < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/size_information_database_size.sql")
end

@[Metadata(label: "Show table sizes", category: "Size Information")]
class TableSize < Query
::CB::QueryMenu.embed_sql("#{__DIR__}/sql/size_information_table_size.sql")
end
end
49 changes: 49 additions & 0 deletions src/cb/query_menu/query.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module CB::QueryMenu
annotation Metadata; end

@[Metadata]
abstract class Query
@dirname : String

def initialize(@dirname); end

def path
File.join(@dirname, sql_filename)
end

def write
File.open(path, "w") { |file| file << sql }
end

def self.all
{{
Query.subclasses.map do |query|
ann = query.annotation(Metadata)
raise "#{query} is missing Metadata annotation" unless ann
query
end
}}.sort_by(&.name)
end

def category
{{ @type.annotation(Metadata)[:category] }}.to_s
end

def label
{{ @type.annotation(Metadata)[:label] }}.to_s
end

abstract def sql
abstract def sql_filename
end

macro embed_sql(path)
def sql
{{ run("../../tools/embed.cr", path).stringify }}
end

def sql_filename
File.basename {{path}}
end
end
end
30 changes: 30 additions & 0 deletions src/cb/query_menu/query_menu.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "ecr"
require "file_utils"

require "./query"

module CB::QueryMenu
class Menu
private property queries : Hash(String, Array(Query)) = Hash(String, Array(Query)).new([] of Query)
private property path : String = ""

def render(cluster : CB::Model::Cluster) : String
temp_dir = "/tmp/crunchy/cli/#{cluster.name}-#{cluster.id}-queries"
FileUtils.mkdir_p(temp_dir) unless File.exists? temp_dir

# Aggregate all queries and group them by category in alphabetical order.
@queries = Query.all.map(&.new(temp_dir)).sort_by!(&.category).group_by(&.category)

# Write the queries to the filesystem.
@queries.each_value { |queries| queries.each(&.write) }

# Render the menu file.
query_menu = File.open(File.join(temp_dir, "menu.psql"), mode: "w") do |menu|
@path = menu.path
menu << ECR.render __DIR__ + "/menu.psql.ecr"
end

"\\set menu '\\\\i #{query_menu.path} '"
end
end
end
54 changes: 54 additions & 0 deletions src/cb/query_menu/sql/cache_hit_rates.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
SELECT
cache_rates.schemaname,
sizes.name AS "Table Name",
cache_rates.ratio AS "Cache Hit Ratio",
indexes.ratio AS "Index Hit Ratio",
CASE WHEN total_reads.cache_reads > 0 THEN ROUND((cache_rates.cache_reads/total_reads.cache_reads * 100), 2) ELSE 0 END AS "Read Percentage",
CASE WHEN rowcount.estimate = -1 THEN 0 ELSE rowcount.estimate END AS "Row Count",
CASE WHEN size = 8192 THEN '0 bytes' ELSE pg_size_pretty(size) END AS "Size"
FROM (
SELECT
n.nspname AS schemaname,
c.relname AS name,
pg_table_size(c.oid) AS size
FROM pg_class c
LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
AND n.nspname !~ '^pg_toast'
AND c.relkind='r'
) AS sizes
INNER JOIN (
SELECT
schemaname,
relname,
(sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100)::int AS ratio,
sum(heap_blks_read) AS cache_reads
FROM pg_statio_user_tables
GROUP BY relname, schemaname) AS cache_rates ON sizes.name = cache_rates.relname
AND sizes.schemaname = cache_rates.schemaname
INNER JOIN (
SELECT sum(heap_blks_read) AS cache_reads
FROM pg_statio_user_tables
) AS total_reads ON 1 = 1
LEFT JOIN (
SELECT
schemaname,
relname,
(sum(idx_blks_hit) / nullif(sum(idx_blks_hit + idx_blks_read),0) * 100)::int AS ratio
FROM pg_statio_user_indexes
GROUP BY schemaname,relname
) AS indexes ON sizes.name = indexes.relname
AND sizes.schemaname = indexes.schemaname
LEFT JOIN (
SELECT
reltuples AS estimate,
c.relname AS name,
n.nspname AS schemaname
FROM pg_class c
LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
AND n.nspname !~ '^pg_toast'
AND c.relkind='r'
) AS rowcount ON sizes.name = rowcount.name
AND sizes.schemaname = rowcount.schemaname
ORDER BY size DESC
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
SELECT
usename AS user_name,
state,
count(*) AS connection_count
FROM pg_stat_activity
WHERE usename NOT IN ('crunchy_replication', 'crunchy_superuser')
GROUP BY usename, state
ORDER BY 3 DESC;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
SELECT
usename as user_name,
application_name,
count(*) as connection_count
FROM pg_stat_activity
WHERE usename NOT IN ('crunchy_replication', 'crunchy_superuser')
GROUP BY usename, application_name
ORDER BY 3 DESC;
1 change: 1 addition & 0 deletions src/cb/query_menu/sql/extensions_available.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT * FROM pg_available_extensions
1 change: 1 addition & 0 deletions src/cb/query_menu/sql/extensions_installed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT * FROM pg_extension;
Loading

0 comments on commit 6223176

Please sign in to comment.