This is the query to retrieve info about:

  • Total Size

  • Total Size of all Indexes

  • Table Size

  • Index Size

  • Estimated table row count

     SELECT i.relname "Table Name",indexrelname "Index Name",
     pg_size_pretty(pg_total_relation_size(relid)) As "Total Size",
     pg_size_pretty(pg_indexes_size(relid)) as "Total Size of all Indexes",
     pg_size_pretty(pg_relation_size(relid)) as "Table Size",
     pg_size_pretty(pg_relation_size(indexrelid)) "Index Size",
     reltuples::bigint "Estimated table row count"
     FROM pg_stat_all_indexes i JOIN pg_class c ON i.relid=c.oid 
     WHERE i.relname='uploads'
    

Maybe for someone it will be useful.

Answer from Elvin Ahmadov on Stack Overflow
🌐
GeeksforGeeks
geeksforgeeks.org › postgresql › postgresql-size-of-indexes
PostgreSQL - Size of Indexes - GeeksforGeeks
July 15, 2025 - The pg_indexes_size() function in PostgreSQL calculates the total size of all indexes associated with a specific table, measured in bytes.
Discussions

postgresql - Postgres index size calculation - Database Administrators Stack Exchange
Is there any way to determine a rough estimate of the size (on disk) of an index before creating it? The size of the table and each column are known. I am particularly interested in GIN indexes. Al... More on dba.stackexchange.com
🌐 dba.stackexchange.com
April 24, 2017
postgresql - Estimating size of Postgres indexes - Stack Overflow
I'm trying to get a better understanding of the tradeoffs involved in creating Postgres indexes. As part of that, I'd love to understand how much space indexes usually use. I've read through the do... More on stackoverflow.com
🌐 stackoverflow.com
postgresql - What accounts for a large single index size (other than bloat?) - Database Administrators Stack Exchange
I've been doing some digging into the size of our production PostgreSQL 9.6 database and found some results I thought were surprising. We have a table (let's call it foos) with about 10 million records. The primary key is an integer. We have a B-tree index on this table for an optional foreign ... More on dba.stackexchange.com
🌐 dba.stackexchange.com
What will break with large page sizes?
Just don't. Random reads can be atrocious for a large DB. Let me explain a bit. First thing is that you're changing the smallest unit of IO. It's kinda like asking what would happen to the universe if the Planck length was different. Maybe... everything? Maybe not? But when you change the smallest unit, there's going to be a lot of ripple effects. So say you have a very large table, where neither the heap (where the rows live) and the index can't fit in memory. In comes a query like SELECT * FROM mytable WHERE id = $some_old_id. None of the relevant pages for the heap or index are warmed up and in memory, so you have to fetch them all. Every time you get that cache miss while traversing the index, you need to pull a page into memory. If your memory usage is tapped out, that means you'll evict the least recently used page for each one you load. And if those are dirty (contain flushed writes), then they have to be written back to disk. If you have standard 8KB page size and need to read 25 pages, and you have to replace dirty pages in memory, that's 200KB that needs to be read and another 200 KB to be flushed. Maybe that's not so bad. But if you have large page sizes like 16MB and maybe need to fetch fewer index pages, say 5 (honestly this is hard to guess but would depend on index size), well now you have to read 80MB into cache and flush 160MB to disk! That's a massive difference in IO! Now what happens if you do that 100 times a second? In my experience the biggest bottleneck by far in Postgres is inefficient access patterns that lead to exacerbated IO workloads, and excessive writes to disk. This is how a ton of random reads can cause terrible performance. Maybe you're lucky and maybe large page sizes are great for your use case. It really depends on your workload, at the end of the day. But this is one example where a typical workload could suffer. Unless you're intimately familiar with the ripple effects and how large page sizes will serve you better, I wouldn't mess around with them. More on reddit.com
🌐 r/PostgreSQL
17
14
May 2, 2025
🌐
PostgreSQL Wiki
wiki.postgresql.org › wiki › Index_Maintenance
Index Maintenance - PostgreSQL wiki
See also: "PostgreSQL index bloat under a microscope" blogpost · Note: You are expected to change "pgbench_accounts_pkey" to the name of the index that is to be summarized. WITH RECURSIVE index_details AS ( SELECT 'pgbench_accounts_pkey'::text idx ), size_in_pages_index AS ( SELECT (pg_relation_size(idx::regclass) / (2^13))::int4 size_pages FROM index_details ), page_stats AS ( SELECT index_details.*, stats.* FROM index_details, size_in_pages_index, lateral (SELECT i FROM generate_series(1, size_pages - 1) i) series, lateral (SELECT * FROM bt_page_stats(idx, i)) stats ), meta_stats AS ( SELEC
🌐
Packtpub
subscription.packtpub.com › book › data › 9781838648138 › 2 › ch02lvl1sec19 › checking-table-and-index-sizes-in-postgresql
Checking table and index sizes in PostgreSQL
percona=# \dt+ public.*bench* List of relations Schema | Name | Type | Owner | Size | Description --------+------------------+-------+----------+---------+------------- public | pgbench_accounts | table | postgres | 1281 MB | public | ...
Top answer
1 of 2
14

Since you did not specify the index type, I'll assume default B-tree indexes. Other types can be a lot different.

Here is a simplistic function to compute the estimated minimum size in bytes for an index on the given table with the given columns:

CREATE OR REPLACE FUNCTION f_index_minimum_size(_tbl regclass, _cols VARIADIC text[], OUT estimated_minimum_size bigint)
  LANGUAGE plpgsql AS
$func$
DECLARE
   _missing_column text;
BEGIN

-- assert
SELECT i.attname
FROM   unnest(_cols) AS i(attname)
LEFT   JOIN pg_catalog.pg_attribute a ON a.attname = i.attname
                                     AND a.attrelid = _tbl
WHERE  a.attname IS NULL
INTO   _missing_column;

IF FOUND THEN
   RAISE EXCEPTION 'Table % has no column named %', _tbl, quote_ident(_missing_column);
END IF;


SELECT INTO estimated_minimum_size
       COALESCE(1 + ceil(reltuples/trunc((blocksize-page_overhead)/(4+tuple_size)))::int, 0) * blocksize -- AS estimated_minimum_size
FROM  (
   SELECT maxalign, blocksize, reltuples, fillfactor, page_overhead
        , (maxalign  -- up to 16 columns, else nullbitmap may force another maxalign step
         + CASE WHEN datawidth <= maxalign  THEN maxalign
                WHEN datawidth%maxalign = 0 THEN datawidth
                ELSE                            (datawidth + maxalign) - datawidth%maxalign END  -- add padding to the data to align on MAXALIGN
          ) AS tuple_size
   FROM  (
      SELECT c.reltuples, count(*)
           , 90 AS fillfactor
           , current_setting('block_size')::bigint AS blocksize
           , CASE WHEN version() ~ '64-bit|x86_64|ppc64|ia64|amd64|mingw32'  -- MAXALIGN: 4 on 32bits, 8 on 64bits
                  THEN 8 ELSE 4 END AS maxalign
           , 40 AS page_overhead  -- 24 bytes page header + 16 bytes "special space"
           -- avg data width without null values
           , sum(ceil((1-COALESCE(s.null_frac, 0)) * COALESCE(s.avg_width, 1024))::int) AS datawidth  -- ceil() because avg width has a low bias
      FROM   pg_catalog.pg_class     c
      JOIN   pg_catalog.pg_attribute a ON a.attrelid = c.oid
      JOIN   pg_catalog.pg_stats     s ON s.schemaname = c.relnamespace::regnamespace::text
                                      AND s.tablename  = c.relname
                                      AND s.attname    = a.attname
      WHERE  c.oid = _tbl
      AND    a.attname = ANY(_cols) --  all exist, verified above
      GROUP  BY 1
      ) sub1
   ) sub2;
END
$func$;

Call examples:

SELECT f_index_minimum_size('my_table', 'col1', 'col2', 'col3');

SELECT f_index_minimum_size('public.my_table', VARIADIC '{col1, col2, col3}');

db<>fiddle here

About VARIADIC parameters:

  • Return rows matching elements of input array in plpgsql function

Basically, all indexes use data pages of typically 8 kb block size (rarely 4 kb). There is one data page overhead for B-tree indexes to start with. Each additional data page has a fixed overhead of 40 bytes (currently). Each page stores tuples like depicted in the manual here. Each tuple has a tuple header (typically 8 bytes incl. alignment padding), possibly a null bitmap, data (possibly incl. alignment padding between columns for multicolumn indices), and possibly alignment padding to the next multiple of MAXALIGN (typically 8 bytes). Plus, there is an ItemId of 4 bytes per tuple. Some space may be reserved initially for later additions with a fillfactor - 90 % by default for B-tree indexes.

Important notes & disclaimers

The reported size is the estimated minimum size. An actual index will typically be bigger by around 25 % due to natural bloat from page splits. Plus, the calculation does not take possible alignment padding between multiple columns into account. Can add another couple percent (or more in extreme cases). See:

  • Calculating and saving space in PostgreSQL

Estimations are based on column statistics in the view pg_stats which is based on the system table pg_statistics. (Using the latter directly would be faster, but only allowed for superusers.) In particular, the calculation is based on null_frac, the "fraction of column entries that are null" and avg_width, the "average width in bytes of column's entries" to compute an average data width - ignoring possible additional alignment padding for multicolumn indexes.

The default 90 % fillfactor is taken into account. (One might specify a different one.)

Up to 50 % bloat is typically natural for B-tree indexes and nothing to worry about.

Does not work for expression indexes.

No provision for partial indexes.

Function raises an exception if anything but existing plain column names is passed. Case-sensitive!

If the table is new (or in any case if statistics may be out of date), be sure to run ANALYZE on the table before calling the function to update (or even initiate!) statistics.

Due to major optimizations, B-tree indexes in Postgres 12 waste less space and are typically closer to the reported minimum size.

Does not account for deduplication that's introduced with Postgres 13, which can compact indexes with duplicate values.

Parts of the code are taken from ioguix' bloat estimation queries here:

  • https://github.com/ioguix/pgsql-bloat-estimation

More gory details i the Postgres source code here:

  • https://doxygen.postgresql.org/bufpage_8h_source.html
2 of 2
10

You can calculate it yourself. Each index entry has an overhead of 8 bytes. Add the average size of your indexed data (in the internal binary format).

There is some more overhead, like page header and footer and internal index pages, but that doesn't account for much, unless your index rows are very wide.

🌐
Dataedo
dataedo.com › kb › query › postgresql › list-of-tables-by-the-size-of-data-and-indexes
List tables by the size of data and indexes in PostgreSQL database - PostgreSQL Data Dictionary Queries
November 5, 2018 - Article for: PostgreSQL ▾ MySQL · Query below returns tables in a database with space they use and space used by indexes ordered from the ones using most. select schemaname as table_schema, relname as table_name, pg_size_pretty(pg_total_relation_size(relid)) as total_size, pg_size_pretty(pg_relation_size(relid)) as data_size, pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) as external_size from pg_catalog.pg_statio_user_tables order by pg_total_relation_size(relid) desc, pg_relation_size(relid) desc; table_schema - table's schema name ·
🌐
Aiven
aiven.io › aiven for postgresql® › scaling and performance › db, table, index size
Check the size of a database, a table or an index | Aiven docs
May 19, 2026 - PostgreSQL® offers different commands and functions to get disk space usage for a database, a table, or an index. ... The pg_database_size function.
Find elsewhere
🌐
PostgreSQL Tutorial
postgresqltutorial.com › postgresql-administration › postgresql-database-indexes-table-size
How to Get Sizes of Database Objects in PostgreSQL
datname | size -----------+--------- postgres | 8452 kB template1 | 7892 kB template0 | 7681 kB dvdrental | 15 MB (4 rows) To get the total size of all indexes attached to a table, you use the pg_indexes_size() function.
🌐
Hakibenita
hakibenita.com › postgresql-unused-index-size
The Unexpected Find That Freed 20GB of Unused Index Space | Haki Benita
February 1, 2021 - If you are migrating from PostgreSQL versions prior to 13, you need to rebuild the indexes using the REINDEX command in order to get the full benefits of index de-deduplication. To illustrate the effect of B-Tree deduplication on the size of the index, create a table with a unique column and a non unique column, and populate it with 1M rows.
🌐
LinkedIn
linkedin.com › posts › milan-jovanovic_did-you-know-postgres-has-a-maximum-index-activity-7040245140690485249-KRG3
Milan Jovanović on LinkedIn: Did you know Postgres has a maximum index row size? 🐘 Let me tell you… | 14 comments
March 11, 2023 - 🐘 Let me tell you how I found this out the hard way. The index row size is roughly one-third of the 8kB page size. It's exactly 2713 bytes. The indexing process will fail when the index row size is more than 2713 bytes.
🌐
Malinga
malinga.me › postgresql-how-to-get-the-total-index-size-used-by-each-table-in-a-database
PostgreSQL – How to get the total index size used by each table in a database | Romesh Malinga Perera
2020 · indices postgresql · database ... to a table, you use the function. The pg_indexes_size() function accepts the OID or table name as the argument and returns the total disk space used by all indexes attached to that table....
🌐
.gitignore generator
mrkandreev.name › blog › postgres-index
Postgres Indexes, Deep dive
When table is large index can be large too. # SELECT pg_size_pretty(pg_relation_size('index_name')); pg_size_pretty ---------------- 118 MB (1 row) # SELECT pg_size_pretty(pg_relation_size('table_name')); pg_size_pretty ---------------- 274 MB (1 row)
🌐
Namehero
namehero.com › blog › table-size-in-postgresql-everything-you-need-to-know
Table Size in PostgreSQL: Everything You Need To Know
November 6, 2024 - In order to get the tablespace size of the dataset in question, Posgres has a function for this as well, i.e. pg_tablespace_size() and it’s utilized in the exact same manner like so: Posgres also has a built-in function for querying index sizes separately from pg_total_relation_size() and this is with pg_indexes_size().
Top answer
1 of 1
5

