Simplify a complicated count SQL statement - php

I would like to know if there is a way to simplify the following SQL statement. This is my table.
| SID | name | l1 | l2 | sch |
| 1 | john | | | sch |
| 2 | mary | l1 | | |
| 3 | zack | l1 | l2 | |
| 4 | paul | l1 | l2 | sch |
Either l1 or l2 is filled, or both can be filled
Not every 'sch' has a value
What I do is to calculate a daily summary table, but I do it via PHP, I am wondering if it can be done just using SQL. So eg,
- Total count (This is just count(name))
- Count(sch)
- If !empty (l1) OR !empty (l2) THEN l + 1
So now, based on the above
Total count = 4
count(sch) = 2
count(l1 || l2) = 3
Can it be done in SQL?

You didn't say if l1, l2 and sch columns could contain NULL or not.
If these columns can not contain NULL, the mysql query would be like:
SELECT COUNT(*) AS `count`, SUM(sch<>'') AS count_sch,
SUM(l1<>'' OR l2<>'') AS count_l
FROM your_table
If these columns can contain NULL, the mysql query would be like:
SELECT COUNT(*) AS `count`, SUM(sch IS NOT NULL) AS count_sch,
SUM(l1 IS NOT NULL OR l2 IS NOT NULL) AS count_l
FROM your_table

select count(name) name_count,
count(coalesce(nullif(l1,''), nullif(l2,''))) l1_or_l2_count,
count(sch) sch_count
from your_table;

Yes:
select count(*) as count, count(sch) as school,
sum(case when l1 is not null or l2 is not null then 1 else 0 end)
from table

Related

Get rows above and below (neighbouring rows) a certain row, based on two criteria SQL

Say I have a table like so:
+---+-------+------+---------------------+
|id | level |score | timestamp |
+---+-------+------+---------------------+
| 4 | 1 | 70 | 2021-01-14 21:50:38 |
| 3 | 1 | 90 | 2021-01-12 15:38:0 |
| 1 | 1 | 20 | 2021-01-14 13:10:12 |
| 5 | 1 | 50 | 2021-01-13 12:32:11 |
| 7 | 1 | 50 | 2021-01-14 17:15:20 |
| 8 | 1 | 55 | 2021-01-14 09:20:00 |
| 10| 2 | 99 | 2021-01-15 10:50:38 |
| 2 | 1 | 45 | 2021-01-15 10:50:38 |
+---+-------+------+---------------------+
What I want to do is show 5 of these rows in a table (in html), with a certain row (e.g. where id=5) in the middle and have the two rows above and below it (in the correct order). Also where level=1. This will be like a score board but only showing the user's score with the two above and two below.
So because scores can be the same, the timestamp column will also need to be used - so if two scores are equal, then the first person to get the score is shown above the other person.
E.g. say the user is id=5, I want to show
+---+-------+------+---------------------+
|id | level |score | timestamp |
+---+-------+------+---------------------+
| 4 | 1 | 70 | 2021-01-14 21:50:38 |
| 8 | 1 | 55 | 2021-01-14 09:20:00 |
| 5 | 1 | 50 | 2021-01-13 12:32:11 |
| 7 | 1 | 50 | 2021-01-14 17:15:20 |
| 2 | 1 | 45 | 2021-01-15 10:50:38 |
| 1 | 1 | 20 | 2021-01-14 13:10:12 |
+---+-------+------+---------------------+
Note that id=7 is below id=5
I am wondering does anyone know a way of doing this?
I have tried this below but it is not outputting what I need (it is outputting where level_id=2 and id=5, and the other rows are not in order)
((SELECT b.* FROM table a JOIN table b ON b.score > a.score OR (b.score = a.score AND b.timestamp < a.timestamp)
WHERE a.level_id = 1 AND a.id = 5 ORDER BY score ASC, timestamp DESC LIMIT 3)
UNION ALL
(SELECT b.* FROM table a JOIN table b ON b.score < a.score OR (b.score = a.score AND b.timestamp > a.timestamp)
WHERE a.level_id = 1 AND a.id = 5 ORDER BY score DESC, timestamp ASC LIMIT 2))
order by score
If it is easier to output all rows in the table, say where level = 1, so it is a full score board.. and then do the getting a certain row and two above and below it using PHP I'd also like to know please :) ! (possibly thinking this may keep the SQL simpler)?
You can use cte and inner join as follows:
With cte as
(select t.*,
dense_rank() over (order by score) as dr
from your_table t)
Select c.*
From cte c join cte cu on c.dr between cu.dr - 2 and cu.dr + 2
Where cu.id = 5
Ordwr by c.dr, c.timestamp
I would suggest window functions:
select t.*
from (select t.*,
max(case when id = 7 then score_rank end) over () as id_rank
from (select t.*,
dense_rank() over (order by score) as score_rank
from t
where level = 1
) t
) t
where score_rank between id_rank - 2 and id_rank + 2;
Note: This returns 5 distinct score values, which may result in more rows depending on duplicates.
Here is a db<>fiddle.
EDIT:
If you want exactly 5 rows using the timestamp, then:
select t.*
from (select t.*,
max(case when id = 7 then score_rank end) over () as id_rank
from (select t.*,
dense_rank() over (order by score, timestamp) as score_rank
from t
where level = 1
) t
) t
where score_rank between id_rank - 2 and id_rank + 2
order by score;
Note: This still treats equivalent timestamps as the same, but they seem to be unique in your data.

