Select distinct from a list based on set of parameters - mysql

I'm trying to get a distinct list of results, distinct based on user, where the selected result would be based on a set of parameters. To break it down, I have users, logs, and files. Each user can be on multiple logs and can have multiple files. Files CAN be associated with logs or not, and can also have a 'billing' flag set to true. What I'm trying to do when someone selects a log is bring up the list of files most closely associated with both the 'billing' flag and the log.
If the user has a file that is associated with the log AND has the
'billing' flag set to true, that is the result for that user.
If that is not available, the next would be the file that only has the 'billing' flag set to true (associated with any highest log or none).
If that is not available, the highest log number.
Here is the generalization of the tables:
Test Table:
+----+------+-----+
| ID | user | log |
+----+------+-----+
| 1 | 1 | 2 |
| 2 | 1 | 2 |
| 3 | 2 | 2 |
| 4 | 3 | 2 |
| 5 | 3 | 2 |
| 6 | 4 | 2 |
+----+------+-----+
File Table:
+----+-------+-----+---------+------+
| ID | file | log | billing | user |
+----+-------+-----+---------+------+
| 1 | a.pdf | 2 | 0 | 1 |
| 2 | b.pdf | 3 | 1 | 1 |
| 3 | c.pdf | 1 | 0 | 2 |
| 4 | d.pdf | 2 | 1 | 2 |
| 5 | e.pdf | 1 | 0 | 3 |
| 6 | f.pdf | 3 | 0 | 3 |
| 7 | g.pdf | 0 | 1 | 4 |
| 8 | h.pdf | 1 | 0 | 4 |
| 9 | i.pdf | 2 | 1 | 4 |
| 10 | j.pdf | 3 | 0 | 4 |
+----+-------+-----+---------+------+
In this case I would want to get:
+------+-------+-----+---------+
| user | file | log | billing |
+------+-------+-----+---------+
| 1 | b.pdf | 3 | 1 |
| 2 | d.pdf | 2 | 1 |
| 3 | f.pdf | 3 | 0 |
| 4 | i.pdf | 2 | 1 |
+------+-------+-----+---------+
My simplified query so far returns all files for the users but I'm having trouble grouping based on the above parameters.
SELECT
user,
file,
log,
billing
FROM
files
WHERE
user IN (
SELECT
DISTINCT(user)
FROM
tests
WHERE
log = 2
)
ORDER BY
CASE
WHEN log = 2 AND billing = 1 THEN 1
WHEN billing = 1 THEN 2
ELSE -1
END
Any help would be greatly appreciated.

You can use a separate query to get the results based on each of the 3 criteria specified in the OP, then UNION the results from these queries and fetch result from first query if available, otherwise from second query, otherwise from third query:
SELECT user, file, log, billing
FROM (
SELECT #row_number:=CASE WHEN #user=user THEN #row_number+1
ELSE 1
END AS row_number,
#user:=user AS user,
file, log, billing
FROM (
-- 1st query: has biggest priority
SELECT 1 AS pri, t.user, f.file, f.log, f.billing
FROM (SELECT DISTINCT user, log
FROM tests
WHERE log = 2) AS t
INNER JOIN files AS f
ON (t.user = f.user AND t.log = f.log AND f.billing = 1)
UNION ALL
-- 2nd query: priority = 2
SELECT 2 AS pri, t.user, f.file, f.log, f.billing
FROM (SELECT DISTINCT user, log
FROM tests
WHERE log = 2) AS t
INNER JOIN files AS f
ON (t.user = f.user AND f.billing = 1)
WHERE f.log > t.log OR f.log = 0
UNION ALL
-- 3rd query: priority = 3
SELECT 3 AS pri, t.user, f.file, f.log, f.billing
FROM (SELECT DISTINCT user, log
FROM tests
WHERE log = 2) AS t
INNER JOIN files AS f ON (t.user = f.user)
ORDER BY user, pri, log DESC ) s ) r
WHERE r.row_number = 1
ORDER BY user
pri column is used so as to discern and prioritize results between the three separate queries. #row_number and #user variables are used in order to simulate ROW_NUMBER() OVER (PARTITION BY user ORDER BY pri) window function. Using #row_number in the outermost query we can select the required record, i.e. the record having the highest priority within each 'user' partition.
SQL Fiddle Demo

