3

Microsoft は、TSQL を解析および生成するためのscriptdomAPIを公開しました。私はそれに慣れていないので、まだ遊んでいます。このようなクエリからデータベース間の参照を取得する方法を知りたいです。

UPDATE  t3
SET     description = 'abc'
FROM    database1.dbo.table1 t1
        INNER JOIN database2.dbo.table2 t2
            ON (t1.id = t2.t1_id)
        LEFT OUTER JOIN database3.dbo.table3 t3
            ON (t3.id = t2.t3_id)
        INNER JOIN database2.dbo.table4 t4
            ON (t4.id = t2.t4_id)

私が欲しいのは参考文献のリストです:

database1.dbo.table1.id = database2.dbo.table2.t1_id
database3.dbo.table3.id = database2.dbo.table2.t3_id
database2.dbo.table4.id = database2.dbo.table2.t4_id

ただし、最後のエントリについてはdatabase2.dbo.table4.id = database2.dbo.table2.t4_id、両端の列の両方が同じデータベースからのdatabase2ものであり、これは私が望むものではありません。したがって、最終的に必要な結果は次のとおりです。

database1.dbo.table1.id = database2.dbo.table2.t1_id
database3.dbo.table3.id = database2.dbo.table2.t3_id

で実装することは可能scriptdomですか?

4

2 に答える 2

9

堅牢な実装は容易ではありません。この質問で提起された限られた問題の場合、解決策は比較的単純です-「比較的」強調します。私は次のことを想定しています:

  • クエリには 1 つのレベルしかありません。UNION、サブクエリ、WITH 式、またはエイリアスの新しいスコープを導入するその他のものはありません (これはすぐに複雑になる可能性があります)。
  • クエリ内のすべての識別子は完全修飾されているため、どのオブジェクトを参照しているかは疑いの余地がありません。

ソリューション戦略は次のようになります。最初に にアクセスしTSqlFragmentてすべてのテーブル エイリアスのリストを作成し、次に再度アクセスしてすべての等結合を取得し、途中でエイリアスを展開します。そのリストを使用して、同じデータベースを参照していない等結合のリストを特定します。コード内:

var sql = @"
  UPDATE  t3
  SET     description = 'abc'
  FROM    database1.dbo.table1 t1
      INNER JOIN database2.dbo.table2 t2
        ON (t1.id = t2.t1_id)
      LEFT OUTER JOIN database3.dbo.table3 t3
        ON (t3.id = t2.t3_id)
      INNER JOIN database2.dbo.table4 t4
        ON (t4.id = t2.t4_id)

";                

var parser = new TSql120Parser(initialQuotedIdentifiers: false);
IList<ParseError> errors;
TSqlScript script;
using (var reader = new StringReader(sql)) {
  script = (TSqlScript) parser.Parse(reader, out errors);
}
// First resolve aliases.
var aliasResolutionVisitor = new AliasResolutionVisitor();
script.Accept(aliasResolutionVisitor);

// Then find all equijoins, expanding aliases along the way.
var findEqualityJoinVisitor = new FindEqualityJoinVisitor(
  aliasResolutionVisitor.Aliases
);
script.Accept(findEqualityJoinVisitor);

// Now list all aliases where the left database is not the same
// as the right database.
foreach (
  var equiJoin in 
  findEqualityJoinVisitor.EqualityJoins.Where(
    j => !j.JoinsSameDatabase()
  )
) {
  Console.WriteLine(equiJoin.ToString());
}

出力:

database3.dbo.table3.id = database2.dbo.table2.t3_id
database1.dbo.table1.id = database2.dbo.table2.t1_id

AliasResolutionVisitor簡単なことです:

public class AliasResolutionVisitor : TSqlFragmentVisitor {
  readonly Dictionary<string, string> aliases = new Dictionary<string, string>();
  public Dictionary<string, string> Aliases { get { return aliases; } }

  public override void Visit(NamedTableReference namedTableReference ) {
    Identifier alias = namedTableReference.Alias;
    string baseObjectName = namedTableReference.SchemaObject.AsObjectName();
    if (alias != null) {
      aliases.Add(alias.Value, baseObjectName);
    }
  }
}

