sql >> Databasteknik >  >> RDS >> PostgreSQL

Fler av mina favoritpostgreSQL-frågor - och varför de också är viktiga

I ett tidigare blogginlägg My Favorite PostgreSQL Queries and Why They Matter besökte jag intressanta frågor som var meningsfulla för mig när jag lär mig, utvecklar och växer till en SQL-utvecklarroll.

En av dessa, särskilt en UPPDATERING i flera rader med ett enda CASE-uttryck, utlöste en intressant konversation på Hacker News.

I det här blogginlägget vill jag observera jämförelser mellan den specifika frågan och en som involverar flera enstaka UPDATE-satser. För gott eller skit.

Maskin-/miljöspecifikationer:

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

Obs:Till att börja med skapade jag en "staging"-tabell med alla kolumner av TEXT-typ för att få data att laddas.

Exempeldatauppsättningen jag använder finns på den här länken.

Men kom ihåg att själva data används i det här exemplet eftersom det är en anständig storlek med flera kolumner. Alla "analyser" eller UPPDATERINGAR/INFOGNINGAR till denna datamängd återspeglar inte faktiska "verkliga" GPS/GIS-operationer och är inte avsedda som sådan.

location=# \d data_staging;
               Table "public.data_staging"
    Column     |  Type   | Collation | Nullable | Default 
---------------+---------+-----------+----------+---------
 segment_num   | text    |           |          | 
 point_seg_num | text    |           |          | 
 latitude      | text    |           |          | 
 longitude     | text    |           |          | 
 nad_year_cd   | text    |           |          | 
 proj_code     | text    |           |          | 
 x_cord_loc    | text    |           |          | 
 y_cord_loc    | text    |           |          | 
 last_rev_date | text    |           |          | 
 version_date  | text    |           |          | 
 asbuilt_flag  | text    |           |          | 

location=# SELECT COUNT(*) FROM data_staging;
count
--------
546895
(1 row)

Vi har cirka en halv miljon rader med data i den här tabellen.

För denna första jämförelse kommer jag att UPPDATERA kolumnen proj_code.

Här är en undersökande fråga för att fastställa dess aktuella värden:

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

Jag använder trim för att ta bort citattecken från värdena och casta till en INT och bestämma hur många rader som finns för varje enskilt värde:

Låt oss använda en CTE för det och välj sedan från den:

location=# WITH cleaned_nums AS (
SELECT NULLIF(trim(both '"' FROM proj_code), '') AS p_code FROM data_staging
)
SELECT COUNT(*),
CASE
WHEN p_code::int = 70 THEN '70'
WHEN p_code::int = 72 THEN '72'
WHEN p_code::int = 71 THEN '71'
WHEN p_code::int = 51 THEN '51'
WHEN p_code::int = 15 THEN '15'
WHEN p_code::int = 16 THEN '16'
ELSE '00'
END AS proj_code_num
FROM cleaned_nums
GROUP BY p_code
ORDER BY p_code DESC;
count  | proj_code_num
--------+---------------
353087 | 0
139057 | 72
25460  | 71
3254   | 70
1      | 51
12648  | 16
13388  | 15
(7 rows)

Innan jag kör dessa tester ska jag gå vidare och ÄNDRA kolumnen proj_code för att skriva INTEGER:

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

Och rensa det NULL-kolumnvärdet (som representeras av ELSE '00' i det utforskande CASE-uttrycket ovan), ställ in det till ett godtyckligt tal, 10, med denna UPPDATERING:

UPDATE data_staging
SET proj_code = 10
WHERE proj_code IS NULL;

Nu har alla proj_code-kolumner ett INTEGER-värde.

Låt oss gå vidare och köra ett enda CASE-uttryck som uppdaterar alla värden för proj_code-kolumnen och se vad tidpunkten rapporterar. Jag placerar alla kommandon i en .sql-källfil för att underlätta hanteringen.

Här är filinnehållet:

