Additional rows needed in SQL view - mysql

I have the following SQL tables, and require a solution compatible with both MySQL and Postgresql
create table price_level (
id serial primary key,
name varchar(200)
);
create table product (
id serial primary key,
name varchar(200),
base numeric not null,
vat int not null
);
create table product_price (
id serial primary key,
base numeric,
vat numeric,
product_id int not null references product(id) on update cascade on delete cascade,
price_level_id int not null references price_level(id) on update cascade on delete cascade,
unique(product_id,price_level_id)
);
For the SQL structure above I've created a view:
create view view_product as
select
p.id as product_id,
coalesce(pp.base, p.base) as base,
coalesce(pp.vat, p.vat) as vat,
pp.price_level_id
from
product as p
left join
product_price as pp on pp.product_id=p.id
;
These are sample data:
Table price_level
id name
1 A
2 B
3 C
4 D
5 E
Table product
id name base vat
1 Test 100 20
Table product_price
id base vat product_id price_level_id
1 NULL NULL 1 1
2 200 NULL 1 2
3 NULL 10 1 3
Output of the view view_product is:
product_id base vat price_level_id
1 100 20 1
1 200 20 2
1 100 10 3
... and the question is: How do I get output like this?:
product_id base vat price_level_id
1 100 20 1
1 200 20 2
1 100 10 3
1 100 20 4
1 100 20 5
As you see in the example above I need to get D and E price_level as additional rows. How do I create such view/join? It should have good performance also because tables can get big with additional price levels.
Thanks for help.

I would use union to add those records from price_level table that do not have corresponding record in product_price table for a certain product:
select
p.id as product_id,
coalesce(pp.base, p.base) as base,
coalesce(pp.vat, p.vat) as vat,
pp.price_level_id
from
product as p
left join
product_price as pp on pp.product_id=p.id
union distinct
select
p.id as product_id,
p.base,
p.vat,
pl.price_level_id
from
price_level pl
join
product as p
where (p.id, pl.id) not in (select product_id, price_level_id from product_price)

I would use following approach, cross join tables price_level and product. Then just lookup if override exists in product_price table.
SELECT
product.id as product_id,
IFNULL(product_price.base, product.base) as `base`,
IFNULL(product_price.vat, product.vat) as `vat`,
price_level.id as price_level_id
FROM price_level
CROSS JOIN product
LEFT JOIN product_price ON
product_price.price_level_id = price_level.id AND
product_price.product_id = product.id
WHERE product.id = 1
ORDER BY product.id, price_level.id
just remember to use product.id and not product_id in WHERE conditions

Try with:
create view view_product as
select
p.id as product_id,
coalesce(pp.base, p.base) as base,
coalesce(pp.vat, p.vat) as vat,
coalesce(pp.price_level_id,pl.id) --modified row
from
product as p
left join
product_price as pp on pp.product_id=p.id
LEFT JOIN price_level pl on pp.price_level_id=pl.id -- modified row
;
(not tested, but for sure you have to catch the price levels from the properly table)

Related

SQL Table with two relations or table with one relation and repeated rows

