Oracle Complex Sort - Multiple Children - sql

I have a table as follows:
BRAND_ID PRODUCT_ID PRODUCT_DESC PRODUCT_TYPE
100 1000 Tools A
100 1500 Tools A
200 2000 Burgers B
300 3000 Clothing C
300 4000 Makeup D
300 5000 Clothing C
So a Brand can have multiple products, all of the same type or mixed types. If a brands products are all of the same type I need them first in the result, sorted by product type, followed by brands that have different product types. I can do this programatically but I wanted to see if there is a way to do it in the query.

I don't have access to Oracle, but I believe something along these lines should work...
WITH
ranked_data
AS
(
SELECT
COUNT(DISTINCT product_type) OVER (PARTITION BY brand_id) AS brand_rank,
MIN(product_type) OVER (PARTITION BY brand_id) AS first_product_type,
*
FROM
yourTable
)
SELECT
*
FROM
ranked_data
ORDER BY
brand_rank,
first_product_type,
brand_id,
product_type,
product_description
An alternative is to JOIN on to a sub-query to calculate the two sorting fields.
SELECT
yourTable.*
FROM
yourTable
INNER JOIN
(
SELECT
brand_id,
COUNT(DISTINCT product_type) AS brand_rank,
MIN(product_type) AS first_product_type,
FROM
yourTable
GROUP BY
brand_id
)
AS brand_summary
ON yourTable.brand_id = brand_summary.brand_id
ORDER BY
brand_summary.brand_rank,
brand_summary.first_product_type,
yourTable.brand_id,
yourTable.product_type,
yourTable.product_description

How about selecting from a sub-select that figures out number of distinct brands and then sorting by the count?
select t.BRAND_ID,
t.PRODUCT_ID,
t.PRODUCT_DESC,
t.PRODUCT_TYPE
from (select t2.BRAND_ID,
t2.PRODUCT_ID,
count(distinct t2.PRODUCT_TYPE) cnt
from YOURTABLE t2
group by t2.BRAND_ID, t2.PRODUCT_ID) data
join YOURTABLE t on t.BRAND_ID = data.BRAND_ID and t.PRODUCT_ID = data.PRODUCT_ID
order by data.cnt, BRAND_ID, PRODUCT_ID, PRODUCT_TYPE

Related

How can I do order by without using it in group by

I have a table called products and I would like to get all my products with same asin inside one row and concat their skus in one column I came up with something like this but I have problem with order by sku and other columns that are not inside group by. Is there any solution?
my sql:
SELECT prod.asin, string_agg(p.sku,', ') AS skus,
SUM(p.amazon_inv_available) AS amazon_available_inv,
SUM(p.amazon_inv_total) AS amazon_total_inv
FROM (
SELECT id, asin, sku, amazon_inv_available, amazon_inv_total
FROM products
WHERE store_id IN (12, 10, 11)
ORDER BY sku
) AS prod
LEFT JOIN products AS p ON prod.asin = p.asin
GROUP by prod.asin
dbfiddle
Desired result when order by prod.sku
order by prod.sku DESC
You can order by inside string_agg(sku,', ' order by sku) remove order by from your subquery
You can add ORDER BY to STRING_AGG() to specify the ordering of the concatenated values.
For example:
SELECT
asin,
string_agg(sku,', ' order by id) AS skus,
SUM(amazon_inv_available) AS amazon_available_inv,
SUM(amazon_inv_total) AS amazon_total_inv
FROM products
WHERE store_id IN (12, 10, 11)
GROUP by asin
ORDER BY amazon_available_inv
Result:
asin skus amazon_available_inv amazon_total_inv
----- ----------- --------------------- ----------------
B HH, QW 200 400
A AC, TT, DD 300 600
See db<>fiddle.
I think I found the solution. I used my query as subquery:
SELECT * FROM
(SELECT prod.asin, string_agg(p.sku,', ' ORDER BY p.sku) AS skus,
SUM(p.amazon_inv_available) AS amazon_available_inv,
SUM(p.amazon_inv_total) AS amazon_total_inv
FROM (SELECT id, asin, sku, amazon_inv_available, amazon_inv_total
FROM products WHERE store_id IN(
10,11,12)) AS prod
LEFT JOIN
products AS p
ON prod.asin = p.asin
GROUP by prod.asin) AS res
ORDER BY skus

Access SQL - Delete records with same identifiers based on criteria

