Showing posts with label Mysql Query. Show all posts
Showing posts with label Mysql Query. Show all posts

Friday, 2 November 2018

SQL select only rows with max value on a column

I have this table for documents (simplified version here):
+------+-------+--------------------------------------+
| id   | rev   | content                              |
+------+-------+--------------------------------------+
| 1    | 1     | ...                                  |
| 2    | 1     | ...                                  |
| 1    | 2     | ...                                  |
| 1    | 3     | ...                                  |
+------+-------+--------------------------------------+
How do I select one row per id and only the greatest rev?
With the above data, the result should contain two rows: [1, 3, ...] and [2, 1, ..]. I'm using MySQL.
Currently I use checks in the while loop to detect and over-write old revs from the resultset. But is this the only method to achieve the result? Isn't there a SQL solution?

 Answers


At first glance...

All you need is a GROUP BY clause with the MAX aggregate function:
SELECT id, MAX(rev)
FROM YourTable
GROUP BY id

It's never that simple, is it?

I just noticed you need the content column as well.
This is a very common question in SQL: find the whole data for the row with some max value in a column per some group identifier. I heard that a lot during my career. Actually, it was one the questions I answered in my current job's technical interview.
It is, actually, so common that community has created a single tag just to deal with questions like that: greatest-n-per-group.
Basically, you have two approaches to solve that problem:

Joining with simple group-identifier, max-value-in-group Sub-query

In this approach, you first find the group-identifier, max-value-in-group (already solved above) in a sub-query. Then you join your table to the sub-query with equality on both group-identifier and max-value-in-group:
SELECT a.id, a.rev, a.contents
FROM YourTable a
INNER JOIN (
    SELECT id, MAX(rev) rev
    FROM YourTable
    GROUP BY id
) b ON a.id = b.id AND a.rev = b.rev

Left Joining with self, tweaking join conditions and filters

