GROUP BY give priority to in MySQL - php

I have the following query.
$query_assignments = "SELECT * FROM tb_scheduler_assignments
WHERE company_id = '".$company_id."' OR
dept_id = '".$dept_id."' OR
user_id = '".$user_id."' ORDER BY
due_date GROUP BY purchase_id";
What I'd like is a single query solution that would keep the results for user_id over dept_id and dept_id over company_id.
For example:
if the same purchase_id occurs for
rows that were gotten via dept_id and
user_id, then I only want the result
for the user_id;
if the same purchase_id occurs for
rows that were gotten via company_id
and user_id, then I only want the
result for the user_id

First, you're interpolating variables in your SQL, which suggests you might be vulnerable to SQL injection. Just to make sure. PHP should offer prepared statements, or some escaping function.
Second, your SQL statement won't compile because you're using GROUP BY a but selecting * which includes at least three more columns.
Third, it sounds like you're misunderstanding SQL in thinking that it might, in a query such as you're trying to formulate (without UNION ALL), retrieve duplicate rows, i.e. the same row multiple times because it matches multiple criteria. This is not so.

The "single query" solution that I was looking for doesn't seem to exist, or if it does, it would be way slower than just handling all the sorting in php.
So, I ran 3 separate queries, put each of them into arrays, and then in order to put them all into a final array with the hierarchy that I needed, I did the loops below to see if the purchaseID existed for the levels up the hierarchy. If it didn't, then I put it in to the array.
$finalArray = array();
foreach ($companyArray as $purchaseID => $companyData) {
if (empty($deptArray[$purchaseID]) && empty($userArray[$purchaseID])) {
$finalArray[] = $companyData;
}
}
foreach ($deptArray as $purchaseID => $deptData) {
if (empty($userArray[$purchaseID])) {
$finalArray[] = $deptData;
}
}
foreach ($userArray as $purchaseID => $userData) {
$finalArray[] = $userData;
}
Then I can sort that array however I want and loop through that to echo what I need to.
Not sure if that's the best way, but it worked well and is lightning fast for me.

$query_assignments = "SELECT *,
IF(user_id = {$user_id}, 30,
IF(dept_id = {$dept_id}, 20,
IF(company_id = {$company_id}, 10, 0)
)
) as priority
FROM tb_scheduler_assignments
WHERE company_id = {$company_id} OR
dept_id = {$dept_id} OR
user_id = {$user_id}
GROUP BY purchase_id
ORDER BY due_date, priority DESC";
You can make a virtual field with the if statement.
user_id: 30 pts
dept_id: 20 pts
company_id: 10 pts
else: 0 pts
WARNING: can not be Indexed!
Syntax FIX: GROUP BY and ORDER BY reordered

Related

Perform query on existing SQL result? Find result from subset of SQL result

