2014-10-31 12 views
5

Ich habe eine SQLCLR-Skalarfunktion, die einen XmlReader dampft, den ich bei Bedarf in eine Inline-Ergebnismenge zerlegen muss. Diese XML-Objekte werden bei Bedarf generiert, sodass ich keinen XML-Index verwenden kann. Es wird üblich sein, über 100 Spalten in den resultierenden Datensätzen zu haben. Betrachten Sie diesen Beispielcode:XQuery-Plan-Komplexität

CREATE XML SCHEMA COLLECTION RAB AS ' 
<xsd:schema xmlns:schema="urn:schemas-microsoft-com:sql:SqlRowSet1" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:sqltypes="http://schemas.microsoft.com/sqlserver/2004/sqltypes" elementFormDefault="qualified"> 
<xsd:import namespace="http://schemas.microsoft.com/sqlserver/2004/sqltypes" schemaLocation="http://schemas.microsoft.com/sqlserver/2004/sqltypes/sqltypes.xsd" /> 

    <xsd:element name="r" type="r"/> 

    <xsd:complexType name="r"> 
    <xsd:attribute name="a" type="sqltypes:int" use="required"/> 
    <xsd:attribute name="b" type="sqltypes:int" use="required"/> 
    <xsd:attribute name="c" type="sqltypes:int" use="required"/> 
    </xsd:complexType> 
</xsd:schema>'; 
GO 

DECLARE @D TABLE(x XML(DOCUMENT RAB) NOT NULL); 

INSERT INTO @D 
VALUES 
('<r a="3" b="4" c="34"/>'), 
('<r a="5" b="6" c="56"/>'), 
('<r a="7" b="8" c="78"/>') 

SELECT x.value('/r/@a', 'int') a, x.value('/r/@b', 'int') b, x.value('/r/@c', 'int') c 
FROM @d a 

Dieser füllt eine typisierte XML-Spalte in einer Tabelle Variable mit einigen XML-Werte und bricht die Attribute in separaten Spalten. Der Ausführungsplan dafür scheint übermäßig unordentlich zu sein:

