I have tables books and bookType which pose a 1 X n relationship.
books
+-----+------------------+----------+-------+
| id | title | bookType | price |
+-----+------------------+----------+-------+
| 1 | Wizard of Oz | 3 | 14 |
| 2 | Huckleberry Finn | 1 | 16 |
| 3 | Harry Potter | 2 | 25 |
| 4 | Moby Dick | 2 | 11 |
+-----+------------------+----------+-------+
bookTypes
+-----+----------+
| id | name |
+-----+----------+
| 1 | Fiction |
| 2 | Drama |
| 3 | Children |
+-----+----------+
How would I retrieve bookTypes where all books are more expensive than e.g. 12($)?
In this case, the expected output would be:
+-----+----------+
| id | name |
+-----+----------+
| 1 | Fiction |
| 3 | Children |
+-----+----------+
You can use not exists:
select t.*
from bookTypes t
where not exists (
select 1
from books b
where b.bookType = t.id and b.price < 12
)
If you want to select book types that also have at least one associated book:
select t.*
from bookTypes t
where
exists (select 1 from books b where b.bookType = t.id)
and not exists (select 1 from books b where b.bookType = t.id and b.price < 12)
Do a GROUP BY, use HAVING to return only booktypes having the lowest price > 12.
SELECT bt.name
FROM bookTypes bt
INNER JOIN books b ON b.bookType = bt.id
group by bt.name
HAVING SUM(b.price <= 12) = 0;
You can directly consider using having min(price) >= 12 with grouping by bookType
select t.id, t.name
from bookTypes t
join books b
on t.id = b.bookType
group by b.bookType
having min(price) >= 12
Moreover, if your DB's version is at least 10.2, then you can also use some window functions for analytical queries such as min(..) over (partition by .. order by ..) :
with t as
(
select t.id, t.name, min(price) over (partition by bookType) as price
from bookTypes t
join books b
on t.id = b.bookType
)
select id, name
from t
where price >= 12
in which min() over (..) window function determines minimum price for each booktype by use of partition by bookType
Demo
I think GMB's solution is likely the best so far. But for sake of completeness: You can also use the ALL operator with a correlated subquery. That's probably the most straight forward solution.
SELECT *
FROM booktypes bt
WHERE 12 < ALL (SELECT b.price
FROM books b
WHERE b.booktype = bt.id);
Can you not just select from books inner join bookTypes on id WHERE price > 12?
SELECT bt.*
FROM bookTypes bt
INNER JOIN books b ON b.bookType = bt.id
WHERE b.price > 12
It's possible left join only one row without sub query?
I need to get product statistics and some of products have multiple groups.
Therefore, the amount of products is incorrect.
SELECT COUNT(p.id) AS total_product, SUM(p.price) AS total_price
FROM product p
LEFT JOIN attribute_group a ON
a.product_id = p.id
WHERE p.created_at >= "2018-01-01" AND (a.id = 1 OR a.id = 2)
GROUP BY p.id
LIMIT 0, 30;
product
id | price
1 | 100
2 | 150
3 | 250
attribute_group
id | product_id | title
1 | 1 | a1
2 | 1 | a2
3 | 2 | a3
4 | 3 | a4
Should be:
1| 100
But i get:
2 | 200
You appear to want all products or the counts/sum of them that have attributes of both 1 and 2. Here is one method:
SELECT COUNT(*) as num_products, SUM(p.price) as total_price
FROM product p
WHERE p.created_at >= '2018-01-01' AND
EXISTS (SELECT 1
FROM attribute_group ag
WHERE ag.product_id = p.id AND ag.id = 1
) AND
EXISTS (SELECT 1
FROM attribute_group ag
WHERE ag.product_id = p.id AND ag.id = 2
);
Here my table structure:
___Lang:
|--------|------------|
| LAN_Id | LAN_En |
|--------|------------|
| DI | Direct |
| WE | Web |
| OT | Other |
|--------|------------|
___Segmentations:
|--------|------------|
| SEG_Id | SEG_Code |
|--------|------------|
| 1 | DI |
| 2 | WE |
| 3 | OT |
|--------|------------|
___Bookings:
|--------|------------------|
| BOO_Id | BOO_Segmentation |
|--------|------------------|
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
|--------|------------------|
___BillableDatas:
|--------|---------------|------------|------------|
| BIL_Id | BIL_BookingId | BIL_Date | BIL_Item |
|--------|---------------|------------|------------|
| 1 | 1 | 2017-02-21 | Night |
| 2 | 1 | 2017-02-22 | Night |
| 3 | 1 | 2017-02-23 | Night |
| 4 | 1 | 2017-02-24 | Night |
| 5 | 2 | 2017-02-25 | Night |
| 6 | 2 | 2017-02-26 | Night |
| 7 | 3 | 2017-02-28 | Night |
| 8 | 3 | 2017-03-01 | Night |
| 9 | 3 | 2017-03-02 | Night |
| 10 | 3 | 2017-03-03 | Night |
|--------|---------------|------------|------------|
I would like to know the most popular segmentation for a range of date.
The desired result should be this one for the following date range :
Form 2017-02-01 to 2017-02-28 inclusive
|------------|------------|------------|--------------|------------|
| ROO_Name | Night_Nb | Percentage | Booking_Nb | Percentage |
|------------|------------|------------|--------------|------------|
| Direct | 6 | 85.71 | 2 | 66.66 |
| Website | 1 | 14.28 | 1 | 33.33 |
| Other | 0 | 0 | 0 | 0 |
|------------|------------|------------|--------------|------------|
What I already tried:
SELECT r.SEG_Id
, Sum(CASE WHEN BOO_Id IS NULL THEN 0 ELSE 1 END) Night_Nb
, Concat(
Format(
Sum(CASE WHEN BOO_Id IS NULL THEN 0 ELSE 1 END)
/ TotalBookings
* 100
, 0) ) AS PercentageTotal
FROM ( ___Segmentations r LEFT JOIN ___Bookings b ON r.SEG_Id = b.BOO_Segmentation
) INNER JOIN (SELECT BOO_HotelId
, Count(*) AS TotalBookings
FROM ___Bookings
GROUP BY BOO_HotelId
) AS TotalHotelBookings
ON r.SEG_HotelId = TotalHotelBookings.BOO_HotelId
WHERE r.SEG_HotelId = :hotel_id
GROUP BY r.SEG_Id
ORDER BY NumBookings DESC
But it doesn't work actually.
Could anyone help me with this please ?
You could use the SQL Fiddle:
http://sqlfiddle.com/#!9/1aa10a
I suggest we build the query incrementally, step by step. Verify that the query results are as we expect at each step. When something "doesn't work", backup a step.
We want to return three rows, one for each row in ___Segmentations, for a specific hotelid
SELECT r.seg_id
, r.seg_text
FROM ___Segmentations r
WHERE r.seg_hotelid = :hotel_id
ORDER BY r.seg_id
Add the outer join to __Bookings
SELECT r.seg_id
, r.seg_text
, b.boo_id
FROM ___Segmentations r
LEFT
JOIN ___Bookings b
ON b.boo_segmentation = r.seg_id
WHERE r.seg_hotelid = :hotel_id
ORDER
BY r.seg_id
, b.boo_id
Add the outer join to ___BillableDatas
SELECT r.seg_id
, r.seg_text
, b.boo_id
, d.bil_id
FROM ___Segmentations r
LEFT
JOIN ___Bookings b
ON b.boo_segmentation = r.seg_id
LEFT
JOIN `___BillableDatas` d
ON d.bil_bookingid = b.boo_id
WHERE r.seg_hotelid = :hotel_id
ORDER
BY r.seg_id
, b.boo_id
, d.bil_id
If that's the rows we're interested in, we can work on aggregation.
SELECT r.seg_id
, r.seg_text
, COUNT(DISTINCT b.boo_id) AS cnt_bookings
, COUNT(DISTINCT d.bil_id) AS cnt_billable
FROM ___Segmentations r
LEFT
JOIN ___Bookings b
ON b.boo_segmentation = r.seg_id
LEFT
JOIN `___BillableDatas` d
ON d.bil_bookingid = b.boo_id
WHERE r.seg_hotelid = :hotel_id
GROUP
BY r.seg_id
, r.seg_text
ORDER
BY r.seg_text
Now to get the aggregation with the "total".
The approach I would take would be to make "copies" of the rows, using a CROSS JOIN operation. We can do the join to the rows returned by the very first query we wrote, referenced as an inline view. (Aliased as q below.)
If we have a complete set of rows, repeated for each seg_id/seg_text (that first query we wrote), we can use conditional aggregation.
That last query we wrote (just above) is an inline view in the query below, aliased as c.
SUM of cnt_bookings from all the rows is the total.
For the individual counts, we can include only the rows that have a matching seg_id, a total of that subset.
SELECT q.seg_id
, q.seg_text
, SUM(IF(c.seg_id=q.seg_id,c.cnt_bookings,0)) AS cnt_bookings
, SUM(c.cnt_bookings) AS tot_bookings
, SUM(IF(c.seg_id=q.seg_id,c.cnt_billable,0)) AS cnt_billable
, SUM(c.cnt_billable) AS tot_billable
FROM ( SELECT t.seg_id
, t.seg_text
FROM ___Segmentations t
WHERE t.seg_hotelid = :hotel_id_1
ORDER BY t.seg_id
) q
CROSS
JOIN ( SELECT r.seg_id
, COUNT(DISTINCT b.boo_id) AS cnt_bookings
, COUNT(DISTINCT d.bil_id) AS cnt_billable
FROM ___Segmentations r
LEFT
JOIN ___Bookings b
ON b.boo_segmentation = r.seg_id
LEFT
JOIN `___BillableDatas` d
ON d.bil_bookingid = b.boo_id
WHERE r.seg_hotelid = :hotel_id
GROUP
BY r.seg_id
) c
GROUP
BY q.seg_id
, q.seg_text
ORDER
BY q.seg_text
In the SELECT list, we can do the division to get the percentage: cnt_bookings * 100.0 / tot_bookings
e.g.
SELECT q.seg_id
, q.seg_text
, SUM(IF(c.seg_id=q.seg_id,c.cnt_bookings,0)) AS cnt_bookings
, SUM(c.cnt_bookings) AS tot_bookings
, SUM(IF(c.seg_id=q.seg_id,c.cnt_bookings,0))
* 100.0 / SUM(c.cnt_bookings) AS pct_bookings
, SUM(IF(c.seg_id=q.seg_id,c.cnt_billable,0)) AS cnt_billable
, SUM(c.cnt_billable) AS tot_billable
, SUM(IF(c.seg_id=q.seg_id,c.cnt_billable,0))
* 100.0 / SUM(c.cnt_billable) AS pct_billable
Modify the ORDER BY clause to return the rows in the order you want
Remove from the SELECT list the expressions that return tot_bookings and tot_billable.
EDIT
I think I missed the date critera. We can make the outer joins into inner joins, and replace the CROSS JOIN with a LEFT JOIN. We have potential to return NULL values for cnt_bookings and cnt_billable, we can wrap those in IFNULL() or COALESCE() function to replace NULL with zero.
SELECT q.seg_id
, q.seg_text
, SUM(IF(c.seg_id=q.seg_id,c.cnt_bookings,0)) AS cnt_bookings
, SUM(c.cnt_bookings) AS tot_bookings
, SUM(IF(c.seg_id=q.seg_id,c.cnt_bookings,0))
* 100.0 / SUM(c.cnt_bookings) AS pct_bookings
, SUM(IF(c.seg_id=q.seg_id,c.cnt_billable,0)) AS cnt_billable
, SUM(c.cnt_billable) AS tot_billable
, SUM(IF(c.seg_id=q.seg_id,c.cnt_billable,0))
* 100.0 / SUM(c.cnt_billable) AS pct_billable
FROM ( SELECT t.seg_id
, t.seg_text
FROM ___Segmentations t
WHERE t.seg_hotelid = :hotel_id_1
ORDER BY t.seg_id
) q
LEFT
JOIN ( SELECT r.seg_id
, COUNT(DISTINCT b.boo_id) AS cnt_bookings
, COUNT(DISTINCT d.bil_id) AS cnt_billable
FROM ___Segmentations r
JOIN ___Bookings b
ON b.boo_segmentation = r.seg_id
JOIN `___BillableDatas` d
ON d.bil_bookingid = b.boo_id
AND d.bil_date BETWEEN '2017-02-21' AND '2017-02-28'
WHERE r.seg_hotelid = :hotel_id
GROUP
BY r.seg_id
) c
ON 1=1
GROUP
BY q.seg_id
, q.seg_text
ORDER
BY q.seg_text
Not sure 100% how the hotelId column comes into play here, you didn't describe it in the question, but try this:
SELECT aaa.SEG_Text, aaa.Night_NB, aaa.Night_NB / totals.total_nights * 100, aaa.Booking_Nb , aaa.Booking_Nb / totals.total_bookings * 100 FROM (
SELECT s.SEG_Text, COUNT(DISTINCT d.BIL_Id) AS `Night_Nb`, COUNT(DISTINCT b.BOO_Id) AS `Booking_Nb` FROM ___Segmentations s
LEFT JOIN ___Bookings b ON s.SEG_Id = b.BOO_Segmentation
LEFT JOIN ___BillableDatas d ON d.BIL_BookingId = b.BOO_Id AND d.BIL_Date BETWEEN '2017-02-01' AND '2017-02-28'
GROUP BY s.SEG_Id ) AS `aaa`
, ( SELECT COUNT(*) AS `total_nights`, COUNT(DISTINCT BIL_BookingId) `total_bookings` FROM ___BillableDatas WHERE BIL_Date BETWEEN '2017-02-01' AND '2017-02-28') AS totals
It basically does the same stuff you did, but uses SELECT(DISTINCT ...) and thus is easier to understand, debug, and I think will also run faster.
For me it returns correct results.
The challenge here seems to be to avoid doing almost the same query twice (repeating the date condition) in order to calculate the two percentages.
You could use the with rollup modifier to generate the total counts which you need to calculate those percentages. You could then capture these totals in variables, and use them in a wrapping query as divisors. Finally, the outer query's where clause would eliminate the rollup record, as it has served its purpose:
select seg_text
, night_nb
, 100*night_nb/#sum_night_nb as night_pct
, booking_nb
, 100*booking_nb/#sum_booking_nb as booking_pct
from (
select seg_text
, #sum_night_nb := count(bil_id) night_nb
, #sum_booking_nb := count(distinct bil_bookingid) booking_nb
from ___segmentations seg
left join (___bookings boo
inner join ___billabledatas bil
on bil_bookingid = boo_id
and bil_hotelid = boo_hotelid)
on seg_id = boo_segmentation
and seg_hotelid = boo_hotelid
and bil_date between '2017-02-01' and '2017-02-28'
where seg_hotelid = 'AAA00'
group by seg_text with rollup
) base
where seg_text is not null
order by night_nb desc
See this sqlfiddle
Here is the scenario
Table income has following column
ID Inc_amt Trans_date
1 100 9/24/2014
2 200 9/24/2014
3 300 9/25/2015
Table expense has following column
ID exp_amt Trans_date
2 100 9/24/2014
3 200 9/24/2014
4 400 9/25/2014
What I need is that for say trans_date = 9/24/2014, I need the following output. Here ID is the common key for both tables
ID inc_amt exp_amt trans_date
1 100 null 9/24/2014
2 200 100 9/24/2014
3 null 200 9/24/2014
I am confused as to what join to do . I am able to get both matched and mismatched rows , but when I add date check condition , then it gives only matching rows for that date of 9/24/2014
you need to left join the two tables to a base table that has the id's from both tables.
when you left join a table it will join to any existing id's so you need to union the two tables to get all id's then join to them, then filter by date.
so something like this
Query:
SELECT t.id, i.inc_amt, e.exp_amt, t.trans_date
FROM
( SELECT id, trans_date FROM income
UNION
SELECT id, trans_date FROM expense
) t
LEFT JOIN income i ON i.id = t.id AND i.trans_date = '9/24/2014'
LEFT JOIN expense e ON t.id = e.id AND e.trans_date = '9/24/2014'
WHERE t.trans_date = '9/24/2014';
Output:
+----+---------+---------+------------+
| id | inc_amt | exp_amt | trans_date |
+----+---------+---------+------------+
| 1 | 100 | null| 9/24/2014 |
| 2 | 200 | 100 | 9/24/2014 |
| 3 | null| 200 | 9/24/2014 |
+----+---------+---------+------------+
This can be achieved by union all income|expense left outer join and conditional right outer join.
SELECT inc.id
,inc.inc_amt
,exp.exp_amt
,inc.trans_date
FROM income inc
LEFT OUTER JOIN expense exp ON inc.id = exp.id
WHERE (inc.trans_date = '2014-09-24')
UNION ALL
(
SELECT exp.id
,CASE
WHEN inc.trans_date <> '2014-09-24'
THEN NULL
END
,exp.exp_amt
,exp.trans_date
FROM income inc
RIGHT OUTER JOIN expense exp ON inc.id = exp.id
WHERE (
exp.trans_date = '2014-09-24'
AND inc.trans_date <> '2014-09-24'
)
)
ORDER BY id;
link to sqlfiddle
I have such query that gives me results about bestseller items from shops, at the moment it works fine, but now I want to get only one product from each shop so to have a distinct si.shop_id only one bestseller product from a shop
SELECT `si`.`id`, si.shop_id,
(SELECT COUNT(*)
FROM `transaction_item` AS `tis`
JOIN `transaction` as `t`
ON `t`.`id` = `tis`.`transaction_id`
WHERE `tis`.`shop_item_id` = `si`.`id`
AND `t`.`added_date` >= '2014-02-26 00:00:00')
AS `count`
FROM `shop_item` AS `si`
INNER JOIN `transaction_item` AS `ti`
ON ti.shop_item_id = si.id
GROUP BY `si`.`id`
ORDER BY `count` DESC LIMIT 7
and that gives mu a result like:
+--------+---------+-------+
| id | shop_id | count |
+--------+---------+-------+
| 425030 | 38027 | 111 |
| 291974 | 5368 | 20 |
| 425033 | 38027 | 18 |
| 291975 | 5368 | 12 |
| 142776 | 5368 | 10 |
| 397016 | 38027 | 9 |
| 291881 | 5368 | 8 |
+--------+---------+-------+
any ideas?
EDIT
so I created a fiddle for it
http://sqlfiddle.com/#!2/cfc4c/1
Now the query returns best selling products I want it to return only one product from shopso the result of fiddle should be
+----+---------+-------+
| ID | SHOP_ID | COUNT |
+----+---------+-------+
| 1 | 222 | 3 |
| 4 | 333 | 2 |
| 8 | 555 | 1 |
| 9 | 777 | 1 |
+----+---------+-------+
Possibly something like this:-
SELECT si.shop_id,
SUBSTRING_INDEX(GROUP_CONCAT(CONCAT_WS(':', si.id, sub1.item_count) ORDER BY sub1.item_count DESC), ',', 1) AS `count`
FROM shop_item AS si
INNER JOIN
(
SELECT tis.shop_item_id, COUNT(*) AS item_count
FROM transaction_item AS tis
JOIN `transaction` as t
ON t.id = tis.transaction_id
AND t.added_date >= '2014-02-26 00:00:00'
GROUP BY tis.shop_item_id
) sub1
ON sub1.shop_item_id = si.id
GROUP BY si.shop_id
ORDER BY `count` DESC LIMIT 7
The sub query gets the count of items for each shop. Then the main query concatenates the item id and the item count together, group concatenates all those for a single shop together (ordered by the count descending) and then uses SUBSTRING_INDEX to grab the first one (ie, everything before the first comma).
You will have to split up the count field to get the item id and count separately (the separator is a : ).
This is taking a few guesses about what you really want, and with no table declares or data it isn't tested.
EDIT - now tested with the SQL fiddle example:-
SELECT SUBSTRING_INDEX(`count`, ':', 1) AS ID,
shop_id,
SUBSTRING_INDEX(`count`, ':', -1) AS `count`
FROM
(
SELECT si.shop_id,
SUBSTRING_INDEX(GROUP_CONCAT(CONCAT_WS(':', si.id, sub1.item_count) ORDER BY sub1.item_count DESC), ',', 1) AS `count`
FROM shop_item AS si
INNER JOIN transaction_item AS ti
ON ti.shop_item_id = si.id
INNER JOIN
(
SELECT tis.shop_item_id, COUNT(*) AS item_count
FROM transaction_item AS tis
JOIN `transaction` as t
ON t.id = tis.transaction_id
AND t.added_date >= '2014-02-26 00:00:00'
GROUP BY tis.shop_item_id
) sub1
ON sub1.shop_item_id = si.id
GROUP BY si.shop_id
) sub2
ORDER BY `count` DESC LIMIT 7;