Pivot table with dynamic columns in date order - tsql

I have a pivot table creating 2 columns for each line that I would like to go in date order.
Below is the data in the raw format
-------------------------------------------------------------
partnum | period | TotalQty | ToldSold
005483-6 | 2015-08 | 100.00000000 | 389.379000
0551105 | 2015-08 | 10.00000000 | 4560.773000
0CT202305 | 2015-09 | 4.00000000 | 2285.430800
0CTR00905 | 2015-10 | 2.00000000 | 654.305400
183386-32 | 2016-01 | 20.00000000 | 75.060400
24-175UV50| 2016-03 | 450.00000000 | 42.723000
I have the following code to generate the dynamic pivot table
DECLARE #cols AS NVARCHAR(MAX),
#colsName AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(period +'_'+c.col)
from #orderhistory
cross apply
(
select 'TotalQty' col
union all
select 'ToldSold'
) c
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #cols
select #colsName
= STUFF((SELECT distinct ', ' + QUOTENAME(period +'_'+c.col)
+' as ['
+ period + case when c.col = 'TotalQty' then ' QtySold]' else 'Total $ Sold]' end
from #orderhistory
cross apply
(
select 'TotalQty' col
union all
select 'ToldSold'
) c
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #colsName
set #query
= 'SELECT partnum, ' + #colsName + '
from
(
select
partnum,
period +''_''+col col,
value
from
(
select partnum,
period,
cast(TotalQty as numeric(10, 2)) TotalQty,
cast(ToldSold as numeric(10, 2)) ToldSold
from #orderhistory
) src
unpivot
(
value
for col in (TotalQty, ToldSold)
) unpiv
) s
pivot
(
sum(value)
for col in (' + #cols + ')
) p
order by partnum'
execute(#query)
It creates data like the following
partnum 2016-02 QtySold 2015-08Total $ Sold 2015-11Total $ Sold 2015-12 QtySold
005483-10 NULL NULL NULL 100.00
005483-12 NULL NULL 1249.68 450.00
005483-14 NULL NULL NULL 70.00
005483-2 NULL NULL 1234.19 350.00
005483-3 10.00 NULL NULL NULL
What I would like to see is the headers go across in date order
partnum | 2015-08 QtySold | 2015-08 Total $ Sold | 2015-09 QtySold | 2015-09 Total $ Sold........
All the way across until I get to my current month.
I think I need to add an order by somewhere, I just don't know where. This is my first multiple column dynamic pivot table so I'm a little lost. Everywhere i have tried has given me some sort of error.
Any help is greatly appreciated!!

you would order your #colNames data..
SELECT #colsName = STUFF((
SELECT DISTINCT
', '
+ QUOTENAME(period + '_' + c.col)
+ ' as ['
+ period
+ CASE WHEN c.col = 'TotalQty' THEN ' QtySold]' ELSE 'Total $ Sold]' END
FROM #orderhistory
CROSS APPLY (SELECT 'TotalQty' col UNION ALL SELECT 'ToldSold') c
--ORDER BY HERE
ORDER BY period, c.col
FOR XML PATH(''), TYPE
).value('.','NVARCHAR(MAX)'),1,1,'')

Related

DB2: How to transpose mutlidimensional table from row to column to find data changes across rows

I am trying the following with Db2:
Problem
So I've got a table with 80+ columns and two rows.
I need to accomplish is checking what columns have changed value between the two rows, and return a table of the column names that have changed, their initial value from row1, and their new value from row2.
Approach so far
My initial idea was to perform a pivot of the two rows into two columns, row 1 as column 1, row 2 as column 2, then join a column of column names (likely taken from syscat.columns) to the table as column 3, at which point I can then do a select where column1 != column2, hence returning the rows with all the data needed. But alas, it was not long after coming up with this that I discover DB2 doesn't support pivot / unpivot...
Question
So is there any idea for how to accomplish this in DB2, taking a table with 80+ columns and two rows like so:
| Col A | Col B | Col C | ... | Col Z|
| ----- | ----- | ----- | --- | ---- |
| Val A | Val B | 123 | ... | 01/01/2021 |
| Val C | Val B | 124 | ... | 02/01/2021 |
And returning a table with the columns changed, their initial value, and their new value:
| Initial | New | ColName|
| ----- | ----- | ----- |
| Val A | Val C | Col A |
| 123 | 124 | Col C |
| 01/01/2021 | 02/01/2021 | Col Z |
Also note the column data types also vary, so will need to be converted to varchar
DB2 version is 11.1
EDIT: Also for reference as per comment request, this is code I attempted to use to achieve this goal:
WITH
INIT AS (SELECT * FROM TABLE WHERE SOMEDATE=(SELECT MIN(SOMEDATE) FROM TABLE),
LATE AS (SELECT * FROM TABLE WHERE SOMEDATE=(SELECT MAX(SOMEDATE) FROM TABLE),
COLS AS (SELECT COLNAME FROM SYSCAT.COLUMNS WHERE TABNAME='TABLE' ORDER BY COLNO)
SELECT * FROM (
SELECT
COLNAME AS ATTRIBUTE,
(SELECT COLNAME AS INITIAL FROM INIT),
(SELECT COLNAME AS NEW FROM LATE)
FROM
COLS
WHERE
(INITIAL != NEW) OR (INITIAL IS NULL AND NEW IS NOT NULL) OR (INITIAL IS NOT NULL AND NEW IS NULL));
Only issue with this one is that I couldn't figure how to use the values from the COLS table as the columns to be selected
You may easily generate text of the expressions needed, if you don't want to type them manually.
Consider the following example, if you want to print different column values only in 2 rows of the same quite a wide table SYSCAT.TABLES. We use the following query for such an expression generation.
SELECT
'DECODE(I.I, '
|| LISTAGG(COLNO || ', A.' || COLNAME || CASE WHEN TYPENAME NOT LIKE '%CHAR%' AND TYPENAME NOT LIKE '%GRAPHIC' THEN '::VARCHAR(128)' ELSE '' END, ', ')
|| ') AS INITIAL' AS EXPR_INITIAL
, 'DECODE(I.I, '
|| LISTAGG(COLNO || ', B.' || COLNAME || CASE WHEN TYPENAME NOT LIKE '%CHAR%' AND TYPENAME NOT LIKE '%GRAPHIC' THEN '::VARCHAR(128)' ELSE '' END, ', ')
|| ') AS NEW' AS EXPR_NEW
, 'DECODE(I.I, '
|| LISTAGG(COLNO || ', ''' || COLNAME || '''', ', ')
|| ') AS COLNAME' AS EXPR_COLNAME
FROM SYSCAT.COLUMNS C
WHERE TABSCHEMA = 'SYSCAT' AND TABNAME = 'TABLES'
AND TYPENAME NOT LIKE '%LOB';
It doesn't matter how many columns the table contains. We just filter out the columns of *LOB types as an example. If you want them as well, you should change the ::VARCHAR(128) casting to some ::CLOB(XXX).
These 3 generated expressions we put to the corresponding places in the query below:
WITH MYTAB AS
(
-- We enumerate the rows to reference them later
SELECT ROWNUMBER() OVER () RN_, T.*
FROM SYSCAT.TABLES T
WHERE TABSCHEMA = 'SYSCAT'
FETCH FIRST 2 ROWS ONLY
)
SELECT *
FROM
(
SELECT
-- Place here the result got in the EXPR_INITIAL column
-- , Place here the result got in the EXPR_NEW column
-- , Place here the result got in the EXPR_COLNAME column
FROM MYTAB A, MYTAB B
,
(
SELECT COLNO AS I
FROM SYSCAT.COLUMNS
WHERE TABSCHEMA = 'SYSCAT' AND TABNAME = 'TABLES'
AND TYPENAME NOT LIKE '%LOB'
) I
WHERE A.RN_ = 1 AND B.RN_ = 2
)
WHERE INITIAL IS DISTINCT FROM NEW;
The result I got in my database:
|INITIAL |NEW |COLNAME |
|--------------------------|--------------------------|---------------|
|2019-06-04-22.44.14.493001|2019-06-04-22.44.14.502001|ALTER_TIME |
|26 |15 |COLCOUNT |
|2019-06-04-22.44.14.493001|2019-06-04-22.44.14.502001|CREATE_TIME |
|2019-06-04-22.44.14.493001|2019-06-04-22.44.14.502001|INVALIDATE_TIME|
|2019-06-04-22.44.14.493001|2019-06-04-22.44.14.502001|LAST_REGEN_TIME|
|ATTRIBUTES |AUDITPOLICIES |TABNAME |

postgres expression to select elements from array

I want to select certain elements from an array column. I know you can do it by position, but I want to filter on content. Here's my data:
table_name | column_names
---------------------+---------------------------------------------------------------
attribute_definition | {attribute_type_concept_id}
cohort_definition | {definition_type_concept_id,subject_concept_id}
condition_occurrence | {condition_concept_id,condition_source_concept_id,condition_type_concept_id}
death | {cause_concept_id,cause_source_concept_id,death_impute_concept_id,death_type_concept_id}
device_exposure | {device_concept_id,device_source_concept_id,device_type_concept_id}
drug_exposure | {dose_unit_concept_id,drug_concept_id,drug_source_concept_id,drug_type_concept_id,route_concept_id}
What I would like to say is something like:
SELECT table_name,
array_agg(SELECT colname FROM column_names WHERE colname LIKE '%type%') AS type_cols,
array_agg(SELECT colname FROM column_names WHERE colname NOT LIKE '%type%') AS other_cols
FROM mytable
GROUP BY table_name
And the result I would like would be:
table_name | type_cols | other_cols
----------------------+--------------------------------------------------------------------------------------------------------------
attribute_definition | {attribute_type_concept_id} | {}
cohort_definition | {definition_type_concept_id} | {subject_concept_id}
condition_occurrence | {condition_type_concept_id} | {condition_concept_id,condition_source_concept_id}
death | {death_type_concept_id} | {cause_concept_id,cause_source_concept_id,death_impute_concept_id}
device_exposure | {device_type_concept_id} | {device_concept_id,device_source_concept_id}
drug_exposure | {drug_type_concept_id} | {dose_unit_concept_id,drug_concept_id,drug_source_concept_id,route_concept_id}
So, I want to end up with the same number of rows but different columns. There's gotta be a simple way to do this. Why can't I find it?
unnest is your friend. As in:
SELECT table_name,
array(SELECT colname FROM unnest(column_names) AS colname WHERE colname LIKE '%type%') AS type_cols,
array(SELECT colname FROM unnest(column_names) AS colname WHERE colname NOT LIKE '%type%') AS other_cols
FROM mytable
GROUP BY table_name, column_names
Here is Dan Getz's answer again but in a self-contained statement so it's easily runnable without copying my data.
with grps as
(
with numlist as
(
select '1 - 10' as grp, generate_series(1,10) num
union
select '11 - 20', generate_series(11,20) order by 1,2
)
select grp, array_agg(num) as nums
from numlist
group by 1
)
select grp,
(select array_agg(evens) from unnest(nums) as evens where evens % 2 = 0) as evens,
(select array_agg(odds) from unnest(nums) as odds where odds % 2 != 0) as odds
from grps
group by grp, nums;
grp | evens | odds
---------+------------------+------------------
11 - 20 | {12,14,16,18,20} | {11,13,15,17,19}
1 - 10 | {2,4,6,8,10} | {1,3,5,7,9}

Postgresql: extract number before given substring

I want to create variable: Number which extract numbers before $
Name | Number
abc1/12 $ | 12
12av 12$ | 12
114$-bgv | 114
I have a query:
select Name, substring(Name FROM '%#"[0-9]+#"$%' FOR '#') as Number
from table;
But it returns:
Name | Number
abc1/12 $ |
12av 12$ | 2
114$-bgv | 4
Any ideas?
Use regexp_matches():
select
(regexp_matches(Name, '^(.*[^\d]+){0,1}(\d+)\$'))[2] as Number
from (
select 'abc1/12$' as Name
union select '12av 12$'
union select 'bgv-114$'
union select '124$'
) t
Or with substring()
select
coalesce(substring(Name FROM '.*[^\d]+(\d+)\$'), '') ||
coalesce(substring(Name FROM '^(\d+)\$'), '') as Number
from (
select 'abc1/12$' as Name
union select '12av 12$'
union select 'bgv-114$'
union select '124$'
) t

joining tables with different length column values

I need to get ID by joining columns of tables with variable length.
Table A has 2 columns ID and PostCode
-----------------
| ID | PostCode |
|----|----------|
| 1 | BR |
|----|----------|
| 2 | WT |
|----|----------|
| 3 | B71 |
|----|----------|
| 4 | BR5 |
|----|----------|
Table B has columns with Name and Full postcode
|------|----------|
| Name | PostCode |
|------|----------|
| Mr X | CR2 5ER |
|------|----------|
| Ms Y | BT2 6ER |
|------|----------|
| XX | B71 4WQ |
|------|----------|
| YY | BR4 8ER |
|------|----------|
| SS | BR5A 5RT |
|------|----------|
I need to get Id's 1 [BR->BR4 8ER], 3 [B71->B71 4WQ] and 4 [BR5->BR5A 5RT]
How do I get to work this?
select A.PostCode, B.PostCode as FullPostCode, B.Name
from A
join B
on substring(B.PostCode,0,len(A.PostCode)) = A.PostCode
Consider the postcode BR29 8LN. If table A has codes B and BR, this postcode will be captured TWICE - not what the OP would want, and not what I wanted.
The below captures everything so long as after the postcode prefix, there is a number thus delimiting the postcode area:
select A.PostCode, B.PostCode as FullPostCode, B.Name
from B
inner join A
on substring(B.PostCode ,0,len(A.PostCode)+1) = A.PostCode
WHERE IsNumeric(substring(B.PostCode ,len(A.PostCode)+1,1)) = 1
This may help.
DECLARE #TableA TABLE (UserID INT,
PostCode VARCHAR(10))
DECLARE #TableB TABLE (Name VARCHAR(10),
PostCode VARCHAR(10))
INSERT INTO #TableA
VALUES
('1', 'BR'),
('2', 'WT'),
('3', 'B71'),
('4', 'BR5')
INSERT INTO #TableB
VALUES
('Mr X', 'CR2 5ER'),
('Ms Y', 'BT2 6ER'),
('XX', 'B71 4WQ'),
('YY', 'BR4 8ER'),
('SS', 'BR5A 5RT');
WITH CTE
AS (
SELECT CAST(UserID AS VARCHAR(10)) AS UserID,
Name,
tb.PostCode,
ta.PostCode AS PostCode2
,
ROW_NUMBER() OVER (PARTITION BY UserID ORDER BY tb.PostCode DESC) AS PcID
FROM #TableA AS ta
JOIN #TableB AS tb
ON ta.PostCode = LEFT(tb.PostCode, LEN(ta.PostCode))
)
, cte2
AS (
SELECT STUFF((SELECT ', ' + c2.UserID + ' [' + c2.PostCode2 + '-' + c2.PostCode + ']'
FROM cte AS c2
WHERE c1.UserID = c2.UserID
AND PcID = 1
FOR XML PATH('')), 1, 2, '') AS PostCodeMatch
FROM cte AS c1
WHERE PcID = 1
)
SELECT DISTINCT STUFF((SELECT ', ' + PostCodeMatch
FROM cte2 AS c2
FOR XML PATH('')), 1, 2, '') AS PostCodeMatch
FROM cte2
You might do something like this:
select A.PostCode, B.PostCode as FullPostCode, B.Name
from A
join B on B.PostCode like A.PostCode + '%'

Troubles with splitting values into multiple rows using T-SQL

I have a Sql Server 2K8 R2 DB with a table that have a column containings multiples values, separated by (char 13 and char 10).
I'm building a script to import the data in a properly normalized schema.
My source table contains something like this :
ID | Value
________________
1 | line 1
line 2
________________
2 | line 3
________________
3 | line 4
line 5
line 6
________________
and so on.
[edit] FYI, Id is integer and value is nvarchar(3072) [/edit]
What I want is to query the table to ouput somethnig like this :
ID | Value
________________
1 | line 1
________________
1 | line 2
________________
2 | line 3
________________
3 | line 4
________________
3 | line 5
________________
3 | line 6
________________
I've read many answer here on SO, and also around the web, and I find that using master..sptvalues should be the solution. Especially, I tried to reprodude the solution of the question Split one column into multiple rows.
However, without success (suspecting having two chars causing problems).
By now, I wrote this query :
SELECT
T.ID,
T.Value,
RIGHT(LEFT(T.Value,spt.Number-1),
CHARINDEX(char(13)+char(10),REVERSE(LEFT(char(13)+char(10)+T.Value,spt.Number-1)))) as Extracted
FROM
master..spt_values spt,
ContactsNew T
WHERE
Type = 'P' AND
spt.Number BETWEEN 1 AND LEN(T.Value)+1
AND
(SUBSTRING(T.Value,spt.Number,2) = char(13)+char(10) OR SUBSTRING(T.Value,spt.Number,2) = '')
This query, unfortunately is returning :
ID | Value | Extracted
________________________________
1 | line 1 | <blank>
line 2 |
________________________________
1 | line 1 | line 2
line 2 |
________________________________
2 | line 3 | <blank>
________________________________
3 | line 4 | <blank>
line 5 |
line 6 |
________________________________
3 | line 4 | line 5
line 5 | line 6
line 6 |
________________________________
3 | line 4 | line 6
line 5 |
line 6 |
________________________________
<blank> is an empty string, not null string.
I'd appreciate some help to tune my query.
[Edit2] My source table contains less than 200 records, and performance is not a requirement, so I'm targeting a simple solution rather than an efficient one [Edit2]
[Edit3] The source database is readonly. I can't add stored procedure, function, or clr type. I have to do this in a single query. [Edit3]
[Edit4] Something strange... it seems that whitespaces are also considered as separators.
If I run the following query :
SELECT
T.ID,
replace(T.Value, '#', ' '),
replace(RIGHT(
LEFT(T.Value,spt.Number-1),
CHARINDEX( char(13) + char(10),REVERSE(LEFT(char(10) + char(13)+T.Value,spt.Number-0)))
), '#', ' ')
FROM
master..spt_values spt,
(
select contactID,
replace(Value,' ', '#') Value
from ContactsNew where Value is not null
) T
WHERE
Type = 'P' AND
spt.Number BETWEEN 1 AND LEN(T.Value)+1
AND
(SUBSTRING(T.Value,spt.Number,2) = char(13) + char(10) OR SUBSTRING(T.Value,spt.Number,1) = '')
I got the correct number of returns (however, still having wrong values), while running this query :
SELECT
T.ID,
T.Value,
RIGHT(
LEFT(T.Value,spt.Number-1),
CHARINDEX( char(13) + char(10),REVERSE(LEFT(char(10) + char(13)+T.Value,spt.Number-0)))
)
FROM
master..spt_values spt,
(
select contactID,
Value
from ContactsNew where Value is not null
) T
WHERE
Type = 'P' AND
spt.Number BETWEEN 1 AND LEN(T.Value)+1
AND
(SUBSTRING(T.Value,spt.Number,2) = char(13) + char(10) OR SUBSTRING(T.Value,spt.Number,1) = '')
splits on spaces also
EDIT #1: I've deleted original answer text. Try following query. I slightly modified your logic. If you should have any questions about it, don't hesitate to ask in comment. If You need another split delimiter just introduce another nested query to replace that delimiter with CHAR(13)+CHAR(10).
SELECT
*
FROM
(
SELECT
T.ID,
T.Value,
CASE
WHEN CHARINDEX(CHAR(13) + CHAR(10), SUBSTRING(T.Value, spt.number, LEN(T.Value) - spt.Number + 1)) > 0 THEN
LEFT(
SUBSTRING(T.Value, spt.number, LEN(T.Value) - spt.Number + 1),
CHARINDEX(CHAR(13) + CHAR(10), SUBSTRING(T.Value, spt.number, LEN(T.Value) - spt.Number + 1)) - 1)
/* added by Steve B. see comments for the reasons */
when len(T.Value) = spt.Number then right(t.Value, spt.number -1)
/* end of edit */
ELSE
SUBSTRING(T.Value, spt.number, LEN(T.Value) - spt.Number + 1)
END EXTRACTED
FROM
master..spt_values spt,
ContactsNew T
WHERE
Type = 'P' AND
spt.Number BETWEEN 1 AND LEN(T.Value)+1
) X
WHERE
EXTRACTED <> '' AND
(
LEFT(X.VALUE, LEN(EXTRACTED)) = EXTRACTED OR
X.Value LIKE '%' + CHAR(13) + CHAR(10) + EXTRACTED + CHAR(13) + CHAR(10) + '%' OR
X.Value LIKE '%' + CHAR(13) + CHAR(10) + EXTRACTED
)
A sample query showing how to perform this kind of operation against some test data similar to described.
If you aren't able to declare variables in your final statement you can find/replace them for their values, but it makes things a bit simpler.
This works by replacing CR+LF with a single character before doing the split.
If '|' is in use in your data, select another single character which isn't to use as the temporary delimiter.
declare #crlf nvarchar(2) = char(10) + char(13)
declare #cDelim nvarchar(1) = N'|'
-- test data
declare #t table
(id int
,value nvarchar(3072))
insert #t
select 1, 'line1' + #crlf + 'line2'
union all select 2, 'line3'
union all select 3, 'line4' + #crlf + 'line5' + #crlf + 'line6'
-- /test data
;WITH charCTE
AS
(
--split the string into a dataset
SELECT D.id, D.value, SUBSTRING(D.s,n,CHARINDEX(#cDelim, D.s + #cDelim,n) -n) AS ELEMENT
FROM (SELECT id, value, REPLACE(value,#crlf,#cDelim) as s from #t) AS D
JOIN (SELECT TOP 3072 ROW_NUMBER() OVER (ORDER BY a.type, a.number, a.name) AS n
FROM master.dbo.spt_values a
CROSS
JOIN master.dbo.spt_values b
) AS numsCte
ON n <= LEN(s)
AND SUBSTRING(#cDelim + s,n,1) = #cDelim
)
SELECT id, ELEMENT
FROM charCTE
order by id, element