There's almost no deletion taking place on the foos table, so the bloat is non-existent.

You don't need deletions to get bloat. updates will do so as well. A freshly made index on 10,000,000 ints gives me a size of 214MB, so you do have bloat (or are using weird hardware, perhaps). You can't detect bloat just by thinking about to. Rebuild the index and see if it gets smaller, or use pgstattuple. Of course having a tightly packed index is not very efficient, as then nearly every operation needs to do page splits.

Your mental model of an index does not match how PostgreSQL does things.

An index does not point to a primary key value, it points directly into the table. This is called a "tid", and generally takes 6 bytes.

The data value itself is stored to 8 byte alignment, so even if your data is an int4, it still takes 8 bytes of disk space. Due to this alignment, You could build an index on two int4 columns, and it will still take the same size as one int4 column.

PostgreSQL is always prepared to handle NULL values on each row, as well as variable length data, even if the definition of the table/index precludes such things. So each index tuple has a header which tells us whether that row has any NULL or variable length data, as well as the overall length of that tuple. This takes up some space.

PostgreSQL uses page-organized storage, not just a huge malloc chunk. Each page contains some more overhead, both per-block and per-entry within the block (see typedef struct ItemIdData).

So you get 8 bytes for the data itself (bar_id, aligned), 8 bytes of the tuple header which includes the tid pointer, and 4 bytes for the line pointer within each block, that comes up to 20 bytes. With some overhead of non-leaf pages plus per-block overhead and some space within blocks which is not usable due to alignment or because they are at the ragged end of the data, I get an actual value of 22.4 bytes. The other 4.5 fold would be bloat (although some of the "bloat" may be useful free space, unless your index is going to be absolutely static from now on)

