Insert results in non-sequential IDs - postgresql

I have the following query:
INSERT INTO hosts (name, domain, ip)
SELECT name, domain, ip
FROM staging_hosts
ON CONFLICT (ip) DO UPDATE
SET name = excluded.name, domain = excluded.domain;
It works fine aside from the fact that the IDs in my "hosts" table are not being incremented sequentially. For example, I'll get the following new IDs after running this query:
114855
114859
114873
114977
117389
115326
The ID column on the hosts table is serial so I'm not sure why the IDs are incrementing sequentially.

If you insist on minimizing gaps, you could do that, with example tables included:
CREATE TABLE foo (
id SERIAL PRIMARY KEY,
ip INT,
name TEXT,
UNIQUE(ip) );
CREATE TABLE staging (
ip INT,
name TEXT );
INSERT INTO foo (ip,name) VALUES (1,'hello'),(2,'abc'),(10,'world');
INSERT INTO staging VALUES (1,'byebye'),(2,'def'),(11,'world');
WITH s AS (
SELECT COALESCE( foo.id, nextval('foo_id_seq'::regclass)) AS id,
staging.ip,staging.name
FROM staging LEFT JOIN foo USING (ip))
INSERT INTO foo (id,ip,name) SELECT * FROM s
ON CONFLICT(id) DO UPDATE
SET ip=excluded.ip, name=excluded.name;
COALESCE does not evaluate the second argument if it is not needed, which means it only calls nextval when required.

Related

Postgres upsert avoid 2nd unique constraint?

I have a table of the form (based on):
create table foo (
name text unique,
ref_id int,
mod_time timestamptz
);
I would like to be able to insert into it, but
instead updating mod_time if (name, ref_id) pair was in use, and failing if name were in use for another ref_id.
I can do this by creating another unique constraint as follows:
alter table foo add unique (name, ref_id);
Then
insert into foo values ( $1, $2, $3 )
on conflict (name, ref_id) do update set
mod_time = excluded.mod_time;
will function as I want:
If new name, inserts
If same name and ref_id, changes mod_time
If same name for different ref_id, fails
However, the cost is a 2nd unique index, which is in fact superfluous for enforcing the constraint as records unique by name will automatically be unique by name and ref_id.
Is there some workaround that will get me this behavior without the 2nd index, and without additional roundtrips to the database?
It depends on what you mean by fail. If it's okay to just not do anything in this case: where If same name for different ref_id you could do the following:
insert into foo values ($1, $2, $3)
on conflict (name) DO update set
mod_time = excluded.mod_time
WHERE foo.ref_id = excluded.ref_id;
You could check how many rows were modified after running the query and raise an exception if none were modified.

Use returned value of INSERT ... RETURNING in multiple following inserts

