Flatten and group a set of data using PIVOT twice - sql

I'm trying to flatten a set of data from a SQL query (using MS SQL Server) and I need to do it twice.
This is the example data I have.
This is the data I would like to show
I managed to get one height and one area for each building using PIVOT but what I need is to pivot again in order to have one row for every building that contains all the related data.
I think the solution requires the use of both PIVOT and CROSS APPLY but I cannot find the right way to use them.
Thanks for your help!

No need for a CROSS APPLY, a simple PIVOT or Conditional Aggregation would do. Just remember to "FEED" your pivot with the minimum number of required columns.
Select *
From (
Select Building
,Item = concat([Floor],MeasureName)
,Value = MeasureValue
From YourTable
) src
Pivot ( max( Value ) for Item in ( [floor1height]
,[floor1area]
,[floor2height]
,[floor2area]
) ) Pvt

Related

Teradata: IN clause in Pivot can't take data from Table

I wish to extract a few Calender Weeks from an yearly data. Once that's done, I want to pivot it, so that there is one row for each ID.
We have a table DB.MY_CWs having just one column CW containing the Calender Weeks we are interested in.
The following code extracts the relevant Calender Weeks.
CREATE TABLE DB.MY_TABLE AS
(
SELECT ID,
WeekNumber_Of_Year(Sales_Date)) AS CW,
AVG(Sales) AS Sales
FROM DB.DataBase_XYZ
WHERE CW IN (SELECT CW FROM DB.MY_CWs)
GROUP BY ID,CW
) WITH DATA;
This Code gives us the output like this:
But, I would like to pivot it so that I get an output like this:
I took the help from code here and ran the following, but TeraData doesn't respond and there is no Error either.
CREATE TABLE DB.MY_TABLE2 AS
(
SELECT *
FROM DB.MY_TABLE
PIVOT
(SUM(Sales) AS Sales
FOR CW IN (SELECT CW FROM DB.MY_CWs)
) AS dt
) WITH DATA;
If instead of (SELECT CW FROM DB.MY_CWs) I would have used (15,16,17), then everything works fine and I would have got the pivoted Table, as shown above.
Can anyone suggest where I am making the mistake?
Many thanks.
I tried to recreate the scenario.
I am getting below error.
CREATE TABLE Failed. 4306: (-4306)Invalid PIVOT query: PIVOT query with sub-query in IN-List is not supported in DDL statement.
There are few limitation while using subquery in pivot table.
TD Documentation:
https://docs.teradata.com/r/Teradata-VantageTM-NewSQL-Engine-Release-Summary/March-2019/Release-16.20-Feature-Update-1-Features/Subquery-Support-in-PIVOT-IN-List
Snippet from TD Documentation
Considerations
PIVOT with a subquery in the IN-list is not supported in a multistatement request. PIVOT columns are decided dynamically at the optimization phase. Because of this dynamic behavior, the following are usage considerations of a PIVOT query with a subquery in the IN-list.
Not supported in DDL creation statements.
Not supported in stored procedure's cursor FETCH statement.
SET operations are not allowed on a PIVOT query if subquery is given in the IN-list.
Resultant PIVOT column names cannot be explicitly specified in the SELECT list.
Does not support ORDER BY clause.
If you are using SQL Assistant, kindly check your history for the error details.
Otherwise you can query dbc.dbqlogtbl to check the errortext.
Workaround:
You can achieve the desired output through Dynamic SQL and Stored Procedure.
Steps:
Convert the output of the subquery to a String. We can do that through XMLAGG.
Concatenate the Step1 output in the IN Clause and execute the dynamically generated SQL.
REPLACE PROCEDURE DYNAMIC_PIVOT()
BEGIN
DECLARE Sqltxt VARCHAR(1000);
DECLARE CWtxt VARCHAR(250);
--Convert rows from MY_CWs to comma delimited string
SET CWtxt=(SELECT TRIM( TRAILING ',' FROM ( XMLAGG(CAST(CW AS VARCHAR(10))||',') (VARCHAR(255)) ) ) FROM MY_CWs);
SET Sqltxt=('CREATE TABLE MY_TABLE2 AS
(
SELECT *
FROM MY_TABLE
PIVOT
(SUM(Sales) AS Sales
FOR CW IN ('|| CWtxt ||')
) AS dt
) WITH DATA;') ;
CALL DBC.SYSEXECSQL(Sqltxt);
END;
CALL DYNAMIC_PIVOT();

Unpivot SQL Table with multiple columns

I would like to unpivot a SQL table around multiple columns.
I have tried a normal UNPIVOT statement but that only ppivots around one value.
See this link for example: https://codingsight.com/understanding-pivot-unpivot-and-reverse-pivot-statements.
I have tried to illustrate my data as well as my desired outcome in the picture below.
The top table is a sample of the data in the SQL table. I have used 3 materials but in reality there are 20.
The bottom table is my desired outcome.
The data is on a SQL 2008-r2 server.
Any pointers on how to go about this task?
Consider using cross apply, like so:
select t.date, t.product, x.*
from mytable t
cross apply (values
(container1material, container1amount),
(container2material, container2amount),
(container3material, container3amount)
) x(material, amount)
Use apply for unpivoting:
select t.date, t.product, v.*
from t cross apply
(values (container1amount, container1material),
(container2amount, container2material),
(container3amount, container3material)
) v(containeramount, containermaterial);
unpivot is bespoke syntax (not-standard) and it only does one thing. By contrast, lateral joins are very powerful and unpivoting is only one thing that you can do with them. Apply is worth learning about.

Sorting concatenated strings after grouping in Netezza

I'm using the code on this page to create concatenated list of strings on a group by aggregation basis.
https://dwgeek.com/netezza-group_concat-alternative-working-example.html/
I'm trying to get the concatenated string in sorted order, so that, for example, for DB1 I'd get data1,data2,data5,data9
I tied modifying the original code to selecting from a pre-sorted table but it doesn't seem to make any difference.
select Col1
, count(*) as NUM_OF_ROWS
, trim(trailing ',' from SETNZ..replace(SETNZ..replace (SETNZ..XMLserialize(SETNZ..XMLagg(SETNZ..XMLElement('X',col2))), '<X>','' ),'</X>' ,',' )) AS NZ_CONCAT_STRING
from
(select * from tbl_concat_demo order by 1,2) AS A
group by Col1
order by 1;
Is there a way to sort the strings before they get aggregated?
BTW - I'm aware there is a GROUP_CONCAT UDF function for Netezza, but I won't have access to it.
This is notoriously difficult to accomplish in sql, since sorting is usually done while returning the data, and you want to do it in the ‘input’ set.
Try this:
1)
Create temp table X as select * from tbl_concat_demo Order by col2
Partition by (col1)
In your original code above: select from X instead of tbl_concat_demo
Let me know if it works ?

