Query showing list of associations in many-to-many relationship - sql

I have two tables, Book and Tag, and books are tagged using the association table BookTag. I want to create a report that contains a list of books, and for each book a list of the book's tags. Tag IDs will suffice, tag names are not necessary.
Example:
Book table:
Book ID | Book Name
28 | Dracula
BookTag table:
Book ID | Tag ID
28 | 101
28 | 102
In my report, I'd like to show that book #28 has the tags 101 and 102:
Book ID | Book Name | Tags
28 | Dracula | 101, 102
Is there a way to do this in-line, without having to resort to functions or stored procedures? I am using SQL Server 2005.
Please note that the same question already has been asked in Combine multiple results in a subquery into a single comma-separated value, but the solution involves creating a function. I am asking if there is a way to solve this without having to create a function or a stored procedure.

You can almost do it. The only problem I haven't resolved is the comma delimiter. Here is a query on a similar structure that separates the tags using a space.
SELECT em.Code,
(SELECT et.Name + ' ' AS 'data()'
FROM tblEmployeeTag et
JOIN tblEmployeeTagAssignment eta ON et.Id = eta.EmployeeTag_Id AND eta.Employee_Id = em.id
FOR XML PATH('') ) AS Tags
FROM tblEmployee em
Edit:
Here is the complete version using your tables and using a comma delimiter:
SELECT bk.Id AS BookId,
bk.Name AS BookName,
REPLACE((SELECT LTRIM(STR(bt.TagId)) + ', ' AS 'data()'
FROM BookTag bt
WHERE bt.BookId = bk.Id
FOR XML PATH('') ) + 'x', ', x','') AS Tags
FROM Book bk
I suppose for future reference I should explain a bit about what is going on. The 'data()' column name is a special value that is related to the FOR XML PATH statement. It causes the XML document to be rendered as if you did an .InnerText on the root node of the resulting XML.
The REPLACE statement is a trick to remove the trailing comma. By appending a unique character (I randomly chose 'x') to the end of the tag list I can search for comma-space-character and replace it with an empty string. That allows me to chop off just the last comma. This assumes that you are never going to have that sequence of characters in your tags.

Unless you know what the tag ids/names are and can hard code them into your query, I'm afraid the answer is no.

If you knew the maximum number of tags for a book, you could use a pivot to get them to the same row and then use COALESCE, but in general, I don't believe there is.

The cleanest solution is probably to use a custom C# CLR aggregate function. We have found that this works really well. You can find instructions for creating this at http://dotnet-enthusiast.blogspot.com/2007/05/user-defined-aggregate-function-in-sql.html

Related

Basic SQL Script to find a special character, but only when present more than once

I am relearning MS-SQL for a project.
I have a table with a field where the data includes the special character |.
Most times the field does not have it, sometimes once, sometimes 4 times.
I have been able to get it filtered to when present, but I would like to try to show only the times it appears more than once.
This is what I have come up so far:
SELECT UID, OBJ_UID, DESCRIPTION
FROM SPECIFICS
WHERE (NAMED LIKE '%[|]%')
Is there an easy way?
You can replace | with blank and compare length of strings
SELECT
UID, OBJ_UID, DESCRIPTION
FROM
SPECIFICS
WHERE
LEN(NAMED) - LEN(REPLACE(NAMED, '|', '')) > 1
Query returns rows where | appears more than one time

SQL Server 2014 equivalent to mysql's find_in_set()

