Skip to content

Commit c09b332

Browse files
authored
Merge pull request #2 from rogerwelin/feature/padding-waste
feature/padding-waste
2 parents af9f7db + fae4e0a commit c09b332

3 files changed

Lines changed: 246 additions & 1 deletion

File tree

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,31 @@ Use this in staging/production databases or CI pipelines to guarantee every new
138138

139139
### As an analysis tool - audit existing tables
140140

141-
Use `check()` to inspect any table's current layout and see where padding is wasted:
141+
Use `padding_wasted()` to quickly check how many bytes a table wastes per row:
142+
143+
```sql
144+
SELECT column_tetris.padding_wasted('orders');
145+
-- Returns: 7 (bytes of avoidable padding per row)
146+
```
147+
148+
Pass `'total'` to see the total waste across all rows in the table. Wrap with `pg_size_pretty()` for human-readable output:
149+
150+
```sql
151+
SELECT pg_size_pretty(column_tetris.padding_wasted('orders', 'total'));
152+
-- Returns: '458 MB'
153+
```
154+
155+
Find all tables with padding waste:
156+
157+
```sql
158+
SELECT schemaname, tablename,
159+
column_tetris.padding_wasted(schemaname || '.' || tablename) AS bytes_per_row
160+
FROM pg_tables
161+
WHERE schemaname = 'public'
162+
AND column_tetris.padding_wasted(schemaname || '.' || tablename) > 0;
163+
```
164+
165+
Use `check()` for a detailed column-by-column layout report:
142166

