Disk Structure: Understanding Physical Storage
Disk Addressing
Read aloud is unavailable in this browser.
Locating Data in a Sea of Billions
A modern hard disk drive contains billions of sectors, each holding a small piece of data. When your database needs to read a specific customer's record or write a transaction log entry, the system must identify the exact physical location among these billions of possibilities. Disk addressing is the mechanism that makes this possible.
Why Disk Addressing Matters for Database Systems:
Database performance fundamentally depends on efficient disk addressing:
- Index lookups translate key values to disk addresses for record retrieval
- Query execution plans are optimized based on understanding access patterns
- Buffer pool management uses addresses to track which pages are cached
- Write-ahead logging depends on knowing where log records are persisted
- Free space management tracks which addresses are available for new data
This page examines how disk addressing works at every level—from the low-level hardware translation to the high-level abstractions databases employ.
By the end of this page, you will understand the evolution from CHS to LBA addressing, the mathematics of address translation, how drives internally map logical to physical addresses, and how database systems organize data to leverage addressing for optimal performance.
CHS Addressing (Historical Foundation)
Cylinder-Head-Sector (CHS) addressing was the original method for specifying disk locations, directly reflecting the physical structure of the drive. While obsolete for modern systems, understanding CHS provides insight into why LBA was developed and how legacy systems operated.
The CHS Triplet:
An address in CHS format consists of three components:
- Cylinder (C) — Which concentric track position (determines arm position)
- Head (H) — Which recording surface (determines active head)
- Sector (S) — Which sector within the track (determines rotation position)
Example: CHS (1000, 4, 32) means:
- Position the arm at cylinder 1000
- Activate head 4 (5th surface, since numbering starts at 0)
- Read/write sector 32 on that track
CHS to Physical Mapping:
Physical Location = f(Cylinder, Head, Sector)
Arm Position determined by Cylinder
Active Head selected by Head number
Rotational Position determined by Sector number
| Component | BIOS Limit | ATA Limit | Maximum Value |
|---|---|---|---|
| Cylinders | 1024 (10 bits) | 65,536 | 65,535 |
| Heads | 256 (8 bits) | 16 | 255 |
| Sectors | 63 (6 bits)* | 255 | 63 or 255 |
| Max Capacity (BIOS) | 1024×256×63×512 | ~8.4 GB | |
| Max Capacity (ATA) | 65536×16×255×512 | ~136 GB |
*Note: Sector numbering in CHS typically started at 1, not 0, limiting practical maximum to 63.
Why CHS Failed:
- Rigid Capacity Limits — BIOS constraints created hard 8.4 GB barrier
- Zone Bit Recording Incompatibility — CHS assumed constant sectors per track, but ZBR varies sector count
- Defect Handling Complexity — Bad sectors couldn't be transparently hidden
- Geometry Knowledge Required — Host needed to know exact physical layout
- Inefficient Space Utilization — Couldn't optimize for ZBR geometry
The CHS Legacy:
Despite obsolescence, CHS influenced:
- MBR (Master Boot Record) partition table format
- Boot sector loading procedures
- Terminology still used in disk discussions
- Some legacy embedded systems still use CHS-like addressing
Modern drives report 'fake' CHS geometry for compatibility with legacy software that queries it. The values are calculated from LBA capacity to satisfy BIOS/UEFI requirements but have no relationship to actual physical structure. Modern operating systems and databases use LBA exclusively.
Logical Block Addressing (LBA)
Logical Block Addressing (LBA) replaced CHS as the universal disk addressing method. LBA presents the disk as a simple linear array of sectors, numbered from 0 to N-1.
The LBA Abstraction:
Disk = [Sector 0][Sector 1][Sector 2]...[Sector N-1]
Where N is the total number of sectors:
- N = Total Capacity / Sector Size
- For a 10 TB drive with 4K sectors: N = 10×10¹² / 4096 ≈ 2.44 billion sectors
Benefits of LBA:
- Simple Linear Model — Just one number identifies each sector
- No Geometry Knowledge Required — Host doesn't need drive internals
- Transparent ZBR Support — Drive handles variable sectors per track internally
- Transparent Defect Management — Bad sectors remapped without host awareness
- Large Address Space — 48-bit LBA supports 128 PB; 64-bit supports 8 ZB
| Standard | LBA Bits | Max Sectors | Max Capacity (512B) | Max Capacity (4K) |
|---|---|---|---|---|
| ATA-1 | 28 bits | 2²⁸ | 128 GiB | 1 TiB |
| ATA-6 (48-bit) | 48 bits | 2⁴⁸ | 128 PiB | 1 EiB |
| SCSI/SAS | 64 bits | 2⁶⁴ | 8 ZiB | 64 ZiB |
| NVMe | 64 bits | 2⁶⁴ | 8 ZiB | 64 ZiB |
LBA Commands:
Modern storage interfaces use LBA-based commands:
READ (LBA, Count):
- Read 'Count' consecutive sectors starting at 'LBA'
- Returns data or error status
WRITE (LBA, Count, Data):
- Write 'Data' to 'Count' consecutive sectors starting at 'LBA'
- Returns success or error status
TRIM/UNMAP (LBA, Count):
- Inform drive that sectors are no longer needed
- Allows drive to optimize (important for SSDs, some HDDs)
VERIFY (LBA, Count):
- Verify sectors are readable without transferring data
- Used for media scanning and integrity checks
Database systems typically work with pages (4KB, 8KB, 16KB) rather than individual sectors. A database page number translates to an LBA: LBA = (Page_Number × Page_Size) / Sector_Size + Partition_Start_LBA. For example, page 1000 in a database with 8KB pages on a 4K sector drive starting at LBA 2048: LBA = (1000 × 8192) / 4096 + 2048 = 2000 + 2048 = LBA 4048.
CHS to LBA Conversion
Understanding the mathematical relationship between CHS and LBA illuminates how disk addressing evolved and enables working with legacy systems that still expose CHS interfaces.
CHS to LBA Formula:
For a drive with geometry (C_max, H_max, S_max):
LBA = (C × H_max × S_max) + (H × S_max) + (S - 1)
Where:
- C = Cylinder number (0 to C_max - 1)
- H = Head number (0 to H_max - 1)
- S = Sector number (1 to S_max, since CHS sectors start at 1)
Example Calculation:
Drive geometry: 1000 cylinders, 16 heads, 63 sectors per track
CHS (500, 8, 32) → LBA:
LBA = (500 × 16 × 63) + (8 × 63) + (32 - 1)
LBA = 504,000 + 504 + 31
LBA = 504,535
| CHS (C, H, S) | Geometry (Cmax, Hmax, Smax) | LBA Calculation | Result LBA |
|---|---|---|---|
| (0, 0, 1) | (1000, 16, 63) | (0×16×63)+(0×63)+(1-1) | 0 |
| (0, 0, 63) | (1000, 16, 63) | (0×16×63)+(0×63)+(63-1) | 62 |
| (0, 1, 1) | (1000, 16, 63) | (0×16×63)+(1×63)+(1-1) | 63 |
| (1, 0, 1) | (1000, 16, 63) | (1×16×63)+(0×63)+(1-1) | 1008 |
| (999, 15, 63) | (1000, 16, 63) | (999×16×63)+(15×63)+(63-1) | 1,007,999 |
LBA to CHS Reverse Conversion:
S = (LBA mod S_max) + 1
H = (LBA / S_max) mod H_max
C = (LBA / S_max) / H_max
LBA Order Implies Access Pattern:
The CHS-to-LBA formula reveals the intended access order:
- First, exhaust sectors on current track (S changes, H and C constant)
- Then, switch to next head (H increments, C constant)
- Finally, seek to next cylinder (C increments)
This produces the optimal access pattern: sectors → heads → cylinders
Why This Matters:
Sequential LBA access (0, 1, 2, 3...) naturally follows this pattern:
- Minimal seeks (cylinders change least frequently)
- Head switches preferred over seeks
- Rotational latency is the primary delay for sequential access
While the CHS-to-LBA formula teaches the intended access pattern, modern drives use much more complex internal mapping due to ZBR, defect management, and optimization algorithms. The drive's internal translator may place sequential LBAs in unexpected physical locations for various reasons. However, drives still optimize for sequential LBA access, so the principle remains valid.
Internal Address Translation
The drive's controller firmware maintains complex translation tables and algorithms to convert LBA addresses into actual physical locations. This internal translation handles ZBR, defect management, and performance optimization.
Translation Layers:
Host Request (LBA)
↓
[Interface Controller]
↓
[LBA Translation Engine]
↓
[Defect Remapping Check]
↓
[Zone Determination]
↓
[Physical CHS Calculation]
↓
[Servo Position Commands]
Zone Bit Recording Translation:
With ZBR, the mapping is no longer a simple formula. The drive maintains a zone table:
| Zone | Start LBA | Track Range | Sectors/Track |
|---|---|---|---|
| 0 | 0 | 0-50,000 | 1200 |
| 1 | 60M | 50,001-100,000 | 1100 |
| 2 | 115M | 100,001-150,000 | 1000 |
| ... | ... | ... | ... |
12345678910111213141516171819202122232425262728
// Simplified LBA to Physical Translation Algorithmfunction translateLBAToPhysical(lba: number): PhysicalAddress { // Step 1: Determine which zone contains this LBA let zone = findZoneForLBA(lba); // Step 2: Calculate offset within zone let lbaWithinZone = lba - zone.startLBA; // Step 3: Calculate track within zone let sectorsPerTrack = zone.sectorsPerTrack; let trackWithinZone = Math.floor(lbaWithinZone / (sectorsPerTrack * numHeads)); // Step 4: Calculate absolute track (cylinder) let cylinder = zone.startTrack + trackWithinZone; // Step 5: Calculate head and sector let remainingSectors = lbaWithinZone % (sectorsPerTrack * numHeads); let head = Math.floor(remainingSectors / sectorsPerTrack); let sector = remainingSectors % sectorsPerTrack; // Step 6: Check defect remapping let physicalAddress = { cylinder, head, sector }; if (isDefective(physicalAddress)) { physicalAddress = getRemappedAddress(lba); } return physicalAddress;}Defect Remapping in Translation:
The translation engine also handles grown defects:
- Before returning physical address, check G-List
- If LBA is in defect list, redirect to spare sector
- Spare sector may be in different zone/cylinder
- This can introduce unexpected seek for 'sequential' access
Firmware Optimization:
Modern drives may use more sophisticated mapping for optimization:
- Shingled zones may require different translation rules
- Media cache areas may temporarily hold writes
- Interleaving patterns may optimize for rotational latency
- Write pointer tracking for SMR zones
Modern drives deliberately hide internal translation details. There is no standard way to query the actual physical location for an LBA. This abstraction enables drives to use proprietary optimization techniques but means performance analysis must focus on observable behavior (latency, throughput) rather than true physical layout.
Partition Addressing
Partition tables divide the disk's LBA space into non-overlapping regions, each addressable as an independent logical volume. Understanding partition addressing is essential for database deployment.
MBR Partition Table (Legacy):
The Master Boot Record format uses 32-bit LBA addresses:
- Maximum addressable: 2³² sectors = 2 TB (512B sectors) or 16 TB (4K sectors)
- 4 primary partitions maximum
- Extended partitions allow more via linked list
MBR Partition Entry Structure:
| Bootable | Start CHS | Type | End CHS | Start LBA | Size (sectors) |
| 1 byte | 3 bytes | 1 byte| 3 bytes | 4 bytes | 4 bytes |
| Feature | MBR | GPT |
|---|---|---|
| LBA Width | 32 bits | 64 bits |
| Max Disk Size (512B) | 2 TiB | 8 ZiB |
| Max Disk Size (4K) | 16 TiB | 64 ZiB |
| Max Partitions | 4 primary (more via extended) | 128 (configurable) |
| Redundancy | None | Backup GPT at end of disk |
| CRC Protection | None | CRC32 on header and entries |
| Alignment | Often sector 63 | Typically sector 2048 (1 MiB) |
GPT Partition Table (Modern):
GUID Partition Table addresses modern needs:
- 64-bit LBA addresses (supports yottabyte-scale storage)
- 128 or more partition entries
- Backup copy at end of disk
- CRC checksums for integrity
- Default alignment at 1 MiB (LBA 2048 for 512B sectors)
GPT Partition Entry Structure:
| Partition Type GUID | Unique GUID | First LBA | Last LBA | Attributes | Name |
| 16 bytes | 16 bytes | 8 bytes | 8 bytes | 8 bytes | 72 bytes |
Address Calculation for File Systems:
When a file system is created on a partition:
File_System_LBA = Partition_Start_LBA + Relative_LBA_within_Partition
For a database file at relative sector 10000 on a partition starting at LBA 2048:
Absolute_LBA = 2048 + 10000 = 12048
Modern partitioning tools align partitions to 1 MiB boundaries (LBA 2048 on 512B sectors). This ensures alignment with Advanced Format 4K sectors, RAID stripe boundaries, and SSD page sizes. Never use legacy tools that place partition at sector 63—this causes read-modify-write overhead on every write operation for misaligned 4K sectors.
File System Addressing
File systems add another addressing layer between applications and raw LBA access. Understanding this layer clarifies how database files map to disk locations.
File System Address Components:
- Inode/File ID — Identifies the file
- Block Number — Position within the file
- Block Offset — Byte position within the block
Block Allocation:
File systems allocate disk space in blocks (clusters):
- Block Size: Typically 4KB, 8KB, 16KB, or larger
- Block Number: Sequential numbering within the file system
- Block Bitmap: Tracks which blocks are allocated
Extent-Based Allocation (Modern):
Modern file systems (ext4, XFS, NTFS) use extents for efficiency:
Extent = { Start_Block, Length }
A file might be described by:
- Extent 1: Start at block 10000, length 1000 blocks
- Extent 2: Start at block 50000, length 500 blocks
This is more efficient than tracking individual blocks.
| Layer | Address Type | Translated To |
|---|---|---|
| Application | File path + byte offset | Inode + file block |
| File System | Inode + file block | FS block number |
| File System | FS block number | Partition-relative LBA |
| Partition | Partition-relative LBA | Disk LBA |
| HBA/Driver | Disk LBA | SCSI/SATA command |
| Drive Controller | LBA | Physical CHS (internal) |
Fragmentation and Address Locality:
File system block allocation affects address locality:
Contiguous Allocation:
File blocks: 1000, 1001, 1002, 1003, 1004 (contiguous)
LBAs: 8000, 8001, 8002, 8003, 8004 (sequential)
Result: Efficient sequential I/O
Fragmented Allocation:
File blocks: 1000, 5000, 2500, 8000, 3000 (scattered)
LBAs: 8000, 40000, 20000, 64000, 24000 (random)
Result: Each read requires a seek
Database Files and Fragmentation:
Databases typically:
- Pre-allocate large contiguous extents
- Grow files in large chunks to maintain contiguity
- Use raw devices or direct I/O to bypass file system caching
- May use file system features to ensure contiguous allocation
High-performance databases often use O_DIRECT (Linux) or FILE_FLAG_NO_BUFFERING (Windows) to bypass the file system page cache. This allows the database to manage its own buffer pool more effectively. However, O_DIRECT typically requires I/O to be aligned to file system block size and to 512-byte or 4K boundaries, reinforcing the importance of understanding addressing constraints.
Database Data File Addressing
Databases implement their own addressing schemes on top of file system primitives, organizing data into pages (blocks) with specific structure and addressing conventions.
Database Page Addressing:
Most relational databases organize storage around pages:
- Page Size: 4KB (MySQL), 8KB (PostgreSQL, SQL Server), 16KB (Oracle)
- Page Number: Sequential identifier within a data file or tablespace
- Page Types: Data pages, index pages, free space pages, header pages
Page Address Structure:
(File_ID, Page_Number) → Physical Page
Where:
- File_ID identifies the data file among potentially many
- Page_Number identifies position within that file
- Offset = Page_Number × Page_Size
- •Table Row → (Table_ID, Row_ID/CTID/ROWID)
- •Row Location → (File_ID, Page_Number, Slot_Number)
- •Page Location → File byte offset = Page_Number × Page_Size
- •File Offset → File system block(s)
- •FS Block → Partition-relative LBA
- •Partition LBA → Disk absolute LBA
Index Pointers:
Indexes store pointers to data locations:
B-tree Leaf Entry: (Key_Value, Row_Pointer)
Row_Pointer: (Page_Number, Slot_Number)
When an index lookup finds a key, the pointer directs the system to:
- The specific page containing the row
- The slot within that page (for slotted page formats)
Buffer Pool and Page Addressing:
The database buffer pool caches pages in memory using page addresses:
Buffer_Pool[hash(File_ID, Page_Number)] → Page_Frame
When a page is requested:
- Hash the page address
- Check if page is in buffer pool
- If hit: Return cached page
- If miss: Read page from disk into a free frame, then return
Example: PostgreSQL CTID
PostgreSQL uses CTID (Current Tuple ID) as row address:
CTID = (block_number, tuple_index)
- Block_number: Page within the table file
- Tuple_index: Offset within the page's item pointer array
Some databases (Oracle, PostgreSQL with HOT updates) allow rows to move between pages. When this happens, either the address must update everywhere (expensive) or a forwarding pointer is left at the old location. Understanding this affects query performance—chained/migrated rows require additional I/O. Excessive row chaining indicates a need for reorganization.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.