|--Compute Scalar(DEFINE:([Expr1009]=[Expr1008], [Expr1016]=[Expr1015], [Expr1023]=[Expr1022])) 
    |--Nested Loops(Inner Join, OUTER REFERENCES:([a].[x])) 
     |--Nested Loops(Inner Join, OUTER REFERENCES:([a].[x])) 
      | |--Nested Loops(Inner Join, OUTER REFERENCES:([a].[x])) 
      | | |--Table Scan(OBJECT:(@d AS [a])) 
      | | |--Stream Aggregate(DEFINE:([Expr1008]=MIN([Expr1024]))) 
      | |   |--Compute Scalar(DEFINE:([Expr1024]=CASE WHEN datalength(CONVERT_IMPLICIT(sql_variant,CONVERT_IMPLICIT(nvarchar(64),xsd_cast_to_maybe_large(XML Reader with XPath filter.[value],XML Reader with XPath filter.[lvalue],XML Reader wi 
      | |    |--Table-valued function 
      | |--Stream Aggregate(DEFINE:([Expr1015]=MIN([Expr1025]))) 
      |   |--Compute Scalar(DEFINE:([Expr1025]=CASE WHEN datalength(CONVERT_IMPLICIT(sql_variant,CONVERT_IMPLICIT(nvarchar(64),xsd_cast_to_maybe_large(XML Reader with XPath filter.[value],XML Reader with XPath filter.[lvalue],XML Reader with XP 
      |    |--Table-valued function 
      |--Stream Aggregate(DEFINE:([Expr1022]=MIN([Expr1026]))) 
       |--Compute Scalar(DEFINE:([Expr1026]=CASE WHEN datalength(CONVERT_IMPLICIT(sql_variant,CONVERT_IMPLICIT(nvarchar(64),xsd_cast_to_maybe_large(XML Reader with XPath filter.[value],XML Reader with XPath filter.[lvalue],XML Reader with XPath f 
        |--Table-valued function 

Es hat eine verschachtelte Schleife für jede Spalte! Der Abfrageplan wird viel zu komplex sein, wenn ich mehrere dieser Tabellen mit jeweils 100 Spalten zusammenführe. Außerdem verstehe ich nicht den Zweck dieser StreamAggregate Betreiber. Der Inhalt geht so:

MIN(
    CASE WHEN @d.[x] as [a].[x] IS NULL 
    THEN NULL ELSE 
    CASE WHEN datalength(CONVERT_IMPLICIT(sql_variant, 
      CONVERT_IMPLICIT(nvarchar(64),xsd_cast_to_maybe_large(xrpf.[value],xrpf.[lvalue],xrpf.[lvaluebin],xrpf.[tid],(15),(7)) 
      ,0),0))>=(128) 
    THEN CONVERT_IMPLICIT(int, 
     CASE WHEN datalength(xsd_cast_to_maybe_large(xrpf.[value],xrpf.[lvalue],xrpf.[lvaluebin],xrpf.[tid],(15),(7)))<(128) 
     THEN NULL 
     ELSE xsd_cast_to_maybe_large(xrpf.[value],xrpf.[lvalue],xrpf.[lvaluebin],xrpf.[tid],(15),(7)) 
     END,0) 
     ELSE CONVERT_IMPLICIT(int, 
      CONVERT_IMPLICIT(sql_variant,CONVERT_IMPLICIT(nvarchar(64),xsd_cast_to_maybe_large(xrpf.[value],xrpf.[lvalue],xrpf.[lvaluebin],xrpf.[tid],(15),(7)),0),0),0) 
    END 
END) 

Yuck! Ich dachte, die Verwendung einer typisierten XML-Gruppe mit einem -Typ sollte die Konvertierung vermeiden?

Entweder ich überschätze, wie effizient das sein wird oder ich mache etwas falsch. Meine Frage ist, wie kann ich das beheben, so dass ich keine zusätzlichen Abfrageplanoperatoren für jede Spalte hinzugefügt habe und idealerweise die Konvertierungen vermeiden würde, oder sollte ich aufgeben und eine Nicht-XPath-Methode dafür finden?

Referenzen:

SqlTypes http://msdn.microsoft.com/en-us/library/ee320775%28v=sql.105%29.aspx

XML-Datentypmethoden http://technet.microsoft.com/en-us/library/ms190798%28v=sql.105%29.aspx

enter image description here

+1

Die übliche Art, wie ich es tun, ist 'SELECT r.value ('@ a', 'int'), r.value ('@ b ',' int '), r.value (' @ c ',' int ') FROM @D KREUZ Übernehmen Sie x.nodes ('/r ') als ca (r) 'aber der Plan sieht sehr ähnlich aus (mit den TVF-Knoten eine viel höhere geschätzte Teilbaum Kosten gegeben) –

Antwort

11

Es gibt einige Geheimnisse in der Abfrage-Plan, der zuerst aussortiert werden muss. Was macht der Rechenskalar und warum gibt es ein Stromaggregat?

Die Tabellenwertfunktion gibt eine Knotentabelle des zerschnittenen XML zurück, eine Zeile für jede geschredderte Zeile. Wenn Sie typisiertes XML verwenden, sind diese Spalten value, lvalue, lvaluebin und tid. Diese Spalten werden im Rechenskalar verwendet, um den tatsächlichen Wert zu berechnen. Der Code darin sieht ein bisschen komisch aus und ich kann nicht sagen, dass ich verstehe, warum es so ist, aber das Wesentliche ist, dass die Funktion xsd_cast_to_maybe_large den Wert zurückgibt und es Code gibt, der den Fall behandelt, wenn der Wert gleich ist und größer als 128 Bytes.

CASE WHEN datalength(
        CONVERT_IMPLICIT(sql_variant, 
         CONVERT_IMPLICIT(nvarchar(64), 
             xsd_cast_to_maybe_large(XML Reader with XPath filter.[value], 
                   XML Reader with XPath filter.[lvalue], 
                   XML Reader with XPath filter.[lvaluebin], 
                   XML Reader with XPath filter.[tid],(15),(5),(0)),0),0))>=(128) 
    THEN CONVERT_IMPLICIT(int,CASE WHEN datalength(xsd_cast_to_maybe_large(XML Reader with XPath filter.[value], 
                     XML Reader with XPath filter.[lvalue], 
                     XML Reader with XPath filter.[lvaluebin], 
                     XML Reader with XPath filter.[tid],(15),(5),(0)))<(128) 
           THEN NULL 
           ELSE xsd_cast_to_maybe_large(XML Reader with XPath filter.[value], 
                  XML Reader with XPath filter.[lvalue], 
                  XML Reader with XPath filter.[lvaluebin], 
                  XML Reader with XPath filter.[tid],(15),(5),(0)) 
          END,0) 
    ELSE CONVERT_IMPLICIT(int,CONVERT_IMPLICIT(sql_variant, 
               CONVERT_IMPLICIT(nvarchar(64), 
                   xsd_cast_to_maybe_large(XML Reader with XPath filter.[value], 
                         XML Reader with XPath filter.[lvalue], 
                         XML Reader with XPath filter.[lvaluebin], 
                         XML Reader with XPath filter.[tid],(15),(5),(0)),0),0),0) 
END 

Derselbe Rechenskalar für nicht typisiertes XML ist viel einfacher und tatsächlich verständlich.

CASE WHEN datalength(XML Reader with XPath filter.[value])>=(128) 
    THEN CONVERT_IMPLICIT(int,XML Reader with XPath filter.[lvalue],0) 
    ELSE CONVERT_IMPLICIT(int,XML Reader with XPath filter.[value],0) 
END 

Wenn es mehr als 128 Bytes in value holen von lvalue sonst aus value holen. Bei nicht typisiertem XML gibt die zurückgegebene Knotentabelle nur die Spalten id, value und lvalue aus.

Wenn Sie typisiertes XML verwenden, wird der Speicher der Knotenwerte basierend auf dem im Schema angegebenen Datentyp optimiert. Es sieht so aus, als könnte es entweder in value, lvalue oder lvaluebin in der Knotentabelle enden, abhängig davon, um welchen Wert es sich handelt und xsd_cast_to_maybe_large hilft dabei, die Dinge zu klären.

Das Stream-Aggregat führt eine min() über die zurückgegebenen Werte aus dem Rechenskalar aus.Wir wissen, und SQL Server weiß (zumindest manchmal), dass immer nur eine Zeile von der Tabellenwertfunktion zurückgegeben wird, wenn Sie in der Funktion value() einen XPath angeben. Der Parser stellt sicher, dass wir den XPath korrekt erstellen, aber wenn der Abfrageoptimierer die geschätzten Zeilen betrachtet, werden 200 Zeilen angezeigt. Die Basisschätzung für die Tabellenwertfunktion, die XML analysiert, ist 10000 Zeilen und dann gibt es einige Anpassungen, die mit dem verwendeten XPath vorgenommen wurden. In diesem Fall endet es mit 200 Reihen, wo es nur eine gibt. Reine Spekulationen von mir sind, dass das Stromaggregat da ist, um diese Diskrepanz zu beseitigen. Es wird niemals etwas aggregieren, sondern nur die eine Zeile, die zurückgegeben wird, zurücksenden, aber es beeinflusst die Kardinalitätsschätzung für den gesamten Zweig und stellt sicher, dass der Optimierer 1 Zeilen als Schätzung für diesen Zweig verwendet. Das ist natürlich sehr wichtig, wenn der Optimierer Join-Strategien usw. auswählt.

Wie wäre es also mit 100 Attributen? Ja, es gibt 100 Verzweigungen, wenn Sie die Wertfunktion 100 Mal verwenden. Aber es gibt einige Optimierungen, die hier gemacht werden müssen. Ich habe einen Testaufbau erstellt, um zu sehen, welche Form und Form der Abfrage mit 100 Attributen über 10 Zeilen am schnellsten ist.

Der Gewinner war, untypisiertes XML zu verwenden und nicht die nodes() Funktion zu verwenden, um auf r zu shredden.

select X.value('(/r/@a1)[1]', 'int') as a1, 
     X.value('(/r/@a2)[1]', 'int') as a2, 
     X.value('(/r/@a3)[1]', 'int') as a3 
from @T 

Es gibt auch eine Möglichkeit, die 100 Filialen mit Dreh zu vermeiden, aber je nachdem, was Ihre eigentliche Abfrage sah aus wie es nicht möglich sein könnte. Der Datentyp, der vom Pivot ausgegeben wird, muss identisch sein. Sie könnten sie natürlich als String extrahieren und in die entsprechende Spaltenliste umwandeln. Außerdem muss Ihre Tabelle einen primären/eindeutigen Schlüssel aufweisen.

select a1, a2, a3 
from (
    select T.ID, -- primary key of @T 
      A.X.value('local-name(.)', 'nvarchar(50)') as Name, 
      A.X.value('.', 'int') as Value 
    from @T as T 
     cross apply T.X.nodes('/r/@*') as A(X) 
    ) as T 
pivot(min(T.Value) for Name in (a1, a2, a3)) as P 

Abfrageplan für die Pivot-Abfrage, 10 Zeilen 100 Attribute:

enter image description here

Im Folgenden finden Sie die Ergebnisse und der Prüfstand I verwendet. Ich habe mit 100 Attributen und 10 Zeilen und allen int Attributen getestet.

Ergebnis:

Test            Duration (ms) 
-------------------------------------------------- ------------- 
untyped XML value('/r[1]/@a')      195  
untyped XML value('(/r/@a)[1]')      108 
untyped XML value('@a') cross apply nodes('/r')  131 
untyped XML value('@a') cross apply nodes('/r[1]') 127 
typed XML value('/r/@a')       185 
typed XML value('(/r/@a)[1]')      148 
typed XML value('@a') cross apply nodes('/r')  176 
untyped XML pivot         34 
typed XML pivot          52 

Code:

drop type dbo.TRABType 
drop type dbo.TType; 
drop xml schema collection dbo.RAB; 

go 

declare @NumAtt int = 100; 
declare @Attribs nvarchar(max); 

with xmlnamespaces('http://www.w3.org/2001/XMLSchema' as xsd) 
select @Attribs = (
select top(@NumAtt) 'a'+cast(row_number() over(order by 1/0) as varchar(11)) as '@name', 

        'sqltypes:int' as '@type', 
        'required' as '@use' 
from sys.columns 
for xml path('xsd:attribute') 
) 
--CREATE XML SCHEMA COLLECTION RAB AS 

declare @Schema nvarchar(max) = 
' 
<xsd:schema xmlns:schema="urn:schemas-microsoft-com:sql:SqlRowSet1" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:sqltypes="http://schemas.microsoft.com/sqlserver/2004/sqltypes" elementFormDefault="qualified"> 
<xsd:import namespace="http://schemas.microsoft.com/sqlserver/2004/sqltypes" schemaLocation="http://schemas.microsoft.com/sqlserver/2004/sqltypes/sqltypes.xsd" /> 
    <xsd:element name="r" type="r"/> 
    <xsd:complexType name="r">[ATTRIBS]</xsd:complexType> 
</xsd:schema>'; 

set @Schema = replace(@Schema, '[ATTRIBS]', @Attribs) 

create xml schema collection RAB as @Schema 

go 

create type dbo.TType as table 
(
    ID int identity primary key, 
    X xml not null 
); 

go 

create type dbo.TRABType as table 
(
    ID int identity primary key, 
    X xml(document rab) not null 
); 


go 

declare @NumAtt int = 100; 
declare @NumRows int = 10; 

declare @X nvarchar(max); 
declare @C nvarchar(max); 
declare @M nvarchar(max); 

declare @S1 nvarchar(max); 
declare @S2 nvarchar(max); 
declare @S3 nvarchar(max); 
declare @S4 nvarchar(max); 
declare @S5 nvarchar(max); 
declare @S6 nvarchar(max); 
declare @S7 nvarchar(max); 
declare @S8 nvarchar(max); 
declare @S9 nvarchar(max); 

set @X = N'<r '+ 
    (
    select top(@NumAtt) 'a'+cast(row_number() over(order by 1/0) as varchar(11))+'="'+cast(row_number() over(order by 1/0) as varchar(11))+'" ' 
    from sys.columns 
    for xml path('') 
)+ 
'/>'; 

set @C = 
    stuff((
    select top(@NumAtt) ',a'+cast(row_number() over(order by 1/0) as varchar(11)) 
    from sys.columns 
    for xml path('') 
), 1, 1, '') 

set @M = 
    stuff((
    select top(@NumAtt) ',MAX(CASE WHEN name = ''a'+cast(row_number() over(order by 1/0) as varchar(11))+''' THEN val END)' 
    from sys.columns 
    for xml path('') 
), 1, 1, '') 


declare @T dbo.TType; 
insert into @T(X) 
select top(@NumRows) @X 
from sys.columns; 

declare @TRAB dbo.TRABType; 
insert into @TRAB(X) 
select top(@NumRows) @X 
from sys.columns; 


-- value('/r[1]/@a') 
set @S1 = N' 
select T.ID'+ 
(
select top(@NumAtt) ', T.X.value(''/r[1]/@a'+cast(row_number() over(order by 1/0) as varchar(11))+''', ''int'')' 
from sys.columns 
for xml path('') 
)+ 
' from @T as T 
option (maxdop 1)'; 

-- value('(/r/@a)[1]') 
set @S2 = N' 
select T.ID'+ 
(
select top(@NumAtt) ', T.X.value(''(/r/@a'+cast(row_number() over(order by 1/0) as varchar(11))+')[1]'', ''int'')' 
from sys.columns 
for xml path('') 
)+ 
' from @T as T 
option (maxdop 1)'; 

-- value('@a') cross apply nodes('/r') 
set @S3 = N' 
select T.ID'+ 
(
select top(@NumAtt) ', T2.X.value(''@a'+cast(row_number() over(order by 1/0) as varchar(11))+''', ''int'')' 
from sys.columns 
for xml path('') 
)+ 
' from @T as T 
    cross apply T.X.nodes(''/r'') as T2(X) 
option (maxdop 1)'; 


-- value('@a') cross apply nodes('/r[1]') 
set @S4 = N' 
select T.ID'+ 
(
select top(@NumAtt) ', T2.X.value(''@a'+cast(row_number() over(order by 1/0) as varchar(11))+''', ''int'')' 
from sys.columns 
for xml path('') 
)+ 
' from @T as T 
    cross apply T.X.nodes(''/r[1]'') as T2(X) 
option (maxdop 1)'; 

-- value('/r/@a') typed XML 
set @S5 = N' 
select T.ID'+ 
(
select top(@NumAtt) ', T.X.value(''/r/@a'+cast(row_number() over(order by 1/0) as varchar(11))+''', ''int'')' 
from sys.columns 
for xml path('') 
)+ 
' from @TRAB as T 
option (maxdop 1)'; 

-- value('(/r/@a)[1]') 
set @S6 = N' 
select T.ID'+ 
(
select top(@NumAtt) ', T.X.value(''(/r/@a'+cast(row_number() over(order by 1/0) as varchar(11))+')[1]'', ''int'')' 
from sys.columns 
for xml path('') 
)+ 
' from @TRAB as T 
option (maxdop 1)'; 

-- value('@a') cross apply nodes('/r') typed XML 
set @S7 = N' 
select T.ID'+ 
(
select top(@NumAtt) ', T2.X.value(''@a'+cast(row_number() over(order by 1/0) as varchar(11))+''', ''int'')' 
from sys.columns 
for xml path('') 
)+ 
' from @TRAB as T 
    cross apply T.X.nodes(''/r'') as T2(X) 
option (maxdop 1)'; 

-- pivot 
set @S8 = N' 
select ID, '[email protected]+' 
from (
    select T.ID, 
      A.X.value(''local-name(.)'', ''nvarchar(50)'') as Name, 
      A.X.value(''.'', ''int'') as Value 
    from @T as T 
     cross apply T.X.nodes(''/r/@*'') as A(X) 
    ) as T 
pivot(min(T.Value) for Name in ('[email protected]+')) as P 
option (maxdop 1)'; 

-- typed pivot 
set @S9 = N' 
select ID, '[email protected]+' 
from (
    select T.ID, 
      A.X.value(''local-name(.)'', ''nvarchar(50)'') as Name, 
      cast(cast(A.X.query(''string(.)'') as varchar(11)) as int) as Value 
    from @TRAB as T 
     cross apply T.X.nodes(''/r/@*'') as A(X) 
    ) as T 
pivot(min(T.Value) for Name in ('[email protected]+')) as P 
option (maxdop 1)'; 


exec sp_executesql @S1, N'@T dbo.TType readonly', @T; 
exec sp_executesql @S2, N'@T dbo.TType readonly', @T; 
exec sp_executesql @S3, N'@T dbo.TType readonly', @T; 
exec sp_executesql @S4, N'@T dbo.TType readonly', @T; 
exec sp_executesql @S5, N'@TRAB dbo.TRABType readonly', @TRAB; 
exec sp_executesql @S6, N'@TRAB dbo.TRABType readonly', @TRAB; 
exec sp_executesql @S7, N'@TRAB dbo.TRABType readonly', @TRAB; 
exec sp_executesql @S8, N'@T dbo.TType readonly', @T; 
exec sp_executesql @S9, N'@TRAB dbo.TRABType readonly', @TRAB; 
+2

+1 Detaillierte und informative mit profilierten Alternativen! Ich habe die PIVOT-Option berücksichtigt und kann die Schlüssel/Name/Wert-Tupel direkt in diesem Fall direkt aus CLR generieren. Ich hatte gehofft, XML zu verwenden, um es effizienter zu machen. Das ist die Antwort, die SO so großartig macht! –