record "new" has no field "user_id" postgreSQL - sql

THIS is what i try and error
postgres=# INSERT INTO cs222p_interchange.Ad(ad_id, plan, content, pic_num, item_id, seller_user_id, placed_date)
postgres-# VALUES ('ADT32457', 'Gold', 'New games available!', 1, 'F7E1N', '4Z5VC', '2022-11-06');
ERROR: record "new" has no field "user_id"
CONTEXT: SQL statement "INSERT INTO TargetedAds(ad_id, user_id)
SELECT NEW.ad_id, NEW.user_id WHERE (
SELECT category
FROM cs222p_interchange.item i
JOIN cs222p_interchange.ad a ON i.item_id = a.item_id
WHERE ad_id = NEW.ad_id AND (buyer_user_id = NEW.user_id OR seller_user_id = NEW.user_id) )
LIKE (
SELECT category
FROM cs222p_interchange.User u
JOIN cs222p_interchange.Categories c ON u.user_id = c.user_id
WHERE user_id = NEW.user_id)
ON CONFLICT DO NOTHING"
PL/pgSQL function addad() line 1 at SQL statement
This is the trigger
CREATE FUNCTION AddAd() RETURNS Trigger AS
$$
BEGIN
INSERT INTO TargetedAds(ad_id, user_id)
SELECT NEW.ad_id, NEW.user_id
WHERE (
SELECT *
FROM cs222p_interchange.item i
JOIN cs222p_interchange.ad a ON i.item_id = a.item_id
WHERE ad_id = NEW.ad_id AND (buyer_user_id = NEW.user_id OR seller_user_id = NEW.user_id) )
= (
SELECT category
FROM cs222p_interchange.User u
JOIN cs222p_interchange.Categories c ON u.user_id = c.user_id
WHERE user_id = NEW.user_id)
ON CONFLICT DO NOTHING;
RETURN NEW;
END;
$$
LANGUAGE PLPGSQL;
CREATE TRIGGER TargetedAdsLogger AFTER INSERT ON cs222p_interchange.Ad FOR EACH ROW EXECUTE FUNCTION AddAd();
This is the Table
CREATE TABLE TargetedAds(
ad_id text,
user_id text,
PRIMARY KEY (ad_id, user_id),
FOREIGN KEY (ad_id) REFERENCES cs222p_interchange.Ad
(ad_id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES cs222p_interchange.Seller(user_id) ON DELETE CASCADE
);
This is what i need to insert
INSERT INTO cs222p_interchange.Ad(ad_id, plan, content, pic_num, item_id, seller_user_id, placed_date)
VALUES ('ADT32457', 'Gold', 'New games available!', 1, 'F7E1N', '4Z5VC', '2022-11-06');
This is the definition of the Ad table:
CREATE TABLE cs222p_interchange.Ad(
ad_id text NOT NULL,
plan text NOT NULL ,
content text ,
pic_num int NOT NULL,
item_id text NOT NULL,
seller_user_id text NOT NULL,
placed_date date NOT NULL,
PRIMARY KEY (ad_id),
FOREIGN KEY(item_id) REFERENCES cs222p_interchange.Item(item_id) ON DELETE CASCADE,
FOREIGN KEY(pic_num, item_id) REFERENCES cs222p_interchange.Picture(pic_num, item_id) ON DELETE CASCADE,
FOREIGN KEY(seller_user_id) REFERENCES cs222p_interchange.Seller(user_id) ON DELETE CASCADE
);
I have checked multiple times and all of my tables and columns are existing.

Related

How to create a trigger for this situation?

I have a problem inserting values into a Class table.
I want to write a trigger to prevent happening "an instructor teaches in different class_Id at the same time".
How can I do this?
CREATE TABLE Class
(
Class_ID BIGINT,
c_InstrumentID BIGINT NOT NULL,
c_StudentID BIGINT,
c_InstructorID BIGINT NOT NULL,
c_InstituteId BIGINT NOT NULL,
c_TermSeason NVARCHAR(10),
c_TermYear INT,
c_TimeOfClass TIME NOT NULL,
c_DayOfClass NVARCHAR(30),
c_Eligibility INT,
c_RemainingSession INT,
CONSTRAINT cons_Season
CHECK(c_TermSeason IN ('Spring', 'Summer', 'Fall', 'Winter')),
CONSTRAINT cons_TimeClass
CHECK(c_TimeOfClass BETWEEN '08:30:00' AND '20:30:00'),
CONSTRAINT cons_RemainSession
CHECK (c_RemainingSession BETWEEN 0 AND 12),
FOREIGN KEY(c_InstrumentID)
REFERENCES Instrument(Instrument_ID) ON DELETE NO ACTION,
FOREIGN KEY(c_StudentID)
REFERENCES Student(Student_ID) ON DELETE NO ACTION,
FOREIGN KEY(c_InstructorID)
REFERENCES Instructor(Instructor_ID) ON DELETE NO ACTION,
FOREIGN KEY(c_InstituteId)
REFERENCES Institute(Institute_ID) ON DELETE NO ACTION,
PRIMARY KEY (Class_ID)
)
This is the trigger which I've created:
CREATE OR ALTER TRIGGER One_InstructorDuplicate
ON Class
AFTER INSERT
AS
BEGIN
IF (NOT EXISTS (SELECT *
FROM Class C, ((SELECT * FROM CLASS)
EXCEPT (SELECT * FROM inserted)) AS newC
WHERE newC.c_InstructorID = C.c_InstructorID
AND newC.c_DayOfClass != C.c_DayOfClass
AND newC.c_TermSeason != C.c_TermSeason
AND newC.c_TermYear != C.c_TermYear
AND newC.c_TimeOfClass != C.c_TimeOfClass))
ROLLBACK TRAN
END;
Use inserted and JOIN to the Class table. Check for existence of rows in table that matches your requirement (c_DayOfClass, c_TermSeason etc)
CREATE OR ALTER TRIGGER One_InstructorDuplicate
ON Class
AFTER INSERT
AS
BEGIN
IF EXISTS
(
SELECT *
FROM inserted i
INNER JOIN Class c ON i.c_InstructorID = c.c_InstructorID
WHERE i.Class_ID <> c.Class_ID
AND i.c_DayOfClass = c.c_DayOfClass
AND i.c_TermSeason = c.c_TermSeason
AND i.c_TermYear = c.c_TermYear
AND i.c_TimeOfClass = c.c_TimeOfClass
)
BEGIN
ROLLBACK TRAN
END
END;

Select form a lot of many-to-many relations at once best practice

I'm wondering what is the best approach to handle the following problem:
I've got a DB-Structure where many tables are linked to my Person table like this:
phone n-n person_phone_realtion n-n person n-n person_email_realtionn-n email
I want to query my tables and parse the result to JSON and store the many to many values inside arrays. Is it better to make only one trip to the database and parse the result of my JOIN-query (see example below), which can be quite large due to duplicates, to my desired schema or should I make more trips to the database and keep the query result small?
What is the best practices for this scenario
Created with the following statement:
DROP TABLE IF EXISTS phone CASCADE;
DROP TABLE IF EXISTS email CASCADE;
DROP TABLE IF EXISTS person CASCADE;
DROP TABLE IF EXISTS person_phone_realtion CASCADE;
DROP TABLE IF EXISTS person_email_realtion CASCADE;
CREATE TABLE phone (
phon_id text NOT NULL,
CONSTRAINT phone_pk PRIMARY KEY (phon_id)
);
CREATE TABLE email (
emai_id text NOT NULL,
CONSTRAINT email_pk PRIMARY KEY (emai_id)
);
CREATE TABLE person (
pers_id INTEGER NOT NULL,
CONSTRAINT person_pk PRIMARY KEY (pers_id)
);
CREATE TABLE person_phone_realtion (
pers_id int NOT NULL,
phon_id int NOT NULL
);
CREATE TABLE person_email_realtion (
pers_id int NOT NULL,
email_id int NOT NULL
);
INSERT INTO person(pers_id)
VALUES (1),(2),(3),(4),(5);
INSERT INTO email(emai_id)
VALUES ('a'),('b'),('c');
INSERT INTO phone(phon_id)
VALUES ('D'),('E'),('F');
INSERT INTO person_email_realtion(pers_id, email_id)
VALUES (1,'a'),(1,'b'), (1,'c'),(2,'b'),(3,'c');
INSERT INTO person_phone_realtion(pers_id, phon_id)
VALUES (1,'D'),(2,'D'), (2,'E'),(5,'F');
DROP TABLE IF EXISTS phone CASCADE;
DROP TABLE IF EXISTS email CASCADE;
DROP TABLE IF EXISTS person CASCADE;
DROP TABLE IF EXISTS person_phone_realtion CASCADE;
DROP TABLE IF EXISTS person_email_realtion CASCADE;
CREATE TABLE phone (
phon_id text NOT NULL,
CONSTRAINT phone_pk PRIMARY KEY (phon_id)
);
CREATE TABLE email (
emai_id text NOT NULL,
CONSTRAINT email_pk PRIMARY KEY (emai_id)
);
CREATE TABLE person (
pers_id INTEGER NOT NULL,
CONSTRAINT person_pk PRIMARY KEY (pers_id)
);
CREATE TABLE person_phone_realtion (
pers_id int NOT NULL,
phon_id int NOT NULL
);
CREATE TABLE person_email_realtion (
pers_id int NOT NULL,
email_id int NOT NULL
);
INSERT INTO person(pers_id)
VALUES (1),(2),(3),(4),(5);
INSERT INTO email(emai_id)
VALUES ('a'),('b'),('c');
INSERT INTO phone(phon_id)
VALUES ('D'),('E'),('F');
INSERT INTO person_email_realtion(pers_id, email_id)
VALUES (1,'a'),(1,'b'), (1,'c'),(2,'b'),(3,'c');
INSERT INTO person_phone_realtion(pers_id, phon_id)
VALUES (1,'D'),(2,'D'), (2,'E'),(5,'F');
Now I can query all the relations at once using JOIN wich would result in a lot of duplicate content:
SELECT * FROM person
RIGHT JOIN person_phone_realtion
ON person.pers_id = person_phone_realtion.pers_id
RIGHT JOIN phone
ON person_phone_realtion.phon_id = phone.phon_id
RIGHT JOIN person_email_realtion
ON person.pers_id = person_email_realtion.pers_id
RIGHT JOIN email
ON person_email_realtion.email_id = email.emai_id;
where I will get a result similar to this:
pers_id phon_id emai_id
1 D a
1 D b
1 D c
2 E b
2 D b
The resulting JSON should look like this:
[
{
"person" : 1,
"email": [
"a", "b", "c"
],
"phone":[
"D"
]
},
{
"person" : 2,
"email": [
"b"
],
"phone":[
"D", "E"
]
}
]
One trip to the database is usually best. You should pre-aggregate the values along each dimension:
select p.*, pp.phones, pe.emails
from person p left join
(select pers_id, array_agg(ppr.phone_id) as phones
from person_phone_realtion ppr
group by pers_id
) pp
on p.pers_id = pp.pers_id left join
(select pers_id, array_agg(per.email_id) as emails
from person_email_realtion ppr
group by pers_id
) pe
on p.pers_id = pe.pers_id ;
You can aggregate into strings or JSON, if you prefer.

Optimize SQL query with 3 FOR loops

I have a fully working SQL query. However, it is very very slow. I am looking for a way to optimize it.
CREATE TABLE trajectory_geom (
id SERIAL PRIMARY KEY,
trajectory_id BIGINT,
user_id BIGINT,
geom GEOMETRY(Linestring, 4326)
);
INSERT INTO trajectory_geom (trajectory_id, user_id, geom)
SELECT
p.trajectory_id,
p.user_id,
ST_Transform(ST_MakeLine(p.geom), 4326)
FROM point p
GROUP BY p.trajectory_id
;
DO $$
DECLARE
urow record;
vrow record;
wrow record;
BEGIN
FOR wrow IN
SELECT DISTINCT(p.user_id) FROM point p
LOOP
raise notice 'User id: %', wrow.user_id;
FOR vrow IN
SELECT DISTINCT(p.trajectory_id) FROM point p WHERE p.user_id = wrow.user_id
LOOP
FOR urow IN
SELECT
analyzed_tr.*
FROM trajectory_start_end_geom analyzed_tr
WHERE
analyzed_tr.user_id = wrow.user_id
AND
ST_Intersects (
(
analyzed_tr.start_geom
)
,
(
SELECT g.geom
FROM trajectory_geom g
WHERE g.trajectory_id = vrow.trajectory_id
)
) = TRUE
LOOP
INSERT INTO trajectories_intercepting_with_starting_point (initial_trajectory_id, mathced_trajectory_id, user_id)
SELECT
vrow.trajectory_id,
urow.trajectory_id,
wrow.user_id
WHERE urow.trajectory_id <> vrow.trajectory_id
;
END LOOP;
END LOOP;
END LOOP;
END;
$$;
It has 3 loops...how can I avoid them?
Basically, I am looping all user IDs, for each user looping all trajectories and checking is trajectory interact with any other trajectory of this user.
Schema:
CREATE TABLE public.trajectory_start_end_geom
(
id integer NOT NULL DEFAULT nextval('trajectory_start_end_geom_id_seq'::regclass),
trajectory_id bigint,
user_id bigint,
start_geom geometry(Polygon,4326),
end_geom geometry(Polygon,4326),
CONSTRAINT trajectory_start_end_geom_pkey PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
);
CREATE TABLE public.trajectory_geom
(
id integer NOT NULL DEFAULT nextval('trajectory_geom_id_seq'::regclass),
trajectory_id bigint,
user_id bigint,
geom geometry(LineString,4326),
CONSTRAINT trajectory_geom_pkey PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
);
CREATE TABLE public.point
(
id integer NOT NULL DEFAULT nextval('point_id_seq'::regclass),
user_id bigint,
date date,
"time" time without time zone,
lat double precision,
lon double precision,
trajectory_id integer,
geom geometry(Geometry,4326),
CONSTRAINT point_pkey PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
);
Try this SQL query. Hope this helps.
INSERT INTO trajectories_intercepting_with_starting_point
(initial_trajectory_id, mathced_trajectory_id, user_id)
SELECT
TG.trajectory_id AS first_trajectory_id,
TG2.trajectory_id AS last_trajectory_id,
TG.user_id
FROM Trajectory_geom AS TG
JOIN Trajectory_geom AS TG2 ON TG.user_id = TG2.user_id
AND TG.trajectory_id < TG2.trajectory_id
JOIN Trajectory_start_end_geom AS TSE ON TSE.trajectory_id = TG.trajectory_id
WHERE ST_Intersects(TSE.start_geom, TG2.geom) = TRUE
This should do the trick:
WITH vrow AS(
INSERT INTO trajectory_geom (trajectory_id, user_id, geom)
SELECT
p.trajectory_id,
p.user_id,
ST_Transform(ST_MakeLine(p.geom), 4326) AS geom
FROM point p
GROUP BY p.trajectory_id
RETURNING trajectory_id, user_id, geom
)
INSERT INTO trajectories_intercepting_with_starting_point (initial_trajectory_id, mathced_trajectory_id, user_id)
SELECT
vrow.trajectory_id,
urow.trajectory_id,
vrow.user_id
FROM trajectory_start_end_geom AS urow
JOIN vrow
ON urow.user_id = vrow.user_id
AND urow.trajectory_id <> vrow.trajectory_id
AND ST_Intersects(urow.start_geom, vrow.geom)
If you don't need insert into trajectory_geom eliminating it (and the CTE) will speed it up

SQL - Efficient versioning of DNS records

So far I have come up with this solution that needs further refinement (big thanks to #postgresql on freenode).
The problem I am trying to overcome is an efficient way of storing DNS records whilst maintaining some sort of history. The issue I am currently having is with the wCTE which is inserting new records and deleting old records correctly. It isn't, however, readding records. The wCTE is:
WITH deltas AS (
SELECT o, n FROM (
SELECT id, name, domain_id, class_id, addr FROM record WHERE tld_id = $1
) AS o FULL OUTER JOIN record_temp n
ON (
o.name = n.name AND
o.domain_id = n.domain_id AND
o.class_id = n.class_id AND
o.addr = n.addr
)
WHERE (o.name, o.domain_id, o.class_id, o.addr)
IS DISTINCT FROM (n.name, n.domain_id, n.class_id, n.addr)
), mark_dead AS (
UPDATE record SET alive = FALSE
WHERE id IN (
SELECT (o).id FROM deltas WHERE (o).id IS NOT NULL
) RETURNING *
)
INSERT INTO record (name, domain_id, tld_id, class_id, addr)
SELECT (n).name, (n).domain_id, (n).tld_id, (n).class_id, (n).addr
FROM deltas WHERE
(n).name IS NOT NULL AND
(n).domain_id IS NOT NULL AND
(n).tld_id IS NOT NULL AND
(n).class_id IS NOT NULL AND
(n).addr IS NOT NULL
;
The o result has all the old records that do not exist in record_temp, n has all the records that are new and need to be inserted. I expect I need to add another join which pulls in (an inner join?) results that exist on both tables (which if marked as dead, need to be marked as alive).
The rest of the schema for reference is:
CREATE TABLE record (
id SERIAL,
name VARCHAR(255),
domain_id INT,
tld_id INT,
class_id INT,
addr INET,
alive BOOLEAN DEFAULT TRUE,
PRIMARY KEY (id),
CONSTRAINT fk1 FOREIGN KEY (domain_id) REFERENCES domain (id) MATCH SIMPLE,
CONSTRAINT fk2 FOREIGN KEY (tld_id) REFERENCES tld (id) MATCH SIMPLE,
UNIQUE(name, domain_id, class_id, addr)
);
CREATE TABLE record_history (
id SERIAL,
record_id INT,
history_type record_history_type,
stamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk1 FOREIGN KEY (record_id) REFERENCES record (id) MATCH SIMPLE,
PRIMARY KEY(id)
);
CREATE TEMP TABLE record_temp (
name VARCHAR(255),
domain_id INT,
tld_id INT,
class_id INT,
addr INET,
UNIQUE(name, domain_id, class_id, addr)
)
ON COMMIT DROP;
record_history is populated using functions and triggers and is populating how I expect it to, below are these triggers:
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO record_history (record_id, history_type) VALUES (NEW.id, 'added');
RETURN NEW;
END;
$$ language 'plpgsql';
RETURNS TRIGGER AS $$
BEGIN
IF NEW.alive = OLD.alive THEN
RETURN NEW;
END IF;
IF NEW.alive THEN
INSERT INTO record_history (record_id, history_type) VALUES (NEW.id, 'added');
END IF;
IF NOT NEW.alive THEN
INSERT INTO record_history (record_id, history_type) VALUES (NEW.id, 'deleted');
END IF;
RETURN NEW;
END;
$$ language 'plpgsql';
ON record FOR EACH ROW EXECUTE PROCEDURE
add_insert_record_history();
ON record FOR EACH ROW EXECUTE PROCEDURE
add_update_record_history();
I seem to have it working how I want with the following query, which I feel is incredibly unoptimized:
WITH deltas AS (
SELECT o, n FROM (
SELECT id, name, domain_id, class_id, addr FROM record WHERE tld_id = $1
) AS o FULL OUTER JOIN record_temp n
ON (
o.name = n.name AND
o.domain_id = n.domain_id AND
o.class_id = n.class_id AND
o.addr = n.addr
)
WHERE (o.name, o.domain_id, o.class_id, o.addr)
IS DISTINCT FROM (n.name, n.domain_id, n.class_id, n.addr)
), mark_dead AS (
UPDATE record SET alive = FALSE
WHERE id IN (
SELECT (o).id FROM deltas WHERE (o).id IS NOT NULL
) RETURNING *
), mark_alive AS (
UPDATE record SET alive = TRUE
WHERE alive = FALSE AND id IN (
SELECT id FROM (
SELECT id, name, domain_id, class_id, addr FROM record WHERE tld_id = $1
) AS o INNER JOIN record_temp n
ON (
o.name = n.name AND
o.domain_id = n.domain_id AND
o.class_id = n.class_id AND
o.addr = n.addr
)
) RETURNING *
)
INSERT INTO record (name, domain_id, tld_id, class_id, addr)
SELECT (n).name, (n).domain_id, (n).tld_id, (n).class_id, (n).addr
FROM deltas WHERE
(n).name IS NOT NULL AND
(n).domain_id IS NOT NULL AND
(n).tld_id IS NOT NULL AND
(n).class_id IS NOT NULL AND
(n).addr IS NOT NULL
;

MySQL: select default row (not default values) if other row not available

edit:
I've included the create statements and a small set of test data for you to try out. Therefor, I've changed the example id to 2 in stead of 5 to represent an existing id in the test data.
/ edit
I have three MySQL tables for keeping localized page info:
CREATE TABLE `locale` (
`languageCode` char(3) NOT NULL,
`regionCode` char(3) NOT NULL default 'ZZ',
`isActive` enum('yes','no') NOT NULL default 'no',
PRIMARY KEY (`languageCode`,`regionCode`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `page` (
`id` int(10) unsigned NOT NULL auto_increment,
`parentId` int(10) unsigned default NULL,
PRIMARY KEY (`id`),
KEY `FK_page_page_1` (`parentId`),
CONSTRAINT `FK_page_page_1` FOREIGN KEY (`parentId`) REFERENCES `page` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `pageInfo` (
`pageId` int(10) unsigned NOT NULL,
`languageCode` char(3) NOT NULL,
`regionCode` char(3) NOT NULL default 'ZZ',
PRIMARY KEY (`pageId`,`languageCode`,`regionCode`),
KEY `FK_pageInfo_locale_1` (`languageCode`,`regionCode`),
KEY `FK_pageInfo_page_1` (`pageId`),
CONSTRAINT `FK_pageInfo_locale_1` FOREIGN KEY (`languageCode`, `regionCode`) REFERENCES `locale` (`languageCode`, `regionCode`) ON DELETE CASCADE,
CONSTRAINT `FK_pageInfo_page_1` FOREIGN KEY (`pageId`) REFERENCES `page` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
And here is some test data:
/* locale */
INSERT INTO `locale` (languageCode,regionCode,isActive) VALUES ('de','ZZ','yes');
INSERT INTO `locale` (languageCode,regionCode,isActive) VALUES ('en','ZZ','yes');
INSERT INTO `locale` (languageCode,regionCode,isActive) VALUES ('nl','ZZ','yes');
/* page */
INSERT INTO `page` (id,parentId) VALUES (1,NULL);
INSERT INTO `page` (id,parentId) VALUES (2,1);
/* pageInfo */
INSERT INTO `pageInfo` (pageId,languageCode,regionCode) VALUES (1,'de','ZZ');
INSERT INTO `pageInfo` (pageId,languageCode,regionCode) VALUES (1,'en','ZZ');
INSERT INTO `pageInfo` (pageId,languageCode,regionCode) VALUES (1,'nl','ZZ');
INSERT INTO `pageInfo` (pageId,languageCode,regionCode) VALUES (2,'en','ZZ');
INSERT INTO `pageInfo` (pageId,languageCode,regionCode) VALUES (2,'nl','ZZ');
The dilemma:
To retrieve pages with id 2 of all active locales I issue the following SQL statement:
SELECT
p.*, pi.languageCode, pi.regionCode
FROM
page AS p
INNER JOIN pageInfo AS pi
ON pi.pageId = p.id
INNER JOIN locale AS l
ON l.languageCode = pi.languageCode AND l.regionCode = pi.regionCode
WHERE
(p.id = '2')
AND (l.isActive = 'yes')
How would I alter this statement, so that, when id 2 is not available for a particular locale, it automatically falls back on the root page for that locale (that is: where page.parentId IS NULL)? My goal is to have MySQL give me either one, but not both, for the active locales.
I tried:
WHERE
(p.id = '2' OR (p.parentId IS NULL))
But, of course, that gives me two records for locales that actually have id 2 also. I'm pretty sure it's possible (either with UNION, sub selects, or a duplicate join on page) but I'm having a total SQL writers block here. I'd appreciate your help.
How about XOR? "one or the other but not both":
WHERE
(p.id = '5' XOR (p.parentId IS NULL))
^---here
You could order by p.id=5 and use a limit of 1. It's a bit of a hack, but it'll work.
SELECT
p.*, pi.languageCode, pi.regionCode
FROM
page AS p
INNER JOIN pageInfo AS pi
ON pi.pageId = p.id
INNER JOIN locale AS l
ON l.languageCode = pi.languageCode AND l.regionCode = pi.regionCode
WHERE
(p.id = '5' OR (p.parentId IS NULL))
AND (l.isActive = 'yes')
ORDER BY p.id = '5' DESC
LIMIT 1
Use LEFT JOINs and join both tables twice. The second time you join them specifically for the case of p.parentId IS NULL.
In the SELECT list use IFNULLs. First arguments are for the id specified, second ones are the fallbacks, i.e. the values for the root page.
SELECT
p.*,
IFNULL(l.languageCode, lr.LanguageCode) AS languageCode,
IFNULL(l.regionCode, lr.regionCode) AS regionCode
FROM
page AS p
LEFT JOIN pageInfo AS pi
ON pi.pageId = p.id
LEFT JOIN locale AS l
ON l.languageCode = pi.languageCode AND l.regionCode = pi.regionCode
LEFT JOIN pageInfo AS pir
ON pir.pageId = p.id AND p.parentId IS NULL
LEFT JOIN locale AS lr
ON lr.languageCode = pir.languageCode AND lr.regionCode = pir.regionCode
WHERE
(p.id = '5' AND l.isActive = 'yes')
OR (p.parentId IS NULL AND lr.isActive = 'yes')