Skip to content
Guide · Database operations

Sizing Storage for One-Second Sensor Telemetry

A repeatable way to size storage for sensors that report every second: load a realistic sample into the real schema, measure bytes per reading with PostgreSQL's size functions, then extrapolate with assumptions you write down. The page shows the measurement queries and their output on a small 180,000-row PGlite sample (illustrative measurements, not a production benchmark), how column order, value type, table shape and index choice change the result, why partitioned tables need a different query, how much write-ahead log the load produced, and which items (WAL, replicas, backups, headroom) the table size leaves out.

Facts checked 27 October 2026 against the vendors' official documentation. Versions covered: PostgreSQL 18 (measurements taken on PostgreSQL 18.6 in PGlite 0.5.11). Next review due April 2027. Products, licences and prices in this field change quickly; confirm on the vendor's site before you choose. No benchmarks are quoted unless the source and test setup are named.
Short answer
  • Do not size one-second telemetry from data type sizes alone. Load a sample into the exact production schema and measure it with pg_relation_size, pg_indexes_size and pg_total_relation_size.
  • Every figure on this page measured in bytes, pages or WAL is an illustrative measurement on a small PGlite sample (PostgreSQL 18.6 compiled to 32-bit WebAssembly): it shows the method, it is not a production benchmark and it is not representative of your server. Estimates built on it inherit that limit.
  • Illustrative measurement on the small PGlite sample (not a production benchmark): a narrow row (point_id, ts, value, quality) took 60.3 bytes of table plus 33.1 bytes of primary-key index, 93.6 bytes per reading, before WAL, replicas and backups. This is a figure for that schema, sample and build, not a universal per-row size.
  • Also illustrative measurements on the same PGlite sample: reordering the columns cut the table to 52.2 bytes per row, and a wide row holding four values took 29.5 bytes per value including its index.
  • Estimate from stated assumptions, built on the illustrative PGlite measurement (so it demonstrates the arithmetic, not a capacity figure for a real server): 2,000 points at one reading per second give 172.8 million rows a day; at 93.6 bytes per reading that is an estimated 16.17 GB a day and about 485 GB for an assumed 30-day raw window.
  • Write every assumption next to the estimate and replace the PGlite figures with a measurement on the target server: the sample here ran on a 32-bit WebAssembly build of PostgreSQL, WAL volume depends on server settings, and compressed or columnar engines need their own measurement.
How we know: Research-based from the PostgreSQL 18 documentation (database object management functions, database page layout, determining disk usage, B-tree indexes, WAL configuration, routine vacuuming, CREATE TABLE) and the Tiger Data hypercore documentation, checked 27 October 2026. Every SQL statement on this page was run on PostgreSQL 18.6 (PGlite 0.5.11, a 32-bit WebAssembly build with 8 kB pages) and the outputs are copied from that run. Every size, row-count-per-page and WAL figure on the page is an illustrative measurement on a small PGlite sample (PostgreSQL 18.6 compiled to 32-bit WebAssembly), not a production benchmark and not representative of a production server; every capacity figure is an estimate from stated assumptions built on those illustrative measurements; none is a universal per-row size. The point counts, rates, retention windows, replica count and backup retention in the examples are illustrative assumptions. No compressed or columnar engine was run.

What this page sizes, and for whom

About the numbers on this page. The byte counts, index sizes, rows per page, WAL bytes and bytes per row below are illustrative measurements on a small PGlite sample (PostgreSQL 18.6 compiled to 32-bit WebAssembly, running in a single session). They show how the method works and what the output looks like. They are not a production benchmark and are not representative of your server: alignment, index growth, WAL and checkpoint behaviour can all differ on a 64-bit production build with your settings and data. Every capacity estimate here is built on those illustrative figures, so treat it as a demonstration of the arithmetic and replace the inputs with measurements from your own server.

This is for the engineer who has to answer "how much disk do we need?" before a project starts: for example a BMS integrator pulling chiller power, AHU supply-air temperature and zone CO2 at one-second resolution for fault detection, or an energy team logging sub-meter demand every second. At this rate storage is often an early constraint, and estimates from data type sizes alone can miss alignment, index and WAL overheads by enough to matter.

The IoT sensor data storage guide already gives an arithmetic estimate from data type sizes for one-minute data. This page replaces arithmetic with measurement: it shows how to measure bytes per reading on a sample, what the measurement includes and leaves out, and how to turn it into a capacity figure with assumptions written down. It uses plain PostgreSQL row storage because that is where the method is easiest to run; the same method applies to any engine that reports its own table sizes.