Related

MySQL query for device monitoring

I need your help. I try to develop monitoring of messages from devices and face some problems with SQL query. I have DB with 4 tables: devices - messages - levels - actions.
devices: id, name;
actions: id, name;
levels: id, action_id, msgcount;
messages: id, action_id, device_id;
Idea is that each devices sending messages about different actions. This messages registered in 'messages' table. Each action has different amount of levels with count of messages to get this level. I want to count registered messages and show in UI progress to next level and som additioonal info. I use following query:
select
mpd.action_id,
mpd.total,
lvl.id as lvl_id,
lvl.msgcount purpose,
lvl.name as level_name,
act.name as action
from
(select
mes.action_id,
count(1) total
from
messages mes,
devices dev
where
mes.device_id=dev.id
and dev.id=5
and mes.action_id not in(select
t.action_id
from
messages t
where
mes.device_id=t.device_id
and t.date > CURDATE())
group by
mes.action_id) mpd,
actions act,
levels lvl
where
mpd.action_id = act.id
and mpd.action_id = lvl.action_id
and lvl.msgcount = (SELECT
MIN(bad.msgcount)
FROM
levels lv
WHERE
lv.msgcount > mpd.total
and lv.action_id = mpd.action_id)
*mpd - messages pro device
But problem is that if top level already recieved this action no more shown in the list. But in this case I want to show the last recieved level(max) and total count of messages. Could someone please help me.
And also I will be very appreciated if you give some advices how to imrove my query.
devices
|------|---------|
| id | name |
|------|---------|
| 3 | RH-SW-12|
| 5 | HRS-PR |
| 6 | PRS-PR |
|------|---------|
levels
|------|-----------|----------|--------|
| id | action_id | msgcount | name |
|------|-----------|----------|--------|
| 1 | 42 | 3 | low |
| 2 | 51 | 3 | start |
| 3 | 51 | 7 | medium |
| 4 | 51 | 15 | hight |
|------|-----------|----------|--------|
actions
|------|--------------|
| id | name |
|------|--------------|
| 42 | connection |
| 51 | stop service |
|------|--------------|
messages
|------|-------------|------------|----------------|
| id | action_id | device_id | date-time |
|------|-------------|------------|----------------|
| 1 | 42 | 3 |14.09.2017 08:51|
| 2 | 42 | 5 |14.09.2017 13:08|
| 3 | 42 | 5 |14.09.2017 16:30|
| 4 | 42 | 5 |15.09.2017 07:43|
| 5 | 51 | 3 |15.09.2017 07:50|
| 6 | 51 | 3 |15.09.2017 10:22|
| 7 | 51 | 3 |15.09.2017 15:11|
| 8 | 51 | 3 |15.09.2017 18:48|
| 9 | 51 | 3 |15.09.2017 19:03|
| 10 | 51 | 5 |15.09.2017 19:18|
| 11 | 42 | 5 |15.09.2017 21:33|
|------|-------------|------------|----------------|
My query now will show following result for device 5:
|------------|---------|----------|-----------|--------------|--------------|
| action_id | total | lvl_id | purpose | level_name | action |
|------------|---------|----------|-----------|--------------|--------------|
| 51 | 1 | 2 | 3 | start | stop service |
|------------|---------|----------|-----------|--------------|--------------|
there is no info about action 42 represented because it has no more levels. Last level was "low" and it was reached.
I want modify the query to get in this case the next result for the device 5:
|------------|---------|----------|-----------|--------------|--------------|
| action_id | total | lvl_id | purpose | level_name | action |
|------------|---------|----------|-----------|--------------|--------------|
| 42 | 4 | 1 | 3 | low | connection |
| 51 | 1 | 2 | 3 | start | stop service |
|------------|---------|----------|-----------|--------------|--------------|
I hope it's possible :)
Try this. This will give you the current levels.
SELECT
*
FROM
devices d,
actions a,
(SELECT
device_id, action_id, COUNT(1) AS count
FROM
messages
GROUP BY device_id , action_id) m,
levels l
WHERE
d.id = m.device_id
AND a.id = m.action_id
AND l.action_id = a.id
AND m.count >= l.msgcount
AND d.id = 5;
Sometimes discussion with right people helps to take a look at the problem from another side. During trying different variant with answer from #HatimStovewala I found right solution:
SELECT
*
FROM
(SELECT
device_id, action_id, COUNT(1) AS count
FROM
messages
WHERE
device_id = 5
GROUP BY device_id , action_id) tmp,
levels lvl
WHERE
lvl.action_id = tmp.action_id
and lvl.msgcount = (SELECT
CASE COALESCE(MIN(msgcount),0) WHEN 0 THEN (SELECT MAX(msgcount) FROM levels WHERE action_id = tmp.action_id) ELSE MIN(repetitions) END AS rep
FROM
levels
WHERE
action_id=tmp.action_id
and msgcount >= tmp.count)
Thank you, Hatim!

