From: Quan Zongliang <quanzongliang@yeah.net>
To: Christoph Berg <myon@debian.org>
To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Subject: Re: Available disk space per tablespace
Date: Fri, 14 Mar 2025 10:14:35 +0800
Message-ID: <2df8509c-e28a-467a-8f03-f3cfe812ed62@yeah.net> (raw)
In-Reply-To: <Z9MfiPfZULuzsedO@msg.df7cb.de>
References: <Z9MfiPfZULuzsedO@msg.df7cb.de>
On 2025/3/14 02:10, Christoph Berg wrote:
> Hi,
>
> I'm picking up a 5 year old patch again:
> https://www.postgresql.org/message-id/flat/20191108132419.GG8017%40msg.df7cb.de
>
> Users will be interested in knowing how much extra data they can load
> into a database, but PG currently does not expose that number. This
> patch introduces a new function pg_tablespace_avail() that takes a
> tablespace name or oid, and returns the number of bytes "available"
> there. This is the number without any reserved blocks (Unix, f_avail)
> or available to the current user (Windows).
>
> (This is not meant to replace a full-fledged OS monitoring system that
> has much more numbers about disks and everything, it is filling a UX
> gap.)
>
> Compared to the last patch, this just returns a single number so it's
> easier to use - total space isn't all that interesting, we just return
> the number the user wants.
>
> The free space is included in \db+ output:
>
> postgres =# \db+
> List of tablespaces
> Name │ Owner │ Location │ Access privileges │ Options │ Size │ Free │ Description
> ────────────┼───────┼──────────┼───────────────────┼─────────┼─────────┼────────┼─────────────
> pg_default │ myon │ │ ∅ │ ∅ │ 23 MB │ 538 GB │ ∅
> pg_global │ myon │ │ ∅ │ ∅ │ 556 kB │ 538 GB │ ∅
> spc │ myon │ /tmp/spc │ ∅ │ ∅ │ 0 bytes │ 31 GB │ ∅
> (3 rows)
>
> The patch has also been tested on Windows.
>
> TODO: Figure out which systems need statfs() vs statvfs()
>
I tested the patch under macos. Abnormal work:
List of tablespaces
Name | Owner | Location | Access privileges | Options | Size
| Free | Description
------------+--------+----------+-------------------+---------+--------+-------+-------------
pg_default | quanzl | | | | 23 MB
|23 TB |
pg_global | quanzl | | | | 556 kB
| 23 TB |
(2 rows)
Actually my disk is 1TB.
According to the statvfs documentation for macOS
f_frsize The size in bytes of the minimum unit of allocation on
this file system.
f_bsize The preferred length of I/O requests for files on this
file system.
I tweaked the code a little bit. See the attachment.
List of tablespaces
Name | Owner | Location | Access privileges | Options | Size
| Free | Description
------------+--------+----------+-------------------+---------+--------+--------+-------------
pg_default | quanzl | | | | 22 MB
| 116 GB |
pg_global | quanzl | | | | 556 kB
| 116 GB |
(2 rows)
In addition, many systems use 1000 as 1k to represent the storage size.
Shouldn't we consider this factor as well?
> Christoph
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgmlindex 1c3810e1a04..c0758b9244f 100644--- a/doc/src/sgml/func.sgml+++ b/doc/src/sgml/func.sgml@@ -30089,6 +30089,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>+ <entry role="func_table_entry"><para role="func_signature">+ <indexterm>+ <primary>pg_tablespace_avail</primary>+ </indexterm>+ <function>pg_tablespace_avail</function> ( <type>name</type> )+ <returnvalue>bigint</returnvalue>+ </para>+ <para role="func_signature">+ <function>pg_tablespace_avail</function> ( <type>oid</type> )+ <returnvalue>bigint</returnvalue>+ </para>+ <para>+ Returns the available disk space in the tablespace with the+ specified name or OID. To use this function, you must+ have <literal>CREATE</literal> privilege on the specified tablespace+ or have privileges of the <literal>pg_read_all_stats</literal> role,+ unless it is the default tablespace for the current database.+ </para></entry>+ </row>+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgmlindex cedccc14129..9e1bec0b422 100644--- a/doc/src/sgml/ref/psql-ref.sgml+++ b/doc/src/sgml/ref/psql-ref.sgml@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.cindex 25865b660ef..a2637953ce0 100644--- a/src/backend/utils/adt/dbsize.c+++ b/src/backend/utils/adt/dbsize.c@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32+#include <fileapi.h>+#else+#include <sys/statvfs.h>+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,99 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*+ * Return available disk space of tablespace. Returns -1 if the tablespace+ * directory cannot be found.+ */+static int64+calculate_tablespace_avail(Oid tblspcOid)+{+ char tblspcPath[MAXPGPATH];+ AclResult aclresult;+#ifdef WIN32+ ULARGE_INTEGER lpFreeBytesAvailable;+#else+ struct statvfs fst;+#endif++ /*+ * User must have privileges of pg_read_all_stats or have CREATE privilege+ * for target tablespace, either explicitly granted or implicitly because+ * it is default for current database.+ */+ if (tblspcOid != MyDatabaseTableSpace &&+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))+ {+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);+ if (aclresult != ACLCHECK_OK)+ aclcheck_error(aclresult, OBJECT_TABLESPACE,+ get_tablespace_name(tblspcOid));+ }++ if (tblspcOid == DEFAULTTABLESPACE_OID)+ snprintf(tblspcPath, MAXPGPATH, "base");+ else if (tblspcOid == GLOBALTABLESPACE_OID)+ snprintf(tblspcPath, MAXPGPATH, "global");+ else+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,+ TABLESPACE_VERSION_DIRECTORY);++#ifdef WIN32+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)+ return -1;++ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */+#else+ if (statvfs(tblspcPath, &fst) < 0)+ return -1;++#if defined(__darwin__)+ return fst.f_bavail * fst.f_frsize; /* available blocks times block size */+#else+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */+#endif /* __darwin__ */+#endif+}++Datum+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)+{+ Oid tblspcOid = PG_GETARG_OID(0);+ int64 avail;++ /*+ * Not needed for correctness, but avoid non-user-facing error message+ * later if the tablespace doesn't exist.+ */+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))+ ereport(ERROR,+ errcode(ERRCODE_UNDEFINED_OBJECT),+ errmsg("tablespace with OID %u does not exist", tblspcOid));++ avail = calculate_tablespace_avail(tblspcOid);++ if (avail < 0)+ PG_RETURN_NULL();++ PG_RETURN_INT64(avail);+}++Datum+pg_tablespace_avail_name(PG_FUNCTION_ARGS)+{+ Name tblspcName = PG_GETARG_NAME(0);+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);+ int64 avail;++ avail = calculate_tablespace_avail(tblspcOid);++ if (avail < 0)+ PG_RETURN_NULL();++ PG_RETURN_INT64(avail);+}++
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.cindex e6cf468ac9e..8c52a126ac1 100644--- a/src/bin/psql/describe.c+++ b/src/bin/psql/describe.c@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),+ gettext_noop("Size"));+ if (pset.sversion >= 180000)+ appendPQExpBuffer(&buf,+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",+ gettext_noop("Free"));+ appendPQExpBuffer(&buf,+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.datindex 890822eaf79..39c3b8c2552 100644--- a/src/include/catalog/pg_proc.dat+++ b/src/include/catalog/pg_proc.dat@@ -7683,6 +7683,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',+ descr => 'disk stats for the specified tablespace',+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },+{ oid => '6016',+ descr => 'disk stats for the specified tablespace',+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
Attachments:
[text/plain] 0002-Add-pg_tablespace_avail-functions.patch (6.9K, ../2df8509c-e28a-467a-8f03-f3cfe812ed62@yeah.net/2-0002-Add-pg_tablespace_avail-functions.patch)
download | inline diff:diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgmlindex 1c3810e1a04..c0758b9244f 100644--- a/doc/src/sgml/func.sgml+++ b/doc/src/sgml/func.sgml@@ -30089,6 +30089,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>+ <entry role="func_table_entry"><para role="func_signature">+ <indexterm>+ <primary>pg_tablespace_avail</primary>+ </indexterm>+ <function>pg_tablespace_avail</function> ( <type>name</type> )+ <returnvalue>bigint</returnvalue>+ </para>+ <para role="func_signature">+ <function>pg_tablespace_avail</function> ( <type>oid</type> )+ <returnvalue>bigint</returnvalue>+ </para>+ <para>+ Returns the available disk space in the tablespace with the+ specified name or OID. To use this function, you must+ have <literal>CREATE</literal> privilege on the specified tablespace+ or have privileges of the <literal>pg_read_all_stats</literal> role,+ unless it is the default tablespace for the current database.+ </para></entry>+ </row>+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgmlindex cedccc14129..9e1bec0b422 100644--- a/doc/src/sgml/ref/psql-ref.sgml+++ b/doc/src/sgml/ref/psql-ref.sgml@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.cindex 25865b660ef..a2637953ce0 100644--- a/src/backend/utils/adt/dbsize.c+++ b/src/backend/utils/adt/dbsize.c@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32+#include <fileapi.h>+#else+#include <sys/statvfs.h>+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,99 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*+ * Return available disk space of tablespace. Returns -1 if the tablespace+ * directory cannot be found.+ */+static int64+calculate_tablespace_avail(Oid tblspcOid)+{+ char tblspcPath[MAXPGPATH];+ AclResult aclresult;+#ifdef WIN32+ ULARGE_INTEGER lpFreeBytesAvailable;+#else+ struct statvfs fst;+#endif++ /*+ * User must have privileges of pg_read_all_stats or have CREATE privilege+ * for target tablespace, either explicitly granted or implicitly because+ * it is default for current database.+ */+ if (tblspcOid != MyDatabaseTableSpace &&+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))+ {+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);+ if (aclresult != ACLCHECK_OK)+ aclcheck_error(aclresult, OBJECT_TABLESPACE,+ get_tablespace_name(tblspcOid));+ }++ if (tblspcOid == DEFAULTTABLESPACE_OID)+ snprintf(tblspcPath, MAXPGPATH, "base");+ else if (tblspcOid == GLOBALTABLESPACE_OID)+ snprintf(tblspcPath, MAXPGPATH, "global");+ else+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,+ TABLESPACE_VERSION_DIRECTORY);++#ifdef WIN32+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)+ return -1;++ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */+#else+ if (statvfs(tblspcPath, &fst) < 0)+ return -1;++#if defined(__darwin__)+ return fst.f_bavail * fst.f_frsize; /* available blocks times block size */+#else+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */+#endif /* __darwin__ */+#endif+}++Datum+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)+{+ Oid tblspcOid = PG_GETARG_OID(0);+ int64 avail;++ /*+ * Not needed for correctness, but avoid non-user-facing error message+ * later if the tablespace doesn't exist.+ */+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))+ ereport(ERROR,+ errcode(ERRCODE_UNDEFINED_OBJECT),+ errmsg("tablespace with OID %u does not exist", tblspcOid));++ avail = calculate_tablespace_avail(tblspcOid);++ if (avail < 0)+ PG_RETURN_NULL();++ PG_RETURN_INT64(avail);+}++Datum+pg_tablespace_avail_name(PG_FUNCTION_ARGS)+{+ Name tblspcName = PG_GETARG_NAME(0);+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);+ int64 avail;++ avail = calculate_tablespace_avail(tblspcOid);++ if (avail < 0)+ PG_RETURN_NULL();++ PG_RETURN_INT64(avail);+}++
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.cindex e6cf468ac9e..8c52a126ac1 100644--- a/src/bin/psql/describe.c+++ b/src/bin/psql/describe.c@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),+ gettext_noop("Size"));+ if (pset.sversion >= 180000)+ appendPQExpBuffer(&buf,+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",+ gettext_noop("Free"));+ appendPQExpBuffer(&buf,+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.datindex 890822eaf79..39c3b8c2552 100644--- a/src/include/catalog/pg_proc.dat+++ b/src/include/catalog/pg_proc.dat@@ -7683,6 +7683,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',+ descr => 'disk stats for the specified tablespace',+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },+{ oid => '6016',+ descr => 'disk stats for the specified tablespace',+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: quanzongliang@yeah.net, myon@debian.org, pgsql-hackers@lists.postgresql.org
Subject: Re: Available disk space per tablespace
In-Reply-To: <2df8509c-e28a-467a-8f03-f3cfe812ed62@yeah.net>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox