MySQL first free number between exists values - mysql

Problem: Get first free port for server between exists values. If there is not a free place then take the highest + 1.
Extra: If server_deleted=1 then we can take the free port. Also we should take the free port when there is a gap between values. We should include server_type in where clause. The min value is first port e.g. 9000.
Now my query looks:
SELECT server_port + 1
FROM pro_servers s
WHERE s.server_port <> 0
AND s.server_type = 'ts3'
AND s.server_deleted = 0
AND NOT EXISTS
( SELECT s1.server_port
FROM pro_servers s1
WHERE s1.server_port <> 0
AND s1.server_type = 'ts3'
AND s1.server_port = s.server_port + 1
AND s1.server_deleted = 0
)
ORDER BY server_port LIMIT 1
I think there is better way to do it. This query executes very slow.
For example the first=minimum=lowest = 9000, next is 9002, 9003. We need to get the 9001. If we add 9001 then get 9004. The first value 9000 exists in the table.
SAMPLE DATA
+-----------+-------------+----------------+-------------+
| server_id | server_port | server_deleted | server_type |
+-----------+-------------+----------------+-------------+
| 151 | 9500 | 1 | teamspeak3 |
| 8459 | 9500 | 0 | teamspeak3 |
| 183 | 9501 | 1 | teamspeak3 |
| 264 | 9502 | 1 | teamspeak3 |
| 4155 | 9502 | 1 | teamspeak3 |
| 2707 | 9503 | 1 | teamspeak3 |
| 4160 | 9503 | 1 | teamspeak3 |
| 154 | 9504 | 1 | teamspeak3 |
| 4163 | 9504 | 1 | teamspeak3 |
| 285 | 9506 | 1 | teamspeak3 |
| 4167 | 9506 | 1 | teamspeak3 |
| 8454 | 9506 | 0 | teamspeak3 |
| 241 | 9507 | 1 | teamspeak3 |
| 4169 | 9507 | 1 | teamspeak3 |
| 188 | 9509 | 1 | teamspeak3 |
| 4177 | 9509 | 1 | teamspeak3 |
+-----------+-------------+----------------+-------------+
QUERY RESULT: 9501. When we used this port then next: 9502, 9503, 9504, 9505, 9507, 9508, 9509, 9510 etc.

SELECT data.sPort
FROM
((SELECT (s.server_port + 1) sPort
FROM pro_servers s
LEFT JOIN pro_servers sp1 ON sp1.server_port = s.server_port + 1
WHERE (sp1.server_port IS NULL)
ORDER BY sPort)
UNION ALL
(SELECT s.server_port sPost
FROM pro_servers s
GROUP BY s.server_port
HAVING COUNT(s.server_port) = SUM(s.server_deleted)
ORDER BY sPort)) AS data
ORDER BY data.sPort
LIMIT 1
SqlFiddle: http://sqlfiddle.com/#!2/12ab1/2
How it works
Left Join pro_servers with pro_servers with server_port = server_port + 1 join condition and take rows, where there is null in the next port. These rows shows first ports from every gap. The port Id can be taken as server_port + 1.
Take all deleted ports.
Union 1. and 2., order by server_port and take the first one.
There is one assumption for an answer - port with the lowest number is always taken. If it's not true, check that port separately (or add another UNION ALL to the query).

Also, you can try to index the table.
CREATE INDEX indexName ON tableName(field1 [,field2...]);