I'm trying to use a value returned by an INSERT ... RETURNING statement in multiple following INSERTs.
Say we have the following tables:
CREATE TABLE hosts (host_id SERIAL, name CHARACTER VARYING(20));
CREATE TABLE interfaces (interface_id SERIAL, host_id INTEGER, name CHARACTER VARYING(10), iface_ip INET);
INSERT INTO hosts (name) VALUES ('Host A'),('Host B');
What I want, is to insert a row in the first table (hosts), get the created host_id and then insert multiple rows into the second table (interfaces) with given values and the host_id from the first statement.
I found the following way, using a CTE and a SELECT with static values which works for me, but I'm pretty sure, that this is not the way to accomplish it...
WITH temp_table AS (
INSERT INTO hosts (name) VALUES ('Host C') RETURNING host_id AS last_hostid
), i1 AS (
INSERT INTO interfaces (host_id, name, iface_ip) SELECT last_hostid, 'eth0', '192.168.1.1' FROM temp_table
), i2 AS (
INSERT INTO interfaces (host_id, name, iface_ip) SELECT last_hostid, 'eth1', '192.168.2.1' FROM temp_table
), i3 AS (
INSERT INTO interfaces (host_id, name, iface_ip) SELECT last_hostid, 'eth2', '192.168.3.1' FROM temp_table
) SELECT 1;
I know that I can easily do this, by talking back to a webserver with say PHP, and then fill in the variable in the next statement. But I wanted to accomplish it without all the back and forth, solely in PostgreSQL. So, if there is a better way than mine (and I'm pretty sure of it) - any hints?
You can create one CTE with the rows you want to insert and then use that as the source for the actual insert:
WITH temp_table AS (
INSERT INTO hosts (name) VALUES ('Host C')
RETURNING host_id AS last_hostid
), new_data (name, iface_ip) AS (
values
('eth0', '192.168.1.1'::inet),
('eth1', '192.168.2.1'::inet),
('eth2', '192.168.3.1'::inet)
)
INSERT INTO interfaces (host_id, name, iface_ip)
SELECT last_hostid, nd.name, nd.iface_ip
FROM new_data as nd, temp_table;
The (implicit) cross join in the SELECT doesn't matter as temp_table only return a single row.

postgres #my table syntax error

The following code shows syntax error near #my table seq in postgres sql.
IF (p_BusinessID = '')
THEN
SELECT RANK() OVER(ORDER BY SSO) SNO,Business, SSO, DisplayName, UPPER(ExServerName) ExServerName, POP, IMAP, EWS, DisplayMonth, DisplayYear
FROM "POP_IMAP_EWS_Data"
WHERE DisplayYear = COALESCE(p_YearID, DisplayYear) AND MonthID = COALESCE(p_MonthID, MonthID) AND SSO = COALESCE(p_SSO, SSO);
ELSE
CREATE SEQUENCE #myTable_seq;
CREATE TABLE #myTable
(
ID INT DEFAULT NEXTVAL ('#myTable_seq'),
Item VARCHAR(100),
);
INSERT INTO #myTable(Item)
SELECT RTRIM(LTRIM(Item)) FROM SplitString(;p_BusinessID, ',');
SELECT RANK() OVER(ORDER BY SSO) SNO, A.BusinessID, Business, SSO, DisplayName, UPPER(ExServerName) ExServerName, POP, IMAP, EWS, DisplayMonth, DisplayYear
FROM "POP_IMAP_EWS_Data"A
INNER JOIN #myTable B ON B.Item = A.BusinessID
WHERE DisplayYear = COALESCE(p_YearID, DisplayYear) AND MonthID = COALESCE(p_MonthID, MonthID) AND SSO = COALESCE(p_SSO, SSO);
Can someone tell why?
Using a # prefix to indicate a temporary table is specific to SQL Server (and possibly Sybase). PostgreSQL wants you to say create temporary table ...:
create temporary table mytable (
id serial primary key,
item varchar(100)
);
Also, you'd usually just make your id column a serial column and let PostgreSQL take care of the sequence rather than hooking it all up yourself so I changed that as well; using serial also takes care of setting the sequence's owner.

How to implicitly insert SERIAL ID via view over more than one table

I have two tables, connected in E/R by a is-relation. One representing the "mother table"
CREATE TABLE PERSONS(
id SERIAL NOT NULL,
name character varying NOT NULL,
address character varying NOT NULL,
day_of_creation timestamp NOT NULL DEFAULT current_timestamp,
PRIMARY KEY (id)
)
the other representing the "child table"
CREATE TABLE EMPLOYEES (
id integer NOT NULL,
store character varying NOT NULL,
paychecksize integer NOT NULL,
FOREIGN KEY (id)
REFERENCES PERSONS(id),
PRIMARY KEY (id)
)
Now those two tables are joined in a view
CREATE VIEW EMPLOYEES_VIEW AS
SELECT
P.id,name,address,store,paychecksize,day_of_creation
FROM
PERSONS AS P
JOIN
EMPLOYEES AS E ON P.id = E.id
I want to write either a rule or a trigger to enable a db user to make an insert on that view, sparing him the nasty details of the splitted columns into different tables.
But I also want to make it convenient, as the id is a SERIAL and the day_of_creation has a default value there is no actual need that a user has to provide those, therefore a statement like
INSERT INTO EMPLOYEES_VIEW (name, address, store, paychecksize)
VALUES ("bob", "top secret", "drugstore", 42)
should be enough to result in
PERSONS
id|name|address |day_of_creation
-------------------------------
1 |bob |top secret| 2013-08-13 15:32:42
EMPLOYEES
id| store |paychecksize
---------------------
1 |drugstore|42
A basic rule would be easy as
CREATE RULE EMPLOYEE_VIEW_INSERT AS ON INSERT TO EMPLOYEE_VIEW
DO INSTED (
INSERT INTO PERSONS
VALUES (NEW.id,NEW.name,NEW.address,NEW.day_of_creation),
INSERT INTO EMPLOYEES
VALUES (NEW.id,NEW.store,NEW.paychecksize)
)
should be sufficient. But this will not be convenient as a user will have to provide the id and timestamp, even though it actually is not necessary.
How can I rewrite/extend that code base to match my criteria of convenience?
Something like:
CREATE RULE EMPLOYEE_VIEW_INSERT AS ON INSERT TO EMPLOYEES_VIEW
DO INSTEAD
(
INSERT INTO PERSONS (id, name, address, day_of_creation)
VALUES (default,NEW.name,NEW.address,default);
INSERT INTO EMPLOYEES (id, store, paychecksize)
VALUES (currval('persons_id_seq'),NEW.store,NEW.paychecksize)
);
That way the default values for persons.id and persons.day_of_creation will be the default values. Another option would have been to simply remove those columns from the insert:
INSERT INTO PERSONS (name, address)
VALUES (NEW.name,NEW.address);
Once the rule is defined, the following insert should work:
insert into employees_view (name, address, store, paychecksize)
values ('Arthur Dent', 'Some Street', 'Some Store', 42);
Btw: with a current Postgres version an instead of trigger is the preferred way to make a view updateable.

Getting a query to index seek (rather than scan)

Running the following query (SQL Server 2000) the execution plan shows that it used an index seek and Profiler shows it's doing 71 reads with a duration of 0.
select top 1 id from table where name = '0010000546163' order by id desc
Contrast that with the following with uses an index scan with 8500 reads and a duration of about a second.
declare #p varchar(20)
select #p = '0010000546163'
select top 1 id from table where name = #p order by id desc
Why is the execution plan different? Is there a way to change the second method to seek?
thanks
EDIT
Table looks like
CREATE TABLE [table] (
[Id] [int] IDENTITY (1, 1) NOT NULL ,
[Name] [varchar] (13) COLLATE Latin1_General_CI_AS NOT NULL)
Id is primary clustered key
There is a non-unique index on Name and a unique composite index on id/name
There are other columns - left them out for brevity
Now you've added the schema, please try this. SQL Server treats length differences as different data types and will convert the varchar(13) column to match the varchar(20) variable
declare #p varchar(13)
If not, what about collation coercien? Is the DB or server different to the column?
declare #p varchar(13) COLLATE Latin1_General_CI_AS NOT NULL
If not, add this before and post results
SET SHOWPLAN_TEXT ON
GO
If the name column is NVARCHAR then u need your parameter to be also of the same type. It should then pick it up by index seek.
declare #p nvarchar(20)
select #p = N'0010000546163'
select top 1 id from table where name = #p order by id desc