143167
```sql
144168
SELECT * FROM column_tetris.check('orders');

pg_column_tetris--0.1.0.sql

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,121 @@ LANGUAGE sql VOLATILE AS $$
261261
SELECT * FROM column_tetris.compute_layout(relation_name::regclass::oid);
262262
$$;
263263

264+
-- ---------------------------------------------------------------------------
265+
-- padding_wasted(text) — Returns bytes of avoidable padding per row
266+
-- ---------------------------------------------------------------------------
267+
268+
CREATE FUNCTION column_tetris.padding_wasted(relation_name text, report_mode text DEFAULT 'row')
269+
RETURNS bigint
270+
LANGUAGE plpgsql AS $$
271+
DECLARE
272+
v_rel_oid oid;
273+
v_current_waste int := 0;
274+
v_optimal_waste int := 0;
275+
v_offset int;
276+
v_align int;
277+
v_padding int;
278+
v_num_cols int;
279+
v_any_nullable bool;
280+
v_row_count bigint;
281+
col record;
282+
BEGIN
283+
IF report_mode NOT IN ('row', 'total') THEN
284+
RAISE EXCEPTION 'invalid report_mode: %. Must be row or total', report_mode;
285+
END IF;
286+
287+
v_rel_oid := relation_name::regclass::oid;
288+
289+
SELECT count(*)::int, bool_or(NOT a.attnotnull)
290+
INTO v_num_cols, v_any_nullable
291+
FROM pg_attribute a
292+
WHERE a.attrelid = v_rel_oid
293+
AND a.attnum > 0
294+
AND NOT a.attisdropped;
295+
296+
IF v_num_cols = 0 THEN
297+
RETURN 0;
298+
END IF;
299+
300+
-- Current layout waste
301+
v_offset := 23;
302+
IF v_any_nullable THEN
303+
v_offset := v_offset + ((v_num_cols + 7) / 8);
304+
END IF;
305+
v_offset := ((v_offset + 7) / 8) * 8;
306+
307+
FOR col IN
308+
SELECT t.typalign, t.typlen
309+
FROM pg_attribute a
310+
JOIN pg_type t ON t.oid = a.atttypid
311+
WHERE a.attrelid = v_rel_oid
312+
AND a.attnum > 0
313+
AND NOT a.attisdropped
314+
ORDER BY a.attnum
315+
LOOP
316+
IF col.typlen = -1 THEN
317+
v_align := 4;
318+
v_offset := ((v_offset + v_align - 1) / v_align) * v_align + 4;
319+
ELSE
320+
v_align := CASE col.typalign
321+
WHEN 'd' THEN 8 WHEN 'i' THEN 4
322+
WHEN 's' THEN 2 WHEN 'c' THEN 1 ELSE 4
323+
END;
324+
v_padding := ((v_offset + v_align - 1) / v_align) * v_align - v_offset;
325+
v_current_waste := v_current_waste + v_padding;
326+
v_offset := ((v_offset + v_align - 1) / v_align) * v_align + col.typlen;
327+
END IF;
328+
END LOOP;
329+
330+
-- Optimal layout waste
331+
v_offset := 23;
332+
IF v_any_nullable THEN
333+
v_offset := v_offset + ((v_num_cols + 7) / 8);
334+
END IF;
335+
v_offset := ((v_offset + 7) / 8) * 8;
336+
337+
FOR col IN
338+
SELECT t.typalign, t.typlen
339+
FROM pg_attribute a
340+
JOIN pg_type t ON t.oid = a.atttypid
341+
WHERE a.attrelid = v_rel_oid
342+
AND a.attnum > 0
343+
AND NOT a.attisdropped
344+
ORDER BY
345+
CASE
346+
WHEN t.typlen = -1 THEN 5
347+
WHEN t.typalign = 'd' THEN 1
348+
WHEN t.typalign = 'i' THEN 2
349+
WHEN t.typalign = 's' THEN 3
350+
WHEN t.typalign = 'c' THEN 4
351+
ELSE 5
352+
END,
353+
CASE WHEN a.attnotnull THEN 0 ELSE 1 END,
354+
a.attnum
355+
LOOP
356+
IF col.typlen = -1 THEN
357+
v_align := 4;
358+
v_offset := ((v_offset + v_align - 1) / v_align) * v_align + 4;
359+
ELSE
360+
v_align := CASE col.typalign
361+
WHEN 'd' THEN 8 WHEN 'i' THEN 4
362+
WHEN 's' THEN 2 WHEN 'c' THEN 1 ELSE 4
363+
END;
364+
v_padding := ((v_offset + v_align - 1) / v_align) * v_align - v_offset;
365+
v_optimal_waste := v_optimal_waste + v_padding;
366+
v_offset := ((v_offset + v_align - 1) / v_align) * v_align + col.typlen;
367+
END IF;
368+
END LOOP;
369+
370+
IF report_mode = 'total' THEN
371+
EXECUTE format('SELECT count(*) FROM %s', v_rel_oid::regclass) INTO v_row_count;
372+
RETURN (v_current_waste - v_optimal_waste)::bigint * v_row_count;
373+
END IF;
374+
375+
RETURN (v_current_waste - v_optimal_waste)::bigint;
376+
END;
377+
$$;
378+
264379
-- ---------------------------------------------------------------------------
265380
-- validate(oid) — Raises exception if layout is suboptimal
266381
-- ---------------------------------------------------------------------------

test/sql/06_padding_wasted.sql

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
-- Test: padding_wasted() function
2+
-- Expected: correct per-row and total waste calculations
3+
4+
-- Setup: need warn mode so test tables can be created with suboptimal order
5+
SELECT column_tetris.set_mode('warn');
6+
7+
-- Test 1: Suboptimal table should have padding waste > 0
8+
CREATE TABLE test_pw_suboptimal (
9+
flag boolean,
10+
big_id bigint
11+
);
12+
13+
DO $$
14+
DECLARE
15+
v_waste int;
16+
BEGIN
17+
SELECT column_tetris.padding_wasted('test_pw_suboptimal') INTO v_waste;
18+
IF v_waste <= 0 THEN
19+
RAISE EXCEPTION 'TEST FAILED: padding_wasted should be > 0 for suboptimal table, got %', v_waste;
20+
END IF;
21+
RAISE NOTICE 'TEST PASSED: suboptimal table has % bytes waste per row', v_waste;
22+
END;
23+
$$;
24+
25+
-- Test 2: Optimal table should have 0 waste
26+
CREATE TABLE test_pw_optimal (
27+
big_id bigint,
28+
count integer,
29+
flag boolean
30+
);
31+
32+
DO $$
33+
DECLARE
34+
v_waste int;
35+
BEGIN
36+
SELECT column_tetris.padding_wasted('test_pw_optimal') INTO v_waste;
37+
IF v_waste <> 0 THEN
38+
RAISE EXCEPTION 'TEST FAILED: padding_wasted should be 0 for optimal table, got %', v_waste;
39+
END IF;
40+
RAISE NOTICE 'TEST PASSED: optimal table has 0 waste';
41+
END;
42+
$$;
43+
44+
-- Test 3: Non-optimal order but zero actual waste (int4, int4, float8, text)
45+
CREATE TABLE test_pw_zero_waste (
46+
a integer,
47+
b integer,
48+
c float8,
49+
d text
50+
);
51+
52+
DO $$
53+
DECLARE
54+
v_waste int;
55+
BEGIN
56+
SELECT column_tetris.padding_wasted('test_pw_zero_waste') INTO v_waste;
57+
IF v_waste <> 0 THEN
58+
RAISE EXCEPTION 'TEST FAILED: int4+int4+float8+text should have 0 waste, got %', v_waste;
59+
END IF;
60+
RAISE NOTICE 'TEST PASSED: zero waste despite non-optimal order';
61+
END;
62+
$$;
63+
64+
-- Test 4: Total mode = per_row * row_count
65+
INSERT INTO test_pw_suboptimal (flag, big_id)
66+
SELECT true, generate_series(1, 100);
67+
68+
DO $$
69+
DECLARE
70+
v_per_row bigint;
71+
v_total bigint;
72+
BEGIN
73+
SELECT column_tetris.padding_wasted('test_pw_suboptimal') INTO v_per_row;
74+
SELECT column_tetris.padding_wasted('test_pw_suboptimal', 'total') INTO v_total;
75+
IF v_total <> v_per_row * 100 THEN
76+
RAISE EXCEPTION 'TEST FAILED: total should be % * 100 = %, got %', v_per_row, v_per_row * 100, v_total;
77+
END IF;
78+
RAISE NOTICE 'TEST PASSED: total mode = % (% per row * 100 rows)', v_total, v_per_row;
79+
END;
80+
$$;
81+
82+
-- Test 5: Invalid mode raises exception
83+
DO $$
84+
DECLARE
85+
v_caught bool := false;
86+
BEGIN
87+
BEGIN
88+
PERFORM column_tetris.padding_wasted('test_pw_suboptimal', 'bad_mode');
89+
EXCEPTION WHEN raise_exception THEN
90+
v_caught := true;
91+
IF SQLERRM NOT LIKE '%invalid report_mode%' THEN
92+
RAISE EXCEPTION 'TEST FAILED: unexpected error message: %', SQLERRM;
93+
END IF;
94+
RAISE NOTICE 'TEST PASSED: invalid mode rejected';
95+
END;
96+
IF NOT v_caught THEN
97+
RAISE EXCEPTION 'TEST FAILED: invalid mode should raise exception';
98+
END IF;
99+
END;
100+
$$;
101+
102+
-- Cleanup
103+
DROP TABLE IF EXISTS test_pw_suboptimal;
104+
DROP TABLE IF EXISTS test_pw_optimal;
105+
DROP TABLE IF EXISTS test_pw_zero_waste;
106+
DO $$ BEGIN RAISE NOTICE 'All 06_padding_wasted tests passed'; END; $$;

0 commit comments

Comments
 (0)