I have a database with multiple records with the same identifier. I want to remove just one of those records.
OrderNum Cost
10001 254
10002 343
10002 300
10003 435
10003 323
For the above table, lets say I just want to delete the records with duplicate Order Numbers that have the smaller cost. Ex: Records 10002, keep the one with a cost of 343, delete the smaller 300.
Here is the query I have come up with, however I am using the cost to identify the duplicate which is bad if there is a similar cost somewhere else in the table.
DELETE Orders.*
FROM Orders
WHERE (cost In
(Select min(cost) FROM Orders
GROUP BY [OrderNum] HAVING Count(*) > 1))
How can I query through using the Order Number and deleting the one smaller of value that has a duplicate?
I'll explain the solution in stages:
SELECT OrderNum, Min(Cost) as MinCost
FROM Orders
GROUP BY OrderNum
HAVING COUNT(*) > 1
This returns records you intend to delete:
OrderNum MinCost
10002 300
10003 323
The following is another version of the same query using sub-SELECTs:
SELECT *
FROM
(
SELECT OrderNum, Min(Cost) as MinCost
FROM Orders
GROUP BY OrderNum
HAVING COUNT(*) > 1
) M
We want to join the marked for deletion records back to the Orders table, one way to achieve this is using an EXISTS statement:
SELECT *
FROM Orders O
WHERE EXISTS (
SELECT *
FROM
(
SELECT OrderNum, Min(Cost) as MinCost
FROM Orders
GROUP BY OrderNum
HAVING COUNT(*) > 1
) M
WHERE O.OrderNum = M.OrderNum
AND O.Cost = M.MinCost
)
Now that we've mastered the SELECT statement needed, we turn it into the DELETE statement:
DELETE
FROM Orders O
WHERE EXISTS (
SELECT *
FROM
(
SELECT OrderNum, Min(Cost) as MinCost
FROM Orders
GROUP BY OrderNum
HAVING COUNT(*) > 1
) M
WHERE O.OrderNum = M.OrderNum
AND O.Cost = M.MinCost
)
If you have large amounts of data, you may wish to create an index to optimize join:
CREATE INDEX IX_Orders_001 ON Orders (OrderNum, Cost);
You want to really do something like:
WHERE (ordernum, cost) IN (SELECT ordernum, min(cost) as cost FROM Orders GROUP BY OrderNum HAVING COUNT(*) > 1);
But Access doesn't support tuples like this as many larger RDBMS's do.
Instead you could concatenate your tuples:
WHERE ordernum & cost IN (SELECT ordernum & min(cost) FROM Orders GROUP BY OrderNum HAVING Count(*) > 1);
This will remove all duplicates but the largest one for each
delete a from yourtable a
join
(select *, row_number() OVER (partition by ordernum, cost ORDER BY ordernum, cost desc) rownum from yourtable )b
on a.ordernum=b.ordernum
where rownum<>1
You can use JOIN to delete the smaller cost of each OrderNum like below :
DELETE Orders.*
FROM Orders
join (Select OrderNum, max(cost) as cost FROM Orders
GROUP BY [OrderNum] HAVING Count(*) > 1) as R
on Orders.OrderNum=R.OrderNum and Orders.cost < R.cost

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.

SQL select x number of rows from table based on column value

