SELECT by value of dynamically generated field value - mysql

I'm working with a table that includes various items of stock on a small, custom built ecommerce site. My client has requested a number of filters, including price range.
Easy enough except they also require user-dependent pricing, based on various factors in the user's account, so the price is calculated in the query.
The ideal SQL is something like this:
SELECT
SQL_CALC_FOUND_ROWS s.*,
IF(s.value_a <= 0.5, s.price*1.1*0.80, s.price*1.5*0.80) AS total_price
FROM stock s
WHERE
(total_price >= :minimum_price_filter AND total_price <= :maximum_price_filter)
AND (s.value_b='X' OR s.value_b='Y')
ORDER BY total_price ASC, s.value_a, s.value_b LIMIT 0,25
(1.1*0.80 and 1.5*0.80 are the four user-dependent values.)
Obviously that SELECT calculation works fine, as does the ORDER, and the WHERE for the other values in the table: but now adding in this price filter doesn't work as that price field (total_price) and its value is dynamically generated in the query.
Any tips or hints as to how adding that price range filter to the WHERE clause might actually be possible?
WHERE (total_price >= :minimum_price_filter AND total_price <= :maximum_price_filter)
If it makes any different to how the problem is approached, I'm working in PHP and PDO.
Thanks for your time.

Use HAVING:
The HAVING clause was added to SQL because the WHERE keyword could not be used with aggregate functions
SELECT
SQL_CALC_FOUND_ROWS s.*,
IF(s.value_a <= 0.5, s.price*1.1*0.80, s.price*1.5*0.80) AS total_price
FROM
stock s
WHERE
s.value_b='X'
OR s.value_b='Y'
HAVING
total_price <= :maximum_price_filter
AND total_price >= :minimum_price_filter
ORDER BY
total_price ASC,
s.value_a, s.value_b
LIMIT
0,25
This query should do the job.

Related

MySQL - get users who placed 25th order during period

