PostgreSQL Antipatterns: ื”ืขื‘ืจืช ืกื˜ื™ื ื•ื‘ื—ื™ืจื•ืช ืœ-SQL

ืžืขืช ืœืขืช, ื”ื™ื–ื ืฆืจื™ืš ืœื”ืขื‘ื™ืจ ืงื‘ื•ืฆื” ืฉืœ ืคืจืžื˜ืจื™ื ืื• ืืคื™ืœื• ื‘ื—ื™ืจื” ืฉืœืžื” ืœื‘ืงืฉื” "ื‘ื›ื ื™ืกื”". ืœืคืขืžื™ื ื™ืฉ ืคืชืจื•ื ื•ืช ืžื•ื–ืจื™ื ืžืื•ื“ ืœื‘ืขื™ื” ื”ื–ื•.
PostgreSQL Antipatterns: ื”ืขื‘ืจืช ืกื˜ื™ื ื•ื‘ื—ื™ืจื•ืช ืœ-SQL
ื‘ื•ื ื ืœืš "ืžื”ื”ืคืš" ื•ื ืจืื” ืื™ืš ืœื ืขื•ืฉื™ื ืืช ื–ื”, ืœืžื” ื•ืื™ืš ืืคืฉืจ ืœืขืฉื•ืช ืืช ื–ื” ื˜ื•ื‘ ื™ื•ืชืจ.

"ื”ื›ื ืกื”" ื™ืฉื™ืจื” ืฉืœ ืขืจื›ื™ื ื‘ื’ื•ืฃ ื”ื‘ืงืฉื”

ื‘ื“ืจืš ื›ืœืœ ื–ื” ื ืจืื” ื‘ืขืจืš ื›ืš:

query = "SELECT * FROM tbl WHERE id = " + value

... ืื• ื›ื›ื”:

query = "SELECT * FROM tbl WHERE id = :param".format(param=value)

ืขืœ ืฉื™ื˜ื” ื–ื• ื ืืžืจ, ื›ืชื•ื‘ ื• ืืคื™ืœื• ืžืฆื•ื™ืจ ืžืกืคื™ืง:

PostgreSQL Antipatterns: ื”ืขื‘ืจืช ืกื˜ื™ื ื•ื‘ื—ื™ืจื•ืช ืœ-SQL

ื›ืžืขื˜ ืชืžื™ื“ ื–ื” ื›ืš ื ืชื™ื‘ ื™ืฉื™ืจ ืœื”ื–ืจืงืช SQL ื•ืขื•ืžืก ื ื•ืกืฃ ืขืœ ื”ื”ื™ื’ื™ื•ืŸ ื”ืขืกืงื™, ืฉื ืืœืฅ "ืœื”ื“ื‘ื™ืง" ืืช ืžื—ืจื•ื–ืช ื”ืฉืื™ืœืชื•ืช ืฉืœืš.

ื’ื™ืฉื” ื–ื• ื™ื›ื•ืœื” ืœื”ื™ื•ืช ืžื•ืฆื“ืงืช ื—ืœืงื™ืช ืจืง ื‘ืžื™ื“ืช ื”ืฆื•ืจืš. ืœื”ืฉืชืžืฉ ื‘ืžื—ื™ืฆื•ืช ื‘-PostgreSQL ื’ืจืกืื•ืช 10 ื•ืžื˜ื” ืœืชื•ื›ื ื™ืช ื™ืขื™ืœื” ื™ื•ืชืจ. ื‘ื’ืจืกืื•ืช ืืœื•, ืจืฉื™ืžืช ื”ืงื˜ืขื™ื ื”ืกืจื•ืงื™ื ื ืงื‘ืขืช ืœืœื ื”ืชื—ืฉื‘ื•ืช ื‘ืคืจืžื˜ืจื™ื ื”ืžื•ืขื‘ืจื™ื, ืจืง ืขืœ ื‘ืกื™ืก ื’ื•ืฃ ื”ื‘ืงืฉื”.

$n ื˜ื™ืขื•ื ื™ื

ืœื”ืฉืชืžืฉ ืžืฆื™ื™ื ื™ ืžืงื•ื ืคืจืžื˜ืจื™ื ื–ื” ื˜ื•ื‘, ื–ื” ืžืืคืฉืจ ืœืš ืœื”ืฉืชืžืฉ ื”ืฆื”ืจื•ืช ืžื•ื›ื ื•ืช, ื”ืคื—ืชืช ื”ืขื•ืžืก ื”ืŸ ืขืœ ื”ืœื•ื’ื™ืงื” ื”ืขืกืงื™ืช (ืžื—ืจื•ื–ืช ื”ืฉืื™ืœืชื” ื ื•ืฆืจืช ื•ืžืฉื•ื“ืจืช ืคืขื ืื—ืช ื‘ืœื‘ื“) ื•ื”ืŸ ืขืœ ืฉืจืช ืžืกื“ ื”ื ืชื•ื ื™ื (ืœื ื ื“ืจืฉ ื ื™ืชื•ื— ื•ืชื›ื ื•ืŸ ืžื—ื“ืฉ ืขื‘ื•ืจ ื›ืœ ืžื•ืคืข ืฉืœ ื”ื‘ืงืฉื”).

ืžืกืคืจ ืžืฉืชื ื” ืฉืœ ืืจื’ื•ืžื ื˜ื™ื

ื‘ืขื™ื•ืช ื™ื—ื›ื• ืœื ื• ื›ืืฉืจ ื ืจืฆื” ืœื”ืขื‘ื™ืจ ืžืกืคืจ ืœื ื™ื“ื•ืข ืฉืœ ื˜ื™ืขื•ื ื™ื ืžืจืืฉ:

... id IN ($1, $2, $3, ...) -- $1 : 2, $2 : 3, $3 : 5, ...

ืื ืชืฉืื™ืจ ืืช ื”ื‘ืงืฉื” ื‘ื˜ื•ืคืก ื”ื–ื”, ืื– ืœืžืจื•ืช ืฉื–ื” ื™ื—ืกื•ืš ืื•ืชื ื• ืžื”ื–ืจืงื•ืช ืคื•ื˜ื ืฆื™ืืœื™ื•ืช, ื–ื” ืขื“ื™ื™ืŸ ื™ื•ื‘ื™ืœ ืœืฆื•ืจืš ืœื”ื“ื‘ื™ืง/ืœื ืชื— ืืช ื”ื‘ืงืฉื” ืขื‘ื•ืจ ื›ืœ ืืคืฉืจื•ืช ืžืžืกืคืจ ื”ืืจื’ื•ืžื ื˜ื™ื. ื›ื‘ืจ ื™ื•ืชืจ ื˜ื•ื‘ ืžืืฉืจ ืœืขืฉื•ืช ืืช ื–ื” ื‘ื›ืœ ืคืขื, ืื‘ืœ ืืชื” ื™ื›ื•ืœ ืœื”ืกืชื“ืจ ื‘ืœืขื“ื™ื•.

ื–ื” ืžืกืคื™ืง ื›ื“ื™ ืœื”ืขื‘ื™ืจ ืจืง ืคืจืžื˜ืจ ืื—ื“ ื”ืžื›ื™ืœ ื™ื™ืฆื•ื’ ืกื“ืจืชื™ ืฉืœ ืžืขืจืš:

... id = ANY($1::integer[]) -- $1 : '{2,3,5,8,13}'

ื”ื”ื‘ื“ืœ ื”ื™ื—ื™ื“ ื”ื•ื ื”ืฆื•ืจืš ืœื”ืžื™ืจ ื‘ืžืคื•ืจืฉ ืืช ื”ืืจื’ื•ืžื ื˜ ืœืกื•ื’ ื”ืžืขืจืš ื”ืจืฆื•ื™. ืื‘ืœ ื–ื” ืœื ื’ื•ืจื ืœื‘ืขื™ื•ืช, ืฉื›ืŸ ืื ื—ื ื• ื›ื‘ืจ ื™ื•ื“ืขื™ื ืžืจืืฉ ืœืืŸ ืื ื—ื ื• ืคื•ื ื™ื.

ื”ืขื‘ืจื” ืœื“ื•ื’ืžื” (ืžื˜ืจื™ืงืก)

ื‘ื“ืจืš ื›ืœืœ ืืœื• ื›ืœ ืžื™ื ื™ ืืคืฉืจื•ื™ื•ืช ืœื”ืขื‘ืจืช ืžืขืจื›ื™ ื ืชื•ื ื™ื ืœื”ื•ืกืคื” ืœืžืกื“ ื”ื ืชื•ื ื™ื "ื‘ื‘ืงืฉื” ืื—ืช":

