SQL SERVER update or insert after left join - sql-server

I have a Table Animals
Id | Name | Count | -- (other columns not relevant)
1 | horse | 11
2 | giraffe | 20
I want to try to insert or update values from a CSV string
Is it possible to do something like the following in 1 query?
;with results as
(
select * from
(
values ('horse'), ('giraffe'), ('lion')
)
animal_csv(aName)
left join animals on
animals.[Name] = animal_csv.aName
)
update results
set
[Count] = 1 + animals.[Count]
-- various other columns are set here
where Id is not null
--else
--insert into results ([Name], [Count]) values (results.aName, 1)
-- (essentially Where id is null)

It looks like what you're looking for is a table variable or temporary table rather than a common table expression.
If I understand your problem correctly, you are building a result set based on data you're getting from a CSV, merging it by incrementing values, and then returning that result set.
As I read your code, it looks as if your results would look like this:
aName | Id | Name | Count
horse | 1 | horse | 12
giraffe | 2 | giraffe | 21
lion | | |
I think what you're looking for in your final result set is this:
Name | Count
horse | 12
giraffe | 21
lion | 1
First, you can get from your csv and table to a resultset in a single CTE statement:
;WITH animal_csv AS (SELECT * FROM (VALUES('horse'),('giraffe'), ('lion')) a(aName))
SELECT ISNULL(Name, aName) Name
, CASE WHEN [Count] IS NULL THEN 1 ELSE 1 + [Count] END [Count]
FROM animal_csv
LEFT JOIN animals
ON Name = animal_csv.aName
Or, if you want to build your resultset using a table variable:
DECLARE #Results TABLE
(
Name VARCHAR(30)
, Count INT
)
;WITH animal_csv AS (SELECT * FROM (VALUES('horse'),('giraffe'), ('lion')) a(aName))
INSERT #Results
SELECT ISNULL(Name, aName) Name
, CASE WHEN [Count] IS NULL THEN 1 ELSE 1 + [Count] END [Count]
FROM animal_csv
LEFT JOIN animals
ON Name = animal_csv.aName
SELECT * FROM #results
Or, if you just want to use a temporary table, you can build it like this (temp tables are deleted when the connection is released/closed or when they're explicitly dropped):
;WITH animal_csv AS (SELECT * FROM (VALUES('horse'),('giraffe'), ('lion')) a(aName))
SELECT ISNULL(Name, aName) Name
, CASE WHEN [Count] IS NULL THEN 1 ELSE 1 + [Count] END [Count]
INTO #results
FROM animal_csv
LEFT JOIN animals
ON Name = animal_csv.aName
SELECT * FROM #results

Related

Stored procedure: set output parameter from first row of select

I have a stored procedure in SQL Server 2014 that selects some rows from a table with pagination, along with total row count:
SELECT
[...], COUNT(*) OVER () AS RowCount
FROM
[...]
WHERE
[...]
ORDER BY
[...]
OFFSET ([..]) ROWS FETCH NEXT 3 ROWS ONLY
Output:
+----+------+----------+
| ID | Name | RowCount |
+----+------+----------+
| 1 | Bob | 55 |
| 123| John | 55 |
| 99 | Jack | 55 |
+----+------+----------+
I would like to return results with actual data only, passing RowCount in an output parameter.
+----+------+
| ID | Name |
+----+------+
| 1 | Bob |
| 123| John |
| 99 | Jack |
+----+------+
#OutRowCount = 55
I tried with a CTE, but CTE is available only within the first SELECT:
WITH CTE AS
(
SELECT [...], COUNT(*) OVER () AS RowCount
FROM [...]
WHERE [...]
ORDER BY [...]
OFFSET ([..]) ROWS FETCH NEXT 3 ROWS ONLY
)
SELECT
ID, Name
FROM
CTE
SET #OutRowCount = (SELECT TOP 1 RowCount FROM CTE) -- here CTE is no longer defined
How can I do this? I think I can use temp table but I wonder if in this case performance might be an issue.
The "total row count" you have in mind is a bit unclear. Typically when paging you also display to total number of (filtered) rows, e.g. "Showing 3 of 42 Blue Widgets". That doesn't involve Max.
A CTE can have multiple queries, e.g.:
with
AllRows as ( -- All of the filtered rows.
select ..., Count(*) over (...) as RowCount
from ...
where ... -- Filter criteria. ),
SinglePage as ( -- One page of filtered rows.
select ...
from AllRows
order by ... -- Order here to get the correct rows in the page.
offset (...) rows fetch next 3 rows only )
select SP.Id, SP.Name,
( select Count(42) from AllRows ) as TotalRowCount -- Constant over all rows.
from SinglePage
order by ...; -- Keep the rows in the desired order.
Re: SET #OutRowCount = (SELECT TOP 1 RowCount FROM CTE)
Note that TOP 1 without order by isn't guaranteed to pick the row you have in mind.
Thanks to #Larnu and #Stu, I solved this using a table variable, this way:
CREATE PROCEDURE MyProc
#OutRowCount INT OUTPUT
AS
BEGIN
DECLARE #TempTbl TABLE (
ID INT,
Name VARCHAR(MAX),
RowCount INT
)
INSERT INTO
#TempTbl
SELECT
ID,
Name,
COUNT(*) OVER() AS TotRighe
FROM
MyTable
WHERE
[...]
ORDER BY
Name
OFFSET ([...]) ROWS FETCH NEXT 3 ROWS ONLY
SELECT
ID,
Name
FROM
#TempTbl
SET #OutRowCount = ISNULL((SELECT TOP 1 RowCount FROM #TempTbl), 0)
END