I have users and orders tables with this structure (simplified for question):
USERS
userid
registered(date)
ORDERS
id
date (order placed date)
user_id
I need to get array of users (array of userid) who placed their 25th order during specified period (for example in May 2019), date of 25th order for each user, number of days to place 25th order (difference between registration date for user and date of 25th order placed).
For example if user registered in April 2018, then placed 20 orders in 2018, and then placed 21-30th orders in Jan-May 2019 - this user should be in this array, if he placed 25th (overall for his account) order in May 2019.
How I can do this with MySQL request?
Sample data and structure: http://www.sqlfiddle.com/#!9/998358 (for testing you can get 3rd order as ex., not 25th, to not add a lot of sample data records).
One request is not required - if this can't be done in one request, few is possible and allowed.
You can use a correlated subquery to get the count of orders placed before the current one by a user. If that's 24 the current order is the 25th. Then check if the date is in the desired range.
SELECT o1.user_id,
o1.date,
datediff(o1.date, u1.registered)
FROM orders o1
INNER JOIN users u1
ON u1.userid = o1.user_id
WHERE (SELECT count(*)
FROM orders o2
WHERE o2.user_id = o1.user_id
AND o2.date < o1.date
OR o2.date = o1.date
AND o2.id < o1.id) = 24
AND o1.date >= '2019-01-01'
AND o1.date < '2019-06-01';
The basic inefficient way of doing this would be to get the user_id for every row in ORDERS where the date is in your target range AND the count of rows in ORDERS with the same user_id and a lower date is exactly 24.
This can get very ugly, very quickly, though.
If you're calling this from code you control, can't you do it from the code?
If not, there should be a way to assign to each row an index describing its rank among orders for its specific user_id, and select from this all user_id from rows with an index of 25 and a correct date. This will give you a select from select from select, but it should be much faster. The difficulty here is to control the order of the rows, so here are the selects I envision:
Select all rows, order by user_id asc, date asc, union-ed to nothing from a table made of two vars you'll initialize at 0.
from this, select all while updating a var to know if a row's user_id is the same as the last, and adding a field that will report so (so for each user_id the first line in order will have a specific value like 0 while the other rows for the same user_id will have a 1)
from this, select all plus a field that equals itself plus one in case the first added field is 1, else 0
from this, select the user_id from the rows where the second added field is 25 and the date is in range.
The union thingy is only necessary if you need to do it all in one request (you have to initialize them in a lower select than the one they're used in).
Edit: Well if you need the date too you can just select it along with the user_id, but calculating the number of days in sql will be a pain. Just join the result table to the users table and get both the date of 25th order and their date of registration, you'll surely be able to do the difference in code.
I'll try building an actual request, however if you want to truly understand what you need to make this you gotta read up on mysql variables, unions, and conditional statements.
"Looks too complicated. I am sure that this can be done with current DB structure and 1-2 requests." Well, yeah. Use the COUNT request, it will be easy, and slow as hell.
For the complex answer, see http://www.sqlfiddle.com/#!9/998358/21
Since you can use multiple requests, you can just initialize the vars first.
It isn't actually THAT complicated, you just have to understand how to concretely express what you mean by "an user's 25th command" to a SQL engine.
See http://www.sqlfiddle.com/#!9/998358/24 for the difference in days, turns out there's a method for that.
Edit 5: seems you're going with the COUNT method. I'll pray your DB is small.
Edit 6: For posterity:
The count method will take years on very large databases. Since OP didn't come back, I'm assuming his is small enough to overlook query speed. If that's not your case and let's say it's 10 years from now and the sqlfiddle links are dead; here's the two-queries solution:
SET #PREV_USR:=0;
SELECT user_id, date_ FROM (
SELECT user_id, date_, SAME_USR AS IGNORE_SMUSR,
#RANK_USR:=(CASE SAME_USR WHEN 0 THEN 1 ELSE #RANK_USR+1 END) AS RANK FROM (
SELECT orders.*, CASE WHEN #PREV_USR = user_id THEN 1 ELSE 0 END AS SAME_USR,
#PREV_USR:=user_id AS IGNORE_USR FROM
orders
ORDER BY user_id ASC, date_ ASC, id ASC
) AS DERIVED_1
) AS DERIVED_2
WHERE RANK = 25 AND YEAR(date_) = 2019 AND MONTH(date_) = 4 ;
Just change RANK = ? and the conditions to fit your needs. If you want to fully understand it, start by the innermost SELECT then work your way high; this version fuses the points 1 & 2 of my explanation.
Now sometimes you will have to use an API or something and it wont let you keep variable values in memory unless you commit it or some other restriction, and you'll need to do it in one query. To do that, you put the initialization one step lower and make it so it does not affect the higher statements. IMO the best way to do this is in a UNION with a fake table where the only row is excluded. You'll avoid the hassle of a JOIN and it's just better overall.
SELECT user_id, date_ FROM (
SELECT user_id, date_, SAME_USR AS IGNORE_SMUSR,
#RANK_USR:=(CASE SAME_USR WHEN 0 THEN 1 ELSE #RANK_USR+1 END) AS RANK FROM (
SELECT DERIVED_4.*, CASE WHEN #PREV_USR = user_id THEN 1 ELSE 0 END AS SAME_USR,
#PREV_USR:=user_id AS IGNORE_USR FROM
(SELECT * FROM orders
UNION
SELECT * FROM (
SELECT (#PREV_USR:=0) AS INIT_PREV_USR, 0 AS COL_2, 0 AS COL_3
) AS DERIVED_3
WHERE INIT_PREV_USR <> 0
) AS DERIVED_4
ORDER BY user_id ASC, date_ ASC, id ASC
) AS DERIVED_1
) AS DERIVED_2
WHERE RANK = 25 AND YEAR(date_) = 2019 AND MONTH(date_) = 4 ;
With that method, the thing to watch for is the amount and the type of columns in your basic table. Here orders' first field is an int, so I put INIT_PREV_USR in first then there are two more fields so I just add two zeroes with names and call it a day. Most types work, since the union doesn't actually do anything, but I wouldn't try this when your first field is a blob (worst comes to worst you can use a JOIN).
You'll note this is derived from a method of pagination in mysql. If you want to apply this to other engines, just check out their best pagination calls and you should be able to work thinks out.

Get count and sum for different purchase types on same table/colum

I have a "transaction" table that has the following columns
ID TIMESTAMP USER ID DESCRIPTION AMOUNT REF_ID TYPE
The description column contains the payment platform used for example "STRIPE-ch_1745". We currently have 4 platforms all described in the reference as in the example above. What I want is to get the payment platform, the total amount processed by the platform and the count of transactions. Like this
Platform Amount Count
Stripe 100,000 78
iOS 78,000 50
My current code only gives me these values for one platform, I've been unable to structure this properly to give me the desired result. I assumed I needed nested select statements, so I wrote the code in that manner
SELECT txn_count, sum
FROM
(SELECT count(*) AS txn_count, sum(`transaction`.`amount`) AS `sum`
FROM `transaction`
WHERE (`transaction`.`type` = 'credit'
AND (`transaction`.`description` like 'stripe%')
AND str_to_date(concat(date_format(`transaction`.`timestamp`, '%Y-%m'), '-01'), '%Y-%m-%d') = str_to_date(concat(date_format(now(), '%Y-%m'), '-01'), '%Y-%m-%d'))) t1
What this gives me right now is
Txn Count Sum
311 501,000
Would appreciate some help on how to get the expected table
Try this : ( edited to remove the reference part, assuming the reference is always separated by the platform by '-' )
SELECT
LEFT(t.description,LOCATE('-',t.description) - 1) as 'Platform',
SUM(t.amount) as 'Amount',
COUNT(*) as 'Count'
FROM transaction t
GROUP BY Platform

