Iterate through SQL select query to get more related data - sql

I would like to generate an excel report based on a SQL stored procedure.
Therefore i have to 'fill' few columns on this report. Some are related to (let's say) the Order table, others are related to the Product table and so on.
On the excel report i need to see the columns: Order no, Order value, Product 1 name, Product 2 name, Product 1 value, Product 2 value (there are max 2 products on each order).
The Product table is linked to the Order table.
I know this kind of listing it's a bit confusing, but that is what i wanna get.
Atm i have written the sequence:
SELECT
order.OrderNo,
order.OrderValue,
product.Name,
product.Value
From ORDER AS order
lEFT outer join PRODUCT as product on order.OrderId = product.OrderId
The query works but i only get data for the first product in each order. Is there a way to select data from all products specific to each order?

You can use row_number() and conditional aggregation:
SELECT op.OrderNo, op.OrderValue,
MAX(CASE WHEN seqnum = 1 THEN Name END) as Name_1,
MAX(CASE WHEN seqnum = 1 THEN Value END) as Value_1,
MAX(CASE WHEN seqnum = 2 THEN Name END) as Name_2,
MAX(CASE WHEN seqnum = 2 THEN Value END) as Value_2
FROM (SELECT o.OrderNo, o.OrderValue, p.Name, p.Value,
ROW_NUMBER() OVER (PARTITION BY o.OrderNo ORDER BY p.Value DESC) as seqnum
FROM ORDER o LEFT JOIN
PRODUCT p
ON o.OrderId = p.OrderId
) op
GROUP BY op.OrderNo, op.OrderValue;

If your DBMS supports it, you can use a PIVOT for this, for example:
declare #order table(
OrderId int,
OrderNo varchar(10),
OrderValue decimal(10,2)
)
declare #product table(
OrderId int,
[Name] varchar(20),
[Value] decimal(10,2)
)
insert into #order values
(1,'ORD001',436.45),
(2,'ORD002',964.33),
(3,'ORD003',1265.98)
insert into #product values
(1,'Widget',195.45),
(1,'Doohickey',241.00),
(2,'Widget',195.45),
(2,'Thingy',397.99),
(2,'Doofer',370.89),
(3,'Widget',195.45),
(3,'Thingy',397.99),
(3,'Foobar',415.78),
(3,'Whatchamacallit',256.76)
select
OrderNo,
OrderValue,
[Widget],
[Doohickey],
[Thingy],
[Doofer],
[Foobar],
[Whatchamacallit]
from
(
select
o.OrderNo,
o.OrderValue,
p.[Name],
p.[Value]
from #order o
left join #product p on o.OrderId = p.OrderId
) SourceTable
pivot
(
sum([Value])
for [Name] in ([Widget], [Doohickey], [Thingy], [Doofer], [Foobar], [Whatchamacallit])
) as PivotTable
gives you this result:
However, this does require you to know all the distinct values of Product.Name which could appear and to include them in your query. The upside is you can perform further aggregation on the resultant values, per Product.

Related

Oracle Row Into Columns

I am having 2 tables, Customers and Customer Contacts table.
Ex: Customers
Column:
Id
Customer Name
Contacts Table
Column
id
customer_id
contact_no
I need to fetch a record by below format.'
Customer Name, contact_no_1, contact_no_2 .... etc.
I'm using oracle 11g.
You can make use of either case expression or PIVOT to achieve the same.
Please refer to below links
Link1
Link2
Link3
You can use a ranking function (rank, dense_rank, row_number) to assign a nominal sequential number to each contact number for each customer:
select cus.id as customer_id,
cus.customer_name,
con.contact_no,
row_number() over (partition by cus.id order by con.id) as rn
from customers cus
left join contacts con on con.customer_id = cus.id
and then either use that in a pivot:
select *
from (
select cus.id as customer_id,
cus.customer_name,
con.contact_no,
row_number() over (partition by cus.id order by con.id) as rn
from customers cus
left join contacts con on con.customer_id = cus.id
)
pivot (max(contact_no) for (rn) in (1 as contact_no_1, 2 as contact_no_2,
3 as contact_no_3, 4 as contact_no_4, 5 as contact_no_5))
or with a manual pivot using aggregation:
select customer_id,
customer_name,
max(case when rn = 1 then contact_no end) as contact_no_1,
max(case when rn = 2 then contact_no end) as contact_no_2,
max(case when rn = 3 then contact_no end) as contact_no_3,
max(case when rn = 4 then contact_no end) as contact_no_4,
max(case when rn = 5 then contact_no end) as contact_no_5
from (
select cus.id as customer_id,
cus.customer_name,
con.contact_no,
row_number() over (partition by cus.id order by con.id) as rn
from customers cus
left join contacts con on con.customer_id = cus.id
)
group by customer_id, customer_name
db<>fiddle with some made-up data.
I've shown handling up to 5 contact numbers; you need to add as many in() values or case expressions as you need to handle whatever value of N you're comfortable limiting the output to. (If you decide you can't set a limit then you'll have to use a dynamic pivot, which is more complicated.)
I've included customer_id in case you can have two customer that happen to have the same name. You don't have to include that in the final projection; in the pivot version that means you will need to list all the columns you do want to include.