BEGIN;
\timing on
UPDATE data_staging
SET proj_code =
(
CASE proj_code
WHEN 72 THEN 7272
WHEN 71 THEN 7171
WHEN 15 THEN 1515
WHEN 51 THEN 5151
WHEN 70 THEN 7070
WHEN 10 THEN 1010
WHEN 16 THEN 1616
END
)
WHERE proj_code IN (72, 71, 15, 51, 70, 10, 16);
SAVEPOINT my_save;

Låt oss köra den här filen och kontrollera vad timingrapporterna rapporterar:

location=# \i /case_insert.sql
BEGIN
Time: 0.265 ms
Timing is on.
UPDATE 546895
Time: 6779.596 ms (00:06.780)
SAVEPOINT
Time: 0.300 ms

Drygt en halv miljon rader på 6+ sekunder.

Här är de reflekterade ändringarna i tabellen hittills:

location=# SELECT DISTINCT proj_code FROM data_staging;
proj_code
-----------
7070
1616
1010
7171
1515
7272
5151
(7 rows)

Jag återställer (visas inte) dessa ändringar så att jag kan köra individuella INSERT-satser för att testa dem också.

Nedan återspeglar ändringarna av .sql-källfilen för denna serie av jämförelser:

BEGIN;
\timing on

UPDATE data_staging
SET proj_code = 7222
WHERE proj_code = 72;

UPDATE data_staging
SET proj_code = 7171
WHERE proj_code = 71;

UPDATE data_staging
SET proj_code = 1515
WHERE proj_code = 15;

UPDATE data_staging
SET proj_code = 5151
WHERE proj_code = 51;

UPDATE data_staging
SET proj_code = 7070
WHERE proj_code = 70;

UPDATE data_staging
SET proj_code = 1010
WHERE proj_code = 10;

UPDATE data_staging
SET proj_code = 1616
WHERE proj_code = 16;
SAVEPOINT my_save;

Och dessa resultat,

location=# \i /case_insert.sql
BEGIN
Time: 0.264 ms
Timing is on.
UPDATE 139057
Time: 795.610 ms
UPDATE 25460
Time: 116.268 ms
UPDATE 13388
Time: 239.007 ms
UPDATE 1
Time: 72.699 ms
UPDATE 3254
Time: 162.199 ms
UPDATE 353087
Time: 1987.857 ms (00:01.988)
UPDATE 12648
Time: 321.223 ms
SAVEPOINT
Time: 0.108 ms

Låt oss kontrollera värdena:

location=# SELECT DISTINCT proj_code FROM data_staging;
proj_code
-----------
7222
1616
7070
1010
7171
1515
5151
(7 rows)

Och timingen (Obs:Jag kommer att räkna ut i en fråga eftersom \timing inte rapporterade hela sekunder denna körning):

location=# SELECT round((795.610 + 116.268 + 239.007 + 72.699 + 162.199 + 1987.857 + 321.223) / 1000, 3) AS seconds;
seconds
---------
3.695
(1 row)

De individuella INFOGARNA tog ungefär hälften så lång tid som det enstaka fallet.

Detta första test inkluderade hela tabellen, med alla kolumner. Jag är nyfiken på eventuella skillnader i en tabell med samma antal rader, men färre kolumner, därav nästa serie av tester.

Jag skapar en tabell med 2 kolumner (som består av en SERIAL-datatyp för PRIMÄRKEY och ett INTEGER för kolumnen proj_code) och flyttar över data:

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

(För att notera:SQL-kommandon från den första uppsättningen operationer används med lämplig(a) modifieringar. Jag utelämnar dem här för korthet och visning på skärmen )

Jag kör det enda CASE-uttrycket först:

location=# \i /case_insert.sql
BEGIN
Timing is on.
UPDATE 546895
Time: 4355.332 ms (00:04.355)
SAVEPOINT
Time: 0.137 ms

Och sedan den individuella UPPDATERING:

location=# \i /case_insert.sql
BEGIN
Time: 0.282 ms
Timing is on.
UPDATE 139057
Time: 1042.133 ms (00:01.042)
UPDATE 25460
Time: 123.337 ms
UPDATE 13388
Time: 212.698 ms
UPDATE 1
Time: 43.107 ms
UPDATE 3254
Time: 52.669 ms
UPDATE 353087
Time: 2787.295 ms (00:02.787)
UPDATE 12648
Time: 99.813 ms
SAVEPOINT
Time: 0.059 ms
location=# SELECT round((1042.133 + 123.337 + 212.698 + 43.107 + 52.669 + 2787.295 + 99.813) / 1000, 3) AS seconds;
seconds
---------
4.361
(1 row)

Timingen är något jämn mellan båda uppsättningarna av operationer på bordet med bara 2 kolumner.

Jag kommer att säga att det är lite lättare att skriva ut uttrycket CASE, men inte nödvändigtvis det bästa valet vid alla tillfällen. Som med vad som sägs i några av kommentarerna i Hacker News-tråden som hänvisas till ovan, beror det normalt "bara" på många faktorer som kan eller kanske inte är det optimala valet.

Jag inser att dessa tester i bästa fall är subjektiva. En av dessa, på en tabell med 11 kolumner medan den andra hade bara 2 kolumner, som båda var av en nummerdatatyp.

CASE-uttrycket för uppdateringar med flera rader är fortfarande en av mina favoritfrågor, om bara för enkelheten att skriva i en kontrollerad miljö där många individuella UPDATE-frågor är det andra alternativet.

Men nu kan jag se var det inte alltid är det optimala valet eftersom jag fortsätter att växa och lära mig.

Som det gamla ordspråket säger, "Ett halvdussin i ena handen, 6 i den andra ."

En ytterligare favoritfråga - Använda PLpgSQL CURSOR's

Jag har börjat lagra och spåra all min träningsstatistik (vandring) med PostgreSQL på min lokala utvecklingsmaskin. Det finns flera tabeller inblandade, som med vilken normaliserad databas som helst.

Men i slutet av månaderna vill jag lagra statistik för specifika kolumner i en egen separat tabell.

Här är "månadstabellen" jag kommer att använda:

fitness=> \d hiking_month_total;
                     Table "public.hiking_month_total"
     Column      |          Type          | Collation | Nullable | Default 
-----------------+------------------------+-----------+----------+---------
 day_hiked       | date                   |           |          | 
 calories_burned | numeric(4,1)           |           |          | 
 miles           | numeric(4,2)           |           |          | 
 duration        | time without time zone |           |          | 
 pace            | numeric(2,1)           |           |          | 
 trail_hiked     | text                   |           |          | 
 shoes_worn      | text                   |           |          |

Jag kommer att koncentrera mig på majs resultat med denna SELECT-fråga:

fitness=> SELECT hs.day_walked, hs.cal_burned, hs.miles_walked, hs.duration, hs.mph, tr.name, sb.name_brand
fitness-> FROM hiking_stats AS hs
fitness-> INNER JOIN hiking_trail AS ht
fitness-> ON hs.hike_id = ht.th_id
fitness-> INNER JOIN trail_route AS tr
fitness-> ON ht.tr_id = tr.trail_id
fitness-> INNER JOIN shoe_brand AS sb
fitness-> ON hs.shoe_id = sb.shoe_id
fitness-> WHERE extract(month FROM hs.day_walked) = 5
fitness-> ORDER BY hs.day_walked ASC;

Och här är 3 exempelrader som returneras från den frågan:

day_walked | cal_burned | miles_walked | duration | mph | name | name_brand
------------+------------+--------------+----------+-----+------------------------+---------------------------------------
2018-05-02 | 311.2 | 3.27 | 00:57:13 | 3.4 | Tree Trail-extended | New Balance Trail Runners-All Terrain
2018-05-03 | 320.8 | 3.38 | 00:58:59 | 3.4 | Sandy Trail-Drive | New Balance Trail Runners-All Terrain
2018-05-04 | 291.3 | 3.01 | 00:53:33 | 3.4 | House-Power Line Route | Keen Koven WP(keen-dry)
(3 rows)