INSERT INTO tbl(k, v) VALUES($1,$2),($3,$4),...

ื‘ื ื•ืกืฃ ืœื‘ืขื™ื•ืช ืฉืชื•ืืจื• ืœืขื™ืœ ื‘"ื”ื“ื‘ืงื” ืžื—ื“ืฉ" ืฉืœ ื”ื‘ืงืฉื”, ื–ื” ื’ื ื™ื›ื•ืœ ืœื”ื•ื‘ื™ืœ ืื•ืชื ื• ืžืชื•ืš ื–ื™ื›ืจื•ืŸ ื•ืงืจื™ืกืช ืฉืจืช. ื”ืกื™ื‘ื” ืคืฉื•ื˜ื” - PG ืฉื•ืžืจืช ื–ื™ื›ืจื•ืŸ ื ื•ืกืฃ ืขื‘ื•ืจ ื”ืืจื’ื•ืžื ื˜ื™ื, ื•ืžืกืคืจ ื”ืจืฉื•ืžื•ืช ื‘ืกื˜ ืžื•ื’ื‘ืœ ืจืง ืขืœ ื™ื“ื™ ื™ื™ืฉื•ื ื”ืœื•ื’ื™ืงื” ื”ืขืกืงื™ืช Wishlist. ื‘ืžืงืจื™ื ืงืœื™ื ื™ื™ื ื‘ืžื™ื•ื—ื“ ื”ื™ื” ืฆื•ืจืš ืœืจืื•ืช ื˜ื™ืขื•ื ื™ื "ืžืžื•ืกืคืจื™ื" ื”ื’ื“ื•ืœื™ื ืž-$9000 - ืืœ ืชืขืฉื” ื–ืืช.

ื‘ื•ื ื ืฉื›ืชื‘ ืืช ื”ืฉืื™ืœืชื”, ื ื—ื™ืœ ื›ื‘ืจ ืกื“ืจื” "ื“ื•-ืžืคืœืกื™ืช".:

INSERT INTO tbl
SELECT
  unnest[1]::text k
, unnest[2]::integer v
FROM (
  SELECT
    unnest($1::text[])::text[] -- $1 : '{"{a,1}","{b,2}","{c,3}","{d,4}"}'
) T;

ื›ืŸ, ื‘ืžืงืจื” ืฉืœ ืขืจื›ื™ื "ืžื•ืจื›ื‘ื™ื" ื‘ืชื•ืš ืžืขืจืš, ื”ื ืฆืจื™ื›ื™ื ืœื”ื™ื•ืช ืžืžื•ืกื’ืจื™ื ืขื ืžืจื›ืื•ืช.
ื‘ืจื•ืจ ืฉื‘ื“ืจืš ื–ื• ื ื™ืชืŸ "ืœื”ืจื—ื™ื‘" ืืช ื”ื‘ื—ื™ืจื” ื‘ืžืกืคืจ ืฉื“ื•ืช ืฉืจื™ืจื•ืชื™.

ืื™-ืฉืงื˜, ืื™-ืฉืงื˜,โ€ฆ

ืžืขืช ืœืขืช ื™ืฉ ืื•ืคืฆื™ื•ืช ืœืžืขื‘ืจ ื‘ืžืงื•ื "ืžืขืจืš ืžืขืจื›ื™ื" ื›ืžื” "ืžืขืจื›ื™ ืขืžื•ื“ื•ืช" ืฉืฆื™ื™ื ืชื™ ื‘ืžืืžืจ ื”ืื—ืจื•ืŸ:

SELECT
  unnest($1::text[]) k
, unnest($2::integer[]) v;

ื‘ืฉื™ื˜ื” ื–ื•, ืื ืืชื” ืขื•ืฉื” ื˜ืขื•ืช ื‘ืขืช ื™ืฆื™ืจืช ืจืฉื™ืžื•ืช ืขืจื›ื™ื ืขื‘ื•ืจ ืขืžื•ื“ื•ืช ืฉื•ื ื•ืช, ืงืœ ืžืื•ื“ ืœืงื‘ืœ ืœื—ืœื•ื˜ื™ืŸ ืชื•ืฆืื•ืช ื‘ืœืชื™ ืฆืคื•ื™ื•ืช, ืฉืชืœื•ื™ ื’ื ื‘ื’ืจืกืช ื”ืฉืจืช:

