How to structure a table for detecting expired licences in mysql? - mysql

I'm trying to design a table that will help me determine if a licence has expired or not. I'd like suggestions for better ways to structure the table.
Specifically I want to do the below 3 things:
I want to know if a licence exists
I want to know if the licence has
expired
I want to extend the licence if it hasn't expired
I came up with the below table. When a user's licence is extended the first entry for that user's licence is marked as expired.
I thought this method was good because if there was some kind of problem I still have the history to go by.
----------------------------------------------------------------------------
| user_id | licence | start | stop | expired |
----------------------------------------------------------------------------
| 22 | other | 03JUL2010 | 03JUL2012 | true |
| 55 | commercial | 03JUL2012 | 03JUL2014 | true | <= marked as expired because it was extended.
| 55 | commercial | 04JUL2012 | 03JUL2016 | false |
----------------------------------------------------------------------------
NOTE: 04JUL2012 shows the day the licence was extended.
Does this seem like a good approach? Is there anything you would change?

I would have three tables, because it seems that there is a N:M (many-to-many relationship) between users and licenses:
users
------------
user_id [PK]
...
users_has_licenses
----------------------
user_id [PK] (references users.user_id)
license_id [PK] (references licenses.license_id)
issued [PK]
expire_date
licenses
-------------------
license_id [PK]
...
The tables users and licenses are fairly straightforward. They store information intrinsic to users and licenses as standalone entities.
In the users_has_licenses cross-reference table, rows are unique across three fields: user_id, license_id, and issued. issued is a datetime field representing the issue date or start date for the license. Since a user can renew licenses more than once, and you require a history to be kept of the license renewals, we include this field as part of the composite primary key.
You can do without an expired field because you can already tell whether or not it's expired from the data itself, and you won't have to do routine updates to keep the data up to date. For example, to find a user's expired licenses, you can just execute the following query:
SELECT
a.license_id
FROM
(
SELECT user_id, license_id, MAX(issued) AS issued
FROM users_has_licenses
WHERE user_id = <user_id here>
GROUP BY user_id, license_id
) a
INNER JOIN
users_has_licenses b ON
a.user_id = b.user_id AND
a.license_id = b.license_id AND
a.issued = b.issued AND
b.expire_date < NOW()
It's a bit more hectic than you might expect, but only because you are retaining the data for past license renewals. In that case, you must do a group-wise maximum to make sure you're getting the most recent license period. The first sub-select figures out the most recent of each of a particular user's licenses, and then joins on the condition that the expire_date has already passed.
If you want to get all of the user's licenses (most recent period) whether or not they're expired and still want to tell whether they are expired or not, just change the INNER JOIN to a LEFT JOIN, and all non-expired licenses will have NULL values for the joined table.
To renew a user's license, you just need one INSERT rather than an INSERT and an UPDATE such as would be the case with your current design:
INSERT INTO users_has_licenses VALUES (<user_id here>, <license_id here>, NOW(), NOW() + INTERVAL 4 YEAR)
EDIT: Addressing the asker's comment made to this answer:
I have one more question in regards to renewing the licence. If the
user renewed their licence 6months into their first licence, how would
you include that extra time in the INSERT INTO statement you listed
above? I think the last value would need to be now() +remaining time +
expiry period (or something like that). Unless I've misunderstood
something. I'm just not sure how to get the remaining time.
Assuming you have chosen not to retain the history of past license periods, you can just do an UPDATE to the current record:
UPDATE users_has_licenses a SET
a.issued = NOW(),
a.expire_date = (
SELECT
NOW()
+ INTERVAL CASE WHEN DATEDIFF(expire_date, NOW()) < 0 THEN 0 ELSE DATEDIFF(expire_date, NOW()) END DAY
+ INTERVAL 4 YEAR
FROM
users_has_licenses
WHERE
user_id = a.user_id AND
license_id = a.license_id
)
WHERE
a.user_id = <user_id here> AND
a.license_id = <license_id here>
That will update issued to the current date and the expire_date to the current date + the remaining days of the old period + the regular expiry period(whatever it may be). If the past license has already expired, then the remaining days will be negative, in which case we would just add the regular expiry period.
As a sidenote in regards to the above schema I posted to the original question, you no longer need to include issued as part of the primary key.

