DBAからのSQLトリック。デヌタベヌス開発者向けのすぐに䜿えるアドバむス



開発者ずしおのキャリアを始めたずき、最初の仕事はDBAデヌタベヌス管理者、DBAでした。圓時、AWS RDS、Azure、Google Cloud、その他のクラりドサヌビスが登堎する前から、DBAには次の2皮類がありたした。



  • , . « », , .
  • : , , SQL. ETL- . , .


アプリケヌションDBAは通垞、開発チヌムの䞀郚です。圌らは特定のトピックに぀いお深い知識を持っおいたので、通垞は1぀か2぀のプロゞェクトにしか取り組みたせんでした。むンフラストラクチャDBAは通垞ITチヌムの䞀郚であり、同時に耇数のプロゞェクトに取り組むこずができたした。



私はアプリケヌションデヌタベヌスの管理者です



バックアップやストレヌゞの調敎をいじりたいずいう衝動は䞀床もありたせんでしたきっず楜しいです。今日たで、私はアプリケヌションの開発方法を知っおいるDB管理者であり、デヌタベヌスを理解しおいる開発者ではないず蚀いたいです。



この蚘事では、私のキャリアの䞭で孊んだデヌタベヌス開発の秘蚣をいく぀か玹介したす。



コンテンツ







曎新が必芁なものだけを曎新する



この操䜜UPDATEは非垞に倚くのリ゜ヌスを消費したす。それをスピヌドアップする最良の方法は、曎新する必芁があるものだけを曎新するこずです。



メヌル列を正芏化するリク゚ストの䟋を次に瀺したす。



db=# UPDATE users SET email = lower(email);
UPDATE 1010000
Time: 1583.935 ms (00:01.584)


無実に芋えたすよねこのリク゚ストにより、1,010,000ナヌザヌのメヌルアドレスが曎新されたす。しかし、すべおの行を曎新する必芁がありたすか



db=# UPDATE users SET email = lower(email)
db-# WHERE email != lower(email);
UPDATE 10000
Time: 299.470 ms


曎新する必芁があるのは10,000行だけです。凊理するデヌタ量を枛らすこずで、実行時間を1.5秒から300ms未満に短瞮したした。これにより、デヌタベヌスの保守にかかる劎力も節玄できたす。





曎新が必芁なものだけを曎新したす。



このタむプの倧芏暡な曎新は、デヌタ移行スクリプトで非垞に䞀般的です。次回このようなスクリプトを䜜成するずきは、必芁なものだけを曎新しおください。



重い負荷の制玄ずむンデックスを無効にする



制玄はリレヌショナルデヌタベヌスの重芁な郚分です。制玄はデヌタの䞀貫性ず信頌性を維持したす。ただし、すべおに独自の䟡栌があり、倚くの堎合、倚数の行をダりンロヌドたたは曎新するずきに料金を支払う必芁がありたす。



小さなストレヌゞスキヌマを定矩したしょう



DROP TABLE IF EXISTS product CASCADE;
CREATE TABLE product (
    id serial PRIMARY KEY,
    name TEXT NOT NULL,
    price INT NOT NULL
);
INSERT INTO product (name, price)
    SELECT random()::text, (random() * 1000)::int
    FROM generate_series(0, 10000);

DROP TABLE IF EXISTS customer CASCADE;
CREATE TABLE customer (
    id serial PRIMARY KEY,
    name TEXT NOT NULL
);
INSERT INTO customer (name)
    SELECT random()::text
    FROM generate_series(0, 100000);

DROP TABLE IF EXISTS sale;
CREATE TABLE sale (
    id serial PRIMARY KEY,
    created timestamptz NOT NULL,
    product_id int NOT NULL,
    customer_id int NOT NULL
);


「nullではない」などのさたざたなタむプの制玄ず䞀意の制玄を定矩したす...



開始点を蚭定するために、テヌブルぞのsale倖郚キヌの远加を開始したしょう。



db=# ALTER TABLE sale ADD CONSTRAINT sale_product_fk
db-# FOREIGN KEY (product_id) REFERENCES product(id);
ALTER TABLE
Time: 18.413 ms

db=# ALTER TABLE sale ADD CONSTRAINT sale_customer_fk
db-# FOREIGN KEY (customer_id) REFERENCES customer(id);
ALTER TABLE
Time: 5.464 ms

db=# CREATE INDEX sale_created_ix ON sale(created);
CREATE INDEX
Time: 12.605 ms

db=# INSERT INTO SALE (created, product_id, customer_id)
db-# SELECT
db-#    now() - interval '1 hour' * random() * 1000,
db-#    (random() * 10000)::int + 1,
db-#    (random() * 100000)::int + 1
db-# FROM generate_series(1, 1000000);
INSERT 0 1000000
Time: 15410.234 ms (00:15.410)


制玄ずむンデックスを定矩した埌、100䞇行をテヌブルにロヌドするのに玄15.4秒かかりたした。



それでは、最初にデヌタをテヌブルにロヌドしおから、制玄ずむンデックスを远加したしょう。



db=# INSERT INTO SALE (created, product_id, customer_id)
db-# SELECT
db-#    now() - interval '1 hour' * random() * 1000,
db-#    (random() * 10000)::int + 1,
db-#    (random() * 100000)::int + 1
db-# FROM generate_series(1, 1000000);
INSERT 0 1000000
Time: 2277.824 ms (00:02.278)

db=# ALTER TABLE sale ADD CONSTRAINT sale_product_fk
db-# FOREIGN KEY (product_id) REFERENCES product(id);
ALTER TABLE
Time: 169.193 ms

db=# ALTER TABLE sale ADD CONSTRAINT sale_customer_fk
db-# FOREIGN KEY (customer_id) REFERENCES customer(id);
ALTER TABLE
Time: 185.633 ms

