Fio 

    To test speed of disks (read/write), for databases is preferred to use fio, is a much more accurate and better to simulate database-like I/O patterns, for example:

    Notice the block size (bs) is set to 16k, which is a common for MariaDB, for PostgreSQL you might want to use 8k or 4k depending on your configuration.

    Note: Make sure you have fio installed on your system. And also run the commands in the mountpoint of the disk you want to test (where the database files are located).

    fio --name=mariadb-rw \
        --ioengine=libaio \
        --rw=randrw \
        --rwmixread=70 \
        --bs=16k \
        --direct=1 \
        --size=10G \
        --numjobs=4 \
        --runtime=60 \
        --group_reporting \
        --filename=testfile
    

    Use --size=10G or adjust based on your available space — ensure it's larger than RAM to avoid cache bias.

    Random Read Only 

    Example simulating read-heavy OLAP load:

    fio --name=mariadb-read \
        --ioengine=libaio \
        --rw=randread \
        --bs=16k \
        --direct=1 \
        --size=10G \
        --numjobs=4 \
        --runtime=60 \
        --group_reporting \
        --filename=testfile
    

    Random Write Only 

    Example simulating intensive insert/update activity:

    fio --name=mariadb-write \
        --ioengine=libaio \
        --rw=randwrite \
        --bs=16k \
        --direct=1 \
        --size=10G \
        --numjobs=4 \
        --runtime=60 \
        --group_reporting \
        --filename=testfile
    

    PostgreSQL 

    Example for PostgreSQL with 8KB block size:

    
    fio --name=postgres-rw \
        --ioengine=libaio \
        --rw=randrw \
        --rwmixread=70 \
        --bs=8k \
        --direct=1 \
        --size=10G \
        --numjobs=4 \
        --runtime=60 \
        --group_reporting \
        --filename=pg_testfile
    

    WAL-like Sequential Writes (fsync) 

    To simulate WAL-like sequential writes, you can use:

    fio --name=postgres-wal \
        --ioengine=libaio \
        --rw=write \
        --bs=8k \
        --direct=1 \
        --size=1G \
        --numjobs=1 \
        --runtime=60 \
        --fdatasync=1 \
        --group_reporting \
        --filename=pg_waltest
    

    Temporary Table I/O 

    To simulate temporary table I/O, (sorts, hashes, joins):

    fio --name=postgres-tempio \
        --ioengine=libaio \
        --rw=randrw \
        --rwmixread=50 \
        --bs=8k \
        --direct=1 \
        --size=2G \
        --numjobs=2 \
        --runtime=60 \
        --group_reporting \
        --filename=pg_tempfile
    

    Cleanup 

    After testing, you can clean up the test files:

    rm -f pg_testfile pg_waltest pg_tempfile
    

    fio output example 

    fio --name=postgres-rw \
        --ioengine=libaio \
        --rw=randrw \
        --rwmixread=70 \
        --bs=8k \
        --direct=1 \
        --size=10G \
        --numjobs=4 \
        --runtime=60 \
        --group_reporting \
        --filename=pg_testfile
    
    postgres-rw: (g=0): rw=randrw, bs=(R) 8192B-8192B, (W) 8192B-8192B, (T) 8192B-8192B, ioengine=libaio, iodepth=1
    ...
    fio-3.33
    Starting 4 processes
    postgres-rw: Laying out IO file (1 file / 10240MiB)
    Jobs: 4 (f=4): [m(4)][100.0%][r=7919KiB/s,w=3443KiB/s][r=989,w=430 IOPS][eta 00m:00s]
    postgres-rw: (groupid=0, jobs=4): err= 0: pid=434548: Wed Jun 18 05:36:07 2025
      read: IOPS=990, BW=7920KiB/s (8110kB/s)(464MiB/60008msec)
        slat (usec): min=2, max=329, avg= 9.89, stdev= 4.57
        clat (nsec): min=1290, max=9574.2k, avg=564570.22, stdev=307178.97
         lat (usec): min=106, max=9631, avg=574.46, stdev=307.37
        clat percentiles (usec):
         |  1.00th=[  198],  5.00th=[  223], 10.00th=[  239], 20.00th=[  273],
         | 30.00th=[  478], 40.00th=[  510], 50.00th=[  545], 60.00th=[  578],
         | 70.00th=[  635], 80.00th=[  701], 90.00th=[  898], 95.00th=[ 1090],
         | 99.00th=[ 1303], 99.50th=[ 1385], 99.90th=[ 3458], 99.95th=[ 5211],
         | 99.99th=[ 8455]
       bw (  KiB/s): min= 5008, max=10544, per=100.00%, avg=7923.79, stdev=277.47, samples=476
       iops        : min=  626, max= 1318, avg=990.47, stdev=34.68, samples=476
      write: IOPS=428, BW=3427KiB/s (3510kB/s)(201MiB/60008msec); 0 zone resets
        slat (usec): min=2, max=129, avg=11.14, stdev= 4.30
        clat (usec): min=4640, max=43184, avg=7993.70, stdev=1768.62
         lat (usec): min=4649, max=43200, avg=8004.84, stdev=1768.41
        clat percentiles (usec):
         |  1.00th=[ 4948],  5.00th=[ 5932], 10.00th=[ 6259], 20.00th=[ 6587],
         | 30.00th=[ 7046], 40.00th=[ 7373], 50.00th=[ 7767], 60.00th=[ 8160],
         | 70.00th=[ 8717], 80.00th=[ 9241], 90.00th=[10028], 95.00th=[10552],
         | 99.00th=[12256], 99.50th=[14222], 99.90th=[25822], 99.95th=[28967],
         | 99.99th=[33162]
       bw (  KiB/s): min= 2864, max= 3760, per=100.00%, avg=3428.71, stdev=36.74, samples=476
       iops        : min=  358, max=  470, avg=428.59, stdev= 4.59, samples=476
      lat (usec)   : 2=0.01%, 100=0.01%, 250=9.69%, 500=15.89%, 750=33.05%
      lat (usec)   : 1000=6.38%
      lat (msec)   : 2=4.67%, 4=0.07%, 10=27.18%, 20=2.99%, 50=0.07%
      cpu          : usr=0.15%, sys=0.53%, ctx=85675, majf=0, minf=45
      IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
         submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
         complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
         issued rwts: total=59408,25708,0,0 short=0,0,0,0 dropped=0,0,0,0
         latency   : target=0, window=0, percentile=100.00%, depth=1
    
    Run status group 0 (all jobs):
       READ: bw=7920KiB/s (8110kB/s), 7920KiB/s-7920KiB/s (8110kB/s-8110kB/s), io=464MiB (487MB), run=60008-60008msec
      WRITE: bw=3427KiB/s (3510kB/s), 3427KiB/s-3427KiB/s (3510kB/s-3510kB/s), io=201MiB (211MB), run=60008-60008msec
    
    Disk stats (read/write):
      vda: ios=59908/26311, merge=0/18, ticks=33860/218043, in_queue=254055, util=97.19%
    

    From the output, you can see:

    • IOPS: Input/Output Operations Per Second, indicating how many read/write operations are performed per second. IOPS: ~990 read, ~428 write = ~1,418 total IOPS
    • BW: Bandwidth, showing the amount of data read/written per second. BW: ~7920KiB/s read, ~3427KiB/s write = ~11,347KiB/s total
    • Latency: The time taken for an I/O operation to complete, with percentiles showing distribution. Latency: avg read ~574.46us, avg write ~8004.84us (reads are faster than writes, which is typical for many workloads)
    • CPU Usage: Indicates how much CPU time was spent on I/O operations. CPU: usr=0.15%, sys=0.53% (low CPU usage, indicating efficient I/O operations)
    • IO Depth: The number of I/O operations that can be in progress at once. IO depths: 1=100.0% (indicating single-threaded I/O, which is common for many database workloads, In PostgreSQL, each backend process (i.e., per connection) typically performs synchronous, blocking I/O — issuing one request at a time, waiting for completion, then issuing the next. So IO depths: 1=100.0% in fio does match the behavior of a single PostgreSQL connection, not because of max_connections = 1, but because each connection handles I/O in a single-threaded, synchronous fashion.

    dd 

    You can also use dd for a quick test, but it is not as accurate for database workloads:

    For writing:

    dd if=/dev/zero of=testfile bs=1G count=1 oflag=direct status=progress
    

    For reading:

    dd if=testfile of=/dev/null bs=1G count=1 iflag=direct status=progress