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

Flere af mine foretrukne PostgreSQL-forespørgsler - og hvorfor de også betyder noget

I et tidligere blogindlæg Mine foretrukne PostgreSQL-forespørgsler og hvorfor de betyder noget, besøgte jeg interessante forespørgsler, der var meningsfulde for mig, mens jeg lærer, udvikler og vokser til en SQL-udviklerrolle.

En af disse, især en OPDATERING i flere rækker med et enkelt CASE-udtryk, udløste en interessant samtale på Hacker News.

I dette blogindlæg vil jeg observere sammenligninger mellem den pågældende forespørgsel og en, der involverer flere enkelte UPDATE-udsagn. For godt eller skidt.

Maskin-/miljøspecifikationer:

  • Intel(R) Core(TM) i5-6200U CPU @ 2,30GHz
  • 8 GB RAM
  • 1 TB lager
  • Xubuntu Linux 16.04.3 LTS (Xenial Xerus)
  • PostgreSQL 10.4

Bemærk:Til at starte med oprettede jeg en "iscenesættelse"-tabel med alle TEXT-type kolonner for at få dataene indlæst.

Eksempeldatasættet, jeg bruger, findes på dette link her.

Men husk på, at selve dataene bruges i dette eksempel, fordi det er et sæt af anstændig størrelse med flere kolonner. Enhver "analyse" eller OPDATERINGER/INDSÆTNINGER til dette datasæt afspejler ikke faktiske GPS/GIS-operationer i "virkelig verden" og er ikke tiltænkt som sådan.

location=# \d data_staging; Tabel "public.data_staging" Kolonne | Skriv | Samling | Nullbar | Standard ---------------+---------+--------------+ ---------- segment_num | tekst | | | point_seg_num | tekst | | | breddegrad | tekst | | | længdegrad | tekst | | | nad_year_cd | tekst | | | projekt_kode | tekst | | | x_cord_loc | tekst | | | y_cord_loc | tekst | | | last_rev_date | tekst | | | version_dato | tekst | | | asbuilt_flag | tekst | | | location=# SELECT COUNT(*) FROM data_staging;count--------546895(1 row) 

Vi har omkring en halv million rækker af data i denne tabel.

Til denne første sammenligning vil jeg OPDATERE kolonnen proj_code.

Her er en undersøgende forespørgsel til at bestemme dens aktuelle værdier:

location=# SELECT DISTINCT proj_code FROM data_staging;proj_code-----"70""""72""71""51""15""16"(7 rows ) 

Jeg bruger trim til at fjerne anførselstegn fra værdierne og cast til en INT og bestemme, hvor mange rækker der findes for hver individuel værdi:

Lad os bruge en CTE til det, så VÆLG fra den:

location=# WITH cleaned_nums AS (SELECT NULLIF(trim(trim(både '"' FROM proj_code), '') AS p_code FROM data_staging)SELECT COUNT(*),CASEWHEN p_code::int =70 THEN '70 'HVORNÅR p_code::int =72 SÅ '72'HÅR p_code::int =71 SÅ '71'HÅR p_kode::int =51 SÅ '51'HVORNÅR p_kode::int =15 SÅ '15'NÅR p_kode::int =16 SÅ '16'ELSE '00'END AS proj_code_numFROM cleaned_numsGROUP BY p_codeORDER BY p_code DESC;count | proj_code_num--------+-----------------353087 | 0139057 | 7225460 | 713254 | 701 | 5112648 | 1613388 | 15(7 rækker) 

Inden jeg kører disse test, vil jeg gå videre og ÆNDRE proj_code-kolonnen for at skrive INTEGER:

BEGIN;ALTER TABLE data_staging ALTER COLUMN proj_code SET DATA TYPE INTEGER USING NULLIF(trim(begge '"' FROM proj_code), '')::INTEGER;SAVEPOINT my_save;COMMIT; 

Og ryd den NULL-kolonneværdi (som er repræsenteret af ELSE '00' i det undersøgende CASE-udtryk ovenfor), og indstil den til et vilkårligt tal, 10, med denne OPDATERING:

