Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: July 16, 2025
Understanding and monitoring database storage usage is an essential aspect of maintaining healthy and performant database systems. Thus, whether we manage a small application or a large enterprise system, knowing how much space our tables and indexes consume helps in capacity planning, performance optimization, and resource allocation.
In this tutorial, we’ll explore the techniques for retrieving table and index storage sizes in MySQL, PostgreSQL, and SQL Server. Specifically, we’ll focus on obtaining two key metrics: The space consumed by table data and the space used by indexes. Moreover, we’ll examine valuable insights like index counts and index-to-data size ratios.
Our examples are based on our University database schema, which includes tables for students, courses, faculty, and related academic data.
Storage analysis in database systems is an aspect of database administration that requires careful attention and regular monitoring. It has several purposes:
When analyzing storage, we focus on data size, the space used by table records; and index size, the space consumed by indexes.
Additionally, total size provides a comprehensive view of combined space usage, while the index-to-data ratio helps us understand the proportion of space used by indexes versus data.
This information helps database administrators decide when to archive or partition data, which indexes to maintain or remove, how to optimize storage allocation, and when to plan for storage upgrades.
MySQL provides comprehensive storage information through its information_schema database. This built-in functionality allows administrators to track storage metrics efficiently without requiring additional tools or complex calculations.
We can query the information_schema.tables view to get detailed insights into how tables use space:
SELECT
table_name,
ROUND((data_length / 1024 / 1024), 2) AS data_size_mb,
ROUND((index_length / 1024 / 1024), 2) AS index_size_mb,
table_rows,
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS total_size_mb
FROM information_schema.tables
WHERE
table_schema = 'University'
AND table_type = 'BASE TABLE'
ORDER BY
total_size_mb DESC;
+---------------+--------------+---------------+------------+---------------+
| TABLE_NAME | data_size_mb | index_size_mb | TABLE_ROWS | total_size_mb |
+---------------+--------------+---------------+------------+---------------+
| Teaching | 0.06 | 0.05 | 612 | 0.11 |
| Exam | 0.02 | 0.05 | 143 | 0.06 |
| Prerequisite | 0.02 | 0.05 | 50 | 0.06 |
| Registration | 0.02 | 0.05 | 147 | 0.06 |
| Specification | 0.02 | 0.05 | 137 | 0.06 |
...
The query provides a comprehensive overview of our University database tables, showing the data size in megabytes, index size in megabytes, number of rows, and total size for each table.
In MySQL, data_length and index_length are reported in bytes. When we divide by 1024 twice (first 1024 to convert bytes to kilobytes, and then another 1024 to convert kilobytes to megabytes), we convert the raw byte measurements to a more readable megabyte format.
This conversion allows database administrators to quickly understand the storage consumption without dealing with large, unwieldy byte numbers.
Index storage analysis is important for understanding how indexes impact database size and identifying potential optimization opportunities:
SELECT
t.table_name,
i.index_name,
COUNT(i.column_name) AS indexed_columns,
ROUND(t.index_length / COUNT(DISTINCT i.index_name) / 1024, 2) AS avg_index_size_kb
FROM information_schema.tables t
JOIN information_schema.statistics i
ON t.table_name = i.table_name
AND t.table_schema = i.table_schema
WHERE
t.table_schema = 'University'
GROUP BY
t.table_name,
i.index_name,
t.index_length
ORDER BY
avg_index_size_kb DESC;
+---------------+-----------------------------------+-----------------+-------------------+
| TABLE_NAME | INDEX_NAME | indexed_columns | avg_index_size_kb |
+---------------+-----------------------------------+-----------------+-------------------+
| Specification | specification_course_id_fkey | 1 | 48.00 |
| Registration | PRIMARY | 1 | 48.00 |
| Registration | id | 1 | 48.00 |
| Registration | registration_course_id_fkey | 1 | 48.00 |
| Registration | registration_student_id_fkey | 1 | 48.00 |
...
This query shows each index’s name, the number of columns it covers, and its average size in kilobytes.
This information can help us identify oversized indexes or those needing optimization.
PostgreSQL offers a rich set of system catalogs and specialized functions designed specifically for storage analysis. These tools provide high-level overviews and detailed insights into how storage is utilized.
PostgreSQL provides specialized system functions for storage analysis.
When we query system catalogs, we use functions like pg_relation_size() to retrieve a table’s physical storage size in bytes and pg_size_pretty() to convert these raw sizes into human-readable formats:
SELECT
relname AS table_name,
pg_size_pretty(pg_relation_size(c.oid)) AS table_size,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size,
reltuples::bigint AS row_estimate
FROM pg_class c
JOIN pg_namespace n
ON n.oid = c.relnamespace
WHERE
nspname = 'public'
AND relkind = 'r'
ORDER BY
pg_total_relation_size(c.oid) DESC;
table_name | table_size | total_size | row_estimate
---------------+------------+------------+--------------
teaching | 40 kB | 104 kB | 612
registration | 16 kB | 56 kB | 147
course | 16 kB | 56 kB | 81
exam | 16 kB | 56 kB | 143
prerequisite | 8192 bytes | 24 kB | -1
...
The query filters results using specific parameters:
Further, we use object identifiers (oid) to reference database objects uniquely.
When analyzing index storage, we leverage PostgreSQL’s system catalogs and statistical views:
SELECT i.schemaname,
i.tablename,
i.indexname,
pg_size_pretty(pg_relation_size(ui.indexrelid)) AS index_size,
i.indexdef
FROM pg_indexes i
JOIN pg_stat_user_indexes ui
ON i.indexname = ui.indexrelname
WHERE i.schemaname = 'public'
ORDER BY pg_relation_size(ui.indexrelid) DESC;
schemaname | tablename | indexname | index_size | indexdef
------------+---------------+--------------------+------------+-----------------------------------------------------
public | teaching | teaching_pkey | 40 kB | CREATE UNIQUE INDEX teaching_pkey ON public.teaching USING btree (id)
public | program | program_pkey | 16 kB | CREATE UNIQUE INDEX program_pkey ON public.program USING btree (id)
public | student | student_pkey | 16 kB | CREATE UNIQUE INDEX student_pkey ON public.student USING btree (id)
public | faculty | faculty_pkey | 16 kB | CREATE UNIQUE INDEX faculty_pkey ON public.faculty USING btree (id)
public | course | course_pkey | 16 kB | CREATE UNIQUE INDEX course_pkey ON public.course USING btree (id)
...
Here:
SQL Server provides comprehensive storage analysis capabilities through system views and dynamic management functions.
SQL Server’s storage architecture is based on pages and extents, which we can analyze using system views to gather detailed storage information.
Let’s write a query to reveal the total and used space for each table, along with row counts:
SELECT
t.name AS table_name,
SUM(a.total_pages) * 8 AS total_space_kb,
SUM(a.used_pages) * 8 AS used_space_kb,
p.rows AS row_count
FROM sys.tables t
INNER JOIN sys.indexes i
ON t.object_id = i.object_id
INNER JOIN sys.partitions p
ON i.object_id = p.object_id
AND i.index_id = p.index_id
INNER JOIN sys.allocation_units a
ON p.partition_id = a.container_id
GROUP BY
t.name,
p.rows
ORDER BY
total_space_kb DESC;
table_name total_space_kb used_space_kb row_count
-----------------------------------------------------------
Department 144 32 5
Program 144 32 24
Student 144 32 25
Prerequisite 144 32 50
Faculty 144 32 72
...
-----------------------------------------------------------
We can see the total allocated vs. used space, which helps us identify potential areas for reclaiming storage.
Here:
Let’s find how much space an index is using, along with its type and the table it belongs to:
SELECT
OBJECT_NAME(i.object_id) AS table_name,
i.name AS index_name,
i.type_desc AS index_type,
CAST(8 * SUM(a.used_pages) AS DECIMAL(10,2)) AS index_size_kb
FROM sys.indexes i
INNER JOIN sys.partitions p
ON i.object_id = p.object_id
AND i.index_id = p.index_id
INNER JOIN sys.allocation_units a
ON p.partition_id = a.container_id
WHERE
i.object_id > 100
GROUP BY
i.object_id,
i.name,
i.type_desc
ORDER BY
index_size_kb DESC;
table_name index_name index_type index_size_kb
------------------------------ -------------------------------------- ---------------- --------------
plan_persist_plan plan_persist_plan_cidx CLUSTERED 104.00
Teaching PK__Teaching__3213E83F7359FDCC CLUSTERED 72.00
plan_persist_query_text plan_persist_query_text_cidx CLUSTERED 56.00
plan_persist_query_text plan_persist_query_text_idx1 NONCLUSTERED 16.00
plan_persist_query plan_persist_query_cidx CLUSTERED 16.00
...
This way, we can identify oversized indexes that might need optimization.
Let’s summarize how different database systems handle storage analysis.
Each database system has its way of measuring and reporting storage usage.
In MySQL, we work with raw bytes. They give us precise measurements but often need conversion for readability.
In contrast, PostgreSQL makes life easier with built-in functions like pg_size_pretty(), which automatically format sizes in human-readable units.
On the other hand, SQL Server takes a unique approach with its 8 KB pages as a basic unit, which affects how we calculate and interpret storage measurements.
Each system organizes its metadata differently.
MySQL’s information_schema provides a straightforward view that makes storage queries relatively simple.
In PostgreSQL, the pg_catalog schema offers more detailed information about object relationships and storage patterns.
SQL Server’s sys schema and a dynamic management view give comprehensive storage insights.
The way each system handles index storage information reflects its underlying architecture.
In MySQL, table and index statistics are combined in information_schema, making it easy to get overall storage pictures.
PostgreSQL keeps relation and index sizes separate, giving us more granular control over our analysis.
Finally, SQL Server’s allocation units let us know how space is used at the physical level. SQL Server also uses a clustered index structure where the table’s primary key data and index are stored together in the same B-tree structure. For non-clustered indexes, the data is stored separately from the table’s physical storage, with the non-clustered indexes containing a pointer to the actual data rows.
In this article, we explored techniques for analyzing table and index storage sizes across MySQL, PostgreSQL, and SQL Server. We learned to use system catalogs and built-in functions to retrieve detailed storage metrics.
By understanding how each database system handles storage measurements and regularly monitoring these metrics, we can maintain efficient database performance while ensuring optimal resource allocation.