Any way to remove the duplicate SELECT statement? - mysql

For the sake of brevity, let's assume we have a numbers table with 2 columns: id & number:
CREATE TABLE numbers(
id INT NOT NULL AUTO_INCREMENT,
NUMBER INT NOT NULL,
PRIMARY KEY ( id )
);
I want the number column to auto-increment, but restart from 1 after hitting 100, satisfying the following requirements:
If there are no records yet, set number to 1.
If there are already records in the table:
If the last record's number is less than 100, set number to that + 1.
If the last record's number is 100 or more, set number to 1.
Here's what I have so far:
INSERT INTO `numbers` (`number`)
VALUES (
IF(
IFNULL(
(SELECT `number`
FROM `numbers` as n
ORDER BY `id` DESC
LIMIT 1),
0
) > 99,
1,
IFNULL(
(SELECT `number`
FROM `numbers` as n
ORDER BY `id` DESC
LIMIT 1),
0
) + 1
)
)
This works, but uses two identical SELECT queries.
Is there any way to remove the duplicate SELECT?
P.S. I want to do all this within a query. No triggers or stored procedures.

Try to use modulo % operator
INSERT INTO `numbers` (`number`)
VALUES (
IFNULL(
(SELECT `number`
FROM `numbers` as n
ORDER BY `id` DESC
LIMIT 1),
0
) % 100 + 1
)

