Postgres SQLクエリを少し調整して改善するだけで、データベースとのインターフェースに必要な、エラーが発生しやすい反復的なアプリケーションコードの量を減らすことができます。多くの場合、このような変更により、アプリケーションコードのパフォーマンスも向上します。
ここでは、アプリケーションコードがより多くの作業をPostgreSQLにアウトソーシングし、アプリケーションをよりスリムで高速にするのに役立つヒントとコツをいくつか紹介します。
Postgres v9.5以降、「競合」が原因で挿入が失敗した場合にどうなるかを指定できます。競合は、一意のインデックス(主キーを含む)または制約(CREATE CONSTRAINTを使用して以前に作成されたもの)の違反である可能性があります。
この機能は、アプリケーションロジックの挿入または更新を単一のSQLステートメントに単純化するために使用できます。たとえば、テーブル kv キーを使用 および値 列の場合、以下のステートメントは新しい行を挿入するか(テーブルにkey ='host'の行がない場合)、値を更新します(テーブルにkey ='host'の行がある場合):
CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT);
INSERT INTO kv (key, value)
VALUES ('host', '10.0.10.1')
ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value;
列key
に注意してください テーブルの単一列の主キーであり、競合句として指定されます。複数の列を持つ主キーがある場合は、代わりにここで主キーインデックスの名前を指定してください。
部分インデックスや制約の指定などの高度な例については、Postgresのドキュメントを参照してください。
挿入..戻る
INSERTステートメントはreturnすることもできます SELECTステートメントのように1つ以上の行。関数によって生成された値、 current_timestampなどのキーワードを返すことができます およびシリアル / sequence/identity列。
たとえば、自動生成されたID列と、行の作成のタイムスタンプを保持する列を持つテーブルは次のとおりです。
db=> CREATE TABLE t1 (id int GENERATED BY DEFAULT AS IDENTITY,
db(> at timestamptz DEFAULT CURRENT_TIMESTAMP,
db(> foo text);
INSERT .. RETURNINGステートメントを使用して、列 fooの値のみを指定できます。 、Postgresが idに対して生成した値を返すようにします およびat 列:
db=> INSERT INTO t1 (foo) VALUES ('first'), ('second') RETURNING id, at, foo;
id | at | foo
----+----------------------------------+--------
1 | 2022-01-14 11:52:09.816787+01:00 | first
2 | 2022-01-14 11:52:09.816787+01:00 | second
(2 rows)
INSERT 0 2
アプリケーションコードから、SELECTステートメントの実行と値の読み込みに使用するのと同じパターン/ APIを使用します( executeQuery()など)。 JDBCまたはdb.Query() Goで)。
別の例を次に示します。これには自動生成されたUUIDがあります:
CREATE TABLE t2 (id uuid PRIMARY KEY, foo text);
INSERT INTO t2 (id, foo) VALUES (gen_random_uuid(), ?) RETURNING id;
INSERTと同様に、UPDATEおよびDELETEステートメントにもPostgresのRETURNING句を含めることができます。 RETURNING句はPostgresの拡張機能であり、SQL標準の一部ではありません。
アプリケーションコードから、列の値を一連の許容可能な値と照合する必要があるWHERE句をどのように作成しますか?値の数が事前にわかっている場合、SQLは静的です:
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key IN (?, ?)");
stmt.setString(1, key[0]);
stmt.setString(2, key[1]);
しかし、キーの数が2ではなく、任意の数にできる場合はどうなるでしょうか。 SQLステートメントを動的に作成しますか?より簡単なオプションは、Postgres配列を使用することです:
SELECT key, value FROM kv WHERE key = ANY(?)
上記のANY演算子は、引数として配列を取ります。句key=ANY(?) keyの値が含まれるすべての行を選択します 提供された配列の要素の1つです。これにより、アプリケーションコードを次のように簡略化できます。
stmt = conn.prepareStatement("SELECT key, value FROM kv WHERE key = ANY(?)");
a = conn.createArrayOf("STRING", keys);
stmt.setArray(1, a);
このアプローチは、限られた数の値に対して実行可能です。一致する値が多数ある場合は、(一時的な)テーブルやマテリアライズドビューとの結合などの他のオプションを検討してください。
はい、1つのSQLステートメントで1つのテーブルから行を削除し、別のテーブルに挿入することができます。メインのINSERTステートメントは、DELETEをラップするCTEを使用して、行をプルして挿入できます。
WITH items AS (
DELETE FROM todos_2021
WHERE NOT done
RETURNING *
)
INSERT INTO todos_2021 SELECT * FROM items;
アプリケーションコードで同等のことを行うことは非常に複雑になる可能性があり、削除の結果全体をメモリに保存し、それを使用して複数のINSERTを実行する必要があります。確かに、行の移動は一般的なユースケースではないかもしれませんが、ビジネスロジックで必要な場合は、このアプローチによって提供されるアプリケーションメモリとデータベースのラウンドトリップの節約が理想的なソリューションになります。
ソーステーブルと宛先テーブルの列のセットは同一である必要はありません。もちろん、関数を並べ替え、並べ替え、使用して、選択/戻りリストの値を操作できます。
アプリケーションコードでNULL値を渡すには、通常、追加の手順が必要です。たとえば、Goでは、 sql.NullStringなどのタイプを使用する必要があります。; Java / JDBCでは、 resultSet.wasNull()のような関数 。これらは面倒でエラーが発生しやすいです。
特定のクエリのコンテキストで、たとえばNULLを空の文字列として、またはNULL整数を0として処理できる場合は、COALESCE関数を使用できます。 COALESCE関数は、NULL値を特定の値に変換できます。たとえば、次のクエリについて考えてみます。
SELECT invoice_num, COALESCE(shipping_address, '')
FROM invoices
WHERE EXTRACT(month FROM raised_on) = 1 AND
EXTRACT(year FROM raised_on) = 2022
これは、2022年1月に発行された請求書の請求書番号と配送先住所を取得します。おそらく shipping_address 商品を物理的に出荷する必要がない場合はNULLです。たとえば、アプリケーションコードがそのような場合に空の文字列をどこかに表示したいだけの場合は、COALESCEを使用し、アプリケーションでNULL処理コードを削除する方が簡単です。
空の文字列の代わりに他の文字列を使用することもできます:
SELECT invoice_num, COALESCE(shipping_address, '* NOT SPECIFIED *') ...
リストから最初のNULL以外の値を取得することも、代わりに指定された文字列を使用することもできます。たとえば、請求先住所または配送先住所のいずれかを使用するには、次を使用できます。
SELECT invoice_num, COALESCE(billing_address, shipping_address, '* NO ADDRESS GIVEN *') ...
CASEは、実際の不完全なデータを処理するためのもう1つの便利な構成です。 shipping_address にNULLを含めるのではなく、たとえば 出荷不可能なアイテムについては、当社のあまり完璧ではない請求書作成ソフトウェアが「NOT-SPECIFIED」に入れています。データを読み込むときに、これをNULLまたは空の文字列にマッピングします。ユースケースを使用できます:
-- map NOT-SPECIFIED to an empty string
SELECT invoice_num,
CASE shipping_address
WHEN 'NOT-SPECIFIED' THEN ''
ELSE shipping_address
END
FROM invoices;
-- same result, different syntax
SELECT invoice_num,
CASE
WHEN shipping_address = 'NOT-SPECIFIED' THEN ''
ELSE shipping_address
END
FROM invoices;
CASEの構文は不格好ですが、機能的にはCのような言語のswitch-caseステートメントに似ています。別の例を次に示します。
SELECT invoice_num,
CASE
WHEN shipping_address IS NULL THEN 'NOT SHIPPING'
WHEN billing_address = shipping_address THEN 'SHIPPING TO PAYER'
ELSE 'SHIPPING TO ' || shipping_address
END
FROM invoices;
Select .. union
UNIONを使用して、2つ(またはそれ以上)の個別のSELECTステートメントからのデータを組み合わせることができます。たとえば、現在のユーザーを保持しているテーブルと削除されているテーブルの2つのテーブルがある場合、両方を同時にクエリする方法は次のとおりです。
SELECT id, name, address, FALSE AS is_deleted
FROM users
WHERE email = ?
UNION
SELECT id, name, address, TRUE AS is_deleted
FROM deleted_users
WHERE email = ?
2つのクエリは同じ選択リストを持つ必要があります。つまり、同じ数とタイプの列を返す必要があります。
UNIONは重複も削除します。一意の行のみが返されます。重複する行を保持したい場合は、UNIONの代わりに「UNIONALL」を使用してください。
UNIONを補完するものとして、INTERSECTとEXCEPTもあります。詳細についてはPostgreSQLのドキュメントを参照してください。
SELECTの後にDISTINCTキーワードを追加することにより、SELECTによって返される重複行を組み合わせることができます(つまり、一意の行のみが返されます)。これは標準SQLですが、Postgresは拡張機能「DISTINCTON」を提供します。使用するのは少し難しいですが、実際には、必要な結果を得るのに最も簡潔な方法であることがよくあります。
顧客を考えてみましょう 顧客ごとの行と購入のテーブル (一部の)顧客による購入ごとに1行のテーブル。以下のクエリは、すべての顧客と各購入を返します。
SELECT C.id, P.at
FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
ORDER BY C.id ASC, P.at ASC;
各顧客の行は、購入するたびに繰り返されます。顧客の最初の購入のみを返品したい場合はどうなりますか?基本的に、行を顧客ごとに並べ替え、行を顧客ごとにグループ化し、各グループ内で行を購入時間ごとに並べ替え、最後に各グループの最初の行のみを返します。 DISTINCT ONを使用してSQLで記述する方が、実際には短くなります。
SELECT DISTINCT ON (C.id) C.id, P.at
FROM customers C LEFT OUTER JOIN purchases P ON P.customer_id = C.id
ORDER BY C.id ASC, P.at ASC;
追加された「DISTINCTON(C.id)」句は、上記で説明したことを実行します。ほんの数文字追加するだけで、大変な作業になります!
顧客名のリストと電話番号の市外局番をテーブルから取得することを検討してください。米国の電話番号は、(123) 456-7890
の形式で保存されていると想定します。 。その他の国では、市外局番として「NON-US」とだけ言います。
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers;
これですべて問題ありません。CASEコンストラクトもありますが、今すぐ市外局番で並べ替える必要がある場合はどうすればよいでしょうか。
これは機能します:
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers
ORDER BY
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END ASC;
でも、うん! case句を繰り返すと、見苦しく、エラーが発生しやすくなります。国コードと電話を受け取り、市外局番を返すストアド関数を作成することもできますが、実際にはもっと良いオプションがあります:
SELECT last_name, first_name,
CASE country_code
WHEN 'US' THEN substr(phone, 2, 3)
ELSE 'NON-US'
END
FROM customers
ORDER BY 3 ASC;
「ORDERBY3」は、3番目のフィールドによる注文を示しています。選択リストを再配置するときに番号を更新することを忘れないでください。ただし、通常はそれだけの価値があります。