T-SQL output inserted clause - access data not in the inserted/deleted tables - sql

I want to collect a value from the source table of a SELECT statement used in an INSERT statement, that is NOT inserted into the target table
I am using Microsoft SQL Server 2017
I think the following code explains what I'm trying to do: Just cut and paste into SSMS to reproduce the error
DECLARE #CrossRef TABLE (
MyTable_ID INT,
C_C VARCHAR(10)
);
DECLARE #MyData TABLE (
A VARCHAR(10),
B VARCHAR(10),
C VARCHAR(10) );
INSERT INTO #MyData (A, B, C)
VALUES ('A1', 'B1', 'C1'), ('A2', 'B2', 'C2'),('A3', 'B3', 'C3');
DECLARE #MyTable TABLE (
ID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
A VARCHAR(10),
B VARCHAR(10) );
INSERT INTO #MyTable (A, B)
OUTPUT INSERTED.Id, MyData.C
INTO #CrossRef (MyTable_ID, C_C)
SELECT A, B
FROM #MyData AS MyData
-- Error: The multi-part identifier "MyData.C" could not be bound.
-- DESIRED OUTPUT
SELECT * FROM #MyTable
/*
ID A B
----------
1 A1 B1
2 A2 B2
3 A3 B3
*/
SELECT * FROM #CrossRef
/*
MyTable_ID C_C
---------------
1 C1
2 C2
3 C3
*/
The OUTPUT clause cannot access anything not in the INSERTED or DELETED internal tables - which is the cause of the error.
However this example Microsoft T-SQL OUTPUT CLAUSE (albeit about DELETED) seems to suggest you can access other tables.
Note - The example has been highly simplified to make the issue as clear as possible
It may seem trivial to get the desired output by other means, but like anything in production the real situation is much more complex

