使用cte索引视图的选项

时间:2012-05-28 16:57:31

标签: sql-server views common-table-expression materialized

我有一个视图,我想为其创建一个索引视图。经过大量的精力,我能够为视图提供sql查询,它看起来像这样 -

ALTER VIEW [dbo].[FriendBalances] WITH SCHEMABINDING  as
WITH

trans (Amount,PaidBy,PaidFor, Id)  AS    

(SELECT Amount,userid AS PaidBy, PaidForUsers_FbUserId AS PaidFor, Id FROM dbo.Transactions
FULL JOIN dbo.TransactionUser ON dbo.Transactions.Id = dbo.TransactionUser.TransactionsPaidFor_Id),

bal (PaidBy,PaidFor,Balance) AS

(SELECT PaidBy,PaidFor, SUM( Amount/ transactionCounts.[_count]) AS Balance FROM trans 
JOIN (SELECT Id,COUNT(*)AS _count FROM trans GROUP BY Id)   AS transactionCounts ON trans.Id = transactionCounts.Id AND trans.PaidBy <> trans.PaidFor
GROUP BY trans.PaidBy,trans.PaidFor )
SELECT ISNULL(bal.PaidBy,bal2.PaidFor)AS PaidBy,ISNULL(bal.PaidFor,bal2.PaidBy)AS PaidFor,
ISNULL( bal.Balance,0)-ISNULL(bal2.Balance,0) AS Balance
FROM bal 
left JOIN bal AS bal2 ON bal.PaidBy = bal2.PaidFor AND bal.PaidFor = bal2.Paidby   
WHERE ISNULL( bal.Balance,0)>ISNULL(bal2.Balance,0)

FriendBalances View的示例数据 -

PaidBy  PaidFor  Balance
------  -------  -------
9990    9991     1000
9990    9992     2000
9990    9993     1000
9991    9993     1000
9991    9994     1000

主要是2个表的连接。

Transactions -

CREATE TABLE [dbo].[Transactions](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Date] [datetime] NOT NULL,
    [Amount] [float] NOT NULL,
    [UserId] [bigint] NOT NULL,
    [Remarks] [nvarchar](255) NULL,
    [GroupFbGroupId] [bigint] NULL,
 CONSTRAINT [PK_Transactions] PRIMARY KEY CLUSTERED 

Transactions表中的示例数据 -

Id  Date                     Amount  UserId  Remarks         GroupFbGroupId
--  -----------------------  ------  ------  --------------  --------------
1   2001-01-01 00:00:00.000  3000    9990    this is a test  NULL
2   2001-01-01 00:00:00.000  3000    9990    this is a test  NULL
3   2001-01-01 00:00:00.000  3000    9991    this is a test  NULL

TransactionUsers -

CREATE TABLE [dbo].[TransactionUser](
    [TransactionsPaidFor_Id] [bigint] NOT NULL,
    [PaidForUsers_FbUserId] [bigint] NOT NULL
) ON [PRIMARY]

TransactionUser表中的示例数据 -

TransactionsPaidFor_Id  PaidForUsers_FbUserId
----------------------  ---------------------
1                       9991
1                       9992
1                       9993
2                       9990
2                       9991
2                       9992
3                       9990
3                       9993
3                       9994

现在我无法创建视图,因为我的查询包含cte(s)。我现在有哪些选择?

如果可以删除cte,那么另一个选项应该有助于创建索引视图。

以下是错误消息 -

  

Msg 10137, Level 16, State 1, Line 1 Cannot create index on view "ShareBill.Test.Database.dbo.FriendBalances" because it references common table expression "trans". Views referencing common table expressions cannot be indexed. Consider not indexing the view, or removing the common table expression from the view definition.

概念: 交易主要包括:

  • 支付的金额
  • 支付该金额的用户
  • UserId
  • 以及其他一些现在不重要的信息。

TransactionUser表是事务和用户表之间的映射。基本上,交易可以在多个人之间共享。所以我们将其存储在此表中。

因此,我们进行了交易,其中1人为此付款而其他人正在分享金额。因此,如果A为B支付100美元,那么B将欠A 100美元。同样如果B为A支付90美元,那么B将仅欠A $ 10.现在如果A为A支付300美元,b,c意味着B将欠110美元,而C则欠A $ 10。

因此,在这个特定的观点中,我们汇总了两个用户之间已经支付的有效金额(如果有的话),从而知道一个人欠另一个人多少钱。

1 个答案:

答案 0 :(得分:6)

好的,这会为您提供一个索引视图(需要一个额外的视图来排序who-owes-who detail),但它可能仍然不能满足您的要求。

