以前のブログ投稿「私のお気に入りのPostgreSQLクエリとその重要性」では、SQL開発者の役割を学び、開発し、成長するにつれて、私にとって意味のある興味深いクエリにアクセスしました。
それらの1つ、特に、単一のCASE式を使用した複数行のUPDATEは、HackerNewsで興味深い会話を引き起こしました。
このブログ投稿では、その特定のクエリと、複数の単一のUPDATEステートメントを含むクエリとの比較を観察したいと思います。善か悪か。
マシン/環境の仕様:
- Intel(R)Core(TM)i5-6200U CPU @ 2.30GHz
- 8GB RAM
- 1TBストレージ
- Xubuntu Linux 16.04.3 LTS(Xenial Xerus)
- PostgreSQL 10.4
注:最初に、データをロードするために、すべてのTEXTタイプの列を含む「ステージング」テーブルを作成しました。
私が使用しているサンプルデータセットは、こちらのリンクにあります。
ただし、この例ではデータ自体が使用されていることに注意してください。これは、複数の列を持つ適切なサイズのセットであるためです。このデータセットに対する「分析」またはUPDATES/INSERTSは、実際の「実際の」GPS / GIS操作を反映しておらず、そのように意図されていません。
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)
このテーブルには約50万行のデータがあります。
この最初の比較では、proj_code列を更新します。
現在の値を決定するための探索的クエリは次のとおりです。
location=# SELECT DISTINCT proj_code FROM data_staging;
proj_code
-----------
"70"
""
"72"
"71"
"51"
"15"
"16"
(7 rows)
トリムを使用して値から引用符を削除し、INTにキャストして、個々の値ごとに存在する行数を決定します。
そのためにCTEを使用して、そこからSELECTしてみましょう:
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)
これらのテストを実行する前に、proj_code列を変更して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;
そして、そのNULL列値(上記の探索的CASE式ではELSE '00'で表されます)をクリーンアップし、このUPDATEを使用して任意の数10に設定します。
UPDATE data_staging
SET proj_code = 10
WHERE proj_code IS NULL;
これで、すべてのproj_code列にINTEGER値が設定されました。
先に進んで、すべてのproj_code列の値を更新する単一のCASE式を実行し、タイミングが何を報告するかを確認しましょう。扱いやすくするために、すべてのコマンドを.sqlソースファイルに配置します。
ファイルの内容は次のとおりです。
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;
このファイルを実行して、タイミングが何を報告するかを確認しましょう:
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
6秒以上で50万行強。
これまでの表に反映された変更は次のとおりです。
location=# SELECT DISTINCT proj_code FROM data_staging;
proj_code
-----------
7070
1616
1010
7171
1515
7272
5151
(7 rows)
これらの変更をロールバック(表示されていません)して、個々のINSERTステートメントを実行してそれらもテストできるようにします。
以下は、この一連の比較のための.sqlソースファイルへの変更を反映しています。
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;
そしてそれらの結果、
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
値を確認しましょう:
location=# SELECT DISTINCT proj_code FROM data_staging;
proj_code
-----------
7222
1616
7070
1010
7171
1515
5151
(7 rows)
そしてタイミング(注:\ Timingはこの実行で秒全体を報告しなかったので、クエリで計算を行います):
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)
個々のINSERTは、単一のCASEの約半分の時間かかりました。
この最初のテストには、すべての列を含むテーブル全体が含まれていました。行数は同じで列数が少ないテーブルの違い、つまり次の一連のテストに興味があります。
2つの列(PRIMARY KEYのSERIALデータ型とproj_code列のINTEGERで構成される)を持つテーブルを作成し、データを移動します:
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
(注:最初の一連の操作のSQLコマンドは、適切な変更を加えて使用されます。画面上の簡潔さと表示のために、ここでは省略しています。 )
最初に単一のCASE式を実行します:
location=# \i /case_insert.sql
BEGIN
Timing is on.
UPDATE 546895
Time: 4355.332 ms (00:04.355)
SAVEPOINT
Time: 0.137 ms
そして、個々のUPDATE:
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)
タイミングは、2列しかないテーブルの両方の操作セット間である程度均等です。
CASE式を使用すると、入力が少し簡単になりますが、すべての場合に必ずしも最良の選択であるとは限りません。上記のハッカーニューススレッドに関するコメントの一部で述べられていることと同様に、それは通常、最適な選択であるかどうかにかかわらず、多くの要因に「依存」します。
これらのテストはせいぜい主観的なものだと思います。そのうちの1つは、11列のテーブルで、もう1つは2列だけで、どちらも数値データ型でした。
複数の行の更新のCASE式は、多くの個別のUPDATEクエリが他の選択肢である制御された環境での入力を容易にするためだけに、今でも私のお気に入りのクエリの1つです。
しかし、私は成長し、学び続けるにつれて、それが常に最適な選択であるとは限らない場所を今では見ることができます。
その古いことわざにあるように、「片手に6ダース、もう片方の手に6ダース 。"
追加のお気に入りのクエリ-PLpgSQLCURSORを使用
ローカル開発マシンでPostgreSQLを使用して、すべての運動(トレイルハイキング)統計の保存と追跡を開始しました。正規化されたデータベースと同様に、複数のテーブルが関係しています。
ただし、月末に、特定の列の統計を独自の個別のテーブルに保存したいと思います。
これが私が使用する「月次」テーブルです:
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 | | |
このSELECTクエリを使用して5月の結果に集中します:
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;
そして、そのクエリから返される3つのサンプル行を次に示します。
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)
正直なところ、INSERTステートメントで上記のSELECTクエリを使用して、ターゲットのhiking_month_totalテーブルにデータを入力できます。
しかし、その楽しみはどこにありますか?
代わりにCURSORを使用したPLpgSQL関数の退屈を忘れます。
カーソルでINSERTを実行するためにこの関数を思いついた:
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;
関数monthly_total_stats()を呼び出してINSERTを実行してみましょう:
fitness=> SELECT monthly_total_stats();
monthly_total_stats
---------------------
(1 row)
関数はRETURNSvoidと定義されているため、呼び出し元に値が返されないことがわかります。
現時点では、戻り値には特に関心がありません。
関数が定義された操作を実行し、hiking_month_totalテーブルにデータを入力することだけです。
ターゲットテーブルのレコード数をクエリして、データがあることを確認します:
fitness=> SELECT COUNT(*) FROM hiking_month_total;
count
-------
25
(1 row)
Monthly_total_stats()関数は機能しますが、CURSORのより良いユースケースは、多数のレコードをスクロールすることです。たぶん、約50万件のレコードがあるテーブルですか?
この次のCURSORは、上記のセクションの一連の比較からのdata_stagingテーブルを対象とするクエリにバインドされています。
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;
次に、このカーソルを使用するには、トランザクション内で操作します(ここのドキュメントで指摘されています)。
location=# BEGIN;
BEGIN
location=# SELECT location_curs();
location_curs
--------------------
<unnamed portal 1>
(1 row)
では、この「<名前のないポータル>」で何ができるでしょうか?
ここにいくつかのことがあります:
firstまたはABSOLUTE1:
を使用して、CURSORから最初の行を返すことができます。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)
結果セットのほぼ半分の行が必要ですか? (推定50万行がCURSORにバインドされていることがわかっていると仮定します。)
CURSORでその「特定」になることはできますか?
うん。
行234888(私が選択したランダムな番号)でレコードの値を配置してフェッチすることができます:
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)
そこに配置されたら、CURSORを「後方に移動」できます:
location=# FETCH BACKWARD FROM "<unnamed portal 1>";
segment_num | latitude | longitude | proj_code | asbuilt_flag
-------------+------------------+-------------------+-----------+--------------
" 11261" | " 28.1159358200" | " -90.7778242300" | 10 | "Y"
(1 row)
これは次と同じです:
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)
次に、次のコマンドを使用して、CURSORをABSOLUTE234888に戻すことができます。
location=# FETCH FORWARD FROM "<unnamed portal 1>";
segment_num | latitude | longitude | proj_code | asbuilt_flag
-------------+------------------+-------------------+-----------+--------------
" 11261" | " 28.1159541400" | " -90.7778003500" | 10 | "Y"
(1 row)
便利なヒント:カーソルの位置を変更するには、その行の値が必要ない場合は、FETCHの代わりにMOVEを使用します。
ドキュメントからこの一節を参照してください:
"MOVEは、データを取得せずにカーソルを再配置します。MOVEは、カーソルを配置するだけで行を返さないことを除いて、FETCHコマンドとまったく同じように機能します。"
「<名前のないポータル1>」の名前は一般的なものであり、実際には「名前」を付けることができます。
フィットネス統計データを再検討して、関数を記述し、潜在的な「実際の」ユースケースとともにCURSORに名前を付けます。
CURSORは、前の例のように5月(基本的にこれまでに収集したすべて)に限定されない結果を格納するこの追加のテーブルを対象とします。
fitness=> CREATE TABLE cp_hiking_total AS SELECT * FROM hiking_month_total WITH NO DATA;
CREATE TABLE AS
次に、データを入力します:
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
次に、以下のPLpgSQL関数を使用して、「名前付き」CURSORを作成します。
CREATE OR REPLACE FUNCTION stats_cursor(refcursor)
RETURNS refcursor
AS $$
BEGIN
OPEN $1 FOR
SELECT *
FROM cp_hiking_total;
RETURN $1;
END;
$$ LANGUAGE plpgsql;
これをCURSORの「統計」と呼びます:
fitness=> BEGIN;
BEGIN
fitness=> SELECT stats_cursor('stats');
stats_cursor
--------------
stats
(1 row)
'12番目の'行をCURSORにバインドしたいとします。
次のコマンドを使用して、その行にCURSORを配置し、それらの結果を取得できます。
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)
このブログ投稿の目的のために、この行のペース列の値が正しくないことを直接知っていると想像してください。
私は特に、その日「疲れた足で死んだ」ことを覚えており、そのハイキングの間は3.0のペースしか維持していませんでした。 (ねえ、それは起こります。)
さて、その変更を反映するためにcp_hiking_totalテーブルを更新します。
間違いなく比較的単純です。退屈…
代わりに統計カーソルを使用するのはどうですか?
fitness=> UPDATE cp_hiking_total
fitness-> SET pace = 3.0
fitness-> WHERE CURRENT OF stats;
UPDATE 1
この変更を永続的にするには、COMMITを発行します:
fitness=> COMMIT;
COMMIT
クエリを実行して、UPDATEがテーブル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)
なんてクールなの?
CURSORの結果セット内を移動し、必要に応じてUPDATEを実行します。
あなたが私に尋ねれば非常に強力です。そして便利です。
このタイプのCURSORに関するドキュメントからのいくつかの「注意」と情報:
"カーソルをUPDATE...WHERECURRENTOFまたはDELETE...WHERE CURRENT OFで使用する場合は、通常、FORUPDATEを使用することをお勧めします。FORUPDATEを使用すると、他のセッションが時間の間に行を変更できなくなります。それらはフェッチされ、更新されます。FORUPDATEがないと、カーソルが作成されてから行が変更された場合、後続のWHERECURRENTOFコマンドは効果がありません。
FOR UPDATEを使用するもう1つの理由は、FOR UPDATEがないと、カーソルクエリが「単に更新可能」であるというSQL標準のルールを満たさない場合に後続のWHERE CURRENT OFが失敗する可能性があるためです(特に、カーソルは1つのテーブルのみを参照する必要があります。グループ化またはORDERBYを使用しないでください)。単に更新可能ではないカーソルは、プランの選択の詳細に応じて機能する場合と機能しない場合があります。そのため、最悪の場合、アプリケーションはテストで動作し、その後本番環境で失敗する可能性があります。」
ここで使用したCURSORを使用して、次の点でSQL標準ルール(上記の節から)に従いました。グループ化やORDER by句を使用せずに、1つのテーブルのみを参照しました。
重要な理由。
PostgreSQL(および一般的なSQL)の多数の操作、クエリ、またはタスクと同様に、通常、最終目標を達成して達成する方法は複数あります。これが、私がSQLに惹かれ、詳細を学ぼうと努力している主な理由の1つです。
このフォローアップブログ投稿を通じて、CASEを使用した複数行のUPDATEが私のお気に入りのクエリの1つとして含まれている理由について、最初の付随するブログ投稿に洞察を提供したことを願っています。オプションとしてそれを持っているだけで私には価値があります。
さらに、大規模な結果セットをトラバースするためのカーソルの探索。正しいタイプのCURSORを使用してUPDATESやDELETESなどのDML操作を実行することは、単に「ケーキの上のアイシング」です。より多くのユースケースについて、さらに調査したいと思っています。