Select using concat in single table by joining parent & child id's - join

I have one table with 3 columns are below
+---------------------------------------+
| id | name | parent_id |
+---------------------------------------+
| -1 | / | |
| 1 | Organization | -1 |
| 2 | United States | 1 |
| 3 | Business Analyst | 1 |
| 4 | Human Resources | 1 |
| 5 | Benefits Manager | 4 |
| 6 | Metropolitan Plant | 2 |
| 7 | Administration | 6 |
+---------------------------------------+
And my query is like this
SELECT CONCAT(parent.name, '/', child.name) AS path
FROM table_name AS child INNER JOIN table_name AS parent
ON child.id = parent.parent_id
I am expecting output as below.
/Organization
/Organization/United States
/Organization/Business Analyst
/Organization/Human Resources
/Organization/Human Resources/Benefits Manager
/Organization/United States/Metropolitan Plant
/Organization/United States/Metropolitan Plant/Administration

Ok...there might be a more elegant way to do this...especially with using do loops...but with what immediately comes to mind, you may need to do several joins. Is the maximum level low? I hope so. Here's an idea, but it's messy and may require a lot of spool depending on your data size:
SELECT CONCAT(path2, '/', D.name) AS path3
FROM
(SELECT CONCAT(path1, '/', B.name) AS path2
FROM
(SELECT CONCAT(parent.name, '/', child.name) AS path1
FROM table_name AS parent LEFT JOIN table_name AS child
ON child.id = parent.parent_id) AS A
LEFT JOIN TABLE_NAME AS B
ON A.id = B.parent_id) AS C
LEFT JOIN TABLE_NAME AS D
ON C.id = D.parent_id
The above code would only take it up to 3 levels. If something better comes to mind, I'll post it.

Suspect you're expected to use a hierarchical query here
WITH foo (id, parent_id, name, fullpath)
AS (SELECT id,
parent_id,
name,
'/' AS fullpath
FROM table_name
WHERE parent_id IS NULL
UNION ALL
SELECT m.id,
m.parent_id,
m.name,
f.fullpath || m.name || '/' AS fullpath
FROM foo f JOIN table_name m ON (m.parent_id = f.id))
SELECT fullpath FROM foo
WHERE id > 0
That'll be pretty close.

Related

How do I get 1 row from set of multiple rows based on a condition in PostgreSQL 10?

I have two tables, user and car with below mentioned rows and columns.
Table 1: user
id | name
---------
1 | ABC
2 | PQR
3 | XYZ
Table 2: car
id | user_id | is_serviced
--------------------------
1 | 1 | 0
2 | 1 | 1
3 | 2 | 0
4 | 2 | 0
User ABC has two cars - only one car has been serviced.
User PQR has two cars - none of the cars has been serviced.
User XYZ has no cars yet.
I want to fetch records as per below output where I want to display all users who have at least one serviced car.
The query I have (I do not understand what clause to use for my query to get the expected output):
SELECT u.user_name,
CASE WHEN c.is_serviced = true THEN 'YES' ELSE 'NO' END AS has_serviced_car
FROM "user" u
LEFT JOIN car c ON c.user_id = u.id;
The output of the above query:
user_name | has_serviced_car
-----------------------
ABC | No
ABC | Yes
PQR | No
PQR | No
XYZ | No
Expected output:
user_name | has_serviced_car
-----------------------
ABC | Yes
PQR | No
XYZ | No
Please note that user XYZ has no cars still I need it to be displayed.
Got it!
SELECT u.name AS user_name,
CASE
WHEN u.id IN (SELECT u.id
FROM "user" u
LEFT JOIN car c ON u.id = c.user_id
WHERE c.is_serviced = true) THEN 'Yes'
ELSE 'No' END AS has_serviced_car
FROM "user" u
LEFT JOIN car c ON u.id = c.user_id
GROUP BY user_name, u.id
ORDER BY u.id;

Rails query through 2 different associated models

