Tree with recursive and default - sql

Using Postgres.
I have a pricelists
CREATE TABLE pricelists(
id SERIAL PRIMARY KEY,
name TEXT,
parent_id INTEGER REFERENCES pricelists
);
and another table, prices, referencing it
CREATE TABLE prices(
pricelist_id INTEGER REFERENCES pricelists,
name TEXT,
value INTEGER NOT NULL,
PRIMARY KEY (pricelist_id, name)
);
Parent pricelist id=1 may have 10 prices.
Pricelist id=2 as a child of parent 1 may have 5 prices which override parent 1 prices of the same price name.
Child Pricelist id=3 as as a child of pricelist 2 may have 2 price which override child 2 prices of the same price name.
Thus when I ask for child 3 prices, I want to get
all prices of child 3 and
those prices of his parent (child 2) that do not exists in child 3 and
all parent 1 prices that do not exists until now.
The schema can be changed in order to be efficient.
Example:
If
SELECT pl.id AS id, pl.parent_id AS parent, p.name AS price_name, value
FROM pricelists pl
JOIN prices p ON pl.id = p.pricelist_id;
gives
| id | parent | price_name | value |
|----------|:-------------:|------------:|------------:|
| 1 | 1 | bb | 10 |
| 1 | 1 | cc | 10 |
| 2 | 1 | aa | 20 |
| 2 | 1 | bb | 20 |
| 3 | 2 | aa | 30 |
then I'm looking for a way of fetching pricelist_id = 3 prices that'd give me
| id | parent | price_name | value |
|----------|:-------------:|------------:|------------:|
| 1 | 1 | cc | 10 |
| 2 | 1 | bb | 20 |
| 3 | 2 | aa | 30 |

WITH RECURSIVE cte AS (
SELECT id, name, parent_id, 1 AS lvl
FROM pricelists
WHERE id = 3 -- provide your id here
UNION ALL
SELECT pl.id, pl.name, pl.parent_id, c.lvl + 1
FROM cte c
JOIN pricelists pl ON pl.id = c.parent_id
)
SELECT DISTINCT ON (p.price_name)
c.id, c.parent_id, p.price_name, p.value
FROM cte c
JOIN prices p ON p.pricelist_id = c.id
ORDER BY p.price_name, c.lvl; -- lower lvl beats higher level
Use a recursive CTE like here:
Total children values based on parent
Recursive SELECT query to return rates of arbitrary depth?
There are many related answers.
Join to prices once at the end, that's cheaper.
Use DISTINCT ON the get the "greatest per group":
Select first row in each GROUP BY group?

Related

SQL - IN clause with no match

I'm trying to build a query where I can select from a table all products with a certain ID but I would also like to find out what products were not found within the IN clause.
Product Table
ID | Name
---|---------
1 | ProductA
2 | ProductB
4 | ProductD
5 | ProductE
6 | ProductF
7 | ProductG
select *
from products
where id in (2,3,7);
As you can see, product id 3 does not exist in the table.
My query will only return rows 2 and 7.
I would like a blank/null row returned if a value in the IN clause did not return anything.
Desired Results:
ID | Name
---|---------
2 | ProductB
3 | null
7 | ProductG
You can use a left join:
select i.id, p.name
from (select 2 as id union all select 3 union all select 7
) i left join
products p
on p.id = i.id
IN is not useful in this case.
Use a CTE with the ids that you want to search for and left join to the table:
with cte(id) as (select * from (values (2),(3),(7)))
select c.id, p.name
from cte c left join products p
on p.id = c.id
See the demo.
Results:
| id | Name |
| --- | -------- |
| 2 | ProductB |
| 3 | |
| 7 | ProductG |

How to count records using an l-tree

