MySQL Dynamic HAVING Clause - mysql

I've written SELECT statement that works perfectly. However, I need to make some changes so that it now gets user submitted information from a form and returns results from the database based on that.
It will essentially be a product search—if a user searches for "red" they'll get back all items that are red. If they search for "red" and "wood" they'll get back only the items that are red and made of wood.
Here is my HAVING clause:
HAVING values LIKE "%Red%" AND values LIKE "%Wood%"
If I had a series of 5 drop down menus, each with a different set of terms, should I try to build the HAVING clause dynamically based on what drop downs the user uses? How would this be done?

I would opt for a static SQL statement, given that there are exactly five "options" the user can select from.
What I would do is use an empty string as the value that represents "no restriction" for a particular option, so my statement would be like this: e.g.
HAVING `values` LIKE CONCAT('%',:b1,'%')
AND `values` LIKE CONCAT('%',:b2,'%')
AND `values` LIKE CONCAT('%',:b3,'%')
AND `values` LIKE CONCAT('%',:b4,'%')
AND `values` LIKE CONCAT('%',:b5,'%')
To apply restrictions only on options 1 and 2, and no restriction on option 3, e.g.
$sth->bind_param(':b1','Red');
$sth->bind_param(':b2','Wood');
$sth->bind_param(':b3','');
$sth->bind_param(':b4','');
$sth->bind_param(':b5','');
To apply restriction only on option 2, e.g.
$sth->bind_param(':b1','');
$sth->bind_param(':b2','Wood');
$sth->bind_param(':b3','');
$sth->bind_param(':b4','');
$sth->bind_param(':b5','');
This allows you to have a static statement, and the only thing that needs to change is the values supplied for the bind parameters.
It's also possible to use a NULL value to represent "no restriction" for an option, but you'd need to modify the SQL statement to do a "no restriction" when a NULL value is supplied. Either check for the NULL value specifically, e.g.
HAVING ( `values` LIKE CONCAT('%',:b1,'%') OR :b1 IS NULL )
AND ( `values` LIKE CONCAT('%',:b2,'%') OR :b2 IS NULL )
AND ( `values` LIKE CONCAT('%',:b3,'%') OR :b3 IS NULL )
-or- just convert the NULL to an empty string for use in the LIKE predicate, e.g.
HAVING `values` LIKE CONCAT('%',IFNULL(:b1,''),'%')
AND `values` LIKE CONCAT('%',IFNULL(:b2,''),'%')
AND `values` LIKE CONCAT('%',IFNULL(:b3,''),'%')
The same technique will work with a WHERE clause as well. You may want to consider whether a WHERE clause is more appropriate in your case. (We can't tell from the information provided.)
But note that the HAVING clause does not restrict which rows are included in the statement; rather, the HAVING clause only restricts which rows from the result set are returned. The HAVING clause is applied nearly last in the execution plan (I think its followed only by the ORDER BY and LIMIT.
The HAVING clause can apply to aggregates, which the WHERE clause cannot do. The HAVING clause can reference columns in the SELECT list, which the WHERE clause cannot do.
(Also note that VALUES is a reserved word, if it's not qualified (preceded by alias., it may need to be enclosed in backticks.)

No, you should not build the HAVING clause.
Yes, you could build the WHERE clause as you suggested.
HAVING is used for imposing conditions involving aggregation functions on groups.

This approach make sense for where more conditions are involved, but you can use them on 5 as well. Collect your menu values into a temp table and then inner join them to your query on LIKE condition
SELECT .... FROM MyTable
INNER JOIN MyTempTable
ON values LIKE '%' + MenuValue '%'

Related

SQL Query - Select a value, then use it again in following statements

I've tried looking it up, and while I think this should be possible I can't seem to find the answer I need anywhere.
I need to lookup a date from one table, then store it for use in a following query.
Below is statements that should work, with my setting the variable (which I know won't work, but I'm unsure the best way to do/show it otherwise - bar maybe querying it twice inside the if statement.)
I'm then wanting to in the latter statement, use either the date given in the second query, or if the date from the first query (that I'm thinking to set as a variable) is newer, use that instead.
startDateVariable = (SELECT `userID`, `startDate`
FROM `empDetails`
WHERE `userID` = 1);
SELECT `userID`, SUM(`weeksGROSS`) AS yearGROSS
FROM `PAYSLIP`
WHERE `date` <= "2021-11-15"
AND `date` >= IF( "2020-11-15" > startDateVariable , "2020-11-15" , startDateVariable )
AND `userID` IN ( 1 )
GROUP BY `userID`
Naturally all dates given in the query ("2021-11-15" etc) would be inserted dynamically in the prepared statement.
Now while I've set the userID IN to just query 1, it'd be ideal if I can lookup multiple users this way at once, though I can accept that I may need to make an individual query per user doing it this way.
Much appreciated!
So turns I was going about this the wrong way, looks like the best way to do this or something similar is by using SQL JOIN
This allows you to query the tables as if they are one.
I also realised rather then using an IF, i could simply make sure i was looking up newer or equal to both the date given and the start date.
Below is working as required. And allows lookup of multiple users at once as wanted.
SELECT PAYSLIP.userID, employeeDetails.startDate, SUM(PAYSLIP.weeksGROSS) AS yearGROSS
FROM PAYSLIP
INNER JOIN employeeDetails ON employeeDetails.userID=PAYSLIP.userID
WHERE PAYSLIP.date <= "2021-11-15"
AND PAYSLIP.date >= "2020-11-15"
AND PAYSLIP.date >= employeeDetails.startDate
AND PAYSLIP.userID IN ( 1,2,8 )
GROUP BY PAYSLIP.userID
See here for more usage examples: https://www.w3schools.com/sql/sql_join.asp
However along the lines of my particular question, it's possible to store variables. I.E.
SET #myvar= 'Example showing how to declare variable';
Then use it in the SQL statement by using
#myvar where you want the variable to go.

Filter on a view is applied to filters excluded in a view

I have a table that can have JSON data in a column. I added a calculated column to the table with the ISJSON() function to mark any rows that do not contain valid JSON data
CREATE TABLE tbl1 (Id INT IDENTITY(1,1) NOT NULL, Content NVARCHAR(MAX), IsJsonRecord AS ISJSON(Content))
GO
INSERT INTO tbl1 (Content) VALUES ('a'), ('{"name":"asd"}')
GO
Now I have a view that parses the JSON data out to more readable formats such as
CREATE VIEW vw1
AS
SELECT Id,
JSON_VALUE(Content, '$."name"') AS Name
FROM tbl1
WHERE IsJsonRecord > 0
The WHERE clause works as expected when I select from the view.
SELECT *
FROM vw1
When I query the view with an additional where clause I get an error due to malformed JSON data (as below).
SELECT *
FROM vw1
WHERE [Name] LIKE '%a%'
It seems like the query WHERE clause is applied to rows that do not conform to the WHERE clause already specified in the view.
Is this the expected behavior?
I understand that the view is "optimized away", but I was expecting the query optimizer to apply filters to distinct fields before it applied filters that requires functions to operate on data. I would think that logic could have performance benefits in some scenarios.
I'm not too sure what to do to accommodate the WHERE clause on the view. My actual case is much more complex than the example, and I'm not sure that I can test each column in the view with a CASE statement over the JSON_VALUE statement.
Any suggestions?
As it stands, no you cannot tell the compiler in which order to do the filters, although there are tricks you can do which usually force it to do so.
The problem is that your query is effectively compiled like this:
SELECT *
FROM (
SELECT Id,
JSON_VALUE(Content, '$."name"') AS Name
FROM tbl1
WHERE IsJsonRecord > 0
) vw1
WHERE [Name] LIKE '%a%'
Which is then trivially optimized to:
SELECT Id,
JSON_VALUE(Content, '$."name"') AS Name
FROM tbl1
WHERE IsJsonRecord > 0 AND [Name] LIKE '%a%'
At this point the compiler will make a decision as to which part to evaluate first. For whatever reason, it has chosen to do the LIKE first, this may be due to a good index.
You have a number of solutions:
Use CASE, which is guaranteed (except in certain situations) to be evaluated in order
SELECT Id,
JSON_VALUE(Content, '$."name"') AS Name
FROM tbl1
WHERE CASE WHEN IsJsonRecord > 0 THEN CASE WHEN [Name] LIKE '%a%' THEN 1 END END = 1
Add an index on the persisted column, either with the column as the leading key, or a filtered index IsJsonRecord > 0.
This is not guaranteed to always work, but it usually does. Make sure to include all columns necessary.
A variation on the above is to add a clustered index to the view. There are many restrictions on doing this, but it can work very well. Make sure to add WITH (NOEXPAND) hint to the query.
A more guaranteed option is to add a TOP to the view.
This forces the compiler to ensure that IsJsonRecord > 0 is logically evaluated first, which nearly always means it will be physically executed first.
SELECT *
FROM (
SELECT TOP (9223372036854775807) *
FROM vw1
) vw1
WHERE [Name] LIKE '%a%'

In MySQL select query, checking for a string or checking for true in a where clause?

Consider the following table:
SELECT id, Bill_Freq, Paid_From, Paid_To, Paid_Dt, rev_code FROM psr_20160708091408;
The requirement is to fetch the row which has rev_code populated with the string **SUM**.
I've also noticed that for every row with rev_code populated as **SUM** its Bill_Freq won't be either null or zero.
So I wrote two queries to fetch the row with the lowest id
Query based on string check in where clause:
select
min(id) as head_id,
bill_freq,
Paid_From,
Paid_To,
Paid_Dt
from
`psr_20160708091408` where rev_code = "**SUM**";
Query based on true condition:
select
min(id) as head_id,
bill_freq,
Paid_From,
Paid_To,
Paid_Dt
from
`psr_20160708091408` where bill_freq;
I haven't seen anyone use the second type, would like to know its reliability and circumstance of failure.
If by "second type" you mean a where clause with no explicit condition, then there is a good reason why you do not see it.
The SQL standard -- and most databases -- require explicit conditions in the where. MySQL allows the shorthand that you use but it really means:
where not billing_freq <=> 0
or equivalently:
where billing_freq <> 0 or billing_freq is null
(The <=> is the null-safe comparison operator.
The more important issue with your query is the min(). I presume that you actually want this:
select p.*
from psr_20160708091408 p
where rev_code = '**SUM**'
order by id
limit 1;
Also, you should use single quotes as string delimiters. That is the ANSI standard and there is rarely any reason to use double quotes.
Actually you can use the second type of query, but as your requirement is based on rev_code, it is always good to have condition with rev_code, because of 2 reasons
Bill_Freq having no NUlls or Zeros might be assumption based on current data
Even if it is true, in future, your application logic might change and it might have a scenario having NULL or zero, which will break your logic in future.
So my suggestion is to use first query with Rev_code
Please try to use below query
select
id,
bill_freq,
Paid_From,
Paid_To,
Paid_Dt
from
`psr_20160708091408` where rev_code = "**SUM**" ORDER BY ASC LIMIT 0,1;
Thanks.
The requirement says it itself.
The requirement is to fetch the row which has rev_code populated with
the string '**SUM**'
In the scenario that bill_freq IS NOT NULL and rev_code is populated with
the string '**SUM**' then your logic will obviously fail.
Go for
where rev_code = "**SUM**";

MySQL returns all rows when field=0 from SECOND Select query

This case is similar to: S.O Question; mySQL returns all rows when field=0, and the Accepted answer was a very simple trick, to souround the ZERO with single quotes
FROM:
SELECT * FROM table WHERE email=0
TO:
SELECT * FROM table WHERE email='0'
However, my case is slightly different in that my Query is something like:
SELECT * FROM table WHERE email=(
SELECT my_column_value FROM myTable WHERE my_column_value=0 AND user_id =15 LIMIT 1 )
Which in a sense, becomes like simply saying: SELECT * FROM table WHERE email=0, but now with a Second Query.
PLEASE NOTE: It is a MUST that I use the SECOND QUERY.
When I tried: SELECT * FROM table WHERE email='( SELECT my_column_value FROM myTable WHERE my_column_value=0 LIMIT 1 )' (Notice the Single Quotes on the second query)
MySql SCREAMED Errors near '(.
How can this be achieved
Any Suggestion is highly honored
EDIT1: For a visual perspective of the Query
See the STEN_TB here: http://snag.gy/Rq8dq.jpg
Now, the main aim is to get the sten_h where rawscore_h = 0;
The CURRENT QUERY as a whole.
SELECT sten_h
FROM sten_tb
WHERE rawscore_h = (
SELECT `for_print_stens_rowscore`
FROM `for_print_stens_tb`
WHERE `for_print_stens_student_id` =3
AND `for_print_stens_factor_name` = 'Factor H' )
The result of the Second Query can be any number including ZERO.
Any number from >=1 Works and returns a single corresponding value from sten_h. Only =0 does not Work, it returns all rows
That's the issue.
CORRECT ANSWER OR SOLUTION FOR THIS
Just in case someone ends up in this paradox, the Accepted answer has it all.
SEE STEN_TB: http://snag.gy/Rq8dq.jpg
SEE The desired Query result here: http://snag.gy/wa4yA.jpg
I believe your issue is with implicit datatype conversions. You can make those datatype conversions explicit, to gain control.
(The "trick" with wrapping a literal 0 in single quotes, that makes the literal a string literal, rather than a numeric.)
In the more general case, you can use a CAST or CONVERT function to explicitly specify a datatype conversion. You can use an expression in place of a column name, wherever you need to...
For example, to get the value returned by my_column_value to match the datatype of the email column, assuming email is character type, something like:
... email = (SELECT CONVERT(my_column_value,CHAR(255)) FROM myTable WHERE ...
or, to get the a literal integer value to be a string value:
... FROM myTable WHERE my_column_value = CONVERT(0,CHAR(30)) ...
If email and my_column_value are just indicating true or false then they should almost certainly be both BIT NOT NULL or other two-value type that your schema uses for booleans. (Your ORM may use a particular one.) Casting is frequently a hack made necessary by a poor design.
If it should be a particular user then you shouldn't use LIMIT because tables are unordered and that doesn't return a particular user. Explain in your question what your query is supposed to return including exactly what you mean by "15th".
(Having all those similar columns is bad design: rawscore_a, sten_a, rawscore_b, sten_b,... . Use a table with two columns: rawscore, sten.)

Running a SQL SELECT statement against a MYSQL column of SET type

I'm trying to run a SQL SELECT statement against a column that is of type SET. The table is called myTable and the columns in myTable are called base_props and names. The base_props column is of type SET. The values in base_prop are vb,nt, cnt,poss and loc. So I would like to SELECT entries from the column 'name' where base_props have both the values, vb and poss. The results I'm looking to get may have values other than just vb and poss. So to be clear I would like to select all entries that have the values vb and poss regardless if they have other values as well. I've tried the following SQL queries but I can't get the desired results.
SELECT name from myTable WHERE base_props = 'vb' AND base_props = 'poss'
That query returns an empty result set. I've tried using FIND_IN_SET() and IN() but I couldn't get anywhere with that. I've written SQL statements before but never had to deal with columns that are type SET. Any help is appreciated.
The only thing I can come up with is using the LIKE keyword:
SELECT name FROM myTable WHERE (base_props LIKE '%vb%' AND base_props LIKE '%poss%');
This will make sure both vb and cnt are in the base_props column. Of course you can use cnt, nt and loc in there, or any number of base_props values in the sql, just add more AND statements.
OR as a deleted answer by samitha pointed out, you can use FIND_IN_SET:
SELECT name from myTable WHERE FIND_IN_SET('vb', base_props) AND FIND_IN_SET('poss', base_props);
Comment (by spencer7593): "both of these work, but there is a slight difference. The LIKE operator will actually match any member that includes the search string anywhere in a term; the FIND_IN_SET function will only match an exact member. It's also possible to search for members in set by the order they appear in the SET definition, using the MySQL BITAND operator: for example, to match the 1st and 4th members of the set: WHERE base_props & 1 AND base_props & 8". So for example, if you have 'a' and 'aaa' in your set, then using the LIKE "%a%" method will also return rows containing 'aaa'.
Conclusion: use the FIND_IN_SET solution since it will work for all cases.
FIND_IN_SET return index, Try this
SELECT name from myTable WHERE FIND_IN_SET(base_props, 'vb') > 0 AND
FIND_IN_SET(base_props, 'poss') > 0