-- $1 : '{a,b,c}', $2 : '{1,2}'
-- PostgreSQL 9.4
k | v
-----
a | 1
b | 2
c | 1
a | 2
b | 1
c | 2
-- PostgreSQL 11
k | v
-----
a | 1
b | 2
c |

JSON

ื”ื—ืœ ืžื’ืจืกื” 9.3, ืœ-PostgreSQL ื™ืฉ ืคื•ื ืงืฆื™ื•ืช ืžืœืื•ืช ืœืขื‘ื•ื“ื” ืขื ืกื•ื’ json. ืœื›ืŸ, ืื ืคืจืžื˜ืจื™ ื”ืงืœื˜ ืฉืœืš ืžื•ื’ื“ืจื™ื ื‘ื“ืคื“ืคืŸ, ืืชื” ื™ื›ื•ืœ ืžืžืฉ ืฉื ื•ืœื™ืฆื•ืจ ืื•ื‘ื™ื™ืงื˜ json ืขื‘ื•ืจ ืฉืื™ืœืชืช SQL:

SELECT
  key k
, value v
FROM
  json_each($1::json); -- '{"a":1,"b":2,"c":3,"d":4}'

ืขื‘ื•ืจ ื’ืจืกืื•ืช ืงื•ื“ืžื•ืช, ื ื™ืชืŸ ืœื”ืฉืชืžืฉ ื‘ืื•ืชื” ืฉื™ื˜ื” ืขื‘ื•ืจ ื›ืœ (hstore), ืื‘ืœ "ืงื™ืคื•ืœ" ื ื›ื•ืŸ ืขื ืื•ื‘ื™ื™ืงื˜ื™ื ืžื•ืจื›ื‘ื™ื ืฉื ืžืœื˜ื™ื ื‘-hstore ืขืœื•ืœ ืœื’ืจื•ื ืœื‘ืขื™ื•ืช.

json_populate_recordset

ืื ืืชื” ื™ื•ื“ืข ืžืจืืฉ ืฉื”ื ืชื•ื ื™ื ืžืžืขืจืš "input" json ื™ืขื‘ืจื• ืœืžื™ืœื•ื™ ื˜ื‘ืœื” ื›ืœืฉื”ื™, ืชื•ื›ืœ ืœื—ืกื•ืš ื”ืจื‘ื” ื‘ืฉื“ื•ืช "dereferencing" ื•ื”ืขื‘ืจื” ืœืกื•ื’ื™ื ื”ืจืฆื•ื™ื™ื ื‘ืืžืฆืขื•ืช ื”ืคื•ื ืงืฆื™ื” json_populate_recordset:

SELECT
  *
FROM
  json_populate_recordset(
    NULL::pg_class
  , $1::json -- $1 : '[{"relname":"pg_class","oid":1262},{"relname":"pg_namespace","oid":2615}]'
  );

json_to_recordset

ื•ืคื•ื ืงืฆื™ื” ื–ื• ืคืฉื•ื˜ "ืชืจื—ื™ื‘" ืืช ืžืขืจืš ื”ืื•ื‘ื™ื™ืงื˜ื™ื ื”ืžื•ืขื‘ืจ ืœื‘ื—ื™ืจื”, ืžื‘ืœื™ ืœื”ืกืชืžืš ืขืœ ืคื•ืจืžื˜ ื”ื˜ื‘ืœื”:

SELECT
  *
FROM
  json_to_recordset($1::json) T(k text, v integer);
-- $1 : '[{"k":"a","v":1},{"k":"b","v":2}]'
k | v
-----
a | 1
b | 2

ืฉื•ืœื—ืŸ ื–ืžื ื™

