Let's say we have simple table T1 which has three columns:
CREATE TABLE T1(C1 INT PRIMARY KEY, C2 VARCHAR(20), C3 XML)
Now we create simple data:
INSERT INTO T1 VALUES(1, 'Test', '<Element></Element>')
Then I want to modify third column to achieve something like this:
<Element>Test</Element>
Which means, C2 is inserted into XML.
So I wanted to do that with variables:
DECLARE #test VARCHAR(20) = 'Example'
UPDATE
T1
SET
#test = C2,
C3.modify('
replace value of
(/Element/text())[1]
with sql:variable("#test") ')
Unfortunately the result is:
<Element>Example</Element>
What I'm doing wrong?
You are modifying the underlying variable in the same statement where it is used; this doesn't work in SQL Server. Either:
Split variable assignment and usage into two separate statements:
DECLARE #test VARCHAR(20);
select #test = t.C2
from T1 t;
UPDATE t SET C3.modify('
replace value of (/Element/text())[1] with sql:variable("#test")
')
from T1 t;
OR
Use the value of the column directly:
UPDATE t SET C3.modify('
replace value of (/Element/text())[1] with sql:column("t.C2")
')
from T1 t;
Unless you have some complex logic behind the variable value calculation, the second option is preferred due to performance reasons - you touch the table only once, not twice. Also, the second variant is highly recommended if you need to update more than 1 row, each with different values.
Related
I have a stored procedure in a program that is not performing well. Its truncated version follows. The MyQuotes table has an IDENTITY column called QuoteId.
CREATE PROCEDURE InsertQuote
(#BinderNumber VARCHAR(50) = NULL,
#OtherValue VARCHAR(50))
AS
INSERT INTO MyQuotes (BinderNumber, OtherValue)
VALUES (#BinderNumber, #OtherValue);
DECLARE #QuoteId INT
SELECT #QuoteId = CONVERT(INT, SCOPE_IDENTITY());
IF #BinderNumber IS NULL
UPDATE MyQuotes
SET BinderNumber = 'ABC' + CONVERT(VARCHAR(10),#QuoteId)
WHERE QuoteId = #QuoteId;
SELECT #QuoteId AS QuoteId;
I feel like the section where we derive the binder number from the scope_identity() can be done much, much, cleaner. And I kind of think we should have been doing this in the C# code rather than the SQL, but since that die is cast, I wanted to fish for more learned opinions than my own on how you would change this query to populate that value.
The following update avoids needing the id:
UPDATE MyQuotes SET
BinderNumber = 'ABC' + CONVERT(VARCHAR(10), QuoteId)
WHERE BinderNumber is null;
If selecting QuoteId as a return query is required then using scope_identity() is as good a way as any.
Dale's answer is better, however this can be useful way too:
DECLARE #Output TABLE (ID INT);
INSERT INTO MyQuotes (BinderNumber, OtherValue) VALUES (#BinderNumber, #OtherValue) OUTPUT inserted.ID INTO #Output (ID);
UPDATE q SET q.BinderNumber = 'ABC' + CONVERT(VARCHAR(10),o.ID)
FROM MyQuotes q
INNER JOIN #Output o ON o.ID = q.ID
;
Also, if BinderNumber is always linked to ID, it would be better to just create computed column
AS 'ABC' + CONVERT(VARCHAR(10),ID)
Say I'm trying to return some results where a column in a table matches a condition I set. But I only want to return the first result from a list of possible values in the condition. Is there a quick and easy way to do that? I'm thinking that I can use coalesce somehow, but not sure how I can structure it.
Something like:
select identifier,purpose from table
where identifier = 'letters'
and purpose = coalesce('A','B','C')
group by purpose
So in the table, if A purpose isn't there, then I only want the B purpose to show up. if it isn't there, then I want the C to show up, if none of them are there, then I would ideally like a null or no results to be returned. I'd rather not make several case statements where if A is null then look to B, then if B is null to look to C. Is there a quick way syntactically to do so?
Edit: I also want this to work if I have multiple identifiers I list, such as:
select identifier,purpose from table
where identifier in ('letters1', 'letters2')
and purpose = coalesce('A','B','C')
group by purpose
where I return two results if they exist - one purpose for each identifier, with the purpose in the order of importance for A first, then B, then C, or null if none exist.
Unforunately my reasoning for caolesce doesn't work above, as none of the variables are null so my query will just try to return all purposes of 'A' without the fallback that I intend my query to do. I want to try and avoid using temp tables if possible.
Sybase ASE does not have support for the row_number() function (else this would be fairly simple), so one option would be to use a #temp table to simulate (to some extent) row_number() functionality.
Some sample data:
create table mytab
(identifier varchar(30)
,purpose varchar(30)
)
go
insert mytab values ('letters1','A')
insert mytab values ('letters1','B')
insert mytab values ('letters1','C')
insert mytab values ('letters2','A')
insert mytab values ('letters2','B')
insert mytab values ('letters2','C')
go
The #temp table is created with an identity column plus a 2nd column to hold the items you wish to prioritize; priority is determined by the order in which the rows are inserted into the #temp table.
create table #priority
(id smallint identity
,purpose varchar(30))
go
insert #priority (purpose)
select 'A' -- first priority
union all
select 'B' -- second priority
union all
select 'C' -- last priority
go
select * from #priority order by id
go
id purpose
------ -------
1 A
2 B
3 C
We'll use a derived table to find the highest priority purpose (ie, minimal id value). We then join this minimal id back to #priority to generate the final result set:
select dt.identifier,
p.purpose
from (-- join mytab with #priority, keeping only the minimal priority id of the rows that exist:
select m.identifier,
min(p.id) as min_id
from mytab m
join #priority p
on p.purpose = m.purpose
group by m.identifier) dt
-- join back to #priority to convert min(id) into the actual purpose:
join #priority p
on p.id = dt.min_id
order by 1
go
Some test runs with different set of mytab data:
/* contents of mytab:
insert mytab values ('letters1','A')
insert mytab values ('letters1','B')
insert mytab values ('letters1','C')
insert mytab values ('letters2','A')
insert mytab values ('letters2','B')
insert mytab values ('letters2','C')
*/
identifier purpose
---------- -------
letters1 A
letters2 A
/* contents of mytab:
--insert mytab values ('letters1','A')
--insert mytab values ('letters1','B')
insert mytab values ('letters1','C')
--insert mytab values ('letters2','A')
insert mytab values ('letters2','B')
insert mytab values ('letters2','C')
*/
identifier purpose
---------- -------
letters1 C
letters2 B
Returning NULL if a row does not exist is not going to be easy since generating a NULL requires existence of a row ... somewhere ... with which to associate the NULL.
One idea would be to expand on the #temp table idea by creating another #temp table (eg, #identifiers) with the list of desired identifier values you wish to search on. You could then make use of a left (outer) join from #identifiers to mytab to ensure you always generate a result record for each identifier.
I have a trigger:
ALTER TRIGGER [dbo].[tg_trs_uharian] ON [dbo].[master_st]
AFTER INSERT AS
BEGIN
SET NOCOUNT ON
declare #tgl_mulai varchar(10),
#tgl_selesai varchar(10),
#kdlokasi int,
#thn_harian int,
#date_diff int
declare #tugasID int;
declare #uangharian20 decimal(15,2);
declare #uangharian80 decimal(15,2);
declare #uangharian100 decimal(15,2);
select #tugasID=tugasID
from inserted
SET #thn_harian=CAST(YEAR(CONVERT(datetime, #tgl_mulai, 103)) AS INT);
SET #date_diff=((SELECT datediff(day,CONVERT([datetime],#tgl_mulai,(103)),CONVERT([datetime],#tgl_selesai,(103))))+1);
SET #uangharian100 = (
SELECT k.uh_nominal
FROM master_st m
LEFT OUTER JOIN ref_uharian AS k
ON k.uh_kdlokasi=m.kdlokasi AND k.uh_tahun=#thn_harian);
insert into trs_uangharian (tugasID, uangharian100) values
(#tugasID, #uangharian100);
END
How to make select #tugasID=tugasID from inserted applicable for multiple row inserted row table with different tugasID? It seems that my code is applicable for single row only.
It seems that #date_diff is not used
You use #thn_harian so we need #tgl_mulai, but it is NULL by default
So your INSERT statement has some problems.
I assumed that #tgl_mulai is a column of the original table master_st so I treat it as a column of "inserted" trigger internal table
ALTER TRIGGER [dbo].[tg_trs_uharian] ON [dbo].[master_st]
AFTER INSERT AS
BEGIN
SET NOCOUNT ON
insert into trs_uangharian(tugasID, uangharian100)
select
i.tugasID,
k.uh_nominal
from inserted i
left join ref_uharian AS k
ON k.uh_kdlokasi = i.kdlokasi AND
k.uh_tahun = CAST(YEAR(CONVERT(datetime, i.tgl_mulai, 103)) AS INT)
END
Please, this is a common problem among new SQL developers
SQL triggers work set-based.
Do not calculate any value using variables.
These can only store the last row's calculations in general.
Instead use Inserted and Deleted internal tables.
Your query is messed up a bit, so I can provide only general solution. Change INSERT part on something like this:
INSERT INTO trs_uangharian (tugasID, uangharian100)
SELECT i.tugasID,
k.uh_nominal
FROM inserted i
LEFT JOIN ref_uharian AS k
ON k.uh_kdlokasi=i.kdlokasi AND k.uh_tahun=#thn_harian
You should be able to replace the INSERT statement with this:
INSERT INTO trs_uangharian (tugasID, uangharian100)
SELECT
tugasID,
#uangharian100
FROM
inserted
However it looks like you also have an issue with #tgl_mulai and #tgl_selesai not being set to anything.
I have a stored procedure that receives a TVP as input. Now, I need to check the received data for a particular ID in a primary key column. If it exists, then I just need to update the table using those new values for other column (sent via TVP). Else, do an insert.
How to do it?
CREATE PROCEDURE ABC
#tvp MyTable READONLY
AS
IF EXISTS (SELECT 1 FROM MYTAB WHERE ID= #tvp.ID)
DO update
ELSE
Create
Just wondering the if exists loop I did is correct. I reckon its wrong as it will only check for first value and then update. What about other values? How should I loop through this?
Looping/CURSOR is the weapon of last resort, always search for solution that is SET based, not ROW based.
You should use MERGE which is designed for this type of operation:
MERGE table_name AS TGT
USING (SELECT * FROM #tvp) AS SRC
ON TGT.id = SRC.ID
WHEN MATCHED THEN
UPDATE SET col = SRC.col
WHEN NOT MATCHED THEN
INSERT (col_name, col_name2, ...)
VALUES (SRC.col_name1, SRC.col_name2, ...)
If you don't like MERGE use INSERT/UPDATE:
UPDATE table_name
SET col = tv.col,
...
FROM table_name AS tab
JOIN #tvp AS tv
ON tab.id = tv.id
INSERT INTO table_name(col1, col2, ...)
SELECT tv.col1, tv.col2, ...
FROM table_name AS tab
RIGHT JOIN #tvp AS tv
ON tab.id = tv.id
WHERE tab.id IS NULL
I'm using MERGE in my query and i'm making INSERT on clause WHEN NOT MATCHED THEN, but then i would like to get the inserted row identity and make another INSERT to some other table. Query for now is:
ALTER PROCEDURE [dbo].[BulkMergeOffers]
#data ImportDataType READONLY
AS
SET NOCOUNT ON;
DECLARE #cid int = 0
MERGE dbo.oferta AS target
USING #data AS source
ON (target.nr_oferty = source.nr_oferty)
WHEN NOT MATCHED THEN
INSERT (nr_oferty,rynek,typ_transakcji, typ_nieruchomosci,cena,powierzchnia, rok_budowy, wojewodztwo, miasto, dzielnica, ulica, opis, wspolrzedne, film, zrodlo, KontaktStore, data, forma_wlasnosci, stan_techniczny, liczba_pokoi, liczba_peter, pietro, material, kuchnia, pow_dzialki, typ_dzialki, woda,gaz, prad,sila, przeznaczenie,lokal_dane)
VALUES (source.nr_oferty,source.rynek,source.typ_transakcji, source.typ_nieruchomosci,source.cena,source.powierzchnia, source.rok_budowy, source.wojewodztwo, miasto, source.dzielnica, source.ulica, source.opis, source.wspolrzedne, source.film, source.zrodlo, source.KontaktStore, source.data, source.forma_wlasnosci, source.stan_techniczny, source.liczba_pokoi, source.liczba_peter, source.pietro, source.material, source.kuchnia, source.pow_dzialki, source.typ_dzialki, source.woda,source.gaz, source.prad,source.sila, source.przeznaczenie,source.lokal_dane);
So as you see i need to insert some values to the target table based on source data, then i need to take the insert identity and insert it into another table but also based on some source data, so something like that, just after the first insert:
SET #cid = SCOPE_IDENTITY();
if source.photo is not null
begin
insert into dbo.photos(offerID, file) values (#cid, source.photo);
end
But i can't assemble it, a have no access to the source no more, also if statement show error :
"the multi-part identifier
source.photo can not be bound"
but it is there. Just for clarity ImportDataType is a table-valued parameter.
Please HELP
If you don't need the WHEN MATCHED part of the MERGE statement in your query, there's no real reason to use MERGE. You could use INSERT with an outer join or NOT EXISTS statement.
In either case, you can use the OUTPUT clause to retrieve the inserted identity value an pass it on to a second query.
I've extended your example:
<stored procedure header - unchanged>
--declare a table variable to hold the inserted values data
DECLARE #newData TABLE
(nr_oferty int
,newid int
) -- I'm guessing the datatype for both columns
MERGE dbo.oferta AS target
USING #data AS source
ON (target.nr_oferty = source.nr_oferty)
WHEN NOT MATCHED THEN
INSERT (nr_oferty,rynek,typ_transakcji, typ_nieruchomosci,cena,powierzchnia, rok_budowy, wojewodztwo, miasto, dzielnica, ulica, opis, wspolrzedne, film, zrodlo, KontaktStore, data, forma_wlasnosci, stan_techniczny, liczba_pokoi, liczba_peter, pietro, material, kuchnia, pow_dzialki, typ_dzialki, woda,gaz, prad,sila, przeznaczenie,lokal_dane)
VALUES (source.nr_oferty,source.rynek,source.typ_transakcji, source.typ_nieruchomosci,source.cena,source.powierzchnia, source.rok_budowy, source.wojewodztwo, miasto, source.dzielnica, source.ulica, source.opis, source.wspolrzedne, source.film, source.zrodlo, source.KontaktStore, source.data, source.forma_wlasnosci, source.stan_techniczny, source.liczba_pokoi, source.liczba_peter, source.pietro, source.material, source.kuchnia, source.pow_dzialki, source.typ_dzialki, source.woda,source.gaz, source.prad,source.sila, source.przeznaczenie,source.lokal_dane)
OUTPUT inserted.nr_oferty, inserted.<tableId> INTO #newData;
-- replace <tableId> with the name of the identity column in dbo.oftera
insert into dbo.photos(offerID, file)
SELECT nd.newid, pt.photo
FROM #data AS pt
JOIN #newData AS nd
ON nd.nr_oferty = pt.nr_oferty
WHERE pt.photo IS NOT NULL