Cannot create indexed view - sql-server

CREATE TABLE orders
(
order_no INT NOT NULL PRIMARY KEY,
prod_id INT NOT NULL,
quantity INT
);
CREATE VIEW product_stats WITH SCHEMABINDING
AS
SELECT a.prod_id, a.product_name,
(SELECT COUNT(*) FROM dbo.orders WHERE prod_id = a.prod_id) AS total FROM dbo.products a;
CREATE UNIQUE CLUSTERED INDEX [IDX_Order_Details_X]
ON product_stats (prod_id, total)
It complains:
Column 'total' in view 'product_stats' cannot be used in an index or statistics or as a partition key because it does user or system data access.
DB is ms sql.

An indexed view cannot contain COUNT(*) or a subquery. See the "View Restrictions" section of this article.

Related

Creating INSERT trigger that sets values to 0

I have two tables : Invoice and Invoice_item, relationship 1 to many.
The Invoice_item table has columns Number_sold and Item_price, and the Invoice table has Number_sold_total and Item_price_total columns that will store total values of columns Number_sold and Item_price from the Invoice_item table with the same Invoice_ID key.
CREATE TABLE [Invoice] (
[Invoice_ID] [int] NOT NULL,
[Number_sold_total] [int] NOT NULL,
[Item_price_total] [decimal] NOT NULL,
PRIMARY KEY ([Invoice_ID]));
CREATE TABLE [Invoice_item] (
[Invoice_item_ID] [int] NOT NULL,
[Invoice_ID] [int] NOT NULL,
[Number_sold] [int] NOT NULL,
[Item_price] [decimal] NOT NULL,
PRIMARY KEY ([Invoice_item_ID],[Invoice_ID],
FOREIGN KEY ([Invoice_ID]) REFERENCES [Invoice]([Invoice_ID]);
So, if there are three rows in Invoice_item with the same Invoice_ID, the row with that Invoice_ID in Invoice table will have SUM values of corresponding columns in Invoice_item table.
Let's say i have three rows in Invoice_item table and columns Item_price with values 100,200 and 300, and they have the Invoice_ID = 3. The column Item_price_total in Invoice will have value of 600, where the Invoice_ID = 3.
QUESTION -
My task is to create an insert trigger on table Invoice that will set the values of Number_sold_total and Item_price_total to 0(ZERO) if there is no Invoice_item with corresponding Invoice_ID -> IF NOT EXISTS (Invoice.Invoice_ID = Invoice_item.Invoice_ID)...
I am using SQL Server 2017.
Ideally you would not implement this using triggers.
Instead you should use a view. If you are worried about querying performance, you can index it, at the cost of insert and delete performance.
CREATE VIEW dbo.Invoice_Totals
WITH SCHEMABINDING
AS
SELECT
i.Invoice_ID,
Number_sold = SUM(i.Number_sold),
Item_price = SUM(i.Item_price),
ItemCount = COUNT_BIG(*) -- must include count for indexed view
FROM dbo.Invoice_item;
And then index it
CREAT UNIQUE CLUSTERED INDEX CX_Invoice_Totals ON Invoice_Totals
(Invoice_ID);
If you really, really want to do this using triggers, you can use the following
CREATE OR ALTER TRIGGER TR_Invoice_Total
ON dbo.Invoice_item
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON; -- prevent spurious resultsets
IF (NOT EXISTS (SELECT 1 FROM inserted) AND NOT EXISTS (SELECT 1 FROM deleted))
RETURN; -- early bail-out if no rows
UPDATE i
SET Number_sold_total += totals.Number_sold_total,
Item_price_total += totals.Item_price_total
FROM Invoice i
JOIN (
SELECT
Invoice_ID = ISNULL(i.Invoice_ID, d.Invoice_ID),
Number_sold_total = SUM(ISNULL(i.Number_sold, 0) - ISNULL(d.Number_sold, 0)),
Item_price_total = SUM(ISNULL(i.Item_price, 0) - ISNULL(d.Item_price, 0))
FROM inserted i
FULL JOIN deleted d ON d.Invoice_ID = i.Invoice_ID
GROUP BY
ISNULL(i.Invoice_ID, d.Invoice_ID)
) totals
ON totals.Invoice_Id = i.Invoice_ID;
db<>fiddle
The steps of the trigger are as follows:
Bail out early if the modification affected 0 rows.
Join the inserted and deleted tables together on the primary key. This needs to be a full-join, because in an INSERT there are no deleted and in a DELETE there are no inserted rows.
Group up the changed rows by Invoice_ID, taking the sum of the differences.
Join back to the Invoice table
Update the Invoice table adding the total difference to each column.
This effectively recreates what the indexed view would do for you automatically.
You cannot just select the first row from inserted and deleted into variables, as there may be multiple rows affected. You must join and group them

Cross apply vs CTE in Sql Server

I have two tables
the first table is named: tblprovince
create table (
provinceid int not null primary key (1,1) ,
provinceNme nvarchar(max),
description nvarchar(max))
the second table is named tblcity:
create table tblcity(
cityid int identity (1,1),
CityName nvarchar(max),
population int,
provinceid int foreign key references tblprovince(provinceid)
);
I need to list all provinces that have at least two large cities. A large city is defined as having a population of at least one million residents. The query must return the following columns:
tblProvince.ProvinceId
tblProvince.ProvinceName
a derived column named LargeCityCount that presents the total count of large cities for the province
 
select p.provinceId, p.provincename, citysummary.LargeCityCount
from tblprovince p
cross apply (
select count(*) as LargeCityCount from tblcity c
where c.population >= 1000000 and c.provinceid=p.provinceid
) citysummary
where citysummary.LargeCityCount
Is this query correct?
Are there other methods that allow me to achieve my goal?
SELECT tp.provinceid, tp.provinceNme, COUNT(tc.cityid) AS largecitycount
FROM tblprovince tp INNER JOIN
tblcity tc ON tc.provinceid=tp.provinceid
WHERE tc.population>=1000000
GROUP BY tp.provinceid, tp.provinceNme
HAVING COUNT(tc.cityid)>1

Self Join on large tables slowness issue

I have two tables like...
table1 (cid, duedate, currency, value)
main_table1 (cid)
My query is like below, I am find out co-relation between each cid and table1 contains 3 million records(cid and duedate column is compositely unique) and main_table contains 1500 records all unique.
SELECT
b.cid, c.cid,
(COUNT(*) * SUM(b.value * c.value) -
SUM(b.value) * SUM(c.value)) /
(SQRT(COUNT(*) * SUM(b.value * b.value) -
SUM(b.value) * SUM(b.value)) *
SQRT(COUNT(*) * SUM(c.value * c.value) -
SUM(c.value) * SUM(c.value))
) AS correl_ij
FROM
main_table1 a
JOIN
table1 AS b ON a.cid = b.cid
JOIN
table1 AS c ON b.cid < c.cid
AND b.duedate = c.duedate
AND b.currency = c.currency
GROUP BY
b.cid, c.cid
Please suggest how to optimize this query because it is running slow.
CREATE TABLE #table1(
id int identity,
cid int NOT NULL,
duedate date NOT NULL,
currency char(3) NOT NULL,
value float,
PRIMARY KEY(id,currency,cid,duedate)
);
CREATE TABLE #main_table1(
cid int NOT NULL PRIMARY KEY,
currency char(3)
);
--#main table contains 155000 cid records there is no duplicate values
insert into #main_table1
values(19498,'ABC'),(19500,'ABC'),(19534,'ABC')
INSERT INTO #table1(CID,DUEDATE,currency,value)
VALUES(19498,'2016-12-08','USD',-0.0279702098021799) ,
(19498,'2016-12-12','USD',0.0151285161000268),
(19498,'2016-12-15','USD',-0.00965080868337728),
(19498,'2016-12-19','USD',0.00808331709091531)
There are 3 million records in this table for diffrent dates and cid and most of the cid are present in #main_table1.
I am using a.cid < b.cid to remove duplicate relationship between a.cid and b.cid beause i am deriving corelation between each cid.
so 19498 -->>19500 corelation is calculated hence then i do not want 19500--> 19498 because it would be same but duplicate.
That PK is silly. Why would you include Iden in a composite PK let alone in the first position? Drop Iden unless you have to have it for some misguided reason.
PRIMARY KEY(cid, currency, duedate)
Or the natural key if different
If you're commonly joining or sorting on the cid column, you probably want a clustered index on that column or a composite beginning with that column.
If cid, duedate is unique then you can consider removing the id altogether.
If you want to retain id for some reason, make it PRIMARY KEY NONCLUSTERED, and specify a clustered index on cid, duedate.