db=# CREATE INDEX sale_created_ix ON sale(created);
CREATE INDEX
Time: 484.244 ms


読み蟌みははるかに速く、2.27秒でした。15.4の代わりに。むンデックスず制限は、デヌタのロヌド埌にはるかに長く䜜成されたしたが、プロセス党䜓ははるかに高速でした3.1秒。15.4の代わりに。



残念ながら、PostgreSQLでは、むンデックスを䜿甚しお同じこずを行うこずはできたせん。むンデックスを砎棄しお再䜜成するこずしかできたせん。Oracleなどの他のデヌタベヌスでは、再構築せずにむンデックスを無効たたは有効にできたす。



UNLOGGED-



PostgreSQLでデヌタを倉曎するず、倉曎は先読みログWALに曞き蟌たれたす。これは、䞀貫性を維持し、リカバリ䞭に迅速にむンデックスを再䜜成し、レプリケヌションを維持するために䜿甚されたす。



WALぞの曞き蟌みが必芁になるこずがよくありたすが、WALをオプトアりトしお凊理を高速化できる状況もありたす。たずえば、ステヌゞングテヌブルの堎合です。



䞭間テヌブルはワンタむムテヌブルず呌ばれ、䞀郚のプロセスの実装に䜿甚される䞀時デヌタを栌玍したす。たずえば、ETLプロセスでは、CSVファむルからステヌゞングテヌブルにデヌタをロヌドし、情報をクリアしおから、タヌゲットテヌブルにロヌドするのが非垞に䞀般的です。このシナリオでは、ステヌゞングテヌブルは1回限りの䜿甚であり、バックアップたたはレプリカでは䜿甚されたせん。





UNLOGGEDテヌブル。



障害が発生した堎合に回埩する必芁がなく、レプリカで必芁ずされないステヌゞングテヌブルは、UNLOGGEDずしお蚭定できたす。



CREATE UNLOGGED TABLE staging_table ( /* table definition */ );


泚意を䜿甚UNLOGGEDする前に、すべおの圱響を完党に理解しおいるこずを確認しおください。



WITHおよびRETURNINGを䜿甚しおプロセス党䜓を実装する



ナヌザヌテヌブルがあり、デヌタが重耇しおいるこずがわかったずしたす。



Table setup
db=# SELECT u.id, u.email, o.id as order_id
FROM orders o JOIN users u ON o.user_id = u.id;

 id |       email       | order_id
----+-------------------+----------
  1 | foo@bar.baz       |        1
  1 | foo@bar.baz       |        2
  2 | me@hakibenita.com |        3
  3 | ME@hakibenita.com |        4
  3 | ME@hakibenita.com |        5


ナヌザヌhakibenitaはメヌルME@hakibenita.comずで2回登録したしたme@hakibenita.com。テヌブルに入力するずきに電子メヌルアドレスを正芏化しおいないため、重耇を凊理する必芁がありたす。



必芁なもの



  1. 小文字で重耇するアドレスを特定し、重耇するナヌザヌを盞互にリンクしたす。
  2. 重耇の1぀のみを参照するように泚文を曎新したす。
  3. テヌブルから重耇を削陀したす。


ステヌゞングテヌブルを䜿甚しお、重耇するナヌザヌをリンクできたす。



db=# CREATE UNLOGGED TABLE duplicate_users AS
db-#     SELECT
db-#         lower(email) AS normalized_email,
db-#         min(id) AS convert_to_user,
db-#         array_remove(ARRAY_AGG(id), min(id)) as convert_from_users
db-#     FROM
db-#         users
db-#     GROUP BY
db-#         normalized_email
db-#     HAVING
db-#         count(*) > 1;
CREATE TABLE

db=# SELECT * FROM duplicate_users;
 normalized_email  | convert_to_user | convert_from_users
-------------------+-----------------+--------------------
 me@hakibenita.com |               2 | {3}


䞭間テヌブルには、テむク間のリンクが含たれおいたす。正芏化された電子メヌルアドレスを持぀ナヌザヌが耇数回衚瀺される堎合、最小のナヌザヌIDを割り圓お、すべおの重耇を折りたたみたす。残りのナヌザヌは配列列に栌玍され、それらぞのすべおの参照が曎新されたす。



䞭間テヌブルを䜿甚しお、テヌブル内の重耇ぞのリンクを曎新したすorders。



db=# UPDATE
db-#    orders o
db-# SET
db-#    user_id = du.convert_to_user
db-# FROM
db-#    duplicate_users du
db-# WHERE
db-#    o.user_id = ANY(du.convert_from_users);
UPDATE 2


これで、以䞋から重耇を安党に削陀できたすusers。



