2014-10-31 19 views
5

Ho una funzione scalare SQLCLR che eseguirà il vapore di un XmlReader che ho bisogno di distruggere su richiesta in un gruppo di risultati in linea. Questi oggetti XML sono generati su richiesta, quindi non posso usare un indice XML. Sarà comune avere oltre 100 colonne nei set di dati risultanti. Considerate questo codice di esempio:Complessità del piano XQuery

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 

Questo riempie una colonna XML tipizzata in una variabile di tabella con alcuni valori XML e rompe gli attributi in colonne separate. Il piano di esecuzione per questo sembra eccessivamente disordinato:

|--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 

Ha un ciclo annidato per ogni colonna! Il piano di query sarà troppo complesso se mi unisco a più di queste tabelle con 100 colonne ciascuna. Inoltre, non capisco lo scopo di quegli operatori StreamAggregate. I contenuti vanno così:

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! Pensavo che l'utilizzo di un gruppo XML digitato con un tipo sqltype avrebbe dovuto evitare la conversione?

O sto sopravvalutando quanto efficiente sarà o sto facendo qualcosa di sbagliato. La mia domanda è come posso risolvere questo problema in modo da non avere operatori di piani di query aggiuntivi aggiunti per ogni colonna e idealmente evitare le conversioni, o dovrei rinunciare e trovare un modo non-xpath di fare questo?

Riferimenti:

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

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

enter image description here

+1

Il solito modo lo faccio è 'SELEZIONA r.value ('@ a', 'int'), r.value ('@ b ',' int '), r.value (' @ c ',' int ') DA @ D APPLICAZIONE CROSS x.nodes ('/r ') come ca (r) 'ma il piano ha la stessa forma (con i nodi TVF dati un costo del sottostrato stimato molto più elevato) –

risposta

11

Ci sono alcuni misteri del piano di query che deve essere risolto prima. Che cosa fa il calcolo scalare e perché c'è un flusso aggregato.

La funzione con valori di tabella restituisce una tabella di nodi dell'XML triturato, una riga per ogni riga triturata. Quando si utilizza l'XML digitato, quelle colonne sono valore, lvalue, lvaluebin e tid. Queste colonne vengono utilizzate nello scalare del calcolo per calcolare il valore effettivo. Il codice lì sembra un po 'strano e non posso dire di aver capito perché è così com'è, ma il succo è che la funzione xsd_cast_to_maybe_large restituisce il valore e c'è un codice che gestisce il caso quando il valore è uguale a e maggiore di 128 byte.

Lo stesso scalare di calcolo per XML non tipizzato è molto più semplice e comprensibile.

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 

Se vi sono più di 128 byte in value recuperano da lvalue altro prelevare da value. Nel caso di XML non tipizzato, la tabella dei nodi restituiti emette solo le colonne id, value e lvalue.

Quando si utilizza l'XML digitato, la memorizzazione dei valori del nodo viene ottimizzata in base al tipo di dati specificato nello schema. Sembra che possa finire nel valore, lvalue o lvaluebin nella tabella dei nodi a seconda del tipo di valore che è e xsd_cast_to_maybe_large è lì per aiutare a risolvere le cose.

L'aggregazione di flusso esegue una min() sui valori restituiti dallo scalare di calcolo.Sappiamo e SQL Server (almeno a volte) sa che ci sarà sempre una sola riga restituita dalla funzione di valore della tabella quando si specifica un XPath nella funzione value(). Il parser si assicura che creiamo l'XPath correttamente ma quando l'ottimizzatore di query guarda le righe stimate vede 200 righe. La stima di base per la funzione valutata a livello di tabella che analizza l'XML è di 10000 righe e quindi sono state apportate alcune rettifiche utilizzando lo XPath utilizzato. In questo caso finisce con 200 righe dove ce n'è solo una. La pura speculazione da parte mia è che l'aggregato del flusso è lì per prendersi cura di questa discrepanza. Non aggregherà mai nulla, invia solo la riga che viene restituita, ma influisce sulla stima della cardinalità per l'intero ramo e si assicura che l'ottimizzatore utilizzi 1 righe come stima per quel ramo. Questo è ovviamente molto importante quando l'ottimizzatore sceglie le strategie di join, ecc.

Quindi, che ne dici di 100 attributi? Sì, ci saranno 100 filiali se si utilizza la funzione valore 100 volte. Ma ci sono alcune ottimizzazioni da fare qui. Ho creato un banco di prova per vedere quale forma e forma della query sarebbe stata la più veloce utilizzando 100 attributi su 10 righe.

Il vincitore era utilizzare XML non tipizzato e non utilizzare la funzione nodes() per distruggere r.

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 

V'è anche un modo per evitare le 100 filiali che utilizzano perno, ma a seconda di ciò che la vostra query reale sembra che potrebbe non essere possibile. Il tipo di dati che esce dal pivot deve essere lo stesso. Ovviamente potresti estrarli come una stringa e convertirli in un tipo appropriato nell'elenco delle colonne. Richiede inoltre che la tabella abbia una chiave primaria/unica.

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 

piano Domanda per interrogazione perno, 10 righe 100 attributi:

enter image description here

seguito si riportano i risultati e il banco di prova ho usato. Ho provato con 100 attributi e 10 righe e tutti gli attributi int.

Risultato:

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 

Codice:

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 dettagliato e informativo con alternative profilate! Ho considerato l'opzione PIVOT, potrebbe anche generare direttamente le chiavi/nome/valore tuple direttamente da CLR in quel caso. Speravo di usare XML per renderlo più efficiente. Questo è il tipo di risposta che rende così eccezionale! –