Stored procedure to do a two-level pivot - sql

I am trying to create a stored procedure in Sybase Adaptive Server Anywhere that will do a double pivot of table. I will first outline with some images what I am trying to accomplish.
The problem
Raw data
Here is the raw data in the table; in the sample code that I have posted lower down this is temporary table #t1 :
First level of pivoting
The first level of pivoting involves grouping on the column rownr and pivoting on the column col, putting the resulting table into temporary table #t2 :
I have the code up to this point which I have posted lower down.
Second level of pivoting
This is the section that I am struggling with. I am now needing to pivot table #t2 grouping on the column ObjectId and replicating the columns Operation and Code for the number of rows in the grouping to produce table #t3. So the result for the example I have given would look like this:
Because two columns are being replicated (Operation and Code) the number of columns in the resulting table should equal 2 multiplied by the number of rows in the grouping with the largest number of rows. Groupings that have less than the maximum number of grouped rows will be padded with null values, as seen in the example.
The code
Here is my code that creates the first two tables, #t1 and #t2 :
begin
create table #t1(rownr int, col nvarchar(15), val nvarchar(300));
insert into #t1 values(1, 'ObjectId', 'A');
insert into #t1 values(1, 'Operation', 'Op1');
insert into #t1 values(1, 'Code', '101');
insert into #t1 values(2, 'ObjectId', 'A');
insert into #t1 values(2, 'Operation', 'Op2');
insert into #t1 values(2, 'Code', '102');
insert into #t1 values(3, 'ObjectId', 'B');
insert into #t1 values(3, 'Operation', 'Op3');
insert into #t1 values(3, 'Code', '103');
insert into #t1 values(4, 'ObjectId', 'B');
insert into #t1 values(4, 'Operation', 'Op4');
insert into #t1 values(4, 'Code', '104');
insert into #t1 values(5, 'ObjectId', 'B');
insert into #t1 values(5, 'Operation', 'Op5');
insert into #t1 values(5, 'Code', '105');
-- Create t2
select
rownr,
Max(case when col = 'ObjectId' then val end) as ObjectId,
Max(case when col = 'Operation' then val end) as Operation,
Max(case when col = 'Code' then val end) as Code
into #t2
from #t1
group by rownr
order by rownr, ObjectId;
select * from #t2;
-- Create #t3 <--- This is where I need help
end
Take note
Please note that I am trying to solve this for Sybase Adaptive Server Anywhere which does not have a pivot statement like Sql Server does, so a solution using a pivot statement will not help me.

You need each set of A, B, etc in a single temporary table with their ObjectIds, with an ordering integer 1,2,3,4 against the ObjectIDs, regardless of the string value of the op.
Getting such an ordered integer is normally easy with an IDENTITY, but you don't want it for all, you want it per A,B,C etc subset.
Thus if you can run a Cursor on each ObjectId value (A,B,C,etc.,) and get the integer ordered operations for those into a temp table, then you can pivot easily with multiple outer joins.
So:
create table #l(Id NUMERIC(8) IDENTITY, op VARCHAR(30), obj VARCHAR(300))
go
set identity_insert #l on
Get a cursor on the objectIds and loop like:
select Id = IDENTITY(8)
, t2.val op
, t1.val obj
into
existing table
#l
from #t1 t1, #t1 t2
where t1.col = 'ObjectId'
and t1.val = 'A' -- this would be the cursors value
and t1.rownr = t2.rownr
and t2.col = 'Operation'
You will then find that #l can be pivotted nicely with multipler outers, because you'll have a table like:
select * from #l order by 3,1
Id op obj
----------- ------ -----
1 Op1 A
2 Op2 A
1 Op3 B
2 Op4 B
3 Op5 B