SQL order by match to specific row

I have a example table below. I am trying to create a SQL query that gets all user_ids besides user_id of the current user and then orders by number of matches to the row with the current user_id
For example, if the user has a user_id of '1', I want to get all of the user_ids corresponding with the rows of id 2-8, and then order the user_ids from most matches to the row of the current user to least matches with the row of the current user
Let's say var current_user = 1
Something like this:
SELECT user_id
FROM assets
WHERE user_id <> `current_user` and
ORDER BY most matches to `current_user`"
The output should get 7,8,3,9,2
I would appreciate anyone's input on how I can effectively achieve this.
Table assets
+----------+---------+-------+--------+-------+
| id | user_id | cars | houses | boats |
+----------+---------+-------+--------+-------+
| 1 | 1 | 3 | 2 | 3 |
| 2 | 8 | 3 | 2 | 5 |
| 3 | 3 | 3 | 2 | 2 |
| 4 | 2 | 5 | 1 | 5 |
| 5 | 9 | 5 | 7 | 3 |
| 8 | 7 | 3 | 2 | 3 |
+----------+---------+-------+--------+-------+
I think you can just do this:
select a.*
from assets a cross join
assets a1
where a1.user_id = 1 and a.user_id <> a1.user_id
order by ( (a.cars = a1.cars) + (a.houses = a1.houses) + (a.boats = a1.boats) ) desc;
In MySQL, a boolean expression is treated as an integer in a numeric context, with 1 for true and 0 for false.
If you want to be fancier, you could order by the total difference:
order by ( abs(a.cars - a1.cars) + abs(a.houses - a1.houses) + abs(a.boats - a1.boats) );
This is called Manhattan distance, and you would be implementing a version of a nearest neighbor model.

Get a row with min(priority) from two tables