Ärligt talat kan jag fylla i tabellen mål hiking_month_total med hjälp av SELECT-frågan ovan i en INSERT-sats.

Men var är det roliga med det?

Jag avstår från tristess för en PLpgSQL-funktion med en CURSOR istället.

Jag kom på den här funktionen för att utföra INSERT med en CURSOR:

CREATE OR REPLACE function monthly_total_stats()
RETURNS void
AS $month_stats$
DECLARE
v_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, 1);
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_brand
FROM hiking_stats AS hs
INNER JOIN hiking_trail AS ht
ON hs.hike_id = ht.th_id
INNER JOIN trail_route AS tr
ON ht.tr_id = tr.trail_id
INNER JOIN shoe_brand AS sb
ON hs.shoe_id = sb.shoe_id
WHERE extract(month FROM hs.day_walked) = 5
ORDER BY hs.day_walked ASC;
BEGIN
OPEN v_cur;
<<get_stats>>
LOOP
FETCH v_cur 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,
duration, pace, trail_hiked, shoes_worn)
VALUES(v_day_walked, v_cal_burned, v_miles_walked, v_duration, v_mph, v_name, v_name_brand);
END LOOP get_stats;
CLOSE v_cur;
END;
$month_stats$ LANGUAGE PLpgSQL;

Låt oss anropa funktionen monthly_total_stats() för att utföra INSERT:

fitness=> SELECT monthly_total_stats();
monthly_total_stats
---------------------
(1 row)

Eftersom funktionen är definierad RETURNS void, kan vi se att inget värde returneras till den som ringer.

För närvarande är jag inte specifikt intresserad av några returvärden,

bara att funktionen utför den definierade operationen och fyller i tabellen hiking_month_total.

Jag frågar efter ett antal poster i måltabellen och bekräftar att den har data:

fitness=> SELECT COUNT(*) FROM hiking_month_total;
count
-------
25
(1 row)

Funktionen monthly_total_stats() fungerar, men kanske ett bättre användningsfall för en CURSOR är att bläddra igenom ett stort antal poster. Kanske ett bord med runt en halv miljon poster?

Denna nästa CURSOR är bunden med en fråga som riktar sig till data_staging-tabellen från serien av jämförelser i avsnittet ovan:

CREATE OR REPLACE FUNCTION location_curs()
RETURNS refcursor
AS $location$
DECLARE
v_cur refcursor;
BEGIN
OPEN v_cur for SELECT segment_num, latitude, longitude, proj_code, asbuilt_flag FROM data_staging;
RETURN v_cur;
END;
$location$ LANGUAGE PLpgSQL;

Sedan, för att använda denna MARKör, agera inom en TRANSAKTION (påpekas i dokumentationen här).

location=# BEGIN;
BEGIN
location=# SELECT location_curs();
location_curs 
--------------------
<unnamed portal 1>
(1 row)

Så vad kan du göra med denna ""?

Här är bara några saker:

Vi kan returnera den första raden från MARKEREN med antingen första eller ABSOLUT 1:

location=# FETCH first FROM "<unnamed portal 1>";
segment_num | latitude | longitude | proj_code | asbuilt_flag 
-------------+------------------+-------------------+-----------+--------------
" 3571" | " 29.0202942600" | " -90.2908612800" | 72 | "Y"
(1 row)

location=# FETCH ABSOLUTE 1 FROM "<unnamed portal 1>";
segment_num | latitude | longitude | proj_code | asbuilt_flag 
-------------+------------------+-------------------+-----------+--------------
" 3571" | " 29.0202942600" | " -90.2908612800" | 72 | "Y"
(1 row)

Vill du ha en rad nästan halvvägs genom resultatuppsättningen? (Förutsatt att vi vet att uppskattningsvis en halv miljon rader är bundna till MARKEREN.)

Kan du vara så "specifik" med en CURSOR?

Japp.

Vi kan placera och HÄMTA värdena för posten på rad 234888 (bara ett slumpmässigt tal jag valde):