Decisions this page helps you make: raw retention window, whether one-second data needs columnar compression, table shape (narrow or wide), which indexes you can afford, and how much to provision for WAL, replicas and backups.

The method in six steps

  1. Copy the production schema exactly: same column order, types, primary key, secondary indexes and partitioning. Size depends on all of them.
  2. Load a representative sample in the order production will write it (time order across many points), with realistic values and realistic gaps, nulls and quality codes.
  3. Make it big enough that fixed per-table overheads (metapages, free space map, visibility map, the last partly filled page) are negligible: thousands of pages, not dozens.
  4. Measure table, index and total bytes, then divide by the row count.
  5. Extrapolate with rows per day times retention days times measured bytes per row.
  6. Add the items the table size does not include: WAL, replicas, backups, roll-up tables, dead rows and operational headroom, each as a written assumption.

Step 1 to 4: measure a sample

The sample below is one hour of one-second readings for 50 points (180,000 rows), inserted in time order as an ingestion service would write them. The table is the narrow design from the storage guide. VACUUM (ANALYZE) runs before measuring so that the free space map and visibility map exist, as they would on a live table.

Sample table and load (run on PostgreSQL 18.6)
CREATE TABLE reading (
    point_id  integer NOT NULL,
    ts        timestamptz NOT NULL,
    value     double precision,
    quality   smallint NOT NULL DEFAULT 0,
    PRIMARY KEY (point_id, ts)
);

-- remember the WAL position before the load (used in step 6)
CREATE TABLE lsn_mark (label text, lsn pg_lsn);
INSERT INTO lsn_mark VALUES ('before', pg_current_wal_lsn());

INSERT INTO reading (point_id, ts, value)
SELECT p, ts, 20 + p * 0.01 + sin(extract(epoch FROM ts) / 600.0)
FROM generate_series(1, 50) AS p
CROSS JOIN generate_series('2026-10-01 00:00:00+00'::timestamptz,
                           '2026-10-01 00:59:59+00'::timestamptz,
                           interval '1 second') AS ts
ORDER BY ts, p;

INSERT INTO lsn_mark VALUES ('after', pg_current_wal_lsn());
VACUUM (ANALYZE) reading;
Measure bytes in total and per row
SELECT pg_relation_size('reading')        AS heap_bytes,
       pg_indexes_size('reading')         AS index_bytes,
       pg_total_relation_size('reading')  AS total_bytes,
       pg_relation_size('reading', 'fsm') AS fsm_bytes,
       pg_relation_size('reading', 'vm')  AS vm_bytes;

SELECT round(pg_relation_size('reading')::numeric / count(*), 1)       AS heap_bytes_per_row,
       round(pg_indexes_size('reading')::numeric / count(*), 1)        AS index_bytes_per_row,
       round(pg_total_relation_size('reading')::numeric / count(*), 1) AS total_bytes_per_row
FROM reading;
Output (illustrative measurement on the 180,000-row PGlite sample, PostgreSQL 18.6 compiled to 32-bit WebAssembly; not a production benchmark and not representative of your server)
 heap_bytes | index_bytes | total_bytes | fsm_bytes | vm_bytes
------------+-------------+-------------+-----------+----------
   10846208 |     5963776 |    16842752 |     24576 |     8192

 heap_bytes_per_row | index_bytes_per_row | total_bytes_per_row
--------------------+---------------------+---------------------
               60.3 |                33.1 |                93.6

Check the measurement against the page layout

A measurement you cannot explain may be measuring the wrong thing, so cross-check it. The PostgreSQL page layout documentation gives each row a fixed header of 23 bytes on most machines, user data starting at a MAXALIGN boundary, and a 4-byte item identifier per row. Two queries on the same sample confirm the arithmetic:

Bytes per column, row datum size and rows on the first 8 kB page (run on PostgreSQL 18.6 in PGlite; outputs are illustrative measurements on the small sample, not representative of your server)
SELECT current_setting('block_size') AS block_size;
--  block_size
-- ------------
--  8192

SELECT avg(pg_column_size(r.*))::numeric(6,1) AS avg_row_datum_bytes,
       avg(pg_column_size(r.point_id)) AS point_id_b, avg(pg_column_size(r.ts)) AS ts_b,
       avg(pg_column_size(r.value))    AS value_b,    avg(pg_column_size(r.quality)) AS quality_b