/* Transactions table, as before, but with handy unique constraint for FK Target */
CREATE TABLE [dbo].[Transactions](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Date] [datetime] NOT NULL,
    [Amount] [float] NOT NULL,
    [UserId] [bigint] NOT NULL,
    [Remarks] [nvarchar](255) NULL,
    [GroupFbGroupId] [bigint] NULL,
 CONSTRAINT [PK_Transactions] PRIMARY KEY CLUSTERED (Id),
 constraint UQ_Transactions_XRef UNIQUE (Id,Amount,UserId)
)

到目前为止,没什么可惊讶的,我希望

/* Much expanded TransactionUser table, we'll hide it away and most of the maintenance is automatic */
CREATE TABLE [dbo]._TransactionUser(
    [TransactionsPaidFor_Id] int NOT NULL,
    [PaidForUsers_FbUserId] [bigint] NOT NULL,
    Amount float not null,
    PaidByUserId bigint not null,
    UserCount int not null,
    LowUserID as CASE WHEN [PaidForUsers_FbUserId] < PaidByUserId THEN [PaidForUsers_FbUserId] ELSE PaidByUserId END,
    HighUserID as CASE WHEN [PaidForUsers_FbUserId] < PaidByUserId THEN PaidByUserId ELSE [PaidForUsers_FbUserId] END,
    PerUserDelta as (Amount/UserCount) * CASE WHEN [PaidForUsers_FbUserId] < PaidByUserId THEN -1 ELSE 1 END,
    constraint PK__TransactionUser PRIMARY KEY ([TransactionsPaidFor_Id],[PaidForUsers_FbUserId]),
    constraint FK__TransactionUser_Transactions FOREIGN KEY ([TransactionsPaidFor_Id]) references dbo.Transactions,
    constraint FK__TransactionUser_Transaction_XRef FOREIGN KEY ([TransactionsPaidFor_Id],Amount,PaidByUserID)
        references dbo.Transactions (Id,Amount,UserId) ON UPDATE CASCADE
)

此表现在保留足够的信息以允许构建视图。我们所做的其余工作是构建/维护表中的数据。请注意,使用外键约束,我们已经确保如果在事务表中更改了金额,则会重新计算所有内容。

/* View that mimics the original TransactionUser table -
in fact it has the same name so existing code doesn't need to change */
CREATE VIEW dbo.TransactionUser
with schemabinding
as
    select
        [TransactionsPaidFor_Id],
        [PaidForUsers_FbUserId]
    from
        dbo._TransactionUser
GO
/* Effectively the PK on the original table */
CREATE UNIQUE CLUSTERED INDEX PK_TransactionUser on dbo.TransactionUser ([TransactionsPaidFor_Id],[PaidForUsers_FbUserId])

任何已经针对TransactionUser编写的内容现在都会违背此观点,并且不会更明智。除此之外,他们无法在没有帮助的情况下插入/更新/删除行:

/* Now we write the trigger that maintains the underlying table */
CREATE TRIGGER dbo.T_TransactionUser_IUD
ON dbo.TransactionUser
INSTEAD OF INSERT, UPDATE, DELETE
AS
    SET NOCOUNT ON;
    /* Every delete affects *every* row for the same transaction
    We need to drop the counts on every remaining row, as well as removing the actual rows we're interested in */
    WITH DropCounts as (
        select TransactionsPaidFor_Id,COUNT(*) as Cnt from deleted group by TransactionsPaidFor_Id
    ), KeptRows as (
        select tu.TransactionsPaidFor_Id,tu.PaidForUsers_FbUserId,UserCount - dc.Cnt as NewCount
        from dbo._TransactionUser tu left join deleted d
            on tu.TransactionsPaidFor_Id = d.TransactionsPaidFor_Id and
                tu.PaidForUsers_FbUserId = d.PaidForUsers_FbUserId
            inner join DropCounts dc
                on
                    tu.TransactionsPaidFor_Id = dc.TransactionsPaidFor_Id
        where
            d.PaidForUsers_FbUserId is null
    ), ChangeSet as (
        select TransactionsPaidFor_Id,PaidForUsers_FbUserId,NewCount,1 as Keep
        from KeptRows
        union all
        select TransactionsPaidFor_Id,PaidForUsers_FbUserId,null,0
        from deleted
    )
    merge into dbo._TransactionUser tu
    using ChangeSet cs on tu.TransactionsPaidFor_Id = cs.TransactionsPaidFor_Id and tu.PaidForUsers_FbUserId = cs.PaidForUsers_FbUserId
    when matched and cs.Keep = 1 then update set UserCount = cs.NewCount
    when matched then delete;

    /* Every insert affects *every* row for the same transaction
    This is why the indexed view couldn't be generated */
    WITH TU as (
        select TransactionsPaidFor_Id,PaidForUsers_FbUserId,Amount,PaidByUserId from dbo._TransactionUser
        where TransactionsPaidFor_Id in (select TransactionsPaidFor_Id from inserted)
        union all
        select TransactionsPaidFor_Id,PaidForUsers_FbUserId,Amount,UserId
        from inserted i inner join dbo.Transactions t on i.TransactionsPaidFor_Id = t.Id
    ), CountedTU as (
        select TransactionsPaidFor_Id,PaidForUsers_FbUserId,Amount,PaidByUserId,
            COUNT(*) OVER (PARTITION BY TransactionsPaidFor_Id) as Cnt
        from TU
    )
    merge into dbo._TransactionUser tu
    using CountedTU new on tu.TransactionsPaidFor_Id = new.TransactionsPaidFor_Id and tu.PaidForUsers_FbUserId = new.PaidForUsers_FbUserId
    when matched then update set Amount = new.Amount,PaidByUserId = new.PaidByUserId,UserCount = new.Cnt
    when not matched then insert
        ([TransactionsPaidFor_Id],[PaidForUsers_FbUserId],Amount,PaidByUserId,UserCount)
    values (new.TransactionsPaidFor_Id,new.PaidForUsers_FbUserId,new.Amount,new.PaidByUserId,new.Cnt);