I'm looking for a way to select top 3 rows from 4 vendors from a table of products, following this criteria:
Must select 4 vendors.
Must select top 3 products for each vendor ordered by product rating.
I tried doing something like:
select top 12 * product, vendor
from products
order by productrating
but obvisously that goesn't give me 3 products for each vendor.
The product table has:
productid (int), productname (nvarchar(500)), productrating (float),
vendor (id), price (float).
These are the relevant columns.
You can use the ANSI standard row_number() function to get 3 products for each vendor:
select p.*
from (select p.*,
row_number() over (partition by vendor order by rating desc) as seqnum
from products p
) p
where p.seqnum <= 3
If you want 4 vendors:
select top 12 p.*
from (select p.*,
row_number() over (partition by vendor order by rating desc) as seqnum
from products p
) p
where p.seqnum <= 3
order by vendor;
This will give you top 3 Products per vendor. You didn't specify how you're selecting the 4 vendors. That logic could easily be included using the WHERE clause or using a different ORDER BY depending on how you select the 4 vendors.
SELECT TOP 12 vnd.Vendor, apl.ProductName
FROM Vendors vnd
CROSS APPLY (
SELECT TOP 3 ProductID
FROM Products prd
WHERE vnd.ProductID = prd.ProductID
ORDER BY prd.ProductRating DESC
) apl
ORDER BY vnd.VendorName
If you have fixed list of vendors you would like to query you can use the following approach:
SELECT TOP 3
p.ProductID
FROM Products p
WHERE p.ProductID IN ( SELECT v.ProductID
FROM Vendors v
WHERE v.VendorID IN (Vendor1ID, Vendor2ID, Vendor3ID, Vendor4ID)
ORDER BY p.ProductRating DESC
You will need to look for a work-around if you have vendor names to select them filtering out by its name but keeping the join coindition trhough its IDs.

Order By in subselect using concat and decode

Say I have two tables:
Product
product_id (other fields are of no concern)
Sku
product_id sku_id color_id color_name (other fields such as size but unimportant)
001 11 5 green
001 12 1 black
001 13 3 red
002 21 1 black
002 22 2 yellow
002 23 8 magenta
002 24 9 turquoise
I need to rewrite a query that gets a list of product ids with comma delimited lists for all colors/color ids associated with that product. The color ids/names must have the same order in both lists.
Desired output:
product_id colorIds colorNames
001 1,3,5 black,red,green
002 1,2,8,9 black,yellow,magenta,turquoise
Note that the concat list of color ids' order maps to the color names order.
Current output:
product_id colorIds colorNames
001 1,3,5 green,black,red -- out of order sometimes
002 1,2,8,9 black,yellow,magenta,turquoise
Currently used query:
select distinct(p.product_id) as product_id,
(select decode(dbms_lob.SubStr(wm_concat(DISTINCT color_name)),'NO COLOR','','No Color','','no color','',null,'',dbms_lob.SubStr(wm_concat(DISTINCT color_name))) as color_name from sku where product_id = p.product_id) as colorName,
(select decode(dbms_lob.SubStr(wm_concat(DISTINCT color_code)),'000','',dbms_lob.SubStr(wm_concat(DISTINCT color_code))) from sku where product_id = p.product_id) as colorCode
from product p;
I was thinking of just adding order by clauses in those sub selects, but the query just errors out, saying missing right parenthesis - oddly there seemed to be no mismatched parens. Any suggestions are welcome.
Edit *
The above query is highly simplified. In reality it joins with over a dozen other tables to get other data columns related to the product, most of which are non-aggregate pieces of data. The solution should have no group by clause in the main query or suggest a reasonable way to accommodate this requirement.
This might work for you:
SELECT p.product_id
, LISTAGG(s.color_id, ',') WITHIN GROUP ( ORDER BY s.color_id ) AS colorIds
, LISTAGG(s.color_name, ',') WITHIN GROUP ( ORDER BY s.color_id ) AS colorNames
FROM product p LEFT JOIN ( SELECT DISTINCT product_id, color_id, color_name FROM sku ) s
ON p.product_id = s.product_id
GROUP BY p.product_id
ORDER BY product_id
LISTAGG() can be sorted while WM_CONCAT() can't (and it's undocumented, etc.).
UPDATE per OP's comment about non-aggregate data:
WITH product_colors AS (
SELECT p.product_id
, LISTAGG(s.color_id, ',') WITHIN GROUP ( ORDER BY s.color_id ) AS colorIds
, LISTAGG(s.color_name, ',') WITHIN GROUP ( ORDER BY s.color_id ) AS colorNames
FROM product p LEFT JOIN ( SELECT DISTINCT product_id, color_id, color_name FROM sku ) s
ON p.product_id = s.product_id
GROUP BY p.product_id
)
SELECT t1.other_column, t2.other_column, etc.
FROM table1 t1 JOIN table2 t2 ON ...
JOIN product_colors pc ON ...
This will achieve the distinct effect (you cannot use distinct with listagg):
select product_id,
listagg(color_id, ',') within group(order by color_id) as colorids,
listagg(color_name, ',') within group(order by color_id) as colornames
from (select distinct product_id, color_id, color_name from sku)
group by product_id
If you want to show columns from the product table and/or you want to show products on the product table not on the sku table you can use:
select p.product_id,
listagg(s.color_id, ',') within group(order by s.color_id) as colorids,
listagg(s.color_name, ',') within group(order by s.color_id) as colornames
from product p
left join (select distinct product_id, color_id, color_name from sku) s
on p.product_id = s.product_id
group by p.product_id
Hi this might work as well .
select product_id,
listagg(color_id,',') within group(order by color_names) as color_ids,
listagg(color_names,',') within group (order by color_names) color_names
from sku
group by product_id;