So I'm trying to build a simple forum. It'll be a list of topics in descending order by the date of either the topic (if no replies) or latest reply. Here's the DB structure:
forum_topic
id, name, email, body, date
forum_reply
id, email, body, date, topic_id
The forum itself will consist of an HTML table with the following headers:
Topic, Last Modified, # Replies
What would the query or queries look like to produce such a structure? I was thinking it would involve a cross join, but not sure... Thanks in advance.
Somewhat like this:
select * from forum_topic
inner join forum_reply on forum_topic.id=topc_id
However, don't use select *
That's bad practice :)
And I don't like the way you avoid normalization! Meaning I would rather have:
Users
UserID
Name
Email
Threads
ThreadID
Subject
Answered
AskedByUserID
Date
Replies
ReplyID
ThreadID
UserID
Answer
Date
Then selecting a Thread like this:
select ThreadID, Subject, Answered, AksedByUserID, Date from Threads
And selecting all replies like this
select Answer, Date, Name, Email from Threads
inner join Replies on Threads,ThreaID=Replies.ThreadID
inner join Users on AskedByUserID=UserID
where Threads.ThreadID=xxx
Now this was just written from the top of my head, but you might need to add some group by as well.
First off, it seems to me noboody is actually answering your question, which was:
What would the query or queries look like to produce such a structure?
with a requested structure of
Topic, LastModified, # Replies.
The SQL to produce a result table with that structure, given the table structures you provided, would be:
SELECT t.Id, t.Name AS Topic,
MAX(r.Date) AS LastModified,
COUNT(*) AS NumReplies
FROM Forum_Topic t
LEFT OUTER JOIN Forum_Reply r ON t.id = r.topic_id
GROUP BY t.Id, t.Name
(sorry, this is tested only on SQL Server, as I don't have access to MySql at the moment)
Also, your structure IS already normalized. Suggestions to the contrary are making assumptions about what you want to do, e.g., assuming that you are interested in tracking user names in addition to email addresses. This is quite reasonable, but is nevertheless an assumption. There is nothing wrong, from a normalization perspective, with using email address as a unique user identifier.
Now, if you are looking for general suggestions on how to set up a database, we can give you LOTS of those. Before normalization, I would start with not using potential keywords as object names (e.g., don't give columns names like 'Name' and 'Date').
Regarding the comment from Matt about the value being NULL when there are no replies: using the COALESCE() function will fix that. COALESCE() returns the first non-NULL argument (or NULL if all arguments are NULL). So replace the MAX(r.Date) with MAX(COALESCE(r.Date, t.Date)).
Yes, you should be able to get it with a query like this:
SELECT
forum_topic.id,
forum_topic.name AS Topic,
MAX(forum_reply.date) AS Last_Modified,
count(*) AS Replies
FROM forum_topic
INNER JOIN forum_reply ON (forum_topic.id=forum_reply.topic_id)
GROUP BY forum_topic.id
The "group by" is the magic that gives us one row per topic, with the MAX() and COUNT() functions giving us the aggregated data you need.
(EDIT: I missed that the body of the first post was in the topic table, so posts with no replies would get missed by the above query. Filip has the right idea suggesting you normalize your data. Once normalized, a query similar the above would get you the data you need).
By "normalized", you mean that the body column of "forum_topic" should be removed, and the actual topic body should be the first reply?
Related
I have a table topics. And I have two queries which select different data, one for the table votings and one from the table messages. topics has a field m_group which is a foreign key for messages. This field can either be NULL or a group for entries in messages (group is a field in this table). If this field is set to a group, it should perform query 1 to select all messages or if it is not set it should perform query 2 to select all votings for this specific topic. I'm using Postgres and PHP on an Apache Webserver.
Now my question is what is the recommended way to go. I came up with two solutions (not sure if solution 2 is actually possible, haven't tried it yet).
Solution 1
First select the field m_group. Then determine if it is set via PHP and perform the associated query.
Solution 2
Use a IF THEN ELSE statement
Basically the query should then look something like this
IF t.m_group IS NULL
THEN
query2
ELSE
query1
As already mentioned, I'm not sure if solution 2 is possible. What would be the best way to handle this? Solution 1 performs two queries, I think this is inefficient.
UPDATE
As mentioned above, it should perform the queries for a specific topic. You have the id of this topic. How can you specify this in solution 2? And does the IF THEN ELSE statement already know the alias t for topics, if that is specified in the queries?
If I understand you right, you need to use two subqueries as sets of data, while you should use one of them as a source depending on what's in the m_group field. Your second approach is good if you only select a few rows. However, if you need to grab a lot of data from the table, this way you will need to perform too many subqueries. I would rather first grab all the data you need from topics and then select what you need with both queries.
Pseudo SQL query will look like that:
with t1 as (select t.id, group_m, ... from topics t where t.m_group is not null),
t2 as (select t.id, ... from topics t where t.m_group is null)
select id, title, SUM(subtable_id) AS votes
from query1
join t1 on t1.id = query1.id and...
union
select id, title, SUM(subtable_id) AS votes
from query2
join t2 on t2.id = query2.id and...
I'm new here and I hope I am asking my question correctly:
I am trying to implement search on forums database.
I have 'questions' table and 'answers' table (they both related by 'id_question' field). I also have 'suggestions' table and 'comments' table that are also related.
In addition I have 'profiles' table which related to every table I've mentioned before by 'profile_id' field, this profiles table holds the information on the users.
I would like to implement wide search on specific fields ('topic', 'description'...) in all the four tables I've mentioned before, and display those fields and the information of the user who wrote the post.
I really messed with this.
right now I have 4 queries (for each table), and each query makes join with profiles table.
Do you know better way to do this?
Thanks!
You can do this with INNER JOINS. I recommend starting with some simple INNER JOINS and building up your queries to be more inclusive and join more tables. It all depends on what you're trying to get, really. Be as precise as possible in returning exactly the information you want.
Here is a simple example and the assumptions I made:
Assuming the following
table: primarykey*, foreignkey#, othercolumns
questions: id_questions*, topic, description, profile_id#
answers: id_answers*, id_questions#, topic, description, profile_id#
suggestions: id_suggestions*, topic, description, profile_id#
comments: id_comments*, topic, description, profile_id#
user: profile_id*, name, details
KEYWORD: replace with your keyword or phrase
Look for a keyword in the questions and answers
SELECT * FROM questions q
INNER JOIN answers a
ON q.profile_id = a.profile_id
WHERE topic LIKE '%KEYWORD%'
OR description LIKE '%KEYWORD%';
Get the user profile for a user who used keyword in the questions and answers tables:
SELECT u.* FROM users u
INNER JOIN answers a
ON u.profile_id = a.profile_id
INNER JOIN questions q
ON q.profile_id = u.profile_id
WHERE topic LIKE '%KEYWORD%'
OR description LIKE '%KEYWORD%';
Hope this helps.
Edit: formatting.
Yes, do it in four queries.
Otherwise you will get a Cartesian product as you try to join the tables.
I think you can try using UNION. Is not it?
I've been scratching my head at this problem all day and I simple just can't work it out. This is the first time I've attempted to try and use SQL Joining, while we do kinda get taught the basics I'm more into pushing a little more into the advanced stuff.
Basically I'm making my own forum, and I have two tables. f_topics (The threads) and f_groups (The forums, or categories). There is a relationship between topicBase in f_topics and groupID in f_groups, this shows which group each topic belongs to. Each topic has a unique ID called topicID and same for the groups, called groupID.
Basically, I'm trying to get all these columns into a single SELECT statement - The title of the topic, the date the topic was posted, the ID of the group the topic belongs in, and the name of that group. This is what I was trying to use, but the group always comes back as 1, even if the topic is in groupID 2:
$query=mysqli_query($link, "
SELECT `topicName`, `topicDate`, `groupName`, `groupID`
FROM `f_topics`
NATURAL JOIN `f_groups`
WHERE `f_topics`.`topicID`='$tid';
") or die("Failed to get topic detail E: ".mysqli_error());
var_dump(mysqli_fetch_assoc($query));
Sorry if this doesn't make much sense, and if my entire logic is completely wrong, if so could you suggest an alternate method?
Thanks for reading!
To join tables, you need to map the foreign keys. Assuming your groups table has an groupID field, this is how you'd join them:
SELECT `topicName`, `topicDate`, `groupName`, `groupID`
FROM `f_topics`
LEFT JOIN `f_groups`
ON `f_topics`.`groupID` = `f_groups`.`groupID`
WHERE`f_topics`.`topicID`='$tid';
So from what I gather there is a column in f_topics named "topicBase" which references the groupID column from the f_groups table.
Based on that assumption, you can perform either an INNER JOIN or a LEFT JOIN. INNER requires there be an entry in both tables while LEFT requires there only be data in f_topics.
SELECT
f_topics.topicName,
f_topics.topicDate
f_groups.groupName
f_groups.groupID
FROM
f_topics
INNER JOIN
f_groups
ON
f_topics.topicBase = f_groups.groupID
WHERE
f_topics.topicID = '$tid'
I recommend you avoid NATURAL JOIN.
Primarily because a working query can be broken by the addition of a new column in a referenced table, which matches a column name in the other referenced table.
Secondly, for any reader (reviewer) of the SQL, which columns are being matched to which columns is not clear, without a careful review of both tables. (And, if someone has added a column that has broken the query, it makes it even more difficult to figure out what the JOIN criteria used to be, before the column was added.
Instead, I recommend you specify the column names in a predicate in the ON clause.
It's also good practice to qualify all column references by table name, or preferably, a shorter table alias.
For simpler statements, I agree that this may look like unnecessary overhead. But once statements become more complicated, this pattern VASTLY improves the readability of the statement.
Absent the definitions of the two tables, I'm going to have to make assumptions, and I "guess" that there is a groupID column in both of those tables, and that is the only column that is named the same. But you specify that its the topicBase column in f_topics that matches groupID in f_groups. (And the NATURAL JOIN won't get you that.)
I think the resultset you want will be returned by this query:
SELECT t.`topicName`
, t.`topicDate`
, g.`groupName`
, g.`groupID`
FROM `f_topics` t
JOIN `f_groups` g
ON g.`groupID` = t.`topicBase`
WHERE t.`topicID`='$tid';
If its possible for the topicBase column to be NULL or to contain a value that does not match a f_groups.GroupID value, and you want that topic returned, with the columns from f_group returned as NULL (when there is no match), you can get that with an outer join.
To get that behavior, in the query above, add the LEFT keyword immediately before the JOIN keyword.
I hate to submit a new question, but everyone else has some slight thing that is different enough to make this one seem necessary to ask.
Users are to type in a vendor name, and then see all the "kinds" of things they have bought from that company, in a list, sorted by the lowest-inventory-on-hand.
Summary:
I have three tables.
There are more fields than these, but these are the relevant ones (as far as I can tell).
stuff_table
stuff_vendor_name *(search this field with $user_input, but only one result per lookup_type)*
lookup_type
lookup_table
lookup_type
lookup_quantity (order by this)
category_type
category_table
category_type
category_location (check if this field == $this_location, which is already assigned)
Wordier Explanation:
The users are searching for a value that is contained only in the stuff_table -- distinct stuff_vendor_name values for each lookup_type. Each item can be bought from multiple sources, the idea is to see if any vendor has ever sold even one of any type of item before.
But the results need to be ORDER BY the lookup_quantity, in the lookup_table.
And importantly, I have to check to see if they are searching the correct location for these categories, located in the category_table in the category_location field.
How do I efficiently make this query?
Above, I mentioned the variables that I have:
$user_input (the value we are searching for distinct matches in the stuff_vendor_name field) and $current_location.
To understand the relationship of these tables, I will use an example.
The stuff_table would have dozens of entries with dozens of vendors, but have a lookup_type of, say, "watermelon," "apple," or "cherry."
The lookup_table would give the category_type of "Jellybean." One category type can have multiple lookup_types. But each lookup_type has exactly one category_type.
You are not sharing much about the relationships, but try this:
SELECT *
FROM stuff_table st
LEFT JOIN lookup_table lt
ON st.lookup_type = lt.lookup_type
LEFT JOIN category_table ct
ON lt.category_type = ct.category_type
AND ct.category_location = $this_location
GROUP BY st.lookup_type
ORDER BY lt.lookup_quantity
WHERE st.stuff_vendor_name = $user_input
From a first glance at it you could use foreign keys in your tables to make link between them or using the LEFT JOIN mysql command to make abstraction of another linked table.
The only example I can think of is on a Doctrine pattern, but I think you'll get what I'm saying:
$q = Doctrine_Query::create()
->from('Default_Model_DbTable_StuffTable s')
->leftJoin('s.LookupTable l')
->leftJoin('s.CategoryTable c')
->orderBy('l.lookup_quantity DESC');
$stuff= $q->execute(array(), Doctrine_Core::HYDRATE_ARRAY);
I made a nested query instead.
The final code looks like this:
$query_row=mysql_query(
"SELECT DISTINCT * FROM table_a WHERE
field_1 IN (SELECT field_1 FROM table_b WHERE field_2 = $field_2)
AND field_3 IN (SELECT field_3 FROM table_c WHERE field_4 = $field_4)
ORDER BY field_5 DESC
");
This was incredibly simple. I just didn't know you could do a nested query like that.
I read it was "bad form" because it makes some kind of search optimization not as good as it could be, so be careful using nested select statements.
However for me, it seemed to actually be significantly faster.
I have written a simple messaging system for a CMS I am developing and am having a hard time finding the best way to grab all the messages and their replies ordered by the newest (message or reply).
I have a table called "messages" that has the fields:
*id, active[1,0], subject, message, datetime, user_from, user_to, reply_id*
It's all straight forward and the *reply_id* is empty for main level messages and contains the parent id for replies. I am at a loss for how to write the SQL to grab all the main level messages in datetime DESC order based on themselves AND their replies. I'm sure it's done with a UNION of some sort but my SQL skills are lacking. And if the same SELECT can give a count of the replies within each main level message that would be amazing!
Any help would be greatly appreciated!
It's late and I'm tired but I think you could try something like that:
SELECT main.*, COUNT(reply.id) AS cnt_replies, MAX(reply.datetime) AS max_datetime
FROM posts AS main
LEFT JOIN posts AS reply
ON main.id = reply.reply_id
GROUP BY main.id
HAVING main.reply_id IS NULL
ORDER BY max_datetime DESC
EDIT: fixed query
EDIT2: now includes messages without any replies