I have the following tables, and values:
t_cars
---------------------------------------------------------
nCars_ID sName sModel sIdentifier
---------------------------------------------------------
1 BMW 3 series D-78-JHG
2 Volvo C30 B-56-KHT
3 Fiat Doblo H-72-ABN
4 Volvo C40 J-78-YTR
t_feature
---------------------------
nFeature_ID sName
---------------------------
1 CMC
2 Doors
3 Color
4 Type
5 Weight
6 Engine
7 Power
t_cars_feature
-------------------------------------------------------------------
nCarsFeature_ID nCars_ID nFeature_ID sValue
------------------------------------------------------------------
1 2 1 2500
2 2 2 5
3 2 4 Diesel
4 2 3 Green
5 3 1 1900
6 3 2 3
7 3 4 Otto
8 3 5 2300 KG
9 1 1 1900
10 1 3 Blue
11 1 4 Diesel
12 1 5 2100 KG
I need to retrieve from DB the cars that has CMC feature, has Color feature, AND CMC = 1900 AND Color = 'Blue' ONLY
I have tried:
SELECT t_cars.sName, t_cars.sModel, t_cars.sIdentifier
FROM t_cars, t_feature, t_cars_feature
WHERE t_feature.nFeature_ID = t_cars_feature.nFeature_ID
AND t_cars.nCars_ID = t_cars_feature.nCars_ID
AND [/*condition that get me cars that has CMC feature, has Color feature, AND CMC = 1900 AND Color = 'Blue' ONLY*/]
I have tried the condition like that:
Trial 1:
AND t_feature.sName = 'CMC'
AND t_feature.sName = 'Color'
AND t_cars_feature.sValue = '1900'
AND t_cars_feature.sValue = 'Blue'
and get me nothing
I have also tried:
Trial 2:
AND t_feature.sName IN ('CMC','Color')
AND t_cars_feature.sValue IN ('1900','Blue')
and get me all records that has CMC 1900 OR color 'Blue' (probably I got here an cartesian product)
In real situation I could have several t_feature.sName values, and several t_cars_feature.sValue values, that is why trial 1 are not suitable for me ...
Can you help me ? Thank you.
Use this query:
SELECT *
FROM t_cars
WHERE EXISTS (SELECT * FROM t_cars_feature AS cf
JOIN t_feature AS f ON (f.nFeature_ID = cf.nFeature_ID)
WHERE t_cars.nCars_ID = cf.nCars_ID AND f.sName = 'CMC' AND cf.sValue = '1900')
AND
EXISTS (SELECT * FROM t_cars_feature AS cf
JOIN t_feature AS f ON (f.nFeature_ID = cf.nFeature_ID)
WHERE t_cars.nCars_ID = cf.nCars_ID AND f.sName = 'Color' AND cf.sValue = 'Blue');
See the full example here.
I would like to comment about some of the things you've done there:
It is generally considered a bad practice to notate the type as a prefix in the column name, or in the object's name.
Your usage of the IN clause is wrong. I suggest you to try and practice about this matter a bit. When you supply a list of values inside the IN clause, it means that any of those values should result in a true value for the predicate - not that both of the them should exists.
Please note that when you use the WHERE clause with multiple predicates separated by an "AND", you require all of them to exists in a SINGLE row. Check out the result of your JOIN and see if this is the case to understand the case better.
It is considered a better practice to use a JOIN (i.e. INNER JOIN) and not a cartesian product (,) - even though both queries will usually compile into the same execution plan. It is just more readable and understandable.
You could do it with EXISTS and NOT EXISTS:
--Alias = Table
-- C = t_cars
-- CF = t_cars_feature
-- F = t_feature
SELECT *
FROM t_cars C
WHERE EXISTS (SELECT *
FROM t_cars_feature CF INNER JOIN t_feature F ON CF.nFeature_ID = F.nFeature_ID
WHERE CF.nCars_ID = C.nCars_ID --matches t_cars ID
AND F.sName = 'CMC' --has CMC feature
AND F.sName = 'Color' --has color feature
AND CF.sValue = '1900' --CMC = 1900
AND CF.sValue = 'Blue' --Color = 'Blue'
)
AND NOT EXISTS
(SELECT *
FROM t_cars_feature CF INNER JOIN t_feature F ON CF.nFeature_ID = F.nFeature_ID
WHERE CF.nCars_ID = C.nCars_ID --matches t_cars ID
AND F.sName = 'CMC' --has CMC feature
AND F.sName = 'Color' --has color feature
AND CF.sValue = '1900' --CMC = 1900
AND CF.sValue <> 'Blue' --Color = 'Blue ONLY'
)
You can make some use of conditional aggregation to get the desired results
SELECT t_cars.sName, t_cars.sModel, t_cars.sIdentifier
FROM t_cars
JOIN t_cars_feature ON t_cars.nCars_ID = t_cars_feature.nCars_ID
JOIN t_feature ON t_feature.nFeature_ID = t_cars_feature.nFeature_ID
WHERE t_feature.sName IN ('CMC','Color')
GROUP BY t_cars.sName, t_cars.sModel, t_cars.sIdentifier
HAVING SUM(CASE WHEN t_feature.sName ='CMC' AND t_cars_feature.sValue ='1900' THEN 1 ELSE 0 END) > 0
AND SUM(CASE WHEN t_feature.sName ='Color' AND t_cars_feature.sValue ='Blue' THEN 1 ELSE 0 END) >0
Demo
Related
I have a pretty straightforward sql query that I'm running on DB2 I-series 4 which is just performing some simple joins. The issue is that my materials table has it so that multiple material/color combos can be used on each body of work. Right now I'm getting rows of every combination but I want to consolidate so that if the value of test.materials.sequence is 2 then it creates two additional columns (if there's no sequence 2 for the combination then it would just be 0 and 0 in those columns.
THe query:
Select bod.code, mat.material, mat.mat_color,
from test.skus sk
inner join test.Bodies bod on sk.body_id = bod.id
inner join test.categories prc on prc.id = sk.category_id
inner join test.skus_to_materials stm on sk.id = stm.sku_id
inner join test.materials mat on stm.mat_id = mat.id
order by prc.desc;
Tables:
skus
id | code | body_id | category_id
-------------------------------------------
1 12345 9912 3
2. 12346 9913 3
Bodies
id | code
--------------------------
9912 1234-5
9913 1234-6
categories
id | category
------------------
3 Test
skus_to_materials
id | sku_id | mat_id | sequence
--------------------------------------
1 1 221 1
2 1 222 2
3 2 223 1
materials
id | material | mat_color
-------------------------------
221 Fabric black
222 Fabric white
223 Leather brown
This is my current result:
code | material | mat_color
-------------------------
1234-5 | Fabric | black
1234-5 | Fabric | white
This is the result I would want:
code | material1 | mat_color1 | material2 | mat_color2
----------------------------------------------------------
1234-5 Fabric black Fabric white
1234-6 Leather brown 0 0
See how 1234-6 only has the one material combination, so material2 and mat_color2 would be zeroes.
Is there a way through grouping and row-operations to achieve this?
UPDATE:
In answer to Charles' answer, I've realized some issues in one case with more data. After grouping by material, color, and desc I realized that I was getting the missing records but now ending up with this issue:
code | material1 | color1 | material2 | color2
------------------------------------------------------------
1234-5 Fabric White 0 0
1234-5 0 0 Leather white
1234-5 Leather Brown 0 0
1234-5 Leather Tan 0 0
1234-6 Fabric Black 0 0
1234-6 0 0 Leather Black
1234-7 Fabric White 0 0
I think Charles first answer is closer. Here is what I got:
SELECT SK.ID SKU
,BOD.CODE
,MAT.MATERIAL MATERIAL_1
,MAT.MAT_COLOR MATERIAL_2
,COALESCE(MAT2.MATERIAL, '0') MATERIAL_2
,COALESCE(MAT2.MAT_COLOR, '0') COLOR_2
FROM SKUS SK
INNER JOIN BODIES BOD ON SK.BODY_ID = BOD.ID
INNER JOIN CATEGORIES PRC ON PRC.ID = SK.CATEGORY_ID
INNER JOIN SKUS_TO_MATERIALS STM ON SK.ID = STM.SKU_ID AND STM.SEQUENCE = 1
INNER JOIN MATERIALS MAT ON STM.MAT_ID = MAT.ID
LEFT JOIN SKUS_TO_MATERIALS STM2 ON SK.ID = STM2.SKU_ID AND STM2.SEQUENCE = 2
LEFT JOIN MATERIALS MAT2 ON STM2.MAT_ID = MAT2.ID;
Resulting in:
SKU
CODE
MATERIAL_1
MATERIAL_2
MATERIAL_2
COLOR_2
6
BodD
Fabric
Black
0
0
4
BodB
Fabric
Black
Leather
Black
3
BodA
Fabric
Black
0
0
2
BodA
Fabric
Black
Leather
Black
1
BodA
Fabric
Black
0
0
1
BodA
Fabric
White
0
0
5
BodC
Leather
Brown
0
0
1
BodA
Leather
Brown
0
0
1
BodA
Leather
Black
0
0
View on DB Fiddle
"rows to columns" is known as "pivoting" the data.
Db2 for IBM i doesn't have a built method to pivot. However, assuming a distinct set of values, you can hardcode it.
Something like so should do what you want:
Select bod.code, max(mat1.material), max(mat1.mat_color),
max(mat2.material), max(mat2.mat_color)
from test.skus sk
inner join test.Bodies bod on sk.body_id = bod.id
inner join test.categories prc on prc.id = sk.category_id
inner join test.skus_to_materials stm on sk.id = stm.sku_id
inner join test.materials mat1 on stm.mat_id = mat1.id
and mat1.sequence = 1
left outer join test.materials mat2 on stm.mat_id = mat2.id
and mat2.sequence = 2
group by bod.code
order by prc.desc;
EDIT
Ok if sequence is actually in the skus_to_materials then you'll need a different set of joins. Actually in that case, I'd probably just go with a CTE based statement.
with allrows as (
Select bod.code, prc.desc, stm.sequence
, mat.material, mat.mat_color,
from test.skus sk
inner join test.Bodies bod on sk.body_id = bod.id
inner join test.categories prc on prc.id = sk.category_id
inner join test.skus_to_materials stm on sk.id = stm.sku_id
inner join test.materials mat on stm.mat_id = mat.id
}
select
code
, max(case when sequence = 1 then material else '0' end) as material1
, max(case when sequence = 1 then color else '0' end) as color1
, max(case when sequence = 2 then material else '0' end) as material2
, max(case when sequence = 2 then color else '0' end) as color2
from allRows
group by code
order by desc;
i have 2 tables like below:
lk_premier:
code descr
P Premier
N Non Premier
Case:
id taxPin
1 123
2 789
Status:
id voting_status premier
1 5 P
1 5 P
1 5 P
2 5 P
2 5 N
2 5 null
3 5 null
3 5 null
3 5 null
3 5 null
3 5 null
I used the below sql
select
decode(premier,
'P',
'PREMIER',
'N',
'NON PREMIER') as caseStatus,
count(*) as count
from
status s,
case c
where
c.id = s.id
and c.id = 1
and s.voting_status = 5
group by
premier
I want to join the lk_premier table, so my output looks like for id=2,
caseStatus count
PREMIER 1
NON PREMIER 1
for id = 3
caseStatus count
PREMIER 0
NON PREMIER 0
You should never do joins this way -- this way of doing joins was depreciated about 20 years ago. the join should look like this:
join status s on c.id = s.id
once you do joins the right way then how you do left joins is easier as seen below:
here you go
select
coalesce(p.desc,'unknown') as caseStatus,
count(*) as count
from case c
join status s on c.id = s.id
left join lk_premier p on p.code = c.premier
where c.id = 1 and s.voting_status = 5
group by coalesce(p.desc,'unknown')
Let's say we have a books website with more than 100.000 books and 1.000.000 rows of tags.
Users will frequently search for books that have some tags they like and at the same time don't have tags that they don't like.
What will be a best way to do search for frequent users requests?
Let's say user want books with tags 15 and 25 (book should have 2 tags, not any of) and user don't wan't books with tags 50 and 99 and ordered by rating. For common sense we will LIMIT results to 5 and use OFFSET for more.
books:
id | rating
1 | 5
2 | 5
3 | 1
tags_books:
book_id | tag_id
1 | 15
1 | 25
1 | 50
2 | 15
2 | 25
P.S. One of solutions was to do request with having sum, but for big table with frequent requests it will be slow as i understand it:
select b.id from books b
left join tags_books tb on tb.book_id = b.id
group by b.id
having sum(case when tb.tag_id in (1,2,3) then 1 else 0 end) >= 2
and sum(case when tb.tag_id in (11,12,13) then 1 else 0 end) = 0
ORDER BY b.rating LIMIT 5 OFFSET 0
For this purpose, I would recommend exists and not exists`:
selet b.*
from books b
where exists (select 1 from tags_books tb where tb.book_id = b.id and tb.tag_id = 15
) and
exists (select 1 from tags_books tb where tb.book_id = b.id and tb.tag_id = 25
) and
not exists (select 1 from tags_books tb where tb.book_id = b.id and tb.tag_id in (50, 99)
) ;
For performance, you want an index on tags_books(book_id, tag_id).
If you phrase this as aggregation, I would recommend:
select bt.book_id
from book_tags bt
where bt.tag_id in (15, 25, 50, 99)
group by bt.book_id
having count(*) filter (where bt.tag_id in (15, 25)) = 2 and
count(*) filter (where bt.tag_id in (50, 99)) = 0;
I have 4 tables:
Table1(Groups):
id_group name_group
1 abc
2 def
3 ghi
Table2(Color):
id_color name_color
1 blue
2 red
3 green
Table3(Variety):
id_variety id_color id_group name_variety
1 1 1 light
2 3 1 dark
3 6 2 dark
Table4(Tone):
id_tone id_color id_group name_tone
1 1 1 ocean
2 5 1 sea
3 9 3 clay
Given an id_group like 1 I want to make a selection resulting something like this:
id_group id_color name_variety name_tone
1 1 light ocean
1 3 dark NULL
1 5 NULL sea
I was able to solve the problem with an union and changing the order of the JOINS but I think that there must be another solution
SELECT
GRO.id_group, COL.id_color, VARI.name_variety, TONE.name_tone
FROM
groups GRO
LEFT OUTER JOIN
variety VARI ON (VARI.id_group = GRO.id_group)
LEFT OUTER JOIN
tone TONE ON (TONE.id_group = GRO.id_group)
AND (TONE.id_color = VARI.id_color)
LEFT OUTER JOIN
color COL ON (COL.id_color = VARI.id_color)
OR (COL.id_color = TONE.id_color)
WHERE
GRO.id_group = 1
UNION
SELECT
GRO.id_group, COL.id_color, VARI.name_variety, TONE.name_tone
FROM
groups GRO
LEFT OUTER JOIN
tone TONE ON (TONE.id_group = GRO.id_group)
LEFT OUTER JOIN
variety VARI ON (VARI.id_group = GRO.id_group)
AND (VARI .id_color = TONE.id_color)
LEFT OUTER JOIN
color COL ON (COL.id_color = VARI.id_color)
OR (COL.id_color = TONE.id_color)
WHERE
GRO.id_group = 1
I think what you want is something more like:
SELECT
id_group,
id_color,
MAX(max_variety),
MAX(max_tone)
FROM
(SELECT
v.id_group,
v.id_color,
MAX(name_variety) AS max_variety,
MAX(name_tone) AS max_tone
FROM
variety V INNER JOIN
color c ON
V.id_color = c.id_color LEFT OUTER JOIN
tone t ON
t.id_group = v.id_group AND
t.id_color = V.id_color
WHERE
v.id_group = 1
GROUP BY
v.id_group,
v.id_color
UNION ALL
SELECT
t.id_group,
t.id_color,
MAX(name_variety) AS max_variety,
MAX(name_tone) AS max_tone
FROM
tone t INNER JOIN
color c ON
t.id_color = c.id_color LEFT OUTER JOIN
variety V ON
v.id_group = t.id_group AND
v.id_color = t.id_color
WHERE
t.id_group = 1
GROUP BY
t.id_group,
t.id_color) u
GROUP BY
id_group,
id_color
instead of using union you could use full join , as full join return all result from first table and also all results from the second tables.
SELECT
GRO.id_group, COL.id_color, VARI.name_variety, Tone.name_tone from Variety VARI
join Color COL on col.id_color=VARI col.id
join Groups GRO on GRO.id_group=VARI.id_group
full join Tone on Tone.id_group=GRO.id_group
where GRO.id_group = 1
I have one table with cars, and another table with fuel types. A third table tracks which cars can use which fuel types. I need to select all data for all cars, including which fuel types they can use:
Car table has Car_ID, Car_Name, etc
Fuel table has Fuel_ID, Fuel_Name
Car_Fuel table has Car_ID, Fuel_ID (one car can have multiple Fuel options)
What I want to return:
SELECT
*
, Can_Use_Gas
, Can_Use_Diesel
, Can_Use_Electric
FROM Car
The Can_Use columns are a BIT value, indicating if the car has a matching Fuel entry in the Car_Fuel table.
I can do this with multiple SELECT statements, but this looks painfully messy (and possibly very inefficient?). I'm hoping there's a better way:
SELECT
c.*
, (SELECT COUNT(*) FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 1) AS Can_Use_Gas
, (SELECT COUNT(*) FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 2) AS Can_Use_Diesel
, (SELECT COUNT(*) FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 3) AS Can_Use_Electric
FROM Car c
Presumably you have no duplicates in Car_fuel, so you don't need aggregation. Hence you can do:
SELECT c.*,
ISNULL((SELECT TOP 1 1 FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 1), 0) AS Can_Use_Gas
ISNULL((SELECT TOP 1 1 FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 2), 0) AS Can_Use_Diesel
ISNULL((SELECT TOP 1 1 FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 3), 0) AS Can_Use_Electric
FROM Car c;
This is one case where ISNULL() has a performance advantage over COALESCE(), because COALESCE() evaluates the first argument twice.
Although not a perfect solution, you could use the pivot clause:
select *
from ( select car_name, fuel_name
from Car
inner join Car_Fuel on Car.car_id = Car_Fuel.car_id
inner join Fuel on Car_Fuel.fuel_id = Fuel.fuel_id
) as data
pivot (
count(fuel_name)
for fuel_name in (Gas, Diesel, Electric)
) as pivot_table;
See this fiddle, which outputs a table like this:
| car_name | Gas | Diesel | Electric |
|----------|-----|--------|----------|
| Jaguar | 0 | 1 | 0 |
| Mercedes | 0 | 1 | 1 |
| Volvo | 1 | 0 | 1 |
The SQL statement still has the hard-coded list in the for clause of the pivot part, but when the number of fuel types increases, this might be easier to manage and have better performance.
Generating the SQL dynamically
If you use an application server, you could first execute this query:
SELECT stuff( ( SELECT ',' + fuel_name
FROM Fuel FOR XML PATH('')
), 1, 1, '') columns
This will return the list of columns as one comma-separated value, for example:
Gas,Diesel,Electric
You would grab that result and inject it in the first query in the FOR clause.
I would suspect using counts would be inefficient as there would be a large number of sub queries running to total all the counts.
Below is an alternative using self joins. It's not as short as your example but may be easier to maintain and read and should be more efficient.
select car.car_id, car.car_name,
-- Select fuel variables
CASE WHEN lpg.fuel_id IS NULL THEN 0 ELSE 1 END AS LPG,
CASE WHEN unleaded.fuel_id IS NULL THEN 0 ELSE 1 END AS Unleaded,
CASE WHEN electric.fuel_id IS NULL THEN 0 ELSE 1 END AS Electric,
CASE WHEN diesel.fuel_id IS NULL THEN 0 ELSE 1 END AS Diesel
FROM car
-- Self Join fuel records
LEFT join car_fuel as lpg on car.car_id = lpg.car_id and lpg.fuel_id = 1
LEFT join car_fuel as unleaded on car.car_id = unleaded.car_id and unleaded.fuel_id = 2
LEFT join car_fuel as electric on car.car_id = electric.car_id and electric.fuel_id = 3
LEFT join car_fuel as diesel on car.car_id = diesel.car_id and diesel.fuel_id = 4
The self join will return a NULL if the car doesn't use that fuel type. The CASE returns 1 if the join found a record for that car/fuel and 0 if it didn't.
I hope this help.
You could use conditional aggregation.
Do an outer join to the Car_Fuel table, and do a GROUP BY Car_ID to collapse the rows.
For each row from Car_Fuel, return a 1 if the Fuel_ID matches the one you are checking for, otherwise return a 0. And use a MAX() aggregate to filter the rows, finding out if any of them returned a 1.
For example:
SELECT c.Car_ID
, c.Car_Name
, MAX(CASE WHEN f.Fuel_ID=1 THEN 1 ELSE 0 END) AS Can_Use_Gas
, MAX(CASE WHEN f.Fuel_ID=2 THEN 1 ELSE 0 END) AS Can_Use_Diesel
, MAX(CASE WHEN f.Fuel_ID=3 THEN 1 ELSE 0 END) AS Can_Use_Electric
FROM Car c
LEFT
JOIN Car_Fuel f
ON f.Car_ID = c.Car_ID
GROUP
BY c.Car_ID
, c.Car_Name
With SQL Server, you'd need to repeat every non-aggregate expression in the SELECT list in the GROUP BY clause. If you add more columns from the Car table to SELECT list, you'll have to copy those down to the GROUP BY.
If that's too painful, you could do the aggregation in an inline view instead, and then do the JOIN. To make sure a NULL doesn't get returned, you can replace a NULL value with a 0, in the outer query:
For example:
SELECT c.Car_ID
, c.Car_Name
, ISNULL(u.Can_Use_Gas,0) AS Can_Use_Gas
, ISNULL(u.Can_Use_Diesel,0) AS Can_Use_Diesel
, ISNULL(u.Can_Use_Electric,0) AS Can_Use_Electric
FROM Car c
LEFT
JOIN ( SELECT f.Car_ID
, MAX(CASE WHEN f.Fuel_ID=1 THEN 1 ELSE 0 END) AS Can_Use_Gas
, MAX(CASE WHEN f.Fuel_ID=2 THEN 1 ELSE 0 END) AS Can_Use_Diesel
, MAX(CASE WHEN f.Fuel_ID=3 THEN 1 ELSE 0 END) AS Can_Use_Electric
FROM Car_Fuel f
GROUP BY f.Car_ID
) u
ON u.Car_ID = c.Car_ID