Mysql query in drupal database - groupwise maximum with duplicate data - mysql

I'm working on a mysql query in a Drupal database that pulls together users and two different cck content types. I know people ask for help with groupwise maximum queries all the time... I've done my best but I need help.
This is what I have so far:
# the artists
SELECT
users.uid,
users.name AS username,
n1.title AS artist_name
FROM users
LEFT JOIN users_roles ur
ON users.uid=ur.uid
INNER JOIN role r
ON ur.rid=r.rid
AND r.name='artist'
LEFT JOIN node n1
ON n1.uid = users.uid
AND n1.type = 'submission'
WHERE users.status = 1
ORDER BY users.name;
This gives me data that looks like:
uid username artist_name
1 foo Joe the Plumber
2 bar Jane Doe
3 baz The Tooth Fairy
Also, I've got this query:
# artwork
SELECT
n.nid,
n.uid,
a.field_order_value
FROM node n
LEFT JOIN content_type_artwork a
ON n.nid = a.nid
WHERE n.type = 'artwork'
ORDER BY n.uid, a.field_order_value;
Which gives me data like this:
nid uid field_order_value
1 1 1
2 1 3
3 1 2
4 2 NULL
5 3 1
6 3 1
Additional relevant info:
nid is the primary key for an Artwork
every Artist has one or more Artworks
valid data for field_order_value is NULL, 1, 2, 3, or 4
field_order_value is not necessarily unique per Artist - an Artist could have 4 Artworks all with field_order_value = 1.
What I want is the row with the minimum field_order_value from my second query joined with the artist information from the first query. In cases where the field_order_value is not valuable information (either because the Artist has used duplicate values among their Artworks or left that field NULL), I would like the row with the minimum nid from the second query.
The Solution
Using divide and conquer as a strategy and mysql views as a technique, and referencing this article about groupwise maximum queries, I solved my problem.
Create the View
# artists and artworks all in one table
CREATE VIEW artists_artwork AS
SELECT
users.uid,
users.name AS artist,
COALESCE(n1.title, 'Not Yet Entered') AS artist_name,
n2.nid,
a.field_image_fid,
COALESCE(a.field_order_value, 1) AS field_order_value
FROM users
LEFT JOIN users_roles ur
ON users.uid=ur.uid
INNER JOIN role r
ON ur.rid=r.rid
AND r.name='artist'
LEFT JOIN node n1
ON n1.uid = users.uid
AND n1.type = 'submission'
LEFT JOIN node n2
ON n2.uid = users.uid
AND n2.type = 'artwork'
LEFT JOIN content_type_artwork a ON n2.nid = a.nid
WHERE users.status = 1;
Query the View
SELECT
a2.uid,
a2.artist,
a2.artist_name,
a2.nid,
a2.field_image_fid,
a2.field_order_value
FROM (
SELECT
uid,
MIN(field_order_value) AS field_order_value
FROM artists_artwork
GROUP BY uid
) a1
JOIN artists_artwork a2
ON a2.nid = (
SELECT
nid
FROM artists_artwork a
WHERE a.uid = a1.uid
AND a.field_order_value = a1.field_order_value
ORDER BY
uid ASC, field_order_value ASC, nid ASC
LIMIT 1
)
ORDER BY artist;

A simple solution to this can be to create views in your database that can then be joined together. This is especially useful if you often want to see the intermediate data in the same way in some other place. While it is possible to mash together the one huge query, I just take the divide and conquer approach sometimes.

Related

How to join conditionally with different tables?