I have a script that goes through all order history. It takes several minutes to print the results, but I noticed I perform several SQL statements that are similar enough I wonder if you could do another query on an existing SQL result.
For example:
-- first SQL request
SELECT * FROM orders
WHERE status = 'shipped'
Then, in a foreach loop, I want to find information from this result. My naive approach is to perform these three queries. Note the similarity to the query above.
-- grabs customer's LTD sales
SELECT SUM(total) FROM orders
WHERE user = :user
AND status = 'shipped'
-- grabs number of orders customer has made
SELECT COUNT(*) FROM orders
WHERE user = :user
AND status = 'shipped'
AND total != 0
-- grabs number of giveaways user has won
SELECT COUNT(*) FROM orders
WHERE user = :user
AND status = 'shipped'
AND total = 0
I end up querying the same table several times when the results I seek are subsets of the first query. I'd like to get information from the first query without performing more SQL calls. Some pseudocode:
$stmt1 = $db->prepare("
SELECT * FROM orders
WHERE status = 'shipped'
");
$stmt1->execute();
foreach($stmt1 as $var) {
$username = $var['username'];
$stmt2 = $stmt1->workOn("
SELECT SUM(total) FROM this
WHERE user = :user
");
$stmt2->execute(array(
':user' => $username
));
$lifesales = $stmt2->fetchColumn();
$stmt3 = $stmt1->workOn("
SELECT COUNT(*) FROM this
WHERE user = :user
AND total != 0
");
$stmt3->execute(array(
':user' => $username
));
$totalorders = $stmt3->fetchColumn();
$stmt4 = $stmt1->workOn("
SELECT COUNT(*) FROM this
WHERE user = :user
AND total = 0
");
$stmt4->execute(array(
':user' => $username
));
$totalgaws = $stmt4->fetchColumn();
echo "Username: ".$username;
echo "<br/>Lifetime Sales: ".$lifesales;
echo "<br/>Total Orders: ".$totalorders;
echo "<br/>Total Giveaways: ".$totalgaws;
echo "<br/><br/>";
}
Is something like this possible? Is it faster? My existing method is slow and ugly, I'd like a quicker way to do this.
We could do one pass through the table to get all three aggregates for all users:
SELECT s.user
, SUM(s.total) AS `ltd_sales`
, SUM(s.total <> 0) AS `cnt_prior_sales`
, SUM(s.total = 0) AS `cnt_giveaways`
FROM orders s
WHERE s.status = 'shipped'
GROUP
BY s.user
That's going to be expensive on large sets. But if we are needing that for all orders, for all users, that's likely going to be faster than doing separate correlated subqueries.
An index with leading column of user is going to allow MySQL to use the index for the GROUP BY operation. Including the status and total columns in the index will allow the query to be satisfied entirely from the index. (With the equality predicate on status column, we could also try an index with status as the leading column, followed by user column, then followed by total.
If we only need this result for a small subset of users e.g. we are fetching only the first 10 rows from the first query, then running a separate query is likely going to be faster. We'd just incorporate the condition WHERE s.user = :user into the query, as in the original code. But run just the one query rather than three separate queries.
We can combine that with the first query by making it into an inline view, wrapping it in parens and putting into the FROM clause as a row source
SELECT o.*
, t.ltd_sales
, t.cnt_prior_sale
, t.cnt_giveaways
FROM orders o
JOIN (
SELECT s.user
, SUM(s.total) AS `ltd_sales`
, SUM(s.total <> 0) AS `cnt_prior_sales`
, SUM(s.total = 0) AS `cnt_giveaways`
FROM orders s
WHERE s.status = 'shipped'
GROUP
BY s.user
) t
ON t.user = o.user
WHERE o.status = 'shipped'
I'm not sure about that column named "prior" sales... this is returning all shipped orders, without regard to comparing any dates (order date, fulfillment date, shipment date), which we would typically associate with a concept of what "prior" means.
FOLLOWUP
noticing that the question is modified, removing the condition "status = 'shipped'" from the count of all orders by the user...
I will note that we can move conditions from the WHERE clause into the conditional aggregates.
Not that all these results are needed by OP, but as a demonstration...
SELECT s.user
, SUM(IF(s.status='shipped',s.total,0)) AS `ltd_sales_shipped`
, SUM(IF(s.status<>'shipped',s.total,0)) AS `ltd_sales_not_shipped`
, SUM(s.status='shipped' AND s.total <> 0) AS `cnt_shipped_orders`
, SUM(s.status='canceled') AS `cnt_canceled`
, SUM(s.status='shipped' AND s.total = 0) AS `cnt_shipped_giveaways`
FROM orders s
GROUP
BY s.user
Once the results are returned from the database, you can not run an SQL on top of them. However you can store them in a temporary table, to reuse them.
https://dev.mysql.com/doc/refman/8.0/en/create-temporary-table.html
https://dev.mysql.com/doc/refman/8.0/en/create-table-select.html
https://dev.mysql.com/doc/refman/8.0/en/insert-select.html
You need to create a temporary table, and insert all the data from the select statement, and then you can run queries on that table. Not sure if it would help much in your case.
For your particular case you can do something like:
select user, (total = 0) as is_total_zero, count(*), sum(total)
from orders
where status = 'shipped'
group by user, total = 0
However you would have to do some additional summing to get the results of the second query which gives you the sums per user, as they would be divided into two different groups with a different is_total_zero value.

How to count if value of a variable is repeated?

I am learning how to work with MySQL, and at the moment I succeed to show data from my table, using:
while($objResult2 = mysqli_fetch_assoc($objQuery_product)) {
Results are shown by using this variable $objResult2["id_product"]; this way i can take from DB any field I want like: $objResult2["name"]; $objResult2["email"]; etc.
But what i do if i have in the table more rows with the same id_product?
I want to write a if statment, which counts if id_product repeats. How to do that? If it is a lot of work, atleast please give me an idea of the right tutorial that I must read. Because i am trying second day to fix this, and searched google but i didnt find what i need, or maybe i coulndt understand it....
This is my query
$sql_product = "SELECT * FROM ps_product AS prod";
$join_product = " LEFT JOIN ps_product_lang AS lang ON lang.id_product = prod.id_product";
$join2_product = " LEFT JOIN ps_stock_available AS stok ON stok.id_product = prod.id_product";
$where_product =" WHERE prod.id_category_default = $idp AND lang.id_lang = 8";
$sql_product = $sql_product.$join_product.$join2_product.$where_product;
$objQuery_product = mysqli_query($objConnect, $sql_product) or die ("Error Query [".$sql_product."]");
You can simple remove the same id_product using DISTINCT keyword in your query. Such as:
SELECT DISTINCT id_product FROM my_table
This will give you results with different ids only.
The second way of doing it is taking the output values inside an array.
In your while loop:
$my_array[] = $objResult2["id_product"];
Then using array_filter remove all the duplicates inside the array.
YOu can also use array_count_values() if you want to count the duplicate values.
Ok here we go. For example you are fetching data with this query.
select id_product, name from PRODUCTS;
Suppose above query gives you 5 records.
id_product name
1 bat
2 hockey
2 hockey
3 shoes
4 gloves
Now you got 2,2 and hockey, hockey. Instead of thinking this way that you have to introduce an if statement to filter repeating records or same name or id_product records.
Rewrite your sql query like this.
select distinct id_product, name from PRODUCTS;
Or if you need count of each then my friend you will write your query something like this...
Graham Ritchie, if Andrei needs count of each repeating record then we will do something like this in our query.
SELECT PRODUCT_ID,
COUNT(PRODUCT_ID) AS Num_Of_Occurrences
FROM PRODUCTS
GROUP BY PRODUCT_ID
HAVING ( COUNT(PRODUCT_ID) > 1 );
SELECT id_product,COUNT(*) AS count
FROM tablename
GROUP BY id_product;
This query will then return you two items in your query
$objResult2["id_product"] //and
$objResult2["count"]
The if statement is then just
if($objResult2["count"] > 1){
//Do whatever you want to do with items with more than 1 occurence.
//for this example we will echo out all of the `product_id` that occur more than once.
echo $objResult2["id_product"] . " occurs more than once in the database<br/>";
}

What is the query statement to write in order to solve the followin database problem?

I have the following 3 tables in the database.
Programs_Table
Program_ID (Primary Key)
Start_Date
End_Date
IsCompleted
IsGoalsMet
Program_type_ID
Programs_Type_Table(different types of programs, supports a dropdown list in the form)
Program_type_ID (Primary Key)
Program_name
Program_description
Client_Program_Table
Client_ID (primary key)
Program_ID (primary key)
What is the best way to find out how many clients are in a specific program (program type)?
Would the following SQL statement be the best way, or even plausible?
SELECT Client_ID FROM Client_Program_Table
INNER JOIN Programs_Table
ON Client_Program_Table.Program_ID = Programs_Table.Program_ID
WHERE Programs_Table.Program_type_ID = "x"
where "x" is the Program_type_ID of the specific program we're interested in.
OR is the following a better way?
$result = mysql_query("SELECT Program_ID FROM Programs_Table
WHERE Program_type_ID = 'x'");
$row = mysql_fetch_assoc($result);
$ProgramID = $row['Program_ID'];
$result = mysql_query("SELECT * FROM Client_Program_Table
WHERE Program_ID = '$ProgramID'");
mysql_num_rows($result) // returns how many rows of clients we pulled.
Thank you in advance, please excuse my inexperience and any mistakes that I've made.
Here is how you can do it:
<?php
// always initialize a variable
$number_of_clients = 0;
// escape the string which will go in an SQL query
// to protect yourself from SQL injection
$program_type_id = mysql_real_escape_string('x');
// build a query, which will count how many clients
// belong to that program and put the value on the temporary colum "num_clients"
$query = "SELECT COUNT(*) `num_clients` FROM `Client_Program_Table` `cpt`
INNER JOIN `Programs_Table` `pt`
ON `cpt`.`Program_ID` = `pt`.`Program_ID`
AND `pt`.`Program_type_ID` = '$program_type_id'";
// execute the query
$result = mysql_query($query);
// check if the query executed correctly
// and returned at least a record
if(is_resource($result) && mysql_num_rows($result) > 0){
// turn the query result into an associative array
$row = mysql_fetch_assoc($result);
// get the value of the "num_clients" temporary created column
// and typecast it to an intiger so you can always be safe to use it later on
$number_of_clients = (int) $row['num_clients'];
} else{
// query did not return a record, so we have no clients on that program
$number_of_clients = 0;
}
?>
If you want to know how many clients are involved in a program, you'd rather want to use COUNT( * ). MySQL (with MyISAM) and SQL Server have a fast way to retrieve the total number of lines. Using a SELECT(*), then mysql_num_rows leads to unnecessary memory ressources and computing time. To me, this is the fastest, though not the "cleanest" way to write the query you want:
SELECT
COUNT(*)
FROM
Client_Program_Table
WHERE
Program_ID IN
(
SELECT
Program_ID
FROM
Programs_Table
WHERE
Program_type_ID = 'azerty'
)
Why is that?
Using JOIN make queries more readable, but subqueries often prove to be computed faster.
This returns a count of the clients in a specific program type (x):
SELECT COUNT(cpt.Client_ID), cpt.Program_ID
FROM Client_Program_Table cpt
INNER JOIN Programs_Table pt ON cpt.Program_ID=pt.Program_ID
WHERE pt.Program_type_ID = "x"
GROUP BY cpt.Program_ID

A logical problem with two tables

Hey guys, I created a list for fixtures.
$result = mysql_query("SELECT date FROM ".TBL_FIXTURES." WHERE compname = '$comp_name' GROUP BY date");
$i = 1;
$d = "Start";
while ($row = mysql_fetch_assoc($result))
{
$odate = $row['date'];
$date=date("F j Y", $row['date']);
echo "<p>Fixture $i - $d to $date</p>";
}
As you can see from the query, the date is displayed from the fixtures table.
The way my system works is that when a fixture is "played", it is removed from this table. Therefore when the entire round of fixtures are complete, there wont be any dates for that round in this table. They will be in another table.
Is there anyway I can run an other query for dates at the same time, and display only dates from the fixtures table if there isnt a date in the results table?
"SELECT * FROM ".TBL_CONF_RESULTS."
WHERE compid = '$_GET[id]' && type2 = '2' ORDER BY date"
That would be the second query!
EDIT FROM HERE ONWARDS...
Is there anyway I can select the date from two tables and then only use one if there are matches. Then use the rows of dates (GROUPED BY) to populate my query? Is that possible?
It sounds like you want to UNION the two result sets, akin to the following:
SELECT f.date FROM tbl_fixtures f
WHERE f.compname = '$comp_name'
UNION SELECT r.date FROM tbl_conf_results r
WHERE r.compid = '$_GET[id]' AND r.type2 = '2'
GROUP BY date
This should select f.date and add rows from r.date that aren't already in the result set (at least this is the behaviour with T-SQL). Apparently it may not scale well, but there are many blogs on that (search: UNION T-SQL).
From the notes on this page:
//performs the query
$result = mysql_query(...);
$num_rows = mysql_num_rows($result);
//if query result is empty, returns NULL, otherwise,
//returns an array containing the selected fields and their values
if($num_rows == NULL)
{
// Do the other query
}
else
{
// Do your stuff as now
}
WHERE compid = '$_GET[id]' presents an oportunity for SQL Injection.
Are TBL_FIXTURES and TBL_CONF_RESULTS supposed to read $TBL_FIXTURES and $TBL_CONF_RESULTS?
ChrisF has the solution!
One other thing you might think about is whether it is necessary to do a delete and move to another table. A common way to solve this type of challenge is to include a status field for each record, then rather than just querying for "all" you query for all where status = "x". For example, 1 might be "staging", 2 might be "in use", 3 might be "used" or "archived" In your example, rather than deleting the field and "moving" the record to another table (which would also have to happen in the foreach loop, one would assume) you could simply update the status field to the next status.
So, you'd eliminate the need for an additional table, remove one additional database hit per record, and theoretically improve the performance of your application.
Seems like what you want is a UNION query.
$q1 = "SELECT DISTINCT date FROM ".TBL_FIXTURES." WHERE compname = '$comp_name'";
$q2 = "SELECT DISTINCT date FROM ".TBL_CONF_RESULTS.
"WHERE compid = '$_GET[id]' && type2 = '2'";
$q = "($q1) UNION DISTINCT ($q2) ORDER BY date";

Summing a field from all tables in a database

I have a MySQL database called "bookfeather." It contains 56 tables. Each table has the following structure:
id site votes_up votes_down
The value for "site" is a book title. The value for "votes_up" is an integer. Sometimes a unique value for "site" appears in more than one table.
For each unique value "site" in the entire database, I would like to sum "votes_up" from all 56 tables. Then I would like to print the top 25 values for "site" ranked by total "votes_up".
How can I do this in PHP?
Thanks in advance,
John
You can do something like this (warning: Extremely poor SQL ahead)
select site, sum(votes_up) votes_up
from (
select site, votes_up from table_1
UNION
select site, votes_up from table_2
UNION
...
UNION
select site, votes_up from table_56
) group by site order by sum(votes_up) desc limit 25
But, as Dav asked, does your data have to be like this? There are much more efficient ways of storing this kind of data.
Edit: You just mentioned in a comment that you expect there to be more than 56 tables in the future -- I would look into MySQL limits on how many tables you can UNION before going forward with this kind of SQL.
Here's a PHP code snip that should get it done.
I have not tested it so it might have some typos and stuff, make sure you replace DB_NAME
$result = mysql_query("SHOW TABLES");
$tables = array();
while ($row = mysql_fetch_assoc($result)) {
$tables[] = '`'.$row["Tables_in_DB_NAME"].'`';
}
$subQuery = "SELECT site, votes_up FROM ".implode(" UNION ALL SELECT site, votes_up FROM ",$tables);
// Create one query that gets the data you need
$sqlStr = "SELECT site, sum(votes_up) sumVotesUp
FROM (
".$subQuery." ) subQuery
GROUP BY site ORDER BY sum(votes_up) DESC LIMIT 25";
$result = mysql_query($sqlStr);
$arr = array();
while ($row = mysql_fetch_assoc($result)) {
$arr[] = $row["site"]." - ".$row["sumVotesUp"];
}
print_r($arr)
The UNION part of Ian Clelland answer can be generated using a statement like the following. The table INFORMATION_SCHEMA.COLUMNS has a column TABLE_NAME to get all tables.
select * from information_schema.columns
where table_schema not like 'informat%'
and column_name like 'VOTES_UP'
Join all inner SELECT with UNION ALL instead of UNION. UNION is doing an implicit DISTINCT (on oracle).
The basic idea would be to iterate over all your tables (using a SQL SHOW TABLES statement or similar) in PHP, then for every table, iterate over the rows (SELECT site,votes_up FROM $table). Then, for every row, check the site against an array that you're building with sites as keys and votes up as values. If the site is already in the array, increment its votes appropriately; otherwise, add it.
Vaguely PHP-like pseudocode:
// Build an empty array for use later
$votes_array = empty_array();
// Get all the tables and iterate over them
$tables = query("SHOW TABLES");
for($table in $tables) {
$rows = query("SELECT site,votes_up FROM $table");
// Iterate over the rows in each table
for($row in $rows) {
$site = $row['site'];
$votes = $row['votes_up'];
// If the site is already in the array, increment votes; otherwise, add it
if(exists_in_array($site, $votes_array)) {
$votes_array[$site] += $votes;
} else {
insert_into_array($site => $votes);
}
}
}
// Get the sites and votes as lists, and print out the top 25
$sorted_sites = array_keys($votes_array);
$sorted_votes = array_values($votes_array);
for($i = 0; $i < 25; $i++) {
print "Site " . $sorted_sites[$i] . " has " . $sorted_votes[$i] . " votes";
}
"I allow users to add tables to the database." - I hope all your users are benevolent and trustworthy and capable. Do you worry about people dropping or truncating tables, creating incorrect new tables that break your code, or other things like that? What kind of security do you have when users can log right into your database and change the schema?
Here's a tutorial on relational database normalization. Maybe it'll help.
Just in case someone else that comes after you wants to find what this could have looked like, here's a single table that could do what you want:
create database bookfeather;
create user bookfeather identified by 'bookfeather';
grant all on bookfeather.* to 'bookfeather'#'%';
use bookfeather;
create table if not exists book
(
id int not null auto_increment,
title varchar(255) not null default '',
upvotes integer not null default 0,
downvotes integer not null default 0,
primary key(id),
unique(title)
);
You'd vote a title up or down with an UPDATE:
update book set upvotes = upvotes + 1 where id = ?
Adding a new book is as easy as adding another row:
insert into book(title) values('grails in action')
I'd strongly urge that you reconsider.

Categories