I'm having a little trouble trying to get a query to work the way I want it, I'm not getting all the results I'm hoping for.
I have 3 models Post, Comment and Tag. Both the posts and the comments can contain tags, and both have a has_and_belongs_to_many relationship with tags. I want to be able to get all the posts that either have a specified tag or have comments with that tag, I've been doing it in the following scope on posts like so:
scope :tag, -> (tag_id) { joins(:tags, :comment_tags).where("tags_posts.tag_id = :tag_id OR comments_tags.tag_id = :tag_id", tag_id: tag_id) }
But that doesn't return all the posts, just a subset of them, seems like its only the ones regarding the comments, this is the query it generates:
SELECT COUNT(*) FROM "posts"
INNER JOIN "tags_posts" ON "tags_posts"."post_id" = "posts"."id"
INNER JOIN "tags" ON "tags"."id" = "tags_posts"."tag_id"
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
INNER JOIN "comments_tags" ON "comments_tags"."comment_id" = "comments"."id"
INNER JOIN "tags" "comment_tags_posts" ON "comment_tags_posts"."id" = "comments_tags"."tag_id"
WHERE (tags_posts.tag_id = 1 OR comments_tags.tag_id = 1)
These are the models:
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags
has_many :comment_tags, through: :comments, source: :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts
has_and_belongs_to_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
has_and_belongs_to_many :tags
end
I'm not certain whether you've already figured this out, but in case you haven't, here is a possible solution:
In plain SQL, mainly for illustration purposes:
SELECT
DISTINCT posts.*
FROM
posts
INNER JOIN
tags_posts ON tags_posts.post_id = posts.id
LEFT JOIN
comments ON comments.post_id = posts.id
LEFT JOIN
comments_tags ON comments_tags.comment_id = comments.id
INNER JOIN
tags ON (tags.id = tags_posts.tag_id OR tags.id = comments_tags.tag_id)
WHERE tags.id = 1
The primary issue in your original version was that you were making an INNER JOIN with comments and comments_tags. As a result you were probably cutting out every Post which did not have any comments. So the solution is to LEFT JOIN everything related to the comments. And then, because we are left joining, we can INNER JOIN tags on either the tag posts or comment posts.
Converting to Active Record is not very pretty, but necessary:
Post.joins("INNER JOIN posts_tags ON posts_tags.post_id = posts.id")
.joins("LEFT JOIN comments ON comments.post_id = posts.id")
.joins("LEFT JOIN comments_tags ON comments_tags.comment_id = comments.id")
.joins("INNER JOIN tags ON (posts_tags.tag_id = tags.id OR comments_tags.tag_id = tags.id)")
.where(tags: {id: 1})
.uniq
Note the necessity of DISTINCT and uniq, as you will get duplicates because of the LEFT JOIN.
Edit
In case there's some misunderstanding of the dataset or structure, this is an example of the data I used in my test to create the above query.
posts
+----+--------------------------+
| id | text |
+----+--------------------------+
| 1 | Post about programming 1 |
| 2 | Post about programming 2 |
| 3 | Post about programming 3 |
| 4 | Post about cooking 1 |
| 5 | Post about cooking 2 |
+----+--------------------------+
tags
+----+-------------+
| id | name |
+----+-------------+
| 1 | programming |
| 2 | cooking |
| 3 | woodworking |
+----+-------------+
tags_posts
+--------+---------+
| tag_id | post_id |
+--------+---------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 4 |
| 2 | 5 |
+--------+---------+
comments
+----+----------------------------------------------+---------+
| id | comment_text | post_id |
+----+----------------------------------------------+---------+
| 1 | comment - programming on programming post 1a | 1 |
| 2 | comment - programming on programming post 1b | 1 |
| 3 | comment - programming on programming post 2a | 2 |
| 4 | comment - cooking on programming post 3a | 3 |
| 5 | comment - programming on cooking post 4a | 4 |
| 6 | comment - cooking on cooking post 4b | 4 |
| 7 | comment - cooking on cooking post 5a | 5 |
+----+----------------------------------------------+---------+
comments_tags
+--------+------------+
| tag_id | comment_id |
+--------+------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 1 | 5 |
| 2 | 4 |
| 2 | 6 |
| 2 | 7 |
+--------+------------+
If I want to search for "programming", the above query will yield this result set:
+----+--------------------------+
| id | text |
+----+--------------------------+
| 1 | Post about programming 1 |
| 2 | Post about programming 2 |
| 4 | Post about cooking 1 |
| 3 | Post about programming 3 |
+----+--------------------------+
since we have 3 posts specifically tagged with "programming", and one comment tagged as "programming" on a differently tagged post.
I am not sure to understand what's a yum, is it a post ?
From your SQL query it seems it will count only the yum that have both a specific tag AND comment with this specific tag. What you want is to count yum that have a specific tag OR comments with this specific tag.
I would do either 2 queries one to count the yum with specific tag + one to count the yum with specific commented tags and add them both to get the total or make one query with an UNION condition.
scope :yums_tagged, -> (tag_id) { joins(:tags).where("tags_yums.tag_id = :tag_id", tag_id: tag_id) }
scope :comments_taged, -> (tag_id) { joins(:comment_tags).where("comments_tags.tag_id = :tag_id", tag_id: tag_id) }