Select rows based on approximate value

I have a table with prices of some goods registered for some 
dates, such as:
2014-01-10      $120
        2014-01-11      $121
        2014-01-13      $119
        2014-01-15      $118
        I would like to create a view which would fill the missing 
prices, ie. quote the existing prices where available, and return the 
last known price if the price for a particular day is unknown, perhaps 
with a reasonable limit in case a product disappears from the market:
2014-01-10      $120
2014-01-11      $121
2014-01-12      $121
2014-01-13      $119
2014-01-14      $119
2014-01-15      $118
2014-01-16      $118
2014-01-17      $118
2014-01-18      $118
..........
2014-02-01      NULL
        If it was a mere matter of calculating the price one time, I 
would probably use a function for that, but I need later to combine 
the figures with the data from other tables with a date being a common  
field.
2014-01-10      $120    data a
    2014-01-11      $121    data b
    2014-01-12      $121    data c
    2014-01-13      $119    data d
    2014-01-14      $119    data e
    2014-01-15      $118    NULL
    2014-01-16      $118    NULL
    2014-01-17      $118    data f
    2014-01-18      $118    data g
        Normally, on 12th, 14th, and after 15th, the price field would 
return NULL, which I want to avoid, as it would ruin calculations which
use the data later on.
        If you believe that a view construction is not the best 
solution here, I'd be grateful for pointing me in the right direction 
- as you can see, the databases are not a field of my deepest 
expertise. ;)
If you have a list of all the dates that you care about, then you can use a correlated subquery to get the price:
select d.*,
(select p.price
from prices p
where p.date <= d.date
order by p.date desc
limit 1
) as price
from dates d;
This is MySQL view-compatible -- the subquery is in the select clause rather than in the from clause.

Mysql returns null for rows that doesn't exist

I have the following code:
while ($row = mysql_fetch_array($result)){
$que ='select SUM(price) from prices_adverts where advert_id="7" and room_type_id="54" and (date >= "2013-09-20" AND date <"2013-09-21") order by price';
$que ='select SUM(price) from prices_adverts where advert_id="7" and room_type_id="55" and (date >= "2013-09-20" AND date <"2013-09-21") order by price'; and etc
$res=mysql_query($que) or die();
$rw=mysql_fetch_row($res);
$price= $rw['0'];
}
this returns sum for some records that have prices in the database and NULL for $price for the records dont exist /when a room doesnt has price for specific dates it doesn't exist in the table /
So my question is how I can get result for records that exist only??? I do not need NULL values for prices and is it possible to access $price outside while ? How? Please help, thanks
May I explain what exactly I need, this may help you to help Me :)
Above I am looping hotels rooms to check how much would cost the room for specific period. Than I need to draw button outside loop which will divert visitor to reservation page. But if a hotel has no room prices available for the dates, I wish to have no button for reservation. That's why I need to figure out is there at least 1 room with prices in the hotel or not.. Hope this helps
########################################################Update
first query: I am taking all London hotels id-s
select id from adverts where town="London" limit 0, 5
than
for($i=0;$i<$num_rows;$i++){
$row=mysql_fetch_row($result);
echo echo_multy_htl_results($row[0]);
}
this function echo_multy_htl_results is:
select a.article_title, a.town, a.small_image, a.plain_text, a.star_rating, a.numberrooms, rta.room_type_id, rt.bg_room_type,a.longitude, a.latitude, a.link_name, a.id from adverts a, rooms_to_adverts rta,room_types rt where a.id = rta.advert_id and rta.advert_id="3" and rta.room_type_id=rt.id and rt.occupants>="1" group by rt.bg_room_type order by rt.occupants ASC
it gets info for the html hotel square and also room_types_id-s and that it comes the cod already added.. What would you suggest ?
Maybe by adding AND price IS NOT NULL ?
The solution to the immediate problem at hand can be this query:
select SUM(price)
from prices_adverts
where advert_id="7"
and room_type_id="54" -- notice, we are filtering on room type here
and (date >= "2013-09-20" AND date <"2013-09-21")
group by room_type_id -- this makes no rows appear when there are no rows found in this case
order by price
It returns 1 row, when there were a corresponding rows, and 0 rows, when there were none.
However, your problem seems to be of a different nature. Your scheme of operation seems to be like this:
query rows from the DB (room_type_ids)
put them in a loop
for each iteration run a query
This is bad. Databases are very good at solving these kinds of problems, using JOINs, and the other appropriate clauses. I'd suggest using these features, and turning things around in your head. That way, you could issue one query returning all data you need. I believe this might be such a query, providing all the room type IDs with their summed prices:
select room_type_id, SUM(price)
from prices_adverts
where advert_id="7" -- notice: no filtering for room_type_id this time
and (date >= "2013-09-20" AND date <"2013-09-21")
group by room_type_id
order by price
This query lists all room_type_ids that have records, and does not list those that don't, and beside each different type_id, it has the summed price. You can see the results in this SQL fiddle. (the data types are obviously off, this is just to show it in operation)
EDIT
To have the advert IDs similar to the room_type_ids too:
select advert_id, room_type_id, SUM(price)
from prices_adverts
where (date >= "2013-09-20" AND date <"2013-09-21")
-- notice: no filtering for room_type_id or advert id this time
group by advert_id, room_type_id
order by price
This will have three columns: advert_id, room_type_id and the summed price...
You could use
sum(case when price is null then 0 else price end)
or
sum(isnull(price,0))
or
just add in your where clause `price is not null` to exclude them.
You need to use HAVING
select SUM(price)
from prices_adverts
where advert_id="7" and room_type_id="54" and (date >= "2013-09-20" AND date <"2013-09-21")
having sum(price) is not null
order by sum(price)

