Last modified: March 11, 2025

This article is written in: 🇺🇸

Understanding the Storage of Tables and Indexes on Disk

Exploring how databases store tables and indexes on disk can provide valuable insights into optimizing performance and managing data efficiently. Let's delve into the fundamental concepts of disk storage in relational databases, focusing on the structures and mechanisms that underlie data organization.

Storage Structures

Databases organize data on disk using structured approaches to ensure efficient access and manipulation.

Pages and Extents

At the core of data storage are pages, sometimes called blocks. These are fixed-size chunks of data, commonly 4KB, 8KB, or 16KB in size, depending on the database system. Pages serve as the basic units for reading from and writing to the disk.

To manage storage more effectively, pages are grouped into extents. An extent is a collection of contiguous pages, which helps reduce fragmentation and improves read/write performance by allowing larger chunks of data to be processed in a single operation.

Here's a simple illustration of pages grouped into extents:

+-------------------+
|      Extent       |
| +-----+  +-----+  |
| |Page1|  |Page2|  |
| +-----+  +-----+  |
| +-----+  +-----+  |
| |Page3|  |Page4|  |
| +-----+  +-----+  |
+-------------------+

In this diagram, we see an extent containing four pages, each holding part of the table's data.

Table and Heap Organization

+----------------------------------------------------+
|                       Heap File                    |
+----------------------------------------------------+
|  Page 1         |  Page 2         |  Page 3        |
+------------------+-----------------+---------------+
| [Row1] [Row2]   | [Row3] [Row4]   | [Row5] [Row6]  |
| [Free Space]    | [Row7]          | [Free Space]   |
+------------------+-----------------+---------------+

Scenario: Fragmentation caused by deletions

+----------------------------------------------------+
|                       Heap File                    |
+----------------------------------------------------+
|  Page 1         |  Page 2         |  Page 3        |
+------------------+-----------------+---------------+
| [Row1] [Deleted]| [Row4] [Deleted]| [Row6] [Row7]  |
| [Free Space]    | [Free Space]    | [Free Space]   |
+------------------+-----------------+---------------+

Deletions leave gaps in pages, creating free space that can be reused.

Scenario: Large row spanning multiple pages

+----------------------------------------------------+
|                       Heap File                    |
+----------------------------------------------------+
|  Page 1         |  Page 2         |  Page 3        |
+------------------+-----------------+---------------+
| [Row1] [Row2]   | [Part of Row8]  | [Part of Row8] |
| [Free Space]    | [Free Space]    | [Free Space]   |
+------------------+-----------------+---------------+

A large row (Row8) is split across multiple pages, linked by pointers.

Scenario: Post-maintenance compacted heap

+----------------------------------------------------+
|                       Heap File                    |
+----------------------------------------------------+
|  Page 1         |  Page 2         |  Page 3        |
+------------------+-----------------+---------------+
| [Row1] [Row2]   | [Row4] [Row6]   | [Row7]         |
| [Free Space]    | [Free Space]    | [Free Space]   |
+------------------+-----------------+---------------+

Periodic maintenance (e.g., VACUUM) reorganizes rows to reclaim space and reduce fragmentation.

Table Storage Models

Tables are stored on disk as collections of pages, but the way data is organized within these pages can vary.

Here's how a page might look in a row-oriented storage:

+-----------------------------------+
| Row1: [Col1, Col2, Col3, Col4]    |
| Row2: [Col1, Col2, Col3, Col4]    |
| Row3: [Col1, Col2, Col3, Col4]    |
+-----------------------------------+

And in a column-oriented storage:

+--------------------+
| Column1 Data       |
| [Value1, Value2,   |
|  Value3, ...]      |
+--------------------+

Indexes

+-----------------+
|    Query        |
+--------+--------+
         |
         v
+--------+--------+
|    Index Pages  |
+--------+--------+
         |
         v
+--------+--------+
|   Heap Pages    |
+-----------------+

B-Tree Indexes

Tree:
        (root page)
       /          \
   (index page)  (index page)
   /       \        /      \
(data)   (data)  (data)  (data)

Hash Indexes

[Diagram: Hash Index Mapping]
Key1 --> Bucket A
Key2 --> Bucket B
Key3 --> Bucket A

Bitmap Indexes

[Diagram: Bitmap Index Example]
Value1: 101010
Value2: 110011
Value3: 100101

Non-Clustered Indexes

[Diagram: Non-Clustered Index]
+------------+       +------------+
| Index Page | ----> | Heap Page  |
+------------+       +------------+

Clustered Indexes

Partial Indexes

+--------------+
| Condition    |
+--------------+
| Indexed Rows |
+--------------+

Covering Indexes

+------------+
| Key + Data |
+------------+