You could use a temp variable.
INSERT INTO `numbers`(`number`) VALUES (
IF((#a := IFNULL((SELECT number FROM `numbers` AS n ORDER BY id DESC LIMIT 1), 0)) > 99, 1, #a + 1)
);

I see. The modulo answer is nice. It feels 'brittle' to me as it would not handle the "100 or more" requirement as stated. It's fine if you change the requirement to "If the last record's number is exactly 100, set number to 1."
To me this is more intuitive:
INSERT INTO numbers (number)
SELECT
CASE
WHEN number >= 100 THEN 0
ELSE COALESCE (number, 0)
END + 1
FROM numbers
ORDER BY id DESC
LIMIT 1

Related

ordering ASC or DESC based on numbers but for non numeric rows

I have a db table which has columns with non numeric data, such as ( 1 - 100 ) and ( / ).
So when ordering based on this query:
SELECT * FROM `kw_keywords` ORDER BY `kw_keywords`.`position` ASC
it returns rows with / at the begining while it must be 1 at the begining
How can I put all rows with / or Null to the end of number based ordering ?
With conditional sorting:
SELECT *
FROM `kw_keywords`
ORDER BY `position` IS NULL, `position` = 0, `position` + 0
or:
SELECT *
FROM `kw_keywords`
ORDER BY COALESCE(`position`, 0) = 0, `position` + 0
See a simplified demo.
ORDER BY if(kw_keywords.position= '' or kw_keywords.positionis null,1,0),kw_keywords.position ASC

MySQL Query to average 3 columns and exclude 0's?

This is obviously wrong, but what would be the correct way to average the SUM of 3 columns and exclude the 0's?
SELECT (
AVG(NULLIF(`dices`.`Die1`,0)) +
AVG(NULLIF(`dices`.`Die2`,0)) +
AVG(NULLIF(`dices`.`Die3`,0))
) /3 as avgAllDice
FROM (
SELECT `Die1`,`Die2`,`Die3` FROM `GameLog`
WHERE PlayerId = "12345"
) dices
Thanks.
If I was keeping the inline view query (it's not clear why it's needed). I'd probably do something like this:
SELECT AVG( NULLIF( CASE d.i
WHEN 1 THEN dices.`Die1`
WHEN 2 THEN dices.`Die2`
WHEN 3 THEN dices.`Die3`
END
,0)
) AS `avgAllDice`
FROM ( SELECT gl.`Die1`
, gl.`Die2`
, gl.`Die3`
FROM `GameLog` gl
WHERE gl.playerId = '12345'
) dices
CROSS
JOIN ( SELECT 1 AS i UNION ALL SELECT 2 UNION ALL SELECT 3 ) d
The trick is the cross join operation, giving me three rows for each row returned from dices, and an expression that picks out values of Die1, Die2 and Die3 on each of three rows, respectively.
To exclude values of 0, we replace 0 with with NULL (since AVG doesn't include NULL values.)
Now with all of the non-zero DieN values stacked into a single column, we can just use the AVG function.
Another way to do it would be to get the numerator and denominator for each of Die1, Die2, Die3.... and then total up the numerators, total up the denominators, and then divide the total numerator by the total denominator.
This will should give an equivalent result.
SELECT ( IFNULL(t.n_die1,0) + IFNULL(t.n_die2,0) + IFNULL(t.n_die3,0) )
/ ( t.d_die1 + t.d_die2 + t.d_die3 )
AS avgAllDice
FROM ( SELECT SUM( NULLIF(gl.die1,0)) AS n_die1
, COUNT(NULLIF(gl.die1,0)) AS d_die1
, SUM( NULLIF(gl.die2,0)) AS n_die2
, COUNT(NULLIF(gl.die2,0)) AS d_die2
, SUM( NULLIF(gl.die3,0)) AS n_die3
, COUNT(NULLIF(gl.die3,0)) AS d_die3
FROM `GameLog` gl
WHERE gl.playerid = '12345'
) t
(I didn't work out what gets returned in the edge and corner cases... no matching rows in GameLog, all values of Die1, Die2 and Die3 are zero, etc., for either query. The results might be slightly different, returning a zero instead of NULL, divide by zero edge case, etc.)
FOLLOWUP
I ran a quick test of both queries.
CREATE DATABASE d20170228 ;
USE d20170228 ;
CREATE TABLE GameLog
( playerid VARCHAR(5) DEFAULT '12345'
, die1 TINYINT
, die2 TINYINT
, die3 TINYINT
);
INSERT INTO GameLog (die1,die2,die3)
VALUES (3,0,0),(2,1,0),(4,3,3),(3,3,3),(0,0,0),(4,4,4),(5,4,0),(0,0,2)
;
SELECT (3+2+1+4+3+3+3+3+3+4+4+4+5+4+2)/15 AS manual_avg
manual_avg is coming out 3.2.
Both queries are also returning 3.2
If you want to eliminate zeroes and NULLs, you can simply SELECT from the filtered master set multiple times, doing a UNION ALL on the results, then averaging against that.
SELECT AVG(`allDice`.`DieResult`)
FROM (
SELECT `Die1` AS `DieResult` FROM `GameLog` WHERE COALESCE(`Die1`, 0) <> 0 AND PlayerId = '12345'
UNION ALL
SELECT `Die2` FROM `GameLog` WHERE COALESCE(`Die2`, 0) <> 0 AND PlayerId = '12345'
UNION ALL
SELECT `Die3` FROM `GameLog` WHERE COALESCE(`Die3`, 0) <> 0 AND PlayerId = '12345'
) AS `allDice`
There's no need to overthink this one, it's not too difficult a problem

SQL select top 3 or more if there are more items sharing the 3rd highest value?

Any SQL experts out there who can explain how I select say the top 3 items. When the 3rd item is equal to the 4th and 5th I'd like those included too but only in that situation. e.g. with the following list
40,
30,
15,
15,
15,
10
it would return 40,30,15,15,15 not 40,30,15.
If you want to output only the score field, you can do it like this:
SET #ranks = 3;
PREPARE stmt_top3 FROM
'SELECT score FROM (
SELECT score, (#row := #row + 1), IF (#row = ?, #min_score := score, NULL)
FROM user_score
WHERE score IN (
SELECT top_score.score FROM (
SELECT DISTINCT score,
(#row := 0), (#min_score := 0)
FROM user_score
ORDER BY score DESC
LIMIT ?
) AS top_score
)
ORDER BY score DESC
) AS score_rows
WHERE score >= #min_score
ORDER BY score DESC';
EXECUTE stmt_top3 USING #ranks, #ranks;
DEMO # SQL Fiddle
I used a prepared statement, so the number of ranks to show is flexible. If you don't want that, just hardcode a 3 instead of the 2 ?.
Otherwise, if you also need the rank and the user_id, i.e., here's a more extensive approach:
Highscore-Like Ranking (Shared Ranks)
Selects all rows that share the first rank, no matter how many there are.
As long as the number of these rows is not greater than or equal to the maximum of ranks allowed (3 in your example) the next rank is the number of rows + 1.
And everything begins from the start and so on ...
SET #ranks = 3;
PREPARE stmt_top3 FROM
'SELECT user_score.user_id, score_rank.rank, score_rank.score
FROM user_score
INNER JOIN (
SELECT (#last_rank := #last_rank + #last_equal_score) AS rank,
score, (#last_equal_score := count(score)) AS equal_score
FROM user_score
WHERE score IN (
SELECT top_score.score FROM (
SELECT DISTINCT score,
(#last_rank := 1), (#last_equal_score := 0)
FROM user_score
ORDER BY score DESC
LIMIT ?
) AS top_score
)
GROUP BY score
ORDER BY score DESC
) AS score_rank
ON user_score.score = score_rank.score
WHERE score_rank.rank <= ?
ORDER BY score_rank.rank ASC';
EXECUTE stmt_top3 USING #ranks, #ranks;
DEMO # SQL Fiddle
Due to the fact that MySQL does not support LIMIT in subqueries for certain subquery operators such as IN, you have to wrap your subquery that contains a LIMIT clause in another simple subquery to avoid the following error:
ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT
& IN/ALL/ANY/SOME subquery'
Restrictions on Subqueries
I used the following setup for testing:
CREATE TABLE user (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
) ENGINE=InnoDB;
INSERT INTO user (id) VALUES
(NULL),
(NULL),
(NULL),
(NULL),
(NULL),
(NULL);
CREATE TABLE user_score (
user_id INT UNSIGNED NOT NULL UNIQUE,
score INT NOT NULL DEFAULT 0,
INDEX (score),
FOREIGN KEY (user_id) REFERENCES user (id)
) ENGINE=InnoDB;
INSERT INTO user_score (user_id, score) VALUES
(1, 40),
(2, 30),
(3, 15),
(4, 15),
(5, 15),
(6, 10);
SELECT *
FROM myTable
WHERE Field1 IN(
SELECT *
FROM (
SELECT DISTINCT Field1
FROM myTable
ORDER BY Field1 DESC
LIMIT 3)
) t
The only issue you might have from your example is if you specifically DON'T want to include duplicates of 40 & 30?

How to get the smallest Integers not yet in a database column

I have a table in a MySQL DB with an UNIQUE INT(10) column. The table is pretty populated and the row contains non-consecutive entries of Integer numbers in that column. I would like to do a query, which gets me the smallest number (or the n smallest numbers) that is not in any row.
Example: The table contains rows with values (1, 2, 3, 5, 7, 8, 10, 12, 15) for the column. The sql statement should return i.e. the five lowest non-contained values, which are 4, 6, 9, 11, 13 in this case.
Is this possible with MySQL?
You can use a "numbers" table (it's handy for various operations):
CREATE TABLE num
( i UNSIGNED INT NOT NULL
, PRIMARY KEY (i)
) ;
INSERT INTO num (i)
VALUES
(1), (2), ..., (1000000) ;
Then:
SELECT
num.i
FROM
num
LEFT JOIN
tableX AS t
ON num.i = t.columnX
WHERE
t.columnX IS NULL
ORDER BY
num.i
LIMIT 5
or:
SELECT
num.i
FROM
num
WHERE
NOT EXISTS
( SELECT *
FROM tableX AS t
WHERE num.i = t.columnX
)
ORDER BY
num.i
LIMIT 5
Another approach, without using an auxilary table, would be to use MySQL variables. You can test it in SQL-Fiddle, test-2. The output is not the same as the previous (just to show that it can be done):
SELECT start_id, end_id
FROM
( SELECT
IF( t.columnX <> #id, #id, NULL) AS start_id
, IF( t.columnX <> #id, t.columnX-1, NULL) AS end_id
, #rows := #rows + (t.columnX - #id) AS r
, #id := t.columnX + 1 AS running_id
FROM
tableX AS t
CROSS JOIN
( SELECT #rows := 0
, #id := 1
) AS dummy
WHERE
#rows < 5
ORDER BY
t.columnX
) AS tmp
WHERE
start_id IS NOT NULL
This will work, but I think it is pretty inefficient. You won't need an extra table though (a table that would be (2^31-1)*4/1024^3 = 8GB for all positive numbers in INT). Also I advise you look at why you need this, because it might not be neccesary.
Also it will return the start and end of a range, but not all numbers in that range. (e.g. if you have numbers 1 and 5 it will return {0,2,4,6})
SELECT (t.num-1) AS bound FROM t
WHERE t.num-1 NOT IN (SELECT t.num FROM t)
UNION
SELECT (t.num+1) AS bound FROM t
WHERE t.num+1 NOT IN (SELECT t.num FROM t)
As I said this will be pretty inefficient, JOINs might be faster but you would need benchmark it.
SELECT (t.num-1) AS bound FROM t
LEFT JOIN t AS u ON t.num-1 = u.num
WHERE u.num IS NULL
UNION
SELECT (t.num+1) AS bound FROM t
LEFT JOIN t AS u ON t.num+1 = u.num
WHERE u.num IS NULL

MySQL - match post code based on one or two first characters

I'm trying to create a SQL statement to find the matching record based on the provided post code and stored post codes in the database plus the weight aspect.
The post codes in the database are between 1 or 2 characters i.e. B, BA ...
Now - the value passed to the SQL statement will always have 2 first characters of the client's post code. How can I find the match for it? Say I have a post code B1, which would only match the single B in the database plus the weight aspect, which I'm ok with.
Here's my current SQL statement, which also takes the factor of the free shipping above certain weight:
SELECT `s`.*,
IF (
'{$weight}' > (
SELECT MAX(`weight_from`)
FROM `shipping`
WHERE UPPER(SUBSTRING(`post_code`, 1, 2)) = 'B1'
),
(
SELECT `cost`
FROM `shipping`
WHERE UPPER(SUBSTRING(`post_code`, 1, 2)) = 'B1'
ORDER BY `weight_from` DESC
LIMIT 0, 1
),
`s`.`cost`
) AS `cost`
FROM `shipping` `s`
WHERE UPPER(SUBSTRING(`s`.`post_code`, 1, 2)) = 'B1'
AND
(
(
'{$weight}' > (
SELECT MAX(`weight_from`)
FROM `shipping`
WHERE UPPER(SUBSTRING(`post_code`, 1, 2)) = 'B1'
)
)
OR
('{$weight}' BETWEEN `s`.`weight_from` AND `s`.`weight_to`)
)
LIMIT 0, 1
The above however uses the SUBSTRING() function with hard coded number of characters set to 2 - this is where I need some help really to make it match only number of characters that matches the provided post code - in this case B1.
Marcus - thanks for the help - outstanding example - here's what my code look like for those who also wonder:
First I've run the following statement to get the right post code:
(
SELECT `post_code`
FROM `shipping`
WHERE `post_code` = 'B1'
)
UNION
(
SELECT `post_code`
FROM `shipping`
WHERE `post_code` = SUBSTRING('B1', 1, 1)
)
ORDER BY `post_code` DESC
LIMIT 0, 1
Then, based on the returned value assigned to the 'post_code' index my second statement followed with:
$post_code = $result['post_code'];
SELECT `s`.*,
IF (
'1000' > (
SELECT MAX(`weight_from`)
FROM `shipping`
WHERE `post_code` = '{$post_code}'
),
(
SELECT `cost`
FROM `shipping`
WHERE `post_code` = '{$post_code}'
ORDER BY `weight_from` DESC
LIMIT 0, 1
),
`s`.`cost`
) AS `cost`
FROM `shipping` `s`
WHERE `s`.`post_code` = '{$post_code}'
AND
(
(
'1000' > (
SELECT MAX(`weight_from`)
FROM `shipping`
WHERE `post_code` = '{$post_code}'
ORDER BY LENGTH(`post_code`) DESC
)
)
OR
('1000' BETWEEN `s`.`weight_from` AND `s`.`weight_to`)
)
LIMIT 0, 1
The following query will get all results where the post_code in the shipping table matches the beginning of the passed in post_code, then it orders it most explicit to least explicit, returning the most explicit one:
SELECT *
FROM shipping
WHERE post_code = SUBSTRING('B1', 1, LENGTH(post_code))
ORDER BY LENGTH(post_code) DESC
LIMIT 1
Update
While this query is flexible, it's not very fast, since it can't utilize an index. If the shipping table is large, and you'll only pass in up to two characters, it might be faster to make two separate calls.
First, try the most explicit call.
SELECT *
FROM shipping
WHERE post_code = 'B1'
If it doesn't return a result then search on a single character:
SELECT *
FROM shipping
WHERE post_code = SUBSTRING('B1', 1, 1)
Of course, you can combine these with a UNION if you must do it in a single call:
SELECT * FROM
((SELECT *
FROM shipping
WHERE post_code = 'B1')
UNION
(SELECT *
FROM shipping
WHERE post_code = SUBSTRING('B1', 1, 1))) a
ORDER BY post_code DESC
LIMIT 1