you are storing the same record twice. Instead you can go for a better design
table1
user-id | license | start | stop | expired | extended
table2
prim_key | user_id | extended_date | extended_date_expiry
In table1 extended column is a boolean value true or false.
If extended is true then you can search for the date the user has extended to in table2.
In table2 you can store multiple extended dates for same user and get the history also. The highest date to which it is extended would be the date of validity of that user_id.

Related

optimized way to calculate compliance in mysql

I have a table which contains task list of persons. followings are columns
+---------+-----------+-------------------+------------+---------------------+
| task_id | person_id | task_name | status | due_date_time |
+---------+-----------+-------------------+------------+---------------------+
| 1 | 111 | walk 20 min daily | INCOMPLETE | 2017-04-13 17:20:23 |
| 2 | 111 | brisk walk 30 min | COMPLETE | 2017-03-14 20:20:54 |
| 3 | 111 | take medication | COMPLETE | 2017-04-20 15:15:23 |
| 4 | 222 | sport | COMPLETE | 2017-03-18 14:45:10 |
+---------+-----------+-------------------+------------+---------------------+
I want to find out monthly compliance in percentage(completed task/total task * 100) of each person like
+---------------+-----------+------------+------------+
| compliance_id | person_id | compliance | month |
+---------------+-----------+------------+------------+
| 1 | 111 | 100 | 2017-03-01 |
| 2 | 111 | 50 | 2017-04-01 |
| 3 | 222 | 100 | 2017-03-01 |
+---------------+-----------+------------+------------+
Here person_id 111 has 1 task in month 2017-03-14 and which status is completed, as 1 out of 1 task is completed in march then compliance is 100%
Currently, I am using separate table which stores this compliance but I have to calculate compliance update that table every time the task status is changed
I have tried creating a view also but it's taking too much time to execute view almost 0.5 seconds for 1 million records.
CREATE VIEW `person_compliance_view` AS
SELECT
`t`.`person_id`,
CAST((`t`.`due_date_time` - INTERVAL (DAYOFMONTH(`t`.`due_date_time`) - 1) DAY)
AS DATE) AS `month`,
COUNT(`t`.`status`) AS `total_count`,
COUNT((CASE
WHEN (`t`.`status` = 'COMPLETE') THEN 1
END)) AS `completed_count`,
CAST(((COUNT((CASE
WHEN (`t`.`status` = 'COMPLETE') THEN 1
END)) / COUNT(`t`.`status`)) * 100)
AS DECIMAL (10 , 2 )) AS `compliance`
FROM
`task` `t`
WHERE
((`t`.`isDeleted` = 0)
AND (`t`.`due_date_time` < NOW())
GROUP BY `t`.`person_id` , EXTRACT(YEAR_MONTH FROM `t`.`due_date_time`)
Is there any optimized way to do it?
The first question to consider is whether the view can be optimized to give the required performance. This may mean making some changes to the underlying tables and data structure. For example, you might want indexes and you should check query plans to see where they would be most effective.
Other possible changes which would improve efficiency include adding an extra column "year_month" to the base table, which you could populate via a trigger. Another possibility would be to move all the deleted tasks to an 'archive' table to give the view less data to search through.
Whatever you do, a view will always perform worse than a table (assuming the table has relevant indexes). So depending on your needs you may find you need to use a table. That doesn't mean you should junk your view entirely. For example, if a daily refresh of your table is sufficient, you could use your view to help:
truncate table compliance;
insert into compliance select * from compliance_view;
Truncate is more efficient than delete, but you can't use a rollback, so you might prefer to use delete and top-and-tail with START TRANSACTION; ... COMMIT;. I've never created scheduled jobs in MySQL, but if you need help, this looks like a good starting point: here
If daily isn't often enough, you could schedule this to run more often than daily, but better options will be triggers and/or "partial refreshes" (my term, I've no idea if there is a technical term for the idea.
A perfectly written trigger would spot any relevant insert/update/delete and then insert/update/delete the related records in the compliance table. The logic is a little daunting, and I won't attempt it here. An easier option would be a "partial refresh" on called within a trigger. The trigger would spot user targetted by the change, delete only the records from compliance which are related to that user and then insert from your compliance_view the records relating to that user. You should be able to put that into a stored procedure which is called by the trigger.
Update expanding on the options (if a view just won't do):
Option 1: Daily full (or more frequent) refresh via a schedule
You'd want code like this executed (at least) daily.
truncate table compliance;
insert into compliance select * from compliance_view;
Option 2: Partial refresh via trigger
I don't work with triggers often, so can't recall syntax, but the logic should be as follows (not actual code, just pseudo-code)
AFTER INSERT -- you may need one for each of INSERT / UPDATE / DELETE
FOR EACH ROW -- or if there are multiple rows and you can trigger only on the last one to be changed, that would be better
DELETE FROM compliance
WHERE person_id = INSERTED.person_id
INSERT INTO compliance select * from compliance_view where person_id = INSERTED.person_id
END
Option 3: Smart update via trigger
This would be similar to option 2, but instead of deleting all the rows from compliance that relate to the relevant person_id and creating them from scratch, you'd work out which ones to update, and update them and whether any should be added / deleted. The logic is a little involved, and I'm not not going to attempt it here.
Personally, I'd be most tempted by Option 2, but you'd need to combine it with option 1, since the data goes stale due to the use of now().
Here's a similar way of writing the same thing...
Views are of very limited benefit in MySQL, and I think should generally be avoided.
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(task_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
,person_id INT NOT NULL
,task_name VARCHAR(30) NOT NULL
,status ENUM('INCOMPLETE','COMPLETE') NOT NULL
,due_date_time DATETIME NOT NULL
);
INSERT INTO my_table VALUES
(1,111,'walk 20 min daily','INCOMPLETE','2017-04-13 17:20:23'),
(2,111,'brisk walk 30 min','COMPLETE','2017-03-14 20:20:54'),
(3,111,'take medication','COMPLETE','2017-04-20 15:15:23'),
(4,222,'sport','COMPLETE','2017-03-18 14:45:10');
SELECT person_id
, DATE_FORMAT(due_date_time,'%Y-%m') yearmonth
, SUM(status = 'complete')/COUNT(*) x
FROM my_table
GROUP
BY person_id
, yearmonth;
person_id yearmonth x
111 2017-03 1.0
111 2017-04 0.5
222 2017-03 1.0

Tracking the user's last activity

I have a table setup for member logins. Right now the last_login field is stamped with MySQL's NOW(). But I want to also track their last active time on the site. And the only way I can think of is to create a new query, insert it into every procedure I have on every page, and update a timestamped last_activity field for the current login. Is there a better way to do this?
Example:
MariaDB [master]> select logintime, last_activity
from memberlogins
where memberid = "1"
order by loginid
desc limit 1;
+---------------------+---------------------+
| logintime | last_activity |
+---------------------+---------------------+
| 2017-02-11 22:28:54 | 2017-02-11 23:48:14 |
+---------------------+---------------------+
That's what I want, to add the last_activity to the table. And the only way I can think of to accomplish this is to add this query:
$stmt = $dbsotp->prepare('UPDATE memberlogins
SET last_activity = NOW()
WHERE memberid = :memberid');
And then the rest of the PDO here. So all I'm asking is if there's a better way to do this than inserting this query into every procedure I have on every page. I have 67 pages with a several hundred procedures, that's why I ask if this is the only way or if there's a better way to go.

Audit trail: return ids of columns with specific column value at a certain time

Using an audit trail in MySQL, how would you find records that had a specific value during a certain time frame?
Let's say I want to get the ids of users who were a site admin one week ago (role_id=1).
Given the table:
**USER**
id | role_id | ...
And a revision table:
**TABLE_HISTORY**
table_name | date | id | column_name | column_value
(assume I'm storing create data in there for better or worse and I'm soft deleting)
How would you find people where role_id = 1 one week ago today using only one query? Keep in mind that their role might have changed to 2 after it was one, this means you can't just select where role_id = 1
Something like this maybe?:
SELECT * FROM
(
SELECT *
WHERE (date < $timestamp AND column_name = 'role_id')
LIMIT 1
)
WHERE ('is_admin'=1)

Associative table with date

In my application I have association between two entities employees and work-groups.
This association usually changes over time, so in my DB I have something like:
emplyees
| EMPLOYEE_ID | NAME |
| ... | ... |
workgroups
| GROUP_ID | NAME |
| ... | ... |
emplyees_workgroups
| EMPLOYEE_ID | GROUP_ID | DATE |
| ... | ... | ... |
So suppose I have an association between employee 1 and group 1, valid from 2014-01-01 on.
When a new association is created, for example from 2014-02-01 on, the old one is no longer valid.
This structure for the associative table is a bit problematic for queries, but I actually would avoid to add an END_DATE field to the table beacuse it will be a reduntant value and also requires the execution of an insert + update or update on two rows every time a change happens in an association.
So have you any idea to create a more practical architecture to solve my problem? Is this the better approach?
You have what is called a slowly changing dimension. That means that you need to have dates in the employees_workgroup table in order to find the right workgroup at the right time for a set of employees.
The best way to handle this is to have to dates, which I often call effdate and enddate on each row. This greatly simplifies queries, where you are trying to find the workgroup at a particular point in time. Such a query might look like with this structure:
select ew.*
from employees_workgroup ew
where MYDATE between effdate and enddate;
Now consider the same results using only one date per field. It might be something like this:
select ew.*,
from employees_workgroup ew join
(select employee_id, max(date) as maxdate
from employees_workgroup ew2
where ew2.employee_id = ew.employee_id and
ew2.date <= MYDATE
) as rec
on ew.employee_id = rec.employee_id and ew.adte = ew.maxdate;
The expense of doing an update along with the insert is minimal compared to the complexity this will introduce in the queries.

How to select a MAX record based on multiple results from a unique ID in MySQL

I run a report in mysql which returns all active members and their associated plan selections. If a member is terminated, this is displayed on the report as well. The issue I am facing, however is when a member simply decides to 'change' plans. Our system effective 'terminates' the original plan and starts fresh with the new one while keeping the original enrollment information for the member.
Sample report data may look like the following:
MemberID | Last | First | Plan Code | Original Effective | Change Date | Term Date
--------------------------------------------------------------------------------------------
12345 | Smith | John | A | 05-01-2011 | | 06-01-2011
12345 | Smith | John | B | 06-01-2011 | |
In the example above, a member had plan code A from May 1, 2011 to June 1, 2011. On June 1, 2011, the member changed his coverage to Plan B (and is still active since no term or change data).
Ultimately, I need my report to generate the following instead of the 2 line items above:
MemberID | Last | First | Plan Code | Original Effective | Change Date | Term Date
--------------------------------------------------------------------------------------------
12345 | Smith | John | B | 05-01-2011 | 06-01-2011 |
which shows his original plan effective date, the date of the change, and leaves the termination field blank.
I know I can get into a single row by using Group By the Member ID and I can even add the change date into the appropriate field with a (IF COUNT() > 1) statement but I am not able to figure out how to show the correct plan code (C) or how to leave the term date blank keeping the original effective date.
Any ideas?
Although I hate columns with embedded spaces and having to tick them, this should do it for you. The PreQuery will detect how many policy entries there are, preserve the first and last too, grouped by member. Once THAT is done, it can re-join the the plan enrollment on the same member but matching ONLY for the last "Original Effective" date for the person... That will give you the correct Plan Code. Additionally, if the person was terminated, it's date would be filled in for you. If still employed, it will be blank on that record.
select STRAIGHT_JOIN
pe2.MemberID,
pe2.Last,
pe2.First,
pe2.`Plan Code`
PreQuery.PlanStarted `Original Effective`,
case when PreQuery.PlanEntries > 1 then PreQuery.LastChange end `Change Date`,
pe2.`Term Date`
from
( select
pe.MemberID,
count(*) as PlanEntries,
min( `pe`.`Original Effective` ) PlanStarted,
max( `pe`.`Original Effective`) LastChange
from
PlanEnrollment pe
group by
pe.MemberID ) PreQuery
join PlanEnrollment pe2
on PreQuery.MemberID = pe2.MemberID
AND PreQuery.LastChange = pe2.`Original Effective`
I remember having the problem, but not coming up with a GROUP BY-based solution ;)
Instead, I think you can do something like
SELECT memberid, plancode
FROM members
INNER JOIN plans ON members.id=plans.member_id
WHERE plans.id IN
(SELECT id FROM plans
WHERE member_id = members.id
ORDER BY date DESC
LIMIT 0,1)