The requirement is to find the first available identifier where an identifier is an alphanumeric string, such as:
ABC10000
ABC10345
ABC88942
ABC90123
The database table has a structure such as:
id, user, identifier
Note that the alpha component ABC is consistent throughout and won't change. The numeric component should be between 10000 and 99999.
How best to tackle this? It does not seem like an overly complex problem - looking for simplest solution using either just MySQL or a combination of SQL and PHP. The current solution pulls each record from the database into an array and then loops from 10000 onwards, prepending ABC and checking availability, which seems like it could be improved significantly.
Edit: Original question was not clear enough in that a certain amount of identifiers have been assigned already, and I am looking to fill in the gaps. From the short list I provided, the next available would be ABC10001. Eventually, however, it would be ABC10346 and then ABC88943 and so on
Edit: Sorry for a poorly structured question. To avoid any further confusion, here is the actual table structure:
CREATE TABLE IF NOT EXISTS `User_Loc` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user` int(11) DEFAULT NULL,
`value` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UNIQ_64FB41DA17323CBC` (`user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=4028 ;
You have to self join the table and look for the first NULL value in the joined table
SELECT CONCAT('ABC', SUBSTRING(t1.value, 4)+1) AS next_value
FROM test t1
LEFT JOIN test t2 on SUBSTRING(t1.value, 4)+1 = SUBSTRING(t2.value, 4)
WHERE ISNULL(t2.value)
ORDER BY t1.value ASC
LIMIT 1
http://sqlfiddle.com/#!2/d69105/22
edit
With the comment about some 'specialities' at ncatnow. There are slight adjusments to make with the help of subselects for ridding the 'ABC' and UNION for having a default value
SELECT
CONCAT('ABC', t1.value+1) AS next_value
FROM
((SELECT '09999' AS value) UNION (SELECT SUBSTRING(value, 4) AS value FROM test)) t1
LEFT JOIN
((SELECT '09999' AS value) UNION (SELECT SUBSTRING(value, 4) AS value FROM test)) t2
ON t1.value+1 = t2.value
WHERE
ISNULL(t2.value)
AND t1.value >= '09999'
ORDER BY t1.value ASC
LIMIT 1
http://sqlfiddle.com/#!2/28acf6/50
Similar to the above reply by #HerrSerker, but this will cope with existing identifiers which have the numeric part starting with a zero.
SELECT CONCAT('ABC',SUBSTRING(CONCAT('00000', CAST((CAST(SUBSTRING(a.identifier, 4) AS SIGNED) + 1) AS CHAR)), -5)) AS NextVal
FROM SomeTable a
LEFT OUTER JOIN SomeTable b
ON b.identifier = CONCAT('ABC',SUBSTRING(CONCAT('00000', CAST((CAST(SUBSTRING(a.identifier, 4) AS SIGNED) + 1) AS CHAR)), -5))
WHERE b.identifier IS NULL
ORDER BY NextVal
LIMIT 1
what comes to my mind is one table with all indentifiers and use this sql
SELECT identifier FROM allIdentifiersTable WHERE identifier NOT IN (SELECT identifier FROM yourTable) LIMIT 1
Reconsidering this from your edit, you should go the PHP route and add another table or other means to store the last filled id:
$identifier = 0;
$i = mysql_query("SELECT identifier FROM last_identifier");
if ($row = mysql_fetch_assoc($i)) $identifier = $row["identifier"];
if ($identifier < 10000) $identifier = 10000;
do {
$identifier += 1;
$result = mysql_query("
INSERT IGNORE INTO table (id, user, identifier)
VALUES ('[...]', '[...]',
'ABC" . str_pad($identifier, 5, "0", STR_PAD_LEFT) . "'
)");
if (mysql_affected_rows($result) < 1) continue;
} while (false);
mysql_query("UPDATE last_identifier SET identifier = '$identifier'");
Of course, you need to add a UNIQUE index on the identifier field.
Related
I have a table called Elements
id reference
101 AES/JN/2001
102 AMES/JN/2001
103 AES/JN/2002
104 AES/JN/2003
105 AMES/JN/2002
I want to get the maximum number from the string. If my search key word is AMES/JN I should get 2002. And If my key word is AES/JN then output should be 2003
I have tried the following code:
select max(convert(substring_index(reference,'/', -1), unsigned)) as max
FROM Elements WHERE reference like 'AES/JN/'
I almost agree with Shyam except for that horribly convoluted function.
I recommend this query:
SELECT SUBSTRING_INDEX(reference,'/',-1) as `max`
FROM `Elements`
WHERE reference LIKE 'AES/JN/%'
ORDER BY reference DESC
LIMIT 1
This will output a single row with with 2003 as the value in the max column.
The reason I like this method is because CONVERT() is omitted/unnecessary.
I've compared my query against Xenofexs' on my server and mine is only .0001 seconds faster -- but this is only running on the 5 rows that the OP posted. As the database volume increases, I am confident that my query's performance lead will increase.
Even if you don't care about the micro-optimization, I think this query is easier to read/comprehend because it doesn't have a function inside a function inside a function.
In fact, I believe this next query may outperform my above query:
SELECT SUBSTRING_INDEX(reference,'/',-1) as `max`
FROM `Elements`
WHERE LOCATE('AES/JN/',reference)
ORDER BY reference DESC
LIMIT 1
Because LOCATE() will be checking the leading characters from the reference column, and the targeted substring will not occur later in the string, LOCATE() has been benchmarked to outperform LIKE.
Additional reading:
MySQL LIKE vs LOCATE
For the record, here is the table that I used:
CREATE TABLE `Elements` (
`id` int(10) NOT NULL,
`reference` varchar(100) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
INSERT INTO `Elements` (`id`, `reference`) VALUES
(101, 'AES/JN/2001'),
(102, 'AMES/JN/2001'),
(103, 'AES/JN/2002'),
(104, 'AES/JN/2003'),
(105, 'AMES/JN/2002');
ALTER TABLE `Elements`
ADD PRIMARY KEY (`id`);
ALTER TABLE `Elements`
MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=106;
Please check how "LIKE" work's.
You can use % as joker
Just change your query and add % character. And it's work
SELECT
max(
CONVERT (
substring_index(reference, '/', - 1),
UNSIGNED
)
) AS max
FROM
reference
WHERE
reference LIKE 'AES/JN/%'
Please note : LIKE 'AES/JN/%'
Please find below solution.
Query:
select id,reference,digits(reference) as num_values from asasas where reference like 'AMES/JN%' order by num_values DESC limit 1
You need to create one function in mysql
DELIMITER $$
DROP FUNCTION IF EXISTS `test`.`digits`$$
CREATE FUNCTION `digits`( str CHAR(32) ) RETURNS char(32) CHARSET latin1
BEGIN
DECLARE i, len SMALLINT DEFAULT 1;
DECLARE ret CHAR(32) DEFAULT '';
DECLARE c CHAR(1);
IF str IS NULL
THEN
RETURN "";
END IF;
SET len = CHAR_LENGTH( str );
REPEAT
BEGIN
SET c = MID( str, i, 1 );
IF c BETWEEN '0' AND '9' THEN
SET ret=CONCAT(ret,c);
END IF;
SET i = i + 1;
END;
UNTIL i > len END REPEAT;
RETURN ret;
END$$
DELIMITER ;
Let me know if it not works for you
SELECT MAX(Z.COUNT),reference FROM
(
SELECT reference,CAST(SUBSTRING_INDEX(reference, '/', -1) AS DECIMAL) count
FROM Elements where reference like 'AES/JN/%'
)Z
Try above code.
Hope this helps.
I have an UPDATE query where I explicitely reference the database, but MySQL still complains with the message: ERROR 1046 (3D000): No database selected.
Other queries that are similar of structure, but use an INSERT work fine. Other queries that only perform SELECTs also run fine.
To repeat the problem in a test case, try running these queries:
create table test.object1 (
id_object1 int unsigned not null auto_increment,
total int,
weight int,
dt datetime,
primary key (id_object1)
) engine=InnoDB;
create table test.object2 (
id_object2 int unsigned not null auto_increment,
primary key (id_object2)
) engine=InnoDB;
create table test.score (
id_object1 int unsigned not null,
id_object2 int unsigned not null,
dt datetime,
score float,
primary key (id_object1, id_object2),
constraint fk_object1 foreign key (id_object1) references object1 (id_object1),
constraint fk_object2 foreign key (id_object2) references object2 (id_object2)
) engine=InnoDB;
insert into test.object1 (id_object1, total, weight, dt) values (1, 0, 0, '2012-01-01 00:00:00');
insert into test.object1 (id_object1, total, weight, dt) values (2, 0, 0, '2012-01-02 00:00:00');
insert into test.object2 (id_object2) values (1);
insert into test.score (id_object1, id_object2, dt, score) values (1, 1, '2012-01-03 00:00:00', 10);
insert into test.score (id_object1, id_object2, dt, score) values (2, 1, '2012-01-04 00:00:00', 8);
update test.object1 p
join (
select ur.id_object1, sum(ur.score * ur.weight) as total, count(*) as weight
from (
select lur.*
from (
select s.id_object1, s.id_object2, s.dt, s.score, 1 as weight
from test.score as s
join test.object1 as o1 using(id_object1)
where s.dt > o1.dt
order by s.id_object1, s.id_object2, s.dt desc
) as lur
group by lur.id_object2, lur.id_object1, date(lur.dt)
order by lur.id_object1, lur.id_object2
) as ur
group by ur.id_object1
) as r using(id_object1)
set
p.total = p.total + r.total,
p.weight = p.weight + r.weight,
p.dt = now();
Note: I'm running these queries from a PHP environment and I have NOT explicitely used mysql_select_db('test'), because I prefer not to and none of the other (many!) queries require it. I'm sure that using mysql_select_db would solve my issue, but I would like to know why exactly this particular query does not work.
For comparison sake: if you'd run this simpler query, also without using mysql_select_db, everything works fine:
update test.object1 set total=1, weight=1, dt=now() where id_object1=1;
I've searched to no avail. The only thing I found that came close, was this bug report: http://bugs.mysql.com/bug.php?id=28551 and especially that last (unanswered) message...
You have fields named incorrectly, but even if you correct them, this is a bug in MySQL that won't let you do it if you don't have default database.
update test.object1 p
join (
select ur.id_object1, sum(ur.score * ur.weight) as total, count(*) as weight
from (
select lur.*
from (
select s.id_object1, s.id_object2, s.dt, s.score, 1 as weight
from test.score as s
join test.object1 as o1
using (id_object1)
where s.dt > o1.dt
order by
s.id_object1, s.id_object2, s.dt desc
) as lur
group by
lur.id_object1, lur.id_object1, date(lur.dt)
order by
lur.id_object1, lur.id_object1
) as ur
group by ur.id_object1
) as r
USING (id_object1)
SET p.total = p.total + r.total,
p.weight = p.weight + r.weight,
p.dt = now();
The problem is specific to UPDATE with double-nested queries and no default database (SELECT or single-nested queries or default database work fine)
You have some wrong field names in the UPDATE statement -
What is s.object? Shouldn't it be s.id_object2?
What is lur.object1? Shouldn't it be lur.id_object1?
What is lur.object2? Shouldn't it be lur.id_object2?
What is ur.id_object at the end?
Fix all these issues and try to update again;-)
First time I ran this script I got that error. My output:
1 row inserted [0,184s]
1 row inserted [0,068s]
1 row inserted [0,066s]
1 row inserted [0,147s]
1 row inserted [0,060s]
Error (32,1): No database selected
When I set default database name the problem disappeared.
Remember that you cannot use foreign keys when the Engine is set to MyISAM. Not only does the table that you are creating a foreign key in need to be InnoDB, but the table you are getting the key from also needs to be InnoDB.
I was getting the same error as you and pulling my hair out for days before I thought of this. I went into each of my tables and made sure the Engines were set to InnoDB for each one, and now I have no issues setting up foreign keys.
I have a query that gets all the info I need for a messaging system's main page (including unread message count, etc)... but it currently retrieves the original threads message. I would like to augment the below query to grab the most recent message in each thread instead.
This query is very close, however my mediocre SQL skills are keeping me from wrapping things up...
$messages = array();
$unread_messages_total = 0;
$messages_query = "
SELECT m.*
, COUNT(r.id) AS num_replies
, MAX(r.datetime) AS reply_datetime
, (m.archived NOT LIKE '%,".$cms_user['id'].",%') AS message_archive
, (m.viewed LIKE '%,".$cms_user['id'].",%') AS message_viewed
, SUM(r.viewed NOT LIKE '%,".$cms_user['id'].",%') AS unread_replies
, CASE
WHEN MAX(r.datetime) >= m.datetime THEN MAX(r.datetime)
ELSE m.datetime
END AS last_datetime
FROM directus_messages AS m
LEFT JOIN directus_messages as r ON m.id = r.reply
WHERE m.active = '1'
AND (m.to LIKE '%,".$cms_user['id'].",%' OR m.to = 'all' OR m.from = '".$cms_user['id']."')
GROUP BY m.id
HAVING m.reply = '0'
ORDER BY last_datetime DESC";
foreach($dbh->query($messages_query) as $row_messages){
$messages[] = $row_messages;
$unread_messages_total += (strpos($row_messages['archived'], ','.$cms_user['id'].',') === false && ( (strpos($row_messages['viewed'], ','.$cms_user['id'].',') === false && $row_messages['unread_replies'] == NULL) || ($row_messages['unread_replies']>0 && $row_messages['unread_replies'] != NULL) ) )? 1 : 0;
}
Thanks in advance for any help you can provide!
EDIT: (Database)
CREATE TABLE `cms_messages` (
`id` int(10) NOT NULL auto_increment,
`active` tinyint(1) NOT NULL default '1',
`subject` varchar(255) NOT NULL default '',
`message` text NOT NULL,
`datetime` datetime NOT NULL default '0000-00-00 00:00:00',
`reply` int(10) NOT NULL default '0',
`from` int(10) NOT NULL default '0',
`to` varchar(255) NOT NULL default '',
`viewed` varchar(255) NOT NULL default ',',
`archived` varchar(255) NOT NULL default ',',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
EDIT 2: (Requirements)
Return all parent messages for a specific user_id: $cms_user['id']
Return the number of replies for that parent message: num_replies
Return the number of unread replies for that parent message: unread_replies
Return the date of the parent message or it's most recent reply: last_datetime
Return whether the message is in the archive: message_archive
Return whether the message has been viewed: message_viewed
Return all messages in DESC datetime order
Return the newest message, from the parent or replies if there are some (like gmail)
If you have only 2 levels of messages (i.e., only parent messages and direct answers), you might try this query:
select
root_message.id,
root_message.active,
root_message.subject,
case
when max_reply_id.max_id is null then
root_message.message
else
reply_message.message
end as message,
root_message.datetime,
root_message.reply,
root_message.from,
root_message.to,
root_message.viewed,
root_message.archived
from
-- basic data
cms_messages as root_message
-- ID of last reply for every root message
left join (
select
max(id) as max_id,
reply as parent_id
from
cms_messages
where
reply <> 0
group by
reply
) as max_reply_id on max_reply_id.parent_id = root_message.id
left join cms_messages as reply_message on reply_message.id = max_reply_id.max_id
where
root_message.reply = 0
It uses subquery max_reply_id as source of data to select ID of the latest answer. If it exists (i.e., if there are answers), reply_message.message is used. If it does not exist (no answer has been found for root message), then root_message.message is used.
You should also think about structure of table. E.g., it would make more sense if reply contained either NULL, if it is parent message, or ID of existing message. Currently, you set it to 0 (ID of non-existent message), which is wrong. Types of viewed and archived are also weird.
Edit: you should also avoid using having clause. Use where instead, when possible.
Here's a new query that should fulfil your requirements. If there is any problem with it (i.e., if it returns wrong data), let me know.
Like the first query, it:
uses subquery reply_summary to accumulate data about replies (ID of last reply, number of replies and number of unread replies);
joins this subquery to the base table;
joins cms_messages as reply_message to the subquery, based on reply_summary.max_reply_id, to get data about the last reply (message, datetime).
I've simplified the way how you determine last_datetime - it now takes either time of last reply (if there is any reply), or time of original post (when no replies are found).
I have not filtered replies by from and to fields. If it is necessary, where clause of reply_summary subquery should be updated.
select
parent_message.id,
parent_message.subject,
parent_message.message,
parent_message.from,
parent_message.to,
coalesce(reply_summary.num_replies, 0) as num_replies,
last_reply_message.datetime as reply_datetime,
(parent_message.archived NOT LIKE '%,{$cms_user['id']},%') AS message_archive,
(parent_message.viewed LIKE '%,{$cms_user['id']},%') AS message_viewed,
reply_summary.unread_replies,
coalesce(last_reply_message.message, parent_message.message) as last_message,
coalesce(last_reply_message.datetime, parent_message.datetime) as last_datetime
from
cms_messages as parent_message
left join (
select
reply as parent_id,
max(id) as last_reply_id,
count(*) as num_replies,
sum(viewed not like '%,{$cms_user['id']},%') as unread_replies
from
cms_messages
where
reply <> 0 and
active = 1
group by
reply
) as reply_summary on reply_summary.parent_id = parent_message.id
left join cms_messages as last_reply_message on last_reply_message.id = reply_summary.last_reply_id
where
parent_message.reply = 0 and
parent_message.active = 1 and
(parent_message.to like '%,{$cms_user['id']},%' or parent_message.to = 'all' or parent_message.from = '{$cms_user['id']}')
order by
last_datetime desc;
your problem is that you are fetching only m records no matter what the order of the r records.
try adding
SELECT m.*, r.*
or
SELECT r.*, m.*
if you are using PDO::FETCH_ASSOC as your PDO fetch mode (assuming you are using PDO to access your database), the result will be an associative array where if the result set contains multiple columns with the same name, PDO::FETCH_ASSOC returns only a single value per column name. not sure which order takes presidence, so you would have to try both.
if your columns are defined in the right order, they will return the r.* value if one exists, or the m.* value if no r records exist. does this make sense? this way your result set will contain the latest record no matter which table (m or r) contains them.
http://www.php.net/manual/en/pdo.constants.php
I am afraid that you wont be able to solve this problem with a single query. Either you have to use more queries and gather the informations in the surrounding code or you will have to redesign the database structure for your messaging system a litte (tables: threads, posts, etc.). If you decide to redesign the database structure, you should also take care of the way you handle the viewed and archived fields. The way you use the fields (varchar 255 only!) might work for some users, but as soon as there are more users and higher user IDs your message system will break down.
This post is taking a substantial amount of time to type because I'm trying to be as clear as possible, so please bear with me if it is still unclear.
Basically, what I have are a table of posts in the database which users can add privacy settings to.
ID | owner_id | post | other_info | privacy_level (int value)
From there, users can add their privacy details, allowing it to be viewable by all [privacy_level = 0), friends (privacy_level = 1), no one (privacy_level = 3), or specific people or filters (privacy_level = 4). For privacy levels specifying specific people (4), the query will reference the table "post_privacy_includes_for" in a subquery to see if the user (or a filter the user belongs to) exists in a row in the table.
ID | post_id | user_id | list_id
Also, the user has the ability to prevent some people from viewing their post in within a larger group by excluding them (e.g., Having it set for everyone to view but hiding it from a stalker user). For this, another reference table is added, "post_privacy_exclude_from" - it looks identical to the setup as "post_privacy_includes_for".
My problem is that this does not scale. At all. At the moment, there are about 1-2 million posts, the majority of them set to be viewable by everyone. For each post on the page it must check to see if there is a row that is excluding the post from being shown to the user - this moves really slow on a page that can be filled with 100-200 posts. It can take up to 2-4 seconds, especially when additional constraints are added to the query.
This also creates extremely large and complex queries that are just... awkward.
SELECT t.*
FROM posts t
WHERE ( (t.privacy_level = 3
AND t.owner_id = ?)
OR (t.privacy_level = 4
AND EXISTS
( SELECT i.id
FROM PostPrivacyIncludeFor i
WHERE i.user_id = ?
AND i.thought_id = t.id)
OR t.privacy_level = 4
AND t.owner_id = ?)
OR (t.privacy_level = 4
AND EXISTS
(SELECT i2.id
FROM PostPrivacyIncludeFor i2
WHERE i2.thought_id = t.id
AND EXISTS
(SELECT r.id
FROM FriendFilterIds r
WHERE r.list_id = i2.list_id
AND r.friend_id = ?))
OR t.privacy_level = 4
AND t.owner_id = ?)
OR (t.privacy_level = 1
AND EXISTS
(SELECT G.id
FROM Following G
WHERE follower_id = t.owner_id
AND following_id = ?
AND friend = 1)
OR t.privacy_level = 1
AND t.owner_id = ?)
OR (NOT EXISTS
(SELECT e.id
FROM PostPrivacyExcludeFrom e
WHERE e.thought_id = t.id
AND e.user_id = ?
AND NOT EXISTS
(SELECT e2.id
FROM PostPrivacyExcludeFrom e2
WHERE e2.thought_id = t.id
AND EXISTS
(SELECT l.id
FROM FriendFilterIds l
WHERE l.list_id = e2.list_id
AND l.friend_id = ?)))
AND t.privacy_level IN (0, 1, 4))
AND t.owner_id = ?
ORDER BY t.created_at LIMIT 100
(mock up query, similar to the query I use now in Doctrine ORM. It's a mess, but you get what I am saying.)
I guess my question is, how would you approach this situation to optimize it? Is there a better way to set up my database? I'm willing to completely scrap the method I have currently built up, but I wouldn't know what to move onto.
Thanks guys.
Updated: Fix the query to reflect the values I defined for privacy level above (I forgot to update it because I simplified the values)
Your query is too long to give a definitive solution for, but the approach I would follow is to simply the data lookups by converting the sub-queries into joins, and then build the logic into the where clause and column list of the select statement:
select t.*, i.*, r.*, G.*, e.* from posts t
left join PostPrivacyIncludeFor i on i.user_id = ? and i.thought_id = t.id
left join FriendFilterIds r on r.list_id = i.list_id and r.friend_id = ?
left join Following G on follower_id = t.owner_id and G.following_id = ? and G.friend=1
left join PostPrivacyExcludeFrom e on e.thought_id = t.id and e.user_id = ?
(This might need expanding: I couldn't follow the logic of the final clause.)
If you can get the simple select working fast AND including all the information needed, then all you need to do is build up the logic in the select list and where clause.
Had a quick stab at simplifying this without re-working your original design too much.
Using this solution your web page can now simply call the following stored procedure to get a list of filtered posts for a given user within a specified period.
call list_user_filtered_posts( <user_id>, <day_interval> );
The whole script can be found here : http://pastie.org/1212812
I haven't fully tested all of this and you may find this solution isn't performant enough for your needs but it may help you in fine tuning/modifying your existing design.
Tables
Dropped your post_privacy_exclude_from table and added a user_stalkers table which works pretty much like the inverse of user_friends. Kept the original post_privacy_includes_for table as per your design as this allows a user restrict a specific post to a subset of people.
drop table if exists users;
create table users
(
user_id int unsigned not null auto_increment primary key,
username varbinary(32) unique not null
)
engine=innodb;
drop table if exists user_friends;
create table user_friends
(
user_id int unsigned not null,
friend_user_id int unsigned not null,
primary key (user_id, friend_user_id)
)
engine=innodb;
drop table if exists user_stalkers;
create table user_stalkers
(
user_id int unsigned not null,
stalker_user_id int unsigned not null,
primary key (user_id, stalker_user_id)
)
engine=innodb;
drop table if exists posts;
create table posts
(
post_id int unsigned not null auto_increment primary key,
user_id int unsigned not null,
privacy_level tinyint unsigned not null default 0,
post_date datetime not null,
key user_idx(user_id),
key post_date_user_idx(post_date, user_id)
)
engine=innodb;
drop table if exists post_privacy_includes_for;
create table post_privacy_includes_for
(
post_id int unsigned not null,
user_id int unsigned not null,
primary key (post_id, user_id)
)
engine=innodb;
Stored Procedures
The stored procedure is relatively simple - it initially selects ALL posts within the specified period and then filters out posts as per your original requirements. I have not performance tested this sproc with large volumes but as the initial selection is relatively small it should be performant enough as well as simplifying your application/middle tier code.
drop procedure if exists list_user_filtered_posts;
delimiter #
create procedure list_user_filtered_posts
(
in p_user_id int unsigned,
in p_day_interval tinyint unsigned
)
proc_main:begin
drop temporary table if exists tmp_posts;
drop temporary table if exists tmp_priv_posts;
-- select ALL posts in the required date range (or whatever selection criteria you require)
create temporary table tmp_posts engine=memory
select
p.post_id, p.user_id, p.privacy_level, 0 as deleted
from
posts p
where
p.post_date between now() - interval p_day_interval day and now()
order by
p.user_id;
-- purge stalker posts (0,1,3,4)
update tmp_posts
inner join user_stalkers us on us.user_id = tmp_posts.user_id and us.stalker_user_id = p_user_id
set
tmp_posts.deleted = 1
where
tmp_posts.user_id != p_user_id;
-- purge other users private posts (3)
update tmp_posts set deleted = 1 where user_id != p_user_id and privacy_level = 3;
-- purge friend only posts (1) i.e where p_user_id is not a friend of the poster
/*
requires another temp table due to mysql temp table problem/bug
http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html
*/
-- the private posts (1) this user can see
create temporary table tmp_priv_posts engine=memory
select
tp.post_id
from
tmp_posts tp
inner join user_friends uf on uf.user_id = tp.user_id and uf.friend_user_id = p_user_id
where
tp.user_id != p_user_id and tp.privacy_level = 1;
-- remove private posts this user cant see
update tmp_posts
left outer join tmp_priv_posts tpp on tmp_posts.post_id = tpp.post_id
set
tmp_posts.deleted = 1
where
tpp.post_id is null and tmp_posts.privacy_level = 1;
-- purge filtered (4)
truncate table tmp_priv_posts; -- reuse tmp table
insert into tmp_priv_posts
select
tp.post_id
from
tmp_posts tp
inner join post_privacy_includes_for ppif on tp.post_id = ppif.post_id and ppif.user_id = p_user_id
where
tp.user_id != p_user_id and tp.privacy_level = 4;
-- remove private posts this user cant see
update tmp_posts
left outer join tmp_priv_posts tpp on tmp_posts.post_id = tpp.post_id
set
tmp_posts.deleted = 1
where
tpp.post_id is null and tmp_posts.privacy_level = 4;
drop temporary table if exists tmp_priv_posts;
-- output filtered posts (display ALL of these on web page)
select
p.*
from
posts p
inner join tmp_posts tp on p.post_id = tp.post_id
where
tp.deleted = 0
order by
p.post_id desc;
-- clean up
drop temporary table if exists tmp_posts;
end proc_main #
delimiter ;
Test Data
Some basic test data.
insert into users (username) values ('f00'),('bar'),('alpha'),('beta'),('gamma'),('omega');
insert into user_friends values
(1,2),(1,3),(1,5),
(2,1),(2,3),(2,4),
(3,1),(3,2),
(4,5),
(5,1),(5,4);
insert into user_stalkers values (4,1);
insert into posts (user_id, privacy_level, post_date) values
-- public (0)
(1,0,now() - interval 8 day),
(1,0,now() - interval 8 day),
(2,0,now() - interval 7 day),
(2,0,now() - interval 7 day),
(3,0,now() - interval 6 day),
(4,0,now() - interval 6 day),
(5,0,now() - interval 5 day),
-- friends only (1)
(1,1,now() - interval 5 day),
(2,1,now() - interval 4 day),
(4,1,now() - interval 4 day),
(5,1,now() - interval 3 day),
-- private (3)
(1,3,now() - interval 3 day),
(2,3,now() - interval 2 day),
(4,3,now() - interval 2 day),
-- filtered (4)
(1,4,now() - interval 1 day),
(4,4,now() - interval 1 day),
(5,4,now());
insert into post_privacy_includes_for values (15,4), (16,1), (17,6);
Testing
As I mentioned before I've not fully tested this but on the surface it seems to be working.
select * from posts;
call list_user_filtered_posts(1,14);
call list_user_filtered_posts(6,14);
call list_user_filtered_posts(1,7);
call list_user_filtered_posts(6,7);
Hope you find some of this of use.
OK, my statement executes well in phpMyAdmin, but not how I expect it in my php page.
This is my statement:
SELECT `egid`, `group_name` , `limit`, MAX( `date` )
FROM employee_groups
GROUP BY `egid`
ORDER BY `egid` DESC ;
This is may table:
CREATE TABLE `employee_groups` (
`egid` int(10) unsigned NOT NULL,
`date` date NOT NULL,
`group_name` varchar(50) NOT NULL,
`limit` smallint(5) unsigned NOT NULL,
PRIMARY KEY (`egid`,`date`)
) ENGINE=MyISAM DEFAULT CHARSET=cp1251;
I want to extract the most recent list of groups, e.g. if a group has been changed I want to have only the last change. And I need it as a list (all groups).
Your query might be broken. You should not select fields that aren't in the group by unless one of the following two conditions apply:
You use an aggregate function.
The value is functionally dependant on the grouped by columns.
The two fields group_name and limit appear to break these rules. This means that you will get indeterminate results for these columns.
If you are trying to select the max per group then you should use a slightly different technique. See Quassnoi's article MYSQL: Selecting records holding a groupwise maximum for a variety of methods you could use.
Here's one way to do it:
SELECT di.*
FROM (
SELECT egid, MAX(date) AS date
FROM employee_groups d
GROUP BY egid
) dd
JOIN employee_groups di
ON di.egid = dd.egid AND di.date = dd.date
aggregate functions will work in mysql, different to the sql standard. to access the value of max(date) from php, you have to alias it:
SELECT `egid`, `group_name` , `limit`, MAX( `date` ) as maxdate
FROM …
you can then select it like any other colum from php with
while($row = mysqli_fetch_row($result)) {
echo htmlspecialchars($row['maxdate']);
}
hope that helps