现在我们正在维护基础表,我们最终可以写出你想要的索引视图......差不多。问题在于我们创建的总数可能是正数还是负数,因为我们已经对交易进行了规范化,以便我们可以轻松地将它们相加:

CREATE VIEW [dbo]._FriendBalances
WITH SCHEMABINDING
as
    SELECT
        LowUserID,
        HighUserID,
        SUM(PerUserDelta) as Balance,
        COUNT_BIG(*) as Cnt
    FROM dbo._TransactionUser
    WHERE LowUserID != HighUserID
    GROUP BY
        LowUserID,
        HighUserID
GO
create unique clustered index IX__FriendBalances on dbo._FriendBalances (LowUserID, HighUserID)

因此,我们最终创建了一个视图,建立在上面的索引视图上,如果余额是负数,我们会翻转所欠的人,以及周围的人。但它会在上面的视图中使用索引,这是我们通过索引视图寻求保存的大部分工作:

create view dbo.FriendBalances
as
    select
        CASE WHEN Balance >= 0 THEN LowUserID ELSE HighUserID END as PaidBy,
        CASE WHEN Balance >= 0 THEN HighUserID ELSE LowUserID END as PaidFor,
        ABS(Balance) as Balance
    from
        dbo._FriendBalances WITH (NOEXPAND)

现在,最后,我们插入您的示例数据:

set identity_insert dbo.Transactions on --Ensure we get IDs we know
GO
insert into dbo.Transactions (Id,[Date] ,   Amount , UserId , Remarks ,GroupFbGroupId)
select 1   ,'2001-01-01T00:00:00.000', 3000,    9990    ,'this is a test',  NULL union all
select 2   ,'2001-01-01T00:00:00.000', 3000,    9990    ,'this is a test',  NULL union all
select 3   ,'2001-01-01T00:00:00.000', 3000,    9991    ,'this is a test',  NULL
GO
set identity_insert dbo.Transactions off
GO
insert into dbo.TransactionUser (TransactionsPaidFor_Id,  PaidForUsers_FbUserId)
select 1,   9991 union all
select 1,   9992 union all
select 1,   9993 union all
select 2,   9990 union all
select 2,   9991 union all
select 2,   9992 union all
select 3,   9990 union all
select 3,   9993 union all
select 3,   9994

查询最终视图:

select * from dbo.FriendBalances

PaidBy  PaidFor Balance
9990    9991    1000
9990    9992    2000
9990    9993    1000
9991    9993    1000
9991    9994    1000

现在,如果我们担心某人可能会找到一种方法来躲避触发器并对基表执行直接更改,那么我们可以做其他工作。第一个是另一个索引视图,它将确保同一事务的每一行都具有相同的 UserCount值。最后,通过一些额外的列,检查约束,FK约束以及触发器中的更多工作,我认为我们可以确保UserCount是正确的 - 但它可能会增加比你更多的开销想。

如果您需要,我可以为这些方面添加脚本 - 这取决于您想要/需要数据库的限制。