OPDATERING data_stagingSET proj_code =10WHERE proj_code IS NULL; 

Nu har alle proj_code-kolonner en INTEGER-værdi.

Lad os gå videre og køre et enkelt CASE-udtryk, der opdaterer alle proj_code-kolonnens værdier og se, hvad timingen rapporterer. Jeg placerer alle kommandoerne i en .sql-kildefil for at lette håndteringen.

Her er filindholdet:

BEGIN;\timing onUPDATE data_stagingSET proj_code =(CASE proj_codeWHEN 72 THEN 7272WHEN 71 THEN 7171WHEN 15 THEN 1515WHEN 51 THEN 5151WHEN 70 THEN 7010WHEN 1WHEN 7010WHEN 1,WHEN 7010WHEN 6 , 70, 10, 16); SAVEPOINT my_save; 

Lad os køre denne fil og tjekke, hvad timingen rapporterer:

location=# \i /case_insert.sqlBEGINTid:0,265 msTiming er aktiveret.OPDATERING 546895Tid:6779.596 ms (00:06.780)SAVEPOINTTid:0.300 ms 

Lidt over en halv million rækker på 6+ sekunder.

Her er de afspejlede ændringer i tabellen indtil videre:

location=# SELECT DISTINCT proj_code FROM data_staging;proj_code------7070161610107171151572725151(7 rows) 

Jeg RULLER TILBAGE (ikke vist) disse ændringer, så jeg kan køre individuelle INSERT-sætninger for også at teste dem.

Nedenfor afspejler ændringerne af .sql-kildefilen for denne serie af sammenligninger:

BEGIN;\timing onUPDATE data_stagingSET proj_code =7222WHERE proj_code =72;OPDATERE data_stagingSET proj_code =7171WHERE proj_code =71;UPDATE data_stagingSET proj_code =1515WHERE proj_SETUP_code_code =1stWHERE projUPSET_DATO_codej_5; 7070WHERE proj_code =70;UPDATE data_stagingSET proj_code =1010WHERE proj_code =10;UPDATE data_stagingSET proj_code =1616WHERE proj_code =16;SAVEPOINT my_save; 

Og disse resultater,

meget )OPDATERING 12648Tid:321,223 msSAVEPOINTTid:0,108 ms

Lad os tjekke værdierne:

location=# SELECT DISTINCT proj_code FROM data_staging;proj_code-----------7222161670701010717115155151(7 rows) 

Og timingen (Bemærk:Jeg vil regne ud i en forespørgsel, da \timing ikke rapporterede hele sekunder denne kørsel):

placering=# VÆLG runde((795.610 + 116.268 + 239.007 + 72.699 + 162.199 + 1987.857 + 321.223) / 1000, 3) AS sekunder; 695-1. sekunder  

De enkelte INSERT'er tog omkring halvdelen af ​​tiden som den enkelte CASE.

Denne første test omfattede hele tabellen med alle kolonner. Jeg er nysgerrig efter eventuelle forskelle i en tabel med det samme antal rækker, men færre kolonner, derfor den næste serie af tests.

Jeg opretter en tabel med 2 kolonner (sammensat af en SERIAL datatype for PRIMÆR NØGLE og et HELTAL for kolonnen proj_kode) og flytter over dataene:

location=# CREATE TABLE proj_nums(n_id SERIAL PRIMARY KEY, proj_code INTEGER);CREATE TABLElocation=# INSERT INTO proj_nums(proj_code) SELECT proj_code FROM data_staging;INSERT 0 546895 

(Bemærk:SQL-kommandoer fra det første sæt af operationer bruges med de(n) passende modifikation(er). Jeg udelader dem her for korthed og visning på skærmen )

Jeg kører først det enkelte CASE-udtryk:

location=# \i /case_insert.sqlBEGINTiming er aktiveret.OPDATERING 546895Tid:4355.332 ms (00:04.355)SAVEPOINTTid:0.137 ms 