SQL show orders where 2 values are distinct and match the first

I'm looking for a way to let me select all orders that have multiple distinct names within the same order-number, it looks like this:
order - name
111-Paul
112-Paula
113-John
113-John
113-Jessica
114-Eric
114-Eric
114-John
115-Zack
115-Zack
115-Zack
etc.
so that i would get all the orders that have 2 or more distinct names in it:
113-John
113-Jessica
114-Eric
114-John
with which I could do further queries but I'm stuck. Can anyone give me some hints on how to tackle this problem please? I've tried it with count(*) which looked like this:
select order, name, count(name) from dbo.orders
group by order, name
having count(name) > 1
which gave me all the orders which had more than 1 name in it but I don't know how to let it only show orders with the distinct names.
Here's one approach using exists:
select distinct [order], name
from orders o
where exists (
select 1
from orders o2
where o.[order] = o2.[order] and o.name != o2.name)
Fiddle Demo
I would use windows functions for this
For example:
select distinct order
from
(select
order,
row_number() over(partition by order, name order by order asc) as rn
) as t1
where rn > 1
you can do the same with count
count(*) over(partition by order,name order by order asc) as cnt
Here's a straight forward implementation in Sql Server:
select distinct *
from table1
where [order] in (
select [order]
from (select distinct * from table1) iq
group by [order]
having count(*) > 1)
It's essentially breaking down the problem into:
Finding the orders that have more than one distinct value.
Finding the pairs of distinct order - name that belong to the list previously calculated.
When you use HAVING COUNT(name) > 1, it is counting all of the rows in those groups, including duplicate rows (rows 113-John and 113-John are 2 rows for order 113). I would query distinct rows from your table, and then select from that:
SELECT [order], [name] FROM (
SELECT DISTINCT [order], [name] FROM dbo.orders
) A
GROUP BY [order], [name]
HAVING COUNT([name]) > 1
As a note, if a [name] is null, then it will not be counted with COUNT(name). If you want nulls to be counted, use COUNT(*) instead.
You can use count(distinct name) to get the number of unique names for each order:
select [order], count(distinct name)
from orders
group by [order]
To just get the order for those you can use having:
select [order]
from orders
group by [order]
having count(distinct name) > 1
To get the details for those orders you can put that in the where clause to just return the rows with order in that list:
select *
from orders
where [order] in (
select [order]
from orders
group by [order]
having count(distinct name) > 1
)
sqlfiddle
I would use RANK (or DENSE_RANK) for this as shown below.
SELECT [Order]
FROM (SELECT
[Order],
RANK() OVER(PARTITION BY [Order] ORDER BY Name) AS NameRank
FROM [StackOverflow].[dbo].[OrderAndName]) ranked
WHERE ranked.NameRank > 1
GROUP BY [Order]
The sub-query ranks (gives a seeding) to the names in an order according to their value. Names with the same value would have the same rank i.e. when an order has one name multiple times (like 115) the rank of all names would be 1.
The partition is important here as otherwise you would get the rank for all names for all orders which wouldn't give you the result you'd like.
It is then just a case of pulling out the orders that have a RANK greater than 1 and grouping (could use distinct if that's a preference).
You can then join to this table to get get the orders and names as follows;
SELECT oan.[Order], [Name]
FROM [StackOverflow].[dbo].[OrderAndName] oan
INNER JOIN (SELECT [Order]
FROM (SELECT [Order],
RANK() OVER(PARTITION BY [Order] ORDER BY Name) AS NameRank
FROM [StackOverflow].[dbo].[OrderAndName]) ranked
WHERE ranked.NameRank > 1
GROUP BY [Order]) twoOrMore ON oan.[Order] = twoOrMore.[Order]

SQL Select Group By Min() - but select other

I want to select the ID of the Table Products with the lowest Price Grouped By Product.
ID Product Price
1 123 10
2 123 11
3 234 20
4 234 21
Which by logic would look like this:
SELECT
ID,
Min(Price)
FROM
Products
GROUP BY
Product
But I don't want to select the Price itself, just the ID.
Resulting in
1
3
EDIT: The DBMSes used are Firebird and Filemaker
You didn't specify your DBMS, so this is ANSI standard SQL:
select id
from (
select id,
row_number() over (partition by product order by price) as rn
from orders
) t
where rn = 1
order by id;
If your DBMS doesn't support window functions, you can do that with joining against a derived table:
select o.id
from orders o
join (
select product,
min(price) as min_price
from orders
group by product
) t on t.product = o.product and t.min_price = o.price;
Note that this will return a slightly different result then the first solution: if the minimum price for a product occurs more then once, all those IDs will be returned. The first solution will only return one of them. If you don't want that, you need to group again in the outer query:
select min(o.id)
from orders o
join (
select product,
min(price) as min_price
from orders
group by product
) t on t.product = o.product and t.min_price = o.price
group by o.product;
SELECT ID
FROM Products as A
where price = ( select Min(Price)
from Products as B
where B.Product = A.Product )
GROUP BY id
This will show the ID, which in this case is 3.

VLookup in SQL? - Joining to only pick out the top row

I am trying to get just the first row from a JOIN in SQL. Something similiar to Vlookup in Excel.
I have the following tables
CREATE TABLE customer_lookup (
customer_product varchar(50),
supplier_product varchar(50),
customer_code varchar(10)
)
CREATE TABLE supplier (
part_number varchar(50)
)
INSERT INTO customer_lookup (
customer_product,
supplier_product,
customer_code ) VALUES ('CONTAINER', 'BOX', 'CUST01')
INSERT INTO customer_lookup (
customer_product,
supplier_product,
customer_code ) VALUES ('CONTAINER', 'BOX', 'CUST02')
INSERT INTO customer_lookup (
customer_product,
supplier_product,
customer_code ) VALUES ('FABRIC', 'MATERIAL', 'CUST01')
INSERT INTO supplier ( part_number ) VALUES ('FABRIC')
INSERT INTO supplier ( part_number ) VALUES ('CONTAINER')
INSERT INTO supplier ( part_number ) VALUES ('PAINT')
and my query is
SELECT
s.part_number, c.supplier_product, c.customer_code
FROM
supplier s
LEFT JOIN
(
SELECT * FROM customer_lookup t
) c
ON s.part_number = c.customer_product
http://sqlfiddle.com/#!6/716b5/1
The result I am trying to get is
part_number supplier_product customer_code
FABRIC MATERIAL CUST01
CONTAINER BOX CUST01
PAINT (null) (null)
but the above SQL query produces
part_number supplier_product customer_code
FABRIC MATERIAL CUST01
CONTAINER BOX CUST01
CONTAINER BOX CUST02
PAINT (null) (null)
I don't care that the row with CONTAINER is missing customer_code CUST02. I just need to top one
I have tried
SELECT
s.part_number, c.supplier_product, c.customer_code
FROM
supplier s
LEFT JOIN
(
SELECT TOP 1 * FROM customer_lookup t
) c
ON s.part_number = c.customer_product
but this just nulls out both FABRIC and PAINT rows
Any help would be appreciated
You can use GROUP BY and MAX to achieve what you're looking for
SELECT
s.part_number, c.supplier_product, MAX(c.customer_code)
FROM
supplier s
LEFT JOIN
(
SELECT * FROM customer_lookup t
) c
ON s.part_number = c.customer_product
GROUP BY s.part_number, c.supplier_product
For every part_number and supplier_product unique identifying combination, you want the highest customer_code value.
If you don't care which row qualifies as the top row, as long as it returns one row at most, then you can use the row_number window function with order by null.
SELECT s.part_number, c.supplier_product, c.customer_code
FROM supplier s
LEFT JOIN (SELECT *,
row_number() over (partition by customer_product order by null) as rn
FROM customer_lookup) c
ON s.part_number = c.customer_product
AND c.rn = 1
If you do care which row gets picked, then just modify the order by clause accordingly.
You can simply use CROSS APPLY to get your results, the main benefit here is that you are not using aggregation (GROUP BY)
SELECT
s.part_number, c.supplier_product, c.customer_code
FROM
supplier s
CROSS APPLY
(
SELECT TOP 1 * FROM customer_lookup t
WHERE s.part_number = t.customer_product
ORDER BY t.customer_code
) c
You should also add an ORDER BY to ensure the results are order the way you want them to be (I have added this in for you).
You should also define columns that you are using rather than using an asterisk (*) but that's up to you (I've left this as is for now)
http://sqlfiddle.com/#!6/716b5/17
If you're wanting the results you showed and in the order you showed them
SELECT
s.part_number, c.supplier_product, MIN(c.customer_code)
FROM
supplier s
LEFT JOIN
(
SELECT * FROM customer_lookup t
) c
ON s.part_number = c.customer_product
GROUP BY s.part_number, c.supplier_product
ORDER BY c.supplier_product DESC

