PostgreSQL-Konventionen
Anwendung
Es gibt zwei Modi. Entscheiden Sie, welcher gilt, bevor Sie etwas schreiben.
Modus A — Sie wählen die Namen. Der Benutzer hat nach einem Schema, einer Entität oder einer Migration gefragt und keine Namen vorgegeben. Wenden Sie die untenstehenden Konventionen direkt an.
Modus B — Die Namen existieren bereits oder wurden vorgegeben. Der Benutzer hat Namen vorgegeben, oder vorhandener Code, Migrationen oder die Datenbank enthalten Namen, die von diesen Konventionen abweichen. Benennen Sie niemals stillschweigend um. Melden Sie die Abweichungen und fragen Sie nach.
Meldeformat in der Sprache des Benutzers, bevor Änderungen vorgenommen werden:
Naming:
Must fix (Muss korrigiert werden)
- `"UserProfile"` (Tabelle) → `user_profiles` — zitierte PascalCase-Namen müssen in jeder Abfrage für immer zitiert werden
- `created` timestamp DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') → `created_at timestamptz DEFAULT now()` — Der Wert verschiebt sich, wenn die Sitzung TimeZone nicht UTC ist
Empfohlen
- `users.user_email` → `users.email` — Der Tabellenname liefert bereits den Kontext
Stellen Sie dann eine Frage: Alle anwenden, nur "Must fix" anwenden oder so belassen. Warten Sie auf die Antwort.
Regeln für die Meldung:
- Must fix (Muss korrigiert werden) = verursacht Fehler, bricht Abfragen oder Tooling: zitierte gemischte Bezeichner, reservierte Wörter, Namen über 63 Bytes,
timestampohne Zeitzone, falsche Zeitstandards,json/float/moneyfür Daten, diejsonb/numericbenötigen, fehlender FK-Index, Sperren außerhalb einer Transaktion. - Recommended (Empfohlen) = Stil und Konsistenz.
- Melden Sie nur Objekte, die von der aktuellen Aufgabe betroffen sind. Wenn Sie andere Abweichungen an anderer Stelle bemerkt haben, fügen Sie eine Zeile hinzu: "Auch N ähnliche Abweichungen in — auflisten?"
- Objekte in angewendeten Migrationen oder einer Live-Datenbank: Eine Umbenennung ist eine Breaking Change. Schlagen Sie es nur vor; wenn genehmigt, implementieren Sie es als neue Migration. Bearbeiten Sie niemals eine angewendete Migration.
- Wenn die Codebasis konsistent eine andere Konvention verwendet, gewinnt diese. Erwähnen Sie den Unterschied einmal, folgen Sie dann der Projektkonvention, ohne jedes Vorkommen zu markieren.
- Namen, die von der ORM-Namenskonvention erzeugt werden (z. B. EFCore.NamingConventions), werden akzeptiert.
- Sobald der Benutzer einen nicht konformen Namen bestätigt hat, verwenden Sie ihn und erwähnen Sie ihn in dieser Sitzung nicht erneut.
Allgemeine Regeln
snake_case, Kleinbuchstaben, niemals zitierte Bezeichner.- Maximal 63 Bytes pro Bezeichner; längere Namen werden stillschweigend abgeschnitten. Sinnvoll kürzen, nicht durch Abschneiden.
- Tabellen im Plural:
users,orders,order_items. - Spalten im Singular, kein Tabellenpräfix:
users.email, nichtusers.user_email. - Ganze Wörter, außer gebräuchliche Abkürzungen:
id,url,ip,sku. - Verwenden Sie niemals reservierte oder mehrdeutige Wörter als Namen:
user,order,group,table,limit,offset,desc,end,date,time,timestamp. - Keine Typ- oder Objektpräfixe: kein
tbl_users,str_name. - Booleans sind positiv:
is_deleted, niemalsis_not_deleted.
Standardspalten
| Zweck | Name | Typ |
|---|---|---|
| Primärschlüssel | id | bigint generated always as identity oder uuid v7 |
| Erstellt / Aktualisiert | created_at, updated_at | timestamptz not null default now() |
| Soft Delete | deleted_at | timestamptz null |
| Audit | created_by, updated_by | FK zu users.id |
| Flags | is_active, is_verified, has_children, can_edit | boolean not null default false |
| Status | status | text + Check, Enum oder Lookup-Tabelle |
| Nur Datum | birth_date, due_on | date |
| Gültigkeit | expires_at, valid_from, valid_to | timestamptz |
| Geld | amount, unit_price, total_amount + currency_code | numeric(19,4) + char(3), oder amount_minor bigint |
| Zähler | items_count, retry_count | integer / bigint |
| Sortierung | sort_order | integer |
| Geschäftsschlüssel | code, slug, external_id | text + unique |
| Mandant | tenant_id | FK |
| Nebenläufigkeit | version oder System xmin | integer / xid |
| Freie Daten | metadata, payload, settings | jsonb |
Suffixe haben eine Bedeutung: _at Zeitpunkt, _on/_date Kalenderdatum, _id Referenz, _count Zähler, _url, _path,
_hash.
Schlüssel und Constraints
| Objekt | Muster | Beispiel |
|---|---|---|
| Primärschlüssel | pk_<table> | pk_orders |
| Fremdschlüssel | fk_<table>_<ref_table>_<column> | fk_orders_customers_customer_id |
| Unique-Constraint | uq_<table>_<cols> | uq_users_email |
| Alternativschlüssel (EF Core) | ak_<table>_<cols> | ak_products_sku |
| Check | ck_<table>_<rule> | ck_orders_amount_positive |
| Ausschluss | ex_<table>_<cols> | ex_bookings_room_id_period |
| Index | ix_<table>_<cols> | ix_orders_customer_id_created_at |
| Unique-Index | ux_<table>_<cols> | ux_users_email |
| Partieller Index | ix_<table>_<cols>_<condition> | ix_users_email_not_deleted |
| Index nach Methode | ix_<table>_<col>_<method> | ix_products_tags_gin |
| Trigger | trg_<table>_<action> | trg_orders_set_updated_at |
| Trigger-Funktion | Verb + Objekt | set_updated_at() |
| Sequenz | PostgreSQL-Standard | orders_id_seq |
- FK-Spalte:
<referenzierte Tabelle, Singular>_id, oder ein Rollenname, wenn die Rolle wichtig ist:author_id,sender_id,recipient_id,parent_id. - Das FK-Muster enthält die Spalte, sodass mehrere FKs zu einer Tabelle eindeutig bleiben:
fk_messages_users_sender_id,fk_messages_users_recipient_id. - PostgreSQL indiziert FK-Spalten nicht. Jeder FK benötigt einen Index, es sei denn, ein zusammengesetzter Index beginnt bereits damit. EF Core erstellt diese per Konvention; Drizzle, Prisma, TypeORM, Knex und sqlc tun dies nicht.
Beziehungen
| Art | Form | Beispiel |
|---|---|---|
| Eins-zu-Viele | FK auf dem Kind | order_items.order_id → orders.id |
| Eins-zu-Eins | FK + Unique, oder PK = FK | user_profiles.user_id |
| Viele-zu-Viele | Junction <a>_<b>, zusammengesetzter PK | user_roles (user_id, role_id), pk_user_roles |
| Viele-zu-Viele mit Attributen | Aussagekräftiger Entitätsname | memberships, enrollments |
| Selbstreferenz | parent_id oder Rolle | categories.parent_id, employees.manager_id |
Polymorphe Paare (commentable_type + commentable_id) können keine FKs haben. Bevorzugen Sie separate nullable FK-Spalten mit einem
Check, dass genau eine gesetzt ist, oder separate Tabellen.
Schemata, Views, Funktionen, Typen
| Objekt | Muster | Beispiel |
|---|---|---|
| Schema | Domäne oder Modul | auth, billing, catalog, audit |
| View | v_<name> | v_active_users |
| Materialisierte View | mv_<name> | mv_daily_sales |
| Funktion / Prozedur | Verb + Objekt | get_user_balance, calculate_order_total |
| Enum-Typ | <entity>_<attribute> | order_status |
| Enum-Werte | snake_case (Kleinbuchstaben) | pending, paid, refunded |
| Datenbank-Rolle | <app>_<access> | shop_app_rw, shop_app_ro, shop_migrator |
Enum-Werte können nicht gelöscht werden, ohne den Typ neu zu erstellen. Für Sätze, die sich ändern, bevorzugen Sie text + Check oder eine Lookup-
Tabelle.
Typen und Standardwerte
timestamptzfür Zeitpunkte, Standardnow().CURRENT_TIMESTAMP AT TIME ZONE 'UTC'auf einertimestamptz-Spalte ist ein Fehler.textanstelle vonvarchar(n), es sei denn, die Länge ist eine Geschäftsregel; bevorzugen Sie dafürcheck (char_length(col) <= n).jsonb, niemalsjson.numericfür Geld; niemalsreal,double precisionodermoney.- Neue Identity-Spalten verwenden
generated always as identity, nichtserial/bigserial. - UUID-Primärschlüssel verwenden v7: PostgreSQL 18
uuidv7(), .NET 9Guid.CreateVersion7(), Nodeuuid-Paketv7(), Gogithub.com/google/uuiduuid.NewV7(). Zufällige v4-Schlüssel fragmentieren B-Tree-Indizes. gen_random_uuid()ist seit PostgreSQL 13 eingebaut;uuid-osspundpgcryptowerden dafür nicht benötigt.citextoder einlower(email)Unique-Index für Groß-/Kleinschreibungs-unabhängige Eindeutigkeit.
Nebenläufigkeit und Abfragemuster
- Optimistische Nebenläufigkeit, EF Core + Npgsql: Eine
uint-Eigenschaft, konfiguriert mitIsRowVersion(), wird aufxminabgebildet. - Optimistische Nebenläufigkeit, Node und Go:
version integer not null default 0, aktualisiert mitwhere id = $1 and version = $2undversion = version + 1; null betroffene Zeilen bedeuten einen Konflikt. Verwenden Sie in sqlc:execrows, um die Anzahl der betroffenen Zeilen zu erhalten. select ... for updateundfor update skip lockednur innerhalb einer expliziten Transaktion; im Autocommit-Modus wird die Sperre sofort freigegeben.- Advisory Locks:
pg_try_advisory_xact_lockinnerhalb einer Transaktion. Session-Levelpg_advisory_locknur auf einer dedizierten Verbindung, die bis zum Entsperren offen gehalten wird; bei Pooling können Sperre und Entsperren auf verschiedenen Verbindungen landen. - Große oder unendliche Listen verwenden Keyset-Paginierung:
where (created_at, id) < ($1, $2) order by created_at desc, id desc limit $3.offsetnur für kleine begrenzte Listen. - Grants für zukünftige Tabellen benötigen
alter default privileges, nicht nurgrant ... on all tables.
ORM-Einrichtung für snake_case
| Stack | Einrichtung |
|---|---|
| EF Core + Npgsql | EFCore.NamingConventions, UseSnakeCaseNamingConvention() |
| Drizzle | casing: 'snake_case' |
| Prisma | @map auf Feldern, @@map auf Modellen |
| TypeORM | SnakeNamingStrategy aus typeorm-naming-strategies |
| Knex / Objection | knexSnakeCaseMappers() |
| Go + sqlc | keine; Go-Namen werden automatisch aus snake_case generiert |
Wenn ein Projekt ein ORM ohne snake_case-Konfiguration verwendet, melden Sie es als Must fix vor der ersten Migration; nachdem Migrationen existieren, melden Sie es als Recommended mit den Migrationskosten.
Go + sqlc
sqlc ist SQL-first: Das Schema stammt aus Migrationsdateien, daher gelten alle obigen Regeln direkt für diese.
- Richten Sie
schemainsqlc.yamlauf das Migrationsverzeichnis aus (goose, golang-migrate, dbmate und atlas-Formate werden unterstützt; Down-Migrationen werden ignoriert). Halten Sie niemals eine separate handgeschriebene Schema-Datei neben Migrationen. - Plural-Snake_case-Tabellen ergeben singuläre Go-Strukturen:
order_items→OrderItem. Enumorder_status→ TypOrderStatusmit KonstantenOrderStatusPending. - Verwenden Sie
sql_package: pgx/v5. Überschreiben Sieuuid→github.com/google/uuid.UUIDund, falls gewünscht,timestamptz→time.Timeanstelle derpgtype-Wrapper. - Beheben Sie Initialismen mit
rename(url: URL,ip: IP) anstatt Spalten umzubenennen. - Abfragenamen sind PascalCase Verb + Objekt, mit dem Ergebnis-Typ:
| Zweck | Beispiel |
|---|---|
| Eine Zeile | -- name: GetUserByEmail :one |
| Viele Zeilen | -- name: ListOrdersByCustomer :many |
| Einfügen | -- name: CreateOrder :one + returning * |
| Aktualisieren | -- name: UpdateOrderStatus :execrows |
| Löschen | -- name: DeleteSession :exec |
| Bulk-Laden | -- name: CreateOrderItems :copyfrom |
- Benennen Sie Parameter mit
@nameodersqlc.arg(name)undsqlc.narg(name)für nullable; positionsbasiertes$1erzeugtColumn1-artige Go-Felder. - Verwenden Sie
sqlc.embed(table)für Joins anstatt Spalten mit Ad-hoc-Aliasen auszuwählen. - Verwenden Sie niemals
select *in Abfragen, die Schemaänderungen überstehen sollen, außerreturning *bei Schreibvorgängen.