Og så er den enkelte OPDATERING:

  placering =# \ i /case_insert.sqlbEintime:0.282 MStiming er på.Update 139057TID:1042.133 MS (00:01.042) Opdatering 25460TID:123.337 MSUPDATE 13388TIME:212.698 MSUPDATE 1TIME:43.107 MSPDATEPDATE 324 2787.295 ms (00:02.787) Opdatering 12648TID:99.813 MSSAVEPOINTTIME:0.059 MSLocation =# Select Round ((1042.133 + 123.337 + 212.698 + 43.107 + 52.669 + 2787.295 + 99.813) / 1000, 3) som en sekunders; ---4.361(1 række) 

Timingen er noget jævn mellem begge sæt operationer på bordet med kun 2 kolonner.

Jeg vil sige, at det er lidt nemmere at skrive CASE-udtrykket, men ikke nødvendigvis det bedste valg ved alle lejligheder. Som med det, der blev anført i nogle af kommentarerne i Hacker News-tråden, der refereres til ovenfor, afhænger det normalt "bare" af mange faktorer, som måske eller måske ikke er det optimale valg.

Jeg er klar over, at disse test i bedste fald er subjektive. En af dem på en tabel med 11 kolonner, mens den anden kun havde 2 kolonner, som begge var af en taldatatype.

CASE-udtrykket for opdateringer af flere rækker er stadig et af mine yndlingsforespørgsler, hvis det kun er for lette at skrive i et kontrolleret miljø, hvor mange individuelle UPDATE-forespørgsler er det andet alternativ.

Jeg kan dog se nu, hvor det ikke altid er det optimale valg, da jeg fortsætter med at vokse og lære.

Som det gamle ordsprog siger, "et halvt dusin i den ene hånd, 6 i den anden ."

En ekstra favoritforespørgsel - Brug af PLpgSQL-CURSOR'er

Jeg er begyndt at gemme og spore alle mine træningsstatistikker (stivandring) med PostgreSQL på min lokale udviklingsmaskine. Der er flere tabeller involveret, som med enhver normaliseret database.

Ved udgangen af ​​måneder vil jeg dog gerne gemme statistik for specifikke kolonner i deres egen, separate tabel.

Her er den 'månedlige' tabel, jeg vil bruge:

fitness=> \d hiking_month_total; Tabel "public.hiking_month_total" Kolonne | Skriv | Samling | Nullbar | Standard ------------------+------------------------+------ -----+----------+--------- dag_vandret | dato | | | kalorier_forbrændte | numerisk(4,1) | | | miles | numerisk(4,2) | | | varighed | tid uden tidszone | | | tempo | numerisk(2,1) | | | trail_hiked | tekst | | | sko_slidte | tekst | | | 

Jeg vil koncentrere mig om majs resultater med denne SELECT-forespørgsel:

fitness=> VÆLG hs.day_walked, hs.cal_burned, hs.miles_walked, hs.duration, hs.mph, tr.name, sb.name_brandfitness-> FRA hiking_stats AS hsfitness-> INNER JOIN hiking_trail AS htfitness -> PÅ hs.hike_id =ht.th_idfitness-> INNER JOIN trail_route AS trfitness-> PÅ ht.tr_id =tr.trail_idfitness-> INNER JOIN shoe_brand AS sbfitness-> PÅ hs.shoe_id =sb.shoe_idfitness-> FRA hs.day_walked) =5fitness-> BESTIL EFTER hs.day_walked ASC; 

Og her er 3 eksempelrækker returneret fra den forespørgsel:

dag_gået | cal_burned | miles_walked | varighed | mph | navn | name_brand ---------------------------------+-------- --+-----+------------------------+---------------- -----------------------2018-05-02 | 311,2 | 3,27 | 00:57:13 | 3.4 | Træsti-forlænget | New Balance Trail Runners-All Terrain2018-05-03 | 320,8 | 3,38 | 00:58:59 | 3.4 | Sandy Trail-Drive | New Balance Trail Runners-All Terrain2018-05-04 | 291,3 | 3,01 | 00:53:33 | 3.4 | Hus-strømledningsrute | Keen Koven WP(keen-dry)(3 rækker) 

Sandt at sige kan jeg udfylde tabellen mål hiking_month_total ved at bruge ovenstående SELECT-forespørgsel i en INSERT-sætning.