I am using MySQL.
I have three tables named ratings, users and one master table master_entityType.
Depending on the values from entityTable column of the master_entityType table, I have to join with another table. If the values from master_entityType is "Blogs", I have to join with blogs table. If the values from master_entityType is "items", I have to join with items table.
SELECT * FROM ratings AS r
LEFT JOIN users AS u ON u.userID = r.userID
LEFT JOIN master_entityType AS ms ON ms.entityTypeID = r.entityTypeID
CASE ms.entityTable
WHEN 'Blogs' THEN INNER JOIN blogs AS b ON b.blogID = r.entityID
END
WHERE r.entityTypeID = '10' AND r.entityID = '1' AND r.userID = '1'
While using the above query I am getting error, please suggest some step to get that query to work.
Structure of the table are as follows,
In users table,
UserID userName isActive
1 Dinesh 1
2 Kumar 1
In ratings table,
ratingID entityID entityTypeID userID rating
1 1 1 1 5
2 4 2 1 4
In master_entityType table,
entityTypeID entityTable entityTypeName entityTypeDescription active
1 blogs Blogs Null 1
2 items Items Null 1
In Items table,
ItemID name collection active
4 pencil 12 1
5 pen 06 1
In blogs table,
blogID name active
1 socail 1
2 private 1
Your design is strange, so performance is likely to be poor.
UNION ALL two tables together and join with the result. Something like this.
If MySQL has views, then create view that unions Items and Blogs table and use the view in other queries. It makes the queries easier to read, understand and maintain.
Here is SQL Fiddle. I adjusted the WHERE condition in the fiddle, because sample data doesn't have any rows with entityTypeID = 10.
SELECT *
FROM
ratings AS r
LEFT JOIN users AS u ON u.userID = r.userID
LEFT JOIN master_entityType AS ms ON ms.entityTypeID = r.entityTypeID
INNER JOIN
(
SELECT
ItemID AS EntityID
,'Items' AS EntityTypeName
,name
,active
FROM items
UNION ALL
SELECT
BlogID AS EntityID
,'Blogs' AS EntityTypeName
,name
,active
FROM blogs
) AS Entities ON
Entities.EntityTypeName = ms.entityTypeName
AND Entities.EntityID = r.entityID
WHERE r.entityTypeID = '10' AND r.entityID = '1' AND r.userID = '1'

Make HAVING count(*) percentage based - complicated query with percentage calculations

