I'm doing an assignment in a database-course at my uni using MySQL. We've made a database of a golf club, where we (among other things) can store players results from different games.
Now I'm writing the report for the assignment, and I'm trying to prove that it's better to use an int rather than a varchar to store the results. If an int is used, disqualified players will get a NULL-value as a result. If a varchar would've been used, they would've got a string: "disqualified".
The things I'm wondering are:
Does MySQL automatically convert varchar to int if avg() is used (for example) when a calculation is about to happen?
If it does, does that slow down the database a lot (compared to if an int would've been used)?
Is it possible to do calculations on string-values? E.g. if the result-attribute is a varchar containing "52", "68", "72", can the average be calculated?
What will happen if I've got the strings listed above plus a result that is "disqualified"? Will it ignore that string like it would've ignored a NULL of an int?
Your questions can be answered by runing simple tests:
drop table if exists golf;
create table golf(id int, int_col int, char_col varchar(50));
insert into golf(id, int_col, char_col) values
(1, 10, '10'),
(2, 20, '20');
select avg(int_col), avg(char_col) from golf;
Result:
avg(int_col) | avg(char_col)
15,0000 | 15
http://rextester.com/NNAZ9432
As you can see AVG over a VARCHAR column returns the expected result.
Now add a row with NULL and 'disqualified'
drop table if exists golf;
create table golf(id int, int_col int, char_col varchar(50));
insert into golf(id, int_col, char_col)values
(1, 10, '10'),
(2, 20, '20'),
(2, NULL, 'disqualified');
select avg(int_col), avg(char_col) from golf;
Now the results are different:
avg(int_col) | avg(char_col)
15,0000 | 10
http://rextester.com/RXOQAZ69820
The reoson is: While NULL is ignored by AVG, 'disqualified' is converted to 0 and the result is (10 + 20 + 0) / 3 = 10.
To test the performance you can create a big table with dummy data. In MariaDB with the sequence plugin this can be done easily:
drop table if exists golf;
create table golf(id mediumint primary key, int_col smallint, char_col varchar(50));
insert into golf(id, int_col, char_col)
select seq id
, floor(rand(1)*1000) int_col
, floor(rand(1)*1000) char_col
from seq_1_to_1000000;
AVG over INT:
select avg(int_col) from golf;
-- query time: 187 msec
AVG over VARCHAR:
select avg(char_col) from golf;
-- query time: 203 msec
Last but not least: You should not use string types for numeric values. One more reason is sorting. If you try to sort numbers stored as strings you will get something like [10, 2, 22, 3].
You should also not use one column for different information types. In your case you could define one more column like status with values 'finished' or 'disqualified'. Another possible way is to have a flag column disqualified with values 0 or 1.
Related
My number column in data table has enum values:
1, 2, 3, 4, 5, 101, 102, 103, 104, 105
I want to set values of number = number - 100 for all rows using query:
UPDATE data SET data.number = (data.number - 100) WHERE data.number > 100
But it does not work on enum data.
This is a little complicated.
Mysql doesn'z allow number to be cast from ENUM, because the says that numbers should not be used as ENUM value.
but as you need the value if the ENUM field you can convert it into characters.
The final step is to convert that text into a number.
Schema (MySQL v5.7)
CREATE TABLE IF NOT EXISTS `data` (
number ENUM('1', '2', '3', '4', '5', '101', '102', '103', '104', '105')
) ;
INSERT INTO `data` VALUES ( '103');
UPDATE `data` SET number = CAST(CAST(`number` AS CHAR) AS UNSIGNED) - 100 WHERE CAST(CAST(`number` AS CHAR) AS UNSIGNED) >= 100;
Query #1
SELECT * FROM `data`;
| number |
| ------ |
| 3 |
View on DB Fiddle
You can only do arithmetic operations on cardinal numbers (integer, float, double, decimal, date). An enum datatype is nominal.
You could try casting it within SQL but if this operation should be possible the issue is that you have chosen the wrong data type.
That you are exclusively using digits to represent states should have been a red flag.
But it does not work on enum data.
Of course. The string values of ENUM datatype are not the values stored in table data body - it contains 2-byte index of the value only, and according string values list is stored in table definition area which is not available for changing by any DML operation. Only DDL (ALTER TABLE) may change ENUM value string representation definition.
The posted query will treate data.number as index value - so you must get the error (source values are 1-10, final values will be negative whereas ENUM index values are UNSIGNED).
DB Fiddle
CREATE TABLE Days (
id int primary key,
date_fix VARCHAR(255),
date_calculated VARCHAR(255) GENERATED ALWAYS AS (DATE_ADD("date_fix", INTERVAL 1 DAY))
);
INSERT Days
(id, date_fix, date_calculated
)
VALUES
("1", "2019-01-01", ""),
("2", "2019-01-06", ""),
("3", "2019-05-01", ""),
("4", "2019-08-15", ""),
("5", "2019-10-03", "");
In the above table I want to insert a column called date_calculated which calculates the date as following:
date_fix + 1 day
Therefore, I tried to combine GENERATED ALWAYS with DATE_ADD("date_fix", INTERVAL 1 DAY) but I could not make it work so far.
I assume the issue is related to the INSERT statement since I currently use only "" for the column date_calculated but I do not have any clue how to replace this "" in order to achieve that the column is calculated as described.
Do you have any idea how to get the desired table with the calculated column?
You should add DATE before DATE_ADD in your query
CREATE TABLE Days (
id int primary key,
date_fix VARCHAR(255),
date_calculated VARCHAR(255)
GENERATED ALWAYS AS (DATE(DATE_ADD(date_fix, INTERVAL 1 DAY)))
);
Then you can insert your data
INSERT INTO Days (id, date_fix)
VALUES ("1", "2019-01-01"),
("2", "2019-01-06"),
("3", "2019-05-01"),
("4", "2019-08-15"),
("5", "2019-10-03");
Your code works fine with a couple of fixes:
CREATE TABLE Days (
id int primary key,
date_fix VARCHAR(255),
date_calculated VARCHAR(255) GENERATED ALWAYS AS (DATE_ADD(date_fix, INTERVAL 1 DAY))
);
INSERT Days (id, date_fix)
VALUES (1, '2019-01-01'),
(2, '2019-01-06'),
(3, '2019-05-01'),
(4, '2019-08-15'),
(5, '2019-10-03');
Here is a db<>fiddle.
Your major issue is that you are quite confused by quotes. When writing SQL, it is pretty simple:
Use SINGLE quotes for date and string constants.
Never use quotes at all for numbers.
Do not use DOUBLE quotes (unless you really understand why you are using them). MySQL extends SQL so this is a synonym for single quotes.
The second issue is that there is no need to insert a value into a generated column. The value is calculated when you query the column, not when you insert values.
Then the third issue is types. Do not store dates as strings. MySQL has a wonderful data type to store dates, called date. It was invented for a reason and you should use it.
The approach you are taking to create the columns is not the right approach and not recommended.
The right approach for this logic would be, create the table with Id & date_fix only and then using select statement generate your calculated results.
Why to use this method?
As, You will be creating a new column unnecessarily to store the information which you can get from select statement will not be scalable enough to work in a large enterprise environment. So always try to minimize the load.
Please find the below code:
CREATE TABLE Days (
id int primary key,
date_fix VARCHAR(255)
);
INSERT Days
(id, date_fix
)
VALUES
('1', '2019-01-01'),
('2', '2019-01-06'),
('3', '2019-05-01'),
('4', '2019-08-15'),
('5', '2019-10-03');
select id,date_fix, DATEADD(DAY,1,CAST(date_fix as DATETIME)) from Days;
I have an MySQL table that I would like sorted in a particular order when displayed on a Web page using PHP. In order to accomplish this I have a "displayorder" column with unique values. Rows with lower values in this column are displayed first.
The problem with this is that inserting new rows manually is difficult, because I have to adjust the "displayorder" for many many rows in order for the new row to fit. Is there a better way to do this that I don't know about?
The desired order may look like this:
Action
Adventure
MMO
Roleplaying
Simulation
Strategy
Software
Reference
One fairly simple way to do this is to create the sort column as decimal rather than an integer. This way, you start your table with values such as 1, 2, 3 etc', and when you want to insert a record between 1 and 2 you simply insert it as 1.5.
For a table that updates frequently, you want to choose a large number of decimal digits (the maximum, decimal(65,30) would probably be too big for this, but it's a possible choice).
You can create a procedure that will perform the insert and the shifting of order and use it instead of writting INSERT INTO ... in php
In example :
DROP TABLE IF EXISTS test;
CREATE TABLE test
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
orderPlace INT NOT NULL,
datas VARCHAR(255) NOT NULL DEFAULT 'foo'
);
INSERT INTO test (orderPlace) VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10);
DROP PROCEDURE IF EXISTS InsertNewRowAndShiftOrder;
DELIMITER $
CREATE PROCEDURE InsertNewRowAndShiftOrder(IN OrderValue INT,
IN SomeData VARCHAR(255))
BEGIN
INSERT INTO test VALUES (DEFAULT, OrderValue, SomeData);
SET #LastId = LAST_INSERT_ID();
UPDATE test SET orderPlace = orderPlace + 1
WHERE orderPlace >= OrderValue
AND id <> #LastId;
END$
DELIMITER ;
CALL InsertNewRowAndShiftOrder(5, 'bar');
SELECT * FROM test ORDER BY orderPlace;
This outputs :
id orderPlace datas
1 1 foo
2 2 foo
3 3 foo
4 4 foo
11 5 bar
5 6 foo
6 7 foo
7 8 foo
8 9 foo
9 10 foo
10 11 foo
Note : I can't provide a DB-fiddle link, because this doesn't work, for some reasons on that website. I tested on my localhost MySQL DB and it worked
I am having trouble, while inserting data from a select statement having scalar function call. I posted sample script below, and little explanation and question in the comments.
---target table (in my case, this is not table variable, but a regular table, here for simplicity to posted this code as table variable to get the idea)
declare #tblItems table (
Id int identity(1,1)
,ItemID int
,TranNo varchar(20)
,Qty decimal(18,3)
,SomeCalculatedValue decimal(18,3)
)
--a dummay temp table, works like a source table
declare #tblTemp table (
Id int identity(1,1)
,ItemID int
,TranNo varchar(20)
,Qty decimal(18,3)
,SomeCalculatedValue decimal(18,3)
)
--put some dummy data in target table
insert into #tblItems(ItemID, TranNo, Qty, SomeCalculatedValue)
values
(1, 'GRN-001', 10, 0),
(2, 'GRN-002', 20, 0),
(3, 'GRN-003', 15, 0),
(4, 'GRN-004', 32, 0),
(5, 'GRN-005', 18, 0)
;
--insert 3 new rows in temp table, which later I want to insert in target table
insert into #tblTemp(ItemID, TranNo, Qty, SomeCalculatedValue)
values
(1, 'GRN-006', 6, 0), -- this line is working work fine,
(1, 'GRN-007', 3, 0), -- but this line is having problem, because it is not considering the last line( with TranNo='GRN-006' )
(2, 'GRN-008', 8, 0)
--here is the actual work, I need to read data from temp table to target table
--and the key requirement is the column 'SomeCalculatedValue'
--it should call a scalar function, and within that function I have to perform some calculations based on same target table
--for each ItemID passed, that scalar function will works as: it performs some sort of calculations on existing rows
--for that particular ItemID, to simplify the understanding you can think of it as Running-Total(not actually running total, but concept
--is same that each row value will based on previous row value)
insert into #tblItems(ItemID, TranNo, Qty, SomeCalculatedValue)
select
ItemID
,TranNo
,Qty
,[dbo].[GetCalculatedValue] (ItemID, Qty) as SomeCalculatedValue -- this function will perform some calcualations
from #tblTemp
select * from #tblItems
I have two tables, #tblItems and #tblTemp. I have to insert rows from #tblTemp to #tblItems, but in the select clause of #tblTemp, I used a scalar function, lets say, GetCalculatedValue(ItemID, Qty), which performs some calculations for specific ItemID from target table, and for each row it calculates a value which should be inserting in the #tblItems. It is not really Running-Total but for the sake for understanding it can think of as running total, because each row value will depend upon last previous lines.
So problem is that when #tblTemp has more than 1 row for a particular ItemID, it should consider the rows already inserted, but I think this insert-into-select statement will insert all rows at once, so it is not considering the last lines for particular ItemID which are in same select statement. You can review the code, I posted some comments also for explanation.
How can I make a table that has rows with order to one another and can be rearranged:
Example:
Rows:idappearance name
Records
(1,"john"),(2,"mike")
Now, I want to insert "Avi" between them:
not having to worry about rearranging them
(1,"john"),(2,"Avi"),(3,"mike")
This table can have a foreign key in
another table (like departments..).
idappearance is the order of appearance I want
to set, doesn't need to be PK.
It needs to handle about 50K of names so O(n) isn't best solution.
Simple solution would be having reasonable numerical gaps between records. In other words;
(10000,"John"),(20000,"mike")
(10000,"John"),(15000,"Avi"),(20000,"mike")
(10000,"John"),(12500,"tom"),(15000,"avi"),(20000,"mike")
etc..
Gap between records should be determined based on your data domain
You could have a trigger on inserts. I don't use MySQL, but here's the code for sql-server...
Basically, on an insert, the trigger increments the appearanceId of all rows with appearanceId which are equal to or greater than the new appearance id.
CREATE Table OrderedTable
(
id int IDENTITY,
name varchar(50),
appearanceOrder int
)
GO
CREATE TRIGGER dbo.MyTrigger
ON dbo.OrderedTable
AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
UPDATE OrderedTable SET
AppearanceOrder = AppearanceOrder + 1
WHERE AppearanceOrder >= (
SELECT TOP 1 AppearanceOrder
FROM inserted )
AND id NOT IN (
SELECT id
FROM inserted )
END
GO
INSERT INTO OrderedTable VALUES ('Alice', 1)
INSERT INTO OrderedTable VALUES ('Bob', 1)
INSERT INTO OrderedTable VALUES ('Charlie', 1)
INSERT INTO OrderedTable VALUES ('David', 1)
This returns David, Charlie, Bob, Alice as expected.
SELECT *
FROM OrderedTable
ORDER BY AppearanceOrder
Note that I haven't fully tested this implementation. One issue is that it will leave holes in the AppearanceOrder if items are deleted, or the inserts deliberately insert outside the current range. If these matter, they are left as an exercise to the reader ;-)
If appearance order were a double-precision floating point number, you could insert any name between any two adjacent names with a single insert. If you start with a table like this:
create table test (
person_id integer primary key,
person_name varchar(10) not null,
appearance_order double precision not null unique
);
insert into test values (100, 'John', 11);
insert into test values (23, 'Mike', 12);
Insert Avi between them by simply
insert into test values (3, 'Avi', 11.5);
Sort by the column 'appearance_order'.
select * from test order by appearance_order
100 John 11
3 Avi 11.5
23 Mike 12
Insert Eva between John and Avi by
insert into test values (31, 'Eva', 11.25);
select * from test order by appearance_order
100 John 11
31 Eva 11.25
3 Avi 11.5
23 Mike 12
You do need to separate identification from sort order. That means using one column for the id number (and as the target for foreign key references) and another for the appearance order. But, depending on your application, you might not need a unique constraint on appearance_order.