Pivoting rows to columns with custom column names in SQL Server - sql

I'm having some difficulty with pivoting rows into columns as I also want to name the columns. Here is my current code, modified:
SELECT Message, value
FROM Table1
CROSS APPLY
(
SELECT value FROM STRING_SPLIT(Message,'"')
WHERE value LIKE '%.%'
)
AS SourceTable
And my current output:
Message value
------------ -----
longmessage1 hello
longmessage1 hi
longmessage1 hey
longmessage1 hola
Just for the sake of shortness, I replaced the actual Message with longmessage1 above. My desired output:
Message greeting1 greeting2 greeting3 greeting4
------------ --------- --------- --------- ---------
longmessage1 hello hi hey hola
The maximum amount of greetings is six, and if a Message doesn't have six, I'm fine with the value of, say greeting 4 and 5 to be NULL.
FYI- I am using SQL Server. I think I could somehow use PIVOT to do this but I'm stuck on the custom column name part and if CROSS APPLY was even the right idea. If anyone could offer some suggestions, that'd be terrific. Thank you!

You can use row_number() and conditional aggregation:
SELECT t1.Message, a.*
FROM Table1 t1 CROSS APPLY
(SELECT MAX(CASE WHEN seqnum = 1 THEN value END) as greeting1,
MAX(CASE WHEN seqnum = 2 THEN value END) as greeting2,
MAX(CASE WHEN seqnum = 3 THEN value END) as greeting3,
MAX(CASE WHEN seqnum = 4 THEN value END) as greeting4,
MAX(CASE WHEN seqnum = 5 THEN value END) as greeting5,
MAX(CASE WHEN seqnum = 6 THEN value END) as greeting6
FROM (SELECT s.value,
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as seqnum
FROM STRING_SPLIT(t1.Message,'"')
WHERE value LIKE '%.%'
) s
) s;
Note: In practice, this will probably preserve the ordering of the values. However that is not guaranteed -- based on the documentation.

Related

flatten data in SQL based on fixed set of column

I am stuck with a specific scenario of flattening the data and need help for it. I need the output as flattened data where the column values are not fixed. Due to this I want to restrict the output to fixed set of columns.
Given Table 'test_table'
ID
Name
Property
1
C1
xxx
2
C2
xyz
2
C3
zz
The scenario is, column Name can have any no. of values corresponding to an ID. I need to flatten the data based in such a way that there is one row per ID field. Since the Name field varies with each ID, I want to flatten it for fix 3 columns like Co1, Co2, Co3. The output should look like
ID
Co1
Co1_Property
Co2
Co2_Property
Co3
Co3_Property
1
C1
xxx
null
null
2
C2
xyz
C3
zz
Could not think of a solution using Pivot or aggregation. Any help would be appreciated.
You can use arrays:
select id,
array_agg(name order by name)[safe_ordinal(1)] as name_1,
array_agg(property order by name)[safe_ordinal(1)] as property_1,
array_agg(name order by name)[safe_ordinal(2)] as name_2,
array_agg(property order by name)[safe_ordinal(2)] as property_2,
array_agg(name order by name)[safe_ordinal(3)] as name_3,
array_agg(property order by name)[safe_ordinal(3)] as property_3
from t
group by id;
All current answers are too verbose and involve heavy repetition of same fragments of code again and again and if you need to account more columns you need to copy paste and add more lines which will make it even more verbose!
My preference is to avoid such type of coding and rather use something more generic as in below example
select * from (
select *, row_number() over(partition by id) col
from `project.dataset.table`)
pivot (max(name) as name, max(property) as property for col in (1, 2, 3))
If applied to sample data in your question - output is
If you want to change number of output columns - you just simply modify for col in (1, 2, 3) part of query.
For example if you would wanted to have 5 columns - you would use for col in (1, 2, 3, 4, 5) - that simple!!!
The standard practice is to use conditional aggregation. That is, to use CASE expressions to pick which row goes to which column, then MAX() to collapse multiple rows into individual rows...
SELECT
id,
MAX(CASE WHEN name = 'C1' THEN name END) AS co1,
MAX(CASE WHEN name = 'C1' THEN property END) AS co1_property,
MAX(CASE WHEN name = 'C2' THEN name END) AS co2,
MAX(CASE WHEN name = 'C2' THEN property END) AS co2_property,
MAX(CASE WHEN name = 'C3' THEN name END) AS co3,
MAX(CASE WHEN name = 'C3' THEN property END) AS co3_property
FROM
yourTable
GROUP BY
id
Background info:
Not having an ELSE in the CASE expression implicitly means ELSE NULL
The intention is therefore for each column to recieve NULL from every input row, except for the row being pivoted into that column
Aggregates, such as MAX() essentially skip NULL values
MAX( {NULL,NULL,'xxx',NULL,NULL} ) therefore equals 'xxx'
A similar approach "bunches" the values to the left (so that NULL values always only appears to the right...)
That approach first uses row_number() to give each row a value corresponding to which column you want to put that row in to..
WITH
sorted AS
(
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY name) AS seq_num
FROM
yourTable
)
SELECT
id,
MAX(CASE WHEN seq_num = 1 THEN name END) AS co1,
MAX(CASE WHEN seq_num = 1 THEN property END) AS co1_property,
MAX(CASE WHEN seq_num = 2 THEN name END) AS co2,
MAX(CASE WHEN seq_num = 2 THEN property END) AS co2_property,
MAX(CASE WHEN seq_num = 3 THEN name END) AS co3,
MAX(CASE WHEN seq_num = 3 THEN property END) AS co3_property
FROM
yourTable
GROUP BY
id

