SQL - Concatenating values when the number of items is not known - sql

I am currently refreshing my knowledge in SQL and encountered some difficulties with the following query.
The requirements were:
For each maker, list in the alphabetical order with "/" as delimiter all the types of products he produces.
Deduce: maker, product types' list
The following solution actually works but I don't exactly understand how..
;with
t1 as
(select maker, type, DENSE_RANK() over(partition by maker order by type) rn
from product
),
tr(maker, type,lev) as
(select distinct t1.maker, cast(t1.type as nvarchar) , 2 from t1 where t1.rn = 1
union all
select t1.maker, cast(tr.type +'/'+t1.type as nvarchar), lev + 1
from t1 join tr on (t1.maker = tr.maker and t1.rn = tr.lev
)
)
select maker, max(type) names from tr group by maker
These output:
1 | A | Laptop/PC/Printer
2 | B | Laptop/PC
3 | C | Laptop
4 | D | Printer
5 | E | PC/Printer
*second column is the maker and third is the dynamically concatenated list of types.
Now, I'm a bit confused on how exactly does the lev grows dynamically.. Is there some kind of loop I'm missing here?
Why does it start with 2?
Why it doesn't work without the "cast"?
I would be very grateful if someone could explain the logic behind this query.
Thanks a lot!

What you're looking at is a recursive CTE,a CTE that calls itself. It's what you think is the "looping" effect. Recursion put a kink in my brain when I first looked at it. It helps to look at some examples and try creating a few simple ones of your own. I'm still not the best at them, but I'm getting better. I added some comments to your code. Hope this helps.
;WITH t1
AS (
SELECT maker,
type,
--Partition says each maker is a group so restart at 1
--Order by type is alphabetic
--DENSE_RANK() means if there are two the of the same item, they get the same number
DENSE_RANK() OVER (PARTITION BY maker ORDER BY type) rn
FROM product
),
--This is a recursive CTE meaning a CTE that calls itself(what is doing the "looping"
tr (maker,type,lev)
AS (
--Grab only the distinct items from your ranked table
SELECT DISTINCT t1.maker,
cast(t1.type AS NVARCHAR),
2 --This is the start of your recursive loop
FROM t1
WHERE t1.rn = 1
UNION ALL
--Recursively loop through you data, adding each string at the end with the '/'
SELECT t1.maker,
cast(tr.type + '/' + t1.type AS NVARCHAR),
--Plus one to grab the next value
lev + 1
FROM t1
INNER JOIN tr ON (
--Only match the same makers
t1.maker = tr.maker
--Match to the next value
AND t1.rn = tr.lev
)
)
--I think you know what this does
SELECT maker,
max(type) names
FROM tr
GROUP BY maker

Related

convert data from multiple columns into single row sorting descending

I am trying to query the original source which contain totals from a category (in this case Vehicles) into the second table.
Motorcycle
Bicycle
Car
1
3
2
Desired Output:
Vehicle
Quantity
Bicycle
3
Car
2
Motorcycle
1
Additionally, I need that the Quantity is sorted in descending order like showing above.
So far I have tried to do an Unpivot, but there is a syntax error in the Unpivot function. Is there another way to reach out the same results?
My code so far:
SELECT Vehicle_Name
FROM
(
SELECT [Motorcycle], [Bycycle], [Car] from Data
) as Source
UNPIVOT
(
Vehicle FOR Vehicle_Name IN ([Motorcycle], [Bycycle], [Car])
) as Unpvt
Edit: Added sort requirement.
You can use CROSS APPLY here too
select vehicle, amnt
from test
cross apply(
VALUES('motorcycle', motorcycle)
,('bicycle', bicycle)
,('car', car)) x (vehicle, amnt)
order by amnt desc
Fiddle here
Try this
with data1 as
(
Select * from data)
Select * From
(
Select 'motorcycle' as "Vehicle", motorcycle as quantity from data1
union all
Select 'bicycle' , bicycle from data1
union all
Select 'car', car from data1
) order by quantity desc;
Since we don't know what DBMS, here's a way that'd work in the one I use the most.
SELECT *
FROM (SELECT map_from_entries(
ARRAY[('Motorcycle', Motorcycle),
('Bicycle', Bicycle),
('Car', Car)])
FROM Source) AS t1(type_quant)
CROSS JOIN UNNEST(type_quant) AS t2(Vehicle, Quantity)
ORDER BY Quantity DESC
-Trino

MAX of SUM and use of TOP in Views

I have the following tables in SQL Server:
COMMANDLINES: ID_LINE - ID_COMMAND - ID_ARTICLE - QUANTITY
COMMAND: ID_COMMAND - ID_CLIENT - PRICE - PRINTED
CLIENT: ID_CLIENT - FULL_NAME - SSN - PH_NUM - MOBILE - USERNAME - PASSWORD
ARTICLE: ID_ARTICLE - DES - NAME - PRICE - TYPE - CURRENT_QTT - MINIMUM_QTT
ID_COMMAND from COMMANDLINES references COMMAND.ID_COMMAND
ID_CLIENT from COMMAND references CLIENT.ID_CLIENT
ID_ARTICLE from COMMANDLINES references ARTICLE.ID_ARTICLE
I need to create a view where I need to show all COMMANDLINES that have the best client (the one with the highest total of PRICE) and then I need to order them by ID_COMMAND in a descending order AND by ID_LINE in ascending order.
Sample data:
COMMANDLINE table:
COMMAND table:
Only these 2 are needed to resolve the problem. I added the other just for more information.
Sample output:
To be honest, I'm not sure if both outputs are supposed to be "output" at the same time or that I need 2 VIEWS for each output.
WHAT HAVE I DONE SO FAR:
I looked through what I could find on StackOverflow about MAX of SUM, but unfortunately, it has not helped me much in this case. I always seem to be doing something wrong.
I also found out that in order to use ORDER BY in VIEWS you need to, in this case, use TOP, but I've no idea how to apply it correctly when I need to select all of the COMMANDLINES. In one of my previous things, I used the following SELECT TOP:
create view PRODUCTS_BY_TYPE
as
select top (select count(*) from ARTICLE
where CURRENT_QTT > MINIMUM_QTT)*
from
ARTICLE
order by
TYPE
This allowed me to show all PRODUCT data where the CURRENT_QTT was more than the minimum ordering them by type, but I can't figure out for the life of me, how to apply this to my current situation.
I could start with something like this:
create view THE_BEST
as
select COMMANDLINE.*
from COMMANDLINE
But then I don't know how to apply the TOP.
I figured that first, I need to see who the best client is, by SUM-ing all of the PRICE under his ID and then doing a MAX on all of the SUM of all clients.
So far, the best I could come up with is this:
create view THE_BEST
as
select top (select count(*)
from (select max(max_price)
from (select sum(PRICE) as max_price
from COMMAND) COMMAND) COMMAND) COMMANDLINE.*
from COMMANDLINE
inner join COMMAND on COMMANDLINE.ID_COMMAND = COMMAND.ID_COMMAND
order by COMMAND.ID_COMMAND desc, COMMANDLINE.ID_LINE asc
Unfortunately, in the select count(*) the COMMAND is underlined in red (a.k.a. the 3rd COMMAND word) and it says that there is "no column specified for column 1 of COMMAND".
EDIT:
I've come up with something closer to what I want:
create view THE_BEST
as
select top (select count(*)
from (select max(total_price) as MaxPrice
from (select sum(PRICE) as total_price
from COMMAND) COMMAND) COMMAND)*
from COMMANDLINE
order by ID_LINE asc
Still missing the ordered by ID_COMMAND and I only get 1 result in the output when it should be 2.
here is some code that hopefully will show you how you can use the top-clause and also a different approche to show only the "top" :-)
/* Creating Tables*/
CREATE TABLE ARTICLE (ID_ARTICLE int,DES varchar(10),NAME varchar(10),PRICE float,TYPE int,CURRENT_QTT int,MINIMUM_QTT int)
CREATE TABLE COMMANDLINES (ID_LINE int,ID_COMMAND int,ID_ARTICLE int,QUANTITY int)
CREATE TABLE COMMAND (ID_COMMAND int, ID_CLIENT varchar(20), PRICE float, PRINTED int)
CREATE TABLE CLIENT (ID_CLIENT varchar(20), FULL_NAME varchar(50), SSN varchar(50), PH_NUM varchar(50), MOBILE varchar(50), USERNAME varchar(50), PASSWORD varchar(50))
INSERT INTO COMMANDLINES VALUES (1,1,10,20),(2,1,12,3),(3,1,2,21),(1,2,30,2),(2,2,21,5),(1,3,32,20),(2,3,21,2)
INSERT INTO COMMAND VALUES (1,'1695152D',1200,0),(2,'1695152D',500,0),(3,'2658492D',200,0)
INSERT INTO ARTICLE VALUES(1, 'A','AA',1300,0,10,5),(2,'B','BB',450,0,10,5),(30,'C','CC',1000,0,5,5),(21,'D','DD',1500,0,5,5),(32,'E','EE',1600,1,4,5),(3,'F','FF',210,2,15,5)
INSERT INTO CLIENT VALUES ('1695152D', 'DoombringerBG', 'A','123','321','asdf','asf'),('2658492D', 'tgr', 'A','123','321','asdf','asf')
GO
/* Your View-Problem*/
CREATE VIEW PRODUCTS_BY_TYPE AS
SELECT TOP 100 PERCENT *
FROM ARTICLE
WHERE CURRENT_QTT > MINIMUM_QTT -- You really don't want >= ??
ORDER BY [Type]
-- why do you need your view with an ordered output? cant your query order the data?
GO
OUTPUT:
ID_ARTICLE | DES | NAME | PRICE | TYPE | CURRENT_QTT | MINIMUM_QTT
-------------+-------+-------+-------+------+--------------+-------------
1 | A | AA | 1300 | 0 | 10 | 5
2 | B | BB | 450 | 0 | 10 | 5
3 | F | FF | 210 | 2 | 15 | 5
I hope this is what you were looking for :-)
-- your top customers
SELECT cli.FULL_NAME, SUM(c.PRICE)
FROM COMMANDLINES as cl
INNER JOIN COMMAND as c
on cl.ID_COMMAND = c.ID_COMMAND
INNER JOIN CLIENT as cli
on cli.ID_CLIENT = c.ID_CLIENT
GROUP BY cli.FULL_NAME
ORDER BY SUM(c.PRICE) DESC -- highest value first
SELECT *
FROM (
-- your top customers with a rank
SELECT cli.FULL_NAME, SUM(c.PRICE) as Price, ROW_NUMBER() OVER (ORDER BY SUM(c.PRICE) DESC) AS RowN
FROM COMMANDLINES as cl
INNER JOIN COMMAND as c
on cl.ID_COMMAND = c.ID_COMMAND
INNER JOIN CLIENT as cli
on cli.ID_CLIENT = c.ID_CLIENT
GROUP BY cli.FULL_NAME
) as a
-- only the best :-)
where RowN = 1
--considerations: what if two customers have the same value?
Output:
FULL_NAME |Price | RowN
----------------+---------+-------
DoombringerBG | 4600 | 1
Regards
tgr
===== EDITED =====
The syntax-corrention to your THE_BEST-View:
create view THE_BEST AS
SELECT TOP (
SELECT count(*) as cnt
FROM (
SELECT max(max_price) as max_price
FROM (
SELECT sum(PRICE) AS max_price
FROM COMMAND
) COMMAND
) COMMAND
)
cl.*
FROM COMMANDLINES as cl
INNER JOIN COMMAND as c
ON cl.ID_COMMAND = c.ID_COMMAND
ORDER BY c.ID_COMMAND DESC
,cl.ID_LINE ASC
Without the OVER-Clause:
SELECT TOP 1 *
FROM (
-- your top customers with a rank
SELECT cli.FULL_NAME, SUM(c.PRICE) as Price
FROM COMMANDLINES as cl
INNER JOIN COMMAND as c
on cl.ID_COMMAND = c.ID_COMMAND
INNER JOIN CLIENT as cli
on cli.ID_CLIENT = c.ID_CLIENT
GROUP BY cli.FULL_NAME
) as a
-- only the best :-)
ORDER BY Price DESC
Your PRODUCTS_BY_TYPE without PERCENT:
CREATE VIEW PRODUCTS_BY_TYPE AS
SELECT TOP (select
SUM(p.rows)
from sys.partitions as p
inner join sys.all_objects as ao
on p.object_id = ao.object_id
where ao.name = 'ARTICLE'
and ao.type = 'U')
*
FROM ARTICLE
WHERE CURRENT_QTT > MINIMUM_QTT -- You really don't want >= ??
ORDER BY [Type]
go
but to be honest - i would never use such a query in production... i only posted this because you need it for studing purposes...
It is quite likely that there is some misunderstanding between you and your teacher. You can technically have ORDER BY clause in a view definition, but it never guarantees any order of the rows in the query that uses the view, such as SELECT ... FROM your_view. Without ORDER BY in the final SELECT the order of the result set is not defined. The order of rows returned to the client by the server is determined only by the final outermost ORDER BY of the query, not by the ORDER BY in the view definition.
The purpose of having TOP in the view definition is to limit the number of returned rows somehow. For example, TOP (1). In this case ORDER BY specifies which row(s) to return.
Having TOP 100 PERCENT in a view does nothing. It doesn't reduce the number of returned rows and it doesn't guarantee any specific order of returned rows.
Having said all that, in your case you need to find one best client, so it makes sense to use TOP (1) in a sub-query.
This query would return the ID of the best client:
SELECT
TOP (1)
-- WITH TIES
ID_CLIENT
FROM COMMAND
GROUP BY ID_CLIENT
ORDER BY SUM(PRICE) DESC
If there can be several clients with the same maximum total price and you want to return data related to all of them, not just one random client, then use TOP WITH TIES.
Finally, you need to return lines that correspond to the chosen client(s):
create view THE_BEST
as
SELECT
COMMANDLINE.ID_LINE
,COMMANDLINE.ID_COMMAND
,COMMANDLINE.ID_ARTICLE
,COMMANDLINE.QUANTITY
FROM
COMMANDLINE
INNER JOIN COMMAND ON COMMAND.ID_COMMAND = COMMANDLINE.ID_COMMAND
WHERE
COMMAND.ID_CLIENT IN
(
SELECT
TOP (1)
-- WITH TIES
ID_CLIENT
FROM COMMAND
GROUP BY ID_CLIENT
ORDER BY SUM(PRICE) DESC
)
;
This is how the view can be used:
SELECT
ID_LINE
,ID_COMMAND
,ID_ARTICLE
,QUANTITY
FROM THE_BEST
ORDER BY ID_COMMAND DESC, ID_LINE ASC;
Note, that ORDER BY ID_COMMAND DESC, ID_LINE ASC has to be in the actual query, not in the view definition.