Men hvor er det sjove i det?

Jeg giver afkald på kedsomhed for en PLpgSQL-funktion med en CURSOR i stedet.

Jeg fandt på denne funktion til at udføre INSERT med en CURSOR:

CREATE OR REPLACE function monthly_total_stats()RETURNS voidAS $month_stats$DECLAREv_day_walked date;v_cal_burned numeric(4, 1);v_miles_walked numeric(4, 2);v_duration time without time zone;v_mph,numeric(2,mph);v_name text;v_name_brand text;v_cur CURSOR for SELECT hs.day_walked, hs.cal_burned, hs.miles_walked, hs.duration, hs.mph, tr.name, sb.name_brandFROM hiking_stats AS hsINNER JOIN hiking_trail AS ht. .th_idINNER JOIN trail_route AS trON ht.tr_id =tr.trail_idINNER JOIN shoe_brand AS sbON hs.shoe_id =sb.shoe_idWHERE ekstrakt(month FROM hs.day_walked) =5ORDER BY hs.day_walked v_curF>OPLOC; INTO v_day_walked, v_cal_burned, v_miles_walked, v_duration, v_mph, v_name, v_name_brand;EXIT WHEN NOT FOUND;INSERT INTO hiking_month_total(day_hiked, calories_burned, miles,varighed, tempo, trail_hiked_day,vm_walked,vm_walked,vm v_name, v_name_brand);END LOOP get_stats;CLOSE v_cur; END;$month_stats$ LANGUAGE PLpgSQL; 

Lad os kalde funktionen monthly_total_stats() for at udføre INSERT:

fitness=> SELECT monthly_total_stats();monthly_total_stats--------------------(1 række) 

Da funktionen er defineret RETURNS void, kan vi se, at ingen værdi returneres til den, der ringer.

På nuværende tidspunkt er jeg ikke specifikt interesseret i nogen returværdier,

kun at funktionen udfører den definerede operation og udfylder tabellen hiking_month_total.

Jeg forespørger efter en optælling af poster i måltabellen og bekræfter, at den har data:

fitness=> VÆLG ANTAL(*) FRA hiking_month_total;count-------25(1 række) 

Funktionen monthly_total_stats() virker, men måske en bedre brug for en CURSOR er at scrolle gennem et stort antal poster. Måske et bord med omkring en halv million poster?

Denne næste CURSOR er bundet til en forespørgsel rettet mod data_stage-tabellen fra rækken af ​​sammenligninger i afsnittet ovenfor:

OPRET ELLER ERSTAT FUNKTION location_curs()RETURNER refcursorAS $location$DECLAREv_cur refcursor;BEGINÅBN v_cur for SELECT segment_num, latitude, longitude, proj_code, asbuilt_flag FROM data_staging;RETURN v_cur;END;$location PL LANGUL$; 

Derefter, for at bruge denne CURSOR, skal du operere inden for en TRANSAKTION (påpeget i dokumentationen her).

location=# BEGIN;BEGINlocation=# SELECT location_curs();location_curs --------------------(1 række) 

Så hvad kan du gøre med denne ""?

Her er blot et par ting:

Vi kan returnere den første række fra CURSOREN ved at bruge enten første eller ABSOLUTE 1:

location=# FETCH first FROM "";segment_num | breddegrad | længdegrad | projekt_kode | asbuilt_flag -------------+------------------------+---------------- ---+-----------+--------------" 3571" | " 29.0202942600" | " -90.2908612800" | 72 | "Y"(1 række)placering=# FETCH ABSOLUTE 1 FRA "";segment_num | breddegrad | længdegrad | projekt_kode | asbuilt_flag -------------+------------------------+---------------- ---+-----------+--------------" 3571" | " 29.0202942600" | " -90.2908612800" | 72 | "Y"(1 række) 

Vil du have en række næsten halvvejs gennem resultatsættet? (Forudsat at vi ved, at en anslået halv million rækker er bundet til MARKEREN.)

Kan du være så 'specifik' med en CURSOR?

Ja.

Vi kan placere og HENTE værdierne for posten i række 234888 (bare et tilfældigt tal, jeg valgte):