ืื‘ืœ ืื ื›ืžื•ืช ื”ื ืชื•ื ื™ื ื‘ื“ื’ื™ืžื” ื”ืžืฉื•ื“ืจืช ื’ื“ื•ืœื” ืžืื•ื“, ืื–ื™ ืœื–ืจื•ืง ืื•ืชื ืœืคืจืžื˜ืจ ืžืกื•ื“ืจ ืื—ื“ ืงืฉื”, ื•ืœืคืขืžื™ื ื‘ืœืชื™ ืืคืฉืจื™ืช, ืžื›ื™ื•ื•ืŸ ืฉื”ื™ื ื“ื•ืจืฉืช ื—ื“ ืคืขืžื™ ื”ืงืฆืืช ื–ื™ื›ืจื•ืŸ ื’ื“ื•ืœื”. ืœื“ื•ื’ืžื”, ืืชื” ืฆืจื™ืš ืœืืกื•ืฃ ืืฆื•ื•ื” ื’ื“ื•ืœื” ืฉืœ ื ืชื•ื ื™ ืื™ืจื•ืขื™ื ืžืžืขืจื›ืช ื—ื™ืฆื•ื ื™ืช ื‘ืžืฉืš ื–ืžืŸ ืจื‘, ื•ืื– ืืชื” ืจื•ืฆื” ืœืขื‘ื“ ืื•ืชื• ื—ื“ ืคืขืžื™ ื‘ืฆื“ ื‘ืกื™ืก ื”ื ืชื•ื ื™ื.

ื‘ืžืงืจื” ื–ื”, ื”ืคืชืจื•ืŸ ื”ื˜ื•ื‘ ื‘ื™ื•ืชืจ ื™ื”ื™ื” ืœื”ืฉืชืžืฉ ืฉื•ืœื—ื ื•ืช ื–ืžื ื™ื™ื:

CREATE TEMPORARY TABLE tbl(k text, v integer);
...
INSERT INTO tbl(k, v) VALUES($1, $2); -- ะฟะพะฒั‚ะพั€ะธั‚ัŒ ะผะฝะพะณะพ-ะผะฝะพะณะพ ั€ะฐะท
...
-- ั‚ัƒั‚ ะดะตะปะฐะตะผ ั‡ั‚ะพ-ั‚ะพ ะฟะพะปะตะทะฝะพะต ัะพ ะฒัะตะน ัั‚ะพะน ั‚ะฐะฑะปะธั†ะตะน ั†ะตะปะธะบะพะผ

ื”ืฉื™ื˜ื” ื˜ื•ื‘ื” ืœืฉื™ื“ื•ืจ ื ื“ื™ืจ ืฉืœ ื ืคื—ื™ื ื’ื“ื•ืœื™ื ื ืชื•ื ื™ื.
ืžื ืงื•ื“ืช ื”ืžื‘ื˜ ืฉืœ ืชื™ืื•ืจ ืžื‘ื ื” ื”ื ืชื•ื ื™ื ืฉืœื”, ื˜ื‘ืœื” ื–ืžื ื™ืช ืฉื•ื ื” ืžื˜ื‘ืœื” "ืจื’ื™ืœื”" ื‘ืชื›ื•ื ื” ืื—ืช ื‘ืœื‘ื“. ื‘ื˜ื‘ืœืช ืžืขืจื›ืช pg_class, ื‘ืขื•ื“ ื‘ pg_type, pg_depend, pg_attribute, pg_attrdef, ... - ื•ื›ืœื•ื ื‘ื›ืœืœ.

ืœื›ืŸ, ื‘ืžืขืจื›ื•ืช ืื™ื ื˜ืจื ื˜ ืขื ืžืกืคืจ ืจื‘ ืฉืœ ื—ื™ื‘ื•ืจื™ื ืงืฆืจื™ ืžื•ืขื“ ืœื›ืœ ืื—ื“ ืžื”ื, ื˜ื‘ืœื” ื›ื–ื• ืชื™ื™ืฆืจ ื‘ื›ืœ ืคืขื ืจืฉื•ืžื•ืช ืžืขืจื›ืช ื—ื“ืฉื•ืช, ืืฉืจ ื ืžื—ืงื•ืช ืขื ืกื’ื™ืจืช ื”ื—ื™ื‘ื•ืจ ืœืžืกื“ ื”ื ืชื•ื ื™ื. ื‘ืกื•ืคื• ืฉืœ ื“ื‘ืจ, ืฉื™ืžื•ืฉ ืœื ืžื‘ื•ืงืจ ื‘-TEMP TABLE ืžื•ื‘ื™ืœ ืœ"ื ืคื™ื—ื•ืช" ืฉืœ ื˜ื‘ืœืื•ืช ื‘-pg_catalog ื•ื”ืื˜ืช ืคืขื•ืœื•ืช ืจื‘ื•ืช ื”ืžืฉืชืžืฉื•ืช ื‘ื”ืŸ.
ื›ืžื•ื‘ืŸ ืฉืืคืฉืจ ืœื”ื™ืœื—ื ื‘ื–ื” ืžืขื‘ืจ ืชืงื•ืคืชื™ ื•ืืงื•ื ืžืœื ืœืคื™ ื˜ื‘ืœืื•ืช ืงื˜ืœื•ื’ ื”ืžืขืจื›ืช.

