Dear hackers, Please find attached the patch to add a new metacommand (\dh) to psql. I find the functionality very useful for my day-to-day job, and decided to add it to psql source code. Summary of the command, justification for adding the functionality, as well as design choices are listed below.
Best Regards, Sadeq Dousti Tech Lead and Staff Software Engineer at Trade Republic Bank GmbH ================ Summary ------------ The \dh command provides a concise overview of high-level (root) relations, including tables and indexes. This helps DBAs and developers quickly identify the core structure of a database, particularly in environments with extensive partitioning. It offers two variants: \dht for only showing root tables, and \dhi for only showing root indexes. By adding the + option, additional details such as the total sizes of the relations are also displayed. Reference ------------ Commit 1c5d927 (psql \dP metacommand) is used as the basis for this patch. Justification ------------ In databases with partitioned tables, standard listing commands such as \dt can produce long and cluttered outputs due to the large number of partitions. The \dh command filters the output to show only root tables (both regular and partitioned) and high-level indexes, improving readability and usability. This enhancement allows users to: * Quickly scan the primary structure of a database without being overwhelmed by partitions. * View table ownership and optionally include size information. * Identify which indexes belong to which tables, along with descriptions. Comparison Example ------------ Using \dt in a partitioned database results in an extensive list that includes all partitions: postgres=# \dt List of tables Schema | Name | Type | Owner --------+------+-------------------+------- public | t | table | sadeq public | z | partitioned table | sadeq public | z_1 | table | sadeq public | z_2 | table | sadeq public | z_3 | table | sadeq public | z_4 | table | sadeq public | z_5 | table | sadeq public | z_6 | table | sadeq public | z_7 | table | sadeq public | z_8 | table | sadeq public | z_9 | table | sadeq public | z_10 | table | sadeq public | z_11 | table | sadeq public | z_12 | table | sadeq public | z_13 | table | sadeq public | z_14 | table | sadeq public | z_15 | table | sadeq The \dh command simplifies this view by displaying only the root tables: postgres=# \dht List of root tables Schema | Name | Owner --------+------+------- public | t | sadeq public | z | sadeq Design Decision: Introduction of the New Command \dh ------------ In the development of the \dh command to list high-level (root) tables and indexes, it was initially considered to extend the existing \dP command to support this functionality. However, after evaluating the existing codebase, it became clear that adding this feature to \dP would introduce significant complexity and reduce code readability. Specifically, the following points led to the decision to implement a new command instead of extending \dP or \d variant: 1. Functionality Mismatch: The \dP command is designed to display partitioned tables. Modifying it to also show non-partitioned table can potentially confuse the user. 2. Additional Branching and Readability: Introducing additional functionality to support high-level (root) relations would require a substantial amount of branching logic, making the code more difficult to follow and potentially error-prone. 3. Consistency with Existing \d Variants: Other \d variants exhibit a high degree of functional separation, with each command serving a unique role related to a specific type of database object. The logic for \dh is closer to \dP than any other \d variant, and it's already ruled out for the sake of readability. Given these factors, the decision was made to introduce a new command, \dh, to provide a concise overview of root tables and indexes. This approach ensures clarity, avoids unnecessary complexity, and maintains consistency with the overall design philosophy of psql metacommands. Further Details ------------ * Patch is against the master branch * Code compiles successfully. New functionality is tested manually with success * Regression tests are added, and all existing and new tests pass * Documentation (psql-ref.sgml) is updated with details for \dh
From 37c70da316ac072ef160e43406906696f1c5787c Mon Sep 17 00:00:00 2001 From: Sadeq Dousti <3616518+msdou...@users.noreply.github.com> Date: Sat, 22 Feb 2025 23:16:44 +0100 Subject: [PATCH] psql \dh: list high-level tables and indexes --- doc/src/sgml/ref/psql-ref.sgml | 22 ++++ src/bin/psql/command.c | 18 +++ src/bin/psql/describe.c | 196 +++++++++++++++++++++++++++++ src/bin/psql/describe.h | 3 + src/bin/psql/help.c | 1 + src/bin/psql/tab-complete.in.c | 43 ++++++- src/test/regress/expected/psql.out | 59 +++++++++ src/test/regress/sql/psql.sql | 13 ++ 8 files changed, 354 insertions(+), 1 deletion(-) diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 3edbd65e46..b1ed4a3bfa 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1871,6 +1871,28 @@ SELECT $1 \parse stmt1 </listitem> </varlistentry> + <varlistentry> + <term><literal>\dh[it+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term> + <listitem> + <para> + Lists high-level (root) relations. This means relations that are + either not partitioned, or partitioned and have no parent. + If <replaceable class="parameter">pattern</replaceable> + is specified, only entries whose name matches the pattern are listed. + The modifiers <literal>t</literal> (tables) and <literal>i</literal> + (indexes) can be appended to the command, filtering the kind of + relations to list. By default, high-level tables and indexes are + listed. + </para> + + <para> + If <literal>+</literal> is appended to the command, the size of the + relation (when it's not partitioned), or the sum of the sizes of + the table's partitions (when the relation is partitioned) is also + displayed, along with the associated description. + </para> + </listitem> + </varlistentry> <varlistentry id="app-psql-meta-command-dl-lc"> <term><literal>\dl[x+]</literal></term> diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 0f27bf7a91..9fff186401 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1117,6 +1117,24 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) /* no longer distinct from \du */ success = describeRoles(pattern, show_verbose, show_system); break; + case 'h': + /* high-level (i.e., root) tables and indexes */ + { + switch (cmd[2]) + { + case '\0': + case '+': + case 't': + case 'i': + case 'x': + success = listRootTables(&cmd[2], pattern, show_verbose); + break; + default: + status = PSQL_CMD_UNKNOWN; + break; + } + } + break; case 'l': success = listLargeObjects(show_verbose); break; diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9..3327065051 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -4247,6 +4247,202 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys return true; } +/* + * \dh + * Takes an optional regexp to select particular relations + * + * As with \d, you can specify the kinds of relations you want: + * + * t for tables + * i for indexes + * + * and you can mix and match them in any order. + */ +bool +listRootTables(const char *reltypes, const char *pattern, bool verbose) +{ + bool showTables = strchr(reltypes, 't') != NULL; + bool showIndexes = strchr(reltypes, 'i') != NULL; + PQExpBufferData buf; + PQExpBufferData title; + PGresult *res; + printQueryOpt myopt = pset.popt; + bool translate_columns[] = {false, false, false, false, false, false, false, false}; + const char *tabletitle; + bool mixed_output = false; + + /* + * Note: Declarative table partitioning is only supported as of Pg 10.0. + */ + if (pset.sversion < 100000) + { + char sverbuf[32]; + + pg_log_error("The server (version %s) does not support declarative table partitioning.", + formatPGVersionNumber(pset.sversion, false, + sverbuf, sizeof(sverbuf))); + return true; + } + + /* If no relation kind was selected, show them all */ + if (!showTables && !showIndexes) + showTables = showIndexes = true; + + if (showIndexes && !showTables) + tabletitle = _("List of root indexes"); /* \dhi */ + else if (showTables && !showIndexes) + tabletitle = _("List of root tables"); /* \dht */ + else + { + /* show all kinds */ + tabletitle = _("List of root relations"); + mixed_output = true; + } + + initPQExpBuffer(&buf); + + printfPQExpBuffer(&buf, + "SELECT n.nspname as \"%s\",\n" + " c.relname as \"%s\",\n" + " pg_catalog.pg_get_userbyid(c.relowner) as \"%s\"", + gettext_noop("Schema"), + gettext_noop("Name"), + gettext_noop("Owner")); + + if (mixed_output) + { + appendPQExpBuffer(&buf, + ",\n CASE c.relkind" + " WHEN " CppAsString2(RELKIND_RELATION) " THEN '%s'" + " WHEN " CppAsString2(RELKIND_PARTITIONED_TABLE) " THEN '%s'" + " WHEN " CppAsString2(RELKIND_INDEX) " THEN '%s'" + " WHEN " CppAsString2(RELKIND_PARTITIONED_INDEX) " THEN '%s'" + " END as \"%s\"", + gettext_noop("table"), + gettext_noop("partitioned table"), + gettext_noop("index"), + gettext_noop("partitioned index"), + gettext_noop("Type")); + + translate_columns[3] = true; + } + + if (showIndexes) + appendPQExpBuffer(&buf, + ",\n c2.oid::pg_catalog.regclass as \"%s\"", + gettext_noop("Table")); + + if (verbose) + { + /* + * Table access methods were introduced in v12, and can be set on + * partitioned tables since v17. + */ + appendPQExpBuffer(&buf, ",\n am.amname as \"%s\"", + gettext_noop("Access method")); + + /* Sizes of all partitions are considered in this case. */ + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(GREATEST(pg_catalog.pg_table_size(c.oid), s.tps)) as \"%s\"", + gettext_noop("Total size")); + + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"", + gettext_noop("Description")); + } + + appendPQExpBufferStr(&buf, + "\nFROM pg_catalog.pg_class c" + "\n LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace"); + + if (showIndexes) + appendPQExpBufferStr(&buf, + "\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid" + "\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid"); + + if (verbose) + { + appendPQExpBufferStr(&buf, + "\n LEFT JOIN pg_catalog.pg_am am ON c.relam = am.oid"); + + if (pset.sversion < 120000) + { + appendPQExpBufferStr(&buf, + ",\n LATERAL (WITH RECURSIVE d\n" + " AS (SELECT inhrelid AS oid, 1 AS level\n" + " FROM pg_catalog.pg_inherits\n" + " WHERE inhparent = c.oid\n" + " UNION ALL\n" + " SELECT inhrelid, level + 1\n" + " FROM pg_catalog.pg_inherits i\n" + " JOIN d ON i.inhparent = d.oid)\n" + " SELECT sum(pg_catalog.pg_table_size(d.oid)) AS tps\n" + " FROM d) s"); + } + else + { + /* PostgreSQL 12 has pg_partition_tree function */ + appendPQExpBufferStr(&buf, + ",\n LATERAL (SELECT sum(pg_catalog.pg_table_size(ppt.relid)) AS tps" + "\n FROM pg_catalog.pg_partition_tree(c.oid) ppt) s"); + } + } + + appendPQExpBufferStr(&buf, "\nWHERE c.relkind IN ("); + if (showTables) + { + appendPQExpBufferStr(&buf, CppAsString2(RELKIND_RELATION) ","); + appendPQExpBufferStr(&buf, CppAsString2(RELKIND_PARTITIONED_TABLE) ","); + } + if (showIndexes) + { + appendPQExpBufferStr(&buf, CppAsString2(RELKIND_INDEX) ","); + appendPQExpBufferStr(&buf, CppAsString2(RELKIND_PARTITIONED_INDEX) ","); + } + appendPQExpBufferStr(&buf, "''"); /* dummy */ + appendPQExpBufferStr(&buf, ")\n"); + + appendPQExpBufferStr(&buf, " AND NOT c.relispartition\n"); + + if (!pattern) + appendPQExpBufferStr(&buf, " AND n.nspname <> 'pg_catalog'\n" + " AND n.nspname !~ '^pg_toast'\n" + " AND n.nspname <> 'information_schema'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + "n.nspname", "c.relname", NULL, + "pg_catalog.pg_table_is_visible(c.oid)", + NULL, 3)) + { + termPQExpBuffer(&buf); + return false; + } + + appendPQExpBuffer(&buf, "ORDER BY \"Schema\", %s\"Name\";", + mixed_output ? "\"Type\" DESC, " : ""); + + res = PSQLexec(buf.data); + termPQExpBuffer(&buf); + if (!res) + return false; + + initPQExpBuffer(&title); + appendPQExpBufferStr(&title, tabletitle); + + myopt.title = title.data; + myopt.translate_header = true; + myopt.translate_columns = translate_columns; + myopt.n_translate_columns = lengthof(translate_columns); + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + + termPQExpBuffer(&title); + + PQclear(res); + return true; +} + + /* * \dP * Takes an optional regexp to select particular relations diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa6094..6abd7ba31b 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -70,6 +70,9 @@ extern bool listAllDbs(const char *pattern, bool verbose); /* \dt, \di, \ds, \dS, etc. */ extern bool listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSystem); +/* \dh */ +extern bool listRootTables(const char *reltypes, const char *pattern, bool verbose); + /* \dP */ extern bool listPartitionedTables(const char *reltypes, const char *pattern, bool verbose); diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 714b861923..3f75be4644 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -254,6 +254,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dFp[x+] [PATTERN] list text search parsers\n"); HELP0(" \\dFt[x+] [PATTERN] list text search templates\n"); HELP0(" \\dg[Sx+] [PATTERN] list roles\n"); + HELP0(" \\dh[tix+] [PATTERN] list high-level (root) relations\n"); HELP0(" \\di[Sx+] [PATTERN] list indexes\n"); HELP0(" \\dl[x+] list large objects, same as \\lo_list\n"); HELP0(" \\dL[Sx+] [PATTERN] list procedural languages\n"); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 8432be641a..2e0ff93b69 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -691,6 +691,16 @@ static const SchemaQuery Query_for_list_of_partitioned_tables = { .result = "c.relname", }; +static const SchemaQuery Query_for_list_of_root_tables = { + .catname = "pg_catalog.pg_class c", + .selcondition = "c.relispartition = false AND " + "c.relkind IN (" CppAsString2(RELKIND_RELATION) ", " + CppAsString2(RELKIND_PARTITIONED_TABLE) ")", + .viscondition = "pg_catalog.pg_table_is_visible(c.oid)", + .namespace = "c.relnamespace", + .result = "c.relname", +}; + static const SchemaQuery Query_for_list_of_tables_for_constraint = { .catname = "pg_catalog.pg_class c, pg_catalog.pg_constraint con", .selcondition = "c.oid=con.conrelid and c.relkind IN (" @@ -795,6 +805,16 @@ static const SchemaQuery Query_for_list_of_partitioned_indexes = { .result = "c.relname", }; +static const SchemaQuery Query_for_list_of_root_indexes = { + .catname = "pg_catalog.pg_class c", + .selcondition = "c.relispartition = false AND " + "c.relkind IN (" CppAsString2(RELKIND_INDEX) ", " + CppAsString2(RELKIND_PARTITIONED_INDEX) ")", + .viscondition = "pg_catalog.pg_table_is_visible(c.oid)", + .namespace = "c.relnamespace", + .result = "c.relname", +}; + /* All relations */ static const SchemaQuery Query_for_list_of_relations = { @@ -814,6 +834,19 @@ static const SchemaQuery Query_for_list_of_partitioned_relations = { .result = "c.relname", }; +/* root relations */ +static const SchemaQuery Query_for_list_of_root_relations = { + .catname = "pg_catalog.pg_class c", + .selcondition = "c.relispartition = false AND " + "c.relkind IN (" CppAsString2(RELKIND_RELATION) ", " + CppAsString2(RELKIND_PARTITIONED_TABLE) ", " + CppAsString2(RELKIND_INDEX) ", " + CppAsString2(RELKIND_PARTITIONED_INDEX) ")", + .viscondition = "pg_catalog.pg_table_is_visible(c.oid)", + .namespace = "c.relnamespace", + .result = "c.relname", +}; + static const SchemaQuery Query_for_list_of_operator_families = { .catname = "pg_catalog.pg_opfamily c", .viscondition = "pg_catalog.pg_opfamily_is_visible(c.oid)", @@ -1880,7 +1913,8 @@ psql_completion(const char *text, int start, int end) "\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp", "\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD", "\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df", - "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL", + "\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\dh", "\\dhi", "\\dht", + "\\di", "\\dl", "\\dL", "\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt", "\\drds", "\\drg", "\\dRs", "\\dRp", "\\ds", "\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dX", "\\dy", @@ -5269,8 +5303,15 @@ match_previous_words(int pattern_id, else if (TailMatchesCS("\\dFt*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ts_templates); /* must be at end of \dF alternatives: */ + else if (TailMatchesCS("\\dF*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_ts_configurations); + else if (TailMatchesCS("\\dhi*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_root_indexes); + else if (TailMatchesCS("\\dht*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_root_tables); + else if (TailMatchesCS("\\dh*")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_root_relations); else if (TailMatchesCS("\\di*")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_indexes); diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 6543e90de7..7ba2fff391 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -5017,6 +5017,33 @@ create index testpart_orange_index on testpart_orange(logdate); testpart | testpart_apple_index | regress_partitioning_role | | testpart_apple (1 row) +-- only root relations should be displayed +\dh test*apple* + List of root relations + Schema | Name | Owner | Type | Table +----------+-----------------------+---------------------------+-------------------+----------------- + testpart | testtable_apple | regress_partitioning_role | table | + testpart | testpart_apple | regress_partitioning_role | partitioned table | + testpart | testpart_apple_index | regress_partitioning_role | partitioned index | testpart_apple + testpart | testtable_apple_index | regress_partitioning_role | index | testtable_apple +(4 rows) + +\dht test*apple* + List of root tables + Schema | Name | Owner +----------+-----------------+--------------------------- + testpart | testpart_apple | regress_partitioning_role + testpart | testtable_apple | regress_partitioning_role +(2 rows) + +\dhi test*apple* + List of root indexes + Schema | Name | Owner | Table +----------+-----------------------+---------------------------+----------------- + testpart | testpart_apple_index | regress_partitioning_role | testpart_apple + testpart | testtable_apple_index | regress_partitioning_role | testtable_apple +(2 rows) + drop table testtable_apple; drop table testtable_orange; drop table testpart_apple; @@ -5038,6 +5065,7 @@ create table child_30_35 partition of child_30_40 create table child_35_40 partition of child_30_40 for values from (35) to (40); insert into parent_tab values (generate_series(30,39)); +-- only partition related object should be displayed \dPt List of partitioned tables Schema | Name | Owner @@ -5106,6 +5134,37 @@ insert into parent_tab values (generate_series(30,39)); testpart | child_30_40_id_idx | regress_partitioning_role | partitioned index | parent_index | child_30_40 (4 rows) +-- only root relations should be displayed +\dht + List of root tables + Schema | Name | Owner +----------+------------+--------------------------- + testpart | parent_tab | regress_partitioning_role +(1 row) + +\dhi + List of root indexes + Schema | Name | Owner | Table +----------+--------------+---------------------------+------------ + testpart | parent_index | regress_partitioning_role | parent_tab +(1 row) + +\dh testpart.* + List of root relations + Schema | Name | Owner | Type | Table +----------+--------------+---------------------------+-------------------+------------ + testpart | parent_tab | regress_partitioning_role | partitioned table | + testpart | parent_index | regress_partitioning_role | partitioned index | parent_tab +(2 rows) + +\dh + List of root relations + Schema | Name | Owner | Type | Table +----------+--------------+---------------------------+-------------------+------------ + testpart | parent_tab | regress_partitioning_role | partitioned table | + testpart | parent_index | regress_partitioning_role | partitioned index | parent_tab +(2 rows) + drop table parent_tab cascade; drop schema testpart; set search_path to default; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 97d1be3aac..5606f70a10 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1257,6 +1257,11 @@ create index testpart_orange_index on testpart_orange(logdate); \dPt test*apple* \dPi test*apple* +-- only root relations should be displayed +\dh test*apple* +\dht test*apple* +\dhi test*apple* + drop table testtable_apple; drop table testtable_orange; drop table testpart_apple; @@ -1280,6 +1285,7 @@ create table child_35_40 partition of child_30_40 for values from (35) to (40); insert into parent_tab values (generate_series(30,39)); +-- only partition related object should be displayed \dPt \dPi @@ -1291,6 +1297,13 @@ insert into parent_tab values (generate_series(30,39)); \dPn \dPn testpart.* +-- only root relations should be displayed +\dht +\dhi + +\dh testpart.* +\dh + drop table parent_tab cascade; drop schema testpart; -- 2.39.5 (Apple Git-154)