使用 CTE 为视图编制索引的选项



我有一个视图,我想为其创建一个索引视图。经过大量的精力,我能够为视图放置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)

好友余额视图的示例数据 -

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。我现在有哪些选择?

如果可以删除 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,b,c 支付 300 美元,这意味着 B 将欠 110 美元,C 将欠 A 10 美元。

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

好的,这为您提供了一个索引视图(需要额外的视图来整理谁欠谁的详细信息),但它可能仍然不能满足您的要求。

/* 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是正确的 - 但它可能会增加比您想要的更多的开销。

如果您希望我添加这些方面的脚本 - 这取决于您希望/需要数据库的限制程度。

相关内容

  • 没有找到相关文章

最新更新