How to tightly contain an SQL query result - sql

I'm writing an application that implements a message system through a 'memos' table in a database. The table has several fields that look like this:
id, date_sent, subject, senderid, recipients,message, status
When someone sends a new memo, it will be entered into the memos table. A memo can be sent to multiple people at the same time and the recipients userid's will be inserted into the 'recipients' field as comma separated values.
It would seem that an SQL query like this would work to see if a specific userid is included in a memo:
SELECT * FROM memos WHERE recipients LIKE %15%
But I'm not sure this is the right solution. If I use the SQL statement above, won't that return everything that "contains" 15? For example, using the above statement, user 15, 1550, 1564, 2015, would all be included in the result set (and those users might not actually be on the recipient list).
What is the best way to resolve this so that ONLY the user 15 is pulled in if they are in the recipient field instead of everything containing a 15? Am I misunderstanding the LIKE statement?

I think you would be better off having your recipients as a child table of the memos table. So your memo's table has a memo ID which is referenced by the child table as
MemoRecipients
-----
MemoRecipientId INT PRIMARY KEY, IDENTITY,
MemoId INT FK to memos NOT NULL
UserId INT NOT NULL
for querying specific memos from a user you would do something like
SELECT *
FROM MEMOS m
INNER JOIN memoRecipients mr on m.Id = mr.memoId
WHERE userId = 15

No, you aren't misunderstood, that's how LIKE works.. But to achieve what you want, it would be better not to combine the recipients into 1 field. Instead try to create separate table that saves the recipient list for each memo..
For me I will use below schema, for your need:
Table_Memo
id, date_sent, subject, senderid, message, status
Table_Recipient
id_memo FK Table_Memo(id), recipient
By doing so, if you want to get specific recipients from a memo, you can do such query:
SELECT a.* FROM Table_Memo a, Table_Recipient b
WHERE a.id = "memo_id" AND a.id = b.id_memo AND b.recipient LIKE %15%

I am not sure how your application is exactly pulling these messages, but I imagine that better way would be creating a table message_recepient, which will represent many-to-many relationship between recipients and memos
id, memoId, recepientId
Then your application could pull messages like this
SELECT m.*
FROM memos m inner join message_recepient mr on m.id = mr.memoId
WHERE recepientId = 15
This way you will get messages for the specific user. Again, don't know what your status field is for but if this is for new/read/unread, you could add in your where
and m.status = 'new'
Order by date_set desc
This way you could just accumulate messages, those that are new

Related

Migrate data from one table into multiple tables