How to merge 2 tables into 1 row, with multiple entries from second table in Oracle SQL

newbee question on (Oracle) SQL.
I'd like this table :
ash_id ash_contact_name ash_contact_telefoonnummber
15313 Name1 022457852114
15313 Name2 122457852114
15313 Name3 222457852114
15313 Name4 322457852114
15313 Name5 422457852114
To Look like this in 1 row :
15313 Name1 022457852114 Name2 122457852114 Name3 222457852114 Name4 322457852114 Name5 422457852114
So I get only 1x the id from first table and multiple coloms with the name with My code now looks like this :
select ash.ash_id ,
con.ash_contact_name, con.ash_contact_telefoonnummer
from D00ASH01.ash_admin_stakeholder ash,ash_contacts con
where con.ash_id = ash.ash_id and con.ash_id = 15313
order by ash.ash_id
The eventual code while not include "con.ash_id = 15313" as I will need to have all the entries. The end result will include more fields from the first table, so I can not just use the second table alone. For now, I want to start to build it up simple.
I tried to make it work with a join but did not made it.
All suggestions welcome,
thanks
Check this, it might help you. If it is not then let me know.
SELECT ash.ash_id , con.ash_contact_name, con.ash_contact_telefoonnummer FROM D00ASH01.ash_admin_stakeholder as ash INNER JOIN ash_contacts as con ON con.ash_id = ash.ash_id WHERE con.ash_id = 15313 ORDER BY ash.ash_id;
SELECT * FROM ash INNER JOIN con USING( ash_id ) Where con_id = 15313 ORDER BY ash_id ;
I'did get the solution for this :
select
c.ash_id,
c.ash_naam_kbo_NL ,
c.ash_naam_kbo_FR ,
c.ash_naam_kbo_DE,
max(Case when rn = 1 then c.ASH_CONTACT_NAME else null end) as name1,
max(Case when rn = 1 then c.ASH_CONTACT_GSMNUMMER else null end) as gsm1,
max(Case when rn = 1 then c.ASH_CONTACT_FAXNUMMER else null end) as fax1,
max(Case when rn = 1 then c.ASH_CONTACT_EMAILADRES else null end) as email1,
max(Case when rn = 2 then c.ASH_CONTACT_NAME else null end) as name2,
max(Case when rn = 2 then c.ASH_CONTACT_GSMNUMMER else null end) as gsm2,
max(Case when rn = 2 then c.ASH_CONTACT_FAXNUMMER else null end) as fax2,
max(Case when rn = 2 then c.ASH_CONTACT_EMAILADRES else null end) as email2
from (
select
table_ash.ash_id,
table_ash.ash_naam_kbo_NL ,
table_ash.ash_naam_kbo_FR ,
table_ash.ash_naam_kbo_DE,
table_contacts.ASH_CONTACT_NAME,
table_contacts.ASH_CONTACT_GSMNUMMER,
table_contacts.ASH_CONTACT_FAXNUMMER,
table_contacts.ASH_CONTACT_EMAILADRES,
ROW_NUMBER () over (partition by table_ash.ash_id order by table_ash.ash_id,
table_ash.ash_naam_kbo_NL) rn
from
ASH_ADMIN_STAKEHOLDER table_ash,
ash_contacts table_contacts
where
table_ash.ash_id = table_contacts.ash_id) c
group by
c.ash_id,
c.ash_naam_kbo_NL ,
c.ash_naam_kbo_FR ,
c.ash_naam_kbo_DE;
So, there's a "select" from a "select". The trick is to generate a rownnumber and use it like a index. For as many as rownumbers as needed, include a max function in the query. This code is the base of answer I needed.
SQL code written in Oracle:
WITH CTE AS(
SELECT
            UP.CLASS,
            UP.NS || UP.RN AS NSR,
            UP.VAL
FROM
            (
            SELECT
                        ROW_NUMBER ()
         OVER (
           PARTITION BY S.CLASS
            ORDER BY
                        S.CLASS) RN,
                        S.*
            FROM
                        STAKEHOLDER S
            ORDER BY
                        CLASS,
                        SID) SS
UNPIVOT (VAL FOR NS IN (NAME, SID)) UP
)
SELECT
            *