SQL convert compatibility matrix into mapping table (columns into rows)

I have the following table:
I want to convert the matrix to this format:
where the new mapping table represents the compatibility between a set of options and a set of models that use those options, and the numeric values represent the price of an option for that specific model.
Bare in mind that this is just a small sample from a much bigger table, so the query needs to be more or less dynamic and not hardcoded based on the number of options or models provided in this example.
Does anyone know how I can achieve this?
Best regards,
UnPivot or Gordon's answer would be more performant, but here is an option that will "dynamically" unpivot your data or query without actually using dynamic SQL.
Example
Select A.OptionID
,ModelName = C.Item
,Price = C.Value
From YourTable A
Cross Apply ( values (cast((Select A.* for XML RAW) as xml))) B(XMLData)
Cross Apply (
Select Item = a.value('local-name(.)','varchar(100)')
,Value = a.value('.','varchar(max)') --<< Change to desired datatype
From B.XMLData.nodes('/row') as C1(n)
Cross Apply C1.n.nodes('./#*') as C2(a)
Where a.value('local-name(.)','varchar(100)') not in ('OptionID','OtherFieldsToExclude')
) C
Returns
I like to do this using outer apply:
select v.*
from t outer apply
(values (option_id, 'model1', model1),
(option_id, 'model2', model2),
(option_id, 'model3', model3)
) v(option_id, model_name, price);
You would just add more rows to the values list for each item you want in the final table.
You can also do this using union all, cross join/case, and unpivot.
However, this method uses lateral joins, and these are very powerful for other purposes as well. This is worthwhile syntax to master.
EDIT:
I'm not really sure what "dynamic" means in this context. You table has columns or it doesn't. You can use dynamic SQL to generate the code based on the input parameters or the layout of a single table. It would follow the same logic as above.

MS Access SQL: select rows with the same order as IN clause