location=# FETCH ABSOLUTE 234888 FROM "<unnamed portal 1>";
segment_num | latitude | longitude | proj_code | asbuilt_flag 
-------------+------------------+-------------------+-----------+--------------
" 11261" | " 28.1159541400" | " -90.7778003500" | 10 | "Y"
(1 row)

När vi väl är placerade där kan vi flytta MARKEREN "bakåt en":

location=# FETCH BACKWARD FROM "<unnamed portal 1>";
segment_num | latitude | longitude | proj_code | asbuilt_flag 
-------------+------------------+-------------------+-----------+--------------
" 11261" | " 28.1159358200" | " -90.7778242300" | 10 | "Y"
(1 row)

Vilket är samma sak som:

location=# FETCH ABSOLUTE 234887 FROM "<unnamed portal 1>";
segment_num | latitude | longitude | proj_code | asbuilt_flag 
-------------+------------------+-------------------+-----------+--------------
" 11261" | " 28.1159358200" | " -90.7778242300" | 10 | "Y"
(1 row)

Sedan kan vi flytta MARKEREN tillbaka till ABSOLUTE 234888 med:

location=# FETCH FORWARD FROM "<unnamed portal 1>";
segment_num | latitude | longitude | proj_code | asbuilt_flag 
-------------+------------------+-------------------+-----------+--------------
" 11261" | " 28.1159541400" | " -90.7778003500" | 10 | "Y"
(1 row)

Behändigt tips:för att flytta markören, använd MOVE istället för HÄMTA om du inte behöver värdena från den raden.

Se detta avsnitt från dokumentationen:

"MOVE flyttar en markör utan att hämta någon data. MOVE fungerar precis som FETCH-kommandot, förutom att den bara placerar markören och inte returnerar rader."

Namnet "" är generiskt och kan faktiskt "döpas" istället.

Jag kommer att gå igenom min träningsstatistik igen för att skriva en funktion och namnge CURSOR, tillsammans med ett potentiellt "verkliga" användningsfall.

MARKEREN kommer att rikta in sig på denna extra tabell, som lagrar resultat som inte är begränsade till maj månad (i princip allt jag har samlat hittills) som i föregående exempel:

fitness=> CREATE TABLE cp_hiking_total AS SELECT * FROM hiking_month_total WITH NO DATA;
CREATE TABLE AS

Fyll sedan i den med data:

fitness=> INSERT INTO cp_hiking_total 
SELECT hs.day_walked, hs.cal_burned, hs.miles_walked, hs.duration, hs.mph, tr.name, sb.name_brand
FROM hiking_stats AS hs
INNER JOIN hiking_trail AS ht
ON hs.hike_id = ht.th_id
INNER JOIN trail_route AS tr
ON ht.tr_id = tr.trail_id
INNER JOIN shoe_brand AS sb
ON hs.shoe_id = sb.shoe_id
ORDER BY hs.day_walked ASC;
INSERT 0 51

Nu med PLpgSQL-funktionen nedan, SKAPA en "namngiven" MARKör:

CREATE OR REPLACE FUNCTION stats_cursor(refcursor)
RETURNS refcursor
AS $$
BEGIN
OPEN $1 FOR
SELECT *
FROM cp_hiking_total;
RETURN $1;
END;
$$ LANGUAGE plpgsql;

Jag kallar denna CURSOR 'stats':

fitness=> BEGIN;
BEGIN
fitness=> SELECT stats_cursor('stats');
stats_cursor 
--------------
stats
(1 row)

Anta att jag vill att den '12:e' raden ska vara bunden till CURSOR.

Jag kan placera MARKEREN på den raden och hämta dessa resultat med kommandot nedan:

fitness=> FETCH ABSOLUTE 12 FROM stats;
day_hiked | calories_burned | miles | duration | pace | trail_hiked | shoes_worn 
------------+-----------------+-------+----------+------+---------------------+---------------------------------------
2018-05-02 | 311.2 | 3.27 | 00:57:13 | 3.4 | Tree Trail-extended | New Balance Trail Runners-All Terrain
(1 row)

