I have a user-settings setup with a so called 'property bag' I guess.
I want to store settings for my users. Most users won't change the default setting, so I thought I should make a 'default value' for each setting. This way I don't have store a user_setting record for each setting for each user.
This is my (simplified) mysql database:
CREATE TABLE `user` (
`user_id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
PRIMARY KEY (`user_id`)
);
CREATE TABLE `setting` (
`setting_id` INT NOT NULL AUTO_INCREMENT,
`key` VARCHAR(100) NOT NULL,
`default_value` VARCHAR(100) NOT NULL,
PRIMARY KEY (`setting_id`)
);
CREATE TABLE `user_setting` (
`user_setting_id` INT NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`setting_id` INT NOT NULL,
`value` VARCHAR(100) NOT NULL,
PRIMARY KEY (`user_setting_id`),
CONSTRAINT `fk_user_setting_1`
FOREIGN KEY (`user_id`)
REFERENCES `user` (`user_id`)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `fk_user_setting_2`
FOREIGN KEY (`setting_id`)
REFERENCES `setting` (`setting_id`)
ON DELETE RESTRICT
ON UPDATE CASCADE
);
INSERT INTO `user` VALUES (1, 'username1'),(2, 'username2');
INSERT INTO `setting` VALUES (1, 'key1', 'somevalue'),(2, 'key2', 'someothervalue');
In my code I can easy do a lookup for each setting for each user. By checking if there is a row in the user_setting table, I know that this is other then the default value.
But is there a way to get an overview for all the settings for each user? Normaly I would left-join the user -> user_setting -> setting tables for each user, but now I don't have a user_setting record for each user/setting. Is this possible with a single query?
If you just had a cartesian join of user against setting, you'll get one row for every user/setting combination. Then simply left join the user_setting table and you can pick up the overridden value when it exists.
So something like this:
SELECT u.user_id, s.key, s.default_value, us.value
FROM user u, setting s
LEFT JOIN user_setting us
ON(us.user_id=u.user_id AND us.setting_id=s.setting_id)
ORDER BY u.user_id, s.key
You could refine this further using IFNULL so that you get the value of the setting regardless of whether it's overridden or default:
SELECT u.user_id, s.key, IFNULL(us.value , s.default_value) AS value
FROM user u, setting s
LEFT JOIN user_setting us
ON(us.user_id=u.user_id AND us.setting_id=s.setting_id)
ORDER BY u.user_id, s.key
(Answering my own question isn't the way I normaly work, but I'm not sure if this is the correct answer and it's based on Paul Dixon's answer)
As mentioned, a cartesian join is needed between user and setting. The correct query would be:
SELECT u.user_id, s.key, IFNULL(us.value , s.default_value) AS value
FROM user u
CROSS JOIN setting s
LEFT JOIN user_setting us ON
(us.user_id=u.user_id AND us.setting_id=s.setting_id)
ORDER BY u.user_id, s.key;
Related
I want to get data just from only one specific user but I get data from both users. Why is that? I don't understand. How can I solve this?.
I have three tables:
/*User*/
CREATE TABLE `User` (
`IDUser` INT NOT NULL AUTO_INCREMENT,
`Name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`IDUser`)
);
/*Category*/
CREATE TABLE `Category` (
`IDCategory` CHAR(3) NOT NULL,
`FK_User` INT NOT NULL,
`CategoryName` VARCHAR(40) NOT NULL,
PRIMARY KEY (`IDCategory`, `FK_User`)
);
/*Product*/
CREATE TABLE `Product` (
`IDProduct` VARCHAR(18) NOT NULL,
`FK_User` INT NOT NULL,
`ProductName` VARCHAR(150) NOT NULL,
`FK_Category` CHAR(3) NOT NULL,
PRIMARY KEY (`IDProduct`, `FK_User`)
);
ALTER TABLE `Product` ADD FOREIGN KEY (`FK_User`) REFERENCES `User`(`IDUser`);
ALTER TABLE `Product` ADD FOREIGN KEY (`FK_Category`) REFERENCES `Category`(`IDCategory`);
ALTER TABLE `Category` ADD FOREIGN KEY (`FK_User`) REFERENCES `User`(`IDUser`);
insert into User(Name) values('User1');
insert into User(Name) values('User2');
insert into Category(IDCategory,FK_User,CategoryName) values('CT1',1,'Category1User1');
insert into Category(IDCategory,FK_User,CategoryName) values('CT1',2,'Category1User2');
If two different users insert both the same product with the same ID:
insert into Product values('001',1,'shoe','CT1');
insert into Product values('001',2,'shoe','CT1');
Why do I keep getting data from both users if I try a query like this one:
SELECT P.IDProduct,P.ProductName,P.FK_Category,C.CategoryName
FROM Product P inner join Category C on P.FK_Category=C.IDCategory
WHERE P.FK_User=1
this is the result I get:
You are getting two rows because both categories have the same IDCategory value which is the value you are JOINing on. You need to also JOIN on the FK_User values so that you don't also get User2's category values:
SELECT P.IDProduct,P.ProductName,P.FK_Category,C.CategoryName
FROM Product P
INNER JOIN Category C ON P.FK_Category=C.IDCategory AND P.FK_User = C.FK_User
WHERE P.FK_User=1
You need to add p.FK_User=C.Fk_User this condition in your join clause
SELECT P.IDProduct,P.ProductName,P.FK_Category,C.CategoryName
FROM Product P inner join Category C
on P.FK_Category=C.IDCategory and p.FK_User=C.Fk_User
WHERE P.FK_User=1
A PRIMARY KEY is a UNIQUE key. Shouldn't CategoryID be unique? That is, shouldn't Category have PRIMARY KEY(CategoryId)?
(Check other tables for a similar problem.)
I am running below mentioned query.
select c.id, c.code, c.name, count(a.iso_country) from countries c
left join airports a on c.code = a.iso_country group by a.iso_country
order by count(a.iso_country);
In my 'countries' table, I have 247 rows.
In 'airports' table, 'iso_country' column maps to 'code' column in 'countries' table.
Below are the table definitions.
Countries table -
CREATE TABLE `countries` (
`id` int(11) NOT NULL,
`code` varchar(2) NOT NULL,
`name` text,
`continent` text,
PRIMARY KEY (`id`),
UNIQUE KEY `code_UNIQUE` (`code`),
KEY `code_idx` (`code`)
)
Airports table -
CREATE TABLE `airports` (
`id` int(11) NOT NULL,
`type` text,
`name` text,
`continent` text,
`iso_country` varchar(2) DEFAULT NULL,
`iso_region` text,
PRIMARY KEY (`id`),
KEY `country_iso_code_fk_idx` (`iso_country`),
CONSTRAINT `country_fk` FOREIGN KEY (`iso_country`) REFERENCES `countries`
(`code`) )
The issue I'm facing is - the query I mentioned above returns 242 countries - 241 countries with airports and 1 with null values for 'airports', but doesn't include other 5 countries who also don't have any airports. Please guide me what am I doing wrong in this query.
PS:- I am just a novice in SQL.
I'm running on MySQL 5.7 Community Edition.
Thanks in advance.
You want a count of airports by country, including those where there are none, right?
Try this:
SELECT
c.id, c.code, c.name, count(a.iso_country) AS airport_count
FROM
countries c LEFT JOIN airports a ON c.code = a.iso_country
GROUP BY
c.id, c.code, c.name
ORDER BY
airport_count DESC;
Could it have something to do with NULL values? Your a.iso_country column is DEFAULT NULL and c.code is NOT NULL. Usually when comparing values, if either can be NULL, you want to use something like COALESCE to provide a second option in the case that the value is NULL. The reason for this being that NULL != '' and NULL != 0. What if you join on COALESCE(a.iso_country, '') = COALESCE(c.code, '')?
I'm also not sure how COUNT behaves when given a NULL. You may need to be careful there.
Can you try with checking this condition in the where clause .Implicitly checking null in table where airport are not found.
where (airports.name.is null)
I have a table describing changes that has been made to an end_customers table. When someone changes and end_customer we create a new row in the end_customers table and add a row to end_customer_history table where end_customer_parent_id is the ID of the old end_customer, and end_customer_child_id is the ID of the new end_customer.
End Customer Table:
CREATE TABLE `end_customers` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`reference_person` varchar(255) DEFAULT NULL,
`phone_number` varchar(255) NOT NULL,
`email` varchar(255) DEFAULT NULL,
`social_security_number` varchar(255) DEFAULT NULL,
`comment` longtext,
`token` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=101107 DEFAULT CHARSET=utf8;
End Customer History Table:
CREATE TABLE `end_customer_history` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`end_customer_parent_id` bigint(20) NOT NULL,
`end_customer_child_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
`date` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `FK_end_customer_parent` (`end_customer_parent_id`),
KEY `FK_end_customer_child` (`end_customer_child_id`),
KEY `FK_user` (`user_id`),
CONSTRAINT `end_customer_history_old_ibfk_1` FOREIGN KEY (`end_customer_parent_id`) REFERENCES `end_customers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `end_customer_history_old_ibfk_2` FOREIGN KEY (`end_customer_child_id`) REFERENCES `end_customers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `end_customer_history_old_ibfk_3` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=67 DEFAULT CHARSET=utf8;
We are now refactoring the schema so that changes made to end_customers table directly edits the row instead of creating a new row, and puts a copy of the old data in a end_customer_history_new table that has the same schema as end_customers.
I need to migrate all old data to this new table.
So for each end_customer I have, I need to check if it has an entry in end_customer_history as a end_customer_child_id (it has been modified), and then check if that entrys parent is also present in end_customer_history as a child, and then check if that entrys parent is also present in end_customer_history as a child, and so forth until there are no more rows.
How do I do this in one migration SQL script?
Unlike Oracle, MySQL does not provide the functionality to recursively query parent-child hierarchy. You can write a self join query (or inner queries) if you know the level beforehand. If not, you have to perform a single query and handle resursiveness in the application or stored procedure.
Here is an example of querying a parent child hierarchy if you know the level already, and here is an example of how to do it in Oracle (for your reference).
In the solution I came up with, it doesn't use any stored functions to loop over rows, because I could never get it to work. It takes the latest version of an end customer, and then joins similar rows using a token that we know is always the same and unique.
-- Move data from old end customer history to the new schema
-- May remove some redundant history, this will be ok
-- If an end customer has been changed more than once, all changes will have the same date (latest change) and the same user responsible for change
-- This is also ok
-- Insert the latest version of changed end_customers into new history table
INSERT INTO end_customer_history (name, reference_person, phone_number, email, social_security_number, comment, end_customer_id, user_id, date)
SELECT
ec.name, ec.reference_person, ec.phone_number, ec.email, ec.social_security_number, ec.comment, ech.end_customer_child_id, ech.user_id, ech.date
FROM end_customer_history_old AS ech
JOIN end_customers AS ec ON ec.id = ech.end_customer_parent_id
WHERE ech.end_customer_child_id IN (SELECT end_customer_id FROM orders);
-- Remove all the old data
DROP TABLE end_customer_history_old;
-- Insert all versions of an end_customer based on the latest history, joined on token (always the same)
INSERT INTO end_customer_history (name, reference_person, phone_number, email, social_security_number, comment, end_customer_id, user_id, date)
SELECT
parent.name, parent.reference_person, parent.phone_number, parent.email, parent.social_security_number, parent.comment, ech.end_customer_id, ech.user_id, ech.date
FROM end_customer_history AS ech
JOIN end_customers AS ec ON ec.id = ech.end_customer_id
JOIN end_customers AS parent ON parent.token = ec.token AND parent.id <> ec.id;
-- This unfortunately creates duplicates of some rows, so it looks like changes have been made multiple times (with no new data)
-- Create a temporary table that copies over unique rows from end_customer_history
CREATE TABLE temp AS
SELECT * FROM end_customer_history
GROUP BY
name, reference_person, phone_number, email, social_security_number, comment, end_customer_id, user_id, date;
-- Clear the end_customer_history table all together
TRUNCATE TABLE end_customer_history;
-- Copy over filtered unique history rows
INSERT INTO end_customer_history (name, reference_person, phone_number, email, social_security_number, comment, end_customer_id, user_id, date)
SELECT
name, reference_person, phone_number, email, social_security_number, comment, end_customer_id, user_id, date
FROM temp;
-- Remove temporary table
DROP TABLE temp;
I have three tables {animal, food, animal_food}
DROP TABLE IF EXISTS `tbl_animal`;
CREATE TABLE `tbl_animal` (
id_animal INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(25) NOT NULL DEFAULT "no name",
sex CHAR(1) NOT NULL DEFAULT "M",
size VARCHAR(10) NOT NULL DEFAULT "Mini",
age VARCHAR(10) NOT NULL DEFAULT "born",
hair VARCHAR(5 ) NOT NULL DEFAULT "short",
color VARCHAR(25) NOT NULL DEFAULT "not defined",
FOREIGN KEY (sex) REFERENCES `tbl_sexes` (sex),
FOREIGN KEY (tamanio) REFERENCES `tbl_sizes` (size),
FOREIGN KEY (age) REFERENCES `tbl_ages` (age),
FOREIGN KEY (hair) REFERENCES `tbl_hair_length` (hair_length),
CONSTRAINT `uc_Info_Animal` UNIQUE (`id_animal`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `tbl_food`;
CREATE TABLE `tbl_food` (
id_food INTEGER NOT NULL PRIMARY KEY,
type_food VARCHAR(20) NOT NULL DEFAULT "Other",
label VARCHAR(50) NOT NULL,
CONSTRAINT `uc_Info_Food` UNIQUE (`id_food`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `animal_food`;
CREATE TABLE `animal_food` (
id_animal INTEGER NOT NULL,
food VARCHAR(50) NOT NULL DEFAULT "",
quantity VARCHAR(50) NOT NULL DEFAULT "",
times VARCHAR(50) NOT NULL DEFAULT "",
description VARCHAR(50) NOT NULL DEFAULT "",
date_last DATE DEFAULT '0000-00-00 00:00:00',
date_water DATE DEFAULT '0000-00-00 00:00:00',
CONSTRAINT fk_ID_Animal_Food FOREIGN KEY (id_animal) REFERENCES `tbl_animal`(id_animal)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
And I have a view where I select the values columns in animal and animal_food depending on ID
CREATE VIEW `CAT_animal_food` AS
SELECT a.name, a.sex,a.size,a.age,a.hair,a.color,
a_f.*
FROM `tbl_animal` a, `animal_food` a_f
WHERE a.id_animal = a_f.id_animal;
What would be better to create a view like above or, to join these animal and animal_food tables?
SELECT ...
FROM A.table t1
JOIN B.table2 t2 ON t2.column = t1.col
What is really the diference between that kind of view and a left join for example?
The only difference between the two SELECT statements is the syntax style. Both perform INNER JOINS. In other words, this style uses what is called "implicit" syntax:
SELECT ...
FROM A.table t1, B.table2 t2
WHERE t2.column = t1.col
It is "implicit" because the join condition is implied by the WHERE clause. This version uses "explicit" syntax:
SELECT ...
FROM A.table t1
JOIN B.table2 t2 ON t2.column = t1.col
Most people prefer to see "explicit" syntax because it make your code easier to follow; the join condition is explicitly understood and any WHERE clause is obvious.
None of this is related to LEFT JOINS of course. Here is a famous link with a great visual description of join types.
There is a big difference.
The old school style using a list of tables only allows inner joins.
Further, placing non-key conditions in the ON clause of a proper join allows greater performance and capability that a where clause can not deliver. The primary reason for this is that the ON clause is evaluated as the join is being made, but the WHERE clause is evaluated after all joins have been made.
The subject is too complex to do justice here.
I have 2 tables, items and members :
CREATE TABLE IF NOT EXISTS `items` (
`id` int(5) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`member` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTS `members` (
`id` int(5) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
What if, for example I have a record inside items, such as
INSERT INTO `test`.`items` (
`id` ,
`name` ,
`member`
)
VALUES (
NULL , 'xxxx', '1, 2, 3'
);
in members :
INSERT INTO `members` (`id`, `name`) VALUES
(1, 'asdf'),
(2, 'qwert'),
(3, 'uiop'),
(4, 'jkl;');
and I'd like to display items.member data with members.name, something like 1#asdf, 2#qwert, 3#uiop??
I've tried the following query,
SELECT items.id, items.name, GROUP_CONCAT(CONCAT_WS('#', members.id, members.name) ) as member
FROM `items`
LEFT JOIN members AS members on (members.id = items.member)
WHERE items.id = 1
But the result is not like I expected. Is there any other way to display the data via one call query? Because I'm using PHP, right now, i'm explode items.member and loop it one by one, to display the members.name.
You could look into using FIND_IN_SET() in your join criteria:
FROM items JOIN members ON FIND_IN_SET(members.id, items.member)
However, note from the definition of FIND_IN_SET():
A string list is a string composed of substrings separated by “,” characters.
Therefore the items.member column should not contain any spaces (I suppose you could use FIND_IN_SET(members.id, REPLACE(items.member, ' ', '')) - but this is going to be extremely costly as your database grows).
Really, you should normalise your schema:
CREATE TABLE memberItems (
item_id INT(5) NOT NULL,
member_id INT(5) NOT NULL,
FOREIGN KEY item_id REFERENCES items (id),
FOREIGN KEY member_id REFERENCES members (id)
);
INSERT INTO memberItems
(item_id, member_id)
SELECT items.id, members.id
FROM items
JOIN members ON FIND_IN_SET(members.id, REPLACE(items.member,' ',''))
;
ALTER TABLE items DROP member;
This is both index-friendly (and therefore can be queried very efficiently) and has the database enforce referential integrity.
Then you can do:
FROM items JOIN memberItems ON memberItems.item_id = items.id
JOIN members ON members.id = memberItems.member_id
Note also that it's generally unwise to use GROUP_CONCAT() to combine separate records into a string in this fashion: your application should instead be prepared to loop over the resultset to fetch each member.
Please take a look at this sample:
SQLFIDDLE
Your query seems to work for what you have mentioned in the question... :)
SELECT I.ID, I.ITEM,
GROUP_CONCAT(CONCAT("#",M.ID,
M.NAME, " ")) AS MEMB
FROM ITEMS AS I
LEFT JOIN MEMBERS AS M
ON M.ID = I.MID
WHERE i.id = 1
;
EDITTED ANSWER
This query will not work for you¬ as your schema doesn't seem to have any integrity... or proper references. Plus your memeber IDs are delimtted by a comma, which has been neglected in this answer.