2013-04-24 43 views
4

我有一個XML列有這樣的元素的含量所有路徑:選擇從XML具有像圖

<Root> 
    <Word Type="pre1" Value="A" /> 
    <Word Type="pre1" Value="D" /> 

    <Word Type="base" Value="B" /> 

    <Word Type="post1" Value="C" /> 
    <Word Type="post1" Value="E" /> 
    <Word Type="post1" Value="F" /> 
</Root> 

這種模式是這樣的:

enter image description here

,並希望在MSSQL中使用XQuery選擇所有可能的路徑以獲得如下結果:

ABC ABE ABF DBC DBE DBF

或者財產以後這樣的:

<Root> 
    <Word Type="pre1" Value="A" /> 
    <Word Type="pre1" Value="D" /> 

    <Word Type="pre2" Value="G" /> 
    <Word Type="pre2" Value="H" /> 

    <Word Type="base" Value="B" /> 

    <Word Type="post1" Value="C" /> 
    <Word Type="post1" Value="E" /> 
    <Word Type="post1" Value="F" /> 
</Root> 

enter image description here

這個結果:

AHBC AHBE AHBF DHBC DHBE DHBF AGBC AGBE AGBF DGBC DGBE DGBF

回答

6

可以使用CTE來構建獨特的類型列表中,然後使用在遞歸CTE打造字符串。最後你挑出上一次迭代中生成的字符串。