This is an example, the names are fictitious.
On the one hand, we have suppliers who provide products to shops:
Suppliers
id name
1 GreatSupplier
2 SuperSupplier
On the other hand, we have shops that sell products to consumers:
Shops
id name supplier
1 NiceShop null
2 ShopShop null
3 Soop 1
4 CheapShop 1
5 MyShop 1
6 Shopping 2
There are shops that have their own prices like NiceShop or ShopShop, so they don't have suppliers. But there are shops that use the prices set by the supplier like Soop, CheapShop, MyShop or Shopping.
Then I want to show all the prices of the products that the shops show to their customers. Something like this:
NiceShop - Tomate: 1.23 // shop price
ShopShop - Tomate: 1.26 // shop price
Soop - Tomate: 1.21 // supplier 1 price
CheapShop - Tomate: 1.21 // supplier 1 price
MyShop - Tomate: 1.21 // supplier 1 price
Shopping - Tomate: 1.19 // supplier 2 price
Two options come to mind:
Option 1:
Products
id id_product id_shop id_supplier price
1 34 1 null 1.23
2 34 2 null 1.26
3 34 null 1 1.21
4 34 null 2 1.19
When displaying prices, if it is a row with id_shop I show it as is, but if it is a row with id_supplier I join the supplier and the shops.
Here I can't make a unique index between id_product-id_store-id_supplier and things like this could happen:
id id_product id_shop id_supplier price
5 34 3 null 1.21 // wrong
This should not happen as shop 3 has supplier 1 and this is already inserted in id 3.
Option 2:
Another option would be:
If we insert a price from a shop that does not have a supplier, it is inserted as is.
If we insert a price from a supplier, a join is made between the supplier and the shops and the same price is inserted several times. (same for updates)
Products
id id_product id_shop price
1 34 1 1.23 // shop price
2 34 2 1.26 // shop price
3 34 3 1.21 // supplier 1 price
4 34 4 1.21 // supplier 1 price
5 34 5 1.21 // supplier 1 price
5 34 6 1.19 // supplier 2 price
This option is a bit cleaner and allows me to create a single id_product-id_shop index but I am creating a lot of records with repeated prices, in this example it is duplicated 3 times but in my real environment it can be duplicated 50 times, and that translates into several extra gigabytes of database space.
Is there a better way to do this?
Most of the issues described can be solved by creating a common associated table for both shops and suppliers that is used to associate prices. In my example, I'll call this the price_setters table.
A Shop's Price-Setter is either itself or its supplier. This is enforced by the constraint on price_setters that the record has either a unique shop_id or a unique supplier_id, but not both.
prices has a FK to price_setters rather than shops or suppliers.
The only "anomaly" remaining is that there is no requirement in the schema for a shop or supplier to have a price_setter; however, that would just prevent you from assigning prices to the entity.
Edit: I moved the FKs between price_setters, shops, and suppliers into price_setters
-- Retrieve all prices by shop by going through the price_setter
select
shops.name,
products.name,
prices.price
from shops
left join suppliers
on shops.supplier_id = suppliers.id
join price_setters
on shops.id = price_setters.shop_id
or suppliers.id = price_setters.supplier_id
join prices
on prices.price_setter_id = price_setters.id
join products
on prices.product_id = products.id
;
create table suppliers (
id unsigned bigint primary key,
name varchar(255) not null
);
create table shops (
id unsigned bigint primary key,
name varchar(255) not null,
supplier_id unsigned bigint,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
);
create table products (
id unsigned bigint primary key,
name varchar(255) not null
);
-- A shop's price setter is either itself or its supplier
create table price_setters (
id unsigned bigint primary key,
shop_id unsigned bigint ,
supplier_id unsigned bigint,
FOREIGN KEY (shop_id) REFERENCES shops(id),
FOREIGN KEY (supplier_id) REFERENCES suppliers(id),
unique(shop_id),
unique(supplier_id),
CONSTRAINT chk_shop_xor_supplier CHECK (
(shop_id IS NOT NULL AND supplier_id IS NULL)
OR (shop_id IS NULL AND supplier_id IS NOT NULL)
)
);
create table prices (
id unsigned bigint primary key,
product_id unsigned bigint not null,
price_setter_id unsigned bigint not null,
price float not null,
unique(product_id, price_setter_id),
FOREIGN KEY (price_setter_id) REFERENCES price_setters(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
If you want to have it clean you will need 3 entities (apart from shops and suppliers):
Products (PK = id_product)
id_product name (and other attributes)
34 Tomate
Shop prices (PK = id_product + id_shop)
id_product id_shop price
34 1 1.23
34 2 1.26
Supplier prices (PK = id_product + id_supplier)
id_product id_supplier price
34 1 1.21
34 2 1.19
In both shop prices and supplier prices product/shop/supplier ids are not null and foreign keys to corresponding tables.
To display a list you will need to union both price tables:
SELECT s.id as id_shop, shp.id_product, shp.price
FROM shops s
JOIN shop_prices shp ON (shp.id_shop = s.id AND s.supplier_id IS NULL)
UNION ALL
SELECT s.id as id_shop, sup.id_product, sup.price
FROM shops s
JOIN supplier_prices sup ON (sup.id_supplier = s.id_supplier)
(+extra joins with products if you need to display product attributes like name...)
This structure can't prevent eg. adding a record to shop prices for a shop that has a supplier, as in your example:
id_product id_shop price
34 3 1.21
But if you're careful such record wouldn't even show, for example in query above it is filtered out with s.supplier_id IS NULL in join condition. If it's mandatory, such case can be caught in a trigger or just at application level.
Can you just add shops without suppliers to the suppliers table?
Also - this phrase scares me:
bit complicated to maintain in the updates
I would use views to display the prices - not new tables.
I would also consider adding some form of versioning to your product/price table. Two ways to do this: add begin_ts and end_ts where end_ts = '9999-12-31' means the record is still active. Or alternatively, have a trigger that automatically tracks when a record changes and writes the record to an archive table. Bottom line is you will probably want something that tracks the price history so you can show what the prices where X days, weeks, months, years ago.

Joining multiple columns into one with union, exclude results with same id

I want to join columns from multiple tables to one column, in my case column 'battery_value' and 'technical_value' into column 'value'. I want to fetch data for only given category_ids, but because of UNION, I get data from other tables as well.
I have 4 tables:
Table: car
car_id model_name
1 e6
Table: battery
battery_category_id car_id battery_value
1 1 125 kW
Table: technical_data
technical_category_id car_id technical_value
1 1 5
3 1 2008
Table: categories
category_id category_name category_type
1 engine power battery
1 seats technical
3 release year technical
From searching, people are suggesting that I use union to join these columns. My query now looks like this:
SELECT CARS.car_id
category_id,
CATEGORIES.category_name,
value,
FROM CARS
left join (SELECT BATTERY.battery_category_id AS category_id,
BATTERY.car_id AS car_id,
BATTERY.value AS value
FROM BATTERY
WHERE `BATTERY`.`battery_category_id` IN (1)
UNION
SELECT TECHNICAL_DATA.technical_category_id AS category_id,
TECHNICAL_DATA.car_id AS car_id,
TECHNICAL_DATA.value AS value
FROM TECHNICAL_DATA
WHERE `TECHNICAL_DATA`.`technical_category_id` IN (3))
tt
ON CARS.car_id = tt.car_id
left join CATEGORIES
ON category_id = CATEGORIES.id
So the result I want is this, because I only want to get the data where category_id 1 is in battery table:
car_id category_id category_name technical_value
1 1 engine power 125 kW
1 3 release year 2008
but with the query above I get this, category_id 1 from technical table is included which is not something I want:
car_id category_id category_name value
1 1 engine power 125 kW
1 1 seats 125 kW
1 3 release year 2008
How can get exclude the 'seats' row?
For the results you want, I don't see why the cars table is needed. Then, you seem to need an additional key for the join to categories based on which table it is referring to.
So, I suggest:
SELECT tt.*, c.category_name
FROM ((SELECT b.battery_category_id AS category_id,
b.car_id AS car_id, b.value AS value,
'battery' as which
FROM BATTERY b
WHERE b.battery_category_id IN (1)
) UNION ALL
(SELECT td.technical_category_id AS category_id,
td.car_id AS car_id, td.value AS value,
'technical' as which
FROM TECHNICAL_DATA td
WHERE td.technical_category_id IN (3)
)
) tt LEFT JOIN
CATEGORIES c
ON c.id = tt.category_id AND
c.category_type = tt.which;
That said, you seem to have a problem with your data model, if the join to categories requires "hidden" data such as the type. However, that is outside the scope of the question.

Updating all columns of a table from another table's data?

What I want to do is simple but I lack some sql knowledge, so I would truly appreciate any kind of help.
I have table 'dev'.
product1 | product2 | count
---------------------
I also have a table likes.
wishlist_id | user_id | product_id
-------------------------------
1 2 54
2 2 60
3 3 54
7 3 60
.. .. ..
99 99 99
I want to find how many times 2 products where liked together, but instead of creating a view I want to save the data in the dev table.
How can I update the rows so dev table becomes:
dev
-----------------------
product1 product2 count
54 60 2
..
EDIT
I don't know how to actually use the update statement here.
my query is :
SELECT i1.product_id as product1, i2.product_id as product2,
COUNT(*) as num
FROM likes i1
INNER JOIN likes i2 ON i1.wishlist_id = i2.wishlist_id
AND i1.product_id < i2.product_id
GROUP BY product1, product2;
Suppose your dev table has a unique key constraint on the pair of product1, product2:
CREATE TABLE dev (
product1 INT NOT NULL,
product2 INT NOT NULL,
count INT NOT NULL DEFAULT 0,
PRIMARY KEY (product1, product2)
);
Now you can use INSERT...ON DUPLICATE KEY UPDATE to add or replace rows corresponding to pairs of your products. You already had a SELECT statement that generated the rows, now you need to use it in an IODKU.
INSERT INTO dev (product1, product2, count)
SELECT i1.product_id as product1, i2.product_id as product2, COUNT(*) as num
FROM likes i1 INNER JOIN likes i2
ON i1.wishlist_id = i2.wishlist_id AND i1.product_id < i2.product_id
GROUP BY product1, product2
ON DUPLICATE KEY UPDATE count = VALUES(count);
Read more about IODKU here: https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html

MySql: Concatenate 2 columns if condition is met

I have the following data set:
CREATE TABLE division (
id INT AUTO_INCREMENT PRIMARY KEY,
division VARCHAR(30) NOT NULL
) ENGINE=INNODB;
INSERT INTO division (division) VALUES ("Division1"), ("Division2"), ("Division3"), ("Division4");
CREATE TABLE product (
product_id INT AUTO_INCREMENT PRIMARY KEY,
product VARCHAR(30) NOT NULL,
divisionID INT
) ENGINE=INNODB;
INSERT INTO product (product, divisionID) VALUES ("Product1", 3), ("Product1", 1), ("Product2", 2), ("Product3", 4);
I have the following query:
SELECT Concat(product,' ',division) as 'product'
FROM products p
LEFT JOIN division d ON d.id = p.divisionID
ORDER BY product;
Above query pulls the following records:
Product
------------------
Product1 DIvision1
Product1 Division3
Product2 Division2
Product3 Division4
Q: How can I modify the query so that only the duplicate Products would be concatenated with their Division, and unique products would be left as is, like below example?
Product
------------------
Product1 Division1
Product1 Division3
Product2
Product3
Here's a very literal solution...
SELECT CONCAT_WS(' ',p.product,CASE WHEN total > 1 THEN d.division ELSE NULL END) name
FROM product p
LEFT
JOIN division d
ON d.id = p.divisionid
JOIN
( SELECT product
, COUNT(*) total
FROM product
GROUP
BY product
) x
ON x.product = p.product
ORDER
BY p.product,d.division;

mysql search keywords in string table

I have a catID column in product table, it is contain category ids as string,
Something like that '142,156,146,143'
and i Have a query '?catID=156,141,120'
i want to search each id in catID column.
I use this query:
SELECT * FROM product WHERE catID REGEXP '156|141|120'
this code return products which have any id in catID column , but I want to return products which is have all id,
So , I'am looking for and operator in REGEXP , but I'am couldn't find.
I want to use REGEXP or something like that which function provide to find product with one query , I don't wan to use
catID LIKE '156' AND catID LIKE '141' ....
if it is posibble.
EDIT : I don't want to perform a function one more time , because the query can be have 100+ id so it's make more harder to write code,
You need to use find_in_set() for each category id parameter in order to find the values in set,also if you can alter the schema then do normalize it, by having another junction table which holds the relation from this table to category table
select * from
product
where
find_in_set('142',catID ) > 0
For multiple values like find_in_set('161,168,234,678',preferred_location ) > 0 no it can't be possible doing like this you have to perform for each location id like
select * from
product
where
find_in_set('142',catID ) > 0
and find_in_set('156',catID ) > 0
and find_in_set('146',catID ) > 0
and find_in_set('143',catID ) > 0 ... for more
Database normalization
find_in_set
Sample Schema
Table
Products (id,other columns)
Categories (id,other columns)
Product_categories (id,product_id,category_id)
Product_categories is a junction table which will hold product_id and one category_id per each product so each will have a relation with single category and single product at a time
For example
Products
id name
1 product 1
2 product 2
Categories
id name
142 category 1
156 category 2
146 category 3
143 category 4
Product_categories
id product_id category_id
1 1 142
2 1 156
3 1 146
4 1 143
Now you can join these tables and query like below using in() and count should be equal to the no of category ids provided as parameter
select p.* from
Products p
join Product_categories pc on (p.id = pc.product_id)
where pc.category_id in(142,156,146,143)
group by p.id
having count(distinct pc.category_id) = 4
Sample Demo
or if you can't count the provided category ids as parameter you can do this by following query
select p.* from
Products p
join Product_categories pc on (p.id = pc.product_id)
where pc.category_id in(142,156,146,143)
group by p.id
having count(distinct pc.category_id) =
ROUND (
(
LENGTH('142,156,146,143')
- LENGTH( REPLACE ( '142,156,146,143', ",", "") )
) / LENGTH(",")
) + 1
Sample Demo 2