In this approach, you left join the table with itself. Equality, of course, goes in the group-identifier. Then, 2 smart moves:
  1. The second join condition is having left side value less than right value
  2. When you do step 1, the row(s) that actually have the max value will have NULL in the right side (it's a LEFT JOIN, remember?). Then, we filter the joined result, showing only the rows where the right side is NULL.
So you end up with:
SELECT a.*
FROM YourTable a
LEFT OUTER JOIN YourTable b
    ON a.id = b.id AND a.rev < b.rev
WHERE b.id IS NULL;

Conclusion

Both approaches bring the exact same result.
If you have two rows with max-value-in-group for group-identifier, both rows will be in the result in both approaches.
Both approaches are SQL ANSI compatible, thus, will work with your favorite RDBMS, regardless of its "flavor".
Both approaches are also performance friendly, however your mileage may vary (RDBMS, DB Structure, Indexes, etc.). So when you pick one approach over the other, benchmark. And make sure you pick the one which make most of sense to you.



Yet another solution is to use a correlated subquery:
select yt.id, yt.rev, yt.contents
    from YourTable yt
    where rev = 
        (select max(rev) from YourTable st where yt.id=st.id)
Having an index on (id,rev) renders the subquery almost as a simple lookup...
Following are comparisons to the solutions in @AdrianCarneiro's answer (subquery, leftjoin), based on MySQL measurements with InnoDB table of ~1million records, group size being: 1-3.
While for full table scans subquery/leftjoin/correlated timings relate to each other as 6/8/9, when it comes to direct lookups or batch (id in (1,2,3)), subquery is much slower then the others (Due to rerunning the subquery). However I couldnt differentiate between leftjoin and correlated solutions in speed.
One final note, as leftjoin creates n*(n+1)/2 joins in groups, its performance can be heavily affected by the size of groups...



I am flabbergasted that no answer offered SQL window function solution:
SELECT a.id, a.rev, a.contents
  FROM (SELECT id, rev, contents,
               ROW_NUMBER() OVER (PARTITION BY id ORDER BY rev DESC) rank
          FROM YourTable) a
 WHERE a.rank = 1 
Added in SQL standard ANSI/ISO Standard SQL:2003 and later extended with ANSI/ISO Standard SQL:2008, window (or windowing) functions are available with all major vendors now. There are more types of rank functions available to deal with a tie issue: RANK, DENSE_RANK, PERSENT_RANK.



Something like this?
SELECT yourtable.id, rev, content
FROM yourtable
INNER JOIN (
    SELECT id, max(rev) as maxrev FROM yourtable
    WHERE yourtable
    GROUP BY id
) AS child ON (yourtable.id = child.id) AND (yourtable.rev = maxrev)



A third solution I hardly ever see mentioned is MySQL specific and looks like this:
SELECT id, MAX(rev) AS rev
 , 0+SUBSTRING_INDEX(GROUP_CONCAT(numeric_content ORDER BY rev DESC), ',', 1) AS numeric_content
FROM t1
GROUP BY id
Yes it looks awful (converting to string and back etc.) but in my experience it's usually faster than the other solutions. Maybe that just for my use cases, but I have used it on tables with millions of records and many unique ids. Maybe it's because MySQL is pretty bad at optimizing the other solutions (at least in the 5.0 days when I came up with this solution).
One important thing is that GROUP_CONCAT has a maximum length for the string it can build up. You probably want to raise this limit by setting the group_concat_max_len variable. And keep in mind that this will be a limit on scaling if you have a large number of rows.
Anyway, the above doesn't directly work if your content field is already text. In that case you probably want to use a different separator, like \0 maybe. You'll also run into the group_concat_max_len limit quicker.



How about this:
select all_fields.*  
from  (select id, MAX(rev) from yourtable group by id) as max_recs  
left outer join yourtable as all_fields  
on max_recs.id = all_fields.id



If you have many fields in select statement and you want latest value for all of those fields through optimized code:
select * from
(select * from table_name
order by id,rev desc) temp
group by id 



NOT mySQL, but for other people finding this question and using SQL, another way to resolve the greatest-n-per-group problem is using Cross Apply in MS SQL
WITH DocIds AS (SELECT DISTINCT id FROM docs)

SELECT d2.id, d2.rev, d2.content
FROM DocIds d1
CROSS APPLY (
  SELECT Top 1 * FROM docs d
  WHERE d.id = d1.id
  ORDER BY rev DESC
) d2


I like to do this by ranking the records by some column. In this case, rank rev values grouped by id. Those with higher rev will have lower rankings. So highest rev will have ranking of 1.
select id, rev, content
from
 (select
    @rowNum := if(@prevValue = id, @rowNum+1, 1) as row_num,
    id, rev, content,
    @prevValue := id
  from
   (select id, rev, content from YOURTABLE order by id asc, rev desc) TEMP,
   (select @rowNum := 1 from DUAL) X,
   (select @prevValue := -1 from DUAL) Y) TEMP
where row_num = 1;
Not sure if introducing variables makes the whole thing slower. But at least I'm not querying YOURTABLE twice.



here is another solution hope it will help someone
Select a.id , a.rev, a.content from Table1 a
inner join 
(SELECT id, max(rev) rev FROM Table1 GROUP BY id) x on x.id =a.id and x.rev =a.rev



SELECT * FROM Employee where Employee.Salary in (select max(salary) from Employee group by Employe_id) ORDER BY Employee.Salary



Many, if not all, of the other answers here are fine for small datasets. For scaling, more care is needed. See here.
It discusses multiple faster ways to do groupwise max and top-N per group.



I used the below to solve a problem of my own. I first created a temp table and inserted the max rev value per unique id.
CREATE TABLE #temp1
(
    id varchar(20)
    , rev int
)
INSERT INTO #temp1
SELECT a.id, MAX(a.rev) as rev
FROM 
    (
        SELECT id, content, SUM(rev) as rev
        FROM YourTable
        GROUP BY id, content
    ) as a 
GROUP BY a.id
ORDER BY a.id
I then joined these max values (#temp1) to all of the possible id/content combinations. By doing this, I naturally filter out the non-maximum id/content combinations, and am left with the only max rev values for each.
SELECT a.id, a.rev, content
FROM #temp1 as a
LEFT JOIN
    (
        SELECT id, content, SUM(rev) as rev
        FROM YourTable
        GROUP BY id, content
    ) as b on a.id = b.id and a.rev = b.rev
GROUP BY a.id, a.rev, b.content
ORDER BY a.id



select * from yourtable
group by id
having rev=max(rev);



SELECT * FROM t1 ORDER BY rev DESC LIMIT 1;

Saturday, 8 September 2018

MySQL: Find records in one table that are not in another

A couple of weeks back I posted how to find records in one table that are not in another with MySQL and received an email with a more efficient way of approaching the same problem and therefore revise my original post with his suggestion.

Example tables

The examples below use three tables as follows:
content: contains the content pages for a website. The primary key is content_id.
tags: the "tags" that a page is tagged with. The primary key is tag_id.
content_to_tags: a table that creates a many-to-many relationship between the above two tables; a page can belong to multiple tags.

Example 1

The first example finds records in content_to_tags that have no associated record in content. This could have happened if the application deleted a record from content but didn't delete the associated records from content_to_tags:
SELECT * 
FROM content_to_tags c2t
WHERE NOT EXISTS (
 SELECT * 
 FROM content c 
 WHERE c.content_id = c2t.content_id
)

Example 2

This is the same as for the first example but comparing tags with content_to_tags. Again, this might have happened if records were deleted from tags but not their associated records from content_to_tags:
SELECT * 
FROM content_to_tags c2t
WHERE NOT EXISTS (
 SELECT * 
 FROM tags t
 WHERE t.tag_id = c2t.tag_id
)

Benchmarking Examples 1 and 2

In my orginal post I used a LEFT JOIN from content_to_tags to content. I benchmarked my original query compared to this query on a small content table with 1000 records, a content_to_tags table with 2000 records and a tags table with 100 records.
Using NOT EXISTS was just over two times faster, and I would assume that as the tables get populated with more records the difference exponential.

A note about ACID compliance and foreign keys

Note that in a properly ACID compliant database with foreign key constraints the first two examples shouldn't actually return any data, because it wouldn't be possible to delete records from tags/content if there are associated records present in the content_to_tags table.
Examples 3 and 4 are looking at something slightly different: finding content that's not tagged at all, and finding tags that are not tagged to any content.

Example 3

The next example looks for records in content where there are no associated records in content_to_tags. This is useful if you want to find any untagged posts:
SELECT * 
FROM content c
WHERE NOT EXISTS (
 SELECT * 
 FROM content_to_tags c2t
 WHERE c.content_id = c2t.content_id
)

Example 4

The final example is the same as the above but to find tags that have no associated records in content_to_tags. This is useful for finding tags that have no content, or in another context (with different table names etc) categories that have no products in them:
SELECT * 
FROM tags t
WHERE NOT EXISTS (
 SELECT * 
 FROM content_to_tags c2t
 WHERE t.tag_id = c2t.tag_id
)

Benchmarking Examples 3 and 4

The difference between my original LEFT JOIN and the NOT EXISTS queries here was minimal for examples 3 and 4 on the example tables although I would imagine as they become bigger the difference would grow. NOT EXISTS was once again faster that the LEFT JOIN syntax.

Related posts:

Thursday, 6 September 2018

Run a single MySQL query from the command line

The MySQL command line utility allows you to run queries and view query results etc from an interactive command prompt. It is also possible to run a single query from the command line without actually going into the interactive prompt. This post looks at how to do this.
As an example, I needed to load some data into a new database the other day from a dump from another server. Normally I'd do something like this:
mysql -u [username] -p somedb < somedb.sql
The database didn't actually exist so I got this error:
ERROR 1049 (42000): Unknown database 'somedb'
The obvious solution is to create the database and then run the same command to load the data. I could either do this by adding the "create database somedb" to the start of the text file I was loading, fire up the MySQL command line, run the command and exit back to the bash shell, or use the -e flag to execute a single query.
So to run a single MySQL query from your regular shell instead of from MySQL's interactive command line you would do this:
mysql -u [username] -p [dbname] -e [query]
In my case I wanted to create the database so it looked like this (note I didn't need to specify a database because my query didn't affect a specific database):
mysql -u [username] -p -e "create database somedb"
You can run any valid queries against any databases you have permissions for, in the same way as running the query from the MySQL command line. Any output will appear on your shell's command line. For example:
 $ mysql -u root -p somedb -e "select * from mytable"
Enter password:
+------------+-------------+----------------------------+
| mytable_id | category_id | name                       |
+------------+-------------+----------------------------+
|          1 |           1 | Lorem ipsum dolor sit amet |
|          2 |           1 | Ut purus est               |
|          3 |           2 | Leo sed condimentum semper |
|          4 |           2 | Donec velit neque          |
|          5 |           3 | Maecenas ullamcorper       |
+------------+-------------+----------------------------+
Update and insert queries do not output anything if they are successful (displaying errors if not successful), but select queries do as shown in the above example.

Related posts:

Running queries from the MySQL Command Line


The MySQL command line tool allows you to run queries and administer databases from the command line. In previous posts I have looked at the basics of using the MySQL command line tool and executing shell commands. In this post I will look at running queries and the output from these.

Running select queries

After you have logged into a database with the MySQL command line tool (covered in my using the MySQL command line tool post), you can run queries by simply typing them in at the command prompt. The query will not be executed until you either enter ; \g or \G and then press the <enter> key. This allows you to write a query across several lines and then execute it at the end by entering ; and then <enter>.
For example, if we have a table which stores country codes and names, we can query the data like so:
mysql> select country_code, name from countries order by name limit 0, 5;
The result set would then be displayed, showing something like below. It will be displayed in this horizontal format whether you use ; or \g to execute the query. We will look at what \G does in the next section below.
+--------------+----------------+
| country_code | name           |
+--------------+----------------+
| AF           | AFGHANISTAN    |
| AL           | ALBANIA        |
| DZ           | ALGERIA        |
| AS           | AMERICAN SAMOA |
| AD           | ANDORRA        |
+--------------+----------------+
5 rows in set (0.00 sec)

Displaying the result set vertically

If your query result contains a lot of columns then the data will wrap if it is too long. This can make it difficult to find the information you are looking for because the output can be quite messy. The MySQL command line tool allows you to show the output vertically which overcomes this issue, using \G to execute the query.
Running the same query as above with \G:
mysql> select country_code, name from countries order by name limit 0, 5\G
would look like this:
*************************** 1. row ***************************
country_code: AF
        name: AFGHANISTAN
*************************** 2. row ***************************
country_code: AL
        name: ALBANIA
*************************** 3. row ***************************
country_code: DZ
        name: ALGERIA
*************************** 4. row ***************************
country_code: AS
        name: AMERICAN SAMOA
*************************** 5. row ***************************
country_code: AD
        name: ANDORRA
5 rows in set (0.00 sec)

MySQL Command Line Tool History

The MySQL command line tool keeps a history of the SQL queries you have run. On a Linux/Unix machine this is stored in your home directory in the file .mysql_history. When you are in the MySQL command line tool, you can go back through this history by using the up and down arrow keys. The up arrow moves you back through the history and the down arrow forward through the history again.

Clearing the current query

If you navigate through the history but decide you don't want to run any of the queries, or if you've typed in a query that you decide you don't want to run after all, you don't need to use the backspace key to clear the query. Enter \c and then press <enter> and the query will be cleared without running.

Summary

The MySQL Command Line Tool is a useful way to run queries from the command line. It's easy to run select queries and display the results in either a horizontal or vertical format, and the queries run are kept in a history file which you can navigate through. If you don't want to run a particular query after all you can use the \c command to clear it. In the next post I will look at displaying database tables and table structure with the MySQL command line tool.


Related posts:

MySQL Query Cache

MySQL has a query cache which caches the results of SELECT queries, if enabled. This means that frequently used database queries will run much faster, because the data resultset will be read from the cache instead of having to run the query again. The MySQL query cache is available from MySQL 4.0.1. Whenever tables in the database are modified the relevant entries in the query cache are flushed so you can be certain that even with the query cache enabled only up to date data is returned.
You can tell if the query cache is enabled, and what parameters are set, by running the following query in MySQL:
SHOW VARIABLES LIKE '%query_cache%'
An example result for the above query is below, which shows that the query cache engine is available, but the query cache size is set to zero and therefore nothing will be cached, and the query cache engine will not actually be used.
+------------------------------+---------+
| Variable_name                | Value   |
+------------------------------+---------+
| have_query_cache             | YES     |
| query_cache_limit            | 1048576 |
| query_cache_min_res_unit     | 4096    |
| query_cache_size             | 0       |
| query_cache_type             | ON      |
| query_cache_wlock_invalidate | OFF     |
+------------------------------+---------+
It is possible to set the query_cache_size variable without actually restarting the MySQL server, by running the following SQL query. In this example, we are enabling a 50MB query cache.
SET GLOBAL query_cache_size = 50*1024*1024;
Running SHOW VARIABLES LIKE '%query_cache%' will now return the following:
+------------------------------+----------+
| Variable_name                | Value    |
+------------------------------+----------+
| have_query_cache             | YES      |
| query_cache_limit            | 1048576  |
| query_cache_min_res_unit     | 4096     |
| query_cache_size             | 52428800 |
| query_cache_type             | ON       |
| query_cache_wlock_invalidate | OFF      |
+------------------------------+----------+
This means a query cache will now be running, using 50MB of memory. However, the next time the MySQL server is restarted the setting will be lost and query cache will no longer be used. Next, we will look at how to make the change permanent.

Setting the MYSQL query cache size in my.cnf

In order to make the query_cache_size setting permanent, the MySQL server configuration file must be modified. These settings are stored in a file called my.cnf which is typically stored on a Linux system at /etc/my.cnf or sometimes at /etc/mysql/my.cnf. If it's not at either of those locations you can try running locate my.cnf or find / -name my.cnf, although note the latter command will take a while.
To enable the query cache with a 50MB query cache in MySQL, you would add the following line to the my.cnf file, under the [mysqld] section:
query-cache-size = 50M
The next time MySQL is restarted it will have the query cache enabled with the size specified.

MySQL Query Cache Speedup Example

Yesterday I posted an SQL query which works out the top selling categories in an ecommerce website. When I first ran this query on my local machine it was reasonably fast, but it took almost 4 seconds to run on the actual production server which is unacceptably slow. It had taken even longer but I optimized the query by adding appropriate indexes etc and only managed to get it down to 4 seconds.
After enabling the query cache, the first time the query was run it would still take almost 4 seconds, but subsequent queries for the same data took around 0.0001 seconds. This is much better and shows the power of the MySQL query cache.
The MySQL manual contains more information.

SQL query to work out top selling categories


 For some reason, I'd never set this up to query the information from the database, instead manually updating it periodically. Today I decided to actually get around to making it read the information from the database and have decided to share the SQL queries etc used to get the information for top sellers by category.
A summary of the relevant fields and tables are as follows (I've modified the tablenames and fieldnames from the actual ones to make them more generic to a regular ecommerce solution):
orders_header
order_id - the primary key
status - the order's status
order_date - the date the order was placed

orders_detail
order_id - value to join this table to orders_header
product_id - value to join this to products_to_categories

products_to_categories
product_id - the product's id, used to join to orders_detail
category_id the category's id, used to join to categories

categories
category_id - the primary key
name - the name of the category
url - the url for the category page
The end result required is to have a list of category names sorted in order from the most popular to the least popular. In my case, I want to show just the top 10 but the SQL query can be easily adjusted to show more. The actual database structure for my Linux CD Mall website allows a product (in this case a CD set or DVD) to belong to one category (a Linux or BSD distribution) so a category can only be counted once per orders_detail line. If a product can belong to more than one category then a category may be counted multiple times per orders_detail line, but this may be acceptable depending on the circumstances.
The query used to get the data for the previous month is as follows (assuming the current date is 11 December 2007, making last month November 2007):
SELECT c.name, c.url, COUNT(*)
FROM orders_header h
INNER JOIN orders_detail d ON h.order_id = d.order_id
INNER JOIN products_to_categories p2c ON d.product_id = p2c.product_id
INNER JOIN product p ON p2c.product_id = p.product_id
WHERE h.status >= 30
AND h.order_date >= '2007-11-01'
AND h.order_date <= '2007-11-30'
GROUP BY c.name, c.url
ORDER BY COUNT(*) DESC
LIMIT 10
In my orders_header table, a status of 30 means it has been paid and a status of 40 means it has been sent; we only want to include orders that have been paid or sent, hence the "WHERE h.status >= 30" condition.
When doing a query like this, you need to make sure all the columns in the joins and where conditions are indexed, otherwise the query may take a considerable amount of time. Adding indexes should make the queries run a lot faster. The actual query I use on my CD site is slightly more complex than the above, and was taking around 4 seconds to run, despite a lot of optimization. In the end I added a query cache to MySQL which fixed the issue for subsequent queries.

Wednesday, 5 September 2018

MySQL JOIN - & Get all the data from the right table even if there is no match advertisements

I have two tables, one with data (A) and other with categories (B) which is in one of those categories or uncategorized. I need to make simple stats, so I am using JOIN to get all data from table A and join it with table B.

What I need is, that even if there is no match in A, I want to show all categories from B on graph (with count = 0, but I need them to be shown).
So if there are no items with that category id, it does not show that category at all, but I need to show it, even if it has 0 items.
I tried LEFT JOIN, RIGHT JOIN, both of them with UNION and nothing worked.
Table A
id | some | irrelevant | data | category
-----------------------------------------
1  |      |            |      | 0
2  |      |            |      | 0
3  |      |            |      | 1
4  |      |            |      | 2
5  |      |            |      | 3
6  |      |            |      | 3
. . . . . . . . . . .

Table B
id | title  | color
---------------------
1  | Cat 1  | #FFFFFF
2  | Cat 2  | #FF00FF
3  | Cat 3  | #FF0000
4  | Cat 4  | #00FF00
5  | Cat 5  | #00FFFF
6  | Cat 6  | #000000
7  | Cat 7  | #FFFF00

With those data I want to get something like:
Array ( [category] => 0 [count] => 3 [title] => [color] => )
Array ( [category] => 1 [count] => 1 [title] => Cat 1 [color] => #FFFFFF )
Array ( [category] => 2 [count] => 1 [title] => Cat 2 [color] => #FF00FF )
Array ( [category] => 3 [count] => 2 [title] => Cat 3 [color] => #FF0000 )
Array ( [category] => 4 [count] => 0 [title] => Cat 4 [color] => #00FF00 )
Array ( [category] => 5 [count] => 0 [title] => Cat 5 [color] => #00FFFF )
Array ( [category] => 6 [count] => 0 [title] => Cat 6 [color] => #000000 )
Array ( [category] => 7 [count] => 0 [title] => Cat 7 [color] => #FFFF00 )

But thats what I actually get:
Array ( [category] => 0 [count] => 3 [title] => [color] => )
Array ( [category] => 1 [count] => 1 [title] => Cat 1 [color] => #FFFFFF )
Array ( [category] => 2 [count] => 1 [title] => Cat 2 [color] => #FF00FF )
Array ( [category] => 3 [count] => 2 [title] => Cat 3 [color] => #FF0000 )

This is the call:
"SELECT category, COUNT(*) AS count, title, color FROM A LEFT JOIN B ON B.id=A.category GROUP BY category"

PS.: Once more, I trued RIGHT join too and UNION of RIGHT and LEFT join. Also, the names of tables are really long, cant really use them in call as A.category etc.
Is there some way to do that without using two SQL calls and two loops? I really don't want to do that.
Thanks.

And what about this :
MySQL 5.5.32 Schema Setup:
CREATE TABLE TableA
    (`id` int, `category` int)
;

INSERT INTO TableA
    (`id`, `category`)
VALUES
    (1, 0),
    (2, 0),
    (3, 1),
    (4, 2),
    (5, 3),
    (6, 3)
;

CREATE TABLE TableB
    (`id` int, `title` varchar(5), `color` varchar(7))
;

INSERT INTO TableB
    (`id`, `title`, `color`)
VALUES
    (1, 'Cat 1', '#FFFFFF'),
    (2, 'Cat 2', '#FF00FF'),
    (3, 'Cat 3', '#FF0000'),
    (4, 'Cat 4', '#00FF00'),
    (5, 'Cat 5', '#00FFFF'),
    (6, 'Cat 6', '#000000'),
    (7, 'Cat 7', '#FFFF00')
;

Query 1:
SELECT TableB.id as category, count(distinct TableA.id) as count,title,color
FROM
(SELECT * FROM TableB
UNION
SELECT 0 as id, '' as title, '' as color) AS TableB
LEFT OUTER JOIN TableA on TableB.id = TableA.category
GROUP BY TableB.id, title, color
ORDER BY TableB.id

| CATEGORY | COUNT | TITLE |   COLOR |
|----------|-------|-------|---------|
|        0 |     2 |       |         |
|        1 |     1 | Cat 1 | #FFFFFF |
|        2 |     1 | Cat 2 | #FF00FF |
|        3 |     2 | Cat 3 | #FF0000 |
|        4 |     0 | Cat 4 | #00FF00 |
|        5 |     0 | Cat 5 | #00FFFF |
|        6 |     0 | Cat 6 | #000000 |
|        7 |     0 | Cat 7 | #FFFF00 |

Also, instead of using UNION SELECT 0 as id, '' as title, '' as color, you can add a category with id 0 in your TableB