In SQL Server, how can I compare rows using the column names from another table - sql

I have two tables A and B, with dynamic columns where I have no idea what columns are key inside them, except from another table called C.
The C table specifies which column/s is a key column in tables A and B. There can be 1 or more key columns.
My question is, how would I generate such a query where I select all rows from A where the key columns are equal to the same key columns in B?
One idea I had was to create a text query that I execute with sp_executesql, but I need some good ideas on how to generate the query.
First of all, I would select all key columns from table C for the table A and B to my declared table #keyColumns.
Then I would use a while loop to go through all key columns inside #keyColumns and generate the query and execute it with sp_executesql.
For example:
UPDATE A
SET ...
FROM B INNER JOIN A
ON A.keycol1 = B.keycol1 AND A.keycol2 = B.keycol2 AND ...
Just to make it clear, the C table only specifies key columns for the table B, and from that I know A has the same key columns.
But I want to know if there's a better way to solve this.

Are the key columns held in 'C' the primary key? If so you can retrieve these from INFORMATION_SCHEMA.TABLE_CONSTRAINTS, and INFORMATION_SCHEMA.KEY_COLUMN_USAGE as described here rather than using a different table.
You have to use dynamic SQL for this I think. There is no syntax like FROM B JOIN A ON PRIMARY KEYS. Instead of the WHILE loop though you can just concatenate your query through a SELECT as below.
DECLARE #DynSql nvarchar(max)
DECLARE #TableA sysname
DECLARE #TableB sysname
SET #TableA = 'A'
SET #TableB = 'B';
WITH C AS
(
SELECT 'B' AS [Table], 'keycol2' As col UNION ALL
SELECT 'B' AS [Table], 'keycol1' As col UNION ALL
SELECT 'X' AS [Table], 'keycol1' As col
)
SELECT #DynSql = ISNULL(#DynSql + ' AND ','')+ #TableA + '.'+QUOTENAME(col) + '= ' + #TableB + '.'+QUOTENAME(col)
FROM C WHERE [Table] = #TableB
IF ##ROWCOUNT=0
RAISERROR('No Entry found for table %s',16,1,#TableB)
SET #DynSql =
'UPDATE ' + #TableA + '
SET ...
FROM ' + #TableB + ' INNER JOIN ' + #TableA + ' ON
' + #DynSql
PRINT #DynSql

Related

Select only those columns from a table header which are present in data dictionary ColumnName

Prerequisite : all the tables are dynamic so i cant use the column names
I have two tables
Candidate table :
Table which has all columns and data required to be selected
DataDictionary :
Table where i have only those columns which are to be selected for querying
Now what i want to do is select only that data and columns from the candidate table which are present in datadictionary and skip those that data and column which are not present in datadictionary
what i have tried is
SELECT ColumnName
INTO #Candidate
FROM DataDictionaryDetail WHERE DataDictionaryId =1
select *
from candidate
where NOT EXISTS (select *from #Candidate)
but this brings only columns but not data
I need a proper way to select data also and columns
You need dynamic SQL for this
DECLARE #sql nvarchar(max) = N'
SELECT
'
+ (
SELECT STRING_AGG(QUOTENAME(ColumnName), ',')
FROM DataDictionaryDetail
WHERE DataDictionaryId = 1
) + N'
from candidate;
';
EXEC sp_executesql #sql;

How to add multiple columns from another table in sql server 2017?

I have a requirement where i need to add multiple columns from a source table after checking existence of those columns. for eg:
Table1 containg 7 coulmns like A, B, C, D, E, F, G and Table2 containing 4 columns like A, B, C, D
I want to check the existency of table1 columns in Table2 and if not exists then add rest 3 columns in Table2. I am looking for a solution where i don't need to add these columns manually if not exists in table2.
How can i do this?
I have tried this:
if exists (SELECT TABLE_NAME, COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='table1' and COLUMN_NAME in('A','B','C','D','E','F','G'))
BEGIN
ALTER TABLE table2
ADD [E] FLOAT null
,[F] FLOAT null
,[G] FLOAT null
END;
But this is not the solution of my query I want to make it dynamic and don't know how to do this.
I don't, for one second, think this is a good idea, but this would achieve what you are after. Note that if the same column exists by name in both tables, but have different data types, the column will be ignored:
CREATE TABLE Table1 (a int,
b numeric(12,2),
c datetime2(0),
d date,
e varchar(20),
f sysname,
g varbinary);
CREATE TABLE Table2 (a int,
b numeric(12,2),
c datetime2(0),
d date);
GO
DECLARE #SQL nvarchar(MAX);
SET #SQL = STUFF((SELECT NCHAR(13) + NCHAR(10) +
N'ALTER TABLE Table2 ADD ' + QUOTENAME(T1.name) + N' ' + T1.system_type_name + N';'
FROM sys.dm_exec_describe_first_result_set(N'SELECT * FROM Table1',NULL, NULL) T1
WHERE NOT EXISTS(SELECT 1
FROM sys.dm_exec_describe_first_result_set(N'SELECT * FROM Table2',NULL, NULL) T2
WHERE T1.[name] = T2.[name])
ORDER BY T1.column_ordinal
FOR XML PATH(N''),TYPE).value('.','nvarchar(MAX)'),1,2,N'');
PRINT #SQL;
EXEC sp_executesql #SQL;
GO
SELECT *
FROM dbo.Table2;
GO
DROP TABLE dbo.Table2;
DROP TABLE dbo.Table1;