FROM reading r;
--  avg_row_datum_bytes | point_id_b | ts_b | value_b | quality_b
-- ---------------------+------------+------+---------+-----------
--                 50.0 | 4.0000000000000000 | 8.0000000000000000 | 8.0000000000000000 | 2.0000000000000000

SELECT count(*) AS rows_on_first_page
FROM reading WHERE (ctid::text::point)[0] = 0;
--  rows_on_first_page
-- --------------------
--                 136
Where the 60.3 table bytes per row come from (arithmetic from the page layout documentation, checked against the illustrative PGlite measurement; MAXALIGN of 8 assumed, as on most 64-bit servers)
PartBytesNote
Row header2423 bytes, padded so user data starts on an 8-byte boundary
point_id integer4
Padding4timestamptz must start on an 8-byte boundary
ts timestamptz, value double precision168 + 8
quality smallint2Row is 50 bytes, matching pg_column_size
Padding to the next row6Rows are stored at 8-byte multiples: 56
Item identifier4In the page header area: 60 bytes per row
Rows per page136136 x 60 = 8,160 of the 8,192 bytes; the rest is the 24-byte page header and 8 unused bytes. 8,192 / 136 = 60.2, close to the 60.3 of the illustrative PGlite measurement

Why the sample must be large

Each table and index carries fixed overhead: metapages, the free space map and visibility map forks, and a last page that is only partly full. On a sample of a few hundred rows that overhead can dominate bytes per row. A smaller partitioned sample in the next section, with two partitions and 72,000 rows, measured 95.1 bytes per row against 93.6 here (both illustrative PGlite measurements); our reading is that most of the difference is that fixed overhead being counted twice. Measure on thousands of pages, then extrapolate.

How layout choices change bytes per reading

At 86,400 rows per point per day, a few bytes per row are worth measuring. The same 180,000 readings were loaded into four variants and measured the same way. All figures in the table are illustrative measurements on the small PGlite sample (PostgreSQL 18.6 compiled to 32-bit WebAssembly), apply to these schemas only, and are not a production benchmark; the differences between variants show what to compare on your own server, not what you will get there:

The wide variant and its per-value size (run on PostgreSQL 18.6 in PGlite; the output is an illustrative measurement, not representative of your server)
CREATE TABLE ahu_reading (
    device_id    integer NOT NULL,
    ts           timestamptz NOT NULL,
    supply_temp  double precision,
    return_temp  double precision,
    zone_co2     double precision,
    fan_speed    double precision,
    PRIMARY KEY (device_id, ts)
);
-- 50 devices x 900 seconds, in time order
INSERT INTO ahu_reading (device_id, ts, supply_temp, return_temp, zone_co2, fan_speed)
SELECT d, ts, 12 + d / 10.0, 22 + d / 10.0, 400 + d, 50 + d / 2.0
FROM generate_series('2026-10-01 00:00:00+00'::timestamptz,
                     '2026-10-01 00:14:59+00'::timestamptz,
                     interval '1 second') AS ts
CROSS JOIN generate_series(1, 50) AS d
ORDER BY ts, d;
VACUUM (ANALYZE);

SELECT round(pg_total_relation_size('ahu_reading')::numeric / (count(*) * 4), 1)
       AS wide_total_bytes_per_value
FROM ahu_reading;
--  wide_total_bytes_per_value
-- ----------------------------
--                        29.5
Illustrative measurements on a small PGlite sample (PostgreSQL 18.6 compiled to 32-bit WebAssembly, after VACUUM); not a production benchmark and not representative of your server
VariantRowsTable bytes per rowIndex bytes per rowWhat changed
Narrow: point_id, ts, value, quality180,00060.333.1Baseline
Narrow, columns reordered: ts, value, point_id, quality180,00052.232.68-byte columns first removes the 4-byte gap and the row fits in 48 bytes
Narrow with value as real (4 bytes)180,00052.232.6Same saving as reordering, for the same reason; precision drops to about 6 significant digits
Wide: device_id, ts, four double values45,000 (180,000 values)76.640.6One header and one key per four values: 29.5 total bytes per value

Index choices, measured