Joining more than 2 tables based on value in mysql

There is a project that I've to work on, the main transaction table and some sub transaction tables called provider. Each provider has its own table. The main table just keeps amount (as sub ones keep too), date and some essential data, also reference id of subtable. I want to join sub tables by based on provider id. If things can go messy, I can keep table names as an associative array. What makes me confused is each provider's table has a different primary key name.
Provider tables are pretty much identical excepts some columns. What I really try to achieve is performing a search in all of these 3 tables as one.
One other question, is this some silly idea, if so which approach would be better? Daily 400-500 records are expected. Also note, more provider tables can be added in future. This structure is designed by someone more experienced than me, I couldn't convince anyone this is bad.
Transaction
+-----+-----+-----+-----+
| id | ref | prv | date|
+-----+-----+-----+-----+
| 1 | 4 | 2 | .. |
+-----+-----+-----+-----+
| 2 | 4 | 3 | .. |
+-----+-----+-----+-----+
| 3 | 5 | 2 | .. |
+-----+-----+-----+-----+
| 4 | 7 | 1 | .. |
+-----+-----+-----+-----+
| 5 | 22 | 3 | .. |
+-----+-----+-----+-----+
Providers (prv value)
+-----+---------------+-----+
| pID | providerName | .. |
+-----+---------------+-----+
| 1 | providerA | .. |
+-----+---------------+-----+
| 2 | providerB | .. |
+-----+---------------+-----+
| 3 | providerC | .. |
+-----+---------------+-----+
p_providerA (ref value)
+-----+--------+------+-----+
| aID | amount | name | .. |
+-----+--------+------+-----+
| 1 | 90.20 | alf | .. |
+-----+--------+------+-----+
| 2 | 70.00 |willie| .. |
+-----+--------+------+-----+
| 3 | 43.10 | kate | .. |
+-----+--------+------+-----+
p_providerB (ref value)
+-----+--------+------+-----+
| bID | amount | name | .. |
+-----+--------+------+-----+
| 3 | 65.20 | jane | .. |
+-----+--------+------+-----+
| 4 | 72.00 | al | .. |
+-----+--------+------+-----+
| 5 | 84.10 | bundy| .. |
+-----+--------+------+-----+
p_providerC (ref value)
+-----+--------+------+-----+
| bID | amount | name | .. |
+-----+--------+------+-----+
| 3 | 10.20 | mike | .. |
+-----+--------+------+-----+
| 4 | 40.00 | kitt | .. |
+-----+--------+------+-----+
| 6 | 27.60 | devon| .. |
+-----+--------+------+-----+
Expected Result
+-----+-----+-----+-----+----+--------+------+-----+
| id | ref | prv | date| | | | |
+-----+-----+-----+-----+----+--------+------+-----+
| 1 | 4 | 2 | .. | 4 | 72.00 | al | .. | (from prv. b)
+-----+-----+-----+-----+----+--------+------+-----+
| 2 | 4 | 3 | .. | 4 | 40.00 | kitt | .. | (from prv. c)
+-----+-----+-----+-----+----+--------+------+-----+
Given the current table design, one of the ways to get the desired result is to "break down" the Transaction table into separate queries, and combine those with a UNION ALL
The rows from Transaction table could be returned like this:
SELECT t.* FROM Transaction t WHERE t.prv = 1
UNION ALL
SELECT t.* FROM Transaction t WHERE t.prv = 2
UNION ALL
SELECT t.* FROM Transaction t WHERE t.prv = 3
UNION ALL
...
Now each of those SELECT can implement a join to the appropriate provider table
SELECT t.*, pa.amount, pa.name
FROM Transaction t
JOIN p_providerA pa ON pa.aid = t.ref
WHERE t.prv = 1
UNION ALL
SELECT t.*, pb.amount, pb.name
FROM Transaction t
JOIN p_providerB pb ON pb.bid = t.ref
WHERE t.prv = 2
UNION ALL
...
The other option is almost equally ugly
SELECT t.*
, CASE t.prv
WHEN 1 THEN pa.amount
WHEN 2 THEN pb.amount
WHEN 3 THEN pc.amount
END AS `p_amount`
, CASE t.prv
WHEN 1 THEN pa.name
WHEN 2 THEN pb.name
WHEN 3 THEN pc.name
END AS `p_name`
FROM Transaction t
LEFT JOIN p_providerA pa ON pa.aid = t.ref AND t.prv = 1
LEFT JOIN p_providerB pb ON pb.bid = t.ref AND t.prv = 2
LEFT JOIN p_providerC pc ON pc.cid = t.ref AND t.prv = 3
Bottom line... there's no way to dynamically use of the Providers table in a single query. We could make use of that information in a pre-query, to get back a resultset that helps us create the statement we need to run.
Another option (if the p_providerX tables aren't too large) would be to concatenate all of those together in an inline view, and the join to that. (This could be expensive for large sets; the derived table might get an index created on it...)
SELECT t.*
, p.amount AS p_amount
, p.name AS p_name
FROM `Transaction` t
JOIN (
SELECT 1 AS pID, pa.aid AS rID, pa.amount, pa.name FROM p_providerA
UNION ALL
SELECT 2 , pb.bid , pb.amount, pb.name FROM p_providerB
UNION ALL
SELECT 3 , pc.cid , pc.amount, pc.name FROM p_providerC
UNION ALL
...
) p
ON p.pID = t.pID
AND p.rID = t.ref
If we are going to be repeatedly running queries like that, we could materialize that inline view into a table... I'm just guessing at the datatypes here...
CREATE TABLE p_provider
( pID BIGINT UNSIGNED NOT NULL
, rID BIGINT UNSIGNED NOT NULL
, amount DECIMAL(20,2)
, name VARCHAR(255)
, PRIMARY KEY (pID,id)
);
INSERT INTO p_provider (pID, rID, amount, name)
SELECT 1 AS pID, pa.aid AS rID, pa.amount, pa.name FROM p_providerA
;
INSERT INTO p_provider (pID, rID, amount, name)
SELECT 2 AS pID, pb.aid AS rID, pb.amount, pb.name FROM p_providerB
;
INSERT INTO p_provider (pID, rID, amount, name)
SELECT 3 AS pID, pc.aid AS rID, pc.amount, pc.name FROM p_providerC
;
...
And then reference the new table
SELECT ...
FROM `Transaction` t
JOIN `p_provider` p
ON p.piD = t.pID
AND p.rID = t.ref
Of course that new p_provider table is going to be out-of-sync when changes are made to p_providerA, p_providerB, et al.

MySQL, Merge selects in order of one record from each select

I have a table that contains too many records and each bunch of records belong to someone:
---------------------
id | data | username
---------------------
1 | 10 | ali
2 | 11 | ali
3 | 12 | ali
4 | 20 | omid
5 | 21 | omid
6 | 30 | reza
now I want to create a query to result me like this:
1-10-ali
4-20-omid
6-30-reza
2-11-ali
5-21-omid
3-12-ali
Is there anyway to create a query to result me one record per each username and then one from another, and another to the end?
Unfortunately MySQL doesn't have a ranking system so you can use UDV (user defined variables) to rank your records like so.
SELECT id, `data`, name
FROM
( SELECT
id, `data`, name,
#rank := if(#name = name, #rank + 1, 1) as rank,
#name := name
FROM test
CROSS JOIN (SELECT #rank := 1, #name := '') temp
ORDER BY name, `data`
) t
ORDER BY t.rank, t.name, t.data
Sql Fiddle to play with
Output:
+---------------------+
| id | data | name |
+-----+------+--------+
| 1 | 10 | ali |
+---------------------+
| 4 | 20 | omid |
+---------------------+
| 6 | 30 | reza |
+---------------------+
| 2 | 11 | ali |
+---------------------+
| 5 | 21 | omid |
+---------------------+
| 3 | 12 | ali |
+---------------------+
The classic SQL approach is a self join and grouping that lets you determine a row's ranking position by counting the number of rows that come before it. As this is probably slower I doubt I could talk you out of the proprietary method but I mention it to give you an alternative.
select t.id, min(t.`data`), min(t.username)
from test t inner join test t2
on t2.username = t.username and t2.id <= t.id
group by t.id
order by count(*), min(t.username)
Your example would work with
SELECT id, `data`, name
FROM tbl
ORDER BY `data` % 10,
username
`data`;
If data and username do not have the desired pattern, then improve on the example.

MySQL Multiple Conditions on Group By / Having Clause

I have three tables that are all inter-related with the following structure.
ModuleCategory Table:
+------------------+----------------+------------+
| ModuleCategoryID | ModuleCategory | RequireAll |
+------------------+----------------+------------+
| 90 | Cat A | YES |
| 91 | Cat B | NO |
+------------------+----------------+------------+
ModuleCategorySkill Table:
+------------------+---------+
| ModuleCategoryID | SkillID |
+------------------+---------+
| 90 | 1439 |
| 90 | 3016 |
| 91 | 1440 |
| 91 | 3016 |
+------------------+---------+
EmployeeSkill Table:
+---------+---------+
| EmpName | SkillID |
+---------+---------+
| Emp1 | 1439 |
| Emp1 | 3016 |
| Emp2 | 1440 |
| Emp2 | 3016 |
| Emp3 | 1439 |
| Emp4 | 3016 |
+---------+---------+
Desired Output:
+------------------+-------+
| ModuleCategory | Count |
+------------------+-------+
| Cat A | 1 |
| Cat B | 3 |
+------------------+-------+
I am trying to group by ModuleCategoryID's and get the count of employees which have the skills being tracked.
Normally, I can do the following query to obtain the numbers:
select mc.ModuleCategory, Count(*) as Count from ModuleCategory as mc
join ModuleCategorySkill as mcs on mc.ModuleCategoryID = mcs.ModuleCategoryID join EmployeeSkill as es on es.SkillID= mcs.SkillID
group by mc.ModuleCategoryID
However, I have a column RequireAll in the ModuleCategory table which if it is set to 'YES' should only count employees as 1 only if they have all the skills in the category. If it is set to NO then it can count each row normally and increase the count by the number of rows it groups by.
I can achieve this by writing separate queries for each modulecategoryID and using a having Count() > 1 (which will find me anyone that has all the skills for ModuleCategoryID 90). If there were 3 skills than I would have to change it to Having Count() > 2. If there isn't anyone that has all the skills specified, the count should be 0.
I need a dynamic way of being able to do this since there is a lot of data and writing one query for each ModuleCategoryID isn't the proper approach.
Also, I am using PHP so I can loop through and create a sql string that can help me achieve this. But I know I will run into performance issues on big tables with a lot of skills and modulecategoryID's.
Any guidance on how to achieve this is much appreciated.
You can do it by joining on the total category counts, and then using conditional aggregation:
select modulecategory,
count(case when requireall = 'yes'
then if(s = t, 1, null)
else s
end)
from (
select modulecategory,empname, requireall, count(*) s, min(q.total) t
from employeeskill e
inner join modulecategoryskill mcs
on e.skillid = mcs.skillid
inner join modulecategory mc
on mcs.modulecategoryid = mc.modulecategoryid
inner join (
select modulecategoryid, count(*) total
from modulecategoryskill
group by modulecategoryid
) q
on mc.modulecategoryid = q.modulecategoryid
group by modulecategory, empname
) qq
group by modulecategory;
demo here
This operates under the assumption an employee isn't going to be allocated the same skill twice, if that is something that may happen, this query is alterable to support it, but it seems like a broken scenario to me.
What we have here is an inner query that collates all the information we need (category name, employee name, whether or not all skills are required, how many skills are in the group per employee, and how many there in the group total), with an outer query that uses a conditional count to change how the rows are tallied, based on the value of requireall.

How to check if a row does not exist in a database

I have the following setup:
A table with columns a, b, c.
An array with a random number of items (i1, i2, i3,... in).
Table contains rows like
1 1 i1
1 1 i2
. . .
. . .
. . .
1 1 in
1 2 i1
. . .
. . .
. . .
2 1 i1
2 2 i2
The thing is that not all records are there. For example row 1 2 i1 might be missing.
What I would like to do, from query (without getting all rows and iterate through them) is to see IF any row is missing (I don't care witch one, only IF one is missing).
This is a very simplified example for a much more complex problem so if I didn't expose it clear, or I forgot to mention anything feel free to ask for details.
A select and process in PHP is acceptable, as long as I don't select everything in table (although I don't see how to do this by processing data without selecting all but felt like it worth mentioning).
Some of you asked for a pattern so...:
Let's simplify some more... let's say column one has an array of possible data that can be found there, same for column 2, and already said it for column 3. All possible combinations between the 3 of then should be found on the table. I need to know if any are missing...
assuming you know he values for column a and b you could try the following:
select c, count (*) group by c;
this would tell you how many entries for each value are there.
i1 3
i2 0
in 3
then you could iterate over that result to see whats missing
Assume we have a table with this data.
mysql> SELECT * FROM stuff;
+------+------+------+
| a | b | c |
+------+------+------+
| 1 | 1 | i1 |
| 1 | 1 | i2 |
| 1 | 2 | i2 |
| 1 | 2 | i3 |
| 2 | 1 | i1 |
+------+------+------+
5 rows in set (0.00 sec)
Lets also assume that all possible values for C is in the table. Then we can construct a reference table like this.
mysql> SELECT a,b,c FROM (SELECT DISTINCT a,b FROM stuff) t1 CROSS JOIN (SELECT DISTINCT c FROM stuff) t2;
+------+------+------+
| a | b | c |
+------+------+------+
| 1 | 1 | i1 |
| 1 | 2 | i1 |
| 2 | 1 | i1 |
| 1 | 1 | i2 |
| 1 | 2 | i2 |
| 2 | 1 | i2 |
| 1 | 1 | i3 |
| 1 | 2 | i3 |
| 2 | 1 | i3 |
+------+------+------+
9 rows in set (0.00 sec)
We can then compare the table with actual data and the reference table by joining them together like this and get all missing rows like this:
mysql> SELECT * FROM stuff RIGHT JOIN (SELECT a,b,c FROM (SELECT DISTINCT a,b FROM stuff) t1 CROSS JOIN (SELECT DISTINCT c FROM stuff) t2) r ON stuff.a = r.a AND stuff.b = r.b AND stuff.c = r.c WHERE stuff.a IS NULL;
+------+------+------+------+------+------+
| a | b | c | a | b | c |
+------+------+------+------+------+------+
| NULL | NULL | NULL | 1 | 2 | i1 |
| NULL | NULL | NULL | 2 | 1 | i2 |
| NULL | NULL | NULL | 1 | 1 | i3 |
| NULL | NULL | NULL | 2 | 1 | i3 |
+------+------+------+------+------+------+
4 rows in set (0.00 sec)
The RIGHT JOIN ON a,b,c will match the rows in the reference table r against the actual rows. The missing rows will manifest as NULL on stuff side. Therefore we can get all missing rows by selecting any row with a NULL field in the stuff table.
Edit: You can change the SELECT * ... in the last query to SELECT count(*) ... and you get the number of missing rows in this case 4.
You can do this with a simple count. The number of expected rows is the number of distinct elements in A times the number of distinct elements in B times the number of distinct elements in C.
To count the number that are missing, just do arithmetic on the appropriate values:
select (cnt - cntA*cntB*cntC) as NumMissingRows
from (select count(distinct a) as cntA,
count(distinct b) as cntB,
count(distinct c) as cntC,
count(*) as cnt
from t
) t
What about such a query, this might not be the best performance but for a one time task this should work.
SELECT t1.id,
(
SELECT t2.id FROM table t2 WHERE t2.id < t1.id ORDER BY t2.id DESC LIMIT 1
) as prv
FROM table t1
HAVING id <> prv + 1
I would think about doing it this way, which will still work even if there are duplicates in your list of values. This sames doing any looping over the resulting fields (just a single row comes back which will tell you how many unique in your array are not found on the table.
SELECT COUNT(*)
FROM (SELECT 'i1' AS aCol
UNION
SELECT 'i2' AS aCol
UNION
SELECT 'i3' AS aCol
UNION
.......
UNION
SELECT 'in' AS aCol) Sub1
LEFT OUTER JOIN aTable
ON Sub1.aCol = aTable.c
WHERE aTable.c IS NULL
Could also be modified very easily to bring back a list of the items that are not found should that be required in the future.

Categories