Neo4j cypher query to retrieve data tree

if u want to query for a specific node type and related nodes solution is simple, by using collect function we can achieve this goal like return country, collect(city) as c
but what we should do if we need to retrieve a data tree like bloodline or user->post->comment->like
is there any solution to handle this kind of data in cypher output?
Given the following graph:
CREATE (user:User { id: 0 })
CREATE (post:Post)
CREATE (comment:Comment)
CREATE (user)-[:POSTED]->(post)<-[:ON]-(comment)<-[:COMMENTED]-(user)
CREATE (user)-[:LIKES]->(comment)
Retrieve the variable length paths using the following query:
MATCH (user:User { id: 0 })
MATCH p=(user)-[*]->(post)
RETURN p
ORDER BY length(p) DESC
Which results in the following output:
+----------------------------------------------------------------+
| p |
+----------------------------------------------------------------+
| [Node[6]{id:0},:COMMENTED[8] {},Node[8]{},:ON[7] {},Node[7]{}] |
| [Node[6]{id:0},:LIKES[9] {},Node[8]{},:ON[7] {},Node[7]{}] |
| [Node[6]{id:0},:POSTED[6] {},Node[7]{}] |
| [Node[6]{id:0},:COMMENTED[8] {},Node[8]{}] |
| [Node[6]{id:0},:LIKES[9] {},Node[8]{}] |
+----------------------------------------------------------------+
5 rows
19 ms
To see what is related and how, run the following query:
// What is related, and how
MATCH (a)-[r]->(b)
WHERE labels(a) <> [] AND labels(b) <> []
RETURN DISTINCT head(labels(a)) AS This, type(r) as To, head(labels(b)) AS That
LIMIT 10
Which has the results:
+-------------------------------------+
| This | To | That |
+-------------------------------------+
| "User" | "POSTED" | "Post" |
| "User" | "COMMENTED" | "Comment" |
| "User" | "LIKES" | "Comment" |
| "Comment" | "ON" | "Post" |
+-------------------------------------+
4 rows
139 ms

Display latest messages from messages table, group by user