Three index observations from the same PGlite sample (illustrative measurements on a small sample compiled to 32-bit WebAssembly; not a production benchmark and not representative of your server), each with a caveat:

  • Insert order affects B-tree size. The (point_id, ts) primary key built up row by row while data arrived in time order measured 5,963,776 bytes on the sample; after REINDEX it measured 5,701,632 bytes (about 4% smaller). Size the index as it will be in production, which means built incrementally.
  • BRIN on ts was tiny here. A BRIN index on ts measured 24,576 bytes for 180,000 rows on the sample, because it stores one summary per block range rather than one entry per row. It only helps while rows stay roughly in time order on disk.
  • A B-tree on ts alone was small here for an artificial reason. It measured 1,302,528 bytes on the sample (about 7 bytes per row), because every timestamp repeats 50 times in this sample and B-tree deduplication stores duplicates compactly. Real gateways rarely stamp 50 points with the identical microsecond, so do not carry this figure over; measure with your real timestamps.

Narrow or wide is a modelling decision first

On the PGlite sample (an illustrative measurement), the wide table used about a third of the bytes per value, but it only works when a device reports the same set of values together at the same instant. BMS points with different intervals, change-of-value reporting or frequently added points fit the narrow shape better. The trade-offs are the subject of narrow vs wide time-series tables; this page only shows how to measure each candidate.

Measuring a partitioned table

One-second data in PostgreSQL is commonly partitioned by time (see choosing partition intervals). The parent of a partitioned table has no storage of its own, so pg_total_relation_size on it returns 0. Sum the leaf partitions from pg_partition_tree instead. The sample below has two daily partitions and 20 points of one-second data around midnight.

Partitioned sample: one hour across midnight, 72,000 rows (run on 18.6)
DROP TABLE IF EXISTS reading;   -- replaces the narrow sample table from the first example
CREATE TABLE reading (
    point_id  integer NOT NULL,
    ts        timestamptz NOT NULL,
    value     double precision,
    quality   smallint NOT NULL DEFAULT 0,
    PRIMARY KEY (point_id, ts)
) PARTITION BY RANGE (ts);
CREATE TABLE reading_2026_10_01 PARTITION OF reading
    FOR VALUES FROM ('2026-10-01 00:00+00') TO ('2026-10-02 00:00+00');
CREATE TABLE reading_2026_10_02 PARTITION OF reading
    FOR VALUES FROM ('2026-10-02 00:00+00') TO ('2026-10-03 00:00+00');

INSERT INTO reading (point_id, ts, value)
SELECT p, ts, 20 + sin(extract(epoch FROM ts) / 600.0)
FROM generate_series(1, 20) AS p
CROSS JOIN generate_series('2026-10-01 23:30:00+00'::timestamptz,
                           '2026-10-02 00:29:59+00'::timestamptz,
                           interval '1 second') AS ts
ORDER BY ts, p;
VACUUM (ANALYZE);
Parent size versus the sum of partitions (run on PostgreSQL 18.6 in PGlite; the byte counts are illustrative measurements on a small sample, not representative of your server)
SELECT pg_total_relation_size('reading') AS parent_total_bytes;
--  parent_total_bytes
-- --------------------
--                   0

SELECT relid, isleaf, level, pg_total_relation_size(relid) AS total_bytes
FROM pg_partition_tree('reading') ORDER BY level, relid::text;
--        relid        | isleaf | level | total_bytes
-- --------------------+--------+-------+-------------
--  reading            | f      |     0 |           0
--  reading_2026_10_01 | t      |     1 |     3424256
--  reading_2026_10_02 | t      |     1 |     3424256

SELECT sum(pg_total_relation_size(relid)) AS all_partitions_bytes,
       round(sum(pg_total_relation_size(relid)) / (SELECT count(*) FROM reading), 1) AS bytes_per_row
FROM pg_partition_tree('reading');
--  all_partitions_bytes | bytes_per_row
-- ----------------------+---------------
--               6848512 |          95.1

Step 5: extrapolate, with the assumptions in the query

Keep the measurement and the assumptions in one place so that anyone can change an assumption and re-run the estimate. The query below uses the 93.6 bytes per reading from the illustrative PGlite measurement above (a small sample on a 32-bit WebAssembly build, not representative of your server) and assumes 2,000 points at one reading per second with no gaps and a 30-day raw window; those three inputs are illustrative assumptions, not typical values. Everything it returns is an estimate built on an illustrative measurement and stated assumptions: it demonstrates the arithmetic and is not a capacity figure for any real server. Replace the 93.6 with the figure you measure on your own server.

