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

見落とされたT-SQLジェム

    私の親友のアーロン・ベルトランは私にこの記事を書くように促しました。彼は、物事が私たちにとって明白であるように見え、その背後にある完全なストーリーを常にチェックする必要がない場合に、私たちが物事を当然のことと見なすことがあることを私に思い出させました。 T-SQLとの関連性は、特定のT-SQL機能について知っておくべきことはすべてわかっていると想定することがあり、ドキュメントをチェックしてさらに多くの機能があるかどうかを常に確認する必要はないということです。この記事では、見過ごされがちなT-SQL機能、または見落とされがちなパラメーターや機能をサポートするT-SQL機能について説明します。見落とされがちな独自のT-SQLgemの例がある場合は、この記事のコメントセクションでそれらを共有してください。

    この記事を読み始める前に、次のT-SQL機能について知っていることを自問してください:EOMONTH、TRANSLATE、TRIM、CONCATおよびCONCAT_WS、LOG、カーソル変数、およびMERGEwithOUTPUT。

    私の例では、TSQLV5というサンプルデータベースを使用します。このデータベースを作成してデータを取り込むスクリプトはここにあり、そのER図はここにあります。

    EOMONTHには2番目のパラメーターがあります

    EOMONTH関数はSQLServer2012で導入されました。多くの人は、入力日を保持するパラメーターを1つだけサポートし、入力日に対応する月末の日付を返すだけだと考えています。

    前月末を計算するためのもう少し洗練されたニーズを考えてみましょう。たとえば、Sales.Ordersテーブルにクエリを実行し、前月末に行われた注文を返す必要があるとします。

    これを実現する1つの方法は、EOMONTH関数をSYSDATETIMEに適用して現在の月の月末日を取得し、次にDATEADD関数を適用して結果から月を減算することです。

    USE TSQLV5; 
     
    SELECT orderid, orderdate
    FROM Sales.Orders
    WHERE orderdate = EOMONTH(DATEADD(month, -1, SYSDATETIME()));

    TSQLV5サンプルデータベースでこのクエリを実際に実行すると、テーブルに記録された最後の注文日が2019年5月6日であるため、空の結果が得られることに注意してください。ただし、テーブルに注文日が最後に当たる注文があった場合前月の日、クエリはそれらを返します。

    多くの人が気付いていないのは、EOMONTHは、加算または減算する月数を示す2番目のパラメーターをサポートしているということです。関数の[完全に文書化された]構文は次のとおりです。

    EOMONTH ( start_date [, month_to_add ] )

    次のように、関数の2番目のパラメーターとして-1を指定するだけで、タスクをより簡単かつ自然に実行できます。

    SELECT orderid, orderdate
    FROM Sales.Orders
    WHERE orderdate = EOMONTH(SYSDATETIME(), -1);

    TRANSLATEはREPLACEよりも単純な場合があります

    多くの人がREPLACE関数とその仕組みに精通しています。入力文字列内のある部分文字列のすべての出現箇所を別の部分文字列に置き換える場合に使用します。ただし、適用する必要のある置換が複数ある場合、REPLACEの使用は少し注意が必要で、複雑な表現になることがあります。

    例として、スペイン語形式の数値を含む入力文字列@sが与えられたとします。スペインでは、数千のグループの区切り文字としてピリオドを使用し、小数点記号としてコンマを使用します。入力を米国形式に変換する必要があります。ここでは、カンマが数千のグループの区切り文字として使用され、ピリオドが小数点記号として使用されます。

    REPLACE関数への1回の呼び出しを使用して、1つの文字または部分文字列のすべての出現箇所のみを別の文字列に置き換えることができます。 2つの置換(ピリオドをコンマに、コンマをピリオドに)を適用するには、関数呼び出しをネストする必要があります。トリッキーな部分は、REPLACEを1回使用してピリオドをコンマに変更し、次に結果に対して2回目にコンマをピリオドに変更すると、ピリオドのみになってしまうことです。試してみてください:

    DECLARE @s AS VARCHAR(20) = '123.456.789,00';
     
    SELECT REPLACE(REPLACE(@s, '.', ','), ',', '.');

    次の出力が得られます:

    123.456.789.00

    REPLACE関数の使用を継続したい場合は、3つの関数呼び出しが必要です。ピリオドを、通常はデータに表示できないことがわかっているニュートラル文字(たとえば、〜)に置き換えるもの。結果に対してもう1つ、すべてのコンマをピリオドに置き換えます。もう1つは、一時文字(この例では〜)のすべての出現箇所をコンマに置き換える結果に対するものです。完全な表現は次のとおりです。

    DECLARE @s AS VARCHAR(20) = '123.456.789,00';
    SELECT REPLACE(REPLACE(REPLACE(@s, '.', '~'), ',', '.'), '~', ',');

    今回は正しい出力が得られます:

    123,456,789.00

    それは一種の実行可能ですが、長く複雑な表現になります。適用する代替品がさらにある場合はどうなりますか?

    多くの人は、SQLServer2017がそのような置換を大幅に簡素化するTRANSLATEと呼ばれる新しい関数を導入したことに気づいていません。関数の構文は次のとおりです。

    TRANSLATE ( inputString, characters, translations )

    2番目の入力(文字)は、置換する個々の文字のリストを含む文字列であり、3番目の入力(翻訳)は、ソース文字を置換する対応する文字のリストを含む文字列です。これは当然、2番目と3番目のパラメーターの文字数が同じでなければならないことを意味します。この関数で重要なのは、置換ごとに個別のパスを実行しないことです。もしそうなら、REPLACE関数への2つの呼び出しを使用して示した最初の例と同じバグが発生する可能性があります。その結果、タスクの処理は簡単になります:

    DECLARE @s AS VARCHAR(20) = '123.456.789,00';
    SELECT TRANSLATE(@s, '.,', ',.');

    このコードは、目的の出力を生成します:

    123,456,789.00

    それはかなりきちんとしています!

    TRIMはLTRIM(RTRIM())以上のものです

    SQL Server 2017では、関数TRIMのサポートが導入されました。私も含めて、多くの人は、最初はそれがLTRIM(RTRIM(input))への単純なショートカットにすぎないと思っています。ただし、ドキュメントを確認すると、実際にはそれよりも強力であることがわかります。

    詳細に入る前に、次のタスクを検討してください。入力文字列@sを指定して、先頭と末尾のスラッシュ(前後)を削除します。例として、@sに次の文字列が含まれているとします。

    //\\ remove leading and trailing backward (\) and forward (/) slashes \\//

    必要な出力は次のとおりです。

     remove leading and trailing backward (\) and forward (/) slashes 

    出力は先頭と末尾のスペースを保持する必要があることに注意してください。

    TRIMの全機能を知らなかった場合は、次の方法で課題を解決できた可能性があります。

    DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
     
    SELECT
      TRANSLATE(TRIM(TRANSLATE(TRIM(TRANSLATE(@s, ' /', '~ ')), ' \', '^ ')), ' ^~', '\/ ')
        AS outputstring;
    >

    解決策は、TRANSLATEを使用してすべてのスペースをニュートラル文字(〜)に置き換え、スラッシュをスペースに置き換え、次にTRIMを使用して結果から先頭と末尾のスペースを削除することから始まります。この手順では、基本的に、元のスペースの代わりに〜を一時的に使用して、前後のスラッシュをトリミングします。このステップの結果は次のとおりです。

    \\~remove~leading~and~trailing~backward~(\)~and~forward~( )~slashes~\\

    次に、2番目のステップでは、TRANSLATEを使用してすべてのスペースを別のニュートラル文字(^)に置き換え、円記号をスペースに置き換え、TRIMを使用して結果から先頭と末尾のスペースを削除します。この手順では、基本的に、中間スペースの代わりに^を一時的に使用して、前後の後方スラッシュをトリミングします。このステップの結果は次のとおりです。

    ~remove~leading~and~trailing~backward~( )~and~forward~(^)~slashes~

    最後のステップでは、TRANSLATEを使用してスペースを円記号に、^を円記号に、〜をスペースに置き換えて、目的の出力を生成します。

     remove leading and trailing backward (\) and forward (/) slashes 

    演習として、TRIMとTRANSLATEを使用できないSQLServer2017以前の互換性のあるソリューションを使用してこのタスクを解決してみてください。

    SQL Server 2017以降に戻って、わざわざドキュメントを確認した場合、TRIMは当初考えていたよりも洗練されていることに気付くでしょう。関数の構文は次のとおりです:

    TRIM ( [ characters FROM ] string )

    オプションの文字FROM partを使用すると、入力文字列の最初と最後からトリミングする1つ以上の文字を指定できます。この場合、次のように、この部分として「/\」を指定するだけです。

    DECLARE @s AS VARCHAR(100) = '//\\ remove leading and trailing backward (\) and forward (/) slashes \\//';
     
    SELECT TRIM( '/\' FROM @s) AS outputstring;

    これは、以前のソリューションと比較してかなり大幅な改善です!

    CONCATおよびCONCAT_WS

    T-SQLをしばらく使用している場合は、文字列を連結する必要があるときにNULLを処理するのがいかに厄介かを知っています。例として、HR.Employeesテーブルに従業員について記録されたロケーションデータについて考えてみます。

    SELECT empid, country, region, city
    FROM HR.Employees;

    このクエリは次の出力を生成します:

    empid       country         region          city
    ----------- --------------- --------------- ---------------
    1           USA             WA              Seattle
    2           USA             WA              Tacoma
    3           USA             WA              Kirkland
    4           USA             WA              Redmond
    5           UK              NULL            London
    6           UK              NULL            London
    7           UK              NULL            London
    8           USA             WA              Seattle
    9           UK              NULL            London

    一部の従業員の場合、リージョン部分は無関係であり、無関係なリージョンはNULLで表されることに注意してください。区切り文字としてコンマを使用し、NULL領域を無視して、場所の部分(国、地域、都市)を連結する必要があるとします。地域が関連している場合は、結果の形式を<coutry>,<region>,<city>にする必要があります。 地域が無関係な場合は、結果を<country>,<city>の形式にする必要があります。 。通常、何かをNULLと連結すると、NULLの結果が生成されます。 CONCAT_NULL_YIELDS_NULLセッションオプションをオフにすることでこの動作を変更できますが、非標準の動作を有効にすることはお勧めしません。

    CONCAT関数とCONCAT_WS関数の存在を知らなかった場合は、ISNULLまたはCOALESCEを使用して、次のようにNULLを空の文字列に置き換えた可能性があります。

    SELECT empid, country + ISNULL(',' + region, '') + ',' + city AS location
    FROM HR.Employees;

    このクエリの出力は次のとおりです。

    empid       location
    ----------- -----------------------------------------------
    1           USA,WA,Seattle
    2           USA,WA,Tacoma
    3           USA,WA,Kirkland
    4           USA,WA,Redmond
    5           UK,London
    6           UK,London
    7           UK,London
    8           USA,WA,Seattle
    9           UK,London

    SQL Server 2012では、関数CONCATが導入されました。この関数は、文字列入力のリストを受け入れてそれらを連結し、その間、NULLを無視します。したがって、CONCATを使用すると、次のようにソリューションを簡素化できます。

    SELECT empid, CONCAT(country, ',' + region, ',', city) AS location
    FROM HR.Employees;

    それでも、関数の入力の一部としてセパレータを明示的に指定する必要があります。私たちの生活をさらに楽にするために、SQL Server 2017では、CONCAT_WSと呼ばれる同様の関数が導入されました。この関数では、区切り文字を指定し、次に連結する項目を指定します。この機能を使用すると、ソリューションは次のようにさらに簡略化されます。

    SELECT empid, CONCAT_WS(',', country, region, city) AS location
    FROM HR.Employees;

    次のステップはもちろん心を読むことです。 2020年4月1日に、MicrosoftはCONCAT_MRのリリースを計画しています。この関数は空の入力を受け入れ、心を読み取ることで、連結する要素を自動的に判断します。クエリは次のようになります:

    SELECT empid, CONCAT_MR() AS location
    FROM HR.Employees;

    LOGには2番目のパラメータがあります

    EOMONTH関数と同様に、多くの人は、SQL Server 2012以降、LOG関数が対数の底を示すことができる2番目のパラメーターをサポートしていることに気づいていません。それ以前は、T-SQLは、入力の自然対数を返す関数LOG(input)(定数eをベースとして使用)、および10をベースとして使用するLOG10(input)をサポートしていました。

    人々がLogb を計算したいときに、LOG関数の2番目のパラメーターの存在に気づいていません (x)、ここでbはeと10以外のベースであり、彼らはしばしばそれを長い道のりで行いました。次の方程式に頼ることができます:

    ログb (x)=Log a (x)/ Log a (b)

    例として、Log 2を計算するには (8)、次の方程式に依存します:

    ログ2 (8)=Log e (8)/ Log e (2)

    T-SQLに変換すると、次の計算が適用されます。

    DECLARE @x AS FLOAT = 8, @b AS INT = 2;
    SELECT LOG(@x) / LOG(@b);

    LOGがベースを示す2番目のパラメーターをサポートしていることに気付くと、計算は単純に次のようになります。

    DECLARE @x AS FLOAT = 8, @b AS INT = 2;
    SELECT LOG(@x, @b);
    カーソル変数

    しばらくの間T-SQLを使用している場合は、カーソルを使用する機会が多かったでしょう。ご存知のように、カーソルを操作するときは、通常、次の手順を使用します。

    • カーソルを宣言します
    • カーソルを開きます
    • カーソルレコードを反復処理します
    • カーソルを閉じます
    • カーソルの割り当てを解除します

    例として、インスタンス内のデータベースごとにいくつかのタスクを実行する必要があるとします。カーソルを使用すると、通常、次のようなコードを使用します。

    DECLARE @dbname AS sysname;
     
    DECLARE C CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
      SELECT name FROM sys.databases;
     
    OPEN C;
     
    FETCH NEXT FROM C INTO @dbname;
     
    WHILE @@FETCH_STATUS = 0
    BEGIN
      PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
      /* ... do your thing here ... */
      FETCH NEXT FROM C INTO @dbname;
    END;
     
    CLOSE C;
    DEALLOCATE C;

    CLOSEコマンドは、現在の結果セットを解放し、ロックを解放します。 DEALLOCATEコマンドはカーソル参照を削除し、最後の参照の割り当てが解除されると、カーソルを構成するデータ構造を解放します。 CLOSEコマンドとDEALLOCATEコマンドを使用せずに上記のコードを2回実行しようとすると、次のエラーが発生します。

    Msg 16915, Level 16, State 1, Line 4
    A cursor with the name 'C' already exists.
    Msg 16905, Level 16, State 1, Line 6
    The cursor is already open.

    続行する前に、必ずCLOSEコマンドとDEALLOCATEコマンドを実行してください。

    多くの人は、カーソルを1つのバッチで操作する必要がある場合、これが最も一般的なケースであり、通常のカーソルを使用する代わりに、カーソル変数を操作できることに気づいていません。他の変数と同様に、カーソル変数のスコープは、それが宣言されたバッチのみです。これは、バッチが終了するとすぐに、すべての変数が期限切れになることを意味します。カーソル変数を使用すると、バッチが終了すると、SQL Serverはそれを自動的に閉じて割り当てを解除するため、CLOSEおよびDEALLOCATEコマンドを明示的に実行する必要がなくなります。

    今回はカーソル変数を使用して修正されたコードを次に示します。

    DECLARE @dbname AS sysname, @C AS CURSOR;
     
    SET @C = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
      SELECT name FROM sys.databases;
     
    OPEN @C;
     
    FETCH NEXT FROM @C INTO @dbname;
     
    WHILE @@FETCH_STATUS = 0
    BEGIN
      PRINT N'Handling database ' + QUOTENAME(@dbname) + N'...';
      /* ... do your thing here ... */
      FETCH NEXT FROM @C INTO @dbname;
    END;

    自由に複数回実行してください。今回はエラーが発生しないことに注意してください。クリーンなだけで、カーソルを閉じて割り当てを解除するのを忘れた場合でも、カーソルリソースを保持することを心配する必要はありません。

    出力とのマージ

    SQL Server 2005で変更ステートメントのOUTPUT句が開始されて以来、変更された行からデータを返したい場合は常に、非常に実用的なツールであることがわかりました。人々は、アーカイブ、監査、その他の多くのユースケースなどの目的でこの機能を定期的に使用しています。ただし、この機能の厄介な点の1つは、INSERTステートメントで使用する場合、出力列の前に insert を付けて、挿入された行からのみデータを返すことができることです。 。ターゲットの列と一緒にソースの列を返す必要がある場合でも、ソーステーブルの列にアクセスすることはできません。

    例として、次のコードを実行して作成および入力するテーブルT1およびT2について考えてみます。

    DROP TABLE IF EXISTS dbo.T1, dbo.T2;
    GO
     
    CREATE TABLE dbo.T1(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
     
    CREATE TABLE dbo.T2(keycol INT NOT NULL IDENTITY PRIMARY KEY, datacol VARCHAR(10) NOT NULL);
     
    INSERT INTO dbo.T1(datacol) VALUES('A'),('B'),('C'),('D'),('E'),('F');

    両方のテーブルでキーを生成するためにIDプロパティが使用されていることに注意してください。

    T1からT2にいくつかの行をコピーする必要があるとします。たとえば、keycol%2 =1の場合です。OUTPUT句を使用してT2で新しく生成されたキーを返しますが、それらのキーと一緒にT1からのそれぞれのソースキーも返します。直感的な期待は、次のINSERTステートメントを使用することです。

    INSERT INTO dbo.T2(datacol)
        OUTPUT T1.keycol AS T1_keycol, inserted.keycol AS T2_keycol
      SELECT datacol FROM dbo.T1 WHERE keycol % 2 = 1;

    ただし、残念ながら、前述のように、OUTPUT句ではソーステーブルの列を参照できないため、次のエラーが発生します。

    メッセージ4104、レベル16、状態1、行2
    マルチパート識別子「T1.keycol」をバインドできませんでした。

    多くの人は、奇妙なことに、この制限がMERGEステートメントに適用されないことに気づいていません。したがって、少し厄介ですが、INSERTステートメントをMERGEステートメントに変換できますが、そのためには、MERGE述語が常にfalseである必要があります。これにより、WHEN NOT MATCHED句がアクティブになり、サポートされている唯一のINSERTアクションが適用されます。 1=2などのダミーのfalse条件を使用できます。完全に変換されたコードは次のとおりです。

    MERGE INTO dbo.T2 AS TGT
    USING (SELECT keycol, datacol FROM dbo.T1 WHERE keycol % 2 = 1) AS SRC 
      ON 1 = 2
    WHEN NOT MATCHED THEN
      INSERT(datacol) VALUES(SRC.datacol)
    OUTPUT SRC.keycol AS T1_keycol, inserted.keycol AS T2_keycol;

    今回はコードが正常に実行され、次の出力が生成されます。

    T1_keycol   T2_keycol
    ----------- -----------
    1           1
    3           2
    5           3

    うまくいけば、Microsoftは他の変更ステートメントのOUTPUT句のサポートを強化して、ソーステーブルからも列を返すことができるようにします。

    結論

    想定しないでください、そしてRTFM! :-)


    1. 外部キーがある場合とない場合の参照の違いは何ですか

    2. クエリ結果から結果のランダムサンプルを選択します

    3. MySQLをSQliteに変換する

    4. SQLサーバーに制約が存在するかどうかを確認するにはどうすればよいですか?