クエリ内のすべての名前付きテーブル参照を調べて、エイリアスがある場合は、これを辞書に追加します。このビジターにはスコープの概念がないため、サブクエリが導入された場合、これは惨めに失敗することに注意してください (実際、ビジターにスコープを追加することはTSqlFragment、解析ツリーに注釈を付けたり、ノードからそれをウォークする方法さえないため、はるかに困難です)。

EqualityJoinVisitorもっと興味深いです:

public class FindEqualityJoinVisitor : TSqlFragmentVisitor {
  readonly Dictionary<string, string> aliases;
  public FindEqualityJoinVisitor(Dictionary<string, string> aliases) {
    this.aliases = aliases;
  }

  readonly List<EqualityJoin> equalityJoins = new List<EqualityJoin>();
  public List<EqualityJoin> EqualityJoins { get { return equalityJoins; } }

  public override void Visit(QualifiedJoin qualifiedJoin) {
    var findEqualityComparisonVisitor = new FindEqualityComparisonVisitor();
    qualifiedJoin.SearchCondition.Accept(findEqualityComparisonVisitor);
    foreach (
      var equalityComparison in findEqualityComparisonVisitor.Comparisons
    ) {
      var firstColumnReferenceExpression = 
        equalityComparison.FirstExpression as ColumnReferenceExpression
      ;
      var secondColumnReferenceExpression = 
        equalityComparison.SecondExpression as ColumnReferenceExpression
      ;
      if (
        firstColumnReferenceExpression != null && 
        secondColumnReferenceExpression != null
      ) {
        string firstColumnResolved = resolveMultipartIdentifier(
          firstColumnReferenceExpression.MultiPartIdentifier
        );
        string secondColumnResolved = resolveMultipartIdentifier(
          secondColumnReferenceExpression.MultiPartIdentifier
        );
        equalityJoins.Add(
          new EqualityJoin(firstColumnResolved, secondColumnResolved)
        );
      }
    }
  }

  private string resolveMultipartIdentifier(MultiPartIdentifier identifier) {
    if (
      identifier.Identifiers.Count == 2 && 
      aliases.ContainsKey(identifier.Identifiers[0].Value)
    ) {
      return 
        aliases[identifier.Identifiers[0].Value] + "." + 
        identifier.Identifiers[1].Value;
    } else {
      return identifier.AsObjectName();
    }
  }
}

これはQualifiedJoinインスタンスを探し、それらが見つかった場合は、検索条件を調べて等価比較のすべての出現箇所を見つけます。これは、ネストされた検索条件でも機能することに注意してください。 ではBar JOIN Foo ON Bar.Quux = Foo.Quux AND Bar.Baz = Foo.Baz、両方の式が見つかります。

それらをどのように見つけますか?別の小さなビジターを使用する:

public class FindEqualityComparisonVisitor : TSqlFragmentVisitor {
  List<BooleanComparisonExpression> comparisons = 
    new List<BooleanComparisonExpression>()
  ;
  public List<BooleanComparisonExpression> Comparisons { 
    get { return comparisons; } 
  }

  public override void Visit(BooleanComparisonExpression e) {
    if (e.IsEqualityComparison()) comparisons.Add(e);
  }
}

ここでは何も複雑ではありません。このコードを他の訪問者に折り畳むのは難しくありませんが、これはより明確だと思います。

コメントなしで提示するいくつかのヘルパー コードを除いて、それだけです。

public class EqualityJoin {
  readonly SchemaObjectName left;
  public SchemaObjectName Left { get { return left; } }

  readonly SchemaObjectName right;
  public SchemaObjectName Right { get { return right; } }

  public EqualityJoin(
    string qualifiedObjectNameLeft, string qualifiedObjectNameRight
  ) {
    var parser = new TSql120Parser(initialQuotedIdentifiers: false);
    IList<ParseError> errors;
    using (var reader = new StringReader(qualifiedObjectNameLeft)) {
      left = parser.ParseSchemaObjectName(reader, out errors);
    }
    using (var reader = new StringReader(qualifiedObjectNameRight)) {
      right = parser.ParseSchemaObjectName(reader, out errors);
    }
  }