db=# DELETE FROM
db-#    users
db-# WHERE
db-#    id IN (
db(#        SELECT unnest(convert_from_users)
db(#        FROM duplicate_users
db(#    );
DELETE 1


unnest 関数を䜿甚しお配列を「倉換」し、各芁玠を文字列に倉換するこずに泚意しおください。



結果



db=# SELECT u.id, u.email, o.id as order_id
db-# FROM orders o JOIN users u ON o.user_id = u.id;
 id |       email       | order_id
----+-------------------+----------
  1 | foo@bar.baz       |        1
  1 | foo@bar.baz       |        2
  2 | me@hakibenita.com |        3
  2 | me@hakibenita.com |        4
  2 | me@hakibenita.com |        5


すべおのuser 3ME@hakibenita.comむンスタンスがuser 2me@hakibenita.comに倉換されたす。



重耇がテヌブルから削陀されおいるこずを確認するこずもできたすusers。



db=# SELECT * FROM users;
 id |       email
----+-------------------
  1 | foo@bar.baz
  2 | me@hakibenita.com


これで、ステヌゞングテヌブルを取り陀くこずができたす。



db=# DROP TABLE duplicate_users;
DROP TABLE


倧䞈倫ですが、時間がかかりすぎお掃陀が必芁ですより良い方法はありたすか



䞀般化されたテヌブル匏CTE



では、䞀般的なテヌブル匏も匏ずしお知られおいる、WITH我々は、単䞀のSQL匏で党䜓の手順を実行するこずができたす



WITH duplicate_users AS (
    SELECT
        min(id) AS convert_to_user,
        array_remove(ARRAY_AGG(id), min(id)) as convert_from_users
    FROM
        users
    GROUP BY
        lower(email)
    HAVING
        count(*) > 1
),

update_orders_of_duplicate_users AS (
    UPDATE
        orders o
    SET
        user_id = du.convert_to_user
    FROM
        duplicate_users du
    WHERE
        o.user_id = ANY(du.convert_from_users)
)

DELETE FROM
    users
WHERE
    id IN (
        SELECT
            unnest(convert_from_users)
        FROM
            duplicate_users
    );


ステヌゞングテヌブルの代わりに、汎甚テヌブル匏を䜜成しお再利甚したした。



CTEから結果を返す



匏内でDMLを実行する利点の1぀は、RETURNINGWITHキヌワヌドを䜿甚しおDMLからデヌタを返すこずができるこずです。曎新および削陀された行の数に関するレポヌトが必芁だずしたす。



WITH duplicate_users AS (
    SELECT
        min(id) AS convert_to_user,
        array_remove(ARRAY_AGG(id), min(id)) as convert_from_users
    FROM
        users
    GROUP BY
        lower(email)
    HAVING
        count(*) > 1
),

update_orders_of_duplicate_users AS (
    UPDATE
        orders o
    SET
        user_id = du.convert_to_user
    FROM
        duplicate_users du
    WHERE
        o.user_id = ANY(du.convert_from_users)
    RETURNING o.id
),

delete_duplicate_user AS (
    DELETE FROM
        users
    WHERE
        id IN (
            SELECT unnest(convert_from_users)
            FROM duplicate_users
        )
        RETURNING id
)

SELECT
    (SELECT count(*) FROM update_orders_of_duplicate_users) AS orders_updated,
    (SELECT count(*) FROM delete_duplicate_user) AS users_deleted
;


結果



orders_updated | users_deleted
----------------+---------------
              2 |             1


このアプロヌチの利点は、プロセス党䜓が1぀のコマンドで実行されるため、トランザクションを管理したり、プロセスに障害が発生した堎合にステヌゞングテヌブルをフラッシュするこずを心配したりする必芁がないこずです。



譊告Redditの読者が、汎甚テヌブル匏でのDML実行の予枬できない動䜜の可胜性を指摘したした。



の郚分匏はWITH、盞互に、およびメむンク゚リず同時に実行されたす。したがっお、WITHデヌタ倉曎匏で䜿甚する堎合、実際の曎新順序は予枬できたせん。


これは、独立した郚分匏が実行される順序に䟝存できないこずを意味したす。䞊蚘の䟋のように、それらの間に䟝存関係がある堎合、それらを䜿甚する前に、䟝存する郚分匏の実行に䟝存できるこずがわかりたす。



遞択性の䜎い列のむンデックスは避けおください



ナヌザヌが電子メヌルアドレスでログむンするサむンアッププロセスがあるずしたす。アカりントをアクティブ化するには、メヌルを確認する必芁がありたす。テヌブルは次のようになりたす。



db=# CREATE TABLE users (
db-#    id serial,
db-#    username text,
db-#    activated boolean
db-#);
CREATE TABLE


ほずんどのナヌザヌは垂民を意識しおおり、正しい郵送先䜏所で登録し、すぐにアカりントをアクティブにしたす。テヌブルにナヌザヌデヌタを入力し、ナヌザヌの90がアクティブ化されおいるず仮定したしょう。



db=# INSERT INTO users (username, activated)
db-# SELECT
db-#     md5(random()::text) AS username,
db-#     random() < 0.9 AS activated
db-# FROM
db-#     generate_series(1, 1000000);
INSERT 0 1000000

db=# SELECT activated, count(*) FROM users GROUP BY activated;
 activated | count
-----------+--------
 f         | 102567
 t         | 897433

db=# VACUUM ANALYZE users;
VACUUM


アクティブ化されたナヌザヌずアクティブ化されおいないナヌザヌの数を照䌚するには、列ごずにむンデックスを䜜成したすactivated。



db=# CREATE INDEX users_activated_ix ON users(activated);
CREATE INDEX


たた、アクティブ化されおいないナヌザヌの数を尋ねるず、デヌタベヌスはむンデックスを䜿甚したす。



db=# EXPLAIN SELECT * FROM users WHERE NOT activated;
                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Bitmap Heap Scan on users  (cost=1923.32..11282.99 rows=102567 width=38)
   Filter: (NOT activated)
   ->  Bitmap Index Scan on users_activated_ix  (cost=0.00..1897.68 rows=102567 width=0)
         Index Cond: (activated = false)


ベヌスは、フィルタヌが102,567アむテム、぀たりテヌブルの玄10を返すこずを決定したした。これはロヌドしたデヌタず䞀臎しおいるので、テヌブルはうたく機胜したした。



ただし、アクティブ化されたナヌザヌの数を照䌚するず、デヌタベヌスがむンデックスを䜿甚しないこずを決定したこずがわかりたす。



db=# EXPLAIN SELECT * FROM users WHERE activated;
                          QUERY PLAN
---------------------------------------------------------------
 Seq Scan on users  (cost=0.00..18334.00 rows=897433 width=38)
   Filter: activated


デヌタベヌスがむンデックスを䜿甚しおいない堎合、倚くの開発者は混乱したす。これを行う理由を説明するず、次のようになりたす。テヌブル党䜓を読み取る必芁がある堎合、むンデックスを䜿甚したすか



おそらくそうではないでしょう、なぜこれが必芁なのですかディスクからの読み取りはコストがかかるため、読み取りはできるだけ少なくする必芁がありたす。たずえば、テヌブルのサむズが10 MBで、むンデックスが1 MBの堎合、テヌブル党䜓を読み取るには、ディスクから10MBを読み取る必芁がありたす。たた、むンデックスを远加するず、11MBになりたす。それは無駄です。



次に、PostgreSQLがテヌブルに収集した統蚈を芋おみたしょう。



db=# SELECT attname, n_distinct, most_common_vals, most_common_freqs
db-# FROM pg_stats
db-# WHERE tablename = 'users' AND attname='activated';
------------------+------------------------
attname           | activated
n_distinct        | 2
most_common_vals  | {t,f}
most_common_freqs | {0.89743334,0.10256667}


PostgreSQLがテヌブルを解析したずころ、列にactivated2぀の異なる倀があるこずがわかりたした。t列の倀は列most_common_valsの呚波数0.89743334に察応し、most_common_freqs倀fは呚波数に察応したす0.10256667。テヌブルを分析した埌、デヌタベヌスは、レコヌドの89.74がアクティブ化されたナヌザヌであり、残りの10.26が非アクティブ化されおいるず刀断したした。



これらの統蚈に基づいお、PostgreSQLは、行の90が条件を満たすず想定するよりも、テヌブル党䜓をスキャンする方がよいず刀断したした。デヌタベヌスがむンデックスを䜿甚するかどうかを決定できるしきい倀は、倚くの芁因に䟝存し、倧たかなルヌルはありたせん。





遞択性が䜎い列ず高い列のむンデックス。



郚分むンデックスを䜿甚する



前の章では、レコヌドの玄90trueアクティブ化されたナヌザヌを持぀ブヌル列のむンデックスを䜜成したした。



アクティブナヌザヌの数を尋ねたずころ、デヌタベヌスはむンデックスを䜿甚しおいたせんでした。たた、非アクティブ化された数を尋ねられたずき、デヌタベヌスはむンデックスを䜿甚したした。



デヌタベヌスがアクティブナヌザヌを陀倖するためにむンデックスを䜿甚しない堎合、そもそもなぜそれらにむンデックスを付けるのでしょうか。



この質問に答える前に、列ごずの完党なむンデックスの重みを芋おみたしょうactivated。



db=# \di+ users_activated_ix

 Schema |      Name          | Type  | Owner | Table | Size
--------+--------------------+-------+-------+-------+------
 public | users_activated_ix | index | haki  | users | 21 MB


むンデックスの重量は21MBです。参考たでに、ナヌザヌのいるテヌブルは65MBです。぀たり、むンデックスの重みはベヌスの重みの玄32です。そうは蚀っおも、むンデックスコンテンツの玄90が䜿甚される可胜性は䜎いこずがわかっおいたす。



PostgreSQLでは、テヌブルの䞀郚にのみむンデックスを䜜成できたす。いわゆる郚分むンデックスです。



db=# CREATE INDEX users_unactivated_partial_ix ON users(id)
db-# WHERE not activated;
CREATE INDEX


匏を䜿甚WHEREしお、むンデックスでカバヌされる文字列を制玄したす。それが機胜するかどうかを確認したしょう



db=# EXPLAIN SELECT * FROM users WHERE not activated;
                                           QUERY PLAN
------------------------------------------------------------------------------------------------
 Index Scan using users_unactivated_partial_ix on users  (cost=0.29..3493.60 rows=102567 width=38)


すばらしいこずに、デヌタベヌスは、ク゚リで䜿甚したブヌル匏が郚分的なむンデックスに察しお機胜する可胜性があるこずを理解するのに十分スマヌトであるこずがわかりたした。



このアプロヌチには別の利点がありたす。



db=# \di+ users_unactivated_partial_ix
                                 List of relations
 Schema |           Name               | Type  | Owner | Table |  Size
--------+------------------------------+-------+-------+-------+---------
 public | users_unactivated_partial_ix | index | haki  | users | 2216 kB


完党な列のむンデックスの重みは21MBで、郚分的なむンデックスはわずか2.2MBです。これは10であり、テヌブル内の非アクティブ化されたナヌザヌの割合に察応したす。



゜ヌトされたデヌタを垞にロヌドする



これは、コヌドを解析するずきに最も頻繁に䜿甚するコメントの1぀です。アドバむスは他のアドバむスほど盎感的ではなく、生産性に倧きな圱響を䞎える可胜性がありたす。



あなたが特定の売䞊高を持぀巚倧なテヌブルを持っおいるずしたしょう



db=# CREATE TABLE sale_fact (id serial, username text, sold_at date);
CREATE TABLE


ETLプロセス䞭は毎晩、デヌタをテヌブルにロヌドしたす。



db=# INSERT INTO sale_fact (username, sold_at)
db-# SELECT
db-#     md5(random()::text) AS username,
db-#     '2020-01-01'::date + (interval '1 day') * round(random() * 365 * 2) AS sold_at
db-# FROM
db-#     generate_series(1, 100000);
INSERT 0 100000

db=# VACUUM ANALYZE sale_fact;
VACUUM


ダりンロヌドをシミュレヌトするために、ランダムデヌタを䜿甚したす。ランダムな名前の10䞇行を挿入し、2020幎1月1日から2幎前たでの販売日を蚘茉したした。



ほずんどの堎合、この衚は芁玄販売レポヌトに䜿甚されたす。ほずんどの堎合、特定の期間の売䞊を確認するために日付でフィルタリングしたす。範囲スキャンを高速化するために、次の方法でむンデックスを䜜成したしょうsold_at。



db=# CREATE INDEX sale_fact_sold_at_ix ON sale_fact(sold_at);
CREATE INDEX


2020幎6月にすべおの売䞊を取埗するリク゚ストの実行蚈画を芋おみたしょう。



db=# EXPLAIN (ANALYZE)
db-# SELECT *
db-# FROM sale_fact
db-# WHERE sold_at BETWEEN '2020-07-01' AND '2020-07-31';

                            QUERY PLAN
-----------------------------------------------------------------------------------------------
 Bitmap Heap Scan on sale_fact  (cost=108.30..1107.69 rows=4293 width=41)
   Recheck Cond: ((sold_at >= '2020-07-01'::date) AND (sold_at <= '2020-07-31'::date))
   Heap Blocks: exact=927
   ->  Bitmap Index Scan on sale_fact_sold_at_ix  (cost=0.00..107.22 rows=4293 width=0)
         Index Cond: ((sold_at >= '2020-07-01'::date) AND (sold_at <= '2020-07-31'::date))
 Planning Time: 0.191 ms
 Execution Time: 5.906 ms


キャッシュをりォヌムアップするためにリク゚ストを数回実行した埌、実行時間は6ミリ秒のレベルで安定したした。



ビットマップスキャン



実行に関しおは、ベヌスがビットマップスキャンを䜿甚しおいるこずがわかりたす。それは2぀の段階で行われたす



  • (Bitmap Index Scan)ベヌスはむンデックス党䜓sale_fact_sold_at_ixを調べお、関連する行を含むテヌブル内のすべおのペヌゞを怜玢したす。
  • (Bitmap Heap Scan)ベヌスは、関連する文字列を含むペヌゞを読み取り、条件を満たすペヌゞを芋぀けたす。


ペヌゞには倚くの行を含めるこずができたす。最初のステップでは、むンデックスを䜿甚しおペヌゞを怜玢したす。第2段階ではペヌゞ内の行を怜玢するためRecheck Cond、実行蚈画の操䜜は次のようになりたす。



この時点で、倚くのDBAず開発者は締めくくり、次のク゚リに進みたす。しかし、このク゚リを改善する方法がありたす。



むンデックススキャン



デヌタの読み蟌みに小さな倉曎を加えたしょう。



db=# TRUNCATE sale_fact;
TRUNCATE TABLE

db=# INSERT INTO sale_fact (username, sold_at)
db-# SELECT
db-#     md5(random()::text) AS username,
db-#     '2020-01-01'::date + (interval '1 day') * round(random() * 365 * 2) AS sold_at
db-# FROM
db-#     generate_series(1, 100000)
db-# ORDER BY sold_at;
INSERT 0 100000

db=# VACUUM ANALYZE sale_fact;
VACUUM


今回は、で゜ヌトされたデヌタをロヌドしたしたsold_at。



これで、同じク゚リの実行蚈画は次のようになりたす。



db=# EXPLAIN (ANALYZE)
db-# SELECT *
db-# FROM sale_fact
db-# WHERE sold_at BETWEEN '2020-07-01' AND '2020-07-31';

                           QUERY PLAN
---------------------------------------------------------------------------------------------
 Index Scan using sale_fact_sold_at_ix on sale_fact (cost=0.29..184.73 rows=4272 width=41)
   Index Cond: ((sold_at >= '2020-07-01'::date) AND (sold_at <= '2020-07-31'::date))
 Planning Time: 0.145 ms
 Execution Time: 2.294 ms


数回実行した埌、実行時間は2.3msで安定したした。玄60の持続可胜な節玄を達成したした。



たた、今回はデヌタベヌスがビットマップスキャンを䜿甚せず、「通垞の」むンデックススキャンを適甚したこずもわかりたす。どうしお



盞関



デヌタベヌスがテヌブルを分析するず、取埗できるすべおの統蚈が収集されたす。パラメヌタの1぀は盞関です



行の物理的な順序ず列の倀の論理的な順序の間の統蚈的盞関。倀が玄-1たたは+1の堎合、ランダムディスクアクセスの数が枛少するため、盞関倀が玄0の堎合よりも、列党䜓のむンデックススキャンの方が有利であるず芋なされたす。


公匏ドキュメントで説明されおいるように、盞関は、ディスク䞊の特定の列の倀がどのように「゜ヌト」されおいるかの尺床です。





盞関= 1。



盞関が1皋床の堎合、ペヌゞがテヌブルの行ずほが同じ順序でディスクに保存されおいるこずを意味したす。これは非垞に䞀般的です。たずえば、自動むンクリメントIDの盞関は1に近い傟向がありたす。行が䜜成された日時を瀺す日付列ずタむムスタンプ列の盞関も1に近くなりたす。



盞関が-1の堎合、ペヌゞは列の逆の順序で䞊べ替えられたす。





盞関〜0。



盞関が0に近い堎合は、列の倀がテヌブルのペヌゞ順序ず盞関しおいないか、ほずんど盞関しおいないこずを意味したす。



に戻りたしょうsale_fact。事前に䞊べ替えずにデヌタをテヌブルにロヌドするず、盞関関係は次のようになりたす。



db=# SELECT tablename, attname, correlation
db-# FROM pg_stats
db=# WHERE tablename = 'sale_fact';

 tablename | attname  | correlation
-----------+----------+--------------
 sale      | id       |            1
 sale      | username | -0.005344716
 sale      | sold_at  | -0.011389783


自動生成された列IDの盞関は1です。列の盞関はsold_at非垞に䜎く、連続する倀がテヌブル党䜓に散らばっおいたす。



゜ヌトされたデヌタをテヌブルにロヌドするず、圌女は盞関関係を蚈算したした。



tablename | attname  |  correlation
-----------+----------+----------------
 sale_fact | id       |              1
 sale_fact | username | -0.00041992788
 sale_fact | sold_at  |              1


これで、盞関sold_atは等しくなり1たす。



では、なぜデヌタベヌスは盞関が䜎いずきにビットマップスキャンを䜿甚し、盞関が高いずきにむンデックススキャンを䜿甚したのでしょうか。



  • 盞関が1の堎合、ベヌスは、芁求された範囲の行が連続したペヌゞにある可胜性が高いず刀断したした。次に、むンデックススキャンを䜿甚しお耇数のペヌゞを読み取るこずをお勧めしたす。
  • 盞関が0に近い堎合、ベヌスは、芁求された範囲の行がテヌブル党䜓に散圚しおいる可胜性が高いず刀断したした。次に、必芁な行を含むペヌゞのビットマップスキャンを䜿甚し、条件を䜿甚しおそれらを抜出するこずをお勧めしたす。


次にデヌタをテヌブルにロヌドするずきは、芁求される情報の量を怜蚎し、むンデックスが範囲をすばやくスキャンできるように䞊べ替えたす。



CLUSTERコマンド



特定のむンデックスで「ディスク䞊のテヌブルを゜ヌト」する別の方法は、CLUSTERコマンドを䜿甚するこずです。



䟋えば



db=# TRUNCATE sale_fact;
TRUNCATE TABLE

-- Insert rows without sorting
db=# INSERT INTO sale_fact (username, sold_at)
db-# SELECT
db-#     md5(random()::text) AS username,
db-#     '2020-01-01'::date + (interval '1 day') * round(random() * 365 * 2) AS sold_at
db-# FROM
db-#     generate_series(1, 100000)
INSERT 0 100000

db=# ANALYZE sale_fact;
ANALYZE

db=# SELECT tablename, attname, correlation
db-# FROM pg_stats
db-# WHERE tablename = 'sale_fact';

  tablename | attname  |  correlation
-----------+-----------+----------------
 sale_fact | sold_at   | -5.9702674e-05
 sale_fact | id        |              1
 sale_fact | username  |    0.010033822


デヌタをランダムな順序でテヌブルにロヌドしたため、盞関sold_atはれロに近くなりたす。



によっおテヌブルを「再構成」するsold_atには、次のコマンドを䜿甚しお、CLUSTERディスク䞊のテヌブルをむンデックスに埓っお゜ヌトしたすsale_fact_sold_at_ix。



db=# CLUSTER sale_fact USING sale_fact_sold_at_ix;
CLUSTER

db=# ANALYZE sale_fact;
ANALYZE

db=# SELECT tablename, attname, correlation
db-# FROM pg_stats
db-# WHERE tablename = 'sale_fact';

 tablename | attname  | correlation
-----------+----------+--------------
 sale_fact | sold_at  |            1
 sale_fact | id       | -0.002239401
 sale_fact | username |  0.013389298


テヌブルをクラスタヌ化した埌、盞関sold_atは1になりたした。





CLUSTERコマンド。



泚意点



  • 特定の列でテヌブルをクラスタヌ化するず、別の列の盞関に圱響を䞎える可胜性がありたす。たずえば、でクラスタリングしsold_atた埌のIDの盞関関係を芋おみたしょう。
  • CLUSTER 重くおブロッキング操䜜なので、ラむブテヌブルには適甚しないでください。


これらの理由から、すでに゜ヌトされおおり、に䟝存しないデヌタを挿入するこずをお勧めしCLUSTERたす。



BRINずの盞関性の高い列むンデックス



むンデックスに関しおは、倚くの開発者がBツリヌに぀いお考えおいたす。ただし、PostgreSQLは、BRINなどの他のタむプのむンデックスを提䟛したす。



BRINは、䞀郚の列がテヌブル内の物理的な䜍眮ず自然に盞関する非垞に倧きなテヌブルで機胜するように蚭蚈されおいたす




BRINはBlockRangeIndexの略です。ドキュメントによるず、BRINは盞関性の高い列で最適に機胜したす。前の章で芋たように、自動むンクリメントIDずタむムスタンプはテヌブルの物理的構造ず自然に盞関するため、BRINはそれらにずっおより有益です。



特定の条件䞋では、BRINは、同等のBツリヌむンデックスず比范しお、サむズずパフォヌマンスの点でより優れた「コストパフォヌマンス」を提䟛できたす。





ブリン。



BRINは、テヌブル内のいく぀かの隣接するペヌゞ内の倀の範囲です。列に次の倀があり、それぞれが別々のペヌゞにあるずしたしょう



1, 2, 3, 4, 5, 6, 7, 8, 9


BRINは、隣接するペヌゞの範囲で機胜したす。隣接する3぀のペヌゞを指定するず、むンデックスはテヌブルを次の範囲に分割したす。



[1,2,3], [4,5,6], [7,8,9]


範囲ごずに、BRINは最小倀ず最倧倀を栌玍したす。



[1–3], [4–6], [7–9]


このむンデックスを䜿甚しお、倀5を探したしょう。



  • [1–3]-圌は確かにここにいたせん。
  • [4–6]-ここにあるかもしれたせん。
  • [7–9]-圌は確かにここにいたせん。


BRINを䜿甚しお、怜玢領域をブロック4〜6に制限したした。



別の䟋を芋おみたしょう。列の倀の盞関がれロに近い、぀たり䞊べ替えられおいないようにしたす



[2,9,5], [1,4,7], [3,8,6]


隣接する3぀のブロックにむンデックスを付けるず、次の範囲が埗られたす。



[2–9], [1–7], [3–8]


倀5を探したしょう



  • [2-9]-ここにあるかもしれたせん。
  • [1-7]-ここにあるかもしれたせん。
  • [3–8]-ここにあるかもしれたせん。


この堎合、むンデックスは怜玢をたったく絞り蟌たないため、圹に立ちたせん。



pages_per_rangeを理解する



隣接するペヌゞの数は、パラメヌタによっお決定されたすpages_per_range。範囲内のペヌゞ数は、BRINのサむズず粟床に圱響したす。



  • pages_per_rangeむンデックスが小さくお粟床が䜎いず、倧きな倀になりたす。
  • 倀pages_per_rangeを小さくするず、むンデックスが倧きくなり、より正確になりたす。


デフォルトpages_per_rangeは128です。





䜎いpages_per_rangeのBRIN。



説明のために、2ペヌゞの範囲でBRINを䜜成し、5の倀を探したしょう。



  • [1–2]-圌は確かにここにいたせん。
  • [3–4]-圌は確かにここにいたせん。
  • [5-6]-ここにあるかもしれたせん。
  • [7–8]-圌は確かにここにいたせん。
  • [9]-ここでは絶察にそうではありたせん。


2ペヌゞの範囲では、怜玢をブロック5ず6に制限できたす。範囲が3ペヌゞの堎合、むンデックスは怜玢をブロック4、5、6に制限したす



。2぀のむンデックスのもう1぀の違いは、範囲が3ペヌゞの堎合、3぀の範囲を栌玍する必芁があるこずです。 、および範囲内に2ペヌゞある堎合、すでに5぀の範囲が取埗され、むンデックスが増加したす。



BRINを䜜成する



テヌブルsales_factを取り、列ごずにBRINを䜜成したしょうsold_at



db=# CREATE INDEX sale_fact_sold_at_bix ON sale_fact
db-# USING BRIN(sold_at) WITH (pages_per_range = 128);
CREATE INDEX


デフォルトはpages_per_range = 128です。



次に、販売期間を照䌚しおみたしょう。



db=# EXPLAIN (ANALYZE)
db-# SELECT *
db-# FROM sale_fact
db-# WHERE sold_at BETWEEN '2020-07-01' AND '2020-07-31';
                                    QUERY PLAN
--------------------------------------------------------------------------------------------
 Bitmap Heap Scan on sale_fact  (cost=13.11..1135.61 rows=4319 width=41)
   Recheck Cond: ((sold_at >= '2020-07-01'::date) AND (sold_at <= '2020-07-31'::date))
   Rows Removed by Index Recheck: 23130
   Heap Blocks: lossy=256
   ->  Bitmap Index Scan on sale_fact_sold_at_bix  (cost=0.00..12.03 rows=12500 width=0)
         Index Cond: ((sold_at >= '2020-07-01'::date) AND (sold_at <= '2020-07-31'::date))
 Execution Time: 8.877 ms


ベヌスはBRINを䜿甚しお日付期間を取埗したしたが、これは興味深いこずではありたせん...



pages_per_rangeの最適化



実行蚈画によるず、デヌタベヌスは、むンデックスを䜿甚しお芋぀けたペヌゞから23130行を削陀したした。これは、むンデックスに指定した範囲がこのク゚リに察しお倧きすぎるこずを瀺しおいる可胜性がありたす。範囲内のペヌゞ数が半分のむンデックスを䜜成したしょう。



db=# CREATE INDEX sale_fact_sold_at_bix64 ON sale_fact
db-# USING BRIN(sold_at) WITH (pages_per_range = 64);
CREATE INDEX

db=# EXPLAIN (ANALYZE)
db- SELECT *
db- FROM sale_fact
db- WHERE sold_at BETWEEN '2020-07-01' AND '2020-07-31';
                                        QUERY PLAN
---------------------------------------------------------------------------------------------
 Bitmap Heap Scan on sale_fact  (cost=13.10..1048.10 rows=4319 width=41)
   Recheck Cond: ((sold_at >= '2020-07-01'::date) AND (sold_at <= '2020-07-31'::date))
   Rows Removed by Index Recheck: 9434
   Heap Blocks: lossy=128
   ->  Bitmap Index Scan on sale_fact_sold_at_bix64  (cost=0.00..12.02 rows=6667 width=0)
         Index Cond: ((sold_at >= '2020-07-01'::date) AND (sold_at <= '2020-07-31'::date))
 Execution Time: 5.491 ms


範囲内の64ペヌゞで、デヌタベヌスはむンデックス-9 434を䜿甚しお芋぀かった行をより少なく削陀したした。これは、実行するI / O操䜜が少なくお枈み、ク゚リが少し速く実行され、玄8.9ではなく玄5.5ミリ秒で実行されたこずを意味したす。



さたざたな倀でむンデックスをテストしおみたしょうpages_per_range



pages_per_range むンデックスを再確認するずきに行を削陀したした
128 23130
64 9 434
8 874
4 446
2 446


枛少pages_per_range指数は、より正確になり、それが芋぀かったペヌゞから少ない行を削陀したす。



非垞に具䜓的なク゚リを最適化したこずに泚意しおください。これは説明には問題ありたせんが、実際には、ほずんどのク゚リのニヌズを満たす倀を䜿甚するこずをお勧めしたす。



むンデックスのサむズの芋積もり



BRINのもう1぀の倧きな利点は、そのサむズです。前の章では、フィヌルドのsold_atBツリヌむンデックスを䜜成したした。サむズは2,224KBでした。たた、パラメヌタを䜿甚したBRINサむズはpages_per_range=128わずか48 KBで、46分の1になりたす。



Schema |         Name          | Type  | Owner |   Table   | Size
--------+-----------------------+-------+-------+-----------+-------
 public | sale_fact_sold_at_bix | index | haki  | sale_fact | 48 kB
 public | sale_fact_sold_at_ix  | index | haki  | sale_fact | 2224 kB


BRINサむズも圱響を受けpages_per_rangeたす。たずえば、BRINのpages_per_range=2重量は56 Kbで、48Kbをわずかに䞊回りたす。



むンデックスを「非衚瀺」にする



PostgreSQLにはクヌルなトランザクションDDL機胜がありたす。Oracleず長幎にわたり、私はDDLのようなコマンドを䜿甚するこずに慣れおきたCREATE、DROPずの取匕の終わりにALTER。ただし、PostgreSQLでは、トランザクション内でDDLコマンドを実行でき、倉曎はトランザクションがコミットされた埌にのみ適甚されたす。



最近、トランザクションDDLを䜿甚するず、むンデックスが非衚瀺になる可胜性があるこずを発芋したした。これは、むンデックスのない実行蚈画を確認する堎合に圹立ちたす。



たずえば、テヌブルsale_factでは、列にむンデックスを䜜成したしたsold_at。7月の販売フェッチリク゚ストの実行蚈画は次のようになりたす。



db=# EXPLAIN
db-# SELECT *
db-# FROM sale_fact
db-# WHERE sold_at BETWEEN '2020-07-01' AND '2020-07-31';
                                         QUERY PLAN
--------------------------------------------------------------------------------------------
 Index Scan using sale_fact_sold_at_ix on sale_fact  (cost=0.42..182.80 rows=4319 width=41)
   Index Cond: ((sold_at >= '2020-07-01'::date) AND (sold_at <= '2020-07-31'::date))P


むンデックスがない堎合のプランがどのようになるかを確認するにはsale_fact_sold_at_ix、トランザクション内にむンデックスを配眮しお、すぐにロヌルバックしたす。



db=# BEGIN;
BEGIN

db=# DROP INDEX sale_fact_sold_at_ix;
DROP INDEX

db=# EXPLAIN
db-# SELECT *
db-# FROM sale_fact
db-# WHERE sold_at BETWEEN '2020-07-01' AND '2020-07-31';
                                   QUERY PLAN
---------------------------------------------------------------------------------

 Seq Scan on sale_fact  (cost=0.00..2435.00 rows=4319 width=41)
   Filter: ((sold_at >= '2020-07-01'::date) AND (sold_at <= '2020-07-31'::date))

db=# ROLLBACK;
ROLLBACK


たず、でトランザクションを開始したしょうBEGIN。次に、むンデックスを削陀しお実行蚈画を生成したす。プランでは、むンデックスが存圚しないかのようにテヌブル党䜓のスキャンが䜿甚されるこずに泚意しおください。この時点では、トランザクションはただ進行䞭であるため、むンデックスはただ削陀されおいたせん。むンデックスを削陀せずにトランザクションを完了するには、コマンドを䜿甚しおトランザクションをロヌルバックしROLLBACKたす。



むンデックスがただ存圚するこずを確認したしょう。



db=# \di+ sale_fact_sold_at_ix
                                 List of relations
 Schema |         Name         | Type  | Owner |   Table   |  Size
--------+----------------------+-------+-------+-----------+---------
 public | sale_fact_sold_at_ix | index | haki  | sale_fact | 2224 kB


トランザクションDDLをサポヌトしない他のデヌタベヌスは、異なる方法で目暙を達成する可胜性がありたす。たずえば、Oracleはあなたにむンデックスをマヌクするこずができたす目に芋えないし、オプティマむザはそれを無芖したす。



譊告あなたはトランザクション内でむンデックスを削陀する堎合、それは競争力のある事業の閉塞に぀ながるSELECT、INSERT、UPDATEずDELETEテヌブルにトランザクションがアクティブになるたで。テスト環境では泚意しお䜿甚し、生産斜蚭での䜿甚は避けおください。



長いプロセスを1時間の開始時に開始するようにスケゞュヌルしないでください



投資家は、株䟡が10ドル、100ドル、1000ドルなどの矎しいラりンド倀に達するず、奇劙なこずが起こる可胜性があるこずを知っおいたす。ここだ、圌らはそれに぀いお曞かれたものは



[...]資産䟡栌は予枬できないほど倉化する可胜性があり、1株あたり50ドルや100ドルなどのラりンド倀を超えたす。経隓の浅いトレヌダヌの倚くは、公正な䟡栌であるず考えおいるため、䟡栌が抂数に達したずきに資産を売買するこずを奜みたす。


この芳点から、開発者は投資家ずそれほど違いはありたせん。長いプロセスをスケゞュヌルする必芁がある堎合、通垞は1時間を遞択したす。





兞型的な倜間のシステム負荷。



これにより、これらの時間垯に負荷が急䞊昇する可胜性がありたす。したがっお、長いプロセスをスケゞュヌルする必芁がある堎合、システムが他の時間にアむドル状態になる可胜性が高くなりたす。



たた、毎回同時に開始しないように、スケゞュヌルでランダムな遅延を䜿甚するこずをお勧めしたす。そうすれば、この時間に別のタスクがスケゞュヌルされおいおも、倧きな問題にはなりたせん。systemdタむマヌを䜿甚しおいる堎合は、RandomizedDelaySecオプションを䜿甚できたす。



結論



この蚘事では、私の経隓に基づいおさたざたな皋床の蚌拠のヒントを提䟛したす。実装が簡単なものもあれば、デヌタベヌスの動䜜を深く理解する必芁があるものもありたす。デヌタベヌスは最新のシステムのバックボヌンであるため、䜜業方法の孊習に費やす時間は、どの開発者にずっおも良い投資です。



All Articles