Index-Only Scans

Free Space Maps

+------------+------------+------------+
| Page 1     | Page 2     | Page 3     |
| Free Space | Used Space | Free Space |
+------------+------------+------------+

Implications for Performance

Understanding how tables and indexes are stored can have significant implications for database performance and optimization strategies.

Data Locality

Storing related data close together on disk improves data locality, which enhances performance by reducing the number of disk I/O operations required to retrieve data.

For example, when a query requests several rows that are stored sequentially on the same page or extent, the database can read them all in a single disk operation, speeding up the retrieval process.

Choosing the Right Storage Model

Selecting between row-oriented and column-oriented storage depends on the workload and query patterns.

Effective Indexing Strategies

Indexes improve query performance by allowing the database to locate data without scanning every row in a table. However, they also consume disk space and can slow down write operations because the index must be updated whenever data is modified.

Balancing the number and types of indexes is crucial. Over-indexing can lead to unnecessary overhead, while under-indexing can result in slow query performance.

Practical Examples and Commands

Managing and optimizing database storage is crucial for maintaining performance and ensuring efficient use of resources. Below are practical commands for PostgreSQL and MySQL that help inspect and manage storage aspects. Each command includes its purpose, when to use it, expected output, and how to interpret the results.

PostgreSQL Commands

I. Check Table and Index Sizes

SELECT
 pg_size_pretty(pg_relation_size('your_table')) AS table_size,
 pg_size_pretty(pg_indexes_size('your_table')) AS indexes_size,
 pg_size_pretty(pg_total_relation_size('your_table')) AS total_size;

Expected Output:

table_size indexes_size total_size
120 MB 30 MB 150 MB

II. Analyze Query Execution with Buffer Usage

EXPLAIN (ANALYZE, BUFFERS) your_query;

Expected Output:

Seq Scan on your_table  (cost=0.00..431.00 rows=21000 width=...)
 Buffers: shared hit=123 read=45
 ...

III. Reclaim Dead Space with VACUUM

VACUUM your_table;

Expected Output:

VACUUM

Successful execution indicates that dead space has been reclaimed. Use VACUUM VERBOSE your_table; for detailed output.

IV. Check Space Usage with pgstattuple

SELECT * FROM pgstattuple('your_table');

Expected Output:

table_len    |  1000000
  tuple_count  |   20000
  tuple_len    |    800000
  dead_tuple_count |  5000
  dead_tuple_len   |  200000
  free_space        |  0

V. Enable Page-Level Checksums

initdb --data-checksums -D /path/to/data

Expected Output:

Data page checksums are enabled.

Ensures that any corrupted data pages are identified, enhancing reliability and data integrity.

VI. Monitor I/O Statistics with pg_stat_io

SELECT * FROM pg_stat_io;

Expected Output:

query_id | read_pages | write_pages
---------|------------|------------
1        |     500    |     20
2        |    1500    |     50

VII. Optimize Memory Usage with shared_buffers

shared_buffers = 2GB

VIII. Update Statistics with ANALYZE

ANALYZE your_table;

Expected Output:

ANALYZE

Successful execution means statistics are updated. Use EXPLAIN to see how updated statistics affect query plans.

IX. Reorder Table Data with CLUSTER

CLUSTER your_table USING your_index;

Expected Output:

CLUSTER

The table is physically reordered, which can lead to faster sequential scans and better cache utilization for indexed queries.

MySQL Commands

I. Show Table Status

SHOW TABLE STATUS LIKE 'your_table';

Expected Output:

Name Engine Version Row_format Rows Avg_row_length Data_length Max_data_length Index_length ...
your_table InnoDB 10 Compact 20000 100 2000000 ... 500000 ...

II. Monitor InnoDB Buffer Pool Metrics

Used to determine how effectively the buffer pool is caching data and to identify if more memory allocation is needed.

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';

Expected Output:

+---------------------------------+---------+
| Variable_name                   | Value   |
+---------------------------------+---------+
| Innodb_buffer_pool_read_requests | 100000 |
+---------------------------------+---------+

+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| Innodb_buffer_pool_reads | 500   |
+--------------------------+-------+

III. Adjust Buffer Pool Size

innodb_buffer_pool_size = 4G

Other Considerations

Table of Contents

    Understanding the Storage of Tables and Indexes on Disk
    1. Storage Structures
      1. Pages and Extents
    2. Table and Heap Organization
      1. Table Storage Models
      2. Indexes
    3. Implications for Performance
      1. Data Locality
      2. Choosing the Right Storage Model
      3. Effective Indexing Strategies
    4. Practical Examples and Commands
      1. PostgreSQL Commands
      2. MySQL Commands
    5. Other Considerations