I have two tables, tickets and categories. The categories table has 3 columns of interest: id, name and path. The data looks like this:
id | Name | Path
------------------
1 | ABC | 1
2 | DEF | 1.2
3 | GHI | 1.2.3
4 | JKL | 4
5 | MNO | 4.5
6 | PQR | 4.5.6
9 | STU | 4.5.9
Note that the path column is an l-tree. What this is meant to represent is that the category with id=2 is a subcategory of id=1 and that id=3 is a subcategory of id=2.
In my tickets table, there's a column called category_id which refers to the id column in my categories table. Each ticket can have up to one category assigned to it (category_id may be null).
I'm trying to count all the tickets for each category.
Suppose my tickets table looks like this:
ticket_id | ticket_title | category_id
1 | A | 1
2 | B | 2
3 | C | 3
4 | D | 5
5 | F | 5
6 | G | 6
7 | H | 9
I would like to output:
category_id | count
1 | 3
2 | 2
3 | 1
4 | 4
5 | 4
6 | 1
9 | 1
I've found that I can get all of the tickets which belong to a given category with the following query: select * from tickets where category_id in (select id from categories where path ~ '*.1.*'); (although now that I'm writing this question I'm not convinced this is correct).
I've also attempted to perform the ticket-count-by-category problem and I came up with:
SELECT
categories.id as cid,
COUNT(*) as tickets_count
FROM tickets
LEFT JOIN categories ON tickets.category_id = categories.id
GROUP BY cid;
which outputs the following:
c_id | count
1 | 1
2 | 1
3 | 1
5 | 2
6 | 1
9 | 1
I'm not very good at SQL. Is it possible to achieve what I want?
Try this:
WITH tickets_per_path AS (
SELECT
c.path AS path,
count(*) AS count
FROM tickets t INNER JOIN categories c ON (t.category_id = c.id)
GROUP BY c.path)
SELECT
c.id,
sum(tickets_per_path.count) AS count
FROM categories c LEFT JOIN tickets_per_path ON (c.path #> tickets_per_path.path)
GROUP BY c.id
ORDER BY c.id;
Which yields the following result:
id| count
1 | 3
2 | 2
3 | 1
4 | 4
5 | 4
6 | 1
9 | 1
It roughly works like this:
the WITH clause computes the number of tickets per path (without
including the count of tickets of descendent paths).
the second select clause joins the categories table with the precomputed tickets_per_path view, but instead of an equi-join on path, it
joins by testing whether a record in the left table (categories) is
an ancestor of the right side table (using #> operator). Then it
groups by category id and sums up the ticket counts by category
including the descendant counts.
You are close, but you need a more general join:
SELECT c.id as cid, COUNT(*) as tickets_count
FROM categories c LEFT JOIN
tickets t
ON t.category_id || '.' LIKE c.id || '.%'
GROUP BY c.id;
The '.' in the comparison is just so 1.100 doesn't match 1.1.

Find a value which contains in ALL rows of a table

I need to select all values which are contained in ALL rows of a table.
I have table “Ingredient” and ProductIngredient(there I have a recipe of a product).
Ingredient
| ingredient_id | name | price |
| 1 | Bla | 100
| 2 | foo | 50
ProductIngredient.
| Product_id | ingredient_id
| 1 | 1
| 1 | 2
| 2 | 1
The output should be
| 1 | Bla |
as it is in all rows of ProductIngredient.
SELECT DISTINCT Ingredient_Id
FROM Ingredients I
WHERE Ingredient_Id = ALL
(SELECT Ingredient_id FROM ProductIngredient PI
WHERE PI.Ingredient_Id = I.Ingredient_Id );
How can I fix my code to make it work?
This will give you all Ingredients from I that are in PI for each product. This is assuming that each product does not have multiple rows for a product and ingredient combination.
SELECT I.Ingredient_Id
FROM Ingredients I INNER JOIN ProductIngredient PI
ON PI.Ingredient_Id = I.Ingredient_Id
GROUP BY I.Ingredient_Id
HAVING COUNT(*) >= (SELECT COUNT(DISTINCT Product_id) FROM ProductIngredient)

Find duplicate combinations

I need a query to find duplicate combinations in these tables:
AttributeValue:
id | name
------------------
1 | green
2 | blue
3 | red
4 | 100x200
5 | 150x200
Product:
id | name
----------------
1 | Produkt A
ProductAttribute:
id | id_product | price
--------------------------
1 | 1 | 100
2 | 1 | 200
3 | 1 | 100
4 | 1 | 200
5 | 1 | 100
6 | 1 | 200
7 | 1 | 100 -- duplicate combination
8 | 1 | 100 -- duplicate combination
ProductAttributeCombinations:
id_product_attribute | id_attribute
-------------------------------------
1 | 1
1 | 4
2 | 1
2 | 5
3 | 2
3 | 4
4 | 2
4 | 5
5 | 3
5 | 4
6 | 3
6 | 5
7 | 1
7 | 4
8 | 1
8 | 5
I need SQL that creates result like:
id_product | duplicate_attributes
----------------------------------
1 | {7,8}
If I understand correct, 7 is a duplicate of 1 and 8 is a duplicate of 2. As phrased, your question is a bit confusing, because 7 and 8 are not related to each other and the only table of interest is ProductAttributeCombinations.
If this is the case, then one method is to use string aggregation
with combos as (
select id_product_attribute,
string_agg(id_attribute::text, ',' order by id_attribute) as combo
from ProductAttributeCombinations pac
group by id_product_attribute
)
select *
from combos c
where exists (select 1
from combos c2
where c2.id_product_attribute > c.id_product_attribute and
c2.combo = c.combo
);
Your question leaves some room for interpretation. Here is my educated guess:
For each product, return an array of all instances with the same set of attributes as any other instance of the same product with smaller ID.
WITH combo AS (
SELECT id_product, id, array_agg(id_attribute) AS attributes
FROM (
SELECT pa.id_product, pa.id, pac.id_attribute
FROM ProductAttribute pa
JOIN PoductAttributeCombinations pac ON pac.id_product_attribute = pa.id
ORDER BY pa.id_product, pa.id, pac.id_attribute
) sub
GROUP BY 1, 2
)
SELECT id_product, array_agg(id) AS duplicate_attributes
FROM combo c
WHERE EXISTS (
SELECT 1
FROM combo
WHERE id_product = c.id_product
AND attributes = c.attributes
AND id < c.id
)
GROUP BY 1;
Sorting can be inlined into the aggregate function so we don't need a subquery for the sort (like #Gordon already provided). This is shorter, but also typically slower:
WITH combo AS (
SELECT pa.id_product, pa.id
, array_agg(pac.id_attribute ORDER BY pac.id_attribute) AS attributes
FROM ProductAttribute pa
JOIN PoductAttributeCombinations pac ON pac.id_product_attribute = pa.id
GROUP BY 1, 2
)
SELECT ...
This only returns products with duplicate instances.
SQL Fiddle.
Your table names are rather misleading / contradict the rest of your question. Your sample data is not very clear either, only featuring a single product. I assume there are many in your table.
It's also unclear whether you are using double-quoted table names preserving CaMeL-case spelling. I assume: no.

select product count for each category, when products are in sub-categories

This is a table structure for products
PROD_ID CATEG_ID
1 2
2 21
3 211
4 5
5 51
This is a table structure for categories
CATEG_ID PARENT_CATEG_ID
2 NULL
5 NULL
21 2
211 21
51 5
I have a difficulty when selecting product count for each category including nested categories.
For example, category 2 has 1 product, category 21 has 1 product, category 211 has 1 product, and since categories 21 and 221 are respective direct/indirect ancestors of the category 2, category 2 has 3 products. So I need a query or just a way to get someting like this:
CATEG_ID PARENT_CATEG_ID PRODUCT_COUNT
2 NULL 3 (including product count for categories 21 and 221)
5 NULL 2 (including product count for category 51)
21 2 2 (including product count for category 221)
211 21 1 (no category ancestor, only product count for self)
51 5 1 (no category ancestor, only product count for self)
Is it possible with SQL only or I need to add some PHP?
The following should do it:
with recursive cat as (
select categ_id,
parent_categ_id,
categ_id as root_category,
1 as level
from categories
where parent_categ_id is null
union all
select c.categ_id,
c.parent_categ_id,
p.root_category,
p.level + 1
from categories c
join cat as p on p.categ_id = c.parent_categ_id
)
select c.categ_id,
p.prod_id,
(select count(*) from cat c2 where c2.level >= c.level and c2.root_category = c.root_category) as cnt
from cat c
left join products p on p.categ_id = c.categ_id
;
The recursive query first builds the whole category tree. It returns the root category for each category together with the nesting level of the category inside the sub-tree for the specific root category. The CTE itself returns this:
categ_id | parent_categ_id | root_category | level
---------+-----------------+---------------+------
2 | (null) | 2 | 1
21 | 2 | 2 | 2
211 | 21 | 2 | 3
5 | (null) | 5 | 1
51 | 5 | 5 | 2
This is then used to join against the product table and do a running sum of the products contained in the same root category (that's the count(p.prod_id) over (partition by c.root_category order by level desc) part). So the result of the complete query is this:
categ_id | prod_id | product_count
---------+---------+--------------
2 | 1 | 3
21 | 2 | 2
211 | 3 | 1
5 | 4 | 2
51 | 5 | 1
SQLFiddle: http://sqlfiddle.com/#!15/d6261/15
Here we check if c1.categ has child categories with the help of recursive query starting from itself that gets all child categories ids by building the tree under it.If it does then it also counts products under child categories
select c1.categ_id,c1.parent_categ_id,count(prods.prod_id)
as product_count from categ c1
join prods on prods.categ_id=c1.categ_id or prods.categ_id
in( with recursive tree(id,parent_id)as
(select categ_id,parent_categ_id from categ
where categ_id=c1.categ_id
union all
select cat.categ_id,cat.parent_categ_id from categ cat
join tree on tree.id=cat.parent_categ_id) select id from tree)
group by c1.categ_id,c1.parent_categ_id order by product_count
The result is as following
+----------+-----------------+---------------+
| categ_id | parent_categ_id | product_count |
+----------+-----------------+---------------+
| 51 | 5 | 1 |
| 211 | 21 | 1 |
| 5 | NULL | 2 |
| 21 | 2 | 2 |
| 2 | NULL | 3 |
+----------+-----------------+---------------+