I know that this question has been asked several times and I've read all the answer but none of them seem to completely solve my problem.
I'm switching from a mySQL database to a MS Access database. In both of the case I use a php script to connect to the database and perform SQL queries.
I need to find a suitable replacement for a query I used to perform on mySQL.
I want to:
perform a first query and order records alphabetically based on one of the columns
construct a list of IDs which reflects the previous alphabetical order
perform a second query with the IN clause applied with the IDs' list and ordered by this list.
In mySQL I used to perform the last query this way:
SELECT name FROM users WHERE id IN ($name_ids) ORDER BY FIND_IN_SET(id,'$name_ids')
Since FIND_IN_SET is available only in mySQL and CHARINDEX and PATINDEX are not available from my php script, how can I achieve this?
I know that I could write something like:
SELECT name
FROM users
WHERE id IN ($name_ids)
ORDER BY CASE id
WHEN ... THEN 1
WHEN ... THEN 2
WHEN ... THEN 3
WHEN ... THEN 4
END
but you have to consider that:
IDs' list has variable length and elements because it depends on the first query
that list can easily contains thousands of elements
Have you got any hint on this?
Is there a way to programmatically construct the ORDER BY CASE ... WHEN ... statement?
Is there a better approach since my list of IDs can be big?
UPDATE: I perform two separated query because I need to access two different tables.
The databse it's not very simple so I try to make an example:
Suppose I have a table which contains a list of users and a table which contains all the books that every user have in their bookshelf.
Since the dabase was designed in mySQL, for every book record I store the user_id in the books table in order to have a relationship between the user and the book.
Suppose now that I want to obtain a list of all the user that have books with a title starting with letter 'a' and I want to order the user based on the alphabetical oder of the books.
This is what I do:
perform a first query to find all the books which start with letter 'a' and sort the alphabetically
create a list of user_id which should reflect the alphabetical order of the book
perform a query in the users table to find out the users names and sort them with the user_id list to have the required sorting by book
Hope this clarify what I need.
If I understand correctly, you're trying to get a set of information in the same order that you specify the ID values. There is a hack that can convert a list into a table using XML and CROSS APPLY. This can be combined with the ROW_NUMBER function to generate your sort order. See the code below:
CREATE FUNCTION [dbo].[GetNvarcharsFromXmlArray]
(
#Strings xml = N'<ArrayOfStrings/>'
)
RETURNS TABLE
AS
RETURN
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS RowNumber, Strings.String.value('.', 'nvarchar(MAX)') AS String
FROM #Strings.nodes('/ArrayOfStrings/string/text()') AS Strings(String)
)
Which functions with the following structure:
<ArrayOfStrings>
<string>myvalue1</string>
<string>myvalue2</string>
</ArrayOfStrings>
It's also the same format .NET xml serializes string arrays.
If you want to pass a comma separated list, you can simply use:
CREATE FUNCTION [dbo].[GetNvarcharsCSV]
(
#CommaSeparatedStrings nvarchar(MAX) = N''
)
RETURNS TABLE
AS
RETURN
(
DECLARE #Strings xml
SET #Strings = CONVERT(xml, N'<ArrayOfStrings><string>' + REPLACE(#CommaSeperatedStrings, ',', N'</string><string>') + N'</string></ArrayOfStrings>')
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS RowNumber, Strings.String.value('.', 'nvarchar(MAX)') AS String
FROM #Strings.nodes('/ArrayOfStrings/string/text()') AS Strings(String)
)
This makes your query:
SELECT name
FROM users
INNER JOIN dbo.GetNvarcharsCSV(#name_ids) AS IDList ON users.ID = IDList.String
ORDER BY RowNumber
Note that it's a pretty simple rewrite to make the function return a table of integers if that's what you need.
You can see xml Data Type Methods to get a better understanding of what you can do with XML in SQL queries. Also, see ROW_NUMBER (Transact-SQL).
It sounds like you need a JOIN...
This should work, although it may need to be translated to Access syntax (which is apparently subtly different):
SELECT b.name, a.title
FROM book as a
JOIN user as b
ON b.id = a.userId
WHERE SUBSTRING(LOWER(a.title), 1, 1) = 'a'
ORDER by a.title
I don't know why you're switching to Access, although I have heard it's been improving in recent years. I think I'd prefer almost any other RDBMS, though. And your schema could probably stand some tweaking, from the sound of things.
You would have to use a user-defined function that maintains the order, and then order by that column. For example:
CREATE FUNCTION dbo.SplitList
(
#List VARCHAR(8000)
)
RETURNS TABLE
AS
RETURN
(
SELECT DISTINCT
[Rank],
[Value] = CONVERT(INT, LTRIM(RTRIM(SUBSTRING(#List, [Rank],
CHARINDEX(',', #List + ',', [Rank]) - [Rank]))))
FROM
(
SELECT TOP (8000) [Rank] = ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS n
WHERE [Rank] <= LEN(#List)
AND SUBSTRING(',' + #List, [Rank], 1) = ','
);
GO
Now your query can look something like this:
SELECT u.name
FROM dbo.users AS u
INNER JOIN dbo.SplitList($name_ids) AS s
ON u.id = s.Value
ORDER BY s.[Rank];
You may have to surround $name_ids with single quotes (dbo.SplitList('$name_ids')) depending on how the SQL statement is constructed. You may want to consider using a stored procedure instead of building this query in PHP.
You might also consider skipping MS-Access as a hopping point altogether. Why not just have PHP communicate directly with SQL Server?