How to write better this Mysql Join query - php

I have the next tables
Users {id, name}
Messages {id, user_id, cache_user_name}
What I want is to do a JOIN only when cache_user_name is NULL for performance reasons.
For example:
SELECT Messages.*, Users.name FROM Messages INNER JOIN Users ON (Messages.user_id = Users.id)
// ON (ISNULL(Messages.cache_user_name) AND ...
The best way is doing 2 queries? 1 for rows without cache (join) and the other for cached rows with a join?
[EDIT]
The result I need is:
Users
ID: 1, NAME: Wiliam
Messages
ID: 1, USER_ID: 1, CACHE_USER_NAME: Wiliam
ID: 2, USER_ID: 1, CACHE_USER_NAME: null
Result
ID: 1, USER_ID: 1, CACHE_USER_NAME: Wiliam, USERS.NAME: null // No join, faster
ID: 2, USER_ID: 1, CACHE_USER_NAME: null, USERS.NAME: Wiliam // Join

You can add WHERE ... IS NULLclause.
The optimizer will (try to) use the best performing plan.
SELECT Messages.*
, Users.name
FROM Messages
INNER JOIN Users ON (Messages.user_id = User.id)
WHERE Users.cache_user_name IS NULL
Edit
Given following data, what would you expect as output?
DECLARE #Users TABLE (ID INTEGER, Name VARCHAR(32))
DECLARE #Messages TABLE (ID INTEGER, User_ID INTEGER, Cache_User_Name VARCHAR(32))
INSERT INTO #Users VALUES (1, 'Wiliam')
INSERT INTO #Users VALUES (2, 'Lieven')
INSERT INTO #Users VALUES (3, 'Alexander')
INSERT INTO #Messages VALUES (1, 1, NULL)
INSERT INTO #Messages VALUES (2, 1, 'Cached Wiliam')
INSERT INTO #Messages VALUES (3, 2, NULL)
INSERT INTO #Messages VALUES (4, 3, 'Cached Alexander')
SELECT *
FROM #Users u
INNER JOIN #Messages m ON m.User_ID = u.ID
WHERE m.Cache_User_name IS NULL

SELECT m.Id, m.user_id, CACHE_USER_NAME user_name
FROM messages m
WHERE CACHE_USER_NAME IS NOT NULL
UNION ALL
SELECT m.Id, m.user_id, u.user_name user_name
FROM (Select * from messages Where cache_user_name IS NULL) m
JOIN users ON (u.user_id = m.user_id)
Anyway best approach store cache_user_name in table message during creating message. Then you will need join at all.

I think those joins in previous answers with a Not Null where clause should work fine, but maybe we're not following your in-efficiencies problem. As long as users.id and messages.user_id are indexed and of the same type, that join shouldn't be slow unless you have a huge user database and lots of messages. Throw more hardware at it if it is; likely you are running a lot of traffic and can afford it. :)
Alternatively, you could handle it like this: do a query on Messages where the name isn't null, run through the results, find the names for each message (and put them in an array), then query the User's table for just those names. Then as you loop over the Messages results you can display the proper name from the array you saved. You'll have two queries, but they'll be fast.
$users = $messages = $users_ids = array ();
$r = mysql_query('select * from Messages where cache_user_name is not null');
while ( $rs = mysql_fetch_array($r, MYSQL_ASSOC) )
{
$user_ids[] = $rs['user_id'];
$messages[] = $rs;
}
$user_ids = implode ( ',', $user_ids );
$u = mysql_query("select * from Users where id in ($users)");
while ( $rs = mysql_fetch_array($r, MYSQL_ASSOC) )
{
$users[$rs['id']] = $rs['name'];
}
foreach ( $messages as $message )
{
echo "message {$message['id']} authored by " . $users[$message['user_id']] . "<br />\n";
}

Related

How to select one value from a two table join?