I don't know if this will be faster but it will certainly help you to avoid repeating same conditions twice:
SELECT
MAX(server_port) + 1 AS first_available_port
FROM (
SELECT
server_port,
#row := #row + 1 AS row
FROM
pro_servers AS s,
(SELECT #row := 0) AS x
WHERE server_port <> 0
AND server_type = 'ts3'
AND server_deleted = 0
) AS s
GROUP BY
server_port - row
ORDER BY
server_port - row
LIMIT 1
;
The inner query enumerates existing ports matching the conditions. Now, the difference between the port numbers and the row numbers will stay the same for rows belonging to the same group of consecutive rows. The outer query groups by that difference and returns the first group's highest port plus one.

I would use this query that uses NOT EXISTS:
SELECT MIN(server_port)+1
FROM pro_servers p1
WHERE
p1.server_type = 'ts3' AND
NOT EXISTS (SELECT server_port
FROM pro_servers p2
WHERE p1.server_port=p2.server_port-1
AND p1.server_type=p2.server_type
AND p2.server_deleted=0)
or this that uses a LEFT JOIN:
SELECT MIN(p1.server_port)+1
FROM
pro_servers p1 LEFT JOIN pro_servers p2
ON p1.server_port=p2.server_port-1
AND p1.server_type=p2.server_type
AND p2.server_deleted=0
WHERE
p2.server_port IS NULL
AND p1.server_type='ts3'
Please see fiddle here.

Related

Performant way to self-join and filter by revised rows

I'm trying to select all rows in this table, with the constraint that revised id's are selected instead of the original ones. So, if a row has a revision, that revision is selected instead of that row, if there are multiple revision numbers the highest revision number is preferred.
I think an example table, output, and query will explain this better:
Table:
+----+-------+-------------+-----------------+-------------+
| id | value | original_id | revision_number | is_revision |
+----+-------+-------------+-----------------+-------------+
| 1 | abcd | null | null | 0 |
| 2 | zxcv | null | null | 0 |
| 3 | qwert | null | null | 0 |
| 4 | abd | 1 | 1 | 1 |
| 5 | abcde | 1 | 2 | 1 |
| 6 | zxcvb | 2 | 1 | 1 |
| 7 | poiu | null | null | 0 |
+----+-------+-------------+-----------------+-------------+
Desired Output:
+----+-------+-------------+-----------------+
| id | value | original_id | revision_number |
+----+-------+-------------+-----------------+
| 3 | qwert | null | null |
| 5 | abcde | 1 | 2 |
| 6 | zxcvb | 2 | 1 |
| 7 | poiu | null | null |
+----+-------+-------------+-----------------+
View Called revisions_max:
SELECT
responses.original_id AS original_id,
MAX(responses.revision_number) AS revision
FROM
responses
WHERE
original_id IS NOT NULL
GROUP BY responses.original_id
My Current Query:
SELECT
responses.*
FROM
responses
WHERE
id NOT IN (
SELECT
original_id
FROM
revisions_max
)
AND
is_revision = 0
UNION
SELECT
responses.*
FROM
responses
INNER JOIN revisions_max ON revisions_max.original_id = responses.original_id
AND revisions_max.revision_number = responses.revision_number
This query works, but takes 0.06 seconds to run. With a table of only 2000 rows. This table will quickly start expanding to tens or hundreds of thousands of rows. The query under the union is what takes most of the time.
What can I do to improve this queries performance?
How about using coalesce()?
SELECT COALESCE(y.id, x.id) AS id,
COALESCE(y.value, x.value) AS value,
COALESCE(y.original_id, x.original_id) AS original_id,
COALESCE(y.revision_number, x.revision_number) AS revision_number
FROM responses x
LEFT JOIN (SELECT r1.*
FROM responses r1
INNER JOIN (SELECT responses.original_id AS
original_id,
Max(responses.revision_number) AS
revision
FROM responses
WHERE original_id IS NOT NULL
GROUP BY responses.original_id) rev
ON r1.original_id = rev.original_id
AND r1.revision_number = rev.revision) y
ON x.id = y.original_id
WHERE y.id IS NOT NULL
OR x.original_id IS NULL;
The approach I would take with any other DBMS is to use NOT EXISTS:
SELECT r1.*
FROM Responses AS r1
WHERE NOT EXISTS
( SELECT 1
FROM Responses AS r2
WHERE r2.original_id = COALESCE(r1.original_id, r1.id)
AND r2.revision_number > COALESCE(r1.revision_number, 0)
);
To remove any rows where a higher revision number exists for the same id (or original_id if it is populated). However, in MySQL, LEFT JOIN/IS NULL will perform better than NOT EXISTS1. As such I would rewrite the above as:
SELECT r1.*
FROM Responses AS r1
LEFT JOIN Responses AS r2
ON r2.original_id = COALESCE(r1.original_id, r1.id)
AND r2.revision_number > COALESCE(r1.revision_number, 0)
WHERE r2.id IS NULL;
Example on DBFiddle
I realise that you have said that you don't want to use LEFT JOIN and check for nulls, but I don't see that there is a better solution.
1. At least this was the case historically, I don't actively use MySQL so don't keep up to date with developments in the optimiser

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.

Select distinct from a list based on set of parameters

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

MySql - Increment according to another column

I have a table (innoDB) that has 3 columns: ID, ID_FATHER, ROWPOS. ID is auto_increment and ROWPOS has values from other table. I need ID_FATHER to be incremented by 1 if ROWPOS is not a sequence, if it is a sequence ID_FATHER should not increment.
Like this:
ID | ID_FATHER | ROWPOS
1 | 1 | 250
2 | 2 | 253
3 | 2 | 254
4 | 3 | 260
5 | 4 | 263
6 | 5 | 268
7 | 6 | 270
8 | 6 | 271
9 | 6 | 272
10 | 7 | 276
Is there a way to do that?
With this query:
INSERT INTO mytable (i, rowpos)
SELECT #i := IF(t.rowpos = #prev_rowpos + 1, #i, #i + 1) AS i
, #prev_rowpos := t.rowpos AS rowpos
FROM temp
JOIN (SELECT #prev_rowpos := NULL, #i := 0) v
ORDER BY t.rowpos
I am able to import into the tables I want. But the problem is in the TABLE.Service, as you can see with this solution the ID_FATHER is wrong because it only increments by 1
but in this case it actually should be 2 because invoice 1 doesn't have service.
How can I solve this problem without changing all my schema.
TABLE.temp
ROW|TYPE |INVOICE_temp
1 |xxx |10
2 |xxP |led tv
3 |xxP |mp3 Player
4 |xxx |11
5 |xxP |tv cable
6 |xxS |install
xxx = Invoice number
xxP = Product
xxs = service
TABLE.Invoice_Number TABLE.Product
ID|ID_FATHER|ROWPOS|NUM ID|ID_FATHER|ROWPOS|PROD
1 | 1 | 1 | 10 1 | 1 | 2 | led tv
2 | 2 | 4 | 11 2 | 1 | 3 | mp3 player
3 | 2 | 5 | tv cable
TABLE.Service
ID|ID_FATHER|ROWPOS|SERV
1 | 1 | 6 | install
I made some changes in the query to work as I needed.
You could do something like this:
INSERT INTO mytable (i, rowpos)
SELECT #i := IF(t.rowpos = #prev_rowpos + 1, #i, #i + 1) AS i
, #prev_rowpos := t.rowpos AS rowpos
FROM another_table t
JOIN (SELECT #prev_rowpos := NULL, #i := 0) v
ORDER BY t.rowpos
(Test just the SELECT query, get that working returning the resultset you want, before you preface it with the INSERT.)
For completeness, I will add that this technique is dependent on UNDOCUMENTED and non-guaranteed behavior in MysQL, using "user variables". I've successfully used this approach many times, but for "one off" type admin functions, not ever embedded as SQL in an application.
Note that the ORDER of the expressions in the SELECT list is important, they are evaluated in the order they appear in the SELECT list. (MySQL doesn't guarantee this behavior, but we do observe it. It's important that the check of the user variables containing values from the previous row to precede the assignment of the current row values to the user variables. That's why i is returned first, followed by rowpos. If you reversed the order of those in the SELECT list, the query would operate differently, and we wouldn't get the same results.
The purpose of the inline view (aliased as v) is to initialize the user variables. Since MySQL materializes that view query into a "derived table" before the outer query runs, those variables get initialized before they are referenced in the outer query. We don't really care what the inline view query actually returns, except that we need it to return exactly one row (because we reference it in a JOIN operation to the table we really want to query).
E.g.:
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
,rowpos INT NOT NULL
);
INSERT INTO my_table (rowpos) VALUES
(250),
(253),
(254),
(260),
(263),
(268),
(270),
(271),
(272),
(276);
SELECT x.*
, #i:=#i+ISNULL(y.id) i
FROM my_table x
LEFT
JOIN my_table y
ON y.id < x.id
AND y.rowpos = x.rowpos - 1
, (SELECT #i:=0) vals
ORDER
BY x.id;
+----+--------+------+
| id | rowpos | i |
+----+--------+------+
| 1 | 250 | 1 |
| 2 | 253 | 2 |
| 3 | 254 | 2 |
| 4 | 260 | 3 |
| 5 | 263 | 4 |
| 6 | 268 | 5 |
| 7 | 270 | 6 |
| 8 | 271 | 6 |
| 9 | 272 | 6 |
| 10 | 276 | 7 |
+----+--------+------+

MySQL: time between rows

I'm new to MySQL and I'm trying to figure out how to calculate the time passed between the rows in a log table.
The table is a basic table with ID , Hostname , Info , Timestamp, with data like:
+---+----------+-------------------+---------------------+
|ID | Hostname | Info | Timestamp |
+---+----------+-------------------+---------------------+
|445| switch1 | "port 1 inserted" | 2013-01-19 19:51:40 |
|446| switch1 | "port 2 inserted" | 2013-01-19 19:59:41 |
|447| Router2 | "alarm fan speed" | 2013-01-19 20:00:40 |
|448| switch1 | "alarm fan speed" | 2013-01-19 20:12:20 |
|449| Router2 | "alarm fan speed" | 2013-01-19 21:42:41 |
+---+----------+-------------------+---------------------+
So basically I want to get the time difference between the rows with the same HOSTNAME, in this case between row 445 and 446 it would result in 8 minutes 1 second. And between 446 and 448 it would result in 12 minutes and 39 seconds . And so on...
Any tips on this would be greatly appreciated.
This will give you the time difference in seconds between rows:
SELECT c.info,
CASE
WHEN f.`timestamp` IS NOT NULL THEN
Timestampdiff(second, f.`timestamp`,
c.`timestamp`)
ELSE NULL
end AS time_diff
FROM (SELECT #rowa := #rowa + 1 AS id,
a.hostname,
a.info,
a.`timestamp`
FROM sparkles a
JOIN (SELECT #rowa := 0) b
WHERE a.hostname = 'switch1') c
LEFT JOIN (SELECT #rowb := #rowb + 1 AS id,
d.hostname,
d.info,
d.`timestamp`
FROM sparkles d
JOIN (SELECT #rowb := 0) e
WHERE d.hostname = 'switch1') f
ON f.id = c.id - 1
Result (for switch1 as the hostname)
| INFO | TIME_DIFF |
-------------------------------
| port 1 inserted | (null) |
| port 2 inserted | 481 |
| alarm fan speed | 759 |
See the demo
Result (for Router2 as the hostname)
| INFO | TIME_DIFF |
-------------------------------
| alarm fan speed | (null) |
| alarm fan speed | 6121 |
See the demo
Try this:
SELECT id, IF(#lastdate = '', NULL, TIMESTAMPDIFF(SECOND, TIMESTAMP, #lastdate)),
#lastdate:=TIMESTAMP
FROM tablename, (SELECT #lastdate:='') a;