I have one table shoutbox_messages with user_id and text.
Now I added some new tables, chats, chat_users and chat_messages.
Now I want to insert the data from chat_messages into the new structure. I created an entry in chat, which needs to be referenced by the chat_users
INSERT INTO chat_users (user_id, chat_id)
SELECT DISTINCT shoutbox_messages.user_id,
(SELECT chats.id FROM chats WHERE chats.shoutbox = true)
FROM shoutbox_messages"
So far so good. Now I want to add all messages from shoutbox_messages to chat_messages
INSERT INTO chat_messages (chat_user_id, text, text_as_html)
SELECT chat_users.id,
(Select shoutbox_messages.text, shoutbox_messages.text_as_html
FROM shoutbox_messages Where shoutbox_messages.user_id =
(SELECT DISTINCT chat_users.id
WHERE chat_users.user_id = shoutbox_messages.user_id
)
)
from chat_users
This is not working, because shoutbox_messages have multiple texts from one user. What do I need to change, to make this work?
The problem you are having is that you are trying to insert multiple columns into one column. as well as not inserting data into the third column, which is presumably not what you want to do.
as seen here: (Select shoutbox_messages.text, shoutbox_messages.text_as_html
FROM shoutbox_messages Where shoutbox_messages.user_id = ... since your inserting data into just the text this query cannot work.
pgsql is interpreting the second query as data for just the text column
You may want to instead of doing the syntax you currently have, use sql join syntax as it would accomplish the same thing
something along the lines of
SELECT chat_users.id, shoutbox_messages.text, shoutbox_messages.text_as_html
FROM chat_users
join shoutbox_messages on shoutbox_messages.id = chat_users.id
just make sure you rewrite my code to the actual code you need to identify each column to the other

Check if inverse relationship exists in table

I'm using a table for signifying friends/friend requests. Basically my idea was to have a table structure like this:
CREATE TABLE friends(
user_id NUMERIC NOT NULL,
friend_user_id NUMERIC NOT NULL
UNIQUE (user_id,friend_user_id)
)
When a user wants to create a friend requests you add a row:
INSERT INTO friends(user_id,friend_user_id) VALUES($1,$2)
Then when the other user accepts the friend request you would simply add the inverse of the previous row (i.e. the recipient would technically be sending a friend request back to the sender thus completing the friend relationship):
INSERT INTO friends(user_id,friend_user_id) VALUES($2,$1)
My question:
If I want to get all the friends of a user I would have to get all the rows with that user's id and I want to inner join that with another table that contains the user's information, how would I check within the query for whether the rows inverse relationship exists?
P.S. I think I could do it pretty easily doing multiple queries but I would rather only have one query if possible.
Solution:
SELECT users.id,users.username,users.avatar FROM users
INNER JOIN friends ON users.id = friends.user_id
WHERE friends.user_id = $1 AND
EXISTS (SELECT 1 FROM friends WHERE friend_user_id = $1 AND user_id = friends.user_id)

How to make all rows in a table identical which the exception of 1 field?

I am trying to make it so all the users have the same items because I am doing an experiment with my app and need the experimental control of flattened data.
I used the following SQL statement in my last attempt:
insert into has (email,id,qty,price,item_info,image)
select 'b#localhost.com',id,qty,price,item_info,image
from
(
select * from has
where email != 'b#localhost.com'
) as o
where o.id not in
(
select id from has
where email = 'b#localhost.com'
);
This should add all items which 'b#localhost.com' does not already have but other users do have, to 'b#localhost.com's inventory. (the 'has' table)
However, I get the following error:
The statement was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index
I understand what this error means, but I do not understand why it is occurring. My statement inserts all records which that email doesn't already have, so there should not be any duplicate id/email pairs.
The database structure is shown below, circles are attributes, squares are tables, and diamonds are relationship sets. The HAS table is a relationship set on USER_ACCOUNT and ITEM, where the primary keys are email and id respectively.
Please try the following...
INSERT INTO has ( email,
id,
qty,
price,
item_info,
image )
SELECT a.email,
b.id,
a.qty,
a.price,
a.item_info,
a.image
FROM has a
CROSS JOIN has b
JOIN
(
SELECT email,
id
FROM has
) c ON a.email = c.email AND
b.id <> c.id;
The CROSS JOIN appends a copy of each row of has to each row of has. Please see http://www.w3resource.com/sql/joins/cross-join.php for more information on CROSS JOIN's.
Each row of the resulting dataset will have two id fields, two email fields, two qty fields, etc. By giving each instance of has an alias we create the fields a.id, b.id, a.email, etc.
We then compare each combination of "old" email address and "new" id to a list of existing email / id combinations and insert the old values with the new id replacing the old one into has
If you have any questions or comments, then please feel free to post a Comment accordingly.
Further Reading
http://www.w3resource.com/sql/joins/cross-join.php for more information on CROSS JOIN's
https://www.w3schools.com/sql/sql_in.asp for more information on WHERE's IN operator
https://www.w3schools.com/sql/sql_groupby.asp for more information on GROUP BY
I think the issue here is not that the code is trying to insert something which already exists, but that it's trying to insert more than one thing with the same PK. In lieu of an response to my comment, here is one way to get around the issue:
INSERT INTO has (email,id,qty,price,item_info,image)
SELECT
'b#localhost.com',
source.id,
source.qty,
source.price,
source.item_info,
source.image
FROM
(
SELECT email, id, qyt, price, item_info, image FROM has
) as source
JOIN
(
SELECT min(email) as min_email, id FROM has GROUP BY by id)
) as filter ON
filter.min_email = source.email
WHERE
source.id not in
(
SELECT id from has WHERE email = 'b#localhost.com'
);
The key difference from your original code is my extra join to the subquery I've aliased as filter. This limits you to inserting the has details from a single email per id. There are other ways to do the same, but I figured that this would be a safe bet for being supported by Derby.
I removed the WHERE clause from the source sub-query as that is handled by the final WHERE.

