sql >> データベース >  >> RDS >> PostgreSQL

私のお気に入りのPostgreSQLクエリの詳細-そしてなぜそれらも重要なのか

    以前のブログ投稿「私のお気に入りの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操作を実行することは、単に「ケーキの上のアイシング」です。より多くのユースケースについて、さらに調査したいと思っています。


    1. 別のテーブルに行を挿入するようにMySQLトリガーをプログラムするにはどうすればよいですか?

    2. utf8_general_ciとutf8_unicode_ciの違いは何ですか?

    3. SQLServerでgroup_concatを使用してクエリを実行する方法

    4. ORA-00001一意性制約に違反しました