Reverse order of a XML Column in SQL Server

In a SQL Server table, I have a XML column where status are happened (first is oldest, last current status).
I have to write a stored procedure that returns the statuses: newest first, oldest last.
This is what I wrote:
ALTER PROCEDURE [dbo].[GetDeliveryStatus]
#invoiceID nvarchar(255)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #xml xml
SET #xml = (SELECT statusXML
FROM Purchase
WHERE invoiceID = #invoiceID )
SELECT
t.n.value('text()[1]', 'nvarchar(50)') as DeliveryStatus
FROM
#xml.nodes('/statuses/status') as t(n)
ORDER BY
DeliveryStatus DESC
END
Example of value in the statusXML column:
<statuses>
<status>A</status>
<status>B</status>
<status>A</status>
<status>B</status>
<status>C</status>
</statuses>
I want the procedure to return:
C
B
A
B
A
with ORDER BY .... DESC it return ALPHABETIC reversed (C B B A A)
How should I correct my procedure ?
Create a sequence for the nodes based on the existing order then reverse it.
WITH [x] AS (
SELECT
t.n.value('text()[1]', 'nvarchar(50)') as DeliveryStatus
,ROW_NUMBER() OVER (ORDER BY t.n.value('..', 'NVARCHAR(100)')) AS [Order]
FROM
#xml.nodes('/statuses/status') as t(n)
)
SELECT
DeliveryStatus
FROM [x]
ORDER BY [x].[Order] DESC
... results ...
DeliveryStatus
C
B
A
B
A
There is no need to declare a variable first. You can (and you should!) read the needed values from your table column directly. Best was an inline table valued function (rather than a SP just to read something...)
Better performance
inlineable
You can query many InvoiceIDs at once
set-based
Try this (I drop the mock-table at the end - carefull with real data!):
CREATE TABLE Purchase(ID INT IDENTITY,statusXML XML, InvocieID INT, OtherValues VARCHAR(100));
INSERT INTO Purchase VALUES('<statuses>
<status>A</status>
<status>B</status>
<status>A</status>
<status>B</status>
<status>C</status>
</statuses>',100,'Other values of your row');
GO
WITH NumberedStatus AS
(
SELECT ID
,InvocieID
, ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS Nr
,stat.value('.','nvarchar(max)') AS [Status]
,OtherValues
FROM Purchase
CROSS APPLY statusXML.nodes('/statuses/status') AS A(stat)
WHERE InvocieID=100
)
SELECT *
FROM NumberedStatus
ORDER BY Nr DESC
GO
--Clean-Up
--DROP TABLE Purchase;
The result
+---+-----+---+---+--------------------------+
| 1 | 100 | 5 | C | Other values of your row |
+---+-----+---+---+--------------------------+
| 1 | 100 | 4 | B | Other values of your row |
+---+-----+---+---+--------------------------+
| 1 | 100 | 3 | A | Other values of your row |
+---+-----+---+---+--------------------------+
| 1 | 100 | 2 | B | Other values of your row |
+---+-----+---+---+--------------------------+
| 1 | 100 | 1 | A | Other values of your row |
+---+-----+---+---+--------------------------+

SQL EXCEPT Not Working

; WITH cte AS
(SELECT p.BudgetNumber, t.MilestoneNumber FROM
(SELECT DISTINCT BudgetNumber FROM tblMilestones) p
CROSS JOIN
(SELECT DISTINCT MilestoneNumber FROM tblMilestoneTemplate) t)
SELECT BudgetNumber, MilestoneNumber FROM cte
EXCEPT (SELECT BudgetNumber, MilestoneNumber FROM tblMilestones)
ORDER BY BudgetNumber, MilestoneNumber
The query above creates all possible BudgetNumber and MilestoneNumber combinations using a cross join, and then attempts to locate combinations that are not in the tblMilestones table (I didn't create this database, I know the table prefixes are weird and this db isn't normalized).
There are no NULL entries in any of these fields. When I use this query with the EXCEPT clause above, I get some missing values (but not all), but I also get some non-missing values. When I change the EXCEPT to a LEFT JOIN, I get the same results. When I change the EXCEPT to a WHERE NOT EXISTS, I get no results at all. Can anyone please help?
SQLFiddle Output:
| BUDGETNUMBER | MILESTONENUMBER |
|--------------|-----------------|
| BA04001 | 0 |
| BA04001 | 99 |
| BA04005 | 0 |
| BA04005 | 99 |
| BA05001 | 0 |
| BA05001 | 99 |
| BA05002 | 0 |
| BA05002 | 99 |
Here is the way you would need to use NOT EXISTS correctly. You need specify where clause inside subquery to get correct result.
;
WITH cte
AS (
SELECT p.BudgetNumber
,t.MilestoneNumber
FROM (
SELECT DISTINCT BudgetNumber
FROM tblMilestones
) p
CROSS JOIN (
SELECT DISTINCT MilestoneNumber
FROM tblMilestoneTemplate
) t
)
SELECT BudgetNumber
,MilestoneNumber
FROM cte t
WHERE NOT EXISTS ( SELECT 1
FROM tblMilestones s
WHERE t.BudgetNumber = s.BudgetNumber
AND t.MilestoneNumber = s.MilestoneNumber )
ORDER BY BudgetNumber
,MilestoneNumber
Look at following two examples
DECLARE #NoPrecision AS TABLE ( MyNumber DECIMAL )
INSERT INTO #NoPrecision ( MyNumber ) VALUES ( 12345.123456789 )
SELECT * FROM #NoPrecision
output: 12345
DECLARE #Precision AS TABLE ( MyNumber DECIMAL(10,4) )
INSERT INTO #Precision ( MyNumber ) VALUES ( 12345.123456789 )
SELECT * FROM #Precision
output: 12345.1235

How to retrieve unique records having unique values in two columns from a table in SQL Server

I want to query a table where I need the result that contains unique values from two columns together. For e.g.
Table
EnquiryId | EquipmentId | Price
-----------+--------------+-------
1 | E20 | 10
1 | E50 | 40
1 | E60 | 20
2 | E30 | 90
2 | E20 | 10
2 | E90 | 10
3 | E90 | 10
3 | E60 | 10
For each EnquiryId, EquipmentId will be unique in the table. Now I want a result where I can get something like this
EnquiryId | EquipmentId | Price
-----------+--------------+-------
1 | E20 | 10
2 | E30 | 90
3 | E90 | 10
In the result each enquiryId present in the table should be displayed uniquely.
If suppose I have 3 EquipmentIds "E20,E50,E60" for EnquiryId "1".. Any random EquipmentId should be displayed from these three values only.
Any help would be appreciated. Thank you in advance.
QUERY
;WITH cte AS
(
SELECT *,
ROW_NUMBER() OVER
(PARTITION BY enquiryID
ORDER BY enquiryID ) AS RN
FROM tbl
)
SELECT enquiryID,equipmentID,Price
FROM cte
WHERE RN=1
FIND FIDDLE HERE
The following code must help you..
Sorry that I ended up in a lengthy solution only. Run it in your SSMS and see the result.
Declare #tab table (EnquiryId int, EquipmentId varchar(10),Price int)
Insert into #tab values
(1,'E20',10),
(1,'E50',40),
(1,'E60',20),
(2,'E30',90),
(2,'E20',10),
(2,'E90',10),
(3,'E90',10),
(3,'E60',10)
----------------------------------------------
Declare #s int = 1
Declare #e int,#z varchar(10)
Declare #Equipment table (EquipmentId varchar(10),ind int)
Insert into #Equipment (EquipmentId) Select Distinct EquipmentId From #tab
Declare #Enquiry table (id int identity(1,1),EnquiryId int,EquipmentId varchar(10))
Insert into #Enquiry (EnquiryId) Select Distinct EnquiryId From #tab
Set #e = ##ROWCOUNT
While #s <= #e
begin
Select Top 1 #z = T.EquipmentId
From #tab T
Join #Enquiry E On T.EnquiryId = E.EnquiryId
Join #Equipment Eq On Eq.EquipmentId = T.EquipmentId
Where E.id = #s
And Eq.ind is Null
Order by NEWID()
update #Enquiry
Set EquipmentId = #z
Where id = #s
update #Equipment
Set ind = 1
Where EquipmentId = #z
Set #s = #s + 1
End
Select T.EnquiryId,T.EquipmentId,T.Price
From #tab T
left join #Enquiry E on T.EnquiryId = E.EnquiryId
Where T.EquipmentId = E.EquipmentId
You can use GROUP BY (Typical way) to remove duplicate value.
Basic steps are:
Alter table & Add Identity Column.
Group by columns which can be dupicate.
Delete those record.
Check here Remove Duplicate Rows from a Table in SQL Server

Sequential SQL inserts when triggered by CROSS APPLY

This process has several steps which are reflected in various tables of a database:
Production --> UPDATE to the inventory table using something like
UPDATE STOR SET
STOR.BLOC1 = T.BLOC1,
STOR.BLOC2 = T.BLOC2,
STOR.BLOC3 = T.BLOC3,
STOR.PRODUCTION = T.PROD,
STOR.DELTA = T.DELTA
FROM BLDG B INNER JOIN STOR S
ON S.B_ID = B.B_ID
CROSS APPLY dbo.INVENTORIZE(B.B_ID) AS T;
The above feeds a log table with a TRIGGER like this:
CREATE TRIGGER trgrCYCLE
ON STOR
FOR UPDATE
AS
INSERT INTO dbo.INVT
(TS, BLDG, PROD, ACT, VAL)
SELECT CURRENT_TIMESTAMP, B_ID, PRODUCTION,
CASE WHEN DELTA < 0 THEN 'SELL' ELSE 'BUY' END,
DELTA
FROM inserted WHERE COALESCE(DELTA,0) <> 0
And finally, every update should INSERT a row into a financials table which I added to the TRIGGER above:
INSERT INTO dbo.FINS
(COMPANY, TS, COST2, BAL)
SELECT CORP, CURRENT_TIMESTAMP, COST,
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
FROM inserted WHERE COALESCE(COST,0) <> 0
The problem is with this line:
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
which is meant to calculate the latest balance of an account. But because the CROSS APPLY treats all the INSERTS as a batch, the calculation is done off of the same last record and I get an incorrect balance figure. Example:
COST BALANCE
----------------
1,000 <-- initial balance
-150 850
-220 780 <-- should be 630
What would be the way to solve that? A trigger on the FINS table instead for the balance calculation?
Understanding existing logic in your query
UPDATE statement will fire a trigger only once for a set or batch satisfying join conditions, Inserted statement will have all the records that are being updated. This is because of BATCH processing not because of CROSS APPLY but because of UPDATE.
In this query of yours
SELECT CORP, CURRENT_TIMESTAMP, COST,
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
FROM inserted WHERE COALESCE(COST,0) <> 0
For each CORP from an Outer query, same BAL will be returned.
(SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)
That being said, your inner query will be replaced by 1000(value you used in your example) every time CORP = 'XYZ'
SELECT CORP, CURRENT_TIMESTAMP, COST, (1000- COST)
FROM inserted WHERE COALESCE(COST,0) <> 0
Now your inserted statement has all the records that are being inserted. So every record's cost will be subtracted by 1000. Hence you are getting unexpected result.
Suggested solution
As per my understanding, you want to calculate some cumulative frequency kind of thing. Or last running total
Data Preparation for problem statement. Used my dummy data to give you an idea.
--Sort data based on timestamp in desc order
SELECT PK_LoginId AS Bal, FK_RoleId AS Cost, AddedDate AS TS
, ROW_NUMBER() OVER (ORDER BY AddedDate DESC) AS Rno
INTO ##tmp
FROM dbo.M_Login WHERE AddedDate IS NOT NULL
--Check how data looks
SELECT Bal, Cost, Rno, TS FROM ##tmp
--Considering ##tmp as your inserted table,
--I just added Row_Number to apply Top 1 Order by desc logic
+-----+------+-----+-------------------------+
| Bal | Cost | Rno | TS |
+-----+------+-----+-------------------------+
| 172 | 10 | 1 | 2012-12-05 08:16:28.767 |
| 171 | 10 | 2 | 2012-12-04 14:36:36.483 |
| 169 | 12 | 3 | 2012-12-04 14:34:36.173 |
| 168 | 12 | 4 | 2012-12-04 14:33:37.127 |
| 167 | 10 | 5 | 2012-12-04 14:31:21.593 |
| 166 | 15 | 6 | 2012-12-04 14:30:36.360 |
+-----+------+-----+-------------------------+
Alternative logic for subtracting cost from last running balance.
--Start a recursive query to subtract balance based on cost
;WITH cte(Bal, Cost, Rno)
AS
(
SELECT t.Bal, 0, t.Rno FROM ##tmp t WHERE t.Rno = 1
UNION ALL
SELECT c.Bal - t.Cost, t.Cost, t.Rno FROM ##tmp t
INNER JOIN cte c ON t.RNo - 1 = c.Rno
)
SELECT * INTO ##Fin FROM cte;
SELECT * FROM ##Fin
Output
+-----+------+-----+
| Bal | Cost | Rno |
+-----+------+-----+
| 172 | 0 | 1 |
| 162 | 10 | 2 |
| 150 | 12 | 3 |
| 138 | 12 | 4 |
| 128 | 10 | 5 |
| 113 | 15 | 6 |
+-----+------+-----+
You have to tweet your columns little bit to get this functionality into your trigger.
I think you can try a trigger on the Fins.
You can use IDENT_CURRENT('Table')) to take the last primary key from the table and make a select.
I think it's better than "select top 1".
To to take the last balance value, set a variable last_bal = select bal from FINS where primary_key = Ident_Current("FINS")
well
first sql is a game where it work with groups or rather "set" so always you have think about that.
if you work with a simple item is correct, it maybe be better approach
declare #myinsert table(id int identity(1,1), company VArchar(35), ts datetime, cost2 smallmoney, bal smallmoney)
insert into #myinsert(company,ts, cost2, bal)
SELECT CORP, CURRENT_TIMESTAMP, COST,
FROM inserted WHERE COALESCE(COST,0) <> 0
declare #current int
select #current = min(id) from #myinsert
while exists(select * from #myinsert where id = #current)
begin
INSERT INTO dbo.FINS
(COMPANY, TS, COST2, BAL)
SELECT COMPANY, CURRENT_TIMESTAMP, COST,
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = my.COMPANY ORDER BY TS DESC)- COST)
from #myinsert my where id = #current
select #current = min(id) from #myinsert where id > #current
end
i am not giving you exact query .For a moment forget trigger.Because you are unable to test your query .
I suggest to use Output clause .This will atleast help you to construct proper query and test it.
this query is running ok,(if you can use merge then that is best).
Declare #t table
(
BLOC1,BLOC2,BLOC3 ,PRODUCTION ,DELTA --whatever column is require here
)
UPDATE STOR SET
STOR.BLOC1 = T.BLOC1,
STOR.BLOC2 = T.BLOC2,
STOR.BLOC3 = T.BLOC3,
STOR.PRODUCTION = T.PROD,
STOR.DELTA = T.DELTA
Output inserted.BLOC1 ,inserted.BLOC2, and so on into #t
FROM BLDG B INNER JOIN STOR S
ON S.B_ID = B.B_ID
CROSS APPLY dbo.INVENTORIZE(B.B_ID) AS T;
now you have inserted value in table variable #t
SELECT CORP, CURRENT_TIMESTAMP, COST,
BAL,Row_Number() over(partition by company order by TS desc) RN
FROM #t inner join FINS on COMPANY = CORP
WHERE COALESCE(COST,0) <> 0
Verify this query till here.Think of optimizing or trigger later on.
I think i gave good suggestion.and I guess subtraction is not a problem.I am telling to put everything in output clause and analyze the query and test it.
you can use CTE inside trigger also but how will you test it.
;With CTE as
(
SELECT CORP, CURRENT_TIMESTAMP, COST,BAL
ROW_NUMBER()over(ORDER BY TS DESC )rn
FROM inserted
inner join FINS on COMPANY = CORP
WHERE COALESCE(COST,0) <> 0
)
select * from CTE --check this what you are getting
Something like that, Isn't complete.
CREATE TRIGGER trgrCYCLE
ON STOR
FOR UPDATE
AS
begin
declare #last_bal int
declare #company varchar(50)
declare #ts --type
declare #cost int
declare #bal --type
--etc whatever you need
select #company = company, #ts= ts , #cost = cost , #bal = bal from INSERTED
--others selects and sets
set #last_bal = select bal from dbo.FINS where you_primary_key = IDENT_CURRENT('FINS'))
set #last_bal = #last_bal - #cost
Insert INTO FINS (company, ts, cost2, bal) VALUES (#company, #ts, #cost, #last_bal) where --your conditions
end
If, similar to #Shantanu's method, you could associate a sequence with inserted, the virtual table associated with the trigger you could do this by subtracting all the COSTs that come before the current record.
This could be accomplished by adding a rowversion to STOR, which will be updated automatically with each delete.
Then instead of:
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
from inserted ...
make the rowversion RV, and:
(SELECT SUM(X.B) FROM
(SELECT TOP 1 BAL B
FROM FINS
WHERE COMPANY = CORP
ORDER BY TS DESC
UNION
SELECT -COST B
FROM inserted ii
WHERE ii.RV >= i.RV AND ii.CORP = i.CORP
) AS X)
FROM inserted i WHERE COALESCE(COST,0) <> 0
Should do what you want. You could conceivably do this with a timestamp that was more find-grained than CURRENT_TIMESTAMP which, I believe, goes down only to seconds but that requires you update it in the UPDATE statement. The rowversion may cause problems with your STOR insert statements.

Resources