SQL Server Select data excluding Junction table

i need to select data that is not present in the junction table
so got three tables
trialsTable (trialID,TrialName)
VolunteerTAble(volunteerID, VolunteerName)
JunctionTAble(JunctionTableIs,TrialID,VolunteerName)
for every trial an email is sent to volunteers, but next time i want to exclude the volunteers which have received email for that trial and only send email to volunteers which have not received email for that trial, hence i created the junction table which have many to many relationship for the trial and volunteers
Assuming you have a variable or something for the #TrialId which you currently want something like:
SELECT *
FROM VolunteerTable v
WHERE NOT EXISTS(SELECT 1
FROM JunctionTable j
WHERE j.VolunteerName = v.VolunteerName
AND j.TrialID = #TrialId)
And you should probably put volunteerID in you juntion table instead of name.

Query to get all revisions of an object graph

I'm implementing an audit log on a database, so everything has a CreatedAt and a RemovedAt column. Now I want to be able to list all revisions of an object graph but the best way I can think of for this is to use unions. I need to get every unique CreatedAt and RemovedAt id.
If I'm getting a list of countries with provinces the union looks like this:
SELECT c.CreatedAt AS RevisionId from Countries as c where localId=#Country
UNION
SELECT p.CreatedAt AS RevisionId from Provinces as p
INNER JOIN Countries as c ON p.CountryId=c.LocalId AND c.LocalId = #Country
UNION
SELECT c.RemovedAt AS RevisionId from Countries as c where localId=#Country
UNION
SELECT p.RemovedAt AS RevisionId from Provinces as p
INNER JOIN Countries as c ON p.CountryId=c.LocalId AND c.LocalId = #Country
For more complicated queries this could get quite complicated and possibly perform very poorly so I wanted to see if anyone could think of a better approach. This is in MSSQL Server.
I need them all in a single list because this is being used in a from clause and the real data comes from joining on this.
You have most likely already implemented your solution, but to address a few issues; I would suggest considering Aleris's solution, or some derivative thereof.
In your tables, you have a "removed at" field -- well, if that field were active (populated), technically the data shouldn't be there -- or perhaps your implementation has it flagged for deletion, which will break the logging once it is removed.
What happens when you have multiple updates during a reporting period -- the previous log entries would be overwritten.
Having a separate log allows for archival of the log information and allows you to set a different log analysis cycle from your update/edit cycles.
Add whatever "linking" fields required to enable you to get back to your original source data OR make the descriptions sufficiently verbose.
The fields contained in your log are up to you but Aleris's solution is direct. I may create an action table and change the field type from varchar to int, as a link into the action table -- forcing the developers to some standardized actions.
Hope it helps.
An alternative would be to create an audit log that might look like this:
AuditLog table
EntityName varchar(2000),
Action varchar(255),
EntityId int,
OccuranceDate datetime
where EntityName is the name of the table (eg: Contries, Provinces), the Action is the audit action (eg: Created, Removed etc) and the EntityId is the primary key of the modified row in the original table.
The table would need to be kept synchronized on each action performed to the tables. There are a couple of ways to do this:
1) Make triggers on each table that will add rows to AuditTable
2) From your application add rows in AuditTable each time a change is made to the repectivetables
Using this solution is very simple to get a list of logs in audit.
If you need to get columns from original table is also possible using joins like this:
select *
from
Contries C
join AuditLog L on C.Id = L.EntityId and EntityName = 'Contries'
You could probably do it with a cross join and coalesce, but the union is probably still better from a performance standpoint. You can try testing each though.
SELECT
COALESCE(C.CreatedAt, P.CreatedAt)
FROM
dbo.Countries C
FULL OUTER JOIN dbo.Provinces P ON
1 = 0
WHERE
C.LocalID = #Country OR
P.LocalID = #Country