Display all values using a CSV column as condition in MySQL - mysql

I have a query that supposed to display a page's name where a particular user has accessed to. The users table has a column that has a CSV of page ids that the user has accessed to.
The page's names is located in another table with corresponding page ids.
Here is the structure of the 2 tables
+------------+---------------------+
| User Table | |
+------------+---------------------+
| USERID | PAGE |
| john01 | 101,102, |
| chris5 | 101,001,003,004,005 |
+------------+---------------------+
+------------+------------------+
| PAGE TABLE | |
+------------+------------------+
| PAGEID | PAGENAME |
| 101 | Account settings |
| 102 | Details |
| 001 | Setup account |
| 002 | Profile |
| 003 | Reset Password |
| 004 | Edit user |
| 005 | Manage accounts |
+------------+------------------+
My problem is how can I display all page names accessible to a particular user like:
john01
My query is:
select pagename as Pages
from
Page
where
pageid in (select page from user where userid = 'john01')
This query only displays one pagename but not 2 pagenames as indicated on the table structure.
How can I retreive all pages for a particular user using this 2 table structure?
Thanks in advance.

This is not the best structure to go, but I believe you can manage it using this query:
( I would however strongly recommend to normalize the user page table to make a single page id per record reference.)
SELECT p.pagename FROM Pages p, User u WHERE u.userid = "john01" AND u.PAGE LIKE "%p.PAGEID%"
EDIT:
I started PhpMyAdmin to recreate the query, however since I believe it's not in my nature to write a query like this.
I would recommend you to normalize your table structure like this:
CREATE TABLE IF NOT EXISTSpage(
idint(10) unsigned NOT NULL AUTO_INCREMENT,
namevarchar(25) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ;
CREATE TABLE IF NOT EXISTSuser(
idint(10) unsigned NOT NULL AUTO_INCREMENT,
namevarchar(25) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE IF NOT EXISTSuserpages(
useridint(10) unsigned NOT NULL,
pageidint(10) unsigned NOT NULL,
PRIMARY KEY (userid,pageid)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Now the page table lists all available pages. the user table just list usernames to user id's. and the magic happens in the userpages table. where you make relevant references (rights?) between pages and users.
You can now query the pages using the following SQL query:
SELECT p.id, p.name FROM page p, user u, userpages up WHERE u.name = 'john01' and u.id = up.userid AND up.pageid = p.id

Related

Can this SQL query be optimized?

This is a query for a Postfix table lookup (smtpd_sender_login_maps) in MariaDB (MySQL). Given an email address it returns the users allowed to use that address. I am using two SQL tables to store accounts and aliases that need to be searched. Postfix requires a single query to return a single result set hence the UNION SELECT. I know there is unionmap:{} in postfix but i do not want to go that route and prefer the union select. The emails.email column is the username that is returned for Postfix SASL authentication. The %s in the query is where Postfix inserts the email address to search for. The reason for matching everything back to the emails.postfixPath is because that is the physical inbox, if two accounts share the same inbox they should both have access to use all the same emails including aliases.
Table: emails
+-------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| email | varchar(100) | NO | PRI | NULL | |
| postfixPath | varchar(100) | NO | MUL | NULL | |
| password | varchar(50) | YES | | NULL | |
| acceptMail | tinyint(1) | NO | | 1 | |
| allowLogin | tinyint(1) | NO | | 1 | |
| mgrLogin | tinyint(1) | NO | | 0 | |
+-------------+--------------+------+-----+---------+-------+
.
Table: aliases
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| email | varchar(100) | NO | PRI | NULL | |
| forwardTo | varchar(100) | NO | | NULL | |
| acceptMail | tinyint(1) | NO | | 1 | |
+------------+--------------+------+-----+---------+-------+
.
SELECT email
FROM emails
WHERE postfixPath=(
SELECT postfixPath
FROM emails
WHERE email='%s'
AND acceptMail=1
LIMIT 1)
AND password IS NOT NULL
AND allowLogin=1
UNION SELECT email
FROM emails
WHERE postfixPath=(
SELECT postfixPath
FROM emails
WHERE email=(
SELECT forwardTo
FROM aliases
WHERE email='%s'
AND acceptMail=1)
LIMIT 1)
AND password IS NOT NULL
AND allowLogin=1
AND acceptMail=1
This query works, it just looks heavy to me and i feel like it should be more streamlined / efficient. Does anyone have a better way to write this or is this as good as it gets?
I added CREATE INDEX index_postfixPath ON emails (postfixPath) per #The Impaler's suggestion.
#Rick James here is the additional table info:
Table: emails
Create Table: CREATE TABLE `emails` (
`email` varchar(100) NOT NULL,
`postfixPath` varchar(100) NOT NULL,
`password` varchar(50) DEFAULT NULL,
`acceptMail` tinyint(1) NOT NULL DEFAULT 1,
`allowLogin` tinyint(1) NOT NULL DEFAULT 1,
`mgrLogin` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`email`),
KEY `index_postfixPath` (`postfixPath`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
Table: aliases
Create Table: CREATE TABLE `aliases` (
`email` varchar(100) NOT NULL,
`forwardTo` varchar(100) NOT NULL,
`acceptMail` tinyint(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
.
Part 1:
SELECT email
FROM emails
WHERE postfixPath=
(
SELECT postfixPath
FROM emails
WHERE email='%s'
AND acceptMail = 1
LIMIT 1
)
AND password IS NOT NULL
AND allowLogin = 1
With indexes:
emails: (email, acceptMail, password)
I assume acceptMail has only 2 values? The Optimizer cannot know that, so it sees AND acceptMail as a range test. AND acceptMail = 1 fixes that. (No, > 0, != 0, etc, can't be optimized.)
Part 2:
This has 3 layers, and is probably where the inefficiency is.
SELECT e.email
FROM ( SELECT forwardTo ... ) AS c
JOIN ( SELECT postfixPath ... ) AS d ON ...
JOIN emails AS e ON e.postfixPath = d.postfixPath
This is how the Optimizer might optimize your version. But I am not sure it did, so I changed it to encourage it to do so.
Again, use =1 when testing for "true". Then have these indexes:
aliases: (email, acceptMail, forwardTo)
emails: (email, postfixPath)
emails: (postfixPath, allowLogin, acceptMail, password, email)
Finally, the UNION:
( SELECT ... part 1 ... )
UNION ALL
( SELECT ... part 2 ... )
I added parentheses to avoid ambiguities about what clauses belong to the Selects versus to the Union.
UNION ALL is faster than UNION (which is UNION DISTINCT), but you might get the same email twice. However, that may be nonsense -- forwarding an email to yourself??
The order of columns in each index is important. (However, some variants are equivalent.)
I think all the indexes I provided are "covering", thereby giving an extra performance boost.
Please use SHOW CREATE TABLE; it is more descriptive than DESCRIBE. "MUL" is especially ambiguous.
(Caveat: I threw this code together rather hastily; it may not be quite correct, but principles should help.)
For further optimization, please do like I did in splitting it into 3 steps. Check the performance of each.
The following three indexes will make the query faster:
create index ix1 on emails (allowLogin, postfixPath, acceptMail, password, email);
create index ix2 on emails (email, acceptMail);
create index ix3 on aliases (email, acceptMail);

MySQL query: get rows of many to many relationship of items that are used together often efficiently

I have a table tag_thread that associates tags with threads, just like here on Stackoverflow where one thread can have multiple tags and one tag can be used on multiple threads.
Now I would like to give a tag_id as an input to get tags that are often used together with the given tag (so to get relevant tags):
Example table tag_thread:
| tag_id | thread_id |
|:-----------|------------:|
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 1 | 2 |
| 21 | 2 |
| 3 | 2 |
Expected output for the query:
getRelevantTagIdsForThreadId(1): [3,2,21]
getRelevantTagIdsForThreadId(2): [1,3]
getRelevantTagIdsForThreadId(3): [1,2,21]
So the query should search for the given tag_id, then take the associated thread_ids, collect the tag_ids of the thread_id`s received in the step before and order them by how often the tag_id was found.
I already have a working query, however, it is not efficient at all and thus doesn't work properly for larger tables:
select `t2`.`tag_id`
from `tag_thread` as `t1`
inner join `tag_thread` as `t2`
on `t1`.`thread_id` = `t2`.`thread_id`
and t1.tag_id = :tagId
where t2.tag_id <> :tagId2
group by `t2`.`tag_id`
order by count(t2.tag_id) desc
Any idea for an efficient solution? I would be okay with limiting the number of tags that are looked at in the first place, too.
result of SHOW CREATE TABLE tag_thread:
CREATE TABLE `tag_thread` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`tag_id` int(11) NOT NULL,
`thread_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38496 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

Mysql query optiomization (to avoid using UNION ALL)

Does exist any way how to optimize given query? I would like to get always all result from the user table and + also result form the picture table (if related exists). It is possible without using UNION ALL?
Lets consider following example
+----+--------+
| id | name |
+----+--------+
| 1 | Drosos |
| 2 | Jack |
+----+--------+
+----+---------+--------------+
| id | user_id | picture_name |
+----+---------+--------------+
| 1 | 1 | avatar.jpg |
| 2 | 1 | avatar2.jpg |
+----+---------+--------------+
Expected result
+--------+--------------+
| name | picture_name |
+--------+--------------+
| Drosos | avatar.jpg |
| Drosos | avatar2.jpg |
| Drosos | NULL |
| Jack | NULL |
+--------+--------------+
4 rows in set (0.00 sec)
User
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;
Picture table
CREATE TABLE `picture` (
`id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`picture_name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Query
SELECT u.name, p.picture_name FROM user u
INNER JOIN picture p ON p.user_id = u.id
UNION ALL
SELECT u.name, NULL FROM user u;
http://sqlfiddle.com/#!9/46d18a/1
Here's a method to get what you're after, but it's really only to illustrate theat UNION ALL is probably your best solution anyway. This is SQL Server syntax which should be pretty close to MySQL
SELECT u.name, p.picture_name
FROM user u
CROSS JOIN
(SELECT 1 as C1 UNION ALL SELECT 2) As CJ
LEFT JOIN picture p ON p.user_id = u.id AND CJ.C1 = 1
This duplicates the user table with a cross join then attaches pictures to just one of the copies
If you didn't need that extra Drosso | NULL then a simple left join would be fine
The best optimization which I have achieved is with using "materialized view" and applied needed indexes. The query which used to take ~0.4000 sec now takes ~0.0025 sec.
Materialized views are not supported by MySQL so I had to create table table and trigger manually (which is not great but in my case was worth to do).

MySql enter duplicate rows into table

I would like to enter two of the same emails as two different rows into MySQL table Person. However, it keeps only one row. How do I modify my code? Thanks.
create table if not exists Person (
Id int auto_increment primary key,
Email varchar(20)
);
insert into Person(Email)
values ('abc#efg.com'),
('abc#efg.com')
;
Your query looks ok
SQL fiddle demo
OUTPUT
| Id | Email |
|----|-------------|
| 1 | abc#efg.com |
| 2 | abc#efg.com |

Insert Into.. Select.. On Duplicate Key Update

I'm trying to test how the insert into duplicate key works. And currently this is what I did:
INSERT INTO
user_test (userName, first, last)
SELECT
u.userName, u.first, u.last
FROM otherdatabase.user as u
ORDER BY u.userName ASC
ON DUPLICATE KEY UPDATE
userName = u.userName
I executed this query and it worked and inserted rows. Then what I did was I modified one row from the user table and then tried to run this query again, but instead of just updating that one row it inserted all the rows again.
From what I understand shouldn't it just update that one row I modified from the user table?
What I'm trying to do is do a "Insert if it doesn't exist and update if it exist" query and found that using insert into .. on duplicate key can do that but I'm obviously doing it wrong...
CREATE TABLE user_test (
id bigint(20) NOT NULL AUTO_INCREMENT,
userName varchar(20) DEFAULT NULL,
first varchar(20) DEFAULT NULL,
last varchar(20) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=latin1
Per Barranka's suggestion I added a unique index to the user_name column
Alter table user_test add unique index idx_userName(userName)
Then I ran the query again and it didn't add any more rows since its already existing..
This is an example of what the user_table looks like now, its now the same on the users table.
user_table:
userName | first | last |
ckeith | Carl | Keith |
mmonroe | Mickey | Monroe |
Then what I did to test it again is from the user table I modified one of the rows
user:
userName | first | last |
ckeithh | Carl | Keith |
mmonroe | Mickey | Monroe |
and executed the query again, this is now what the users_table looks like:
user_table:
userName | first | last |
ckeith | Carl | Keith |
mmonroe | Mickey | Monroe |
ckeithh | Carl | Keith |
I thought it would just update the first row to ckeithh but it just inserted one row?
My expected output was:
user_table:
userName | first | last |
ckeithh | Carl | Keith |
mmonroe | Mickey | Monroe |
Update:
I added a unique index and made sure that it is unique. The inserting works but now the update is not working. Anything else i should try?
Still not able to get this to work, I have confirmed that the column i'm using is unique and that my version of mysql is v5 (i saw on one of the forums that for this to work mysql should be v5, not sure if thats real or not, but still i checked and im using v5.5.37)