Index number for records within a pipe-delimited field inside a csv

I have a csv that I'm bringing into a SQL table. The csv has a field within it for CrimeType. That field is pipe delimited. So, I'm using cross apply to break up the pipe, like this:
SELECT CrimeRecords.CaseNum, CrimeRecords.Offense, PrimaryCrime.PrimaryCrime
FROM (SELECT CaseNum ,x.i.value('.','varchar(20)') AS Offense
FROM (SELECT CaseNum, CONVERT(XML,'<i>'+REPLACE(CrimeType, '|', '</i><i>') + '</i>') AS d
FROM CrimeView.dbo.tblCrimeData)x1 CROSS APPLY d.nodes('i') AS x(i)) AS CrimeRecords
Can someone help me add a step to create a field for a sequence number? Basically I just want to return the order of the items in the pipe.
For rows like:
1, Burglary|Assault
2, Burglary
3, Assault|Assault-Weapon|Theft
My result table would look like this:
CaseNum CrimeType SeqNum
1 Burglary 1
1 Assault 2
2 Burglary 1
3 Assault 1
3 Assault-Weapon 2
3 Theft 3
Edit to show that the Sequence Number resets for each CaseNum.
Edit tags to clarify that this is Microsoft SQL, not MySQL.
Try including the ROW_NUMBER() function in your SELECT statement (http://technet.microsoft.com/en-us/library/ms186734.aspx).
i.e.
SELECT ROW_NUMBER() OVER (PARTITION BY CrimeRecords.CaseNum ORDER BY CrimeRecords.CaseNum) As Idx, CrimeRecords.CaseNum, CrimeRecords.Offense, PrimaryCrime.PrimaryCrime
FROM (SELECT CaseNum ,x.i.value('.','varchar(20)') AS Offense
FROM (SELECT CaseNum, CONVERT(XML,'<i>'+REPLACE(CrimeType, '|', '</i><i>') + '</i>') AS d
FROM CrimeView.dbo.tblCrimeData)x1 CROSS APPLY d.nodes('i') AS x(i)) AS CrimeRecords
Edit: Included Partition By to reset the sequence for each case.
if you have a simple table CrimeRecords like CaseNum | CrimeType
you have to do something like this
SELECT CaseNum,CrimeType, #row:=#row+1 SeqNum
FROM CrimeRecords a JOIN (SELECT #row := 0) b;
ok.. I cant see crearly in your query and i cant try it in a db, buy try to use the query i shared.
It is just an example to show how you can add numbers in order 1,2,3...x from some elements in the rows.. so try to mix it code in your query and reestart the #row each time the group change..
so you ll get it

Issue selecting Category and SubCategory of an Item while creating one delimited string per row of a table

Some Context: The DB is Oracle. I am trying to create one delimited string per row of a table. The delimited string needs to contain two of the item's categories (if available, there will always be one category at a minimum). There are 3 tables, ITEM, ITEM_CAT and ITEM_ITEM_CAT. The ITEM table holds all the items. The ITEM_CAT table holds all the possible item categories. The ITEM_ITEM_CAT holds all the mappings between the item IDs and the category IDs, a key value table essentially.
I have created the below SQL which is able to get the delimited string for a specific item, but I need a query which can run against the entire table.
SELECT 'ITEM'||'%#'|| outerTable.ITEM_ID ||'%#'||
(SELECT midTable.item_cat_nam
FROM
(SELECT innerTable.item_cat_nam AS item_cat_nam, innerTable.item_id AS item_id, ROWNUM AS rn
FROM
(SELECT ic.ITEM_CAT_NAM AS item_cat_nam, i.ITEM_ID AS item_id
FROM ITEM_CAT ic, ITEM_ITEM_CAT iic, ITEM i
WHERE i.ITEM_ID = iic.ITEM_ID
AND iic.ITEM_CAT_CD = ic.ITEM_CAT_CD
AND 287484 = i.item_id
) innerTable
) midTable
WHERE rn = 1
) ||'%#'||
(SELECT midTable.item_cat_nam
FROM
(SELECT innerTable.item_cat_nam AS item_cat_nam, innerTable.item_id AS item_id, ROWNUM AS rn
FROM
(SELECT ic.ITEM_CAT_NAM AS item_cat_nam, i.ITEM_ID AS item_id
FROM ITEM_CAT ic, ITEM_ITEM_CAT iic, ITEM i
WHERE i.ITEM_ID = iic.ITEM_ID
AND iic.ITEM_CAT_CD = ic.ITEM_CAT_CD
AND 287484 = i.item_id
) innerTable
) midTable
WHERE rn = 2
)
FROM OFR outerTable
WHERE outerTable.ITEM_ID = 287484;
I need to be able to pass the outer table's ITEM_ID down into the last inner join. I could do this when I only need the category (via the below SQL statement, only one inner join needed), but with the introduction of multiple categories; I need rownum (to get multiple categories) which then needs more inner joins and I can't seem to pass the ITEM_ID down more than one inner join, and here lies the problem...
SELECT 'ITEM'||'%#'|| outerTable.OFR_ID ||'%#'||
(SELECT ic.ITEM_CAT_NAM
FROM ITEM_CAT ic, ITEM_ITEM_CAT iic, ITEM i
WHERE i.ITEM_ID = iic.ITEM_ID
AND iic.ITEM_CAT_CD = ic.ITEM_CAT_CD
AND outerTable.OFR_ID = i.item_id
AND rownum = 1
) innerTable
FROM OFR outerTable;
Can anyone help with this?
Thank you in advance for any assitance.
No worries. You need something like this...
SELECT 'ITEM' || '%#' || Item_ID || '%#' || CatName1 || '%#' || CatName2
FROM outerTable
INNER JOIN (
SELECT
Item_ID,
MAX(CASE WHEN rn = 1 THEN Item_Cat_Nam ELSE NULL END) CatName1,
MAX(CASE WHEN rn = 2 THEN Item_Cat_Nam ELSE NULL END) CatName2
FROM (
SELECT
Item_ID,
Item_Cat.Item_Cat_Nam,
ROW_NUMBER() OVER (PARTITION BY Item_ID ORDER BY Item_ID) rn
FROM Item
INNER JOIN Item_Item_Cat USING (Item_ID)
INNER JOIN Item_Cat USING (Item_Cat_Cd)
) GROUP BY Item_ID
) USING (Item_ID)
The innermost query uses the ROW_NUMBER function to assign 1, 2, 3, etc. to every category found for each item. The PARTITION BY restarts the numbering at 1 for each item. The ORDER BY is required so I used Item_ID because hey, why not? If you have a preferred column to order by just use that - it will be used to assign the row numbers. The inner query will output something like this:
Item_ID Item_Cat_Nam rn
------- ------------ --
1 Category aa 1
1 Category xy 2
1 Category ef 3
2 Category xy 1
2 Category ax 2
3 Category ef 1
The query surrounding the innermost query uses MAX to flatten the first two rn values for each Item_ID into a single row. The Item_Cat_Nam for rn=1 goes to the CatName1 column and the Item_Cat_Nam for rn=2 goes to the CatName2 column. When it's fed the results shown above you'll end up with this:
Item_ID CatName1 CatName2
------- ----------- -----------
1 Category aa Category xy (note Category ef is rn=3 so it's ignored)
2 Category xy Category ax
3 Category ef (note only one row for Item_ID 3)
Then the very outer query just concatenates everything.
One other thing: I used the "JOIN ... USING" syntax because in this case it lets you eliminate all of the aliases (innerTable, i, ic, iic, midTable, etc.). That's purely because I'm more comfortable with it so it helped me figure this out a lot quicker. You should feel free to use your own join style - after all you'll be the one stuck maintaining it :)

Ordering a SQL query based on the value in a column determining the value of another column in the next row

My table looks like this:
Value Previous Next
37 NULL 42
42 37 3
3 42 79
79 3 NULL
Except, that the table is all out of order. (There are no duplicates, so that is not an issue.) I was wondering if there was any way to make a query that would order the output, basically saying "Next row 'value' = this row 'next'" as it's shown above ?
I have no control over the database and how this data is stored. I am just trying to retrieve it and organize it. SQL Server I believe 2008.
I realize that this wouldn't be difficult to reorganize afterwards, but I was just curious if I could write a query that just did that out of the box so I wouldn't have to worry about it.
This should do what you need:
WITH CTE AS (
SELECT YourTable.*, 0 Depth
FROM YourTable
WHERE Previous IS NULL
UNION ALL
SELECT YourTable.*, Depth + 1
FROM YourTable JOIN CTE
ON YourTable.Value = CTE.Next
)
SELECT * FROM CTE
ORDER BY Depth;
[SQL Fiddle] (Referential integrity and indexes omitted for brevity.)
We use a recursive common table expression (CTE) to travel from the head of the list (WHERE Previous IS NULL) to the trailing nodes (ON YourTable.Value = CTE.Next) and at the same time memorize the depth of the recursion that was needed to reach the current node (in Depth).
In the end, we simply sort by the depth of recursion that was needed to reach each of the nodes (ORDER BY Depth).
Use a recursive query, with the one i list here you can have multiple paths along your linked list:
with cte (Value, Previous, Next, Level)
as
(
select Value, Previous, Next, 0 as Level
from data
where Previous is null
union all
select d.Value, d.Previous, d.Next, Level + 1
from data d
inner join cte c on d.Previous = c.Value
)
select * from cte
fiddle here
If you are using Oracle, try Starts with- connect by
select ... start with initial-condition connect by
nocycle recursive-condition;
EDIT: For SQL-Server, use WITH syntax as below:
WITH rec(value, previous, next) AS
(SELECT value, previous, next
FROM table1
WHERE previous is null
UNION ALL
SELECT nextRec.value, nextRec.previous, nextRec.next
FROM table1 as nextRec, rec
WHERE rec.next = nextRec.value)
SELECT value, previous, next FROM rec;
One way to do this is with a join:
select t.*
from t left outer join
t tnext
on t.next = tnext.val
order by tnext.value
However, won't this do?
select t.*
from t
order by t.next
Something like this should work:
With Parent As (
Select
Value,
Previous,
Next
From
table
Where
Previous Is Null
Union All
Select
t.Value,
t.Previous,
t.Next
From
table t
Inner Join
Parent
On Parent.Next = t.Value
)
Select
*
From
Parent
Example