Tablas y Tipos de Datos en Supabase

Una tabla es la estructura fundamental donde guardas tus datos en PostgreSQL. Piensa en ella como una hoja de cálculo: tiene columnas (que definen el tipo de dato) y filas (que son los registros individuales).

En esta sección vas a aprender a crear tablas, elegir los tipos de datos correctos para cada columna y configurar las restricciones que mantienen tus datos consistentes.

Crear tablas

Tienes dos formas de crear tablas en Supabase: desde el dashboard con la interfaz visual, o escribiendo SQL directamente.

Desde el dashboard

  1. Abre tu proyecto en el dashboard de Supabase
  2. Ve a Table Editor en el menú lateral
  3. Haz clic en New Table
  4. Define el nombre de la tabla y sus columnas
  5. Haz clic en Save

El dashboard te genera el SQL por detrás. Es cómodo para empezar, pero a medida que tu proyecto crezca vas a preferir escribir SQL directamente para tener control total y poder versionar tus cambios.

Con SQL

sql
CREATE TABLE productos (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  nombre text NOT NULL,
  descripcion text,
  precio numeric(10, 2) NOT NULL,
  stock int4 NOT NULL DEFAULT 0,
  disponible bool NOT NULL DEFAULT true,
  categoria text NOT NULL,
  tags text[] DEFAULT '{}',
  metadata jsonb DEFAULT '{}',
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

Vamos a desglosar cada parte de esta tabla.

Tipos de datos

PostgreSQL tiene un sistema de tipos extenso. Estos son los que vas a usar el 90% del tiempo en Supabase.

text

Cadenas de texto de longitud variable. Sin límite práctico de tamaño.

sql
nombre text NOT NULL,
descripcion text,
email text UNIQUE NOT NULL,

PostgreSQL también soporta varchar(n) para limitar la longitud, pero en la práctica text es igual de eficiente y más flexible. Usa text a menos que necesites forzar una longitud máxima a nivel de base de datos.

Numéricos: int4, int8, numeric

sql
-- int4 (integer): numeros enteros de -2,147,483,648 a 2,147,483,647
stock int4 NOT NULL DEFAULT 0,
cantidad int4,
 
-- int8 (bigint): enteros mas grandes, hasta 9.2 quintillones
visitas int8 DEFAULT 0,
 
-- numeric(precision, escala): numeros con decimales exactos
precio numeric(10, 2),  -- hasta 99,999,999.99
Cuándo usar cada tipo numérico

Usa int4 para contadores y cantidades normales. Usa int8 cuando los números puedan crecer mucho (visitas, IDs secuenciales grandes). Usa numeric para dinero y valores donde los decimales importan -- float tiene errores de redondeo que con dinero no te puedes permitir.

bool

Valores booleanos: true o false.

sql
disponible bool NOT NULL DEFAULT true,
verificado bool NOT NULL DEFAULT false,
es_admin bool DEFAULT false,

uuid

UUID (Universally Unique Identifier -- identificador único universal) es una cadena de 128 bits que es prácticamente imposible de repetir. Supabase los usa como primary keys por defecto.

sql
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_id uuid REFERENCES auth.users(id),

gen_random_uuid() es una función de PostgreSQL que genera un UUID v4 aleatorio cada vez que insertas un registro. No necesitas pasar el ID manualmente.

Ejemplo de UUID: 550e8400-e29b-41d4-a716-446655440000

UUID vs ID secuencial

Los UUIDs son mejores que IDs auto-incrementales (1, 2, 3...) por varias razones: no exponen cuántos registros tienes, se pueden generar en el cliente sin consultar la base de datos, y no causan conflictos en sistemas distribuidos. Supabase usa UUIDs por defecto y es la recomendación general.

timestamptz

Timestamps (marcas de tiempo) con zona horaria. Almacenan fecha y hora con precisión de microsegundos.

sql
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
publicado_en timestamptz,
expira_en timestamptz,

now() retorna el timestamp actual del servidor. Siempre usa timestamptz (con zona horaria) en vez de timestamp (sin zona horaria) -- evita problemas cuando tus usuarios están en diferentes zonas horarias.

jsonb

JSON binario. Te permite guardar datos semi-estructurados dentro de una columna.

sql
metadata jsonb DEFAULT '{}',
configuracion jsonb DEFAULT '{}',
direccion jsonb,
sql
-- Insertar JSON
INSERT INTO productos (nombre, metadata)
VALUES (
  'Laptop Pro',
  '{"marca": "TechCo", "ram": "16GB", "almacenamiento": "512GB SSD"}'
);
 
-- Consultar dentro del JSON
SELECT nombre, metadata->>'marca' AS marca
FROM productos
WHERE metadata->>'ram' = '16GB';
 
-- Filtrar por valor dentro del JSON
SELECT * FROM productos
WHERE metadata @> '{"marca": "TechCo"}';

El operador ->> extrae un valor como texto. El operador @> verifica si el JSON contiene cierto sub-objeto.

No abuses de jsonb

jsonb es útil para datos variables o metadata, pero no lo uses para reemplazar columnas normales. Si siempre necesitas filtrar u ordenar por un campo, ese campo debe ser una columna propia, no un valor dentro de un JSON. Las columnas normales son más eficientes para queries y se pueden indexar directamente.

Arrays

PostgreSQL soporta columnas de tipo array -- listas de valores del mismo tipo.

sql
tags text[] DEFAULT '{}',
categorias text[],
precios_historicos numeric[],
sql
-- Insertar con arrays
INSERT INTO productos (nombre, tags)
VALUES ('Laptop', ARRAY['electronica', 'computacion', 'portatil']);
 
-- Buscar productos que tengan cierto tag
SELECT * FROM productos
WHERE 'electronica' = ANY(tags);
 
-- Agregar un elemento al array
UPDATE productos
SET tags = array_append(tags, 'oferta')
WHERE id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
 
-- Ver cuantos tags tiene cada producto
SELECT nombre, array_length(tags, 1) AS total_tags
FROM productos;

Primary keys

La primary key (clave primaria) identifica de forma única cada fila de una tabla. No puede repetirse y no puede ser NULL.

sql
-- UUID como primary key (recomendado en Supabase)
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
 
-- ID secuencial (menos comun en Supabase)
id serial PRIMARY KEY,
 
-- Primary key compuesta (dos columnas juntas)
CREATE TABLE producto_categoria (
  producto_id uuid REFERENCES productos(id),
  categoria_id uuid REFERENCES categorias(id),
  PRIMARY KEY (producto_id, categoria_id)
);

En Supabase la convención es usar id uuid PRIMARY KEY DEFAULT gen_random_uuid(). Cuando creas una tabla desde el dashboard, esta columna se agrega automáticamente.

Valores por defecto

Los defaults (valores por defecto) se asignan automáticamente cuando insertas un registro sin especificar esa columna.

sql
CREATE TABLE pedidos (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  estado text NOT NULL DEFAULT 'pendiente',
  total numeric(10, 2) NOT NULL DEFAULT 0,
  items int4 NOT NULL DEFAULT 0,
  pagado bool NOT NULL DEFAULT false,
  created_at timestamptz NOT NULL DEFAULT now()
);
 
-- No necesitas pasar id, estado, total, items, pagado ni created_at
INSERT INTO pedidos DEFAULT VALUES;
 
-- Solo necesitas pasar los valores que quieras especificar
INSERT INTO pedidos (total, items)
VALUES (1500.00, 3);

Los defaults más comunes:

DefaultUso
gen_random_uuid()IDs únicos
now()Timestamps de creación
true / falseBooleanos con valor inicial
0Contadores
'{}'JSON o arrays vacíos
'pendiente'Estados iniciales

NOT NULL vs nullable

NOT NULL significa que la columna siempre debe tener un valor. Si intentas insertar un registro sin esa columna y no tiene default, PostgreSQL te devuelve un error.

sql
CREATE TABLE usuarios (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  email text NOT NULL,           -- obligatorio, siempre debe tener valor
  nombre text NOT NULL,          -- obligatorio
  avatar_url text,               -- opcional, puede ser NULL
  bio text,                      -- opcional
  telefono text,                 -- opcional
  created_at timestamptz NOT NULL DEFAULT now()
);

La regla general:

  • NOT NULL para datos que siempre necesitas (email, nombre, precio)
  • Nullable (sin NOT NULL) para datos opcionales (avatar, bio, teléfono)
sql
-- Esto funciona: email y nombre están presentes
INSERT INTO usuarios (email, nombre)
VALUES ('ana@ejemplo.com', 'Ana Lopez');
 
-- Esto falla: falta email que es NOT NULL
INSERT INTO usuarios (nombre)
VALUES ('Ana Lopez');
-- ERROR: null value in column "email" violates not-null constraint

Constraints adicionales

Los constraints (restricciones) son reglas que PostgreSQL aplica para mantener la integridad de tus datos.

UNIQUE

Garantiza que no haya valores duplicados en una columna.

sql
email text UNIQUE NOT NULL,
slug text UNIQUE NOT NULL,

CHECK

Valida que un valor cumpla una condición.

sql
precio numeric(10, 2) CHECK (precio >= 0),
edad int4 CHECK (edad >= 18 AND edad <= 120),
estado text CHECK (estado IN ('pendiente', 'activo', 'cancelado')),

Combinando constraints

sql
CREATE TABLE usuarios (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  email text UNIQUE NOT NULL CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
  username text UNIQUE NOT NULL CHECK (length(username) >= 3),
  edad int4 CHECK (edad >= 13),
  created_at timestamptz NOT NULL DEFAULT now()
);

El operador ~* es un match de expresión regular case-insensitive en PostgreSQL.

Ejemplo real: tabla de productos

Vamos a crear una tabla de productos completa paso a paso.

sql
-- 1. Crear la tabla
CREATE TABLE productos (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  nombre text NOT NULL CHECK (length(nombre) >= 2),
  slug text UNIQUE NOT NULL,
  descripcion text,
  precio numeric(10, 2) NOT NULL CHECK (precio >= 0),
  precio_descuento numeric(10, 2) CHECK (precio_descuento >= 0),
  stock int4 NOT NULL DEFAULT 0 CHECK (stock >= 0),
  disponible bool NOT NULL DEFAULT true,
  categoria text NOT NULL,
  tags text[] DEFAULT '{}',
  imagenes text[] DEFAULT '{}',
  metadata jsonb DEFAULT '{}',
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);
 
-- 2. Crear un indice para busquedas por categoria
CREATE INDEX idx_productos_categoria ON productos(categoria);
 
-- 3. Crear un indice para busquedas por slug
CREATE INDEX idx_productos_slug ON productos(slug);
 
-- 4. Insertar datos de prueba
INSERT INTO productos (nombre, slug, descripcion, precio, stock, categoria, tags)
VALUES
  ('Monitor 4K 27"', 'monitor-4k-27', 'Monitor IPS 4K de 27 pulgadas', 8500.00, 15, 'monitores', ARRAY['electronica', '4k', 'monitor']),
  ('Teclado Mecanico RGB', 'teclado-mecanico-rgb', 'Teclado mecanico con switches Cherry MX', 2200.00, 30, 'perifericos', ARRAY['electronica', 'teclado', 'gaming']),
  ('Mouse Inalambrico Pro', 'mouse-inalambrico-pro', 'Mouse ergonomico con sensor de alta precision', 950.00, 50, 'perifericos', ARRAY['electronica', 'mouse', 'ergonomico']);
 
-- 5. Verificar los datos
SELECT nombre, precio, stock, categoria FROM productos;

Este SQL lo puedes ejecutar directamente en el SQL Editor de Supabase. Después de crearlo, la tabla aparece automáticamente en el Table Editor y queda disponible vía el SDK.

Acceder desde el SDK

Una vez creada la tabla, puedes interactuar con ella desde tu aplicación:

typescript
import { createClient } from '@supabase/supabase-js'
 
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
 
// Obtener todos los productos disponibles
const { data: productos, error } = await supabase
  .from('productos')
  .select('id, nombre, precio, categoria, tags')
  .eq('disponible', true)
  .order('created_at', { ascending: false })
 
if (error) {
  console.error('Error al obtener productos:', error.message)
} else {
  console.log('Productos:', productos)
}

Modificar tablas existentes

Conforme tu proyecto evolucione, vas a necesitar agregar columnas, cambiar tipos o renombrar cosas.

sql
-- Agregar una columna
ALTER TABLE productos ADD COLUMN sku text UNIQUE;
 
-- Agregar columna con default
ALTER TABLE productos ADD COLUMN peso_kg numeric(6, 2) DEFAULT 0;
 
-- Renombrar una columna
ALTER TABLE productos RENAME COLUMN categoria TO categoria_principal;
 
-- Cambiar el tipo de una columna
ALTER TABLE productos ALTER COLUMN stock TYPE int8;
 
-- Hacer una columna NOT NULL (todos los registros deben tener valor primero)
ALTER TABLE productos ALTER COLUMN descripcion SET NOT NULL;
 
-- Quitar NOT NULL
ALTER TABLE productos ALTER COLUMN descripcion DROP NOT NULL;
 
-- Eliminar una columna
ALTER TABLE productos DROP COLUMN metadata;
Cuidado con ALTER TABLE en producción

Modificar tablas en producción puede causar downtime si la tabla tiene muchos registros. Agrega columnas nuevas como nullable o con defaults para evitar bloqueos. Nunca elimines columnas que tu aplicación todavía está usando.

Eliminar tablas

sql
-- Eliminar una tabla (falla si tiene foreign keys apuntandole)
DROP TABLE productos;
 
-- Eliminar forzando (elimina también las foreign keys)
DROP TABLE productos CASCADE;
 
-- Eliminar solo si existe (evita errores)
DROP TABLE IF EXISTS productos;

Usa DROP TABLE con cuidado. En producción, casi nunca quieres eliminar una tabla completa.

Siguiente paso

Ya sabes crear tablas y elegir los tipos de datos correctos. El siguiente paso es conectar tablas entre sí usando relaciones (foreign keys), que es donde PostgreSQL realmente muestra su poder.