MySQL to not return same results - mysql

I have a table with list of customers:
customer
c_id c_name c_email c_role
1 abc1 a1#abc.com Dev
2 abc2 a2#abc.com Dev
3 abc3 a3#abc.com Dev
4 abc4 a4#abc.com Dev
5 abc5 a5#abc.com Dev
6 abc6 a6#abc.com Dev
7 abc7 a7#abc.com Dev
8 abc8 a8#abc.com Dev
9 abc9 a9#abc.com Dev
I query the table in the following way:
select * from customer where c_role = 'Dev' order by c_id limit 2;
So, I get the results with:
c_id c_name c_email c_role
1 abc1 a1#abc.com Dev
2 abc2 a2#abc.com Dev
The business requirements says that if any records are accessed by a set of users for last 3 days, then those should not return in the subsequent query output.
So, if the user runs a query again for the next 3 days:
select * from customer where c_role = 'Dev' order by c_id limit 2;
The result should be:
c_id c_name c_email c_role
3 abc3 a3#abc.com Dev
4 abc4 a4#abc.com Dev
Can anyone help me how to create this kind of rule in MySQL?

Adding a new column in current table is not going to help you.
You will have to create another table where you store all c_ids a user has accessed and the datetime when the query was executed.
CREATE TABLE IF NOT EXISTS `access_record` (
`id` INT(11) NOT NULL AUTO_INCREMENT ,
`c_id` INT(11) NOT NULL , // id of the record which user accessed
`user_id` INT(11) NOT NULL , // id of the user who accessed the record
`accessed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ,
PRIMARY KEY (`id`)
);
So whenever the user runs the next query you can use this table to know if user has already accessed a record or not and then use those c_ids to exclude them from next result set.
SELECT
c.c_id, c.c_role,c.c_name,c.c_email
FROM
customer AS c
WHERE
c.c_role = 'Dev'
AND c.c_id NOT IN (
SELECT
ar.c_id
FROM
access_record AS ar
WHERE ar.user_id = 1 // ofcourse this will change with each user (current user in your case I assume)
AND ar.accessed_at > DATE_SUB(NOW(), INTERVAL 3 DAY)
)
ORDER BY c.c_id LIMIT 2;
This will give you all records which were not accessed by specific user within last 3 days.
I hope this helps.
Answering #dang's question in comment
How do I populate access_record when a query runs?
When you have fetched all the records then you extract c_ids from those records then you insert those c_ids into the access_record table.
In MYSQL this query should do the trick
INSERT INTO access_record (c_id,user_id)
SELECT
c.c_id, 1 // user_id of the user who is fetching records
FROM
customer AS c
WHERE
c.c_role = 'Dev'
AND c.c_id NOT IN (
SELECT
ar.c_id
FROM
access_record AS ar
WHERE ar.user_id = 1 // ofcourse this will change with each user (current user in your case I assume)
AND ar.accessed_at > DATE_SUB(NOW(), INTERVAL 3 DAY)
)
ORDER BY c.c_id LIMIT 2;
You can also fetch those c_ids with one query then use second query to insert those c_ids into the access_record table.
If you have all your records fetched in $records then
$c_ids = array_column($temp, 'c_id'); // get all c_ids from fetched record array
Now run a query to insert all those c_ids.

I would add an extra table with users and accessdate. And make the business logic update those on access. For example:
user | accessdate | c_id

Your 'customer' table is data about customers. That is all it should have.
However, your selection criteria is really not what it appears to be. What business requirements want you to implement is a feed, or pipeline, with the selection acting as a consumer, being fed by un-accessed customers.
Each user (or group of users, ie: 'set of users') needs it's own feed, but that can be managed by a single table with a distinguishing field. So we need a user_group table to group your 'set of users'.
// user_group
g_id g_data
201 abc1
202 abc2
203 abc3
We will need to populate customer_feed with the access timestamps for each customer. We can add a foreign keys to delete customers and user_groups when they are deleted, but we will need update the customer feed when we use it.
create table customer_feed (
c_id int(11) not null,
g_id int(11) not null,
at timestamp not null,
primary key (c_id, g_id),
constraint customer_fk foreign key (c_id) references customer on delete cascade
constraint user_group_fk foreign key (g_id) references user_group on delete cascade,
);
// customer_feed
c_id g_id at
101 201 2018-11-26 07:40:21
102 201 2018-11-26 07:40:21
103 201 2018-11-26 07:40:22
When we want to read the customer data, we must do three queries:
Update the feed for the current user-group.
Get the users from the feed
Mark the users in the feed as consumed.
So, let's say we are using user_group 201.
When we update the feed, any new users to the feed are available to be read straight away,we give them a timestamp which is very early. So we can commemorate the battle of hastings...
// 1. update the customer_feed for user_group 201
insert into customer_feed(select c.c_id,201,TIMESTAMP('1066-10-14')
from customer c left join customer_feed f
on c.c_id = f.c_id and f.g_id=201 where f.c_id is null);
we select from the customer and the feed... We only accept records whose access dates are less than three days ago. This is the query you originally had, but with the feed restrictions.
// 2. read from feed for user_group 201
select c.* from customer c,customer_feed f where c.c_role='Dev'
and f.g_id=201 and f.c_id=c.c_id
and f.at < date_sub(now(), interval 3 day) limit 2
..and now we need to mark the values from the feed as being consumed. So, we gather the c_id we have selected into a list of c_id, eg: '102,103', and we mark them as consumed.
// 3. Mark the feed as consumed for user_group 201
update customer_feed set at=now() where g_id=201 and c_id in '102,103'

Add a new column in your customer table like start_accessing and then you can run the query:
SELECT *
FROM customer
WHERE c_role = 'Dev'
AND Date_add(start_accessing, INTERVAL 3 day) >= Curdate()
ORDER BY c_id
LIMIT 2;
start_accessing will be the column that will save when the user started accessing the resource.

Add a datetime stamp to the table and query from that.

There might be a way to get a 3 day rotation without having to change the tables.
By calculating batches of devs.
And calculate the current dev batch based on the current date.
The example below is for MySql 7.x (no window functions)
set #date = current_date;
-- set #date = date('2020-07-04');
set #dayslimit = 3;
set #grouplimit = 2;
set #devcnt = (select count(*) cnt
from test_customer
where c_role = 'Dev');
set #curr_rnk = ((floor(datediff(#date, date('2000-01-01')) / #dayslimit)%floor(#devcnt / #dayslimit))+1);
select c_id, c_name, c_email, c_role
-- , rnk
from
(
select t.*,
case when #rn := #rn +1 then #rnk := ceil((#rn/#grouplimit)%(#devcnt+1/#grouplimit)) end as rnk
from test_customer t
cross join (select #rn:=0, #rnk:= null) vars
where c_role = 'Dev'
order by c_id
) q
where rnk = #curr_rnk
order by c_id
limit 2;
A test on rextester here

Related

SQL Formula for mysql Table

Hello – I have a DB table (MySQL ver 5.6.41-84.1-log) that has about 92,000 entries, with columns for:
id (incremental unique ID)
post_type (not important)
post_id (not important, but shows relation to another table)
user_id (not important)
vote (not important)
ip (IP Address, ie. 123.123.123.123)
voted (Datestamp in GMT, ie. 2018-12-03 04:50:05)
I recently ran a contest and we had a rule that no single IP could vote more than 60 times per day. So now I need to run a custom SQL formula that applies the following rule:
For each IP address, for each day, if there are > 60 rows, delete those additional rows.
Thank you for your help!
This is a complicated one, and I think it is hard to provide a 100% sure answer without actual table and data to play with.
However let me try to describe the logic, and build the query step by step so you can paly around with it and possibly fix lurking erros.
1) We start with selecting all ip adresses that posted more than 60 votes on a given day. For this we use a group by on the voting day and on the ip adress, combined with a having clause
select date(voted), ip_adress
from table
group by date(voted), ip_adress
having count(*) > 60
2) From then, we go back to the table and select the first 60 ids corresponding to each voting day / ip adress couple. id is an autoincremented field so we just sort using this field and the use the mysql limit instruction
select id, ip_adress, date(voted) as day_voted
from table
where ip_adress, date(voted) in (
select date(voted), ip_adress
from table
group by date(voted), ip_adress
having count(*) > 60
)
order by id
limit 60
3) Finally, we go back once again to the table and search for the all ids whose ip adress and day of vote belong to the above list, but whose id is greater than the max id of the list. This is achieved with a join and requires a group by clause.
select t1.id
from
table t1
join (
select id, ip_adress, date(voted) as day_voted
from table
where ip_adress, date(voted) in (
select date(voted), ip_adress
from table
group by date(voted), ip_adress
having count(*) > 60
)
order by id
limit 60
) t2
on t1.ip_adress = t2.ip_adress
and date(t1.voted) = t2.day_voted and t1.id > max(t2.id)
group by t1.id
That should return the list of all ids that we need to delete. Test if before you go further.
4) The very last step is to delete those ids. There are limitations in mysql that make a delete with subquery condition quite uneasy to achieve. See the following SO question for more information on the technical background. You can either use a temporary table to store the selected ids, or try to outsmart mysql by wrapping the subquery and aliasing it. Let us try with the second option :
delete t.* from table t where id in ( select id from (
select t1.id
from
table t1
join (
select id, ip_adress, date(voted) as day_voted
from table
where ip_adress, date(voted) in (
select date(voted), ip_adress
from table
group by date(voted), ip_adress
having count(*) > 60
)
order by id
limit 60
) t2
on t1.ip_adress = t2.ip_adress
and date(t1.voted) = t2.day_voted
and t1.id > max(t2.id)
group by t1.id
) x );
Hope this helps !
You could approach this by vastly simplifying your sample data and using row number simulation for mysql version prior to 8.0 or window function for versions 8.0 or above. I assume you are not on version 8 or above in the following example
drop table if exists t;
create table t(id int auto_increment primary key,ip varchar(2));
insert into t (ip) values
(1),(1),(3),(3),
(2),
(3),(3),(1),(2);
delete t1 from t t1 join
(
select id,rownumber from
(
select t.*,
if(ip <> #p,#r:=1,#r:=#r+1) rownumber,
#p:=ip p
from t
cross join (select #r:=0,#p:=0) r
order by ip,id
)s
where rownumber > 2
) a on a.id = t1.id;
Working in to out the sub query s allocates a row number per ip, sub query a then picks row numbers > 2 and the outer multi-table delete deletes from t joined to a to give
+----+------+
| id | ip |
+----+------+
| 1 | 1 |
| 2 | 1 |
| 3 | 3 |
| 4 | 3 |
| 5 | 2 |
| 9 | 2 |
+----+------+
6 rows in set (0.00 sec)
I had someone help me write the following query, which addressed my question.
SET SQL_SAFE_UPDATES = 0;
create table temp( SELECT id, ip, voted
FROM
(SELECT id, ip, voted,
#ip_rank := IF(#current_ip = ip, #ip_rank + 1, 1) AS ip_rank,
#current_ip := ip
FROM `table_name` where ip in (SELECT ip from `table_name` group by date(voted),ip having count(*) >60)
ORDER BY ip, voted desc
) ranked
WHERE ip_rank <= 2);
DELETE FROM `table_name`
WHERE id not in (select id from temp) and ip in (select ip from temp);
drop table temp;

Update entire column with 1 to n

I have table where there is column named uid , It uses Autoincrement and its updated with 1,2,3 etc. Now I have cron job that deleted rows older than 2 days.So now my uid column is 2345 to n..I want to reset it to 1 to n again.I tried below code
UPDATE `tv` SET `uid` = ''
I was thinking to loop through all rows and update uid via php script, Is there any other alternative with single SQL command ?
You can try something like this:
UPDATE `tv` t
set t.`uid` = (SELECT count(*)
from `tv` s
WHERE t.`uid` >= s.`uid`)
This will count how many uid's are there that are smaller or equal then the one being updated, so when the first UID, lets say 2345 is being updated, there is only 1 uid that is smaller/equal to him so it will get the value 1 and so on...
EDIT: Try this-
UPDATE `tv` t
INNER JOIN(SELECT s.`uid`,count(*) as cnt
from `tv` s
INNER JOIN `tv` ss
ON(s.`uid` >= ss.`uid`)
GROUP BY s.`uid) tt
ON(t.`uid`=tt.`uid`)
SET t.`uid` = tt.cnt
Why don't decrease the uid by:
update tv set uid = uid -1