I'm trying to achieve something in Laravel/MySQL and cannot seem to be pointed in the right direction for a solution. I can achieve what I am looking for with subqueries, but I have been told they are not as efficient as joins. And, I'm going to have to convert the solution for this into Eloquent/Query Builder, and the way I have it working with subqueries and unions doesn't seem to convert easily.
What I am trying to do is select one row from two possible tables, based on the created_at date of the row. I want to join this created_at value with my users table as a new column called started_at. Here is some sample data and how I can achieve the query with a subquery/union of the two possible tables that I can get the data from:
CREATE TABLE users (
id INTEGER,
first_name TEXT,
last_name TEXT
);
INSERT INTO users (id, first_name, last_name)
VALUES
(1, 'Craig', 'Smith'),
(2, 'Bill', 'Nye'),
(3, 'Bloop', 'Blop');
CREATE TABLE old_activity (
id INTEGER,
user_id INTEGER,
firm_id INTEGER,
amount INTEGER,
created_at DATE
);
INSERT INTO old_activity (id, user_id, firm_id, amount, created_at)
VALUES
(1, 1, 3, 5.24, '2019-04-29'),
(2, 2, 7, 4, '2019-03-28'),
(3, 3, 4, 6.99, '2019-04-28');
CREATE TABLE new_activity (
id INTEGER,
user_id INTEGER,
firm_id INTEGER,
plays INTEGER,
saves INTEGER,
created_at DATE
);
INSERT INTO new_activity (id, user_id, firm_id, plays, saves, created_at)
VALUES
(1, 1, 3, 10, 1, '2019-04-27'),
(2, 2, 3, 12, 2, '2019-03-29'),
(3, 3, 3, 6, 3, '2019-04-27');
CREATE TABLE firms (
id INTEGER,
name TEXT
);
INSERT INTO firms (id, name)
VALUES
(1, 'apple'),
(2, 'banana'),
(3, 'orange');
select
id,
first_name,
last_name,
(select created_at from old_activity
where user_id = users.id
union
select created_at from new_activity
where user_id = users.id
order by created_at asc
limit 1) as started_at
from users
The query should only return the oldest created_at for a particular user in one of the two activity tables.
How can I achieve this with a join? Any help with this would be greatly appreciated.
Hmmm . . . You could always use window functions:
select u.*, a.*
from users u left join
(select a.*,
row_number() over (partition by a.user_id order by a.created_at desc) as seqnum
from ((select oa.* from old_activity oa) union all
(select na.* from new_activity na)
) a
) a
on a.user_id = a.id and a.seqnum = 1

Not able to get distinct and max of the third table join in MySql

Schemas
// First table
CREATE TABLE assignments (
id int,
uid int,
comments varchar(255),
assignmentdate date,
status int
);
INSERT INTO assignments (id, uid, comments, assignmentdate, status)
values (1, 6, 'a', '2019-07-15', 0), (2, 6, 'ab', '2019-07-15', 0),
(3, 6, 'abc', '2019-07-14', 0), (4, 6, 'abc', '2019-07-14', 1)
, (5, 7, 'xyz', '2019-07-14', 1), (6, 7, 'zyx', '2019-07-14', 1);
// Second table
CREATE TABLE users (
id int,
username varchar(255),
status int
);
INSERT INTO users (id, username, status)
values (6, 'user1', 0), (7, 'user2', 0),
(8, 'user3', 1);
// Third table
CREATE TABLE user_images (
id int,
uid int,
imagename varchar(255),
status int
);
INSERT INTO user_images (id, uid, imagename, status)
values (1, 6, 'abc.jpeg', 0), (2, 6, 'def.jpeg', 0), (3, 8, 'ghi.png', 1);
what I'm looking for here is to get
1) distinct and latest row of table assignments which,
2) joins the table users and get a row and then joins,
3) distinct and latest row of table user_images.
So far i have gone through this answer
My trial query:
SELECT
p.*,
u.username,
groupedpi.*
FROM
assignments p
INNER JOIN(
SELECT
comments,
MAX(id) AS latest
FROM
assignments
WHERE
STATUS
= 0
GROUP BY
uid
) AS groupedp
ON
groupedp.latest = p.id
LEFT JOIN users u ON
p.uid = u.id AND u.status = 0
LEFT JOIN(
SELECT
uid,
MAX(id) AS latesti,
imagename
FROM
user_images us
WHERE
STATUS = 0
GROUP BY
uid
order by id desc LIMIT 1
) AS groupedpi
ON
groupedpi.uid = p.uid
Output:
The 3rd result I'm not getting, i.e I'm not getting the distinct and latest record of the third table while joining.
Instead of abc.jpeg, I want to get def.jpeg.
MySQL is tripping you up here, because it automatically adds columns to GROUP BY if they aren't specified, so it's grouping the groupedpi subquery on imagename too - this will lead to duplicated rows. Remove the imagename column from the subquery (and the order by clause is irrelevant too) and have it just output the userid and the max image id
If you want the image name, join the images table in again on images.id = groupedpi.latesti (In the main query not the subquery that is finding the latest image id)
(Note that your screenshot says lastesti 2 but imagename abc- it's not the right pairing. ID 2 is def.jpg. When you want latest Id but also other data from the same row you can't do it in one hit unless you use an analytic (mysql8+) - you have to write a subquery that finds the max id and then join it back to the same table to get the rest of the data off that row)