Just in case this helps someone else, here is the code that I eventually came up with to accomplish the required double pivot:
begin
DECLARE #nr_of_columns smallint;
DECLARE #qry long varchar;
DECLARE #i SMALLINT;
DECLARE #createTable nvarchar(1000);
create table #t1(rownr int, col nvarchar(15), val nvarchar(300));
insert into #t1 values(1, 'ObjectId', 'A');
insert into #t1 values(1, 'Operation', 'Op1');
insert into #t1 values(1, 'Code', '101');
insert into #t1 values(2, 'ObjectId', 'A');
insert into #t1 values(2, 'Operation', 'Op2');
insert into #t1 values(2, 'Code', '102');
insert into #t1 values(3, 'ObjectId', 'B');
insert into #t1 values(3, 'Operation', 'Op3');
insert into #t1 values(3, 'Code', '103');
insert into #t1 values(4, 'ObjectId', 'B');
insert into #t1 values(4, 'Operation', 'Op4');
insert into #t1 values(4, 'Code', '104');
insert into #t1 values(5, 'ObjectId', 'B');
insert into #t1 values(5, 'Operation', 'Op5');
insert into #t1 values(5, 'Code', '105');
-- create t2
select
rownr,
Max(case when col = 'ObjectId' then val end) as ObjectId,
Max(case when col = 'Operation' then val end) as Operation,
Max(case when col = 'Code' then val end) as Code
into #t2
from #t1
group by rownr
order by rownr, ObjectId;
-- create #t3
-- Maximum number of column groups in result table
select max(cols) into #nr_of_columns from (SELECT count() over (partition by ObjectId) as cols from #t2) A;
-- Create temporary table #t3 to hold results
SET #i = 1;
SET #createTable = 'create table #t3(ObjectId nvarchar(300)';
while #i <= #nr_of_columns loop
set #createTable = #createTable || ', Operation' || #i || ' nvarchar(300), Code' || #i || ' nvarchar(300)';
set #i = #i + 1;
end loop;
set #createTable = #createTable || ')';
execute immediate (#createTable);
-- Pivot into #t3
for whatever as cur cursor for
select 'insert into #t3 select ' || rw as qry from
(select '''' || A.ObjectId || ''' AS ObjectId, ' || LIST(attributes) || repeat(',null,null', #nr_of_columns-A.nr_in_group) AS rw from
(SELECT ObjectId, count() over (partition by ObjectId) nr_in_group, row_number() over (partition by ObjectId order by Operation) nr, ''''||Operation||''' AS Operation' || nr || ',' || '''' || Code || ''' AS Code' || nr as attributes FROM #t2 order by ObjectId,Operation) A
group by ObjectId,#nr_of_columns, nr_in_group) B
DO
execute IMMEDIATE (qry);
end for;
-- Output #t2
select * from #t3;
end

Related

Sql Server While Loop with Changing Condition

I have a User Table in my database that contains two fields
user_id
manager_id
I am trying to construct a query to list all of the manager_ids that are associated with a user_id in a hierarchical structure.
So if i give a user_id, i will get that users manager, followed by that persons manager all the way to the very top.
So far i have tried but it doesnt give what i need:
WITH cte(user_id, manager_id) as (
SELECT user_id, manager_id
FROM user
WHERE manager_id=#userid
UNION ALL
SELECT u.user_id, u.manager_id,
FROM user u
INNER JOIN cte c on e.manager_id = c.employee_id
)
INSERT INTO #tbl (manager_id)
select user_id, manager_id from cte;
If anyone can point me in the right direction that would be great.
I thought about a While loop but this may not be very efficient and im not too sure how to implement that.
OP asked for a while loop, and while (ha, pun) this may not be the best way... Ask and you shall receive. (:
Here is sample data I created (in the future, please provide this):
CREATE TABLE #temp (userID int, managerID int)
INSERT INTO #temp VALUES (1, 3)
INSERT INTO #temp VALUES (2, 3)
INSERT INTO #temp VALUES (3, 7)
INSERT INTO #temp VALUES (4, 6)
INSERT INTO #temp VALUES (5, 7)
INSERT INTO #temp VALUES (6, 9)
INSERT INTO #temp VALUES (7, 10)
INSERT INTO #temp VALUES (8, 10)
INSERT INTO #temp VALUES (9, 10)
INSERT INTO #temp VALUES (10, 12)
INSERT INTO #temp VALUES (11, 12)
INSERT INTO #temp VALUES (12, NULL)
While Loop:
CREATE TABLE #results (userID INT, managerID INT)
DECLARE #currentUser INT = 1 -- Would be your parameter!
DECLARE #maxUser INT
DECLARE #userManager INT
SELECT #maxUser = MAX(userID) FROM #temp
WHILE #currentUser <= #maxUser
BEGIN
SELECT #userManager = managerID FROM #temp WHERE userID = #currentUser
INSERT INTO #results VALUES (#currentUser, #userManager)
SET #currentUser = #userManager
END
SELECT * FROM #results
DROP TABLE #temp
DROP TABLE #results
Get rid of this column list in your CTE declaration that has nothing to do with the columns you are actually selecting in the CTE:
WITH cte(employee_id, name, reports_to_emp_no, job_number) as (
Just make it this:
WITH cte as (
I recommend recursive solution:
WITH Parent AS
(
SELECT * FROM user WHERE user_id=#userId
UNION ALL
SELECT T.* FROM user T
JOIN Parent P ON P.manager_id=T.user_id
)
SELECT * FROM Parent
To see demo, run following:
SELECT * INTO #t FROM (VALUES (1,NULL),(2,1),(3,2),(4,1)) T(user_id,manager_id);
DECLARE #userId int = 3;
WITH Parent AS
(
SELECT * FROM #t WHERE user_id=#userId
UNION ALL
SELECT T.* FROM #t T
JOIN Parent P ON P.manager_id=T.user_id
)
SELECT * FROM Parent

SQL Server increment two tables in an update

First, SQL is NOT my strong point, as you'll see. I have one table that keeps track of the next item number, by some type, like so:
declare #maxs as table
(
Equip int,
NextId int
);
-- initial id values
insert into #maxs (Equip, NextId) values (400, 40);
insert into #maxs (Equip, NextId) values (500, 50);
If I create an item of type '400' then the next Id is 40, and that should be incremented to 41. In a case of a single add, that's easy enough. Our program does adds in batch, so here is my problem.
declare #t as table (Id int, Equip int, Descr varchar(20));
-- simulates the batch processing
insert into #t (Equip, Descr) values (400, 'Item 1');
insert into #t (Equip, Descr) values (400, 'Item 2');
insert into #t (Equip, Descr) values (500, 'Item 3');
-- generate the new id's in batch
UPDATE t
SET Id = (SELECT m.NextId + ROW_NUMBER() OVER (PARTITION BY t.Equip ORDER BY t.Equip))
FROM #t t
INNER JOIN #maxs m ON m.Equip = t.Equip
SELECT * FROM #t
This results in both Item 1 and Item 2 having the same Id because only 1 row is returned for 400, so ROW_NUMBER is the same for both. I need to be able to increment the NextId value in #maxs as well as update the entry in #t so that the second row that joins into the 400 value in #maxs will have the next value (almost like a x++ reference in c#). Is there a clean way to do that in SQL?
Thanks in advance.
Just go with JOIN and nested select
declare #t as table (Id int, Equip int, Descr varchar(20));
-- simulates the batch processing
insert into #t (Equip, Descr) values (400, 'Item 1');
insert into #t (Equip, Descr) values (400, 'Item 2');
insert into #t (Equip, Descr) values (500, 'Item 3');
-- generate the new id's in batch
UPDATE t
SET
Id = t.Equip + s.RowNum
FROM #t t
JOIN (select Equip,
Descr,
ROW_NUMBER() OVER (PARTITION BY Equip ORDER BY Equip) RowNum
from #t) s
on t.Equip = s.Equip and t.Descr = s.Descr
select * from #t
And if possible, try to switch from table variable to temporary table
You can do what you want with a CTE:
WITH toupdate as (
SELECT t.*,
ROW_NUMBER() OVER (PARTITION BY t.Equip ORDER BY t.Equip) as seqnum
FROM #t t
UPDATE t
SET
Id = m.NextId + seqnum
FROM toupdate t INNER JOIN
#maxs m
ON m.Equip = t.Equip;
I'm not sure this is a good idea, though. It is better to use identity columns to identify each row. There are definitely some cases, though, where numbering within a group is useful as a secondary key (for instance, line items on an invoice).

SQL Server Merge - Getting matched records to another temp table

I have a MERGE query to update data. In case of no match I am inserting records to source getting the output to a temporary table.
Would it be possible to get the matched records to temporary table as well? Basically to avoid duplication of data in further processing I need to have copy of matched records.
This is my MERGE command:
MERGE Product.ProductHeaderRepository AS t
USING (SELECT GETDATE() as d, c1, c2, c3,
Name FROM Supplier.ProductHeaderImport
WHERE (BatchID = #BatchID) ) AS s
ON dbo.GetProductHeaderId(s.c1,S.c2,S.c3) <0
WHEN NOT MATCHED BY TARGET THEN
INSERT (Name, c1,c2,c3) VALUES (Name, c2,c2,c3)
OUTPUT INSERTED.iD, s.c1, s.c2, s.c3 INTO #TmpTable;
You could create a MATCHED clause that does not change anything and just updates a variable, e.g.
DECLARE #T1 TABLE (A INT, B INT);
DECLARE #T2 TABLE (A INT, B INT);
DECLARE #T3 TABLE (Action VARCHAR(20), A INT, B INT);
INSERT #T1 VALUES (1, 1), (2, 2), (3, 3);
INSERT #T2 VALUES (1, 0), (2, NULL), (4, 0);
DECLARE #I INT; -- VARIABLE TO UPDATE
MERGE #T2 B
USING #T1 A
ON A.A = B.A
WHEN MATCHED THEN
UPDATE SET #I = 1 -- DO NOTHING MEANINGFUL IN THE UPDATE;
WHEN NOT MATCHED BY TARGET THEN
INSERT (A, B) VALUES (A.A, A.B)
OUTPUT $action, ISNULL(inserted.A, deleted.A), ISNULL(inserted.B, deleted.B) INTO #T3;
SELECT *
FROM #T3;
Will return:
Action A B
INSERT 3 3
UPDATE 1 0
UPDATE 2 NULL
So if you add a new column to #TmpTable to store the action you can get your matched rows using:
SELECT *
FROM #TmpTable
WHERE Action = 'UPDATE';
And your new rows using:
SELECT *
FROM #TmpTable
WHERE Action = 'INSERT';

comparing two colums in sqlserver and returing the remaining data

I have two tables. First one is student table where he can select two optional courses and other table is current semester's optional courses list.
When ever the student selects a course, row is inserted with basic details such as roll number, inserted time, selected course and status as "1". When ever a selected course is de-selected the status is set as "0" for that row.
Suppose the student has select course id 1 and 2.
Now using this query
select SselectedCourse AS [text()] FROM Sample.dbo.Tbl_student_details where var_rollnumber = '020803009' and status = 1 order by var_courseselectedtime desc FOR XML PATH('')
This will give me the result as "12" where 1 is physics and 2 is social.
the second table holds the value from 1-9
For e.g course id
1 = physics
2 = social
3 = chemistry
4 = geography
5 = computer
6 = Spoken Hindi
7 = Spoken English
8 = B.EEE
9 = B.ECE
now the current student has selected 1 and 2. So on first column, i get "12" and second column i need to get "3456789"(remaining courses).
How to write a query for this?
This is not in single query but is simple.
DECLARE #STUDENT AS TABLE(ID INT, COURSEID INT)
DECLARE #SEM AS TABLE (COURSEID INT, COURSE VARCHAR(100))
INSERT INTO #STUDENT VALUES(1, 1)
INSERT INTO #STUDENT VALUES(1, 2)
INSERT INTO #SEM VALUES(1, 'physics')
INSERT INTO #SEM VALUES(2, 'social')
INSERT INTO #SEM VALUES(3, 'chemistry')
INSERT INTO #SEM VALUES(4, 'geography')
INSERT INTO #SEM VALUES(5, 'computer')
INSERT INTO #SEM VALUES(6, 'Spoken Hindi')
INSERT INTO #SEM VALUES(7, 'Spoken English')
INSERT INTO #SEM VALUES(8, 'B.EEE')
INSERT INTO #SEM VALUES(9, 'B.ECE')
DECLARE #COURSEIDS_STUDENT VARCHAR(100), #COURSEIDS_SEM VARCHAR(100)
SELECT #COURSEIDS_STUDENT = COALESCE(#COURSEIDS_STUDENT, '') + CONVERT(VARCHAR(10), COURSEID) + ' ' FROM #STUDENT
SELECT #COURSEIDS_SEM = COALESCE(#COURSEIDS_SEM , '') + CONVERT(VARCHAR(10), COURSEID) + ' ' FROM #SEM WHERE COURSEID NOT IN (SELECT COURSEID FROM #STUDENT)
SELECT #COURSEIDS_STUDENT COURSEIDS_STUDENT, #COURSEIDS_SEM COURSEIDS_SEM
try this:
;WITH CTE as (select ROW_NUMBER() over (order by (select 0)) as rn,* from Sample.dbo.Tbl_student_details)
,CTE1 As(
select rn,SselectedCourse ,replace(stuff((select ''+courseid from course_details for xml path('')),1,1,''),SselectedCourse,'') as rem from CTE a
where rn = 1
union all
select c2.rn,c2.SselectedCourse,replace(rem,c2.SselectedCourse,'') as rem
from CTE1 c1 inner join CTE c2
on c2.rn=c1.rn+1
)
select STUFF((select ''+SselectedCourse from CTE1 for xml path('')),1,0,''),(select top 1 rem from CTE1 order by rn desc)

Formulation of insert into keyword in SQL Server

I want to insert some values to the table, there is an order such as 1,2,3....n
Insert Into table_name VALUES ( '1', 'A' )
Insert Into table_name VALUES ( '2', 'AA' )
Insert Into table_name VALUES ( '3', 'AAC' )
Insert Into table_name VALUES ( '.', '....' )
Insert Into table_name VALUES ( '.', '....' )
Insert Into table_name VALUES ( 'n', '....' )
How can I formulate this INSERT statement?
If you want to insert a series of rows - sure, you can use a loop - but how do you know what other values (other than the index) to get??
DECLARE #index INT
SET #index = 0
WHILE #index < 10
BEGIN
INSERT INTO dbo.table_name(Index)
VALUES( CAST(#index AS VARCHAR(50)) ) -- or whatever type you need....
SET #index = #index + 1
END
The usual way to do this is to select the values to insert from somewhere else:
INSERT INTO company1.new_customers (id, name, address)
SELECT
NULL -- this will trigger the DB to auto-generate the new id's
,name
,address
FROM company2.old_customers
If you have to use a loop in SQL you're doing it wrong.
SQL works with sets.