Duplicates in MYSQL based on date and status

This is my current query to find duplicate phone numbers.
SELECT
id, cust_num, entry_date
FROM
nums
GROUP BY
cust_num
HAVING
count(*) >= 2
ORDER BY
id
DESC
However, now Im looking to update them all in one go, based on criteria.
I only want to update the newer ones with higher id's than the original.
And only if they have a certain status.
Heres an example, of what I would update based on a list of duplicates pulled from database.
ID | Num | date | status
1 555 Sep-12 NEW (this row wouldnt update first instance of 555)
33 555 Oct-12 NEW (this row would update, duplicate with NEW as status)
42 333 Dec-12 NEW (this row wouldn't update first instance of 333)
5 555 Jan-13 ACTIVE (this row wouldnt update, duplicate but wrong status)
66 333 Feb-14 NEW (this row would update, duplicate with NEW as status)
6 555 Jan-13 NEW (this row would update, duplicate with NEW as status)
77 333 Mar 15 ACTIVE (this row wouldnt update, duplicate but wrong status)
So the real question is, what query would I use to pull all the duplicates like this, and then update them based on their status.
UPDATE nums n SET ... WHERE n.status='NEW' AND (select count(*) from nums where num = n.num and id < n.id and status = 'NEW') > 0;
Add in your SET statement for whatever you want to update.
Here is the select. and the link to fiddle
select * from mytable as m where `status` = 'NEW' and exists
(select * from mytable as mm where mm.`id` < m.`id` and mm.`num` = m.`num`)
Update status to UPDATED
update mytable as m set m.`status` = 'UPDATED' where `status` = 'NEW' and exists
(select * from mytable as mm where mm.`id` < m.`id` and mm.`num` = m.`num`)

How can I run a second query against results from my first query?

Here is the updated scenario, hope this is clearer than the last version.
Table 1 is where all data for this process is housed and contains around 5 million records.
`table1`
ID Forename Surname Tel Source Optin DistributedDate
1 A Test 0131 TL037 NULL NULL
2 B Test 0141 TL035 NULL NULL
v v v v v v v
Table 2 is updated via an insert script and contains a distinct list of sources used within a period of time, this could just as easily be created by a view.
INSERT INTO table2(`Sourcecode`)
(SELECT DISTINCT(a.`Sourcecode`) FROM `table1` a)
`table2 - structure`
ID Sourcecode
1 TL037
2 TL031
3 TL004
4 TL029
5 TL035
I am now trying to pull back the details of 5 random records from each distinct source from table 2 and insert these details into table 3.
`table3 - structure`
ID Forename Surname Tel Source Optin DistributedDate
NULL NULL NULL NULL NULL NULL NULL
This is the code I've created so far:
SET #Sourcecode =
(SELECT b.`Sourcecode`
FROM `table2` b
WHERE b.id = b.id
AND b.`Sourcecode` NOT IN (SELECT DISTINCT(source) FROM `table3`)
LIMIT 1);
INSERT INTO table3.*
(SELECT
a.id AS 'ID',
a.`FirstName` AS 'Forename',
a.`Surname` AS 'Surname',
a.`TelephoneNumber2` AS 'Tel',
a.`SourceCode` AS 'Source',
a.`optin` AS 'Optin',
a.`DateExported` AS 'DistributedDate'
FROM `table1`
WHERE a.`SourceCode` = #SourceCode
ORDER BY RAND(a.sourcecode)
LIMIT 5
Basically the end result I'm looking for is to run the above queries multiple times automatically from the result-set against #Sourcecode until value is NULL.
I've tried this using the loop function but I'm not having much luck with it.
You can create a View like this:
CREATE VIEW query1 AS
SELECT DISTINCT(b.`source`)
FROM `table2` b
Then collect from the View as:
SELECT *
FROM `table1` a
WHERE a.`source` IN(DISTINCT(b.`source`) FROM `query1` b)
ORDER BY RAND(a.id)
LIMIT 5)
I haven't tested it myself, if this doesn't work, please give some source and expected data.
Try this:
SELECT a.id, A2.RAND_NUMBER
FROM `table1` a, (SELECT DISTINCT(b.`source`)
,ABS(CHECKSUM(NewId())) % #INT AS RAND_NUMBER
FROM `table2` b) AS A2
WHERE A2.RAND_NUMBER BETWEEN 1 AND 5
GROUP BY a.id, A2.RAND_NUMBER
ORDER BY RAND(a.id)

How to distribute accounts on users equally based on a time zone in mysql?

I have 1000 account. each account has account_name, account_id, time_zone_id
I want to generate activities for every account to 3 users.
So I will need to generate 333 activities for used #10 and 333 for user #11 and 334 for user #12. But I need to make sure that the time zone is distributed equally. so if I have 200 account in a time zone 18 and 400 account in time zone 10 and 200 in time zone 7 and 200 in time zone 39 then I want to make sure I distribute those new activities for the users equally
I have tried something like this as a select to get the count and see if I am going the correct direction
SELECT count(ac.account_id) AS total, ac.time_zone_id,
(SELECT user_id FROM users where team_id = 4 ORDER BY RAND() LIMIT 1 ) AS user_id
FROM accounts AS ac
GROUP BY ac.time_zone_id
this created the activities but it is not equal distribution.
The following would return a user_no ( 10-12 ) for each account.
It sorts by time_zone_id and then uses the mod function to pick each of the three users in turn (user 10 for the first result, 11 for second, 12 for third, 10 for fourth and so on).
set #r = 0 ;
select
#r:=#r+1 row_no,
account_id,
account_name,
mod(#r,3)+10 user_no
from
account
order by
time_zone_id
Revision
you can get users in a similar way
set #ur = 0;
select
#ur:=#ur+1 user_row_no,
user_id
from users
where team_id = 4
Revised again
It would be something like this
Make some sample data
create table users( user_id int, team_id int) ;
insert into users select 2,4
union select 3,4
union select 1,2
union select 7,4
union select 6,4;
create table account ( account_id int,
account_name varchar(20),
time_zone_id varchar(3),
assigned_to int
);
insert into account(account_id ,account_name,time_zone_id)
select 1,'Janice','pst'
union select 2,'Jonny','gmt'
union select 3,'Jane','gmt'
union select 4,'Janet','pst'
union select 5,'James','gmt';
Make a table to pop the users in that we are interested in
(could/should be a temp_table)
create table temp_user( id int AUTO_INCREMENT primary key,
user_id int
);
insert into temp_user( user_id )
select user_id
from users
where team_id = 4;
The update
set #r=0;
update account join
(
select
#r:=#r+1 row_no,
account_id,
account_name,
assigned_to
from
account
order by
time_zone_id
) x
on x.account_id = account.account_id
join
temp_user
on
temp_user.id=(1+ mod(row_no,(select count(*) from temp_user)))
set account.assigned_to = temp_user.user_id
http://sqlfiddle.com/#!2/164733/10