Estimate from the illustrative measurement (run on PostgreSQL 18.6 in PGlite; results are estimates, not capacity figures)
WITH measured AS (
    SELECT 93.6::numeric AS bytes_per_reading          -- illustrative PGlite figure: replace with yours
), assumptions AS (
    SELECT 2000  AS points,                             -- one-second points in scope
           86400 AS readings_per_point_per_day,         -- 1 s interval, no gaps
           30    AS raw_days                            -- raw retention window
)
SELECT a.points * a.readings_per_point_per_day AS rows_per_day,
       round(a.points * a.readings_per_point_per_day * m.bytes_per_reading / 1e9, 2) AS est_gb_per_day,
       round(a.points * a.readings_per_point_per_day * m.bytes_per_reading * a.raw_days / 1e9, 1) AS est_gb_raw_window
FROM measured m CROSS JOIN assumptions a;
Output (estimate from stated assumptions and an illustrative PGlite measurement, not representative of your server; 1 GB = 1,000,000,000 bytes)
 rows_per_day | est_gb_per_day | est_gb_raw_window
--------------+----------------+-------------------
    172800000 |          16.17 |             485.2

Check the row rate before trusting the estimate

The largest error in most estimates is the row count, not bytes per row. "One second" is often a polling ceiling, not what arrives: some points report only on change, gateways drop samples under load, and some meters report every 15 minutes. If you have a day of real data, count actual rows per point per day and use that instead of 86,400. If you plan to keep one-second data only for a subset of points (for example chiller and AHU control loops for fault detection) and one-minute data for the rest, size the two groups separately.

Step 6: what the table size does not include

The measured figure is table plus indexes at rest. A server needs more than that. List each item below as an assumption with its own number; none of them can be read off the sample.

WAL generated by the 180,000-row load (illustrative measurement on the small PGlite sample, PostgreSQL 18.6 compiled to 32-bit WebAssembly; PGlite reports fsync off and full_page_writes on; not a production benchmark and not representative of your server)
SELECT pg_wal_lsn_diff(
         (SELECT lsn FROM lsn_mark WHERE label = 'after'),
         (SELECT lsn FROM lsn_mark WHERE label = 'before')) AS wal_bytes_for_insert;
--  wal_bytes_for_insert
-- ----------------------
--              29616512
Capacity items beyond table and index size (fill in your own assumptions)
ItemWhy it uses spaceHow to size it
Write-ahead log (WAL) in pg_walEvery insert is logged first. Checkpoints happen every 5 minutes or before max_wal_size (1 GB by default, a soft limit) is exceeded. With WAL archiving on, segments stay until archived.Measure WAL per row on the sample (below), then size pg_wal for max_wal_size plus a backlog if archiving or a replica falls behind
Roll-up tablesFifteen-minute or hourly aggregates kept for yearsMeasure bytes per roll-up row the same way; rows per day = points x buckets per day
Dead rows and free spaceUpdates and deletes leave dead row versions; standard VACUUM makes the space reusable but generally does not return it to the operating systemAppend-only readings stay close to the measurement; add an allowance if you update readings or use a fillfactor below 100
Maintenance headroomVACUUM FULL writes a complete new copy of the table (REINDEX similarly builds a new index) before the old one is removedOur inference from that: allow free space at least the size of the largest table or partition you would rebuild, plus its indexes
ReplicasEach physical replica holds a full copy, including indexesMultiply by the number of copies
Backups and WAL archiveBase backups plus the WAL needed to replay from themBackup retention x backup size, plus archived WAL for that period; see the backup article
GrowthMore points and more sites over the life of the hardwarePlan for the point count you expect at the end of the period, not today's

Reading the WAL figure

In this illustrative PGlite measurement (a small sample, single session, not representative of your server), the load wrote 29,616,512 bytes of WAL for 180,000 rows, about 164.5 bytes per row: more than the 93.6 bytes the rows occupy at rest. Part of that is likely to come from full-page images (our inference from the documentation, not separately measured): with full_page_writes on (the default), the first change to a data page after each checkpoint logs the whole page. The ratio depends on checkpoint timing, batch size and server settings, so treat it as a reason to measure WAL on your own server, not as a constant. It matters for pg_wal sizing, the WAL archive used for point-in-time recovery, and network traffic to replicas.

Compressed and columnar engines need their own measurement

Row-store arithmetic does not transfer to columnar engines. For example, the vendor reports in Tiger Data's hypercore documentation that converting TimescaleDB data to its columnstore includes compression of up to 98%. That is the vendor's figure, not reproduced here; what you get depends on how repetitive your values and timestamps are, how the data is segmented and ordered, and how long data stays in the uncompressed row store before conversion.

