Convenciones de PostgreSQL
Cómo aplicar
Hay dos modos. Decide cuál aplica antes de escribir nada.
Modo A — tú eliges los nombres. El usuario pidió un esquema, entidad o migración y no dictó nombres. Aplica las convenciones siguientes directamente.
Modo B — los nombres ya existen o fueron dictados. El usuario especificó nombres, o el código, las migraciones o la base de datos existentes contienen nombres que se desvían de estas convenciones. Nunca renombres silenciosamente. Reporta y pregunta.
Formato del reporte, en el idioma del usuario, antes de hacer cambios:
Nombramiento:
Debe corregirse
- `"UserProfile"` (tabla) → `user_profiles` — PascalCase entrecomillado debe estar entrecomillado en cada consulta para siempre
- `created` timestamp DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') → `created_at timestamptz DEFAULT now()` — el valor cambia cuando la zona horaria de la sesión no es UTC
Recomendado
- `users.user_email` → `users.email` — el nombre de la tabla ya es el contexto
Luego pregunta una pregunta: aplicar todo, aplicar solo "Debe corregirse", o mantener como está. Espera la respuesta.
Reglas para reportar:
- Debe corregirse = causa errores, rompe consultas o herramientas: identificadores entrecomillados mixtos, palabras reservadas, nombres de más de 63 bytes,
timestampsin zona horaria, valores predeterminados incorrectos de tiempo,json/float/moneypara datos que necesitanjsonb/numeric, falta de índice FK, bloqueos fuera de una transacción. - Recomendado = estilo y consistencia.
- Reporta solo objetos tocados por la tarea actual. Si notaste otras desviaciones en otro lugar, agrega una línea: "¿También N desviaciones similares en ? ¿Las listo?"
- Objetos en migraciones aplicadas o una base de datos activa: un cambio de nombre es un cambio destructivo. Solo sugiérelo; si se aprueba, impleméntalo como una nueva migración. Nunca edites una migración aplicada.
- Si el código usa consistentemente una convención diferente, esa gana. Menciona la diferencia una vez, luego sigue la convención del proyecto sin marcar cada ocurrencia.
- Los nombres producidos por la convención de nomenclatura del ORM (por ejemplo, EFCore.NamingConventions) se aceptan tal cual.
- Una vez que el usuario confirma un nombre no conforme, úsalo y no lo menciones de nuevo en esta sesión.
Reglas generales
snake_case, minúsculas, nunca identificadores entrecomillados.- Máximo 63 bytes por identificador; los nombres más largos se truncan silenciosamente. Acorta de forma significativa, no cortando.
- Tablas en plural:
users,orders,order_items. - Columnas en singular, sin prefijo de tabla:
users.email, nousers.user_email. - Palabras completas excepto abreviaturas comunes:
id,url,ip,sku. - Nunca uses palabras reservadas o ambiguas como nombres:
user,order,group,table,limit,offset,desc,end,date,time,timestamp. - Sin prefijos de tipo u objeto: no
tbl_users,str_name. - Los booleanos son positivos:
is_deleted, nuncais_not_deleted.
Columnas estándar
| Propósito | Nombre | Tipo |
|---|---|---|
| Clave primaria | id | bigint generated always as identity o uuid v7 |
| Creado / actualizado | created_at, updated_at | timestamptz not null default now() |
| Borrado suave | deleted_at | timestamptz null |
| Auditoría | created_by, updated_by | FK a users.id |
| Banderas | is_active, is_verified, has_children, can_edit | boolean not null default false |
| Estado | status | text + check, enum, o tabla de búsqueda |
| Solo fecha | birth_date, due_on | date |
| Validez | expires_at, valid_from, valid_to | timestamptz |
| Dinero | amount, unit_price, total_amount + currency_code | numeric(19,4) + char(3), o amount_minor bigint |
| Contadores | items_count, retry_count | integer / bigint |
| Ordenamiento | sort_order | integer |
| Clave de negocio | code, slug, external_id | text + único |
| Inquilino | tenant_id | FK |
| Concurrencia | version o sistema xmin | integer / xid |
| Datos libres | metadata, payload, settings | jsonb |
Los sufijos tienen significado: _at instante, _on/_date fecha calendario, _id referencia, _count contador, _url, _path, _hash.
Claves y restricciones
| Objeto | Patrón | Ejemplo |
|---|---|---|
| Clave primaria | pk_<tabla> | pk_orders |
| Clave foránea | fk_<tabla>_<ref_tabla>_<columna> | fk_orders_customers_customer_id |
| Restricción única | uq_<tabla>_<cols> | uq_users_email |
| Clave alternativa (EF Core) | ak_<tabla>_<cols> | ak_products_sku |
| Check | ck_<tabla>_<regla> | ck_orders_amount_positive |
| Exclusión | ex_<tabla>_<cols> | ex_bookings_room_id_period |
| Índice | ix_<tabla>_<cols> | ix_orders_customer_id_created_at |
| Índice único | ux_<tabla>_<cols> | ux_users_email |
| Índice parcial | ix_<tabla>_<cols>_<condición> | ix_users_email_not_deleted |
| Índice por método | ix_<tabla>_<col>_<método> | ix_products_tags_gin |
| Disparador | trg_<tabla>_<acción> | trg_orders_set_updated_at |
| Función de disparador | verbo + objeto | set_updated_at() |
| Secuencia | Predeterminado de PostgreSQL | orders_id_seq |
- Columna FK:
<tabla referenciada, singular>_id, o un nombre de rol cuando el rol importa:author_id,sender_id,recipient_id,parent_id. - El patrón FK incluye la columna, por lo que varias FK a una misma tabla siguen siendo únicas:
fk_messages_users_sender_id,fk_messages_users_recipient_id. - PostgreSQL no indexa las columnas FK. Cada FK necesita un índice a menos que un índice compuesto ya comience con él. EF Core los crea por convención; Drizzle, Prisma, TypeORM, Knex y sqlc no.
Relaciones
| Tipo | Forma | Ejemplo |
|---|---|---|
| Uno a muchos | FK en el hijo | order_items.order_id → orders.id |
| Uno a uno | FK + única, o PK = FK | user_profiles.user_id |
| Muchos a muchos | Unión <a>_<b>, PK compuesta | user_roles (user_id, role_id), pk_user_roles |
| Muchos a muchos con atributos | Nombre de entidad significativo | memberships, enrollments |
| Autorreferencia | parent_id o rol | categories.parent_id, employees.manager_id |
Los pares polimórficos (commentable_type + commentable_id) no pueden tener FK. Prefiere columnas FK separadas anulables con un check que exactamente una esté establecida, o tablas separadas.
Esquemas, vistas, funciones, tipos
| Objeto | Patrón | Ejemplo |
|---|---|---|
| Esquema | dominio o módulo | auth, billing, catalog, audit |
| Vista | v_<nombre> | v_active_users |
| Vista materializada | mv_<nombre> | mv_daily_sales |
| Función / procedimiento | verbo + objeto | get_user_balance, calculate_order_total |
| Tipo enum | <entidad>_<atributo> | order_status |
| Valores del enum | snake_case minúscula | pending, paid, refunded |
| Rol de base de datos | <app>_<acceso> | shop_app_rw, shop_app_ro, shop_migrator |
Los valores del enum no se pueden eliminar sin recrear el tipo. Para conjuntos que cambian, prefiere text + check o una tabla de búsqueda.
Tipos y valores predeterminados
timestamptzpara instantes, predeterminadonow().CURRENT_TIMESTAMP AT TIME ZONE 'UTC'en una columnatimestamptzes un error.texten lugar devarchar(n)a menos que la longitud sea una regla de negocio; prefierecheck (char_length(col) <= n)para eso.jsonb, nuncajson.numericpara dinero; nuncareal,double precisionomoney.- Nuevas columnas de identidad usan
generated always as identity, noserial/bigserial. - Las claves primarias UUID usan v7: PostgreSQL 18
uuidv7(), .NET 9Guid.CreateVersion7(), paquete Nodeuuidv7(), Gogithub.com/google/uuiduuid.NewV7(). Las claves v4 aleatorias fragmentan los índices B-tree. gen_random_uuid()está integrado desde PostgreSQL 13;uuid-osspypgcryptono son necesarios para ello.citexto un índice únicolower(email)para unicidad sin distinción de mayúsculas y minúsculas.
Patrones de concurrencia y consultas
- Concurrencia optimista, EF Core + Npgsql: una propiedad
uintconfigurada conIsRowVersion()se asigna axmin. - Concurrencia optimista, Node y Go:
version integer not null default 0, actualizado conwhere id = $1 and version = $2yversion = version + 1; cero filas afectadas significa un conflicto. En sqlc usa:execrowspara obtener el número de filas afectadas. select ... for updateyfor update skip lockedsolo dentro de una transacción explícita; en autocommit el bloqueo se libera inmediatamente.- Bloqueos asesores:
pg_try_advisory_xact_lockdentro de una transacción.pg_advisory_locka nivel de sesión solo en una conexión dedicada mantenida abierta hasta desbloquear; con pooling, el bloqueo y desbloqueo pueden caer en diferentes conexiones. - Las listas grandes o infinitas usan paginación por conjunto de claves:
where (created_at, id) < ($1, $2) order by created_at desc, id desc limit $3.offsetsolo para listas pequeñas y acotadas. - Los permisos para tablas futuras necesitan
alter default privileges, no sologrant ... on all tables.
Configuración de ORM para snake_case
| Stack | Setup |
|---|---|
| EF Core + Npgsql | EFCore.NamingConventions, UseSnakeCaseNamingConvention() |
| Drizzle | casing: 'snake_case' |
| Prisma | @map en campos, @@map en modelos |
| TypeORM | SnakeNamingStrategy de typeorm-naming-strategies |
| Knex / Objection | knexSnakeCaseMappers() |
| Go + sqlc | ninguno; los nombres de Go se generan desde snake_case automáticamente |
Si un proyecto usa un ORM sin snake_case configurado, repórtalo como Debe corregirse antes de la primera migración; después de que existan migraciones, repórtalo como Recomendado con el costo de la migración.
Go + sqlc
sqlc es primero SQL: el esquema proviene de los archivos de migración, por lo que cada regla anterior se aplica directamente a ellos.
- Apunta
schemaensqlc.yamlal directorio de migraciones (se admiten formatos goose, golang-migrate, dbmate y atlas; las migraciones descendentes se ignoran). Nunca mantengas un archivo de esquema escrito a mano separado junto a las migraciones. - Las tablas en snake_case plural dan estructuras Go singulares:
order_items→OrderItem. Enumorder_status→ tipoOrderStatuscon constantesOrderStatusPending. - Usa
sql_package: pgx/v5. Sobrescribeuuid→github.com/google/uuid.UUIDy, si se desea,timestamptz→time.Timeen lugar de los envoltoriospgtype. - Corrige inicialismos con
rename(url: URL,ip: IP) en lugar de renombrar columnas. - Los nombres de las consultas son PascalCase verbo + objeto, con el tipo de resultado:
| Propósito | Ejemplo |
|---|---|
| Una fila | -- name: GetUserByEmail :one |
| Muchas filas | -- name: ListOrdersByCustomer :many |
| Insertar | -- name: CreateOrder :one + returning * |
| Actualizar | -- name: UpdateOrderStatus :execrows |
| Eliminar | -- name: DeleteSession :exec |
| Carga masiva | -- name: CreateOrderItems :copyfrom |
- Nombra los parámetros con
@nameosqlc.arg(name), ysqlc.narg(name)para los anulables; los posicionales$1producen campos Go del estiloColumn1. - Usa
sqlc.embed(table)para uniones en lugar de seleccionar columnas con alias ad-hoc. - Nunca uses
select *en consultas que deban sobrevivir a cambios de esquema, exceptoreturning *en escrituras.