I need to query data from multiple tables, below are the major tables(simplified).
Project
+-----+-------+-------+
| pid | pname | status| //status: 0 = pending, 1 = complete
+-----+-------+-------+
| 1 | Proj1 | 0 |
| 2 | Proj2 | 1 |
| 3 | Proj3 | 0 |
+-----+-------+-------+
Module
+-----+--------+-------+----------+-----------------+
| mid | pid | status| priority |modulecategoryid |
+-----+--------+-------+----------+-----------------+
| 1 | 1 | 1 | 1 | 1 |
| 2 | 1 | 0 | 2 | 3 |
| 3 | 3 | 1 | 1 | 1 |
| 4 | 3 | 0 | 2 | 3 |
| 5 | 3 | 0 | 3 | 5 |
+-----+--------+-------+----------+-----------------+
Task
+----+--------+-------+----------+-----------------+
| id | mid | status| priority | taskcategoryid |
+----+--------+-------+----------+-----------------+
| 1 | 2 | 1 | 2 | 2 |
| 2 | 2 | 0 | 1 | 1 |
| 3 | 4 | 1 | 1 | 2 |
| 4 | 4 | 1 | 2 | 3 |
| 5 | 4 | 0 | 3 | 4 |
| 6 | 5 | 0 | 1 | 1 |
+----+--------+-------+----------+-----------------+
I am trying to get the pending tasks for all the pending projects that can be started first based on the module priority and task priority. i.e. for Proj3, module with priority 1 is completed so i should get first priority pending task for module 2.
I need to get the most prior task for each pending project with modulecategoryid and taskcategoryid for get its related info like this
+-----+--------+-----+------------------+----------------+
| pid | mid | tid | modulecategoryid | taskcategoryid |
+-----+--------+-----+------------------+----------------+
| 1 | 2 | 2 | 3 | 2 |
| 2 | 4 | 5 | 3 | 4 |
+----+---------+-----+------------------+----------------+
I am new to MySql and I have tried query with multiple joins and group it by projectids and min(priority) to get desired result. But columns that are not in group by are fetched randomly from the aggregate.
I have seen this answer SQL Select only rows with Max Value on a Column but that solves the problem for data in only one table.
Shall I get some help on that?
I can post my query if needed but it is getting wrong data.
SQL Select only rows with Max Value on a Column has the right approach. You just need to do it twice.
First create a subquery a showing the highest priority task for each module.
Then create a subquery b showing the highest priority Module for each project.
Then join your three tables and two subqueries together.
Here's a. It shows the highest priority Task id for each Module mid. (http://sqlfiddle.com/#!9/7eb1f3/4/0)
SELECT Task.id, Task.mid
FROM Task
JOIN (
SELECT MAX(priority) priority,
mid
FROM Task
WHERE status = 0
GROUP BY mid
) q ON q.priority = Task.priority AND q.mid = Task.mid
Here's b. It works the same way as a and shows the highest priority Module mid for each Project pid. (http://sqlfiddle.com/#!9/7eb1f3/3/0)
SELECT Module.mid, Module.pid
FROM Module
JOIN (
SELECT MAX(priority) priority,
pid
FROM Module
WHERE status = 0
GROUP BY pid
) q ON q.priority = Module.priority AND q.pid = Module.pid
Then you need a big JOIN to pull everything together. In outline it looks like this.
SELECT Project.pid, Project.pname,
Module.mid, Task.id tid,
Module.modulecategoryid, Task.taskcategoryid
FROM Project
JOIN ( /* the subquery called b */
) b ON Project.pid = b.pid
JOIN Module ON b.mid = Module.mid
JOIN ( /* the subquery called a */
) a ON Module.mid = a.mid
JOIN Task ON a.id = Task.id
WHERE Task.status = 0
The actual query looks like this, with the subqueries put in. (http://sqlfiddle.com/#!9/7eb1f3/2/0)
SELECT Project.pid, Project.pname,
Module.mid, Task.id tid,
Module.modulecategoryid, Task.taskcategoryid
FROM Project
JOIN (
SELECT Module.mid, Module.pid
FROM Module
JOIN (
SELECT MAX(priority) priority, pid
FROM Module
WHERE status = 0
GROUP BY pid
) q ON q.priority = Module.priority
AND q.pid = Module.pid
) b ON Project.pid = b.pid
JOIN Module ON b.mid = Module.mid
JOIN (
SELECT Task.id, Task.mid
FROM Task
JOIN (
SELECT MAX(priority) priority, mid
FROM Task
WHERE status = 0
GROUP BY mid
) q ON q.priority = Task.priority
AND q.mid = Task.mid
) a ON Module.mid = a.mid
JOIN Task ON a.id = Task.id
WHERE Task.status = 0
The secret to this is understanding that subqueries are virtual tables that you can join to each other or to ordinary tables. The skill you need is sorting out the combination of physical and virtual tables you need, and the join sequence.

Joining and nesting queries in mysql

Currently, I'm using this nice query:
select
users.name,
sum(race_results.winnings) as total_winnings,
count(CASE WHEN race_results.place=1 THEN 1 ELSE 0 END) AS times_won_first_place
from users
inner join race_results
where race_results.userid = users.id and race_results.place = 1
group by users.id
order by total_winnings desc
to get this
************************************************
| name | total_winnings | times_won_first_place |
| Bob | 4000 | 4 |
| John | 1000 | 1 |
************************************************
the race_results table looks like this
*******************************************
| id | raceid | userid | place | winnings |
| 1 | 1 | 1 | 1 | 1000 |
| 2 | 1 | 2 | 5 | 50 |
| 3 | 1 | 3 | 6 | 50 |
| 4 | 2 | 1 | 1 | 1000 |
| 5 | 2 | 2 | 3 | 250 |
*******************************************
I would like to include four three more columns for something like this
***************************************************************************
| name | total_winnings | total_races | 1st_place | 2nd_place | 3rd_place |
| Bob | 4000 | 5 | 4 | 0 | 0 |
| John | 1000 | 5 | 1 | 1 | 1 |
***************************************************************************
If I were to do separate queries for the new columns, I'd use
select count(raceid) from race_results where userid = 1
select count(raceid) from race_results where userid = 1 and place = 1
select count(raceid) from race_results where userid = 1 and place = 2
select count(raceid) from race_results where userid = 1 and place = 3
to do separate queries would be easy but with the existing query I had to use CASE just to get the count of times a user won 1st place. (using
count(CASE WHEN race_results.place=2 THEN 1 ELSE 0 END)
returns the same results).
How would I nest these or join them into my existing query to get what I want?
You can do it this way:
select
users.name,
sum(race_results.winnings) as total_winnings,
count(*) AS total_races,
sum(race_results.place = 1) AS times_won_first_place ,
sum(race_results.place = 2) AS times_won_second_place,
sum(race_results.place = 3) AS times_won_third_place
from users
inner join race_results
where race_results.userid = users.id
group by users.id
order by total_winnings desc;
With ANSI standard SQL you could use case expressions inside the sum function but since MySQL (and some other databases) evaluate boolean expressions to 1 for true you can replace the case expression with the just the condition to evaluate and then just sum them.
So instead of CASE WHEN race_results.place=1 THEN 1 ELSE 0 END you can do sum(race_results.place=1) and save some space and typing :)
See this SQL Fiddle for an example.