location=# FETCH ABSOLUTE 234888 FRA "";segment_num | breddegrad | længdegrad | projekt_kode | asbuilt_flag -------------+------------------------+---------------- ---+-----------+--------------" 11261" | " 28.1159541400" | " -90.7778003500" | 10 | "Y"(1 række) 

Når vi først er placeret der, kan vi flytte CURSOR 'et tilbage':

location=# HENT TILBAGE FRA "";segment_num | breddegrad | længdegrad | projekt_kode | asbuilt_flag -------------+------------------------+---------------- ---+-----------+--------------" 11261" | " 28.1159358200" | " -90.7778242300" | 10 | "Y"(1 række) 

Hvilket er det samme som:

location=# FETCH ABSOLUTE 234887 FRA "";segment_num | breddegrad | længdegrad | projekt_kode | asbuilt_flag -------------+------------------------+---------------- ---+-----------+--------------" 11261" | " 28.1159358200" | " -90.7778242300" | 10 | "Y"(1 række) 

Så kan vi flytte MARKøren tilbage til ABSOLUTE 234888 med:

location=# HENT FRAM FRA "";segment_num | breddegrad | længdegrad | projekt_kode | asbuilt_flag -------------+------------------------+---------------- ---+-----------+--------------" 11261" | " 28.1159541400" | " -90.7778003500" | 10 | "Y"(1 række) 

Handy Tip:For at flytte MARKøren skal du bruge MOVE i stedet for FETCH, hvis du ikke har brug for værdierne fra den række.

Se denne passage fra dokumentationen:

"MOVE flytter en markør uden at hente nogen data. MOVE fungerer nøjagtigt som FETCH-kommandoen, bortset fra at den kun placerer markøren og ikke returnerer rækker."

Navnet "" er generisk og kan faktisk "navngives" i stedet for.

Jeg vil gense mine fitnessstatistikdata for at skrive en funktion og navngive CURSOR'en sammen med en potentiel "virkelig verden" brugssag.

CURSOR'en vil målrette mod denne ekstra tabel, som gemmer resultater ikke begrænset til maj måned (dybest set alt, hvad jeg har indsamlet indtil videre) som i det forrige eksempel:

fitness=> OPRET TABEL cp_hiking_total AS SELECT * FRA hiking_month_total UDEN DATA; OPRET TABEL SOM 

Udfyld den derefter med data:

fitness=> INSERT INTO cp_hiking_total VÆLG hs.day_walked, hs.cal_burned, hs.miles_walked, hs.duration, hs.mph, tr.name, sb.name_brandFROM hiking_stats AS hsINNER JOIN hiking_trail AS hs.hike_id =ht.th_idINNER JOIN trail_route AS trON ht.tr_id =tr.trail_idINNER JOIN shoe_brand AS sbON hs.shoe_id =sb.shoe_idORDER BY hs.day_walked ASC;INSERT 0 51 

Nu med nedenstående PLpgSQL-funktion, OPRET en "navngivet" CURSOR:

OPRET ELLER ERSTAT FUNKTION stats_cursor(refcursor)RETURNER refcursorAS $$BEGINÅBN $1 FORSELECT *FRA cp_hiking_total;RETURNER $1;END;$$ LANGUAGE plpgsql; 

Jeg vil kalde dette CURSOR 'stats':

fitness=> BEGIN;BEGINfitness=> SELECT stats_cursor('stats');stats_cursor --------------stats(1 række) 

Antag, at jeg vil have den '12.' række bundet til CURSOR'en.

Jeg kan placere CURSOR'en på den række og hente disse resultater med nedenstående kommando:

fitness=> FETCH ABSOLUTE 12 FROM stats;day_hiked | kalorier_forbrændte | miles | varighed | tempo | trail_hiked | sko_båret ------------+-----------------+-------+---------- +------+----------------------+--------------------- ------------------2018-05-02 | 311,2 | 3,27 | 00:57:13 | 3.4 | Træsti-forlænget | New Balance Trail Runners-All Terrain(1 række) 

I forbindelse med dette blogindlæg kan du forestille dig, at jeg ved første hånd, at tempokolonnens værdi for denne række er forkert.