Using the MERGE statement - as Suggested by Tab Alleman here is the solution:
DECLARE #CrossRef TABLE (
MyTable_ID INT,
C_C VARCHAR(10)
);
DECLARE #MyData TABLE (
ID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
A VARCHAR(10),
B VARCHAR(10),
C VARCHAR(10) );
INSERT INTO #MyData (A, B, C)
VALUES ('A1', 'B1', 'C1'), ('A2', 'B2', 'C2'),('A3', 'B3', 'C3');
DECLARE #MyTable TABLE (
ID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
A VARCHAR(10),
B VARCHAR(10) );
-- MERGE statement does UPDATE where join condition exists and INSERT where it does not
MERGE #MyTable
USING (SELECT A, B, C FROM #MyData) AS [Source]
ON (1=0) -- join never true so everything inserted, nothing updated
WHEN NOT MATCHED THEN
INSERT (A, B)
VALUES ([Source].A, [Source].B)
OUTPUT INSERTED.Id, [Source].C
INTO #CrossRef (MyTable_ID, C_C);
SELECT * FROM #MyData
SELECT * FROM #MyTable
SELECT * FROM #CrossRef

Related

Problem with create and drop temp table several times in a query

I create a temp table in a query and put some value in it. After that I drop that table and create another temp table with same name. but the SSMS shows this error:
There is already an object named '#t' in the database.
This is my code in first scenario:
IF OBJECT_ID('tempdb..#t') IS NOT NULL
BEGIN
DROP TABLE #t;
END;
CREATE TABLE #t
(
b INT,
c INT
);
INSERT INTO #t
(
b,
c
)
VALUES
( 2, -- b - int
3 -- c - int
);
SELECT b,
c
FROM #t;
IF OBJECT_ID('tempdb..#t') IS NOT NULL
BEGIN
DROP TABLE #t;
END;
CREATE TABLE #t
(
b INT,
c INT
);
INSERT INTO #t
(
b,
c
)
VALUES
( 4, -- b - int
5 -- c - int
);
SELECT b,
c
FROM #t;
I put Go phrase after second table dropping and the code runs successfully, insert new values and select them.
This is second scenario:
IF OBJECT_ID('tempdb..#t') IS NOT NULL
BEGIN
DROP TABLE #t;
END;
CREATE TABLE #t
(
b INT,
c INT
);
INSERT INTO #t
(
b,
c
)
VALUES
( 2, -- b - int
3 -- c - int
);
SELECT b,
c
FROM #t;
IF OBJECT_ID('tempdb..#t') IS NOT NULL
BEGIN
DROP TABLE #t;
END;
Go -- THIS IS THE DIFFERENCE BETWEEN TWO CODES
CREATE TABLE #t
(
b INT,
c INT
);
INSERT INTO #t
(
b,
c
)
VALUES
( 4, -- b - int
5 -- c - int
);
SELECT b,
c
FROM #t;
I get confused. Can you help me and explain the usage of GO in queries?
Thanks
The scope of the Temp table is the issue here. The table does not get completely dropped until the end of the session or batch - which is where the GO statement comes in. This terminates the batch and allows the Temp table to be fully removed.
Why drop the table and recreate with the same structure? You could just execute DELETE FROM #t to remove the current data and then do the next insert.
The "GO" separates the pieces of code into 2 different batches - so even though they share the same window - they are effectively run in 2 different batches.
GO Utility
The problem you have encountered is a limitation within SQL - you cannot create a temp table with the same name more than once within a batch. Even if they are dropped.

SQL Server: Insert batch with output clause

I'm trying the following
Insert number of records to table A with a table-valued parameter (tvp). This tvp has extra column(s) that are not in A
Get the inserted ids from A and the corresponding extra columns in the the tvp and add them to another table B
Here's what I tried
Type:
CREATE TYPE tvp AS TABLE
(
id int,
otherid int,
name nvarchar(50),
age int
);
Tables:
CREATE TABLE A (
[id_A] [int] IDENTITY(1,1) NOT NULL,
[name] [varchar](50),
[age] [int]
);
CREATE TABLE B (
[id_B] [int] IDENTITY(1,1) NOT NULL,
[id_A] [int],
[otherid] [int]
);
Insert:
DECLARE #a1 AS tvp;
DECLARE #a2 AS tvp
-- create a tvp (dummy data here - will be passed to as a param to an SP)
INSERT INTO #a1 (name, age, otherid) VALUES ('yy', 10, 99999), ('bb', 20, 88888);
INSERT INTO A (name, age)
OUTPUT
inserted.id_A,
inserted.name,
inserted.age,
a.otherid -- <== isn't accepted here
INTO #a2 (id, name, age, otherid)
SELECT name, age FROM #a1 a;
INSERT INTO B (id_A, otherid) SELECT id, otherid FROM #a2
However, this fails with The multi-part identifier "a.otherid" could not be bound., which I guess is expected because columns from other tables are not accepted for INSERT statement (https://msdn.microsoft.com/en-au/library/ms177564.aspx).
from_table_name
Is a column prefix that specifies a table included in the FROM clause of a DELETE, UPDATE, or MERGE statement that is used to specify the rows to update or delete.
So is there any other way to achieve this?
You cannot select value from a source table by using INTO operator.
Use OUTPUT clause in the MERGE command for such cases.
DECLARE #a1 AS tvp;
DECLARE #a2 AS tvp
INSERT INTO #a1 (name, age, otherid) VALUES ('yy', 10, 99999), ('bb', 20, 88888);
MERGE A a
USING #a1 a1
ON a1.id =a.[id_A]
WHEN NOT MATCHED THEN
INSERT (name, age)
VALUES (a1.name, a1.age)
OUTPUT inserted.id_A,
a1.otherId,
inserted.name,
inserted.age
INTO #a2;
INSERT INTO B (id_A, otherid) SELECT id, otherid FROM #a2

Fetch row from one of the two tables in sql

Consider two tables: TableA and TableB with columns as,
TableA - (col1a, col2a, col3a)
TableB - (col1b, col2b, col3b).
I have one key which is either present in TableA or TableB. Based on the presence of the key in either of the table, I have to query one of the respective table. Is it possible using sql ?
Instead of using a single query, you can create a database function and pass key to the function and perform table selection and data extraction in that function.
And after this you can call the database functions from your code. Please refer to database manual for creating functions as they can be optimized for the database.
The problem with pseudo-code like that is that it's unclear exactly how your data is structured. You said the key can be in either table, but you aren't clear if it could be in any of the 3 columns in either table. You also don't indicate the data type of the key or the columns. Additionally, I'd recommend not using such simplified column names - unless that was just your pseudo-code.
That being said, assuming that the key could exist in any column in either table, here's a sample of how you could union multiple SELECT statements looking for the key in any one of the 6 columns across the two tables.
DECLARE #TableA TABLE
(
col1a VARCHAR(10)
, col2a VARCHAR(10)
, col3a VARCHAR(10)
)
INSERT INTO #TableA
( col1a
, col2a
, col3a )
VALUES
('key1', 'key2', 'key3')
INSERT INTO #TableA
( col1a
, col2a
, col3a )
VALUES
('key4', 'key5', 'key6')
DECLARE #TableB TABLE
(
col1b VARCHAR(10)
, col2b VARCHAR(10)
, col3b VARCHAR(10)
)
INSERT INTO #TableB
( col1b
, col2b
, col3b )
VALUES
('key7', 'key8', 'key9');
INSERT INTO #TableB
( col1b
, col2b
, col3b )
VALUES
('key10', 'key11', 'key12');
DECLARE #SearchKey VARCHAR(10) = 'key6'
SELECT * FROM #TableA WHERE col1a = #SearchKey
UNION
SELECT * FROM #TableA WHERE col2a = #SearchKey
UNION
SELECT * FROM #TableA WHERE col3a = #SearchKey
UNION
SELECT * FROM #TableB WHERE col1b = #SearchKey
UNION
SELECT * FROM #TableB WHERE col2b = #SearchKey
UNION
SELECT * FROM #TableB WHERE col3b = #SearchKey

Insert - Select keeping identity mapping

I have 2 tables, and im trying to insert data from one to another and keepeng the mappings between ids.
I found here someone with the same problem, but the solution isnt good for me.
here is the example:
the two tables
CREATE TABLE [source] (i INT identity PRIMARY KEY, some_value VARCHAR(30))
CREATE TABLE [destination] (i INT identity PRIMARY KEY, some_value VARCHAR(30))
CREATE TABLE [mapping] (i_old INT, i_new INT) -- i_old is source.i value, i_new is the inserted destination.i column
some sample data
INSERT INTO [source] (some_value)
SELECT TOP 30 name
FROM sysobjects
INSERT INTO [destination] (some_value)
SELECT TOP 30 name
FROM sysobjects
Here, i want to transfer everything from source into destination, but be able to keep a mapping on the two tables:
I try to use OUTPUT clause, but i cannot refer to columns outside of the ones being inserted:
INSERT INTO [destination] (some_value)
--OUTPUT inserted.i, s.i INTO [mapping] (i_new, i_old) --s.i doesn't work
SELECT some_value
FROM [source] s
Anyone has a solution for this?
Not sure is it write way but it works :D
MERGE [#destination] AS D
USING [#source] AS s
ON s.i <> s.i
WHEN NOT MATCHED BY TARGET
THEN
INSERT (some_value) VALUES (some_value)
OUTPUT inserted.i, s.i INTO [#mapping] (i_new, i_old);
try this sql below if you don't have permission to modify the tables:
The idea is using a temp table to be a bridge between destination table and the mapping table.
SQL Query:
declare #source table (i INT identity PRIMARY KEY, some_value VARCHAR(30))
declare #destination table (i INT identity PRIMARY KEY, some_value VARCHAR(30))
declare #mapping table (i_old INT, i_new INT) -- i_old is source.i value, i_new is the inserted destination.i column
declare #tempSource table
(
id_source INT identity , source_value VARCHAR(30)
,Id_New int,source_new VARCHAR(30)
)
insert into #source
output inserted.i, inserted.some_value into #tempSource(id_source,source_value)
SELECT TOP 10 name
FROM sysobjects
--select * from #tempsource
insert into #destination
OUTPUT inserted.i, inserted.some_value INTO #tempSource (Id_New,source_new)
select source_value from #tempSource
insert into #mapping
select Id_source, Id_New from
(
select a.id_source, a.source_value
from
#tempSource a
where id_source is not null and source_value is not null
) aa
inner join
(
select a.Id_New, a.source_new
from
#tempSource a
where Id_New is not null and source_new is not null
) bb on aa.source_value = bb.source_new
select * from #mapping
The mapping table result:
i_old i_new
----------- -----------
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 10

project a sparse result at some level

I don't really know what to call this but it's not that hard to explain
Basically what I have is a result like this
Similarity ColumnA ColumnB ColumnC
1 SomeValue NULL SomeValue
2 NULL SomeB NULL
3 SomeValue NULL SomeC
4 SomeA NULL NULL
This result is created by matching a set of strings against another table. Each string also contains some values for these ColumnA..C which are the values I wan't to aggregate in some way.
Something like min/max works very well but I can't figure out how to get it to account for the highest similarity not just the min/max value. I don't really want the min/max, I want the first non-null value with the highest similarity.
Ideally the result would look like this
ColumnA ColumnB ColumnC
SomeA SomeB SomeC
I'd like be able to efficiently join in the temporary result to compute the rest and I've been exploring different options. Something which I've been considering is creating a SQL Server CLR aggregate the yields the "first" non-null value but I'm unsure if there's even such a thing as a first or last when running an aggregate on a result.
Okay, so I figured it out, I originally had trouble with the UPDATE FROM and JOIN not playing well together. I was counting on that the UPDATE would just occur multiple times and that would give me the correct results, however, there's no such guarantee from SQL Server (it's actually undefined behavior and alltough it appeared to work we'll have none of that) but since you can run UPDATE against a CTE I combined that with the OUTER APPLY to select the exactly 1 row to complement a missing value if possible.
Here's the whole thing with test data as well.
DECLARE #cost TABLE (
make nvarchar(100) not null,
model nvarchar(100),
a numeric(18,2),
b numeric(18,2)
);
INSERT #cost VALUES ('a%', null, 100, 2);
INSERT #cost VALUES ('a%', 'a%', 149, null);
INSERT #cost VALUES ('a%', 'ab', 349, null);
INSERT #cost VALUES ('b', null, null, 2.5);
INSERT #cost VALUES ('b', 'b%', 249, null);
INSERT #cost VALUES ('b', 'b', null, 3);
DECLARE #unit TABLE (
id int,
make nvarchar(100) not null,
model nvarchar(100)
);
INSERT #unit VALUES (1, 'a', null);
INSERT #unit VALUES (2, 'a', 'a');
INSERT #unit VALUES (3, 'a', 'ab');
INSERT #unit VALUES (4, 'b', null);
INSERT #unit VALUES (5, 'b', 'b');
DECLARE #tmp TABLE (
id int,
specificity int,
a numeric(18,2),
b numeric(18,2),
primary key(id, specificity)
);
INSERT #tmp
OUTPUT inserted.* --FOR DEBUGGING
SELECT
unit.id
, ROW_NUMBER() OVER (
PARTITION BY unit.id
ORDER BY cost.make DESC, cost.model DESC
) AS specificity
, cost.a
, cost.b
FROM #unit unit
INNER JOIN #cost cost ON unit.make LIKE cost.make
AND (cost.model IS NULL OR unit.model LIKE cost.model)
;
--fix the holes
WITH tmp AS (
SELECT *
FROM #tmp
WHERE specificity = 1
AND (a IS NULL OR b IS NULL) --where necessary
)
UPDATE tmp
SET
tmp.a = COALESCE(tmp.a, a.a)
, tmp.b = COALESCE(tmp.b, b.b)
OUTPUT inserted.* --FOR DEBUGGING
FROM tmp
OUTER APPLY (
SELECT TOP 1 a
FROM #tmp a
WHERE a.id = tmp.id
AND a.specificity > 1
AND a.a IS NOT NULL
ORDER BY a.specificity
) a
OUTER APPLY (
SELECT TOP 1 b
FROM #tmp b
WHERE b.id = tmp.id
AND b.specificity > 1
AND b.b IS NOT NULL
ORDER BY b.specificity
) b
;