This query suggests friendship based on how many words users have in common. in_common sets this threshold.
I was wondering if it was possible to make this query completely % based.
What I want to do is have user suggested to current user, if 30% of their words match.
curent_user total words 100
in_common threshold 30
some_other_user total words 10
3 out of these match current_users list.
Since 3 is 30% of 10, this is a match for the current user.
Possible?
SELECT users.name_surname, users.avatar, t1.qty, GROUP_CONCAT(words_en.word) AS in_common, (users.id) AS friend_request_id
FROM (
SELECT c2.user_id, COUNT(*) AS qty
FROM `connections` c1
JOIN `connections` c2
ON c1.user_id <> c2.user_id
AND c1.word_id = c2.word_id
WHERE c1.user_id = :user_id
GROUP BY c2.user_id
HAVING count(*) >= :in_common) as t1
JOIN users
ON t1.user_id = users.id
JOIN connections
ON connections.user_id = t1.user_id
JOIN words_en
ON words_en.id = connections.word_id
WHERE EXISTS(SELECT *
FROM connections
WHERE connections.user_id = :user_id
AND connections.word_id = words_en.id)
GROUP BY users.id, users.name_surname, users.avatar, t1.qty
ORDER BY t1.qty DESC, users.name_surname ASC
SQL fiddle: http://www.sqlfiddle.com/#!2/c79a6/9
OK, so the issue is "users in common" defined as asymmetric relation. To fix it, let's assume that in_common percentage threshold is checked against user with the least words.
Try this query (fiddle), it gives you full list of users with at least 1 word in common, marking friendship suggestions:
SELECT user1_id, user2_id, user1_wc, user2_wc,
count(*) AS common_wc, count(*) / least(user1_wc, user2_wc) AS common_wc_pct,
CASE WHEN count(*) / least(user1_wc, user2_wc) > 0.7 THEN 1 ELSE 0 END AS frienship_suggestion
FROM (
SELECT u1.user_id AS user1_id, u2.user_id AS user2_id,
u1.word_count AS user1_wc, u2.word_count AS user2_wc,
c1.word_id AS word1_id, c2.word_id AS word2_id
FROM connections c1
JOIN connections c2 ON (c1.user_id < c2.user_id AND c1.word_id = c2.word_id)
JOIN (SELECT user_id, count(*) AS word_count
FROM connections
GROUP BY user_id) u1 ON (c1.user_id = u1.user_id)
JOIN (SELECT user_id, count(*) AS word_count
FROM connections
GROUP BY user_id) u2 ON (c2.user_id = u2.user_id)
) AS shared_words
GROUP BY user1_id, user2_id, user1_wc, user2_wc;
Friendship_suggestion is on SELECT for clarity, you probably need to filter by it, so yu may just move it to HAVING clause.
I throw this option into your querying consideration... The first part of the from query is to do nothing but get the one user you are considering as the basis to find all others having common words. The where clause is for that one user (alias result OnePerson).
Then, add to the from clause (WITHOUT A JOIN) since the OnePerson record will always be a single record, we want it's total word count available, but didn't actually see how your worked your 100 to 30 threashold if another person only had 10 words to match 3... I actually think its bloat and unnecessary as you'll see later in the where of PreQuery.
So, the next table is the connections table (aliased c2) and that is normal INNER JOIN to the words table for each of the "other" people being considered.
This c2 is then joined again to the connections table again alias OnesWords based on the common word Id -- AND -- the OnesWords user ID is that of the primary user_id being compared against. This OnesWords alias is joined to the words table so IF THERE IS a match to the primary person, we can grab that "common word" as part of the group_concat().
So, now we grab the original single person's total words (still not SURE you need it), a count of ALL the words for the other person, and a count (via sum/case when) of all words that ARE IN COMMON with the original person grouped by the "other" user ID. This gets them all and results as alias "PreQuery".
Now, from that, we can join that to the user's table to get the name and avatar along with respective counts and common words, but apply the WHERE clause based on the total per "other users" available words to the "in common" with the first person's words (see... I didn't think you NEEDED the original query/count as basis of percentage consideration).
SELECT
u.name_surname,
u.avatar,
PreQuery.*
from
( SELECT
c2.user_id,
One.TotalWords,
COUNT(*) as OtherUserWords,
GROUP_CONCAT(words_en.word) AS InCommonWords,
SUM( case when OnesWords.word_id IS NULL then 0 else 1 end ) as InCommonWithOne
from
( SELECT c1.user_id,
COUNT(*) AS TotalWords
from
`connections` c1
where
c1.user_id = :PrimaryPersonBasis ) OnePerson,
`connections` c2
LEFT JOIN `connections` OnesWords
ON c2.word_id = OnesWords.word_id
AND OnesWords.user_id = OnePerson.User_ID
LEFT JOIN words_en
ON OnesWords.word_id = words_en.id
where
c2.user_id <> OnePerson.User_ID
group by
c2.user_id ) PreQuery
JOIN users u
ON PreQuery.user_id = u.id
where
PreQuery.OtherUserWords * :nPercentToConsider >= PreQuery.InCommonWithOne
order by
PreQuery.InCommonWithOne DESC,
u.name_surname
Here's a revised WITHOUT then need to prequery the total original words of the first person.
SELECT
u.name_surname,
u.avatar,
PreQuery.*
from
( SELECT
c2.user_id,
COUNT(*) as OtherUserWords,
GROUP_CONCAT(words_en.word) AS InCommonWords,
SUM( case when OnesWords.word_id IS NULL then 0 else 1 end ) as InCommonWithOne
from
`connections` c2
LEFT JOIN `connections` OnesWords
ON c2.word_id = OnesWords.word_id
AND OnesWords.user_id = :PrimaryPersonBasis
LEFT JOIN words_en
ON OnesWords.word_id = words_en.id
where
c2.user_id <> :PrimaryPersonBasis
group by
c2.user_id
having
COUNT(*) * :nPercentToConsider >=
SUM( case when OnesWords.word_id IS NULL then 0 else 1 end ) ) PreQuery
JOIN users u
ON PreQuery.user_id = u.id
order by
PreQuery.InCommonWithOne DESC,
u.name_surname
There might be some tweaking on the query, but your original query leads me to believe you can easily find simple things like alias or field name type-o instances.
Another options might be to prequery ALL users and how many respective words they have UP FRONT, then use the primary person's words to compare to anyone else explicitly ON those common words... This might be more efficient as the multiple joins would be better on the smaller result set. What if you have 10,000 users and user A has 30 words, and only 500 other users have one or more of those words in common... why compare against all 10,000... but if having up-front a simple summary of each user and how many should be an almost instant query basis.
SELECT
u.name_surname,
u.avatar,
PreQuery.*
from
( SELECT
OtherUser.User_ID,
AllUsers.EachUserWords,
COUNT(*) as CommonWordsCount,
group_concat( words_en.word ) as InCommonWords
from
`connections` OneUser
JOIN words_en
ON OneUser.word_id = words_en.id
JOIN `connections` OtherUser
ON OneUser.word_id = OtherUser.word_id
AND OneUser.user_id <> OtherUser.user_id
JOIN ( SELECT
c1.user_id,
COUNT(*) as EachUserWords
from
`connections` c1
group by
c1.user_id ) AllUsers
ON OtherUser.user_id = AllUsers.User_ID
where
OneUser.user_id = :nPrimaryUserToConsider
group by
OtherUser.User_id,
AllUsers.EachUserWords ) as PreQuery
JOIN users u
ON PreQuery.uer_id = u.id
where
PreQuery.EachUserWords * :nPercentToConsider >= PreQuery.CommonWordCount
order by
PreQuery.CommonWordCount DESC,
u.name_surname
May I suggest a different way to look at your problem?
You might look into a similarity metric, such as Cosine Similarity which will give you a much better measure of similarity between your users based on words. To understand it for your case, consider the following example. You have a vector of words A = {house, car, burger, sun} for a user u1 and another vector B = {flat, car, pizza, burger, cloud} for user u2.
Given these individual vectors you first construct another that positions them together so you can map to each user whether he/she has that word in its vector or not. Like so:
| -- | house | car | burger | sun | flat | pizza | cloud |
----------------------------------------------------------
| A | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
----------------------------------------------------------
| B | 0 | 1 | 1 | 0 | 1 | 1 | 1 |
----------------------------------------------------------
Now you have a vector for each user where each position corresponds to the value of each word to each user. Here it represents a simple count but you can improve it using different metrics based on word frequency if that applies to your case. Take a look at the most common one, called tf-idf.
Having these two vectors, you can compute the cosine similarity between them as follows:
Which basically is computing the sum of the product between each position of the vectors above, divided by their corresponding magnitude. In our example, that is 0.47, in a range that can vary between 0 and 1, the higher the most similar the two vectors are.
If you choose to go this way, you don't need to do this calculation in the database. You compute the similarity in your code and just save the result in the database. There are several libraries that can do that for you. In Python, take a look at the numpy library. In Java, look at Weka and/or Apache Lucene.

Mysql query -- choosing highest rank/ordered subject in table (join)

Scenario:
We have 5 users. (users table)
Each user has up to 10 imgs. (image table)
These 10 images can be ordered 1 – 10. (image table)
Each img can be listed in multiple categories (say there are 5 categories – birds, bees, bunnies, brains, belugas (category table connected to img table via table that stores img_ids and category_ids)
In searching through the categories, say someone chooses bees. The search should find the images in that category that is listed CLOSEST to the #1 img for all users. So if each user has 3 images in the bees category, ordered as numbers 4, 7 & 9, the search should show the 4th as its closest to the number 1.
The results I keep getting are all over the place and almost seems like it is choosing the images via WHEN they were added to the DB.
SELECT i.img_name, i.ordered, a.user_name, c.keyword, c.cat_id
FROM images AS i JOIN artists AS a USING (user_id)
JOIN img_cat_table AS im USING ( img_id )
JOIN catkeys AS c USING (cat_id)
WHERE ( cat_id = 3) // THE BEES ID #
GROUP BY user_id ORDER BY user_name DESC
I'm also not sure if you want to show all of the relevant images in the right order, or only the top one. Assuming that it is the latter situation, you will need to join to a subquery or view that returns the min rank for each user, category:
SELECT i.img_name, i.ordered, a.user_name, c.keyword, c.cat_id
FROM images AS i JOIN artists AS a USING (user_id)
JOIN img_cat_table AS im USING ( img_id )
JOIN catkeys AS c USING (cat_id)
JOIN (
SELECT user_id, min(img_rank) img_rank
FROM images AS i
JOIN artists AS a on i.user_id = a.user_id
JOIN img_cat_table AS im on im.img_id = i.img_id
JOIN catkeys AS c on c.cat_id = i.cat_id
WHERE ( cat_id = 3) ) x on x.user_id = a.user_id and x.img_rank = img_rank
WHERE c.cat_id = 3
I'm not sure what the name of the column that holds the image ranking is. I called it img_rank. Hopefully this will give you the idea
though if you can post the table structure and data, that will be great but Here is what I haved tried
SELECT i.img_name, i.ordered, a.user_name, c.keyword, c.cat_id
from (
select img_name, ordered, img_id, user_id from
images
group by user_id
order by user_img ) as i
JOIN artists AS a USING (user_id)
JOIN img_cat_table AS im USING ( img_id )
JOIN catkeys AS c USING (cat_id)
WHERE ( cat_id = 3) // THE BEES ID #
Try removing DESC from your ORDER BY clause.

mySQL SELECT FROM table WHERE ... AND ... AND ... AND

I have a table "articles" with columns and data:
article_id title body
1 This is the title This is the body text
2 Another title Another body text
Another table "category" with columns and data:
category_id category
1 localnews
2 visible
3 first10
And a table "categories" with columns and data:
categories_id article_id category_id
1 1 1
2 1 2
3 1 3
4 2 1
5 2 3
I want to SELECT the row(s) WHERE categories.category_id = 1 AND =2 AND =3
I'm using:
SELECT articles.article_id, articles.title, articles.body,
categories.article_id, categories.category_id
FROM articles, categories
WHERE articles.article_id = categories.article_id
AND categories.article_id = 1
AND categories.article_id = 2
AND categories.article_id = 3
but it doesn't work. Obviously mySQL needs another syntax.
Can someone help?
Thanks
SELECT
Articles.article_id,
COUNT( Categories.article_id ) AS total
FROM CategoryArticles
LEFT JOIN Articles USING (article_id)
WHERE
CategoryArticles.category_id IN (1,2,3)
GROUP BY CategoryArticles.article_id
HAVING total = 3
I used a bit different names for table because in your example the distinction between category and categories is hard to notice.
An column of a row cannot be 1, 2 or 3 at the same time, which is what AND stipulates. Use OR in your WHERE condition. Better yet - for readability - you can use IN:
SELECT ...
WHERE `categories`.`article_id` IN(1,2,3)
In addition to the commonly used IN() and using a HAVING count, I would be interested in the performance difference by doing a multiple-join as follows...
SELECT STRAIGHT_JOIN
articles.article_id,
articles.title,
articles.body
FROM
categories c1
JOIN articles
on c1.article_id = articles.article_id
JOIN categories c2
on c1.article_id = c2.article_id
AND c2.category_id = 2
JOIN categories c3
on c1.article_id = c3.article_id
AND c3.category_id = 3
WHERE
c1.Category_ID = 1
Yes, this may look obscure, but lets think about it... by doing a join FIRST on the categories table where ONE of your specific categories -- THIS FIRST FROM instance of categories should be representative of whichever category would have the smallest granularity. Ex: Your categories of Local News, Visible and First 10. Local news would probably have the most entries, while Visible and First 10 would have even less... of those, which would have even the smallest number of records. Use THIS category as the where clause.
So, say you have 100,000 articles, and 90,000 are in local news, 45,000 in Visible, and 12,000 in First 10. By starting your query on only those in the 12,000, you are eliminating most of the data.
By then joining to the articles table, and categories AGAIN as alias C2 and C3 respectively based on the other conditions, if found, done, if not, its excluded.
Again, I'm wondering the performance impact. I would also have a compound index on the categories table on both (article_id, category_id)
The value cannot be all three values simultaneously, so you'd better use an IN clause in your WHERE to define which you want to return. Give you've already got a join condition there, you'd want to move that to an ON clause instead as well; ie:
SELECT articles.article_id, articles.title, articles.body, categories.article_id, categories.category_id
FROM articles
INNER JOIN categories ON articles.article_id = categories.article_id
WHERE categories.article_id IN ( 1, 2, 3 )
Of course, you can go to the next step and do:
SELECT articles.article_id, articles.title, articles.body, category.category
FROM articles
INNER JOIN categories ON articles.article_id = categories.article_id
INNER JOIN category ON categories.category_id = category.category_id
WHERE categories.article_id IN ( 1, 2, 3 )
If instead you wanted to show only articles that appear in all three categories, then you could take an approach like:
SELECT articles.article_id, articles.title, articles.body
FROM articles
INNER JOIN categories AS c1
ON articles.article_id = c1.article_id
AND c1.category_id = 1
INNER JOIN categories AS c2
ON articles.article_id = c2.article_id
AND c2.category_id = 2
INNER JOIN categories AS c3
ON articles.article_id = c3.article_id
AND c3.category_id = 3

Building SQL query on a one-to-many relationship

I have a search page where I am trying to build a complex search condition on two tables which look something like:
Users
ID NAME
1 Paul
2 Remy
...
Profiles
FK_USERS_ID TOPIC TOPIC ID
1 language 1
1 language 2
1 expertise 1
1 expertise 2
1 expertise 3
2 language 1
2 language 2
The second table Profiles, lists the "languages" or the "expertises" (among other stuff) of each user, and topic id is a foreign key to another table depending on the topic (if topic is "language", than topic ID is the ID of a language in the languages table, etc...).
The search needs to find something like where user name LIKE %PAU% and the user "has" language 1 and has language 2 and has expertise 1 and has expertise 2.
Any help would be really appreciated! I am performing a LEFT JOIN on the two tables although I am not sure that is the correct choice. My main problem lies on the "AND". The same user has to have both languages 1 and 2, and at the same time expertise 1 and 2.
I work in PHP and I usually try to avoid inner SELECTs and even joins, but I think an inner SELECT is imminent here?
You can accomplish this by building a set of users that matches the criterias from your profile tables, something like this:
SELECT FK_USERS_ID
FROM Profiles
WHERE topic='x'
AND TOPIC_ID IN (1,2)
GROUP BY FK_USERS_ID
HAVING COUNT(1) = 2
Here you list your users that matches the topics you need. By grouping by the user id and specifying the amount of rows that should be returned, you can effectively say "only those that has x and y in topic z. Just make sure that the COUNT(1) = x has the same number of different TOPIC_IDs to look for.
You can then query the user table
SELECT ID
FROM Users
WHERE name like '%PAU%'
AND ID IN (<insert above query here>)
You can also do it in a join and a derived table, but the essence should be explained above.
EDIT:
if you are looking for multiple combinations, you can use mysql's multi-column IN:
SELECT FK_USERS_ID
FROM Profiles
WHERE (topic,topic_id) IN (('x',3),('x',5),('y',3),('y',6))
GROUP BY FK_USERS_ID
HAVING COUNT(1) = 4
This will look for uses matching the pairs x-3, x-5, y-3 and y-6.
You should be able to build the topic-topic_id pairs easily in php and stuffing it into the SQL string, and also just counting the number of pairs you generate into a variable for using for the count(1) number. See http://www.mysqlperformanceblog.com/2008/04/04/multi-column-in-clause-unexpected-mysql-issue/ for performance talk using this approach.
Isn't it just a simple classical INNER JOIN?
SELECT
p.topic, p.topic_id
FROM
profiles p
INNER JOIN
users u
ON
u.id = p.fk_users_id
WHERE
u.name LIKE '%Paul%'
This query would return all the languages and expertise with their IDs for the users matching the pattern, in this case containing Paul in their name. Is this what you like? Or something else?
select *
from users u, profiles p
where u.id = p.fk_users_id
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'language'
and topic_id = 1)
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'language'
and topic_id = 22)
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'expertise'
and topic_id = 1)
and exists (select 1
from profiles
where fk_users_id = u.id
and topic = 'expertise'
and topic_id = 1)
and u.name like '%PAU%'
EDIT:
Ok, a slight variation on #cairnz' answer:
SELECT ID
FROM Users
WHERE name like '%PAU%'
AND ID IN (SELECT FK_USERS_ID
FROM Profiles
WHERE topic='x'
AND ((TOPIC_ID = 1 AND TOPIC = 'language')
OR (TOPIC_ID = 2 AND TOPIC = 'language')
OR (TOPIC_ID = 1 AND TOPIC = 'expertise')
OR (TOPIC_ID = 2 AND TOPIC = 'expertise'))
GROUP BY FK_USERS_ID
HAVING COUNT(1) = 4)
I would do based on JOIN conditions multiple times against each condition that you are "requiring". I would also ensure an index on the Profiles table based on the each part of the key looking for... (FK_User_ID, Topic_ID, Topic)
SELECT STRAIGHT_JOIN
U.ID
FROM Users U
JOIN Profiles P1
on U.ID = P1.FK_User_ID
AND P1.Topic_Id = 1
AND P1.Topic = "language"
JOIN Profiles P2
on U.ID = P2.FK_User_ID
AND P2.Topic_Id = 2
AND P2.Topic = "language"
JOIN Profiles P3
on U.ID = P3.FK_User_ID
AND P3.Topic_Id = 1
AND P3.Topic = "expertise"
JOIN Profiles P4
on U.ID = P4.FK_User_ID
AND P4.Topic_Id = 2
AND P4.Topic = "expertise"
WHERE
u.name like '%PAU%'
This way, any additional criteria as expressed in other answer provided shouldn't be too much an impact. The tables are setup by the criteria as if simultaneous, and if any are missing, they will be excluded from the result immediately instead of trying to do a sub-select counting for every entry (which I think might be the lag you are encountering).
So, each of your "required" criteria would take the same "JOIN" construct, and as you can see, I'm just incrementing the "alias" of the join instance.