Jeg husker specifikt, at jeg var 'død på fødderne træt' den dag og holdt kun et tempo på 3,0 under den vandretur. (Hey det sker.)

Okay, jeg vil lige OPDATERE cp_hiking_total-tabellen for at afspejle denne ændring.

Relativt simpelt uden tvivl. Kedeligt...

Hvad med statistikken CURSOR i stedet?

fitness=> OPDATERING cp_hiking_totalfitness-> INDSTIL tempo =3.0fitness-> HVOR AKTUELT AF statistik; OPDATERING 1 

For at gøre denne ændring permanent skal du udstede COMMIT:

fitness=> COMMIT;COMMIT 

Lad os forespørge og se, at OPDATERING afspejles i tabellen cp_hiking_total:

fitness=> VÆLG * FRA cp_hiking_totalfitness-> WHERE day_hiked ='2018-05-02';day_hiked | kalorier_forbrændte | miles | varighed | tempo | trail_hiked | sko_båret ------------+-----------------+-------+---------- +------+----------------------+--------------------- ------------------2018-05-02 | 311,2 | 3,27 | 00:57:13 | 3,0 | Træsti-forlænget | New Balance Trail Runners-All Terrain(1 række) 

Hvor fedt er det?

Bevæg dig inden for CURSOR's resultatsæt, og kør en OPDATERING om nødvendigt.

Ret kraftfuldt, hvis du spørger mig. Og praktisk.

Nogle 'forsigtighed' og oplysninger fra dokumentationen om denne type CURSOR:

"Det anbefales generelt at bruge TIL OPDATERING, hvis markøren er beregnet til at blive brugt med OPDATERING ... HVOR AKTUEL AF eller SLET ... HVOR AKTUEL AF. Brug af TIL OPDATERING forhindrer andre sessioner i at ændre rækkerne mellem tidspunktet de hentes og tidspunktet for opdatering. Uden TIL OPDATERING vil en efterfølgende WHERE CURRENT OF-kommando ikke have nogen effekt, hvis rækken blev ændret, siden markøren blev oprettet.

En anden grund til at bruge TIL OPDATERING er, at uden den kan en efterfølgende WHERE CURRENT OF mislykkes, hvis markørforespørgslen ikke opfylder SQL-standardens regler for at være "simpelthen opdaterbar" (især skal markøren kun referere til én tabel og ikke bruge gruppering eller BESTIL EFTER). Markører, der ikke blot kan opdateres, fungerer muligvis, eller måske ikke, afhængigt af planvalgsdetaljer; så i værste fald kan en applikation fungere i test og derefter fejle i produktionen."

Med den CURSOR, jeg har brugt her, har jeg fulgt SQL-standardreglerne (fra ovenstående passager) med hensyn til:Jeg refererede kun til én tabel uden gruppering eller ORDER by-sætning.

Hvorfor det betyder noget.

Som det er med adskillige operationer, forespørgsler eller opgaver i PostgreSQL (og SQL generelt), er der typisk mere end én måde at opnå og nå dit slutmål på. Hvilket er en af ​​hovedårsagerne til, at jeg er tiltrukket af SQL og stræber efter at lære mere.

Jeg håber, at jeg gennem dette opfølgende blogindlæg har givet lidt indsigt i, hvorfor multi-row UPDATE med CASE blev inkluderet som en af ​​mine yndlingsforespørgsler i det første medfølgende blogindlæg. Bare at have det som en mulighed er umagen værd for mig.

Derudover udforske CURSORS, for at krydse store resultatsæt. Udførelse af DML-operationer, såsom OPDATERINGER og/eller SLETTER, med den korrekte type CURSOR, er bare 'prikken over i'et'. Jeg er ivrig efter at studere dem yderligere for flere use cases.


  1. Tilføjelse af flere kolonner EFTER en specifik kolonne i MySQL

  2. CTE-fejl:Typerne matcher ikke mellem ankeret og den rekursive del

  3. SQL NOT Operator for begyndere

  4. Vigtigheden af ​​vedligeholdelse på MSDB