FROM
            CTE
PIVOT(MAX(VAL) FOR NSR IN ('NAME1' AS NAME1,
            'SID1' AS SID1,
            'NAME2' AS NAME2,
            'SID2' AS SID2,
            'NAME3' AS NAME3,
            'SID3' AS SID3))
This is not difficult if we handle it with our natural way of thinking. After grouping the table by CLASS, we convert NAME and SID columns into rows and create names commanding values to be converted to columns. Format of names is the original column name + number of subgroups, like NAME1, SID1, NAME2, SID2,… for group 1 and NAME1, SID1, … for group2. Then we concatenate groups and transpose row to columns. The problem is SQL does not support dynamic row-to-column/column-to-row transposition. When the number of columns is small and columns are fixed, the language can mange to do the transpositions. As the number of columns increases, the scenario becomes more and more awkward. Enumerating all columns to be converted is complicated and SQL code becomes bloated. If columns are dynamic, SQL needs to turn to complex and roundabout ways to handle them.
Yet, it is really easy to code the transposition task with the open-source esProc SPL:
| |A|
|1|=connect("ORACLE")|
|2|=A1.query#x("SELECT \* FROM STAKEHOLDER ORDER BY CLASS,SID")|
|3|=A2.fname().m(2:)|
|4|=A2.group#o(CLASS)|
|5|=A4.conj(\~.news(A3;CLASS,A3(#)/A4.\~.#:COL,\~:VAL))|

pivot table returns more than 1 row for the same ID

I have a sql code which I am using to do pivot. Code is as follows:
SELECT DISTINCT PersonID
,MAX(pivotColumn1)
,MAX(pivotColumn2) --originally these were in 2 separate rows)
FROM(SELECT srcID, PersonID, detailCode, detailValue) FROM src) AS SrcTbl
PIVOT(MAX(detailValue) FOR detailCode IN ([pivotColumn1],[pivotColumn2])) pvt
GROUP BY PersonID
In the source data the ID has 2 separate rows due to having its own ID which separates the values. I have now pivoted it and its still giving me 2 separate rows for the ID even though i grouped it and used aggregation on the pivot columns. Ay idea whats wrong with the code?
So I have all my possible detailCode listed in the IN clause. So I have null returned when the value is none but I want it all summarised in 1 row. See image below.
If those are all the options of detailCode , you can use conditional aggregation with CASE EXPRESSION instead of Pivot:
SELECT t.personID,
MAX(CASE WHEN t.detailCode = 'cas' then t.detailValue END) as cas,
MAX(CASE WHEN t.detailCode = 'buy' then t.detailValue END) as buy,
MAX(CASE WHEN t.detailCode = 'sel' then t.detailValue END) as sel,
MAX(CASE WHEN t.detailCode = 'pla' then t.detailValue END) as pla
FROM YourTable t
GROUP BY t.personID

How to join to a table using aliases

Thanks in advance for any help.
I'm wondering how to join two SQL statements I have, based on the fact that I've given the columns aliases.
The source table looks like this (please bear in mind that changing the layout of this table is enormously expensive due to other requirements):
ColumnID RowIdx Val
1 0 "Mr A"
1 1 "Mr B"
1 2 "Mr C"
2 0 "M40 2TB"
2 1 "G23 XYN"
2 2 "HJ 23N"
I use two sql statements to create views on the table, which I think join. This looks something like this:
-- Statement #1
CREATE TEMP TABLE nameTable AS (SELECT
max(CASE WHEN ColumnID=1 THEN Val ELSE null END) AS Name,
row_number() over (ORDER BY RowIdx) AS rowindex
FROM myTable
WHERE (ColumnID=1)
GROUP BY RowIdx
ORDER BY RowIdx);
-- Statement #2
CREATE TEMP TABLE postcodeTable AS (SELECT
max(CASE WHEN ColumnID=2 THEN Val ELSE null END) AS PostCode,
row_number() over (ORDER BY RowIdx) AS rowindex
FROM myTable
WHERE (ColumnID=2)
GROUP BY RowIdx
ORDER BY RowIdx);
-- Statement #3
SELECT * FROM nameTable
INNER JOIN postcodeTable ON nameTable.rowIndex=postcodeTable.rowIndex
The result is the following:
Name PostCode RowIndex
Mr A M40 2TB 0
Mr B G23 XYN 1
Mr C HJ 23N 2
I would like to combine these into a single statement (as it makes generating the statement simpler for my program.
I have chosen to omit some logic from statements 1 and 2 as it is needlessly complicated for my example, but what I have left out prohibits me doing this:
SELECT
max(CASE WHEN ColumnID=1 THEN Val ELSE null END) AS Name,
max(CASE WHEN ColumnID=2 THEN Val ELSE null END) AS PostCode,
row_number() over (ORDER BY RowIdx) AS rowindex
FROM myTable
WHERE (ColumnID=1 OR ColumnID=2)
GROUP BY RowIdx
ORDER BY RowIdx
I'm now considering doing something like this, but I'm not sure if it's possible:
SELECT
max(CASE WHEN ColumnID=1 THEN Val ELSE null END) AS Name,
row_number() over (ORDER BY RowIdx) AS rowindex
FROM myTable
INNER JOIN (SELECT
max(CASE WHEN ColumnID=2 THEN Val ELSE null END) AS PostCode,
row_number() over (ORDER BY RowIdx) AS rowindex
FROM myTable
WHERE (ColumnID=2)
GROUP BY RowIdx
ORDER BY RowIdx) AS postcodeTable ON rowIndex=postcodeTable.rowIndex
WHERE (ColumnID=1)
GROUP BY RowIdx
ORDER BY RowIdx);
Is this something I should even be considering doing? Or should I just stick with temporary tables? If it is, how do I get this to work? The compile issue in on the rowIndex=postcodeTable.rowIndex rowIndex doesn't exist.
I know the original schema is formatted weirdly, but there are all sorts of external reasons for that. Please let me know if you need any more info.
I may be missing something, but I don't understand the need for the complexity in your example queries. Would something like the following (untested) query work?
SELECT Name, PostCode, A.RowIdx AS RowIdx
FROM
(SELECT Val AS Name, RowIdx FROM myTable WHERE ColumnID=1) A
INNER JOIN
(SELECT Val AS PostCode, RowIdx FROM myTable WHERE ColumnID=2) B
ON A.RowIdx = B.RowIdx;

2 Rows to 1 Row - Nested Query

I have a response column that stores 2 different values for a same product based on question 1 and question 2. That creates 2 rows for each product but I want only one row for each product.
Example:
select Product, XNumber from MyTable where QuestionID IN ('Q1','Q2')
result shows:
Product XNumber
Bat abc
Bat abc12
I want it to display like below:
Product Xnumber1 Xnumber2
Bat abc abc12
Please help.
Thanks.
If you always have two different values you can try this:
SELECT a.Product, a.XNumber as XNumber1, b.XNumber as XNumber2
FROM MyTable a
INNER JOIN MyTable b
ON a.Product = b.Product
WHERE a.QuestionId = 'Q1'
AND b.QuestionId = 'Q2'
I assume that XNumber1 is the result for Q1 and Xnumber2 is the result for Q2.
This will work best if you don't have answers for both Q1 and Q2 for all ids
SELECT a.Product, b.XNumber as XNumber1, c.XNumber as XNumber2
FROM (SELECT DISTINCT Product FROM MyTable) a
LEFT JOIN MyTable b ON a.Product = b.Product AND b.QuestionID = 'Q1'
LEFT JOIN MyTable c ON a.Product = c.Product AND c.QuestionID = 'Q2'
This is one way to achieve your expected results. However, it relies on knowing that only xNumber abc and abc12 are the values. If this is not the case, then a dynamic pivot would be likely needed.
SELECT product, max(case when XNumber = 'abc' then xNumber end) as XNumber1,
max(Case when xNumber = 'abc12' then xNumber end) as xNumber2
FROM MyTable
GROUP BY Product
The problem is that SQL needs to know how many columns will be in the result at the time it compiles the SQL. Since the number of columns could be dependent on the data itself (2 rows vs 5 rows) it can't complete the request. Using Dynamic SQL you can find out the number of rows, then pass those values in as the column names which is why the dynamic SQL works.
This will get you two columns, the first will be the product, and the 2nd will be a comma delimited list of xNumbers.
SELECT DISTINCT T.Product,
xNumbers = Stuff((SELECT DISTINCT ', ' + T1.XNumber
FROM MyTable T1
WHERE t.Product = T1.Product
FOR XML PATH ('')),1,1,'')
FROM MyTable T
To get what you want, we need to know how many columns there will be, what to name them, and how to determine which value goes into which column
Been using rank() a lot in current code we have been working on at my day job. So this fun variant came to mind for your solution.
Using rank to get the 1st, 2nd, and 3rd possible item identifier then grouping them to create a simulated pivot
DECLARE #T TABLE (PRODUCT VARCHAR(50), XNumber VARCHAR(50))
INSERT INTO #T VALUES
('Bat','0-12345-98765-6'),
('Bat','0-12345-98767-2'),
('Bat','0-12345-98768-1'),
('Ball','0-12345-98771-6'),
('Ball','0-12345-98772-7'),
('Ball','0-12345-98777-9'),
('Hat','0-12345-98711-6'),
('Hat','0-12345-98712-3'),
('Tee','0-12345-98465-1')
SELECT
PRODUCT,
MAX(CASE WHEN I = 1 THEN XNumber ELSE '' END) AS Xnumber1,
MAX(CASE WHEN I = 2 THEN XNumber ELSE '' END) AS Xnumber2,
MAX(CASE WHEN I = 3 THEN XNumber ELSE '' END) AS Xnumber3
FROM
(
SELECT
PRODUCT,
XNumber,
RANK() OVER(PARTITION BY PRODUCT ORDER BY XNumber) AS I
FROM #T
) AS DATA
GROUP BY
PRODUCT