问题
In a game using PostgreSQL 9.3 as backend I am trying to limit the number of games played by a user per week.
I have prepared an SQL Fiddle, but unfortunately it doesn't work.
My (test, not production) code is here:
create table pref_users (
id varchar(32) primary key,
last_ip inet
);
create table pref_match (
id varchar(32) references pref_users on delete cascade,
completed integer default 0 check (completed >= 0),
yw char(7) default to_char(current_timestamp, 'IYYY-IW'),
primary key(id, yw)
);
And here is a stored procedure with which I try to find the number of games played this week:
create or replace function pref_get_user_info(
IN _id varchar,
IN _last_ip inet,
OUT games_this_week integer
) as $BODY$
begin
select sum(completed)
into games_this_week
from pref_match where
(id = _id or
id in (select id from pref_users where last_ip=_last_ip)) and
yw=to_char(current_timestamp, 'IYYY-IW');
end;
$BODY$ language plpgsql;
With this condition:
(id = _id or
id in (select id from pref_users where last_ip=_last_ip))
I am trying to catch users who will try to cheat and join the game with a different player id
but from the same IP-address.
But I am worried, that sometimes I will get doubled number of completed games - because in the above condition first the 1st part will match: id = _id
and then the 2nd part id in (...)
- and this will give me the number of games 2 times.
Is there please any cure for that?
I need to "detect" when an id
is used twice in the above condition.
回答1:
Table layout
Do not use char(7)
to store a timestamp.
To be precise, do not use char(7)
to store anything. Ever. Details:
Compare varchar with char
And do not store date / time data in any text representation. Use timestamp or date.
If you are only interested in the week of the year, you could just store an integer
(or even a smallint
), which you get with with extract():
SELECT extract(week FROM now())::int;
But I suggest to store a date
which occupies 4 bytes, just like an integer
, while char(7)
occupies 11 bytes. You can extract the week with above function cheaply. Or use date_trunc():
SELECT date_trunc('week', now())
And id
should much rather be int
- or bigint
if you must. varchar(32)
is rather inefficient.
And declare your column completed
NOT NULL
! Or you'd have to deal with possible NULL values. Your check constraint does not cover that. NULL does not violate the constraint.
Query / function
Assuming data type date
for yw
and int
for id
:
CREATE OR REPLACE FUNCTION pref_get_user_info(_id int, _last_ip inet
,OUT games_this_week int) AS
$func$
DECLARE
_this_monday date := date_trunc('week', now())::date;
BEGIN
SELECT sum(completed)::int
INTO games_this_week
FROM pref_users u
JOIN pref_match m USING (id)
WHERE (u.id = _id OR u.last_ip = _last_ip)
AND m.yw BETWEEN _this_monday
AND _this_monday + 6; -- "sargable"
END
$func$ LANGUAGE plpgsql;
If last_ip
was defined NOT NULL
, you would not need _id
as parameter at all. Just _last_ip
.
回答2:
I would tried some sort of pivot table, although i don't know if plpgsql supports it.
begin
select
SUM
(
CASE WHEN pref_match.id IN (select id from pref_users where last_ip=_last_ip)
THEN completed
) AS ip_matches,
SUM
(
CASE WHEN pref_match.id = _id
THEN completed
) AS id_matches,
into games_this_week
from pref_match
and yw=to_char(current_timestamp, 'IYYY-IW');
end;
Then get max of two values.
But one user can play from more than 1 IP, this is not covered here (most probably you need to log every game IP to catch those situtations)
Also note, that this is will be VERY low-perfomance code. The subquery will run on every matched row at filter stage.
来源:https://stackoverflow.com/questions/24266979/get-sum-of-integers-for-unique-ids