How to interrogate multiple tables with different structure?

I am using Sql-Server 2016 in a C# application.
Let's say I have two tables:
CREATE TABLE Table_A
(
UserID NVARCHAR2(15),
FullName NVARCHAR2(25),
Available NUMBER(1),
MachineID NVARCHAR2(20),
myDate date
);
and
CREATE TABLE Table_B
(
UserID NVARCHAR2(15),
FullName NVARCHAR2(25),
Team NVARCHAR2(15),
MachineID NVARCHAR2(20),
Stuff NUMBER(2)
);
I want to perform a global select so that I will get as result data from both tables, somehow concatenated and of course, when a column does not exist in one of the tables, that column to be automatically populated with NULL, and if a column exists on both tables the results must be merged in a single column.
The first solution that pops-up is a UNION with NULL aliases for the missing columns, sure. The problem is that at runtime I will not be able to know in advance which tables are interrogated so that I could anticipate the column names. I need a more general solution.
The expected result from the two tables must look like this:
user_Table_A; fullName_Table_A; 1; machineID_Table_A; 12-JUN-18; NULL; 10;
user_Table_B; fullName_Table_B; NULL; machineID_Table_B; NULL; team_Table_B; 20;
The data for the two tables is inserted with the following commands:
INSERT INTO Table_A VALUES ('user_Table_A', 'fullName_Table_A', 1, 'machineID_Table_A', TO_DATE('12-06-2018', 'DD-MM-YYYY'));
INSERT INTO Table_B VALUES ('user_Table_B', 'fullName_Table_B', 'team_Table_B', 'machineID_Table_B', 20);
You can do something like this. I havent have time to completely tweak it, so there can be something the order of the columns. But perhaps it can get you started:
You also write that you use Oracle - Im not sure what you wanted, but this is in pure sql-server version.
SQL:
IF OBJECT_ID('tempdb..#temp') IS NOT NULL
/*Then it exists*/
DROP TABLE #temp;
GO
DECLARE #SQLList nvarchar(max)
DECLARE #SQLList2 nvarchar(max)
DECLARE #SQL nvarchar(max)
with table_a as (
select column_name as Table_aColumnName,ORDINAL_POSITION from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'table_a'
)
,
table_b as (
select column_name as Table_bColumnName,ORDINAL_POSITION from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'table_b'
)
,preresult as (
select case when Table_aColumnName IS null then 'NULL as ' + Table_bColumnName else Table_aColumnName end as Table_a_ColumnName,case when Table_bColumnName IS null then 'NULL as ' +Table_aColumnName else Table_bColumnName end as Table_b_ColumnName
,a.ORDINAL_POSITION,b.ORDINAL_POSITION as Table_b_Ordinal from table_a a full join Table_B b on a.Table_aColumnName = b.Table_bColumnName
)
select * into #temp from preresult
SET #SQLList = (
select distinct display = STUFF((select ','+table_a_columnName from #temp b order by table_b_ordinal FOR XML PATH('')),1,1,'') from #temp a
)
SET #SQLList2 = (
select distinct display = STUFF((select ','+table_b_columnName from #temp b order by Table_b_Ordinal FOR XML PATH('')),1,1,'') from #temp a
)
SET #SQL = 'select ' +#SQLList +' from dbo.Table_a union all select ' + #SQLList2 + ' from dbo.table_b'
exec(#SQL)
Result:

How to UPDATE all columns of a record without having to list every column

I'm trying to figure out a way to update a record without having to list every column name that needs to be updated.
For instance, it would be nice if I could use something similar to the following:
// the parts inside braces are what I am trying to figure out
UPDATE Employee
SET {all columns, without listing each of them}
WITH {this record with id of '111' from other table}
WHERE employee_id = '100'
If this can be done, what would be the most straightforward/efficient way of writing such a query?
It's not possible.
What you're trying to do is not part of SQL specification and is not supported by any database vendor. See the specifications of SQL UPDATE statements for MySQL, Postgresql, MSSQL, Oracle, Firebird, Teradata. Every one of those supports only below syntax:
UPDATE table_reference
SET column1 = {expression} [, column2 = {expression}] ...
[WHERE ...]
This is not posible, but..
you can doit:
begin tran
delete from table where CONDITION
insert into table select * from EqualDesingTabletoTable where CONDITION
commit tran
be carefoul with identity fields.
Here's a hardcore way to do it with SQL SERVER. Carefully consider security and integrity before you try it, though.
This uses schema to get the names of all the columns and then puts together a big update statement to update all columns except ID column, which it uses to join the tables.
This only works for a single column key, not composites.
usage: EXEC UPDATE_ALL 'source_table','destination_table','id_column'
CREATE PROCEDURE UPDATE_ALL
#SOURCE VARCHAR(100),
#DEST VARCHAR(100),
#ID VARCHAR(100)
AS
DECLARE #SQL VARCHAR(MAX) =
'UPDATE D SET ' +
-- Google 'for xml path stuff' This gets the rows from query results and
-- turns into comma separated list.
STUFF((SELECT ', D.'+ COLUMN_NAME + ' = S.' + COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = #DEST
AND COLUMN_NAME <> #ID
FOR XML PATH('')),1,1,'')
+ ' FROM ' + #SOURCE + ' S JOIN ' + #DEST + ' D ON S.' + #ID + ' = D.' + #ID
--SELECT #SQL
EXEC (#SQL)
In Oracle PL/SQL, you can use the following syntax:
DECLARE
r my_table%ROWTYPE;
BEGIN
r.a := 1;
r.b := 2;
...
UPDATE my_table
SET ROW = r
WHERE id = r.id;
END;
Of course that just moves the burden from the UPDATE statement to the record construction, but you might already have fetched the record from somewhere.
How about using Merge?
https://technet.microsoft.com/en-us/library/bb522522(v=sql.105).aspx
It gives you the ability to run Insert, Update, and Delete. One other piece of advice is if you're going to be updating a large data set with indexes, and the source subset is smaller than your target but both tables are very large, move the changes to a temporary table first. I tried to merge two tables that were nearly two million rows each and 20 records took 22 minutes. Once I moved the deltas over to a temp table, it took seconds.
If you are using Oracle, you can use rowtype
declare
var_x TABLE_A%ROWTYPE;
Begin
select * into var_x
from TABLE_B where rownum = 1;
update TABLE_A set row = var_x
where ID = var_x.ID;
end;
/
given that TABLE_A and TABLE_B are of same schema
It is possible. Like npe said it's not a standard practice. But if you really have to:
1. First a scalar function
CREATE FUNCTION [dte].[getCleanUpdateQuery] (#pTableName varchar(40), #pQueryFirstPart VARCHAR(200) = '', #pQueryLastPart VARCHAR(200) = '', #pIncludeCurVal BIT = 1)
RETURNS VARCHAR(8000) AS
BEGIN
DECLARE #pQuery VARCHAR(8000);
WITH cte_Temp
AS
(
SELECT
C.name
FROM SYS.COLUMNS AS C
INNER JOIN SYS.TABLES AS T ON T.object_id = C.object_id
WHERE T.name = #pTableName
)
SELECT #pQuery = (
CASE #pIncludeCurVal
WHEN 0 THEN
(
STUFF(
(SELECT ', ' + name + ' = ' + #pQueryFirstPart + #pQueryLastPart FROM cte_Temp FOR XML PATH('')), 1, 2, ''
)
)
ELSE
(
STUFF(
(SELECT ', ' + name + ' = ' + #pQueryFirstPart + name + #pQueryLastPart FROM cte_Temp FOR XML PATH('')), 1, 2, ''
)
) END)
RETURN 'UPDATE ' + #pTableName + ' SET ' + #pQuery
END
2. Use it like this
DECLARE #pQuery VARCHAR(8000) = dte.getCleanUpdateQuery(<your table name>, <query part before current value>, <query part after current value>, <1 if current value is used. 0 if updating everything to a static value>);
EXEC (#pQuery)
Example 1: make all employees columns 'Unknown' (you need to make sure column type matches the intended value:
DECLARE #pQuery VARCHAR(8000) = dte.getCleanUpdateQuery('employee', '', 'Unknown', 0);
EXEC (#pQuery)
Example 2: Remove an undesired text qualifier (e.g. #)
DECLARE #pQuery VARCHAR(8000) = dte.getCleanUpdateQuery('employee', 'REPLACE(', ', ''#'', '''')', 1);
EXEC (#pQuery)
This query can be improved. This is just the one I saved and sometime I use. You get the idea.
Similar to an upsert, you could check if the item exists on the table, if so, delete it and insert it with the new values (technically updating it) but you would lose your rowid if that's something sensitive to keep in your case.
Behold, the updelsert
IF NOT EXISTS (SELECT * FROM Employee WHERE ID = #SomeID)
INSERT INTO Employee VALUES(#SomeID, #Your, #Vals, #Here)
ELSE
DELETE FROM Employee WHERE ID = #SomeID
INSERT INTO Employee VALUES(#SomeID, #Your, #Vals, #Here)
you could do it by deleting the column in the table and adding the column back in and adding a default value of whatever you needed it to be. then saving this will require to rebuild the table

SELECT INTO behavior and the IDENTITY property

I've been working on a project and came across some interesting behavior when using SELECT INTO. If I have a table with a column defined as int identity(1,1) not null and use SELECT INTO to copy it, the new table will retain the IDENTITY property unless there is a join involved. If there is a join, then the same column on the new table is defined simply as int not null.
Here is a script that you can run to reproduce the behavior:
CREATE TABLE People (Id INT IDENTITY(1,1) not null, Name VARCHAR(10))
CREATE TABLE ReverseNames (Name varchar(10), ReverseName varchar(10))
INSERT INTO People (Name)
VALUES ('John'), ('Jamie'), ('Joe'), ('Jenna')
INSERT INTO ReverseNames (Name, ReverseName)
VALUES ('John','nhoJ'), ('Jamie','eimaJ'), ('Joe','eoJ'), ('Jenna','anneJ')
--------
SELECT Id, Name
INTO People_ExactCopy
FROM People
SELECT Id, ReverseName as Name
INTO People_WithJoin
FROM People
JOIN ReverseNames
ON People.Name = ReverseNames.Name
SELECT Id, (SELECT ReverseName FROM ReverseNames WHERE Name = People.Name) as Name
INTO People_WithSubSelect
FROM People
--------
SELECT OBJECT_NAME(c.object_id) as [Table],
c.is_identity as [Id Column Retained Identity]
FROM sys.columns c
where
OBJECT_NAME(c.object_id) IN ('People_ExactCopy','People_WithJoin','People_WithSubSelect')
AND c.name = 'Id'
--------
DROP TABLE People
DROP TABLE People_ExactCopy
DROP TABLE People_WithJoin
DROP TABLE People_WithSubSelect
DROP TABLE ReverseNames
I noticed that the execution plans for both the WithJoin and WithSubSelect queries contained one join operator. I'm not sure if one will be significantly better on performance if we were dealing with a larger set of rows.
Can anyone shed any light on this and tell me if there is a way to utilize SELECT INTO with joins and still preserve the IDENTITY property?
From Microsoft:
When an existing identity column is
selected into a new table, the new
column inherits the IDENTITY property,
unless one of the following conditions
is true:
The SELECT statement contains a join, GROUP BY clause, or aggregate function.
Multiple SELECT statements are joined by using UNION.
The identity column is listed more than one time in the select list.
The identity column is part of an expression.
The identity column is from a remote data source.
If any one of these conditions is
true, the column is created NOT NULL
instead of inheriting the IDENTITY
property. If an identity column is
required in the new table but such a
column is not available, or you want a
seed or increment value that is
different than the source identity
column, define the column in the
select list using the IDENTITY
function.
You could use the IDENTITY function as they suggest and omit the IDENTITY column, but then you would lose the values, as the IDENTITY function would generate new values and I don't think that those are easily determinable, even with ORDER BY.
I don't believe there is much you can do, except build your CREATE TABLE statements manually, SET IDENTITY_INSERT ON, insert the existing values, then SET IDENTITY_INSERT OFF. Yes you lose the benefits of SELECT INTO, but unless your tables are huge and you are doing this a lot, [shrug]. This is not fun of course, and it's not as pretty or simple as SELECT INTO, but you can do it somewhat programmatically, assuming two tables, one having a simple identity (1,1), and a simple INNER JOIN:
SET NOCOUNT ON;
DECLARE
#NewTable SYSNAME = N'dbo.People_ExactCopy',
#JoinCondition NVARCHAR(255) = N' ON p.Name = r.Name';
DECLARE
#cols TABLE(t SYSNAME, c SYSNAME, p CHAR(1));
INSERT #cols SELECT N'dbo.People', N'Id', 'p'
UNION ALL SELECT N'dbo.ReverseNames', N'Name', 'r';
DECLARE #sql NVARCHAR(MAX) = N'CREATE TABLE ' + #NewTable + '
(
';
SELECT #sql += c.name + ' ' + t.name
+ CASE WHEN t.name LIKE '%char' THEN
'(' + CASE WHEN c.max_length = -1
THEN 'MAX' ELSE RTRIM(c.max_length/
(CASE WHEN t.name LIKE 'n%' THEN 2 ELSE 1 END)) END
+ ')' ELSE '' END
+ CASE c.is_identity
WHEN 1 THEN ' IDENTITY(1,1)'
ELSE ' ' END + ',
'
FROM sys.columns AS c
INNER JOIN #cols AS cols
ON c.object_id = OBJECT_ID(cols.t)
INNER JOIN sys.types AS t
ON c.system_type_id = t.system_type_id
AND c.name = cols.c;
SET #sql = LEFT(#sql, LEN(#sql)-1) + '
);
SET IDENTITY_INSERT ' + #NewTable + ' ON;
INSERT ' + #NewTable + '(';
SELECT #sql += c + ',' FROM #cols;
SET #sql = LEFT(#sql, LEN(#sql)-1) + ')
SELECT ';
SELECT #sql += p + '.' + c + ',' FROM #cols;
SET #sql = LEFT(#sql, LEN(#sql)-1) + '
FROM ';
SELECT #sql += t + ' AS ' + p + '
INNER JOIN ' FROM (SELECT DISTINCT
t,p FROM #cols) AS x;
SET #sql = LEFT(#sql, LEN(#sql)-10)
+ #JoinCondition + ';
SET IDENTITY_INSERT ' + #NewTable + ' OFF;';
PRINT #sql;
With the tables given above, this produces the following, which you could pass to EXEC sp_executeSQL instead of PRINT:
CREATE TABLE dbo.People_ExactCopy
(
Id int IDENTITY(1,1),
Name varchar(10)
);
SET IDENTITY_INSERT dbo.People_ExactCopy ON;
INSERT dbo.People_ExactCopy(Id,Name)
SELECT p.Id,r.Name
FROM dbo.People AS p
INNER JOIN dbo.ReverseNames AS r
ON p.Name = r.Name;
SET IDENTITY_INSERT dbo.People_ExactCopy OFF;
I did not deal with other complexities such as DECIMAL columns or other columns that have parameters such as max_length, nor did I deal with nullability, but these things wouldn't be hard to add it if you need greater flexibility.
In the next version of SQL Server (code-named "Denali") you should be able to construct a CREATE TABLE statement much easier using the new metadata discovery functions - which do much of the grunt work for you in terms of specifying precision/scale/length, dealing with MAX, etc. You still have to manually create indexes and constraints; but you don't get those with SELECT INTO either.
What we really need is DDL that allows you to say something like "CREATE TABLE a IDENTICAL TO b;" or "CREATE TABLE a BASED ON b;"... it's been asked for here, but has been rejected (this is about copying a table to another schema, but the same concept could apply to a new table in the same schema with a different table name). http://connect.microsoft.com/SQLServer/feedback/details/632689
I realize this is a really late response but whoever is still looking for this solution, like I was until I found this solution:
You can't use the JOIN operator for the IDENTITY column property to be inherited.
What you can do is use a WHERE clause like this:
SELECT a.*
INTO NewTable
FROM
MyTable a
WHERE
EXISTS (SELECT 1 FROM SecondTable b WHERE b.ID = a.ID)
This works.