  public bool JoinsSameDatabase() {
    return left.Identifiers[0].Value == right.Identifiers[0].Value;
  }

  public override string ToString() {
    return String.Format("{0} = {1}", left.AsObjectName(), right.AsObjectName());
  }
}

public static class MultiPartIdentifierExtensions {
  public static string AsObjectName(this MultiPartIdentifier multiPartIdentifier) {
    return string.Join(".", multiPartIdentifier.Identifiers.Select(i => i.Value));
  }
}

public static class ExpressionExtensions {
  public static bool IsEqualityComparison(this BooleanExpression expression) {
    return 
      expression is BooleanComparisonExpression && 
      ((BooleanComparisonExpression) expression).ComparisonType == BooleanComparisonType.Equals
    ;
  }
}

前述したように、このコードは非常に脆弱です。クエリには特定の形式があると想定しており、そうでない場合は失敗する可能性があります (誤解を招く結果を与えることにより、非常に悪い)。主な未解決の課題は、スコープと非修飾参照を正しく処理できるように拡張することです。また、T-SQL スクリプトが特徴とするその他の奇妙さにも対応できますが、それでも出発点としては有用だと思います。

于 2014-12-04T22:07:14.940 に答える
3

おそらく、これを試みる別の方法は、クエリを次のように実行することです。

SET SHOWPLAN_XML ON;
UPDATE  t3
SET     description = 'abc'
FROM    database1.dbo.table1 t1
        INNER JOIN database2.dbo.table2 t2
            ON (t1.id = t2.t1_id)
        LEFT OUTER JOIN database3.dbo.table3 t3
            ON (t3.id = t2.t3_id)
        INNER JOIN database2.dbo.table4 t4
            ON (t4.id = t2.t4_id)

これは、XML クエリ プランを返します。XML では、RelOp ノードの下に結合条件があります。たとえば、ハッシュ結合ループの場合、次のようになります。

<RelOp NodeId="7" PhysicalOp="Hash Match" LogicalOp="Inner Join" EstimateRows="1" EstimateIO="0" EstimateCPU="0.0177716" AvgRowSize="15" EstimatedTotalSubtreeCost="0.0243408" Parallel="0" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row">
.. some stuff cut from here
  <Hash>
..
<ProbeResidual>
  <ScalarOperator ScalarString="[database2].[dbo].[table4].[Id] as [t4].[Id]=[database2].[dbo].[table2].[t4_Id] as [t2].[t4_Id]">
   <Compare CompareOp="EQ">
     <ScalarOperator>
       <Identifier>
         <ColumnReference Database="[database2]" Schema="[dbo]" Table="[table4]" Alias="[t4]" Column="Id" />
       </Identifier>
     </ScalarOperator>
     <ScalarOperator>
       <Identifier>
         <ColumnReference Database="[database2]" Schema="[dbo]" Table="[table2]" Alias="[t2]" Column="t4_Id" />
       </Identifier>
     </ScalarOperator>
   </Compare>
 </ScalarOperator>

ネストされたループの場合、次の行に沿って何か:

<NestedLoops Optimized="0">
<Predicate>
  <ScalarOperator ScalarString="[database3].[dbo].[table3].[Id] as [t3].[Id]=[database2].[dbo].[table2].[t3_id] as [t2].[t3_id]">
    <Compare CompareOp="EQ">
      <ScalarOperator>
        <Identifier>
          <ColumnReference Database="[database3]" Schema="[dbo]" Table="[table3]" Alias="[t3]" Column="Id" />
        </Identifier>
      </ScalarOperator>
      <ScalarOperator>
        <Identifier>
          <ColumnReference Database="[database2]" Schema="[dbo]" Table="[table2]" Alias="[t2]" Column="t3_id" />
        </Identifier>
      </ScalarOperator>
    </Compare>
  </ScalarOperator>
</Predicate>

おそらく、これを C# で処理してすべての結合を抽出し、列参照に保持されているデータベースを比較できます。

整形でごめんなさい。

于 2014-12-09T00:17:30.957 に答える