with Types as 
(
    select row_number() over(order by T.N) as ID, 
     T.N.value('.', 'varchar(10)') as Type 
    from (select @XML.query('for $t in distinct-values(/Root/Word/@Type) 
          return <T>{$t}</T>') 
     ) as X(T) 
    cross apply X.T.nodes('/T') as T(N) 
), 
Recu as 
(
    select T.Type, 
     T.ID, 
     X.N.value('@Value', 'varchar(max)') as Value 
    from Types as T 
    cross apply @XML.nodes('/Root/Word[@Type=sql:column("T.Type")]') as X(N) 
    where T.ID = 1 
    union all 
    select T.Type, 
     T.ID, 
     R.Value+X.N.value('@Value', 'varchar(max)') as Value 
    from Types as T 
    inner join Recu as R 
     on T.ID = R.ID + 1 
    cross apply @XML.nodes('/Root/Word[@Type=sql:column("T.Type")]') as X(N)  
) 
select R.Value 
from Recu as R 
where R.ID = (select max(T.ID) from Types as T) 
order by R.Value 

SQL Fiddle

更新

這裏是有更好的性能版本。它將XML分解爲兩個臨時表。每種類型一個,所有單詞一個。遞歸CTE仍然是需要的,但它使用表而不是XML。 CTE中的連接使用的每個臨時表上還有一個索引。

-- Table to hold all values 
create table #Values 
(
    Type varchar(10), 
    Value varchar(10) 
); 

-- Clustered index on Type is used in the CTE 
create clustered index IX_#Values_Type on #Values(Type) 

insert into #Values(Type, Value) 
select T.N.value('@Type', 'varchar(10)'), 
     T.N.value('@Value', 'varchar(10)') 
from @XML.nodes('/Root/Word') as T(N); 

-- Table that holds one row for each Type 
create table #Types 
(
    ID int identity, 
    Type varchar(10), 
    primary key (ID) 
); 

-- Add types by document order 
-- Table-Valued Function Showplan Operator for nodes guarantees document order 
insert into #Types(Type) 
select T.Type 
from (
    select row_number() over(order by T.N) as rn, 
      T.N.value('@Type', 'varchar(10)') as Type 
    from @XML.nodes('/Root/Word') as T(N) 
    ) as T 
group by T.Type 
order by min(T.rn); 

-- Last level of types 
declare @MaxID int; 
set @MaxID = (select max(ID) from #Types); 

-- Recursive CTE that builds the strings 
with C as 
(
    select T.ID, 
     T.Type, 
     cast(V.Value as varchar(max)) as Value 
    from #Types as T 
    inner join #Values as V 
     on T.Type = V.Type 
    where T.ID = 1 
    union all 
    select T.ID, 
     T.Type, 
     C.Value + V.Value 
    from #Types as T 
    inner join C 
     on T.ID = C.ID + 1 
    inner join #Values as V 
     on T.Type = V.Type 
) 
select C.Value 
from C 
where C.ID = @MaxID 
order by C.Value; 

-- Cleanup 
drop table #Types; 
drop table #Values; 

SQL Fiddle

+0

謝謝@Mikael。這個解決方案是否保證了基於'Type'屬性的元素順序? – ARZ 2013-04-25 05:02:39

+0

@ARZ我這麼認爲,但正如你在C. M. Sperberg-McQueen的回答中所看到的那樣,當使用'distinct-values'時,順序不能保證。我將發佈一個更新,其中我不使用'distinct-values'並利用一個(或兩個)表變量來代替更「SQL」的答案。 – 2013-04-25 05:11:03

4

你需要這三個元素集的叉積,所以基本上書寫一個連接沒有條件:

for $pre in //Word[@Type="pre1"] 
for $base in //Word[@Type="base"] 
for $post in //Word[@Type="post1"] 
return concat($pre/@Value, $base/@Value, $post/@Value) 

對於擴展版本,我使用了兩個幫助函數獲取所有屬性,然後遞歸地連接結果。

看來MSSQL不允許定製XQuery函數。此代碼適用於符合規範的XQuery 1.0(和更新版本)處理器。

declare function local:call($prefix as xs:string) as xs:string* { 
    local:recursion('', 
    for $value in distinct-values(//Word/@Type[starts-with(., $prefix)]) 
    order by $value 
    return $value 
) 
}; 

declare function local:recursion($strings as xs:string*, $attributes as xs:string*) as xs:string* { 
    if (empty($attributes)) 
    then $strings 
    else 
    for $string in $strings 
    for $append in //Word[@Type=$attributes[1]] 
    return local:recursion(concat($string, $append/@Value), $attributes[position() != 1]) 
}; 

for $pre in local:call('pre') 
for $base in local:call('base') 
for $post in local:call('post') 
return concat($pre, $base, $post) 
+0

感謝@Jens,但真正循環的次數也不是一成不變的,並依賴於XML內容! (我們可能有pre2,pre3,post2,...)那麼解決方案是什麼? – ARZ 2013-04-24 09:29:04

+0

發佈示例輸入,否則它只是猜測。這些如何連接? – 2013-04-24 09:38:19

+0

Q已更新。 – ARZ 2013-04-24 12:23:48

4

如果我正確理解你的XML,所有的圖形基本上的步驟,如果沒有一步可以省略,每一個步驟可能有幾個備選序列。 (因此,通過圖的路徑集本質上是各種替代方案的笛卡爾乘積。)如果不是這樣,那麼接下來的將不會是你想要的。

獲得笛卡爾產品的最簡單方法是在笛卡爾乘積中爲每個因子使用一個for子句的XQuery FLWOR表達式,如Jens Erat的初始答案所示。

如果您事先不知道會有多少因素(因爲您不知道圖表中可能會出現什麼樣的「類型」值序列),並且不希望重新制定查詢那麼最簡單的事情就是編寫一個遞歸函數,它將一系列「類型」值作爲一個參數,並將您正在處理的「根」元素作爲另一個參數,並且一次處理一個因子。

這個函數做這項工作,爲您的樣品輸入:

declare function local:cartesian-product(
    $doc as element(), 
    $types as xs:string* 
) as xs:string* { 

    (: If we have no $types left, we are done. 
    Return the empty string. :) 
    if (empty($types)) then 
    '' 

    (: Otherwise, take the first value off the 
    sequence of types and return the Cartesian 
    product of all Words with that type and 
    the Cartesian product of all the remaining 
    types. :) 
    else 
    let $t := $types[1], 
     $rest := $types[position() > 1] 
    for $val in $doc/Word[@Type = $t]/@Value 
    for $suffix in 
     local:cartesian-product($doc,$rest) 
    return concat($val, $suffix) 
    }; 

唯一剩下的問題是越來越明顯的「類型」值的序列中的文檔順序的一點技巧之一。我們可以撥打distinct-values($doc//Word/@Type)來獲取這些值,但不能保證它們將按文檔順序排列。

Dimitre Novatchev's solution to a related problem借用,我們可以由此計算的「類型」值的適當的序列:

let $doc := <Root> 
    <Word Type="pre1" Value="A" /> 
    <Word Type="pre1" Value="D" /> 

    <Word Type="pre2" Value="G" /> 
    <Word Type="pre2" Value="H" /> 

    <Word Type="base" Value="B" /> 

    <Word Type="post1" Value="C" /> 
    <Word Type="post1" Value="E" /> 
    <Word Type="post1" Value="F" /> 
</Root> 

let $types0 := ($doc/Word/@Type), 
    $types := $types0[index-of($types0,.)[1]] 

這將返回不同的值,按文檔順序。

現在,我們已經可以計算出你想要的結果:

return local:cartesian-product($doc, $types) 

結果在從你給的順序略微不同的順序返回;我以爲你不關心結果的順序:

AGBC AGBE的AgBF AHBC AHBE AHBF DGBC DGBE DGBF DHBC DHBE DHBF

+0

非常好的答案。 +1。 – 2013-04-25 04:11:09

+0

這可能更通用一些,但我不確定這些屬性是否總是以文檔順序出現,所以我更傾向於使用一種可以定義順序的解決方案。我想他會像我的解決方案一樣有問題:MSSQL不支持自定義功能... – 2013-04-25 07:09:56