🌐
PostgreSQL Tutorial
postgresqltutorial.com › home › postgresql administration › how to get table, database, indexes, tablespace, and value size in postgresql
How to Get Table, Database, Indexes, Tablespace, and Value Size in PostgreSQL
August 1, 2020 - datname | size ----------------+--------- postgres | 7055 kB template1 | 7055 kB template0 | 6945 kB dvdrental | 15 MB · To get total size of all indexes attached to a table, you use the pg_indexes_size() function.
🌐
Sling Academy
slingacademy.com › article › size-of-the-table-index-in-postgresql
Checking Index Size in PostgreSQL - Sling Academy
In PostgreSQL, an index is a database ... table. CREATE INDEX idx_name ON table_name (column_name); To determine the size of an index, you can use the pg_relation_size() function:...
🌐
GitHub
gist.github.com › hatarist › 675dc3debf6cf5f825b5c15aed4cbac0
PostgreSQL: get table size (data & index & toast) · GitHub
PostgreSQL: get table size (data & index & toast) Raw · pg_get_table_sizes.sql · This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
🌐
Postgres AI
postgres.ai › docs › how-to-guides › joe-bot › get-database-table-index-size
Postgres
🚀 New Postgres.AI assistant: ... 1.5 for Postgres topics · On this page · How to get a query execution plan (EXPLAIN) How to create an index using Joe bot · How to reset the state of a Joe session / clone · How to get a list of active queries in a Joe session and stop long-running queries · How to visualize a query plan · How to work with SQL optimization history · How to get row counts for arbitrary SELECTs · How to get sizes of PostgreSQL ...