極簡邏輯表達式的設計和查詢
在數據庫開發中,對兩個關系表進行連接查詢,能夠直接做“邏輯或”的查詢,而對於邏輯與和邏輯非的查詢,則稍復雜點,需要編寫額外的代碼來實現。在關系型數據庫中,所謂的連接,實際上是集合的包含,只要包含一項,就滿足連接條件,實現的邏輯或,這種設計,能夠滿足絕大多數的查詢需求。有時,對於一條數據,可能需要通過多個邏輯表達式來定性,比如,判定一篇文章是否跟Microsoft Azure有關,通常簡單的做法是從多個關鍵字的邏輯組合來定性:文章中同時含有關鍵字“Microsoft”和“Azure”,或者同時含有關鍵字“Windows”和“Azure”,把這種邏輯組合抽象成表達式,就是:( Microsoft & Azure ) | ( Windows & Azure )。
邏輯表達式的基本操作符是:與(&),或(|)和非(!),邏輯表達式的最小組合是:A&B,A|B 和 !A。關系型數據庫的開發人員,在設計邏輯表達式時,必須保證滿足業務需求,同時,盡可能支持多種邏輯組合,通常情況下,按照表達式的關系,我們把邏輯表達式拆分成四個元數據類型:Expression(表達式),SubExpression(子表達式),Operator(操作符)和Operand(操作數)。
在我目前接觸的項目中,業務需求的邏輯表達式的組合相對簡單,標準的表達式格式如下所示:
Expression = ((A & B) | (C & D)) &!(E | F)
該邏輯表達式表示:查詢語句返回的結果集中,不能包含E和F,但是,必須包含A和B,或者包含C且不包含D。
對於該類表達式,我們可以抽象成更為通用的邏輯公式是:
Expression= (SubExpression1 | SubExpression2 | ...) & !ExcludeExpression SubExpression=Operand1 & Operand2 & ... ExcludeExpression=Operand1 | Operand1 | ...
這種高度概括的表達式蘊含的邏輯關系是:SubExpression中操作數之間的關系是邏輯與,ExcludeExpression中操作數之間的關系是邏輯或,該表達式蘊含的意思是:任意一條數據,不能夠包括ExcludeExpression中的任何一個項目,但是,必須至少滿足一個子表達式(SubExpression),子表達式中的項目都是邏輯與的關系。
一,用關系表存儲邏輯表達式
舉個例子,為了描述方便,我們用下面的邏輯表達式來說明:
Expression= ((A & B & C) | (C & D)) & !(E | F)
該表達式可以拆分成三個子表達式:A & B & C,C & D, !(E | F),對於子表達式 (A & B & C,C & D),其操作數之間的關系都是邏輯與;而對於子表達式!(E | F),其操作數之間的關系是邏輯或。
1,通過XML文檔表示邏輯表達式
邏輯表達式的子表達式之間,不是無關系的,為了在不同的應用程序間傳遞邏輯表達式,數據結構必須包含該表達式的所有關系和操作數,XML文檔特別適合表達有特定關系的數據結構,按照之前拆分的四種元數據類型,我們把XML文檔定義為三種不同的標簽,分別是<Expression>,<SubExpression>和<Operand>,並為標簽設置不同的屬性,通過格式化的XML實現邏輯表達式的轉存,示例如下:
declare @xml xml set @xml=‘ <Expression ID="1"> <SubExpression ID="1" OperandType="Tag" Operator="and"> <Operand ID="1" /> <Operand ID="2" /> <Operand ID="3" /> </SubExpression> <SubExpression ID="2" OperandType="Tag" Operator="and"> <Operand ID="4" /> <Operand ID="5" /> </SubExpression> <SubExpression ID="3" OperandType="Tag" Operator="not"> <Operand ID="6" /> <Operand ID="7" /> </SubExpression> </Expression>‘
2,把邏輯表達式存儲到關系表
XML格式的文檔適合數據的傳遞,不適合直接存儲在關系型數據庫中,並且XML格式的文檔也不利於對數據執行查詢操作,因此,必須把XML文檔存儲到數據表中,以利用關系數據庫引擎執行集合操作的優勢,實現數據的快速查詢。當應用程序接收到這個XML文檔,必須把XML蘊含的表達式拆分成:表達式(Expression),子表達式(SubExpression),操作數(Operand)和操作符(Operator),例如,我們可以利用以下腳本創建數據表:
create table dbo.Operands ( ExpressionID bigint not null, SubExpressionID smallint not null, OperandID bigint not null, --EntityID, TagID Operator varchar(8) not null, --&,! OperandType varchar(8) not null, --Entity,Tag primary key clustered(ExpressionID,SubExpressionID,OperandID) ) with(data_compression=page); go
註:OperandType是操作數的類型,分為:Entity和Tag,相應的操作數ID(OperandID)就是EntityID和TagID,本例只使用TagID,EntityID沒有使用。
SQL Server內置函數用於解析XML文檔,通過XML解析函數,我們可以利用TSQL腳本把XML文檔插入到表Operands中。這意味著,在表Operands中,當Operator列值為not時,表示邏輯非,表示OperandID是<ExcludeExpression>標簽存儲的操作數;當Operator列值為and時,表示邏輯與,表示OperandID是<SubExpression>標簽存儲的操作數。
;with cte_Expressions as ( select e.v.query(‘.‘) as Expression ,e.v.value(‘@ID‘,‘int‘) as ExpressionID from @xml.nodes(‘/Expression‘) as e(v) ) ,cte_SubExpression as ( select e.ExpressionID ,se.SubExpression ,se.SubExpressionID ,se.OperandType ,se.Operator from cte_Expressions e cross apply ( select t.v.query(‘.‘) as SubExpression ,t.v.value(‘@ID‘,‘int‘) as SubExpressionID ,t.v.value(‘@OperandType‘,‘varchar(16)‘) as OperandType ,t.v.value(‘@Operator‘,‘varchar(16)‘) as Operator from e.Expression.nodes(‘/Expression/SubExpression‘) as t(v) ) as se ) --insert into dbo.Operands select p.ExpressionID ,p.SubExpressionID ,d.OperandID ,p.Operator ,p.OperandType from cte_SubExpression p cross apply ( select t.v.value(‘@ID‘,‘int‘) as OperandID ,t.v.value(‘@Exclude‘,‘int‘) as Exclude from p.SubExpression.nodes(‘/SubExpression/Operand‘) as t(v) ) as dView Code
二,使用TSQL實現邏輯與和邏輯非
數據表包含的數據是Tag和Data之間的映射關系,如果Data包含的Tag滿足邏輯表達式,那麽就認為Data和邏輯表達式(Expression)之間存在映射關系,也就是說,Data滿足Expression。
例如,有以下數據表DataTable,該表只有兩列DataID和TagID,表示Data具有相應的Tag:
create table dbo.DataTable ( DataID bigint not null, TagID bigint not null, ) go insert into dbo.DataTable (DataID,TagID) values (1,1) ,(1,2) ,(1,3) ,(1,7) ,(2,4) ,(2,5) ,(3,1) ,(3,2) ,(3,3) ,(4,1) ,(4,3) ,(6,6) goView Code
1,邏輯非的實現
邏輯非的含義是:在DataTable中,一個DataID不能包含任意一個ExcludeExpression中的TagID。
;with cte_exclude_data as ( select distinct dt.DataID from dbo.DataTable dt inner join dbo.Operands o on dt.TagID=o.OperandID where Operator=‘not‘ ) select dt.DataID ,dt.TagID from dbo.DataTable dt left join cte_exclude_data ed on dt.DataID=ed.DataID where ed.DataID is null
2,邏輯與的實現
邏輯與的含義是:在DataTable中,一個DataID必須包含SubExpression中的所有Tag。
;with cte_operands as ( select o.ExpressionID ,o.SubExpressionID ,o.OperandID ,count(0) over(partition by o.ExpressionID,o.SubExpressionID) as Operands from dbo.Operands o where o.Operator=‘and‘ ) select o.ExpressionID ,o.SubExpressionID ,o.Operands ,dt.DataID from cte_operands o inner join dbo.DataTable dt on o.OperandID=dt.TagID group by o.ExpressionID ,o.SubExpressionID ,o.Operands ,dt.DataID having count(distinct dt.TagID)=o.Operands
3,查詢腳本示例
把邏輯與和邏輯非的代碼合並到一起,就能實現表達式蘊含的邏輯,這裏給出一個示例腳本:
;with cte_operands as ( select o.ExpressionID ,o.SubExpressionID ,o.OperandID ,count(o.OperandID) over(partition by o.ExpressionID,o.SubExpressionID) as Operands from dbo.Operands o where Operator=‘and‘ ) ,cte_exclude_data as ( select distinct d.DataID from dbo.DataTable d inner join dbo.Operands o on d.TagID=o.OperandID where o.Operator=‘not‘ ) ,cte_data as ( select d.DataID ,d.TagID from dbo.DataTable d left join cte_exclude_data ed on d.DataID=ed.DataID where ed.DataID is null ) select o.ExpressionID ,o.SubExpressionID ,dt.DataID from cte_operands o inner join cte_data dt on o.OperandID=dt.TagID group by o.ExpressionID ,o.SubExpressionID ,dt.DataID ,o.Operands having count(distinct dt.TagID)=o.OperandsView Code
附上較復雜的邏輯表達式,該格式能夠表達的邏輯組合更為豐富,後續我會分享深入探索的隨筆:
Expression = ((A & B) | (C & ! D)) & !(E | F)
本文列舉的邏輯表達式非常簡單,鑒於本人的知識和經驗有限,難免有紕漏,如果有更好的解決方案,還請不吝告知,十分感謝!
極簡邏輯表達式的設計和查詢