sql >> Database teknologi >  >> RDS >> PostgreSQL

Mere SQL, mindre kode, med PostgreSQL

Med bare en lille smule justering og forbedring af dine Postgres SQL-forespørgsler kan du skære ned på mængden af ​​gentagne, fejltilbøjelige applikationskode, der kræves for at interface med din database. Oftere end ikke, forbedrer en sådan ændring også applikationskodens ydeevne.

Her er et par tips og tricks, der kan hjælpe din applikationskode med at outsource mere arbejde til PostgreSQL og gøre din applikation slankere og hurtigere.

Upsert

Siden Postgres v9.5 er det muligt at specificere, hvad der skal ske, når en indsættelse fejler på grund af en "konflikt". Konflikten kan enten være en overtrædelse af et unikt indeks (inklusive en primær nøgle) eller en hvilken som helst begrænsning (oprettet tidligere ved hjælp af CREATE CONSTRAINT).

Denne funktion kan bruges til at forenkle indsæt-eller-opdater applikationslogik i en enkelt SQL-sætning. For eksempel givet en tabel kv med nøgle og værdi kolonner, vil sætningen nedenfor indsætte en ny række (hvis tabellen ikke har en række med key='host') eller opdatere værdien (hvis tabellen har en række med key='host'):

OPRET TABEL kv (nøgle TEKST PRIMÆR NØGLE, værdi TEKST);INDSÆT I kv (nøgle, værdi)VÆRDIER ('vært', '10.0.10.1') PÅ KONFLIKT (nøgle) OPDATERE SET værdi=EXCLUDED .værdi; 

Bemærk, at kolonnen nøgle er den primære nøgle med en enkelt kolonne i tabellen og er angivet som konfliktsætningen. Hvis du har en primærnøgle med flere kolonner, skal du i stedet angive navnet på primærnøgleindekset her.

For avancerede eksempler, herunder angivelse af delvise indekser og begrænsninger, se Postgres-dokumenterne.

Indsæt .. returnerer

INSERT-sætningen kan også vende tilbage en eller flere rækker, som en SELECT-sætning. Det kan returnere værdier genereret af funktioner, nøgleord som current_timestamp og seriel /sekvens/identitetskolonner.

For eksempel er her en tabel med en autogenereret identitetskolonne og en kolonne, der indeholder tidsstemplet for oprettelsen af ​​rækken:

db=> OPRET TABEL t1 (id int GENERERET AF STANDARD SOM IDENTITET,db(> på timestamptz DEFAULT CURRENT_TIMESTAMP,db(> foo text); 

Vi kan bruge INSERT .. RETURNING-sætningen til kun at angive værdien for kolonnen foo , og lad Postgres returnere de værdier, den genererede for id og kolonner:

db=> INSERT INTO t1 (foo) VALUES ('first'), ('second') RETURNING id, at, foo; id | ved | foo----+------------------------------------------+-------- 1 | 14-01-2022 11:52:09.816787+01:00 | første 2 | 14-01-2022 11:52:09.816787+01:00 | sekund(2 rækker)INDSÆT 0 2 

Fra applikationskode skal du bruge de samme mønstre/API'er, som du ville bruge til at køre SELECT-sætninger og indlæse værdier (såsom executeQuery() i JDBC eller db.Query() i gang).

Her er et andet eksempel, denne har en automatisk genereret UUID:

OPRET TABEL t2 (id uuid PRIMÆR NØGLE, foo-tekst);INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id; 

I lighed med INSERT kan UPDATE- og DELETE-sætningerne også indeholde RETURNING-sætninger i Postgres. RETURNING-sætningen er en Postgres-udvidelse og ikke en del af SQL-standarden.

Alle i et sæt

Hvordan ville du oprette en WHERE-sætning ud fra applikationskoden, der skal matche en kolonnes værdi mod et sæt acceptable værdier? Når antallet af værdier er kendt på forhånd, er SQL'en statisk:

stmt =conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");stmt.setString(1, key[0]);stmt.setString(2, key[ 1]); 

Men hvad nu hvis antallet af nøgler ikke er 2, men kan være et hvilket som helst tal? Ville du konstruere SQL-sætningen dynamisk? En nemmere mulighed er at bruge Postgres-arrays:

VÆLG nøgle, værdi FRA kv HVOR nøgle =ENHVER(?) 

ANY-operatoren ovenfor tager et array som argument. Klausulen nøgle =ANY(?) vælger alle rækker, hvor værdien af ​​nøgle er et af elementerne i det medfølgende array. Hermed kan applikationskoden forenkles til:

stmt =conn.prepareStatement("SELECT key, value FROM kv WHERE key =ANY(?)");a =conn.createArrayOf("STRING", nøgler);stmt.setArray(1, a); 

Denne tilgang er mulig for et begrænset antal værdier, hvis du har mange værdier at matche med, kan du overveje andre muligheder som f.eks. at slutte sig til (midlertidige) tabeller eller materialiserede visninger.

Flytning af rækker mellem tabeller

Ja, du kan slette rækker fra en tabel og indsætte dem i en anden med en enkelt SQL-sætning! En hoved INSERT-sætning kan trække i rækkerne for at indsætte ved hjælp af en CTE, som ombryder en DELETE.

MED elementer SOM (SLET FRA todos_2021 WHERE NOT done RETURNING *)INSERT INTO todos_2021 SELECT * FROM items; 

At gøre det tilsvarende i applikationskode kan være meget omfattende, hvilket involverer at gemme hele resultatet af sletningen i hukommelsen og bruge det til at lave flere INSERT. Indrømmet, flytning af rækker er måske ikke et almindeligt eksempel, men hvis forretningslogikken kræver det, gør besparelserne på applikationshukommelse og database-rundture, som denne tilgang giver, det til den ideelle løsning.

Sættet af kolonner i kilde- og destinationstabellerne behøver ikke at være identiske, du kan selvfølgelig omarrangere, omarrangere og bruge funktioner til at manipulere værdierne i valg-/returlisterne.

Koalescer

Indlevering af NULL-værdier i applikationskoden kræver normalt ekstra trin. I Go, for eksempel, skal du bruge typer som sql.NullString; i Java/JDBC, funktioner som resultSet.wasNull() . Disse er besværlige og fejlbehæftede.

Hvis det er muligt at håndtere, f.eks. NULLs som tomme strenge, eller NULL-heltal som 0, i sammenhæng med en specifik forespørgsel, kan du bruge COALESCE-funktionen. COALESCE-funktionen kan omdanne NULL-værdier til en hvilken som helst specifik værdi. Overvej for eksempel denne forespørgsel:

SELECT invoice_num, COALESCE(shipping_address, '') FROM invoices WHERE EXTRACT(month FROM raised_on) =1 AND EXTRACT(year FROM raised_on) =2022 

som får fakturanumre og forsendelsesadresser på fakturaer rejst i januar 2022. Formentlig shipping_address er NULL, hvis varer ikke skal sendes fysisk. Hvis applikationskoden blot ønsker at vise en tom streng et eller andet sted i sådanne tilfælde, f.eks. er det nemmere blot at bruge COALESCE og fjerne NULL-håndteringskoden i applikationen.

Du kan også bruge andre strenge i stedet for en tom streng:

SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ... 

Du kan endda få den første ikke-NULL værdi fra en liste, eller bruge den angivne streng i stedet. For eksempel til enten at bruge faktureringsadressen eller leveringsadressen, kan du bruge:

VÆLG fakturanummer, COALESCE(faktureringsadresse, forsendelsesadresse, '* INGEN ADRESSE GIVEN *') ... 

Sag

CASE er en anden nyttig konstruktion til at håndtere ufuldkomne data fra det virkelige liv. Lad os sige i stedet for at have NULL i shipping_address for varer, der ikke kan sendes, har vores ikke-så-perfekte fakturaoprettelsessoftware sat "NOT-SPECIFIED". Du vil gerne knytte dette til en NULL eller en tom streng, når du læser dataene ind. Du kan bruge CASE:

-- map NOT-SPECIFIED til en tom strengSELECT invoice_num, CASE shipping_address WHEN 'NOT-SPECIFIED' THEN '' ELLES shipping_address ENDFROM invoices;-- samme resultat, forskellig syntaksSELECT invoice_num, CASE WHEN shipping_address ='NOT- SPECIFICERET' SÅ '' ELLES shipping_address ENDFROM fakturaer; 

CASE har en klodset syntaks, men ligner funktionelt set switch-case-udsagn i C-lignende sprog. Her er et andet eksempel:

VÆLG fakturanummer, TILFÆLDE NÅR shipping_address ER NULL, SÅ 'SENDES IKKE' NÅR faktureringsadresse =shipping_address SÅ 'SENDING TIL BETALER' ELLERS 'SENDER TIL ' || shipping_address ENDFROM fakturaer; 

Vælg .. union

Data fra to (eller flere) separate SELECT-sætninger kan kombineres ved hjælp af UNION. Hvis du for eksempel har to tabeller, en med nuværende brugere og en slettet, kan du forespørge dem begge på samme tid her:

SELECT id, name, address, FALSE AS is_deleted FROM users WHERE email =?UNIONSELECT id, name, address, TRUE AS is_deleted FROM deleted_users WHERE email =? 

De to forespørgsler skal have den samme udvalgte liste, dvs. skal returnere det samme antal og samme type kolonner.

UNION fjerner også dubletter. Kun unikke rækker returneres. Hvis du hellere vil beholde duplikerede rækker, skal du bruge "UNION ALL" i stedet for UNION.

Som kompliment til UNION er der også INTERSECT og UNDTAGET, se PostgreSQL-dokumenterne for mere info.

Vælg .. distinct on

Duplikerede rækker returneret af en SELECT kan kombineres (det vil sige, at kun unikke rækker returneres) ved at tilføje DISTINCT nøgleordet efter SELECT. Selvom dette er standard SQL, tilbyder Postgres en udvidelse, "DISTINCT ON". Det er lidt vanskeligt at bruge, men i praksis er det ofte den mest kortfattede måde at få de resultater, du har brug for.

Overvej en kunde tabel med en række pr. kunde og et køb tabel med én række pr. køb foretaget af (nogle) kunder. Forespørgslen nedenfor returnerer alle kunder sammen med hvert af deres køb:

 VÆLG C.id, P.at FRA kunder C VENSTRE YDRE JOIN køb P ON P.customer_id =C.id BESTILLING AF C.id ASC, P.at ASC; 

Hver kunderække gentages for hvert køb, de har foretaget. Hvad hvis vi kun ønsker at returnere det første køb af en kunde? Vi ønsker som udgangspunkt at sortere rækkerne efter kunde, gruppere rækkerne efter kunde, inden for hver gruppe sortere rækkerne efter købstid og til sidst kun returnere den første række fra hver gruppe. Det er faktisk kortere at skrive det i SQL med DISTINCT ON:

VÆLG DISTINCT ON (C.id) C.id, P.at FRA kunder C VENSTRE YDRE JOIN køb P ON P.customer_id =C.id BESTILLING AF C.id ASC, P.at ASC;

Den tilføjede "DISTINCT ON (C.id)"-klausul gør præcis, hvad der blev beskrevet ovenfor. Det er meget arbejde med blot nogle få ekstra bogstaver!

Brug af tal i rækkefølge efter klausul

Overvej at hente en liste over kundenavne og områdenummeret for deres telefonnumre fra en tabel. Vi antager, at amerikanske telefonnumre er gemt formateret som (123) 456-7890 . For andre lande siger vi bare "NON-US" som områdenummer.

SELECT last_name, first_name, CASE country_code WHEN 'US' THEN substr(phone, 2, 3) ELSE 'NON-US' ENDFROM kunder; 

Det er alt i orden, og vi har også CASE-konstruktionen, men hvad nu hvis vi skal sortere det efter områdenummeret nu?

Dette virker:

SELECT last_name, first_name, CASE country_code WHEN 'US' THEN substr(phone, 2, 3) ELSE 'NON-US' ENDFROM customersORDER BY CASE country_code WHEN 'US' THEN substr(phone, 2, 3) ELSE 'NON-US' END ASC; 

Men øvh! At gentage sagsklausulen er grimt og fejlbehæftet. Vi kunne skrive en gemt funktion, der tager landekode og telefon og returnerer områdenummeret, men der er faktisk en bedre mulighed:

SELECT last_name, first_name, CASE country_code WHEN 'US' THEN substr(phone, 2, 3) ELSE 'NON-US' ENDFROM customersORDER BY 3 ASC; 

"ORDER BY 3" siger rækkefølge efter 3. felt! Du skal huske at opdatere nummeret, når du omarrangerer den valgte liste, men det er normalt det værd.


  1. Openshift og net-ssh inkompatibilitet? (2.9.3-beta1 vs. 2.9.2)

  2. Sådan minimerer du RPO for dine PostgreSQL-databaser ved hjælp af punkt-i-tidsgendannelse

  3. 25 Microsoft Access-genveje for at spare tid i tabeller i dataarkvisning

  4. Hvad er den bedste løsning til pooling af databaseforbindelser i python?