MySql retrieve products and prices

I would like to retrieve a list of all the products, with their associated prices for a given period.
The Product table:
Id
Name
Description
The Price table:
Id
Product_id
Name
Amount
Start
End
Duration
The most important thing to not here, is that a Product can have mutliple prices, even over the same period, but not with the same duration.
For example, a price from "2013-06-01 -> 2013-06-08" and another from "2013-06-01 -> 2013-06-05"
So my aim is to retrieve, for a given period, the lists of all products, paginated by 10 product for example, joined to the prices existant over the period.
The basic way to do so would be:
SElECT *
FROM product
LEFT JOIN prices ON ...
WHERE prices.start >= XXX And prices.end <= YYY
LIMIT 0,10
The problem while using this simple solution, is that I can't retrieve only 10 Products, but 10 Products*Prices, which is not acceptable in my case.
So the solution would be:
SElECT *
FROM product
LEFT JOIN prices ON ...
WHERE prices.start >= XXX And prices.end <= YYY
GROUP BY product.id
LIMIT 0,10
But the problem here is, i'll only retrieve "1" price for each product.
So I wonder what would be the best way to handle this.
I could for example use a group function, like "group_concat", and retrieve in a field all the prices in a string, like "200/300/100" and so on. That seem weird, and would need work on server-language side to transform to a readable information, but it could work.
Another solution would be to use different column for each prices, depending on duration:
SELECT
IF( NOT ISNULL(price.start) AND price.duration = 1, price.amount , NULL) AS price_1_day
---- same here for all possible durations ---
From ...
Thta would work too i guess (i'm not really sure if this is possible however), but I may need to create about 250 columns to cover all possibilities. Is that a safe option ?
Any help will be much appreciated
I believe that a group_concat would be the best way forward on this, as its very purpose is to aggregate multiple pieces of data relating to a particular column.
However, adapting on peterm's SQL fiddle, this is possible to do in 1 query if using user defined variables. (If one ignores the initial query for setting the vars)
http://dev.mysql.com/doc/refman/5.7/en/user-variables.html
SET #productTemp := '', #increment := 0;
SElECT
#increment := if(#productTemp != Product_id, #increment + 1, #increment) AS limiter,
#productTemp :=Product_id as Product_id,
Product.name,
Price.id as Price_id,
Price.start,
Price.end
FROM
Product
LEFT JOIN
Price ON Product.Id=Price.Product_id
WHERE
`start` >= '2013-05-01' AND `end` <= '2013-05-15'
GROUP BY
Price_id
HAVING
limiter <=2
What we're doing here is only incrementing the user defined var "incrementer" only when the product id is not the same as the last one that was encountered.
As aliases cannot be used in the WHERE condition, we must GROUP by the unique ID (in this case price ID) so that we can reduce the result using HAVING. In this case, I have a full result set that should include 3 Product IDs, reduced to only showing 2.
Please note: This is not a solution I would recommend on large data sets, or in a production enviornment. Even the mysql manual makes a point of highlighting that user defined vars can behave somewhat erratically depending on what paths the optimizer takes. However, I have used them to great effect for some internal statistics in the past.
Fiddle: http://sqlfiddle.com/#!2/96c92/3
It's hard to tell without sample data and desired output but you can try something like this
SElECT p.*, q2.*
FROM
(
SElECT Product_id
FROM Price
WHERE `start` >= '2013-05-01' AND `end` <= '2013-05-15'
GROUP BY Product_id
LIMIT 0,10
) q1 JOIN
(
SELECT *
FROM Price
WHERE `start` >= '2013-05-01' AND `end` <= '2013-05-15'
) q2 ON q1.Product_id = q2.Product_id JOIN product p
ON q1.Product_id = p.Id
Here is SQLFiddle demo