简体   繁体   中英

UNION loses functional dependence on GROUP BY columns

I'm trying to implement a product search form that uses MySQL 5.7.18-0ubuntu0.16.04.1 as its backend. I want the user to be able to type a SKU prefix, a title prefix, or an entire EAN or UPC barcode, and have the results be the product ID, SKU, title, and a newline-separated list of the product's barcodes. But when I UNION together the results from queries to match each possible table where a product might be found, somehow MySQL loses track of the functional dependence , causing the GROUP BY to fail with error 1055.

To simplify the problem for an MCVE, I have removed the part that searches by supplier SKU, which resembles the search by barcode, and the part that adds quantity across purchase orders, which is similar to the part that adds newline-separated barcodes.

-- ONLY_FULL_GROUP_BY is enabled by default in MySQL 5.7.5+ but
-- is disabled by default in rextester
SET SESSION SQL_MODE = 'STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY';
SHOW VARIABLES LIKE 'version';

-- These commands need to be run on a database where you have
-- privileges including CREATE TABLE.
CREATE DATABASE IF NOT EXISTS this_mcve;
USE this_mcve;

-- Now create some tables to query
DROP TABLE IF EXISTS t_products, t_barcodes;

CREATE TABLE t_products (
  skuid INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  sku VARCHAR(40) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL UNIQUE,
  title VARCHAR(80) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
  INDEX (title(10))
);
CREATE TABLE t_barcodes (
  barcode VARCHAR(15) CHARACTER SET ascii COLLATE ascii_general_ci PRIMARY KEY,
  skuid INTEGER UNSIGNED NOT NULL,
  INDEX (skuid)  -- in production this is a FOREIGN KEY
);

INSERT INTO t_products (skuid, sku, title) VALUES
(1, 'SKU1', 'title 1'),
(2, 'SKU2', 'title 2'),
(3, 'SKU3', 'title 3');

INSERT INTO t_barcodes (barcode, skuid) VALUES
('BC1FORSKU1', 1),
('BC2FORSKU1', 1),
('BC1FORSKU2', 2),
('BC2FORSKU2', 2),
('BC1FORSKU3', 3),
('BC2FORSKU3', 3);

-- WORKING: Get product ID, SKU, title, and barcodes where the SKU
-- prefix or title prefix matches.  In the production code, each
-- 'SK%' or the like is a PDO placeholder.
SELECT pr.skuid, pr.sku, pr.title,
GROUP_CONCAT(bc.barcode SEPARATOR '
') AS barcodes
FROM (
(SELECT DISTINCT skuid, sku, title
FROM t_products
WHERE sku LIKE 'SK%' OR title LIKE 'SK%')
) AS pr
LEFT JOIN t_barcodes AS bc ON bc.skuid = pr.skuid
GROUP BY pr.skuid;

-- WORKING: Look up all barcodes of products where one barcode
-- matches the query string
SELECT pr.skuid, pr.sku, pr.title,
GROUP_CONCAT(bc.barcode SEPARATOR '
') AS barcodes
FROM (
(SELECT DISTINCT t_products.skuid, t_products.sku, t_products.title
FROM t_products
INNER JOIN t_barcodes ON t_products.skuid = t_barcodes.skuid
WHERE t_barcodes.barcode = 'BC1FORSKU1')
) AS pr
LEFT JOIN t_barcodes AS bc ON bc.skuid = pr.skuid
GROUP BY pr.skuid;

-- But this statement gives error 1055:
-- Expression #2 of SELECT list is not in GROUP BY clause and
-- contains nonaggregated column 'pr.sku' which is not functionally
-- dependent on columns in GROUP BY clause; this is incompatible
-- with sql_mode=only_full_group_by
SELECT pr.skuid, pr.sku, pr.title,
GROUP_CONCAT(bc.barcode SEPARATOR '
') AS barcodes
FROM (
(SELECT DISTINCT skuid, sku, title
FROM t_products
WHERE sku LIKE 'BC1FORSKU1%' OR title LIKE 'BC1FORSKU1%')
UNION
(SELECT DISTINCT t_products.skuid, t_products.sku, t_products.title
FROM t_products
INNER JOIN t_barcodes ON t_products.skuid = t_barcodes.skuid
WHERE t_barcodes.barcode = 'BC1FORSKU1')
) AS pr
LEFT JOIN t_barcodes AS bc ON bc.skuid = pr.skuid
GROUP BY pr.skuid

In this statement, how is pr.sku "not functionally dependent on" pr.skuid ? The working statements show that MySQL is correctly inferring the functional dependence of sku on skuid from the DISTINCT -ness of the inner queries. But somehow this functional dependence doesn't survive a UNION .

Indeed. It seems UNION hides the functional dependency to MySql.

I believe you could however simplify your query to this, avoiding the union :

SELECT    pr.skuid, 
          pr.sku, 
          pr.title,
          GROUP_CONCAT(bc.barcode SEPARATOR '
') AS barcodes
FROM      t_products pr
LEFT JOIN t_barcodes AS bc1 
       ON bc1.skuid = pr.skuid
      AND bc1.barcode = 'BC1FORSKU1'
LEFT JOIN t_barcodes AS bc
       ON bc.skuid = pr.skuid
WHERE     sku LIKE 'BC1FORSKU1%' OR title LIKE 'BC1FORSKU1%' OR bc1.skuid IS NOT NULL
GROUP BY  pr.skuid

Can we start over? We have this data set...

SELECT * FROM t_products p JOIN t_barcodes b ON b.skuid = p.skuid;

+-------+------+---------+------------+-------+
| skuid | sku  | title   | barcode    | skuid |
+-------+------+---------+------------+-------+
|     1 | SKU1 | title 1 | BC1FORSKU1 |     1 |
|     1 | SKU1 | title 1 | BC2FORSKU1 |     1 |
|     2 | SKU2 | title 2 | BC1FORSKU2 |     2 |
|     2 | SKU2 | title 2 | BC2FORSKU2 |     2 |
|     3 | SKU3 | title 3 | BC1FORSKU3 |     3 |
|     3 | SKU3 | title 3 | BC2FORSKU3 |     3 |
+-------+------+---------+------------+-------+

... what result set are we trying to achieve?

And before the chorus of disapproval, obviously this isn't (yet) an answer, but rather seeks to take advantage of certain formatting options.

(Of course, only a moron would fail to spot that)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM