mysql - can i differentiate values of same column? - mysql

Is there a way to make two or more same values in the same column to be different by database engine?
Imagine that I have column like this:
id | votes
----------
1 | 20
2 | 20
3 | 19
4 | 16
5 | 15
I have to make all the votes different in that way that i increment first of two equal values, and substract other one.
After one iteration my DB should look like this:
----------
1 | 21
2 | 19
3 | 19
4 | 16
5 | 15
Because we still have two same values (id 2 and 3) we keep going with second iteration:
----------
1 | 21
2 | 20
3 | 18
4 | 16
5 | 15
Can i update mysql database somehow to make that for me? Or should i select values, compare them in php and update?
Thank you!

This does not exactly match your expected result, but may lead you toward an acceptable compromise.
MySQL does not yet have window functions such as rank(), dense_rank() or row_number() but you can mimic row_number using #variables such as seen below.
See this demo
MySQL 5.6 Schema Setup:
CREATE TABLE Table1
(`id` int, `votes` int)
;
INSERT INTO Table1
(`id`, `votes`)
VALUES
(1, 20),
(2, 20),
(3, 19),
(4, 16),
(5, 15)
;
Query 1:
SELECT
t.id
, t.votes
, m.v - #row_num AS adjusted_votes
, #row_num := #row_num+1 AS RowNumber
FROM Table1 t
CROSS JOIN (SELECT MAX(votes) v from Table1) m
CROSS JOIN (SELECT #row_num :=0) vars
ORDER BY
t.votes DESC
, t.id
Results:
| id | votes | adjusted_votes | RowNumber |
|----|-------|----------------|-----------|
| 1 | 20 | 20 | 1 |
| 2 | 20 | 19 | 2 |
| 3 | 19 | 18 | 3 |
| 4 | 16 | 17 | 4 |
| 5 | 15 | 16 | 5 |
Note that the reduction of 20 to 19 for id 2 appears to be completely arbitrary and that id is used purely as a tie-breaker when values are equal.

Related

Dividing a row by reference value in mysql

Please help me.
Here is the raw data:
uid | id | value |
1 | a | 389 |
2 | b | 201 |
3 | c | 170 |
if.... When the reference value is '200'
How do you get it to come out like this?
mysql..
no| uid | id | value | cut
1 | 1 | a | 200 | 200
2 | 1 | a | 189 | 200
3 | 2 | b | 200 | 200
4 | 2 | b | 1 | 200
5 | 3 | c | 170 | 200
help me!!!!
If you are using MySQL 8 or later, Recursive CTEs can help:
CREATE DATABASE test;
USE test;
CREATE TABLE TestData (uid INTEGER, id VARCHAR(8), value INTEGER);
INSERT INTO TestData VALUES (1, 'a', 389);
INSERT INTO TestData VALUES (2, 'b', 201);
INSERT INTO TestData VALUES (3, 'c', 170);
INSERT INTO TestData VALUES (4, 'd', 550);
-- Set up an auto-incrementing row number
SET #row_num = 0;
WITH RECURSIVE cte (uid, id, value, remainder) AS (
-- start with a copy of the table, but adding another column for the value that is at most 200
SELECT a.uid, a.id, LEAST(a.value, 200), a.value AS "remainder" FROM TestData a
UNION
-- repeatedly select from the previous result set, meanwhile decrementing the "remainder" column
SELECT uid, id, LEAST(remainder - 200, 200), remainder - 200 FROM cte WHERE remainder > 200
)
-- select the actual data that we care about
SELECT (#row_num := #row_num + 1) AS no, uid, id, value, 200 AS "cut" FROM cte ORDER BY id, value DESC;
This results in the following table:
no | uid | id | value | cut
1 | 1 | a | 200 | 200
2 | 1 | a | 189 | 200
3 | 2 | b | 200 | 200
4 | 2 | b | 1 | 200
5 | 3 | c | 170 | 200
6 | 4 | d | 200 | 200
7 | 4 | d | 200 | 200
8 | 4 | d | 150 | 200
How about this:
SET #row_num = 0;
SELECT (#row_num := #row_num + 1) AS "no",uid,id,valueA, "200" as cut FROM
(SELECT uid,id,IF(valueA/200 > 1,200,valueA) AS "valueA" FROM tableA UNION ALL
SELECT uid,id,IF(valueA/200 > 2,200,valueA-200) AS "valueA" FROM tableA UNION ALL
SELECT uid,id,IF(valueA/200 > 3,200,valueA-400) AS "valueA" FROM tableA) a
WHERE valueA > 0
ORDER BY uid,id,valueA DESC;
If valueA divide by 200 equal to more than 1, it will return 200, otherwise it will only return what's in valueA. That's the first syntax among the UNION ALL queries.
The second one I added another condition to as if valueA divided by 200 IS NOT equal to more than 2, it will return valueA-200. You can try to execute the UNION ALL queries to see the results.
It's not the best way to do it and I am sure that there are better solution out there but I got it working using this method. You might use this as temporary solution or at least give you some ideas.
Consider writing a stored procedure instead of doing this all in pure SQL or just select the rows from the table and do the following in php:
From my understanding, you're looking for this:
no = 1
For each row in the result set
for i = 0 to int(value/cut)
output the row with value set to cut
no += 1
output the row with value set to mod(value, cut)
no += 1

Latest datetime from unique mysql index

I have a table. It has a pk of id and an index of [service, check, datetime].
id service check datetime score
---|-------|-------|----------|-----
1 | 1 | 4 |4/03/2009 | 399
2 | 2 | 4 |4/03/2009 | 522
3 | 1 | 5 |4/03/2009 | 244
4 | 2 | 5 |4/03/2009 | 555
5 | 1 | 4 |4/04/2009 | 111
6 | 2 | 4 |4/04/2009 | 322
7 | 1 | 5 |4/05/2009 | 455
8 | 2 | 5 |4/05/2009 | 675
Given a service 2 I need to select the rows for each unique check where it has the max date. So my result would look like this table.
id service check datetime score
---|-------|-------|----------|-----
6 | 2 | 4 |4/04/2009 | 322
8 | 2 | 5 |4/05/2009 | 675
Is there a short query for this? The best I have is this, but it returns too many checks. I just need the unique checks at it's latest datetime.
SELECT * FROM table where service=?;
First you need find out the biggest date for each check
SELECT `check`, MAX(`datetime`)
FROM YourTable
WHERE `service` = 2
GROUP BY `check`
Then join back to get the rest of the data.
SELECT Y.*
FROM YourTable Y
JOIN ( SELECT `check`, MAX(`datetime`) as m_date
FROM YourTable
WHERE `service` = 2
GROUP BY check) as `filter`
ON Y.`service` = `filter`.service
AND Y.`datetime` = `fiter`.m_date
WHERE Y.`service` = 2

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 |
+----+--------+------+

Split data in to ranges and display count

I am not an expert in MySql. I am trying to split the data in my table in to ranges based on account_no. This is my table.
mysql> select * from manager;
+----+-------+------------+
| id | name | account_no |
+----+-------+------------+
| 1 | John | 5 |
| 2 | Peter | 15 |
| 3 | Tony | 18 |
| 4 | Mac | 35 |
| 5 | Max | 55 |
| 6 | Smith | 58 |
+----+-------+------------+
As you see the account_no is a positive number. I want to split these records in to batches of 10, based on account_no and display the count in that range.
For an example
between 0 and 10 there is only 1 record
between 11 and 20 there are 2 records
between 21 and 30 there are no records*(So this should be omitted.)*
etc...
Actually I am hoping to get an output like this.
+-------------+-----------+-------+
| range_start | range_end | count |
+-------------+-----------+-------+
| 1 | 10 | 1 | -> because there is 1 record between 1 and 10
| 11 | 20 | 2 | -> because there are 2 records between 11 and 20
| 31 | 40 | 1 | -> because there is 1 record between 31 and 40
| 51 | 60 | 2 | -> because there are 2 records between 51 and 60
+-------------+-----------+-------+
I tried several combinations but all of them give me only one row in the result. Can anybody help me?
My suggestion would be to create a table that contains the ranges - startRange and endRange:
CREATE TABLE range_values (`startRange` int, `endRange` int) ;
INSERT INTO range_values(`startRange`, `endRange`)
VALUES (1, 10), (11, 20), (21, 30), (31, 40), (51, 60);
Once the table is created, then you can easily join on the table to get the count.
select r.startRange,
r.endRange,
count(m.account_no) totalCount
from manager m
inner join range_values r
on m.account_no >=startrange
and m.account_no <= endrange
group by r.startRange, r.endRange
See SQL Fiddle with Demo.
The benefit of the table is that you are not coding the range values and can easily updated the ranges in a table without having to change your code.
This query return:
| STARTRANGE | ENDRANGE | TOTALCOUNT |
--------------------------------------
| 1 | 10 | 1 |
| 11 | 20 | 2 |
| 31 | 40 | 1 |
| 51 | 60 | 2 |
If you don't want to create a new table, then you can use something similar to the following:
select startrange,
endrange,
count(m.account_no) TotalCount
from manager m
inner join
(
select 1 startRange, 10 endrange union all
select 11 startRange, 20 endrange union all
select 21 startRange, 30 endrange union all
select 31 startRange, 40 endrange union all
select 41 startRange, 50 endrange union all
select 51 startRange, 60 endrange
) r
on m.account_no >=startrange
and m.account_no <= endrange
group by r.startRange, r.endRange
See SQL Fiddle with demo
This should give you the output you would like, and includes the ranges with zero in the count column.
SET #rs = 0; SELECT IF(#rs, #rs := #rs + 10, #rs := 1) AS range_start, #rs + 9 AS range_end, (SELECT COUNT(id) FROM manager WHERE account_no >= #rs AND account_no <= #rs + 9) AS `count` FROM manager;
To omit the rows with zero in the count column;
SET #rs = 0; SELECT * FROM (SELECT IF(#rs, #rs := #rs + 10, #rs := 1) AS range_start, #rs + 9 AS range_end, (SELECT COUNT(id) FROM manager WHERE account_no >= #rs AND account_no <= #rs + 9) AS `count` FROM manager) AS data WHERE `count` > 0;
Try with something like this:
SELECT
CASE
WHEN range_start < 10 THEN 'Under 10'
WHEN range_start BETWEEN 11 and 29 THEN '11 - 29'
(...more ranges...)
END as range,
COUNT(*) AS count
GROUP BY range
ORDER BY range
You should use functions similar to rank and dense_rank in MSSQL, you can implement them in MySQL starting at the following link:
http://www.folkstalk.com/2013/03/grouped-dense-rank-function-mysql-sql-query.html

How do I create a period date range from a mysql table grouping every common sequence of value in a column

My goal is to return a start and end date having same value in a column. Here is my table. The (*) have been marked to give you the idea of how I want to get "EndDate" for every similar sequence value of A & B columns
ID | DayDate | A | B
-----------------------------------------------
1 | 2010/07/1 | 200 | 300
2 | 2010/07/2 | 200 | 300 *
3 | 2010/07/3 | 150 | 250
4 | 2010/07/4 | 150 | 250 *
8 | 2010/07/5 | 150 | 350 *
9 | 2010/07/6 | 200 | 300
10 | 2010/07/7 | 200 | 300 *
11 | 2010/07/8 | 100 | 200
12 | 2010/07/9 | 100 | 200 *
and I want to get the following result table from the above table
| DayDate |EndDate | A | B
-----------------------------------------------
| 2010/07/1 |2010/07/2 | 200 | 300
| 2010/07/3 |2010/07/4 | 150 | 250
| 2010/07/5 |2010/07/5 | 150 | 350
| 2010/07/6 |2010/07/7 | 200 | 300
| 2010/07/8 |2010/07/9 | 100 | 200
UPDATE:
Thanks Mike, The approach of yours seems to work in your perspective of considering the following row as a mistake.
8 | 2010/07/5 | 150 | 350 *
However it is not a mistake. The challenge I am faced with this type of data is like a scenario of logging a market price change with date. The real problem in mycase is to select all rows with the beginning and ending date if both A & B matches in all these rows. Also to select the rows which are next to previously selected, and so on like that no data is left out in the table.
I can explain a real world scenario. A Hotel with Room A and B has room rates for each day entered in to table as explained in my question. Now the hotel needs to get a report to show the price calendar in a shorter way using start and end date, instead of listing all the dates entered. For example, on 2010/07/01 to 2010/07/02 the price of A is 200 and B is 300. This price is changed from 3rd to 4th and on 5th there is a different price only for that day where the Room B is price is changed to 350. So this is considered as a single day difference, thats why start and end dates are same.
I hope this explained the scenario of the problem. Also note that this hotel may be closed for a specific time period, lets say this is an additional problem to my first question. The problem is what if the rate is not entered on specific dates, for example on Sundays the hotel do not sell these two rooms so they entered no price, meaning the row will not exist in the table.
Creating related tables allows you much greater freedom to query and extract relevant information. Here's a few links that you might find useful:
You could start with these tutorials:
http://dev.mysql.com/tech-resources/articles/intro-to-normalization.html
http://net.tutsplus.com/tutorials/databases/sql-for-beginners/
There are also a couple of questions here on stackoverflow that might be useful:
Normalization in plain English
What exactly does database normalization do?
Anyway, on to a possible solution. The following examples use your hotel rooms analogy.
First, create a table to hold information about the hotel rooms. This table just contains the room ID and its name, but you could store other information in here, such as the room type (single, double, twin), its view (ocean front, ocean view, city view, pool view), and so on:
CREATE TABLE `room` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `name_UNIQUE` (`name` ASC) )
ENGINE = InnoDB;
Now create a table to hold the changing room rates. This table links to the room table through the room_id column. The foreign key constraint prevents records being inserted into the rate table which refer to rooms that do not exist:
CREATE TABLE `rate` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`room_id` INT UNSIGNED NOT NULL,
`date` DATE NOT NULL,
`rate` DECIMAL(6,2) UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
INDEX `fk_room_rate` (`room_id` ASC),
CONSTRAINT `fk_room_rate`
FOREIGN KEY (`room_id` )
REFERENCES `room` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
Create two rooms, and add some daily rate information about each room:
INSERT INTO `room` (`id`, `name`) VALUES (1, 'A'), (2, 'B');
INSERT INTO `rate` (`id`, `room_id`, `date`, `rate`) VALUES
( 1, 1, '2010-07-01', 200),
( 2, 1, '2010-07-02', 200),
( 3, 1, '2010-07-03', 150),
( 4, 1, '2010-07-04', 150),
( 5, 1, '2010-07-05', 150),
( 6, 1, '2010-07-06', 200),
( 7, 1, '2010-07-07', 200),
( 8, 1, '2010-07-08', 100),
( 9, 1, '2010-07-09', 100),
(10, 2, '2010-07-01', 300),
(11, 2, '2010-07-02', 300),
(12, 2, '2010-07-03', 250),
(13, 2, '2010-07-04', 250),
(14, 2, '2010-07-05', 350),
(15, 2, '2010-07-06', 300),
(16, 2, '2010-07-07', 300),
(17, 2, '2010-07-08', 200),
(18, 2, '2010-07-09', 200);
With that information stored, a simple SELECT query with a JOIN will show you the all the daily room rates:
SELECT
room.name,
rate.date,
rate.rate
FROM room
JOIN rate
ON rate.room_id = room.id;
+------+------------+--------+
| A | 2010-07-01 | 200.00 |
| A | 2010-07-02 | 200.00 |
| A | 2010-07-03 | 150.00 |
| A | 2010-07-04 | 150.00 |
| A | 2010-07-05 | 150.00 |
| A | 2010-07-06 | 200.00 |
| A | 2010-07-07 | 200.00 |
| A | 2010-07-08 | 100.00 |
| A | 2010-07-09 | 100.00 |
| B | 2010-07-01 | 300.00 |
| B | 2010-07-02 | 300.00 |
| B | 2010-07-03 | 250.00 |
| B | 2010-07-04 | 250.00 |
| B | 2010-07-05 | 350.00 |
| B | 2010-07-06 | 300.00 |
| B | 2010-07-07 | 300.00 |
| B | 2010-07-08 | 200.00 |
| B | 2010-07-09 | 200.00 |
+------+------------+--------+
To find the start and end dates for each room rate, you need a more complex query:
SELECT
id,
room_id,
MIN(date) AS start_date,
MAX(date) AS end_date,
COUNT(*) AS days,
rate
FROM (
SELECT
id,
room_id,
date,
rate,
(
SELECT COUNT(*)
FROM rate AS b
WHERE b.rate <> a.rate
AND b.date <= a.date
AND b.room_id = a.room_id
) AS grouping
FROM rate AS a
ORDER BY a.room_id, a.date
) c
GROUP BY rate, grouping
ORDER BY room_id, MIN(date);
+----+---------+------------+------------+------+--------+
| id | room_id | start_date | end_date | days | rate |
+----+---------+------------+------------+------+--------+
| 1 | 1 | 2010-07-01 | 2010-07-02 | 2 | 200.00 |
| 3 | 1 | 2010-07-03 | 2010-07-05 | 3 | 150.00 |
| 6 | 1 | 2010-07-06 | 2010-07-07 | 2 | 200.00 |
| 8 | 1 | 2010-07-08 | 2010-07-09 | 2 | 100.00 |
| 10 | 2 | 2010-07-01 | 2010-07-02 | 2 | 300.00 |
| 12 | 2 | 2010-07-03 | 2010-07-04 | 2 | 250.00 |
| 14 | 2 | 2010-07-05 | 2010-07-05 | 1 | 350.00 |
| 15 | 2 | 2010-07-06 | 2010-07-07 | 2 | 300.00 |
| 17 | 2 | 2010-07-08 | 2010-07-09 | 2 | 200.00 |
+----+---------+------------+------------+------+--------+
You can find a good explanation of the technique used in the above query here:
http://www.sqlteam.com/article/detecting-runs-or-streaks-in-your-data
My general approach is to join the table onto itself based on DayDate = DayDate+1 and the A or B values not being equal
This will find the end dates for each period (where the value is going to be different on the following day)
The only problem is, that won't find an end date for the final period. To get around this, I selct the max date from the table and union that into my list of end dates
Once you have the list of end dates defined, you can join them to the original table based on the end date being greater than or equal to the original date
From this final list, select the minimum daydate grouped by the other fields
select
min(DayDate) as DayDate,EndDate,A,B from
(SELECT DayDate, A, B, min(ends.EndDate) as EndDate
FROM yourtable
LEFT JOIN
(SELECT max(DayDate) as EndDate FROM yourtable UNION
SELECT t1.DayDate as EndDate
FROM yourtable t1
JOIN yourtable t2
ON date_add(t1.DayDate, INTERVAL 1 DAY) = t2.DayDate
AND (t1.A<>t2.A OR t1.B<>t2.B)) ends
ON ends.EndDate>=DayDate
GROUP BY DayDate, A, B) x
GROUP BY EndDate,A,B
I think I have found a solution which does produce the table desired.
SELECT
a.DayDate AS StartDate,
( SELECT b.DayDate
FROM Dates AS b
WHERE b.DayDate > a.DayDate AND (b.B = a.B OR b.B IS NULL)
ORDER BY b.DayDate ASC LIMIT 1
) AS StopDate,
a.A as A,
a.B AS B
FROM Dates AS a
WHERE Coalesce(
(SELECT c.B
FROM Dates AS c
WHERE c.DayDate <= a.DayDate
ORDER BY c.DayDate DESC LIMIT 1,1
), -99999
) <> a.B
AND a.B IS NOT NULL
ORDER BY a.DayDate ASC;
is able to generate the following table result
StartDate StopDate A B
2010-07-01 2010-07-02 200 300
2010-07-03 2010-07-04 150 250
2010-07-05 NULL 150 350
2010-07-06 2010-07-07 200 300
2010-07-08 2010-07-09 100 200
But I need a way to replace the NULL with the same date of the start date.