Speed up view performance

I have an old view that takes 4 mins to run, I have been asked to speed it up. The FROM looks like this:
FROM TableA
CROSS JOIN ViewA
INNER JOIN TableB on ViewA.Name = TableB.Name
AND TableA.Code = TableB.Code
AND TableA.Location = TableB.Location
WHERE (DATEDIFF(m, ViewA.SubmitDate, GETDATE()) = 1) -- Only pull last months rows
Table A has around 99k rows, ViewA has around 2000 rows and TableB has around 101K rows. I think the problem is at the INNER JOIN because it I remove it, the query takes 1 second.
My first thought was to see if I could down the number of rows in ViewA by breaking the whole thing into CTEs but this made zero impact. I am thinking I need to index TableB, because it is just a bunch of varchars being used in the joins. I am now changing it to temp tables so I can index it. I can not change the underlying tables and views. Is index temp tables a good way to go, or is there a better solution.
Edit to add info regarding existing indexes. Only thing with an index on it right now is TableA.Id which is the PK and a clustered Index. TableB has an Id field but it is not the PK. ViewA is not indexed.
Edit again to correct some structure. SubmitDate is in the View, not the table.
Here is a very basic structure:
CREATE TABLE TableA
(
Id int NOT NULL PRIMARY KEY,
Section varchar(20) NULL,
Code varchar(20) NULL
)
CREATE TABLE TableB
(
Id int NOT NULL PRIMARY KEY,
Name varchar(20) NULL,
Code varchar(20) NULL,
Section varchar(20) NULL
)
CREATE TABLE TableC
(
Id int NOT NULL PRIMARY KEY,
Name varchar(20) NULL,
SubmitDate DateTime NOT NULL
)
CREATE TABLE TableD
(
Id int NOT NULL PRIMARY KEY,
Section varchar(20) NULL
)
CREATE VIEW ViewA
AS
SELECT c.Section, d.Name, c.SubmitDate
FROM TableC c
JOIN TableD d ON a.Id = b.Id
One improovement is to rewrite where clause into sargable clause. Add index to SubmitDate if there is no index and change query to:
FROM TableA
CROSS JOIN ViewA
INNER JOIN TableB on ViewA.Name = TableB.Name
AND TableA.Code = TableB.Code
AND TableA.Location = TableB.Location
WHERE
TableA.SubmitDate >=DATEADD(MONTH,DATEDIFF(MONTH,0,GETDATE())-1,0)
And TableA.SubmitDate < Dateadd(DAY, 1, DATEADD(MONTH,
DATEDIFF(MONTH, -1, GETDATE())-1, -1) )
Also add nonclustered indexes on Name, Code and Location columns.

