1

I want to create a table and then initialize it with some values, in as concise manner as possible.

However, this script gets executed every time my app starts, so the insert should happen only on items that were not already added previously.

I do not want to use IGNORE directive in 'INSERT IGNORE INTO', because I do not want to ignore unexpected errors.

For some reason, INSERT INTO fails with "SQL error (1136): Column count doesn't match value count at row 1", even though the select that follows gives the values that need to be added.

Here's the failing code:

START TRANSACTION;

CREATE TABLE IF NOT EXISTS `privileges` (
    `id` TINYINT NOT NULL AUTO_INCREMENT,
    `label` VARCHAR(25) UNIQUE,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `privileges` (`label`)
SELECT `label` FROM (
SELECT NULL AS `label`
UNION VALUES
    ('item1'),
    ('item2')
) X
WHERE `label` IS NOT NULL
AND `label` NOT IN (SELECT `label` FROM `privileges`)

COMMIT;

Currently I am solving this by first inserting the values into a temporary table, and then performing a select on that. But why isn't the above working and is there a more concise way to do what I'm trying to do?

I'm using MariaDB 10.3.9, added missing UNIQUE constraint

Edit 2: Thanks to LukStorms for figuring out the error was related to AUTO_INCREMENT, it seems passing NULL for AUTO_INCREMENT column solves the problem like so:

INSERT INTO `privileges` (id, label)
WITH ITEMS(label) AS (VALUES
    ('users:read'),('users:create'),
    ('clients:read'),('clients:write'),
    ('catalog:read'),('catalog:write'),
    ('cart:read'),('cart:write'),
    ('orders:read'),('orders:write'), ('test1')
) SELECT NULL, label FROM ITEMS i
WHERE label NOT IN (SELECT label FROM `privileges`);
Fay
  • 173
  • 13
  • looks like you're attempting to do an idempotent migration? You want to insert items from a list (item1 item2 etc) which will grow with time, and by running this script you hope to add all the items that are missing? – erik258 Dec 27 '18 at 23:47
  • 1
    Syntax error? `UNION VALUES`? – Rick James Dec 28 '18 at 04:16
  • @DanFarrell The script is a web service that self-initializes the tables it operates on. It's capable of upgrading an old database, in backwards compatible way. Most tables are edited through API calls, but some tables contain 'static' data that can only grow 'manually'. But these new items must be included in next automatic initialization. This initialization happens when service is started with command line flag "--init-db". When running this on an existing database, it should add missing items, but not remove any data. – Fay Dec 28 '18 at 13:44
  • @RickJames The select with union values works (try it). Only the INSERT doesn't work. – Fay Dec 28 '18 at 13:44
  • 2
    Caution to others: That use of `VALUES` is new with MariaDB 10.3; it does not [yet] exist in MySQL. – Rick James Dec 28 '18 at 16:55

2 Answers2

1

First, your application is trying to double-insert values. It probably shouldn't be doing that (though I can think of a few valid use cases). Consider making it so that it does not try to add data that it's already added before. If you don't have easy access to inter-instance state, pull the current list out of the database on startup before deciding what to insert.

Second, if you want labels to be unique, why is there not a unique key on the label field? At the moment, INSERT IGNORE wouldn't even work because there is nothing in your schema preventing duplicate label values. I would ask yourself why you need an auto-incrementing ID: why not just have the label, and make it the primary key?

Then, if you still need to do this duplicate-elision at the SQL layer, you may use ON DUPLICATE KEY to suck up redundant inserts of an existing primary key:

INSERT INTO `privileges` (`label`)
VALUES
    ('item1'),
    ('item2')
)
ON DUPLICATE KEY UPDATE `label` = `label`

This solution is difficult to implement with your auto-increment ID key, because your application probably doesn't know what the ID is going to be. Another reason to consider dropping it.

Unfortunately, there's no ON DUPLICATE KEY IGNORE.

If you want to keep the ID key, and you don't want your application to do a read step on startup (perhaps for scalability reasons), then INSERT IGNORE to be quite honest is your best bet, though you're still going to need at least a unique key on label to make that work.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • _why not just have the `label`, and make it the primary key?_ If you have a LOT of rows in other tables referencing the label, and you want to update a label name, it's expensive because you have to update every row that references the label. When you have an `id` column as a surrogate key and just a unique key on the `label` column, you only need to update 1 row to update the label name. – Adrian Wiik May 06 '22 at 08:21
1

In MariaDb 10.3+, using a CTE with a the VALUES expression can let you assign a column name to it.

with ITEMS(label) as
(VALUES 
 ('item1')
,('item2'))
select i.label 
from ITEMS i
where not exists (select 1 from privileges p where p.label = i.label)

But somehow it gives an error when inserting into a table that has a field with an AUTO_INCREMENT. Seems like a bug to me.

However, when you insert a NULL into a an AUTO_INCREMENT field then the NULL gets ignored. But you discovered that behaviour yourself.

So this works:

INSERT INTO privileges (id, label)
WITH ITEMS(label) as (
 VALUES ('item1'), ('item2')
)
SELECT null, i.label
FROM ITEMS i
WHERE NOT EXISTS (SELECT 1 FROM privileges p WHERE p.label = i.label);

Test on db<>fiddle here

Using unioned selects also works though.

INSERT INTO privileges (label)
SELECT label
FROM (
 SELECT 'item1' as label UNION ALL 
 SELECT 'item2'
) i
WHERE NOT EXISTS (SELECT 1 FROM privileges p WHERE p.label = i.label);

db<>fiddle here

Maybe another way is to use a temporary table (that will vanish when the session expires)

CREATE TEMPORARY TABLE tmp_items (label VARCHAR(25) NOT NULL PRIMARY KEY);

INSERT INTO tmp_items (label) VALUES 
 ('item1')
,('item2');

INSERT INTO privileges (label)
SELECT label
FROM tmp_items i
WHERE label NOT IN (SELECT DISTINCT label FROM privileges);

Test on db<>fiddle here

LukStorms
  • 28,916
  • 5
  • 31
  • 45
  • It's very nice to be able to assign a column name to VALUES. Unfortunately the above query still gives me the same SQL 1136 error. – Fay Dec 28 '18 at 01:46
  • @Fay Urgh, seems to be a bug or missing feature in MariaDb? Check out this *db<>fiddle [here](https://dbfiddle.uk/?rdbms=mariadb_10.3&fiddle=fb5297d7f961b0aba9b0984ca40ebf06)*. Run it with and without that id commented. The AUTO_INCREMENT works fine if you insert by values, but gives issues when you insert from a select with values? weird. Btw, not sure you need to put the CREATE in a transaction. – LukStorms Dec 28 '18 at 11:52
  • I can't make sense of it, but the AUTO_INCREMENT seems to work when I simply pass NULL for the id parameter, i.e. INSERT INTO privileges (id,label) SELECT NULL, label FROM (....) – Fay Dec 28 '18 at 16:39
  • @Fay Lol. That's kinda interesting. It looks so counter intuitive that it didn't even cross my mind. – LukStorms Dec 28 '18 at 17:20
  • @Fay Guess I have to update my answer now :p It also works with the CTE. – LukStorms Dec 28 '18 at 17:22