Há muito tempo, em uma galáxia muito, muito distante… lá na era do COBOL, os dados eram armazenados em uma tabela onde cada coluna ocupava espaços fixos. Assim, o CEP ocupava a posição de 1 a 7, o número de 8 a 12, o logradouro de 13 a 33 e assim por diante.
Quando os bancos de dados relacionais foram criados, criou-se também o CHAR, tipos de dados para strings com tamanho fixo que funcionam de forma semelhante. Infelizmente, o CHAR armazenava desnecessariamente espaços em branco e foi substituído posteriormente pelo VARCHAR, que são strings com tamanho variável e bem mais flexíveis, o qual se tornou o modelo padrão.
Por motivos que não vêm ao caso, muita gente ainda usa CHAR em bancos de dados. Vou demonstrar aqui, de forma simples, como converter um campo CHAR para VARCHAR numa tabela simples, o que pode economizar imediatamente 40% do espaço da tabela:
CREATE TABLE t (n integer, v char(100));
INSERT INTO t SELECT n, repeat('x',round(random()*100)::integer)
FROM generate_series(1,1000000) n;
SELECT * FROM t limit 10;
n | v
--+-------------------------------------------------------------------------------------
1 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3 | xxxxxxx
4 | xxxxxxxxxxxxxx
5 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
6 | xxxxx
7 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
8 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
9 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
10 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
(10 rows)
\dt+ t
List of relations
Schema | Name | Type | Owner | Size | Description
-------+------+-------+----------+--------+-------------
public | t | table | postgres | 135 MB |
(1 row)
ALTER TABLE t ALTER v TYPE varchar(100);
\dt+ t
List of relations
Schema | Name | Type | Owner | Size | Description
-------+------+-------+----------+-------+-------------
public | t | table | postgres | 83 MB |
(1 row)Como você pode ver, mesmo sem índices, a tabela caiu de 135 MB para 83 MB. Agora, eu montei um script rápido para listar as tabelas com colunas do tipo CHAR:
SELECT n.nspname, c.relname, a.attname, a.attnum, atttypmod
FROM
pg_type t
JOIN pg_attribute a ON a.atttypid = t.oid
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE
t.typname = 'bpchar' AND
n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema')
ORDER BY 1,2,3
;Por fim, montei um script para alterar todos os campos CHAR para VARCHAR, tomando cuidado para alterar vários campos num único ALTER TABLE para não ter que reescrever tabelas mais de uma vez:
SELECT
'ALTER TABLE ' || n.nspname || '.' || c.relname ||
string_agg ($$ ALTER $$ || a.attname || $$ TYPE varchar($$ || a.attnum || $$)$$,',') ||
';'
FROM
pg_type t
JOIN pg_attribute a ON a.atttypid = t.oid
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE
t.typname = 'bpchar' AND
a.attnum > 1 AND
n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema')
GROUP BY n.nspname, c.relname
ORDER BY n.nspname, c.relname
;Vale a recomendação: faça um teste antes de usar isso em produção!

Muito bem observado, Fábio! Me admira que alguns softwares comerciais de grandes corporações usam o PostgreSQL e outros SGBDR adotando o CHAR como tipo global para todas as colunas com texto/strings. Mas, apesar de eu nunca ter feito um teste apropriado, percebo que na maioria dos casos o uso de CHAR oferece um pouco mais de desempenho em relação a VARCHAR. Seria isto efeito placebo “positivista” ou realmente o fato da compressão/TOAST implica em perdas de desempenho? Em sua extensa trajetória profissional, já chegou a testar isso?