TSQL to insert a set of rows and dependent rows

I have 2 tables:
Order (with a identity order id field)
OrderItems (with a foreign key to order id)
In a stored proc, I have a list of orders that I need to duplicate. Is there a good way to do this in a stored proc without a cursor?
Edit:
This is on SQL Server 2008.
A sample spec for the table might be:
CREATE TABLE Order (
OrderID INT IDENTITY(1,1),
CustomerName VARCHAR(100),
CONSTRAINT PK_Order PRIMARY KEY (OrderID)
)
CREATE TABLE OrderItem (
OrderID INT,
LineNumber INT,
Price money,
Notes VARCHAR(100),
CONSTRAINT PK_OrderItem PRIMARY KEY (OrderID, LineNumber),
CONSTRAINT FK_OrderItem_Order FOREIGN KEY (OrderID) REFERENCES Order(OrderID)
)
The stored proc is passed a customerName of 'fred', so its trying to clone all orders where CustomerName = 'fred'.
To give a more concrete example:
Fred happens to have 2 orders:
Order 1 has line numbers 1,2,3
Order 2 has line numbers 1,2,4,6.
If the next identity in the table was 123, then I would want to create:
Order 123 with lines 1,2,3
Order 124 with lines 1,2,4,6
On SQL Server 2008 you can use MERGE and the OUTPUT clause to get the mappings between the original and cloned id values from the insert into Orders then join onto that to clone the OrderItems.
DECLARE #IdMappings TABLE(
New_OrderId INT,
Old_OrderId INT)
;WITH SourceOrders AS
(
SELECT *
FROM Orders
WHERE CustomerName = 'fred'
)
MERGE Orders AS T
USING SourceOrders AS S
ON 0 = 1
WHEN NOT MATCHED THEN
INSERT (CustomerName )
VALUES (CustomerName )
OUTPUT inserted.OrderId,
S.OrderId INTO #IdMappings;
INSERT INTO OrderItems
SELECT New_OrderId,
LineNumber,
Price,
Notes
FROM OrderItems OI
JOIN #IdMappings IDM
ON IDM.Old_OrderId = OI.OrderID

Resources