Information
Currently building an notification page which lists all of the logged in users notifications which contain information about the notification on each one.
For example, without information
You have an unread message
With information
<Sarah> Sent you an message
Problem
Because the notifications require data such as Username (for message notifications) or a article title (say your following an author and they release a new blog post, one notification would need to pull username form users table and then also the title of the blog from the blog table) this causes my page to lag even on localhost which I'm guessing would get significantly worse once uploaded and tested in the wild.
Current Code
function showNotifications($userid){
$STH = $this->database->prepare('SELECT * FROM notifications WHERE user_id = :userid ORDER BY timestamp DESC');
$STH->execute(array(':userid' => $userid));
while($row = $STH->fetch(PDO::FETCH_ASSOC)){
$this->sortNotif($row);
}
Quick explanation about the function below, because I have different types of notifications I created a bunch of ID's for specific types, for example type 1 = new message, type 2 = new blog post
function sortNotif($notif){
switch ($notif['type']) {
case "1":
$msg = $this->getMessageData($notif['feature_id']);
$user = $this->userData($msg['sender']);
echo '<li><i>'.timeAgo($notif['timestamp']).'</i>'.$user['first_name'].' sent you a message</li>';
break;
}
}
As you can see for just showing that a user has a new message it creates 2 query's and once looped through 40 or so notifications, over 100 or so users becomes a strain on the server.
Final Words
If anyone needs more information please ask and I'll be sure to update this question asap, thanks!
Edit
Below are table structures as requested in the below comments.
notifications
id | user_id | feature_id | type | timestamp | read
users
id | username | password | first_name | last_name | email | verify_hash | avatar | type
messages
id | receiver | sender | replying_to | deleted | body | timestamp | read
Change of plan, since I misunderstood the set up.
You will want to pull all of the data from each of the 'type' tables in one go, rather than on a per notification basis. This means that you will need to loop through your notifications twice, once to grab all the ids and appropriate types and then a second time to output the results.
function showNotifications($userid){
$STH = $this->database->prepare('SELECT * FROM notifications WHERE user_id = :userid ORDER BY timestamp DESC');
$STH->execute(array(':userid' => $userid));
// Centralized Book keeping for the types.
// When you add a new type to the entire system, add it here as well.
$types = array();
// Add the first notification type
$types["1"] = array();
// "query" is pulling all the data you need concerning a notification
$types["1"]["query"] = "SELECT m.id, u.username, u.firstname FROM messages m, users u WHERE m.sender = u.id AND m.id IN ";
// "ids" will hold the relevant ids that you need to look up.
$types["1"]["ids"] = array();
// A second type, just for show.
// $types["2"] = array();
// $types["2"]["query"] = "SELECT a.id, u.username, u.firstname FROM articles a, users u WHERE a.sender = u.id AND a.id IN ";
// $types["2"]["ids"] = array();
// Use fetchAll to gather all of the notifications into an array
$notifications = $STH->fetchAll();
// Walk through the notifications array, placing the notification id into the corret
// "ids" array in the $types array.
for($i=0; $i< count($notifications); $i++){
$types[$notifications[$i]['type']]["ids"][] = $notifications[$i]['feature_id'];
}
// Walk through the types array, hit the database once for each type of notification that has ids.
foreach($types as $type_id => $type){
if(count($type["ids"]) > 0){
$STH = $this->database->prepare($type["query"] . "( " . implode(",", $type["ids"]) . " )");
$STH->execute();
// Creates a hash table with the primary key as the array key
$types[$type_id]['details'] = $STH->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_ASSOC);
$types[$type_id]['details'] = array_map('reset', $types[$type_id]['details']);
// run array_map to make it easier to work with, otherwise it looks like this:
// $results = array(
// 1234 => array(0 => array('username' => 'abc', 'firstname' => '[...]')),
// 1235 => array(0 => array('username' => 'def', 'firstname' => '[...]')),
// );
}
}
// Now walk through notifications again and write out based on notification type,
// referencing $types[<notification type>]["details"][<message id>] for the details
for($i=0; $i< count($notifications); $i++){
// check to see if details for the specific notification exist.
if(isset($types[$notifications[$i]['type']]["details"][$notifications[$i]['feature_id']])){
$notification_details = $types[$notifications[$i]['type']]["details"][$notifications[$i]['feature_id']];
switch ($notifications[$i]['type']) {
case "1":
echo '<li><i>'.timeAgo($notifications[$i]['timestamp']).'</i>' . $notification_details['first_name'].' sent you a message</li>';
break;
}
}
}
}
Update : Added logic to skip a notification if no details are pulled (ex. either the message or the user were deleted)
I think you want to run a single query that gathers all the info either via joins or a more complicated where statement.
Option 1: this might need to be tweaked so as to not the cartesian product of the tables
SELECT n.id, n.type, m.id, m.body, u.username, u.first_name
FROM notifications n, messages m, users u
WHERE n.user_id = :userid AND m.id = n.feature_id AND u.id = m.sender
Option 2: if the table aliases don't work, then you'll need to replace them with the full table name
SELECT SELECT n.id, n.type, m.id, m.body, u.username, u.first_name
FROM notifications n
JOIN messages m
ON n.feature_id = m.id
JOIN users u
ON m.sender = u.id
WHERE n.user_id = :userid
Related
I have been scratching my head for a very long time about this PHP code. I am trying to achieve something like
->Get each status
->Get each user in user's friends list
->Display status' from each user that is in the user's friends list
and repeat until there is no more. I have been looking for a solution for more a few days and it is really bugging me. Here is the code I tried:
EDIT: posted schema as requested
https://kjf-tech.net/files/schema.png
<?php
$connect = new MySQLi($DBhost,$DBuser,$DBpass,$DBname);
$querya = "SELECT * FROM statuses ORDER BY `id` DESC";
$result = mysqli_query($connect, $querya);
$ALLDATA = array();
$DBcon2 = new MySQLi($DBhost,$DBuser,$DBpass,$DBname);
if ($DBcon2->connect_errno) {
die("ERROR : -> ".$DBcon2->connect_error);
}
while ($record = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
array_push($ALLDATA, $record);
$queryb = "SELECT * FROM friendslist WHERE idOfPerson1='".$record['idOfUser']."' OR idOfPerson2='".$record['idOfUser']."' OR idOfPerson2='".$userRow['user_id']."' OR idOfPerson1='".$userRow['user_id']."' ORDER BY `id` DESC";
$result2 = mysqli_query($connect, $queryb);
$ALLDATA2 = array();
while ($record2 = mysqli_fetch_array($result2, MYSQLI_ASSOC)) {
array_push($ALLDATA2, $record2);
if($record['idOfUser'] == $userRow['user_id']) {
echo '<div>You Posted on '.$record['whenPosted'].'<br />'.$record['content'].'</div>';
}
elseif($record2['idOfPerson1'] == $userRow['user_id']) {
$query2 = $DBcon2->query("SELECT * FROM tbl_users WHERE user_id='".$record2['idOfPerson2']."'");
$userRow2=$query2->fetch_array();
echo '<div>'.$userRow2['username'].' Posted on '.$record['whenPosted'].'<br />'.$record['content'].'</div>';
}
elseif($record2['idOfPerson2'] == $userRow['user_id']) {
$query2 = $DBcon2->query("SELECT * FROM tbl_users WHERE user_id='".$record2['idOfPerson1']."'");
$userRow2=$query2->fetch_array();
echo '<div>'.$userRow2['username'].' Posted on '.$record['whenPosted'].'<br />'.$record['content'].'</div>';
}
}
mysqli_free_result($result2);
}
$DBcon2->close();
mysqli_free_result($result);
?>
Your schema looks good, but let's take another look at the relations. I'm going to rename some of the columns for convenience.
Users:
+- user_id
| user_name
|
|
| Friendships:
| fid
+- user_id
| friend_id --------+
| friendship_start |
| |
| |
| Statuses: |
| sid |
+- user_id ----------+
post_date
content
If you just wanted to find statuses of your friends, the query would look thus:
SELECT statuses.content
FROM friendships, statuses
WHERE friendship.user_id=? AND
friendships.friend_id = statuses.user_id
You would, of course, bind the appropriate user_id value when you ->prepare() the statement.
(See http://php.net/manual/en/mysqli.prepare.php for the proper way to do sql. You don't ever want to do something like mysql_query("select * from table where id=".$_POST['id']) because it's open to SQL injection)
Unfortunately, though, this does not include your own status in the query results. We'll have to do a little more work on the query...
SELECT statuses.content
FROM friendships, statuses
WHERE
( friends.user_id = ? AND
friends.friend_id = stuatuses.user_id )
OR
statuses.user_id = ?
ORDER BY statuses.post_date DESC
So far, so good... but we don't have the names nor the post date. The post date is easy, just add that to the select:
SELECT statuses.content, statuses.post_date
To add the name, we have to get data from Users also.
SELECT users.user_name, statuses.content, statuses.post_date
FROM users, friendships, statuses
WHERE
users.user_id = ? AND
(
( users.user_id = friendships.user_id AND
friendships.friend_id = statuses.user_id )
OR
statuses.user_id = users.user_id
)
ORDER BY statuses.post_date DESC
And there you have it; the database does all the work for you. No need for nested queries and such. This will just give you the simple list to print on your page. Please keep in mind that this is off the top of my head, so you may have to tweak it if I overlooked something.
I have a history table which contains changes made to data and the date the change was made - the history are the old values and the date the change was made.
There is then a main table which contains the current values.
The actual table layouts are like:
history
id user_id colour change_date
1 1 Red 2016-01-01
2 1 Blue 2016-04-05
3 1 Green 2016-08-05
and then:
current
user_id colour entry_date
1 Yellow 2015-10-14
I want to try and get a chronological list of all the values so the output for user_id 1 would look like:
2015-10-14 Red
2016-01-01 Blue
2016-04-05 Green
2016-08-05 Yellow
At the moment I am taking each user in the user table and checking if they have a history value - if they do not then the output simply is:
2015-10-14 Yellow
However, when they do have a history I am having to start with the entry date and assign the first colour history to that and storing in an array.
Then I take the first history date and get the second history value (via another query) and store that in the array and loop and so on - there are 150k users and each one can have 20 or 30 changes and it is horribly inefficient!
I would like to find a more efficient way of doing this either in PHP or MySQL - can anyone help?
Firstly, I am unsure what connection class you have decided to use but for this, I'll provide a class I wrote to query which uses PDO.
class Entity extends PDO {
public function __construct(
) {
try {
parent::__construct(
'mysql:host=localhost;dbname=example',
'db_user',
'db_pass'
);
} catch (PDOException $ex) {
die($ex->getMessage());
}
}
public function query(
$statement,
$values = array()
) {
$stmp = parent::Prepare($statement);
$stmp->execute($values);
return $stmp;
}
}
Now, assuming you already know the user_id you can begin to now query through the database to retrieve the change logs.
$con = new Entity();
$user_id_example = 1;
$sql = 'SELECT colour, change_date FROM history WHERE user_id = ? ORDER BY change_date ASC';
var_dump($con
->query($sql, [$user_id_example])
->fetchAll()
);
Update: If you're trying to get the newest change log you can add an LIMIT to your query
$sql = 'SELECT colour, change_date FROM history WHERE user_id = ? ORDER BY change_date ASC LIMIT 1';
Update: Note, if you want to bring results out of both table current and history, you can use an INNER JOIN
$sql = 'SELECT colour, change_date FROM history h INNER JOIN current c ON h.change_date = c.change_date WHERE user_id = ? ORDER BY change_date ASC';
I propose you two querys, in the first one you will have the colours ordered and in the second one you will have the dates ordered, so, in php you will have two arrays with the data and only remains to put together the data.
There is the code, only you have to set the conection to the database
$sqlColour = "select c.user_id, u.colour from current as c inner join ( select user_id, colour from history union all select user_id , colour from current ) u on c.user_id=u.user_id";
$sqlDate = "select c.user_id, u.date from current as c inner join ( select user_id, entry_date as date from current union all select user_id , change_date as date from history ) u on c.user_id=u.user_id";
$stmt = $db->query($sqlColour);
$colours = $stmt->fetchAll();
$stmt = $db->query($sqlDate);
$dates = $stmt->fetchAll();
$answer = array();
for ($i = 0; $i < count($colours); $i++) {
$answer[] = array("date" => $dates[$i]['date'], "colour" => $colours[$i]['colour'], "user_id" => $colours[$i]['user_id']);
}
echo $answer;
I have this 3 tables namely form, form_responses, metrics with the following structure
form
->id
->phone
->calldatetime
form_reponses
->id
->form_id
->metrics_id
->response
metrics
->id
->description
->question
And I want to make report with a format something like this
|Metrics Description|Metrics Question|Phone1|Phone2|Phone3|Phone4
|___________________|________________|______|______|______|______
| Sample | Sample | Yes | Yes | Yes | Yes
Is it possbile to this output just by the mysql query alone? Please note that the Phone1, Phone2, Phone3... is scaling horizontally. Originally I need that output in the excel file I have already tried this using Laravel PHP and http://www.maatwebsite.nl/laravel-excel/docs
$query = "SELECT id, phone FROM qcv.forms WHERE calldatetime >= '$from' AND calldatetime <= '$to' ORDER BY id ASC LIMIT 250 ;";
$phone = DB::connection('mysql')->select($query);
$metrics = Metric::all();
$metric_start = 10;
$start = "D";
$count = 10;
foreach ($phone as $key => $value2) // Populate Phone Numbers Horizontally
{
$sheet->cell($start.'9', $value2->phone);
// This will fill the responses for each number
foreach ($metrics as $key => $value)
{
$responses = FormResponses::where('form_id', '=', $value2->id)->where('metrics_id', '=', $value->id)->get();
$sheet->cell($start.$count, $responses[0]->response);
$count++;
}
$start++;
$count = 10;
}
foreach ($metrics as $key => $value) // Populate Metrics Vertically
{
$sheet->cell('C'.$metric_start, $value->question);
$sheet->cell('B'.$metric_start, $value->description);
$sheet->cell('A'.$metric_start, $value->metrics_name);
$metric_start++;
}
But seems this method is really slow especially in processing so I'm wondering if I could do the output in mysql command alone?
To get multiple sub-records per row in a one-to-many relationship using SQL, you would have to use a sub-query:
SELECT
m.description,
m.question,
(select phone from form f1 where f1.id = m.id and ...
/* some other unique criteria */) as Phone1,
(select phone from form f2 where f2.id = m.id and ...
/* some other unique criteria */) as Phone2,
(select phone from form f3 where f3.id = m.id and ...
/* some other unique criteria */) as Phone3,
(select phone from form f4 where f4.id = m.id and ...
/* some other unique criteria */) as Phone4
FROM metrics m
However...you may not have any columns to uniquely identify each form in this way...and your SQL engine may not allow a 3rd level of nesting of sub-queries (which is another way to individually select records from the same table).
So here's one other variation that would work. It should be slightly less code and fewer database connections, so it should perform better, even if you find it less intuitive. Here's the SQL portion:
SELECT
m.description,
m.question,
f.phone
FROM metrics m
INNER JOIN form f ON f.id = m.id
And then in PHP:
$lastid = '';
$phone_count = 0;
foreach ($record as $key => $value) {
$phone[$phone_count] = $value->phone;
$phone_count++;
if ($lastid != $value->id) {
// new record
$sheet->cell ( /* whatever */ );
$phone_count = 0;
}
$lastid = $value->id;
}
I have a mysql table consisting of users following other users.
USER_ID FOLLOW_ID
1 2
1 3
2 4
3 4
6 4
2 6
I am user No. 2 and the person in question is user No. 4. You see that three people are following user No.4 including me. I can query the users who are following user No.4. How can I add to the query if I am following these people or not? So what I would like to know is who are following a specific user (No.4 in this case) and which one of them I am following.
Desired result:
USER_ID (No.4 is FOLLOWED BY), DO_I_FOLLOW_HIM
2 No
3 No
6 Yes
As you see from the last record of the table I (No.2) am following User No.6.
Query the list of people following user No.4:
$myid = '6';
$userid = '4';
$i = 0;
try {
$stmt = $conn->prepare("SELECT USER_ID FROM FOLLOW WHERE FOLLOW_ID=?");
$stmt -> execute(array($userid));
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$row2[$i] = $row['USER_ID'];
$i++;
}
} catch(PDOException $e) {
echo 'ERROR: ' . $e->getMessage();
$response["success"] = 0;
}
Sample SQL Fiddle for help: http://sqlfiddle.com/#!2/97ac6
SELECT him.user_id, IF(i.follow_id,'yes','no') AS i_follow_him
FROM Follows him
LEFT JOIN Follows i ON (i.follow_id = him.user_id AND i.user_id = 2)
WHERE him.follow_id = 4
To get a list of users that you follow that are following another given user, you need to use a subquery. You can see it when written above you are asking two things
Who is following person A
From that list who am I following.
So You could try using a query like so,
Select User_ID, Follows_ID
From Follows
Where User_ID = ?
And Follow_ID In (Select User_ID From Follows Where Follow_ID = ?)
To see a list and whether you're following
Select f.User_ID,
Case When ? In (Select User_ID From Follows Where Follow_ID = f.User_ID) Then 'Yes' Else 'No' End
From Follows f
Where f.Follow_ID = ?
#Shudder makes a good point, you may see performance increases (especially if this is a large table) by using a join instead of a subquery.
SELECT him.user_id, IF(i.follow_id,'yes','no') AS i_follow_him
FROM Follows him
LEFT JOIN Follows i ON (i.follow_id = him.user_id AND i.user_id = ?)
WHERE him.follow_id = ?
I made it easier by adding your own id and user No 4.
select follow_id
from follows
where user_id=2 and follow_id IN (select user_id from follows where follow_id=4)
I'm loading comments for product with id = '3'
$get_comments = mysql_query("SELECT * FROM products_comments WHERE product_id = '3'");
Now I want to add the "report abuse" option for each comment, for this purpose I'm having another table as "abuse_reports" which user abuse reports will be stored in this table, now if a user reported a comment, the report abuse option should not be there for that comment for that user there anymore, for this I'm doing:
while($row = mysql_fetch_array($get_comments)){
echo blah blah blah // comment details
// now for checking if this user should be able to report this or not, i make this query again:
$check_report_status = mysql_query("SELECT COUNT(id) FROM abuse_reports WHERE reporter_user_id = '$this_user_id' AND product_id = 'this_product_id'");
// blah blah count the abuse reports which the current user made for this product
if($count == 0) echo "<a>report abuse</a>";
}
With the above code, for each comment I'm making a new query, and that's obviously wrong, how I should join the second query with the first one?
Thanks
Updated query (that is working now, commited by questioner)
SELECT pc. * , count( ar.`id` ) AS `abuse_count`
FROM `products_comments` pc
LEFT OUTER JOIN `abuse_reports` ar ON pc.`id` = ar.`section_details`
AND ar.`reporter_id` = '$user_id'
WHERE pc.`product_id` = '$product_id'
GROUP BY pc.`id`
LIMIT 0 , 30
The query works as follow: You select all the fields of your products_comments with the given product_id but you also count the entries of abuse_reports for the given product_id. Now you LEFT JOIN the abuse_reports, which means that you access that table and hang it on to the left (your products_comments table). The OUTER allows that there is no need for a value in the abuse_reports table, so if there is no report you get null, and therefore a count of 0.
Please read this:
However, I needed to group the results, otherwise you get only one merged row as result. So please extend your products_comments with a field comment_id of type int that is the primary key and has auto_increment.
UPDATE: abuse count
Now you can do two things: By looping through the results, you can see for each single element if it has been reported by that user or not (that way you can hide abuse report links for example). If you want the overall number of reports, you just increase a counter variable which you declare outside the loop. Like this:
$abuse_counter = 0;
while($row = mysql....)
{
$abuse_counter += intval($row['abuse_count']); // this is 1 or 0
// do whatever else with that result row
}
echo 'The amount of reports: '.$abuse_counter;
Just a primitive sample
I believe your looking for a query something like this.
SELECT pc.*, COUNT(ar.*)
FROM products_comments AS pc
LEFT JOIN abuse_reports AS ar ON reporter_user_id = pc.user_id AND ar.product_id = pc.product_id
WHERE product_id = '3'"
try this SQL
SELECT pc.*, COUNT(ar.id) AS abuse_count
FROM products_comments pc
LEFT JOIN abuse_reports ar ON pc.product_id = ar.product_id
WHERE pc.product_id = '3' AND ar.reporter_user_id = '$this_user_id'
GROUP BY pc.product_id
The result is list of products_comments with abuse_reports count if exist for reporter_user_id