Right query to limit results in search - php

My db structure is as follows: categories table
+-----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| parent_id | int(11) | NO | | NULL | |
| title | varchar(260) | NO | | NULL | |
| slug | varchar(260) | NO | | NULL | |
| custom | int(11) | NO | | NULL | |
+-----------+--------------+------+-----+---------+----------------+
category_to_content table
+-------------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| category_id | int(11) | NO | | NULL | |
| content_id | int(11) | NO | | NULL | |
+-------------+---------+------+-----+---------+----------------+
And content table:
+--------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| heading | varchar(200) | NO | | NULL | |
| subheading | text | NO | | NULL | |
| content_text | longtext | NO | | NULL | |
+--------------+--------------+------+-----+---------+----------------+
As you can see, typical adjacency list model in categories table, category_to_content is here because some categories share same content...
There are 4 levels currently, and 'level 0' categories are actually big/main categories (infobases in this case), and i want to limit search JUST to (sub)categories/content which belongs to: current level 0 category (parent_id of level 0 categories is, well.. 0).
I'm using Codeigniter, query builder class (not mandatory, i can use plain sql query, too, if needed), and returned query looks like this:
SELECT `heading`, `slug`
FROM `content`
JOIN `category_to_content` ON `category_to_content`.`content_id` = `content`.`id`
JOIN `categories` ON `categories`.`id`=`category_to_content`.`category_id`
WHERE `categories`.`id` IN('1,2,3') // current level 0 category //subcategories id's, 12,3, just as example...
AND (
`content`.`heading` LIKE '%pod%'
OR `content`.`subheading` LIKE '%pod%'
)
LIMIT 10
However, this doesn't return desired results in all cases. If i remove parentheses from AND portion of query, i got results, BUT from all main categories. With parentheses, no results, in most cases...
E.g. when search term is 'about' i got proper results, when i try with 'podiatry' - no results.
Sample data:
Just to clarify - IN portion works fine, i got proper level 0 subcategories ids on every page.
If you can't see my error (probably something trivial for MySQL gurus), i can send sample data (export from PhPMyAdmin).
EDIT1: Example: When i type 'pod' i would like to get slug from categories table -> 'podiatry', and heading from 'content' table - 'What is podiatry'.
Interesting, when i type 'about' i get desired results (from desired level 0 SUBcategory). DB export - ASAP, so you can test by your self...
EDIT2: link to sample data: http://pastebin.com/YHziZH8f
EDIT3: in this case, i made 3 main/level0 categories (podiatry - cat id=1, infobase2, cat id=208, test3, cat id=213). They all (almost) have some subcategories, and content related to them. If we are on 'podiatry' page - current level 0 category subcategories should be searched. I want to limit search just to these subcategories (current level 0 subcategories). So, if i am on 'podiatry' page, i need 'about' content just for podiatry category (no 'about' from/for the rest of categories).
EDIT4:
Example of successful query:
SELECT `heading`, `slug`
FROM `content`
JOIN `category_to_content` ON `category_to_content`.`content_id` = `content`.`id`
JOIN `categories` ON `categories`.`id`=`category_to_content`.`category_id`
WHERE `categories`.`id` IN('209,210,211,212')
AND (
`content`.`heading` LIKE '%about%'
OR `content`.`subheading` LIKE '%about%'
)
LIMIT 10
I got: heading 'About Infobase2', and 'about-infobase2' slug.
Second search term:
SELECT `heading`, `slug`
FROM `content`
JOIN `category_to_content` ON `category_to_content`.`content_id` = `content`.`id`
JOIN `categories` ON `categories`.`id`=`category_to_content`.`category_id`
WHERE `categories`.`id` IN('209,210,211,212')
AND (
`content`.`heading` LIKE '%categ%'
OR `content`.`subheading` LIKE '%categ%'
)
LIMIT 10
No results. If you test sample data you will see that there is Category 2 (id=210) in categories table, referenced as: category_id 210, content id 48, in category_to_content table, and Category 2 heading in content table (id=48)... What i'm doing wrong... I can't get that data. :(

Try this: first I join all tables needed
SELECT content.heading, categories.slug
FROM content
INNER JOIN category_to_content ctc ON ctc.content_id = content.id
INNER JOIN categories ON categories.id = ctc.category_id
Then we need to keep only "level 1 categories" (direct subcategories of level 0 categories): I used a subquery instead of listing needed ids. It's more dynamic that way: if you add new categories, no need to update the query.
WHERE categories.parent_id IN (SELECT id FROM categories WHERE parent_id = 0)
Finally, search by keyword.
AND (content.heading LIKE '%pod%' OR content.subheading LIKE '%pod%')
(I'm not sure wether you want to add OR categories.slug LIKE '%pod%' in that part of your query or not.)
And you can add LIMIT 10 at the end if needed.
A word about what was wrong in your query, it's this line:
WHERE `categories`.`id` IN('1,2,3')
it should have been IN(1,2,3) or IN('1','2','3') (see the quotes)
"level 1" categories are not only 1, 2 and 3

Related

how to return something even if inner join doesnt match

i have two tables
Cursos
+-------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| curso | varchar(255) | NO | | NULL | |
+-------+------------------+------+-----+---------+----------------+
Trienios
+--------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| data_trienio | varchar(255) | NO | | NULL | |
| curso_id | int(11) | NO | | NULL | |
| oe_id | int(11) | NO | | NULL | |
+--------------+------------------+------+-----+---------+----------------+
those tables are connected through a relationship (as seen through curso_id), and i want to make a query where i retrieve the curso records and the number of trienio records related to each one of them
so i've done this query in laravel
$curso = Curso::select([
'cursos.curso',
\DB::raw('count(trienios.curso_id) as count')
])->join('trienios', 'trienios.curso_id', '=', 'cursos.id')
->groupBy('trienios.curso_id');
which translates to this
select `cursos`.`curso`,
count(trienios.curso_id) as count
from `cursos`
inner join `trienios`
on `trienios`.`curso_id` = `cursos`.`id`
group by `trienios`.`curso_id`
and it gets me the number of cursos with trienios related to them. HOWEVER, it only gives those who have a number of trienios related to them. the other ones who dont have trienios related to them are not queried, and i want to query them. so, how do i solve this issue ?
Use leftJoin(), select cursos.id, and group by cursos.id:
$curso = Curso::select([
'cursos.id',
DB::raw('count(trienios.curso_id) as count')
])->leftJoin('trienios', 'trienios.curso_id', '=', 'cursos.id')
->groupBy('cursos.id');
This is the query I want to run:
SELECT c.id,
COUNT(t.curso_id) AS count
FROM cursos c
LEFT JOIN trienios t
ON t.curso_id = c.id
GROUP BY c.id
The LEFT JOIN should prevent records from cursos from being dropped even if they have no matching counterparts in trienios. As for the error you were seeing, you were selecting cursos.curso, which is not an aggregate and which does not appear in your GROUP BY clause. When MySQL is in only_full_group_by this is not allowed, hence I changed the select clause to cursos.id.

Optimize SQL query to fetch file names

I've two tables, the first table contains information on the ideas submitted by user and the second table contains information on the file attachments that are part of the idea. An idea submitted by the user can have 0 or any number of attachments.
Table 1:
-------------------------------------
Id Title Content Originator
-------------------------------------
1 aaa bbb John
2 ccc ddd Peter
--------------------------------------
Table 2:
---------------------------------------------
Id Idea_id Attachment_name
---------------------------------------------
1 1 file1.doc
2 1 file2.doc
3 1 file3.doc
4 2 user2.doc
---------------------------------------------
Table 1 primary key is Id and table 2 primary key is Id as well. Idea_id is the foreign key in table 2 mapping to table 1 Id.
I'm trying to display all the ideas, along with their attachments in a html page. So what I've been doing is: get all the ideas from Table 1 and then for each idea record, retrieve the attachment records from table 2.It seems to be extremely inefficient. Could this be optimized so that I can retrieve idea records and their corresponding attachment records in one query?
I tried with left outer join(Table 1 left outer join Table 2) but that would give me three records for Id = 1 in table 1. I'm looking for a SQL query to club idea detail and attachment names in 1 row to make HTML page processing efficient. Otherwise, What would be the best solution for this?
If you want to get all attachments along with all ideas, you may use GROUP_CONCAT. such as
SELECT *, (SELECT GROUP_CONCAT(attachment_name separator ', ') FROM TABLE2 WHERE idea_id = TABLE1.id) attachments FROM TABLE1
I probably missed the point but a left join should bring back all the records
create table `ideas` (
`id` int(10) unsigned not null auto_increment,
`title` varchar(50) not null,
`content` varchar(50) not null,
`originator` varchar(50) not null,
primary key (`id`)
)
engine=innodb
auto_increment=3;
create table `attachments` (
`id` int(10) unsigned not null auto_increment,
`idea_id` int(10) unsigned not null default '0',
`attachment` varchar(50) not null default '0',
primary key (`id`),
index `idea_id` (`idea_id`),
constraint `fk_ideas` foreign key (`idea_id`) references `ideas` (`id`) on update cascade on delete cascade
)
engine=innodb
auto_increment=5;
mysql> select * from ideas;
+----+----------------+-----------+-----------------+
| id | title | content | originator |
+----+----------------+-----------+-----------------+
| 1 | Flux capacitor | Rubbish | Doc |
| 2 | Star Drive | Plutonium | Professor Frink |
+----+----------------+-----------+-----------------+
mysql> select * from attachments;
+----+---------+------------------------------+
| id | idea_id | attachment |
+----+---------+------------------------------+
| 1 | 1 | Flux capacitor schematic.jpg |
| 2 | 1 | Sensors.docx |
| 3 | 1 | fuel.docx |
| 4 | 2 | plans.jpg |
+----+---------+------------------------------+
mysql> select * from ideas i
-> left outer join attachments a on a.idea_id=i.id;
+----+----------------+-----------+-----------------+------+---------+------------------------------+
| id | title | content | originator | id | idea_id | attachment |
+----+----------------+-----------+-----------------+------+---------+------------------------------+
| 1 | Flux capacitor | Rubbish | Doc | 1 | 1 | Flux capacitor schematic.jpg |
| 1 | Flux capacitor | Rubbish | Doc | 2 | 1 | Sensors.docx |
| 1 | Flux capacitor | Rubbish | Doc | 3 | 1 | fuel.docx |
| 2 | Star Drive | Plutonium | Professor Frink | 4 | 2 | plans.jpg |
+----+----------------+-----------+-----------------+------+---------+------------------------------+

MYSQL query with 3 INNER JOINs

I'm trying to create a book-catalogue. I have 3 basic tables - books, authors, books_authors;
books
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| book_id | int(11) | NO | PRI | NULL | auto_increment |
| book_title | varchar(250) | NO | | NULL | |
+------------+--------------+------+-----+---------+----------------+
authors
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| author_id | int(11) | NO | PRI | NULL | auto_increment |
| author_name | varchar(250) | NO | | NULL | |
+-------------+--------------+------+-----+---------+----------------+
books_authors
+-----------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+---------+------+-----+---------+-------+
| book_id | int(11) | NO | MUL | NULL | |
| author_id | int(11) | NO | MUL | NULL | |
+-----------+---------+------+-----+---------+-------+
I have a query that takes the book name and all authors for each book and displays the result:
$booksAndAuthors = mysqli_query($connection, 'SELECT * FROM books LEFT JOIN books_authors ON books.book_id=books_authors.book_id LEFT JOIN authors ON authors.author_id=books_authors.author_id');
It returns:
Book Name -> Author 1, Author 2
Book Name 2 -> Author 3, Author 2
And so on.
And I have another query that it's:
$booksAndAuthors = mysqli_query($connection, 'SELECT * FROM books_authors as ba
INNER JOIN books as b ON ba.book_id=b.book_id
INNER JOIN books_authors as booaut ON booaut.book_id=ba.book_id
INNER JOIN authors as a ON booaut.author_id=a.author_id
WHERE ba.author_id=' . $author_id);
When I click over an author (authors are links), the query returns all books of an author the opposite; The queries all work;
My Question is:
Could someone explain to me why I'm comparing a table with itself. Just explain for dummie like myself. I want to understand the action that is done by this query, with words or something else.
*If my question isn't properly asked! Edit me!
*Regards!
A book can have more than one author. The point of the self-join is to find the other authors for the book.
FROM books_authors as ba
...
INNER JOIN books_authors as booaut ON booaut.book_id=ba.book_id
...
WHERE ba.author_id=42
The join picks up any author who co-authored a book with author 42.
Another way to write the query:
FROM books_authors as ba
...
WHERE EXISTS
(
SELECT *
FROM books_authors ba2
WHERE ba2.book_id = ba.book_id
and ba2.author_id = 42
)
This says, select all rows where a matching book_authors entry exists for author 42.
It seems a self join ( joining the table with itself ) is unnecessary here since you are picking the same rows.
Usually self joins are performed to join two different rows in a table. For example, if you have a table with monthly account balances for example
acount_id |as_of_date | balance_amount
-----------|---------------------------
12213 |2014-01-01 | 10000
12213 |2014-02-01 | 20000
12213 |2014-03-01 | 25000
Let's say the table name is monthly_account_balances
Now you want to compute the difference between monthly balances
For instance, between February and January the difference is 20000 - 10000 = 10000
And between March and February the difference is 25000 - 20000 = 5000
And the output you need is
acount_id |as_of_date | balance_amount|difference
-----------|-----------|---------------|-------------
12213 |2014-01-01 | 10000 | null
12213 |2014-02-01 | 20000 | 10000
12213 |2014-03-01 | 25000 | 5000
Here you do a self join as follows:
select a.*, b.balance_amount - a.balance_amount as difference
from monthly_account_balances a
inner join monthly_account_balances b on a.account_id = b.account_id
and a.as_of_date + interval '1 month' = b.as_of_date
Notice the date condition. It's comparing two different records with same id but different as_of_date. Self join is useful in such situations.
However in your case you are just joining on id and I see no point in doing that unless I am missing something

Appending matching column onto each result row

I'm not a SQL veteran so please excuse me if this is obvious; I'm learning.
I have two tables in a database for a wedding invite system; guests and invites. One invite (invitation) can contain many guests.
For purposes of creating a mail merge for the invites, I'm trying to select the firstname and lastname from the guest table, where the guest's inviteID is the same as the others; effectively returning on row of data containing the inviteID's data and a column each for the names of the guests.
My problem is I can return the data, but across multiple rows which won't work for the mail-merge. I can create a PHP script to do a work-around, but I would like to learn how this could be achieved in pure SQL.
Can anybody shed some light? Can this be done? Is this sheer madness?
Hoping to achieve:
*************************** 1. row ***************************
inviteID: 39
inviteURLSlug: thewinnetts
....
guestFirstName1: Sid
guestSurname1: Winnett
guestFirstName2: Claire
guestSurname2: Winnett
'invite' table:
+---------------------+--------------+
| Field | Type |
+---------------------+--------------+
| inviteID | int(11) |
| inviteURLSlug | varchar(64) |
| inviteQRValue | varchar(255) |
| inviteQRImageURL | varchar(255) |
| inviteAddress1 | varchar(32) |
| inviteAddress2 | varchar(32) |
| inviteAddress3 | varchar(32) |
| inviteCity | varchar(32) |
| inviteCounty | varchar(32) |
| inviteCountry | varchar(32) |
| invitePostcode | varchar(16) |
| inviteDateSend | datetime |
| inviteDateResponded | datetime |
| inviteCreated | datetime |
| inviteUpdated | timestamp |
+---------------------+--------------+
'guest' table:
+-------------------+--------------+
| Field | Type |
+-------------------+--------------+
| guestID | int(11) |
| inviteID | int(11) |
| guestFirstName | varchar(32) |
| guestSurname | varchar(32) |
| guestSide | varchar(8) |
| guestAttending | tinyint(1) |
| guestEmail | varchar(255) |
| guestPhone | varchar(32) |
| guestMobile | varchar(16) |
| guestAddress1 | varchar(32) |
| guestAddress2 | varchar(32) |
| guestAddress3 | varchar(32) |
| guestCity | varchar(32) |
| guestCounty | varchar(32) |
| guestCountry | varchar(32) |
| guestPostCode | varchar(16) |
| guestProfilePhoto | varchar(64) |
| guestFoodVeg | tinyint(1) |
| guestFoodReq | varchar(255) |
| guestTwitter | varchar(15) |
| guestFacebook | varchar(32) |
| guestPlusone | int(1) |
| guestCreated | datetime |
| guestUpdated | timestamp |
+-------------------+--------------+
Failed Join attempt and cropped results sample:
SELECT * FROM guest INNER JOIN invite on guest.inviteID = invite.inviteID \G
*************************** 64. row ***************************
guestID: 72
inviteID: 39
guestFirstName: Claire
guestSurname: Winnett
.......
*************************** 65. row ***************************
guestID: 73
inviteID: 39
guestFirstName: Sid
guestSurname: Winnett
.......
To achieve those results will require additional processing in PHP.
To do so, modify your query to this:
SELECT i.*,
g.guestFirstName,
g.guestSurname
GROUP_CONCAT(DISTINCT g.guestFirstName, '|', g.guestSurname ORDER BY g.guestSurname DESC) AS names
FROM invite i
INNER JOIN guest g ON i.inviteID = g.inviteID;
making sure to return your results as an array. From there, convert the concatenated results into an array then iterate through them to set the custom key name and corresponding value (assumes your query results are in array format with the variable name $queryresults):
$names = explode(',' $queryresults['names']);
unset($queryresults['names']);
$i = 1;
foreach ($names as $name) {
$split_name = str_split("|", $name);
$queryresults['guestFirstName' . $i] = $split_name[0];
$queryresults['guestSurname' . $i] = $split_name[1];
$i++;
}
This should give you your desired results.
SQL is not good at this sort of thing. It wants to work in sets, where all the items in the set are the same. You are asking for two items within the set to be smooshed together. Here is a possible solution:
SELECT
t1.inviteID,
t1.guestID,
t1.guestFirstName,
t1.guestSurname,
t2.guestFirstName AS guestFirstName2,
t2.guestSurname AS guestSurname2
FROM
guests as t1
LEFT OUTER JOIN
guests as t2
ON t1.inviteID = t2.inviteID
AND t1.guestID <> t2.guestID
WHERE
t1.guestID = (select min(t3.guestID) from guests as t3 where t3.inviteID = t1.inviteID)
;
The guests table is used twice, once to provide the first guest on each invite, and a second time, using a LEFT OUTER JOIN to provide the second. The LEFT OUTER ensures you still get the first even if there isn't a second. The other criteria are there to ensure you don't join a row to itself, and you only output the firsts, with the seconds attached (and not the other way around).
Here is a sample fiddle (in MySQL)
Is something like this what you are looking for?
Updated after your comment.
SELECT GROUP_CONCAT( DISTINCT `guest`.`guestFirstName`,`guest`.`guestSurname`, `invite`.`inviteURLSlug` ) FROM `guest`
LEFT JOIN `invite` ON `invite`.`inviteID` = `guest`.`inviteID`
WHERE `invite`.`inviteID` = 5
I just made two simple tables with minimal information for testing, but is easily expandable to whatever you have/want.
You are going to have some fields repeated since the number of rows is going to be related to the number of guests.

Perform delete while inner joining table

I want to delete an entry in my PAGES table. Upon deletion of it, it will cascade to OBJECTS table. No worries in here, it's working if I delete the entry by using simple DELETE. However, I need to specify some conditions:
PAGES table
+--------------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------------------+--------------+------+-----+---------+----------------+
| page_id | int(11) | NO | PRI | NULL | auto_increment |
| users_id | int(11) | NO | MUL | NULL | |
| page_value | varchar(20) | NO | UNI | NULL | |
+--------------------------+--------------+------+-----+---------+----------------+
OBJECTS table
+----------------------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------------------+-------------+------+-----+---------+----------------+
| objects_id | int(11) | NO | PRI | NULL | auto_increment |
| page_id | int(11) | NO | MUL | NULL | |
| objects_name | varchar(50) | NO | | NULL | |
| objects_avail | varchar(20) | NO | | NULL | |
+----------------------------+-------------+------+-----+---------+----------------+
If objects_avail == "ALL", I must not include that entry in cascade delete. I came up with this SQL query but got an error:
$query = "
DELETE FROM pages AS p
INNER JOIN objects AS o ON p.page_id = o.page_id
WHERE p.page_id = ?
AND p.users_id = ?
AND p.page_value = ?
AND o.objects_avail != ?";
The error thrown:
["42000",1064,"You have an error in your SQL syntax; check the manual
that corresponds to your MySQL server version for the right syntax to
use near 'AS p INNER JOIN objects AS o ON p.page_id = o.page_id WHER'
at line 1"]
Example value for the PDO placeholders:
$params = array(81,5,"main page","ALL");
where all of this is valid and I'm sure this is not where the problem is.
I doubt or prettry sure I'm missing some in my query, any suggestions please?
For an inner join UPDATE or DELETE, you need to specify which of the tables you actually want to delete explicitly, or else the parser won't know what you mean. You can choose 1 or more tables to delete from. In your case, it makes sense to just delete p, the alias for pages.
DELETE p
FROM pages AS p
INNER JOIN objects AS o ON p.page_id = o.page_id
WHERE
p.page_id = ? AND
p.users_id = ? AND
p.page_value = ? AND
o.objects_avail != ?
The only line I changed was DELETE became DELETE p

Categories