Type optimization at scale

Concepts covered: Storage Optimization

A column's type fixes how many bytes each value occupies once the engine reads it, and that width is paid on every row. At 10 billion rows, 1 extra byte per row is 10 GB more that the engine scans and holds in memory on every query that touches the column, and that data also travels between machines. The table below lists the widths you are choosing between.

TypeBytes per valueWhat it holds
TINYINT1Integers from -128 to 127
SMALLINT2Integers from -32,768 to 32,767
INTEGER4Integers up to about 2.1 billion either way
BIGINT8Integers up to about 9.2 quintillion either way
REAL4Floating point, about 7 significant digits
DOUBLE8Floating point, about 15 significant digits
DECIMAL(p,s), p up to 188Exact decimals
DECIMAL(p,s), p 19 to 3816Exact decimals, wider
DATE4A calendar day
TIMESTAMP8A date and time to the millisecond
BOOLEAN1true or false

These are the widths the engine uses for each value in memory. Files on disk usually hold less, which the compression section covers.

Size each column to its worst case

Pick the narrowest type that holds the largest value the column could ever take, judged by what the value measures. watch_ms in the Reels view log is a single view's watch time in milliseconds. SMALLINT stops at 32,767, under 33 seconds, so the first long view breaks it. INTEGER reaches 2,147,483,647 ms, almost 25 days, far past any single view, so it fits with room to spare, while BIGINT would spend 4 more bytes per row on range no view will use. IDs are the exception: generators at this scale hand out 64-bit values, and a narrower column fails the first time an ID passes its ceiling, so IDs get BIGINT.

Here is the first of those traps on a realistic video ID. Predict the last column, then run it.

SELECT
  9007199254740993 AS video_id,
  CAST(9007199254740993 AS DOUBLE) AS as_double,
  CAST(CAST(9007199254740993 AS DOUBLE) AS BIGINT) AS back_to_bigint
Result
video_idas_doubleback_to_bigint
900719925474099290071992547409929007199254740992

A DOUBLE keeps 53 bits for its digits, so above 2 to the 53rd power it can only land on even numbers. The ID ending in 993 comes back ending in 992, and 2 different videos now share an ID: views of one get credited to the other, with no error anywhere.

Text that holds numbers sorts as text

Raw logs often land with every field as text. Keeping watch_ms as VARCHAR costs more bytes than INTEGER and changes how values compare: text is compared character by character, so '15000' sorts ahead of '800' because '1' comes before '8'. Predict the order in each tab before you run it.

raw_views
view_idwatch_ms_text
1900
215000
34200
431000
5800

As VARCHAR

Orders by the VARCHAR column, comparing the text a character at a time.

SELECT
  view_id,
  watch_ms_text
FROM raw_views
ORDER BY watch_ms_text
Result
view_idwatch_ms_text
5800
1900
34200
215000
431000

As INTEGER

Casts to INTEGER first, so ORDER BY compares numeric values.

SELECT
  view_id,
  CAST(watch_ms_text AS INTEGER) AS watch_ms
FROM raw_views
ORDER BY watch_ms
Result
view_idwatch_ms
5800
1900
34200
215000
431000

The VARCHAR tab returns 15000, 31000, 4200, 800, 900, the order a word list would file them in. The INTEGER tab returns 800, 900, 4200, 15000, 31000. The cast fixes the order, and the worst-case rule from earlier decides which integer type it targets. A single view's milliseconds stay under INTEGER's ceiling. A column that adds views together keeps growing, so its worst case is its largest future total.

A video_stats table keeps a row per reel with lifetime_watch_ms, the total milliseconds all viewers have spent on that reel. Which type should the column use?

About this lesson

This section is part of the Data Types: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.