Selecting multiple unrelated data from two tables and insert into one table mysql

This is my scenario
I have a permissions table with the following fields.
id | module | permission
1 | client | add
2 | client | edit
3 | client | delete
4 | someth | edit
5 | someth | delete
employee table
id | status | somestatus
1 | act | 1
2 | den | 1
3 | act | 0
4 | den | 1
5 | act | 0
6 | act | 1
Now what i would need to do is select the employee who have status="act" and somestatus=1 and give them all permissions where module="client"
so the table employee_permissions should have these rows
id | empid | permid | permvalue
1 | 1 | 1 | 1
2 | 1 | 2 | 1
3 | 1 | 3 | 1
1 | 6 | 1 | 1
2 | 6 | 2 | 1
3 | 6 | 3 | 1
This is the query I tried and I'm stuck here
INSERT INTO at2_permission_employee (employee_id,permission_id)
SELECT at2_employee.employee_id as employee_id
, (SELECT at2_permission.permission_id as permission_id
FROM at2_permission
where at2_permission.permission_module='client'
)
from at2_employee
where at2_employee.employee_status='Active'
and at2_employee.employees_served_admin = 1;
I get the error sub query returns multiple rows which makes sense to me. But I'm not sure how to modify the query to account for iterating over the rows returned by sub query
If I'm not wrong, like this:
INSERT INTO at2_permission_employee (employee_id, permission_id, permvalue)
SELECT
at2_employee.employee_id,
at2_permission.permission_id,
1
FROM at2_permission cross join at2_employee
WHERE
at2_employee.employee_status='Active'
and at2_employee.employees_served_admin = 1
and at2_permission.permission_module='client';
It's a bit unclear where the value for permvalue should come from so I hard coded it and used the permission.id for both id and permid, but this query should give you an idea on how to accomplish what you want:
insert employee_permissions (id, empid, permid, permvalue)
select p.id, e.id, p.id, 1
from employee e, permissions p
where p.module = 'client' and e.status = 'act' and e.somestatus = 1;