I'm working with a database that has a locations table such as:
locationID | locationHierarchy
1 | 0
2 | 1
3 | 1,2
4 | 1
5 | 1,4
6 | 1,4,5
which makes a tree like this
1
--2
----3
--4
----5
------6
where locationHierarchy is a csv string of the locationIDs of all its ancesters (think of a hierarchy tree). This makes it easy to determine the hierarchy when working toward the top of the tree given a starting locationID.
Now I need to write code to start with an ancestor and recursively find all descendants. MySQL has a function called 'find_in_set' which easily parses a csv string to look for a value. It's nice because I can just say "find in set the value 4" which would give all locations that are descendants of locationID of 4 (including 4 itself).
Unfortunately this is being developed on SQL Server 2014 and it has no such function. The CSV string is a variable length (virtually unlimited levels allowed) and I need a way to find all ancestors of a location.
A lot of what I've found on the internet to mimic the find_in_set function into SQL Server assumes a fixed depth of hierarchy such as 4 levels maximum) which wouldn't work for me.
Does anyone have a stored procedure or anything that I could integrate into a query? I'd really rather not have to pull all records from this table to use code to individually parse the CSV string.
I would imagine searching the locationHierarchy string for locationID% or %,{locationid},% would work but be pretty slow.
I think you want like -- in either database. Something like this:
select l.*
from locations l
where l.locationHierarchy like #LocationHierarchy + ',%';
If you want the original location included, then one method is:
select l.*
from locations l
where l.locationHierarchy + ',' like #LocationHierarchy + ',%';
I should also note that SQL Server has proper support for recursive queries, so it has other options for hierarchies apart from hierarchy trees (which are still a very reasonable solution).
Finally It worked for me..
SELECT * FROM locations WHERE locationHierarchy like CONCAT(#param,',%%') OR
o.unitnumber like CONCAT('%%,',#param,',%%') OR
o.unitnumber like CONCAT('%%,',#param)

Postgres text search on multiple rows

I have a table called 'exclude' that contains hashtags:
-------------
id | tag
-------------
1 #oxford
2 #uk
3 #england
-------------
I have another table called 'post':
-----------------------------------------------
id | tags | text
1 #oxford #funtimes Sitting in the sun
2 #oz Beach fun
3 #england Milk was a bad choice
-----------------------------------------------
In order to do a text search on the posts tags I've been running a query like follows:
SELECT * FROM post WHERE to_tsvector(tags) ## plainto_tsquery('mysearchterm')
However, I now want to be able to exclude all posts where some or all of the tags are in my exclude table. Is there any easy way to do this in SQL/Postgres?
I tried converting the tags row into one column, and using this term within the plainto_tsquery function but it doesn't work (I don't know how to do a text search 'not equal' to either, hence the logic is actual wrong, albeit on the right lines in my mind):
select * from post where to_tsvector(tags) ## plainto_tsquery(
select array_to_string(array(select RTRIM(value) from exclude where key = 'tag'), ' ')
)
What version of PostgreSQL are you on? And how flexible is your schema design? In other words, can you change it at will? Or is this out of your control?
Two things immediately popped to mind when I read your questions. One is you should be able to use array and the the #> (contains) or <# (is contains by) operators.
Here is documentation
Second, you might be able to utilize an hstore and do a similar operation.
to:
hstore #> hstore
It's not a true hstore, because you are not using a real key=>value pair. But, I guess you could do {tagname}=True or {tagname}=NULL. Might be a bit hackish.
You can see the documentation (for PostgreSQL 9.1) hstore and how to use it here

Merge Multiple Data from Rows/Records into One Row w/ Comma Separated Fields

If I were to query our ORDERS table, I might enter the following:
SELECT * FROM ORDERS
WHERE ITEM_NAME = 'Fancy Pants'
In the results for this query, I might get the following:
----------------------------------------------------------------------
ORDER_ID WAIST First_Name Email
----------------------------------------------------------------------
001 32 Jason j-diddy[at]some-thing.com
005 28 Pip pirrip[at]british-mail.com
007 28 HAL9000 olhal[at]hot-mail.com
Now, I'm also wanting to pull information from a different table:
SELECT * FROM PRODUCTS
WHERE ITEM_NAME = 'Fancy Pants'
------------------------------------------
PRODUCT_ID Product Prod_Desc
------------------------------------------
008 Fancy Pants Really fancy.
In the end, however, I'm actually wanting to condense these records into one row via SQL query:
-----------------------------------------------------------------------------
PRODUCT ORDER_Merged First_Name_Merged Email_Merged
-----------------------------------------------------------------------------
Fancy Pants 001,005,007 Jason,Pip,Hal9000 j-di[...].com, pirrip[...].com
Anyway, that's how it would look. What I can't figure out is what that "merge" query would look like.
My searches here unfortunately keep leading me to results for PHP. I have found a couple of results re: merging into CSV rows via SQL but I don't think they'll work in my scenario.
Any insight would, as always, be greatly appreciated.
UPDATE:
Ah, turns out the STUFF and FOR XML functions were exactly what I needed. Thanks all!!
Select
A.name,
stuff((
select ',' + B.address
from Addresses B
WHERE A.id=B.name_id
for xml path('')),1,1,'')
From Names A
This is an excellent article on various approaches to group concatenation with pro's and con's of each.
http://www.simple-talk.com/sql/t-sql-programming/concatenating-row-values-in-transact-sql/
Personally however, I like the Coalesce approach as I demonstrate here:
https://dba.stackexchange.com/a/2615/1607

How do you concat multiple rows into one column in SQL Server?

I've searched high and low for the answer to this, but I can't figure it out. I'm relatively new to SQL Server and don't quite have the syntax down yet. I have this datastructure (simplified):
Table "Users" | Table "Tags":
UserID UserName | TagID UserID PhotoID
1 Bob | 1 1 1
2 Bill | 2 2 1
3 Jane | 3 3 1
4 Sam | 4 2 2
-----------------------------------------------------
Table "Photos": | Table "Albums":
PhotoID UserID AlbumID | AlbumID UserID
1 1 1 | 1 1
2 1 1 | 2 3
3 1 1 | 3 2
4 3 2 |
5 3 2 |
I'm looking for a way to get the all the photo info (easy) plus all the tags for that photo concatenated like CONCAT(username, ', ') AS Tags of course with the last comma removed. I'm having a bear of a time trying to do this. I've tried the method in this article but I get an error when I try to run the query saying that I can't use DECLARE statements... do you guys have any idea how this can be done? I'm using VS08 and whatever DB is installed in it (I normally use MySQL so I don't know what flavor of DB this really is... it's an .mdf file?)
Ok, I feel like I need to jump in to comment about How do you concat multiple rows into one column in SQL Server? and provide a more preferred answer.
I'm really sorry, but using scalar-valued functions like this will kill performance. Just open SQL Profiler and have a look at what's going on when you use a scalar-function that calls a table.
Also, the "update a variable" technique for concatenation is not encouraged, as that functionality might not continue in future versions.
The preferred way of doing string concatenation to use FOR XML PATH instead.
select
stuff((select ', ' + t.tag from tags t where t.photoid = p.photoid order by tag for xml path('')),1,2,'') as taglist
,*
from photos
order by photoid;
For examples of how FOR XML PATH works, consider the following, imagining that you have a table with two fields called 'id' and 'name'
SELECT id, name
FROM table
order by name
FOR XML PATH('item'),root('itemlist')
;
Gives:
<itemlist><item><id>2</id><name>Aardvark</a></item><item><id>1</id><name>Zebra</name></item></itemlist>
But if you leave out the ROOT, you get something slightly different:
SELECT id, name
FROM table
order by name
FOR XML PATH('item')
;
<item><id>2</id><name>Aardvark</a></item><item><id>1</id><name>Zebra</name></item>
And if you put an empty PATH string, you get even closer to ordinary string concatenation:
SELECT id, name
FROM table
order by name
FOR XML PATH('')
;
<id>2</id><name>Aardvark</a><id>1</id><name>Zebra</name>
Now comes the really tricky bit... If you name a column starting with an # sign, it becomes an attribute, and if a column doesn't have a name (or you call it [*]), then it leaves out that tag too:
SELECT ',' + name
FROM table
order by name
FOR XML PATH('')
;
,Aardvark,Zebra
Now finally, to strip the leading comma, the STUFF command comes in. STUFF(s,x,n,s2) pulls out n characters of s, starting at position x. In their place, it puts s2. So:
SELECT STUFF('abcde',2,3,'123456');
gives:
a123456e
So now have a look at my query above for your taglist.
select
stuff((select ', ' + t.tag from tags t where t.photoid = p.photoid order by tag for xml path('')),1,2,'') as taglist
,*
from photos
order by photoid;
For each photo, I have a subquery which grabs the tags and concatenates them (in order) with a commma and a space. Then I surround that subquery in a stuff command to strip the leading comma and space.
I apologise for any typos - I haven't actually created the tables on my own machine to test this.
Rob
I'd create a UDF:
create function GetTags(PhotoID int) returns #tags varchar(max)
as
begin
declare #mytags varchar(max)
set #mytags = ''
select #mytags = #mytags + ', ' + tag from tags where photoid = #photoid
return substring(#mytags, 3, 8000)
end
Then, all you have to do is:
select GetTags(photoID) as tagList from photos
Street_Name ; Street_Code
west | 14
east | 7
west+east | 714
If want to show two different row concat itself , how can do it?
(I mean last row i want to show from select result. My table had first and secord record)