Related
I have an ads table like this:
id adid approveby
1 10 MR A
2 11 MR A
3 12 MR B
I would like to count total record of MR A and echo each record out so I have this sql query:
$sql = "SELECT *, count(*) AS `totaladsapproved` FROM ads GROUP BY approveby";
$result = $conn->prepare($sql);
$result->execute();
foreach ($result as $row){
echo "Name: $row[approveby]"." - Total ads approved: $row[totaladsapproved] <br>";
for($i=0;$i<$row['totaladsapproved'];$i++){
echo "Ad ID: $row[adid] <br>"
}
}
But it only shows adid 10 twice. Here is the result i got:
Name: MR A - Total ads approved: 2
AD ID: 10
AD ID: 10
I would like to show AD ID 10 and then AD ID 11...What should I do to achieve this?
Many thanks
your query is grouping so you would not see each id
use following, this will give you count of total "aproveby" present for each row
SELECT id,
adid,
approveby,
(select count(*) from ads where approveby
= t1.approveby) AS `totaladsapproved`
FROM ads t1
if you want to restrict to MR A you can use where clause
The code has a lot of problems. They are addressed below one by one.
Undefined $i
Your code reads:
foreach ($result as $row){
echo "Name: $row[approveby]"." - Total ads approved: $row[totaladsapproved] <br>";
for ($i; $i < $row['totaladsapproved']; $i ++) {
echo "Ad ID: $row[adid] <br>";
}
}
First thing to notice is that $i is not initialized but you use it to iterate in a for() loop. PHP is nice and, besides showing you a notice you don't care of, because of the numeric context it uses 0 instead of NULL on the first loop when it handles $i to check the condition and to increment $i.
The correct way to do it is:
for ($i = 0; $i < $row['totaladsapproved']; $i ++) {
not only because of what I mentioned above but also because using your code, on the next foreach() iteration, $i starts with 2 and the for() loop skips the first two iterations.
The logic error in the loops
You get one row in variable $row but then you just print it 2 times (or as many times $row['totaladsapproved'] says) without getting the subsequent rows from the result set.
To fix it you could get new rows from the result set inside the for() loop. I don't provide code here because it doesn't matter, the query is wrong anyway.
General considerations about the queries that use GROUP BY
If you want to get the records then do not use GROUP BY. GROUP BY approveby produces a single row from all the rows that have the same value for approveby.
In the SELECT clause of a query that has a GROUP BY clause you can use only:
the columns that also appear in the GROUP BY clause;
other columns that are functionally dependent on the fields that appear in the GROUP BY clause; this means you can use any column of the table only if the table's PK or an UNIQUE INDEX of it appears in the GROUP BY clause;
any column of the table if it is used as an argument of aGROUP BY aggregate function.
Any column that appear in the SELECT clause and is not described in the list above renders the query invalid (according to the SQL standard).
MySQL extends the standard and allows such invalid queries (as long as they are syntactically correct, of course) but it does not guarantee anything about the values of these columns returned in the result set.
... a MySQL extension to the use of GROUP BY is to permit the SELECT list, HAVING condition, or ORDER BY list to refer to nonaggregated columns even if the columns are not functionally dependent on GROUP BY columns. This causes MySQL to accept the (...) query. In this case, the server is free to choose any value from each group, so unless they are the same, the values chosen are indeterminate, which is probably not what you want.
The query is incorrect
Back to your query, because the columns id and adid are not included in the list above, using * in the SELECT list renders the query invalid and the values returned for columns id and adid indeterminate.
In your case it happened that for approvedby == 'MR A', MySQL decided to return (1, 10) for (id, adid). It could return (2, 11) instead tomorrow, after you add or remove some rows from the table (or when the table is dumped and imported on a different server). It could even return (1, 11) and it is still right. It could even return -1 or NULL or whatever value it wants and you still cannot blame it because indeterminate is something that is not settled or decided.
The correct query
The correct query for your case is as simple as:
SELECT * FROM ads ORDER BY approveby
Using ORDER BY is important to get the rows having the same value for approveby one next to the other. Then, in the PHP code you run through the returned result set, count the rows for each value of approveby, store it in another list or do whatever you want with it.
$howMany = array(); // store the number of rows in each group
$current = '--fake--'; // the current group (use a value that does not appear in the data)
$count = 0; // the number of rows in the current group
foreach ($result as $row) {
// Check if a new group started
if ($row['approvedby'] != $current) {
// Store the value for the current 'approveby'
$howMany[$current] = $count;
// Start counting for the new value of 'approvedby'
$current = $row['approveby'];
$count = 0;
}
// Count the new occurrence
$count ++;
}
// Store the number of rows from the last group
$howMany[$current] = $count;
// Remove the fake value inserted on the first loop
unset($howMany['--fake--']); // Use the initial value of $current
If you don't need the number of rows in each group before enumerating the rows in the group then your code becomes even simpler: remove from the code above all the references to variable $howMany.
From PHP Code $Lines is defined as a list of accessions e.g. 123,146,165,1546,455,155
plant table has sequential records with the highest idPlant (unique identifier) of say 1000.
My simple SQL Query:
SELECT * FROM plant WHERE `plant`.idPlant IN($Lines) order by plant.idPlant;
This brings back row data for '123,146,165' etc.
Is there away to be told that '1546' was not found? (and thus the user probably entered a typo, I can not use a 'confirm all numbers are below X' because in the real data the idPlant may not be sequential and the upper bound will increase during use).
Update:
Looking to get an output that will tell me what Numbers were not found.
You can build up a sub query using unions that returns a list of all your values, then LEFT JOIN against that, checking for NULL in the WHERE clause to find the non matching values.
Basic php for this would be something like this:-
<?php
$sub_array = explode(',', $Lines);
$sub = '(SELECT '.implode(' AS i UNION SELECT ', $sub_array).' AS i) sub0';
$sql = "SELECT sub0.i
FROM $sub
LEFT OUTER JOIN plant
ON plant.idPlant = sub0.i
WHERE plant.idPlant IS NULL";
?>
You can create a temporary table and compare it to the original table. It goes something like this:
CREATE TEMPORARY TABLE IF NOT EXISTS plantIDs (
ID INT(11) NOT NULL UNIQUE,
found INT(11) NOT NULL);
INSERT INTO plantIDs(ID) VALUES (123),(146),(165),(1546),(455),(155);
SELECT plantIDs.ID, COALESCE(plant.name, "Not Found") as PlantName, plant.* FROM plant RIGHT JOIN plantIDs ON plant.idPlant=plantIDs.ID ORDER BY plantIDs.ID;
Assuming you have a field named name inside the table plant, this code will produce a row for each plant and the column named PlantName will contain the name of hte plant or the text "Not Found", ofc you can change the coalesce value to anything that fits your needs.
Good evening guys,
I'm a newbie to web programming and I need your help to solve a problem inherent to SQL query.
The database engine I'm using is MySQL and I access it via PHP, here I'll explain a simplified version of my database, just to fix ideas.
Let's suppose to work with a database containing three tables: teams, teams_information, attributes. More precisely:
1) teams is a table containing some basic information about italian football teams (soccer, not american football :D), it is formed by three fields: 'id' (int, primary key), 'name' (varchar, team name), nickname (Varchar, team nickname);
2) attributes is a table containing a list of possible information about a football team, such as city (the city where team plays its home match), captain (team captain's fullname), f_number (number of fans) and so on. This table is formed by three fields: id (int, primary key), attribute_name (varchar, an identifier for the attribute), attribute_desc (text, an explanation of the meaning of attribute). Each record of this table represents a single possible attribute of a football team;
3) teams_information is a table where some information, about teams listed in team table, are available. This table contains three fields: id (int, primary key), team_id (int, a foreign key which identifies a team), attribute_id (int, a foreign key which identifies one of the attributes listed in attributes table), attribute_value (varchar, the value of the attribute). Each record represents a single attribute of a single team. In general, different teams will have a different number of information, so for some teams a large number of attributes will be available while for other teams only a small number of attributes will be available.
Note that relation between teams and teams_information is one to many and the same relation exists between attributes and teams_information
Well, given this model my purpose is to realize a grid (maybe with ExtJS 4.1) to show user the list of italian football team, each record of this grid will represent a single football team and will contain all possible attributes: some fields may be empty (because, for considered team, the correspondent attribute is unknown), while the others will contain the values stored in teams_information table (for the considered team).
According to the above grid's field are: id, team_name and a number of fields to represent all the different attributes listed in 'attributes' table.
My question is: can I realize such a grid by using a SINGLE SQL query (maybe a proper SELECT query, to fetch all data I need from database tables) ?
Can anyone suggest me how to write a similar query (if it exists) ?
Thanks in advance for helping me.
Regards.
Enrico.
The short answer to your question is no, there is no simple construct in MySQL to achieve the result set you are looking for.
But it is possible to carefully (painstakingly) craft such a query. Here is an example, I trust you will be able to decipher it. Basically, I'm using correlated subqueries in the select list, for each attribute I want returned.
SELECT t.id
, t.name
, t.nickname
, ( SELECT v1.attribute_value
FROM team_information v1
JOIN attributes a1
ON a1.id = v1.attribute_id AND a1.attribute_name = 'city'
WHERE v1.team_id = t.id ORDER BY 1 LIMIT 1
) AS city
, ( SELECT v2.attribute_value
FROM team_information v2 JOIN attributes a2
ON a2.id = v2.attribute_id AND a2.attribute_name = 'captain'
WHERE v2.team_id = t.id ORDER BY 1 LIMIT 1
) AS captain
, ( SELECT v3.attribute_value
FROM team_information v3 JOIN attributes a3
ON a3.id = v3.attribute_id AND a3.attribute_name = 'f_number'
WHERE v3.team_id = t.id ORDER BY 1 LIMIT 1
) AS f_number
FROM teams t
ORDER BY t.id
For 'multi-valued' attributes, you'd have to pull each instance of the attribute separately. (Use the LIMIT to specify whether you are retrieving the first one, the second one, etc.)
, ( SELECT v4.attribute_value
FROM team_information v4 JOIN attributes a4
ON a4.id = v4.attribute_id AND a4.attribute_name = 'nickname'
WHERE v4.team_id = t.id ORDER BY 1 LIMIT 0,1
) AS nickname_1st
, ( SELECT v5.attribute_value
FROM team_information v5 JOIN attributes a5
ON a5.id = v5.attribute_id AND a5.attribute_name = 'nickname'
WHERE v5.team_id = t.id ORDER BY 1 LIMIT 1,1
) AS nickname_2nd
, ( SELECT v6.attribute_value
FROM team_information v6 JOIN attributes a6
ON a6.id = v6.attribute_id AND a6.attribute_name = 'nickname'
WHERE v6.team_id = t.id ORDER BY 1 LIMIT 2,1
) AS nickname_3rd
I use nickname as an example here, because American soccer clubs frequently have more than one nickname, e.g. Chicago Fire Soccer Club has nicknames: 'The Fire', 'La Máquina Roja', 'Men in Red', 'CF97', et al.)
NOT AN ANSWER TO YOUR QUESTION, BUT ...
Have I mentioned numerous times before, how much I dislike working with EAV database implementations? What should IMO be a very simple query turns into an overly complicated beast of a potentially light dimming query.
Wouldn't it be much simpler to create a table where each "attribute" is a separate column? Then queries to return reasonable result sets would look more reasonable...
SELECT id, name, nickname, city, captain, f_number, ... FROM team
But what really makes me shudder is the prospect that some developer is going to decide that the LDQ should be "hidden" in the database as a view, to enable the "simpler" query.
If you go this route, PLEASE PLEASE PLEASE resist any urge you may have to store this query in the database as a view.
I'm going to take a slightly different route. Spencer's answer is fantastic, and it addresses the issue quite well, but there's still a large underlying problem.
The data that you are trying to display on the site is over-normalized in the database. I won't elaborate, since, again, Spencer's answer highlights the issue pretty well.
Rather, I'd like to recommend a solution that denormalizes the data a bit.
Convert all of your Team data into a single table with many columns. (If there is Player data that isn't covered in the question, that would be a second table, but I'll gloss over that for now.)
Sure, you'll have a whole bunch of columns, and a lot of the columns might be NULL for a lot of the rows. It's not normalized, and it's not pretty, but here's the huge advantage that you gain.
Your query becomes:
SELECT * FROM Teams
That's it. That gets displayed right to the website and you are done. You might have to go out of your way to realize this schema, but it would be totally worth the time investment.
I think what you're saying is that you want the rows in the attributes table to appear as columns in the result recordset. If this is correct, then then in SQL you would use PIVOT.
A quick search on SO seems to indicate that there is no PIVOT equivalent in MySql.
I wrote a simple PHP script to generalize spencer's idea to solve my issue.
Here's the code:
<?php
require_once('includes/db.config.php'); //this file performs connection to mysql
/*
* Following function requires a table name ($table)
* and a number of service fields ($num). Given those parameters
* it returns the number of table fields (excluding service fields).
*/
function get_fields_number($table,$num,$conn)
{
$query = "SELECT * FROM $table";
$result = mysql_query($query,$conn);
return mysql_num_fields($result)-$num; //remember there are $num service fields
}
/*
* Following function requires a table name ($table) and an array
* containing a list of service fields names. Given those parameters,
* it returns the list of field names. That list is contained within an array and
* service fields are excluded.
*/
function get_fields_name($table,$service,$conn)
{
$query = "SELECT * FROM $table";
$result = mysql_query($query,$conn);
$name = array(); //Array to be returned
for ($i=0;$i<mysql_num_fields($result);$i++)
{
if(!in_array(mysql_field_name($result,$i),$service))
{
//currently selected field is not a service field
$name[] = mysql_field_name($result,$i);
}
}
return $name;
}
//Below $conn is db connection created in 'db.config.php'
$query = "SELECT `name` FROM `detail_arg` WHERE visibility = 0";
$res = mysql_query($query,$conn);
if($res===false)
{
$err_msg = mysql_real_escape_string(mysql_error($conn));
echo "{success:false,data:'".$err_msg."'}";
die();
}
$arg = array(); //list of argument names
while($row = mysql_fetch_assoc($res))
{
$arg[] = $row['name'];
}
//Following function writes the select subquery which is
//necessary to build a column containing a single attribute.
function make_subquery($attribute) //$attribute contains attribute name
{
$query = "";
$query.="(SELECT incident_detail.arg_value ";
$query.="FROM incident_detail ";
$query.="INNER JOIN detail_arg ";
$query.="ON incident_detail.arg_id = detail_arg.id AND detail_arg.name='".$attribute."' ";
$query.="WHERE incident.id = incident_detail.incident_id) ";
$query.="AS $attribute";
return $query;
}
/*
echo make_subquery("date"); //debug code
*/
$subquery = array(); //list of subqueries
for($i=0;$i<count($arg);$i++)
{
$subquery[] = make_subquery($arg[$i]);
}
$query = "SELECT "; //final query containing subqueries
$fields = get_fields_name("incident",array("id","visibility"),$conn);
//list of 'incident' table's fields
for($i=0;$i<count($fields);$i++)
{
$query.="incident.".$fields[$i].", ";
}
//insert the subqueries
$sub = implode($subquery,", ");
$query .= $sub;
$query.=" FROM incident ORDER BY incident.id";
echo $query;
?>
Consider following two tables:
tag_names (tag_id, tag_name)
tag_links (tag_id, image_id)
An image can have multiple tags, I want to select all tags for a specific image id.
I am trying following query, but it doesnt seem to select correctly (selects only one row), What is wrong with it?
SELECT tag_name
FROM tag_names
LEFT JOIN tag_links.tag_id = tag_names.tag_id
WHERE tag_links.image_id = $image_id
Edit: I'm using CodeIgniter Active record query, but I wrote in basic SQL format so that if someone is not fimiliar with CodeIgniter can help. However, this query works fine with simple mysql format (without using CodeIgniter) but strangely does not work with CodeIgniter, even there is no any problem with the syntax, it just selects one row.
Here is CodeIgniter Syntax:
$this->db->select('tag_name');
$this->db->from('tag_names');
$this->db->join('tag_links', 'tag_links.tag_id = tag_names.tag_id', 'left');
$this -> db -> where('tag_links.image_id', (int)$image_id);
$query = $this->db->get();
Try this:
SELECT tag_name
FROM tag_names
LEFT JOIN tag_links
ON tag_links.tag_id = tag_names.tag_id
WHERE tag_links.image_id = $image_id
IMHO you forgot to join table (properly with ON statement) you are using.
EDIT: I have 2 ideas how to get rid of the problem:
First:
Change the line with SELECT
$this->db->select('tag_names.tag_name');
Second:
Use select() function with complete query:
$this->db->select($query, false);
$this->db->select() accepts an optional second parameter. If you set
it to FALSE, CodeIgniter will not try to protect your field or table
names with backticks. This is useful if you need a compound select
statement.
from: http://codeigniter.com/user_guide/database/active_record.html#select
It seems that you have a syntax error (you forgot tag_links in JOIN clause). By the way in my opinion you don't need LEFT JOIN for this purpose otherwise you may get incorrect results.
SELECT tag_name
FROM
tag_names
JOIN tag_links ON tag_links.tag_id = tag_names.tag_id
WHERE tag_links.image_id = $image_id
SELECT tag_names.tag_name
FROM tag_links
LEFT JOIN tag_names.tag_id = tag_links.tag_id
WHERE tag_links.image_id = $image_id
tag_names is only going to have single entry for a given ID, which means your query will return a single result. You need to primarily select from tag_links and then join the name of the tag on top of it, so you correctly select from the table with the multiple entries.
I need to return a single row with some datas taken from some tables not related each others.
So, for example, my actual queries are these (I done it trought a PHP script) :
$query=mysql_query("SELECT trackid FROM tracklist WHERE usersub='".$_SESSION['nickname']."'",$mydb);
echo mysql_num_rows($query);
$query=mysql_query("SELECT trackid FROM comments WHERE usercom='".$_SESSION['nickname']."'",$mydb);
echo mysql_num_rows($query);
$query=mysql_query("SELECT vote FROM vote WHERE uservote='".$_SESSION['nickname']."'",$mydb);
echo mysql_num_rows($query);
$query = mysql_query("SELECT datereg FROM users WHERE nickname='".$_SESSION['nickname']."'",$mydb);
echo mysql_result($query,0,'datereg');
But this will call the MySql server 4 times.
Whats your suggestion to better this situation?
If the tables are not related then you will have to make 4 seperate calls
If the tables COULD be related by foreign keys then you could join them in some way and possibly cut down your sql calls
Ultimately though if you need all of the data then you'll have to request it from the database
You could use a UNION. And, btw, mysql_result is poor. And FFS don't forget to sanitize your inputs!
<?php
$nickname = mysql_escape_string($_SESSION['nickname']);
$sql = "
SELECT COUNT(trackid) AS n FROM tracklist WHERE usersub='{$nickname}'
UNION
SELECT COUNT(trackid) FROM comments WHERE usercom='{$nickname}'
UNION
SELECT COUNT(vote) FROM vote WHERE uservote='{$nickname}'
UNION
SELECT datereg FROM users WHERE nickname='{$nickname}'
";
$result = mysql_query($sql, $db);
while ($row = mysql_fetch_assoc($result)) {
echo $row['n'];
}
?>
I wouldn't really recommend this as it's a bit of a mess combining "count" values with a date in the same column, but you can do it. It's the direct answer to your question.
Well, you could create a fifth table and use it as an index.
If all the values { trackid, vote, datareg } are integers, the index table could contain three columns - nickname, value, and table. When you add records to one of the other tables, add a corresponding record to the index table.
For example,
INSERT INTO vote (vote, uservote, ...) VALUES (123, 'abc', ...);
INSERT INTO myindex (nickname, nvalue, ntable) VALUES ('abc', 123, 'vote');
(I wouldn't actually store the table name as a string but as a numeric value, but you get the idea)
Then on a query, you just SELECT nvalue, ntable FROM myindex WHERE nickname = 'abc';
You will possibly get more than one row.
I think that this is a lot of work and you are better off sticking with the four original queries.
Have you tried combining the select statement together like
SELECT .. Actually.
Maybe you should normalise your database and set up links between your tables...
Edit :: And i'm not sure how you're preparing yourself against mysql injection, but be careful with where your $_SESSION[] comes from
If all the selects return a single row:
$query=mysql_query("
(SELECT trackid FROM tracklist WHERE usersub='".$_SESSION['nickname']."'") as tracklist,
(SELECT trackid FROM comments WHERE usercom='".$_SESSION['nickname']."'") as trackid,
(SELECT vote FROM vote WHERE uservote='".$_SESSION['nickname']."'") as vote,
(SELECT datereg FROM users WHERE nickname='".$_SESSION['nickname']."'") as datereg
"