Build MySql Query with multiple where conditions

I have to build a some complicated mysql query. I have a table with some user informations, with 3 columns: id, id_user, id_campo, valore. So, for example:
1, 1, 1, "Roberto" (where id_campo=1 is for the user name);
2, 2, 1, "Luca";
3, 1, 2, "Windows"; (where id_campo=2 is for used OS);
4, 2, 2, "Linux";
and so on.
Now, I have to select users where: name="Roberto" AND os="Linux", but the same user :
SELECT id_user WHERE (id_campo=1 AND valore="Roberto") OR (id_campo=2 AND valore="Linux").
In this sample case, the query return id_user=1 and id_user=2 but I would obtain no result. How I can modify the query so I can include the condition "the same user" ?
Thanks!
You need to build two "subqueries" and then join them by user id.
SELECT a.id_user
from (
SELECT id_user FROM table
WHERE (id_campo=1 AND valore="Roberto")
) a,
(
SELECT id_user FROM table
WHERE (id_campo=2 AND valore="Linux")
) b
WHERE a.is_user = b.id_user
You have a wrong db design but anyway you can usie an inner join on the same table based on id_user
select a.id_user, a.id_campo, a.valore
from my_table as a
inner join my_table as b on .id_user = b-id_user
where ( a.id_campo = 1 and a.valore ='Roberto')
AND ( b.id_campo = 2 and b.valore ='Linux')

update (or insert) multiple rows in one query