ืžืฉืชื ื™ ื”ืคืขืœื”

ื ื ื™ื— ืฉื”ืขื™ื‘ื•ื“ ืฉืœ ื”ื ืชื•ื ื™ื ืžื”ืžืงืจื” ื”ืงื•ื“ื ื”ื•ื ื“ื™ ืžื•ืจื›ื‘ ืขื‘ื•ืจ ืฉืื™ืœืชืช SQL ื‘ื•ื“ื“ืช, ืื‘ืœ ืืชื” ืจื•ืฆื” ืœืขืฉื•ืช ืืช ื–ื” ืœืขืชื™ื ืงืจื•ื‘ื•ืช ืœืžื“ื™. ื›ืœื•ืžืจ, ืื ื—ื ื• ืจื•ืฆื™ื ืœื”ืฉืชืžืฉ ื‘ืขื™ื‘ื•ื“ ืคืจื•ืฆื“ื•ืจืœื™ ื‘ ืœื—ืกื•ื, ืื‘ืœ ื”ืฉื™ืžื•ืฉ ื‘ื”ืขื‘ืจืช ื ืชื•ื ื™ื ื“ืจืš ื˜ื‘ืœืื•ืช ื–ืžื ื™ื•ืช ื™ื”ื™ื” ื™ืงืจ ืžื“ื™.

ืื ื—ื ื• ื’ื ืœื ื™ื›ื•ืœื™ื ืœื”ืฉืชืžืฉ ื‘-$n-ืคืจืžื˜ืจื™ื ื›ื“ื™ ืœืขื‘ื•ืจ ืœื‘ืœื•ืง ืื ื•ื ื™ืžื™. ืžืฉืชื ื™ ื”ืคื’ื™ืฉื” ื•ื”ืคื•ื ืงืฆื™ื” ื™ืขื–ืจื• ืœื ื• ืœืฆืืช ืžื”ืžืฆื‘. ื”ื”ื’ื“ืจื” ื”ื ื•ื›ื—ื™ืช.

ืœืคื ื™ ื’ืจืกื” 9.2, ื”ื™ื” ืขืœื™ืš ืœื”ื’ื“ื™ืจ ืžืจืืฉ ืžืจื—ื‘ ืฉืžื•ืช ืžื™ื•ื—ื“ ืžื—ืœืงื•ืช_ืžืฉืชื ื™ื_ืžื•ืชืืžืช ืื™ืฉื™ืช ืขื‘ื•ืจ ืžืฉืชื ื™ ื”ืคื’ื™ืฉื” "ืฉืœื”ื". ื‘ื’ืจืกืื•ืช ื”ื ื•ื›ื—ื™ื•ืช, ืืชื” ื™ื›ื•ืœ ืœื›ืชื•ื‘ ืžืฉื”ื• ื›ื–ื”:

SET my.val = '{1,2,3}';
DO $$
DECLARE
  id integer;
BEGIN
  FOR id IN (SELECT unnest(current_setting('my.val')::integer[])) LOOP
    RAISE NOTICE 'id : %', id;
  END LOOP;
END;
$$ LANGUAGE plpgsql;
-- NOTICE:  id : 1
-- NOTICE:  id : 2
-- NOTICE:  id : 3

ื™ืฉื ื ืคืชืจื•ื ื•ืช ืื—ืจื™ื ื–ืžื™ื ื™ื ื‘ืฉืคื•ืช ืคืจื•ืฆื“ื•ืจืœื™ื•ืช ื ืชืžื›ื•ืช ืื—ืจื•ืช.

ืžื›ื™ืจ ื“ืจื›ื™ื ื ื•ืกืคื•ืช? ืฉืชืคื• ื‘ืชื’ื•ื‘ื•ืช!

ืžืงื•ืจ: www.habr.com

ื”ื•ืกืคืช ืชื’ื•ื‘ื”