För det här blogginläggets syfte, tänk dig att jag vet att tempokolumnvärdet för den här raden är felaktigt.

Jag minns specifikt att jag var "död på fötterna trött" den dagen och bara höll ett tempo på 3,0 under den vandringen. (Hej det händer.)

Okej, jag ska bara UPPDATERA tabellen cp_hiking_total för att återspegla den förändringen.

Relativt enkelt utan tvekan. Tråkigt...

Vad sägs om med statistiken CURSOR istället?

fitness=> UPDATE cp_hiking_total
fitness-> SET pace = 3.0
fitness-> WHERE CURRENT OF stats;
UPDATE 1

För att göra denna ändring permanent, utfärda COMMIT:

fitness=> COMMIT;
COMMIT

Låt oss fråga och se att UPPDATERING återspeglas i tabellen cp_hiking_total:

fitness=> SELECT * FROM cp_hiking_total
fitness-> WHERE day_hiked = '2018-05-02';
day_hiked | calories_burned | miles | duration | pace | trail_hiked | shoes_worn 
------------+-----------------+-------+----------+------+---------------------+---------------------------------------
2018-05-02 | 311.2 | 3.27 | 00:57:13 | 3.0 | Tree Trail-extended | New Balance Trail Runners-All Terrain
(1 row)

Hur coolt är det?

Flytta dig inom CURSOR:s resultatuppsättning och kör en UPPDATERING om det behövs.

Ganska mäktigt om du frågar mig. Och bekvämt.

Lite "försiktighet" och information från dokumentationen om denna typ av CURSOR:

"Det rekommenderas generellt att använda FÖR UPPDATERING om markören är avsedd att användas med UPPDATERING ... WHERE CURRENT OF eller DELETE ... WHERE CURRENT OF. Användning FÖR UPPDATERING förhindrar andra sessioner från att ändra raderna mellan tiden de hämtas och tiden de uppdateras. Utan FOR UPDATE kommer ett efterföljande WHERE CURRENT OF-kommando inte att ha någon effekt om raden ändrades sedan markören skapades.

En annan anledning att använda FÖR UPPDATERING är att utan den kan en efterföljande WHERE CURRENT OF misslyckas om markörfrågan inte uppfyller SQL-standardens regler för att vara "helt enkelt uppdateringsbar" (i synnerhet måste markören referera till bara en tabell och inte använda gruppering eller BESTÄLL EFTER). Markörer som inte bara är uppdateringsbara kanske fungerar, eller kanske inte, beroende på planvalsdetaljer; så i värsta fall kan en applikation fungera i testning och sedan misslyckas i produktionen."

Med den CURSOR som jag har använt här har jag följt SQL-standardreglerna (från styckena ovan) i aspekten:Jag refererade bara till en tabell, utan gruppering eller ORDER by-sats.

Varför det är viktigt.

Precis som med många operationer, frågor eller uppgifter i PostgreSQL (och SQL i allmänhet), finns det vanligtvis mer än ett sätt att uppnå och nå ditt slutmål. Vilket är en av huvudorsakerna till att jag dras till SQL och strävar efter att lära mig mer.

Jag hoppas att jag genom detta uppföljande blogginlägg har gett lite insikt om varför UPDATEN med flera rader med CASE inkluderades som en av mina favoritfrågor, i det första medföljande blogginlägget. Att bara ha det som ett alternativ är värt besväret för mig.

Dessutom utforska CURSORER, för att korsa stora resultatuppsättningar. Att utföra DML-operationer, som UPPDATERINGAR och/eller DELETES, med rätt typ av CURSOR, är bara "grädde på moset". Jag är angelägen om att studera dem vidare för fler användningsfall.


  1. Hur gör man en kolumn unik i SQL?

  2. Oracle Database 21c

  3. PostgreSQL:Tid för att skapa tabeller

  4. Hur man skapar Not Null-begränsning på kolumn i SQL Server-tabell - SQL Server / T-SQL självstudie del 51