SQL Group BY SUM one column and select of first row of grouped items

I have a part table where I have 5 fields. I want to sum the QTY of the mfgpn while showing the first returned row for the other 3 fields (Manfucturer, DateCode, Description). I initially thought of using the MIN function as follows, but that doesn't really help me insofar as that the data is not a int data type. How would I go about doing this? Right now I'm stuck at the following query below:
SELECT SUM([QTY]) AS QTY
,[MFGPN]
,MIN([MANUFACTURER]) AS MANUFACTURER
,MIN([DATECODE]) AS DateCode
,MIN([DESCRIPTION]) AS DESCRIPTION
INTO part
GROUP BY MFGPN, MANUFACTURER, DATECODE, description
ORDER BY mfgpn ASC
Would CROSS APPLY work for you?
SELECT
SUM(a.[QTY]) AS QTY
,a.[MFGPN]
,c.[MANUFACTURER]
,c.[DATECODE]
,c.[DESCRIPTION]
FROM part a
CROSS APPLY (SELECT TOP 1 * FROM part b WHERE a.[MFGPN] = b.[MFGPN]) c
GROUP BY
a.[MFGPN]
,c.[MANUFACTURER]
,c.[DATECODE]
,c.[DESCRIPTION]
Tested with the following:
DECLARE #T1 AS TABLE (
[QTY] int
,[MFGPN] NVARCHAR(50)
,[MANUFACTURER] NVARCHAR(50)
,[DATECODE] DATE
,[DESCRIPTION] NVARCHAR(50));
INSERT #T1 VALUES
(2, 'MFGPN-1', 'MANUFACTURER-A', '20120101', 'A-1'),
(4, 'MFGPN-1', 'MANUFACTURER-B', '20120102', 'B-1'),
(3, 'MFGPN-1', 'MANUFACTURER-C', '20120103', 'C-1'),
(1, 'MFGPN-2', 'MANUFACTURER-A', '20120101', 'A-2'),
(5, 'MFGPN-2', 'MANUFACTURER-B', '20120101', 'B-2')
SELECT
SUM(a.[QTY]) AS QTY
,a.[MFGPN]
,c.[MANUFACTURER]
,c.[DATECODE]
,c.[DESCRIPTION]
FROM #T1 a
CROSS APPLY (SELECT TOP 1 * FROM #T1 b WHERE a.[MFGPN] = b.[MFGPN]) c
GROUP BY
a.[MFGPN]
,c.[MANUFACTURER]
,c.[DATECODE]
,c.[DESCRIPTION]
Produces
QTY MFGPN MANUFACTURER DATECODE DESCRIPTION
9 MFGPN-1 MANUFACTURER-A 2012-01-01 A-1
6 MFGPN-2 MANUFACTURER-A 2012-01-01 A-2
This can be easily managed with a windowed SUM():
WITH summed_and_ranked AS (
SELECT
MFGPN,
MANUFACTURER,
DATECODE,
DESCRIPTION,
QTY = SUM(QTY) OVER (PARTITION BY MFGPN),
RNK = ROW_NUMBER() OVER (
PARTITION BY MFGPN
ORDER BY DATECODE -- or which column should define the order?
)
FROM atable
)
SELECT
MFGPN,
MANUFACTURER,
DATECODE,
DESCRIPTION,
QTY,
INTO parts
FROM summed_and_ranked
WHERE RNK = 1
;
For every row, the total group quantity and the ranking within the group is calculated. When actually getting rows for inserting into the new table (the main SELECT), only rows with RNK values of 1 are pulled. Thus you get a result set containing group totals as well as details of certain rows.