I'm trying to create an inbox for messaging between users.
Here are the following tables:
Messsages
Id | Message_from | message_to | message
1 | 2 | 1 | Hi
2 | 2 | 1 | How are you
3 | 1 | 3 | Hola
4 | 4 | 1 | Whats up
5 | 1 | 4 | Just Chilling
6 | 5 | 1 | Bonjour
Users
Id | Name
1 | Paul
2 | John
3 | Tim
4 | Rob
5 | Sarah
6 | Jeff
I'd like to display an inbox showing the list of users that the person has communicated and the last_message from either users
Paul's Inbox:
Name | user_id | last_message
Sarah| 5 | bonjour
Rob | 4 | Just Chilling
Tim | 3 | Hola
John | 2 | How are you
How do I do this with Active Records?
This should be rather efficient:
SELECT u.name, sub.*
FROM (
SELECT DISTINCT ON (1)
m.message_from AS user_id
, m.message AS last_message
FROM users u
JOIN messages m ON m.message_to = u.id
WHERE u.name = 'Paul' -- must be unique
ORDER BY 1, m.id DESC
) sub
JOIN users u ON sub.user_id = u.id;
Compute all users with the latest message in the subquery sub using DISTINCT ON. Then join to
table users a second time to resolve the name.
Details for DISTINCT ON:
Select first row in each GROUP BY group?
Aside: Using "id" and "name" as column names is not a very helpful naming convention.
How about this:
#received_messages = current_user.messages_to.order(created_at: :desc).uniq
If you want to include messages from the user as well, you might have to do a union query, or two queries, then merge and join them. I'm just guessing with some pseudocode, here, but this should set you on your way.
received_messages = current_user.messages_to
sent_messages = current_user.messages_from
(received_messages + sent_messages).sort_by { |message| message[:created_at] }.reverse
This type of logic is belongs to a model, not the controller, so perhaps you can add this to the message model.
scope :ids_of_latest_per_user, -> { pluck('MAX(id)').group(:user_id) }
scope :latest_per_user, -> { where(:id => Message.latest_by_user) }
Message.latest_per_user

Does it make sense to convert DB-ish queries into Rails ActiveRecord Model lingo?

mysql> desc categories;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(80) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
mysql> desc expenses;
+-------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| created_at | datetime | NO | | NULL | |
| description | varchar(100) | NO | | NULL | |
| amount | decimal(10,2) | NO | | NULL | |
| category_id | int(11) | NO | MUL | 1 | |
+-------------+---------------+------+-----+---------+----------------+
Now I need the top N categories like this...
Expense.find_by_sql("SELECT categories.name, sum(amount) as total_amount
from expenses
join categories on category_id = categories.id
group by category_id
order by total_amount desc")
But this is nagging at my Rails conscience.. it seems that it may be possible to achieve the same thing via Expense.find and supplying options like :group, :joins..
Can someone translate this query into ActiveRecord Model speak ?
Is it worth it... Personally i find the SQL more readable and gets my job done faster.. maybe coz I'm still learning Rails. Any advantages with not embedding SQL in source code (apart from not being able to change DB vendors..sql flavor, etc.)?
Seems like find_by_sql doesn't have the bind variable provision like find. What is the workaround? e.g. if i want to limit the number of records to a user-specified limit.
Expense.find(:all,
:select => "categories.name name, sum(amount) total_amount",
:joins => "categories on category_id = categories.id",
:group => "category_id",
:order => "total_amount desc")
Hope that helps!
Seems like find_by_sql doesn't have the bind variable provision like find.
It sure does. (from the Rails docs)
# You can use the same string replacement techniques as you can with ActiveRecord#find
Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
Well this is the code that finally worked for me.. (Francois.. the resulting sql stmt was missing the join keyword)
def Expense.get_top_n_categories options={}
#sQuery = "SELECT categories.name, sum(amount) as total_amount
# from expenses
# join categories on category_id = categories.id
# group by category_id
# order by total_amount desc";
#sQuery += " limit #{options[:limit].to_i}" if !options[:limit].nil?
#Expense.find_by_sql(sQuery)
query_options = {:select => "categories.name name, sum(amount) total_amount",
:joins => "inner join categories on category_id = categories.id",
:group => "category_id",
:order => "total_amount desc"}
query_options[:limit] = options[:limit].to_i if !options[:limit].nil?
Expense.find(:all, query_options)
end
find_by_sql does have rails bind variable... I don't know how I overlooked that.
Finally is the above use of user-specified a potential entry point for sql-injection or does the to_i method call prevent that?
Thanks for all the help. I'm grateful.

Resources