The method still applies: load a sample of real data (a day exported from an existing BMS historian is far more realistic than generated sine waves, which may compress better than real readings), let the engine compress it as it would in production, read the size the engine reports, then extrapolate. Keep the recent uncompressed window separate in the estimate, because it is stored at row-store size. For how compression works and what it costs, see time-series compression and columnar storage.

Worked example: one-second data for a plant room

An estate team wants one-second data for fault detection on 2,000 points (chillers, pumps, AHU control loops) and one-minute data for everything else, with 30 days of raw one-second data. The written sizing note might read:

  • Illustrative measurement on the small PGlite sample (180,000 rows, PostgreSQL 18.6 row storage compiled to 32-bit WebAssembly; not a production benchmark and not representative of the production server, so a real note would replace it with a measurement on that server before sign-off): 93.6 bytes per reading including the primary key; about 164.5 bytes of WAL per reading during load.
  • Assumed (illustrative values for this example): 2,000 points at 86,400 readings per day (to be replaced by a counted rate after the first week), 30-day raw window, one physical replica, backups kept for 14 days.
  • Estimated from those assumptions and the illustrative measurement (placeholders until the server measurement replaces them): 16.17 GB per day and 485.2 GB for the raw window on the primary; the same again on the replica; roll-ups, pg_wal, the backup set and maintenance headroom listed as separate lines with their own assumptions.
  • Decision trigger: if the counted row rate or retention grows enough that row storage becomes the dominant cost, re-run the measurement in a compressed engine before buying disk.

Limitations of this method

  • Build differences. The sample ran on PGlite, a 32-bit WebAssembly build. Its heap measurement matched the 8-byte-alignment arithmetic above for these column types (136 rows per page), but the page layout documentation says user data alignment depends on the platform's MAXALIGN, and index and WAL sizes were not cross-checked the same way. Re-run the same script on the target server before signing off.
  • Synthetic values. Fixed-width types (integer, timestamptz, double precision) take the same space whatever the value, so generated data is fine for row stores. Variable-width columns (text, numeric, jsonb) and every compressed engine need real values.
  • Nulls. A row with any null value gains a null bitmap, which can change its padded size. Include nulls in the sample at the rate you expect.
  • Time. A freshly loaded table has no dead rows or index churn. Re-measure after a few weeks in production and compare.

Related pages: retention and downsampling design for choosing the windows that drive this estimate, choosing partition intervals, and backup and restore for time-series data for the backup line items. To run the queries yourself, use the PostgreSQL playground or PostgreSQL in Docker.

Frequently asked questions

How do I calculate IoT data storage requirements?

Multiply rows per day by retention days by bytes per row, where bytes per row is measured on a sample loaded into your real schema (table plus indexes). Then add WAL, replicas, backups, roll-ups and headroom as separate, written assumptions.

How many bytes does one sensor reading take in PostgreSQL?

It depends on the schema, the platform and the data; there is no universal figure. As an illustrative measurement on the small PGlite sample on this page (PostgreSQL 18.6 compiled to 32-bit WebAssembly; not a production benchmark and not representative of your server), a narrow row of point_id, timestamptz, double precision and smallint with a primary key on (point_id, ts) took 93.6 bytes per reading: 60.3 for the row and 33.1 for the index. Reordering columns brought the row to 52.2 bytes on the same sample. Measure on your own server before using any figure.

Why does pg_total_relation_size return 0 for my table?

Because it is a partitioned table: the parent has no storage of its own. Sum pg_total_relation_size(relid) over pg_partition_tree('your_table') to get the size of all partitions.

How much storage does one-second data need compared with one-minute data?

Sixty times as many rows for the same points, so roughly sixty times the raw storage in a row store if every point really reports every second (an estimate that assumes the same bytes per row at both rates). Count the real row rate first; change-of-value reporting and dropped samples often make it lower.

Does compression change the calculation?

Yes. Columnar engines can store fewer bytes per reading (vendors report large reductions), but the ratio depends on your data. Measure a sample of real data in the engine you plan to use rather than applying a vendor percentage.

Sources

Checked 27 October 2026.

How we research these articles: our editorial method. Research-based from official documentation; SQL examples are run on a local database where the page says so. CodeWithSQL earns nothing from the vendors mentioned.

Building a sensor or telemetry database?

Start with the overview, or compare time-series databases side by side.