I am trying to update multiple rows in a table at once (or insert them if they do not exist) given settings and an array of user id's.
An example of how I select all rows from the settings table for all users of specific computers of a specific account. This is the result set which I need to insert values in or update if they exist.
$stmt = $db->prepare("
SELECT settings.*
FROM
( SELECT account_id, computer_id
FROM computers
ORDER BY computer_id ASC LIMIT 0, ".$_SESSION['user']['licenses']."
) as c
LEFT JOIN users
on users.computer_id = c.computer_id
LEFT JOIN accounts
on accounts.account_id = c.account_id
LEFT JOIN settings
on settings.user_id = users.user_id
WHERE accounts.account_id = ".$_SESSION['user']['account_id']."
");
What I am trying to do :
I am trying to update/insert three columns (enabled, status, and user_id) in the settings table for only those user ids listed in the array. Enabled and status values will be the same for all, but the user_id will be different for each.
$users = array(12, 36, 43, 56, 76)
$binding = array(
'enabled' => 1,
'status' => 2,
'user_id' => from the array
);
If my thought process is correct I can create a virtual table from my statement listed above then use ON DUPLICATE KEY UPDATE to insert/update on the results of that virtual table?
Is this possible? If so, example, tips, or point in the right direction? The way I have working involves a foreach loop for each user id in the given array so there will be x number of queries depending on its count. If I can cut that to only one that would be great!
UPDATE:
Ok, I am totally confused now... this is an attempt for defined values which is not working... and I still need a way to do so for every user_id in my array as mentioned above. Don't I need to 'save' my entire FROM clause AS a new reference as well?
$stmt = $db->prepare("
INSERT INTO settings (user_id, enabled, status)
VALUES (:user_id, :enabled, :alert_user)
SELECT user_id, enabled, status
FROM
( SELECT account_id, computer_id
FROM computers
ORDER BY computer_id ASC LIMIT 0, ".$_SESSION['user']['licenses']."
) as c
LEFT JOIN users
on users.computer_id = c.computer_id
LEFT JOIN accounts
on accounts.account_id = c.account_id
LEFT JOIN settings
on settings.user_id = users.user_id
WHERE accounts.account_id = ".$_SESSION['user']['account_id']."
ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), status = VALUES(status)
");
$binding = array(
'enabled' => 1,
'alert_user' => 4,
'user_id' => 6
);
$stmt->execute($binding);
Yes, your thinking is correct. It should be something like this:
INSERT INTO settings (user_id, enabled, status)
SELECT user_id, enabled, status
FROM ... -- rest of your query here
ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), status = VALUES(status)
VALUES(colname) in the ON DUPLICATE KEY UPDATE clause gets the value that would have been inserted if there hadn't been a dupliate.
OK, based on your comment, I think this may be what you want:
INSERT INTO settings (user_id, enabled, status)
SELECT :user_id, :enabled, :status
FROM ...
JOIN ...
JOIN ...
WHERE ...
ON DUPLICATE KEY UPDATE enabled = VALUES(enabled), status = VALUES(status)
If the join returns any rows, this will insert or update the specified row. If it doesn't find any rows, no insert/update will be done.
You should probably be using inner joins rather than left joins, if you don't want anything returned when there are no matches in the tables being joined with.

MySQL query for selecting a maximum element

I have a table with 4 columns: place_id, username, counter, last_checkin
I'm writing a check-in based system and I'm trying to get a query that will give me the "mayor" of each place. The mayor is the one with most check-ins, and if there is more than 1 than the minimum last_checkin wins.
For example, if I have:
place_id, username, counter, last_checkin
123, tom, 3 , 13/4/10
123, jill, 3, 14/4/10
365, bob, 2, 15/4/10
365, alice, 1, 13/4/10
I want the result to be:
123, tom
365, bob
I'm using it in PHP code
Here is the test data:
CREATE TABLE `my_table` ( `place_id` int(11), `username` varchar(50), `counter` int(11), `last_checkin` date);
INSERT INTO `my_table` VALUES (123,'tom',3,'2010-04-13'),(123,'jill',3,'2010-04-14'),(365,'bob',2,'2010-04-15'),(365,'alice',1,'2010-04-13');
How about..
SELECT
place_id,
(SELECT username
FROM my_table MT2
WHERE MT2.place_id = MT1.place_id
ORDER BY counter DESC, last_checkin ASC
LIMIT 1) AS mayor
FROM my_table MT1
GROUP BY place_id;
Edited as Unreason suggests to have ascending order for last_checkin.
Brian's correlated query is something I would write. However I found this different take and it might perform differently depending on the data
SELECT
mt1.place_id,
mt1.username
FROM
my_table mt1 LEFT JOIN my_table mt2
ON mt1.place_id = mt2.place_id AND
(mt1.counter < mt2.counter OR
(mt1.counter = mt2.counter AND mt1.last_checkin > mt2.last_checkin)
)
WHERE
mt2.place_id IS NULL
Which uses left join to get to the top records according to certain conditions.
$data = query("SELECT max(counter) counter,username FROM table GROUP By place_id ORDER By last_checkin DESC");

Categories