wiki:Phase4_Profiling_Optimization

Профилирање и оптимизација на извршувањето на прашалниците

Профилирањето на прашалници претставува анализа на начинот на кој PostgreSQL го извршува одреден SQL прашалник. Целта е да се открие дали прашалникот користи неефикасни операции, како секвенцијално читање на големи табели, непотребно голем број join операции или сортирање на голем број редови. За оваа анализа во PostgreSQL се користи EXPLAIN ANALYZE, кој го прикажува планот на извршување и реалното време потребно за извршување на прашалникот.

EXPLAIN ANALYZE
WITH customer_history AS (
    SELECT DISTINCT
        f.film_id,
        f.rating,
        fc.category_id
    FROM rental r
    JOIN inventory i ON r.inventory_id = i.inventory_id
    JOIN film f ON i.film_id = f.film_id
    JOIN film_category fc ON f.film_id = fc.film_id
    WHERE r.customer_id = 101
),
preferred_categories AS (
    SELECT
        category_id,
        COUNT(*) AS category_score
    FROM customer_history
    GROUP BY category_id
),
candidate_films AS (
    SELECT
        f.film_id,
        f.title,
        f.rating,
        fc.category_id,
        pc.category_score
    FROM film f
    JOIN film_category fc ON f.film_id = fc.film_id
    JOIN preferred_categories pc ON fc.category_id = pc.category_id
    WHERE NOT EXISTS (
        SELECT 1
        FROM customer_history ch
        WHERE ch.film_id = f.film_id
    )
),
film_popularity AS (
    SELECT
        i.film_id,
        COUNT(r.rental_id) AS rental_count
    FROM rental r
    JOIN inventory i ON r.inventory_id = i.inventory_id
    WHERE r.rental_date >= DATE '2024-01-01'
      AND r.rental_date < DATE '2025-01-01'
    GROUP BY i.film_id
)
SELECT
    cf.film_id,
    cf.title,
    cf.rating,
    c.name AS category,
    cf.category_score,
    COALESCE(fp.rental_count, 0) AS yearly_rentals,
    (cf.category_score * 10 + COALESCE(fp.rental_count, 0)) AS recommendation_score
FROM candidate_films cf
JOIN category c ON cf.category_id = c.category_id
LEFT JOIN film_popularity fp ON cf.film_id = fp.film_id
WHERE cf.rating IN (
    SELECT rating
    FROM customer_history
)
ORDER BY recommendation_score DESC, cf.title
LIMIT 10;

Овој прашалник претставува пример за препорака на филмови за клиент. Прво се анализира историјата на изнајмувања на клиентот, односно кои филмови, категории и ratings ги има гледано. Потоа се избираат кандидати од истите категории, се исклучуваат филмовите што клиентот веќе ги изнајмил, и се додава популарноста на филмовите според бројот на изнајмувања во 2024 година. На крај се пресметува recommendation_score и се враќаат најдобрите 10 препораки.

CTE customer_history
-> Parallel Seq Scan on rental r_1
   Filter: (customer_id = 101)
   Rows Removed by Filter: 3333329
film_popularity
-> Parallel Seq Scan on rental r
   Filter: ((rental_date >= '2024-01-01') AND (rental_date < '2025-01-01'))
Rows Removed by Filter: 2666129
Buffers: shared hit=207891
Execution Time: 865.763 ms

При првото извршување, прашалникот се изврши без дополнителни индекси. Времето на извршување беше околу 866 ms. Во планот се забележува Parallel Seq Scan врз табелата rental, и тоа на две места: при пронаоѓање на историјата на клиентот и при пресметување на популарноста на филмовите во 2024 година. Ова значи дека PostgreSQL мора да прочита голем дел од табелата rental, која има над 10 милиони редови. За оптимизација се додаваат индекси врз колоните што најмногу се користат во WHERE условите и JOIN операциите. Индексот idx_rental_customer_inventory директно помага при пронаоѓање на изнајмувањата за конкретен клиент. Индексот idx_inventory_inventory_film помага при поврзување на rental со inventory и film. Индексите врз rental_date, film_category и rating се додадени како можни оптимизации, но PostgreSQL не мора секогаш да ги користи ако процени дека друг план е поефикасен.

За оптимизација се додаваат индекси врз колоните што најмногу се користат во WHERE условите и JOIN операциите. Индексот idx_rental_customer_inventory директно помага при пронаоѓање на изнајмувањата за конкретен клиент. Индексот idx_inventory_inventory_film помага при поврзување на rental со inventory и film. Индексите врз rental_date, film_category и rating се додадени како можни оптимизации, но PostgreSQL не мора секогаш да ги користи ако процени дека друг план е поефикасен.

CREATE INDEX idx_rental_customer_inventory ON rental(customer_id, inventory_id);
CREATE INDEX idx_rental_date_inventory ON rental(rental_date, inventory_id);
CREATE INDEX idx_inventory_inventory_film ON inventory(inventory_id, film_id);
CREATE INDEX idx_film_category_category_film ON film_category(category_id, film_id);
CREATE INDEX idx_film_rating ON film(rating);

По додавање на индексите, истиот прашалник повторно се извршува со EXPLAIN ANALYZE.

CTE customer_history
-> Index Only Scan using idx_rental_customer_inventory on rental r_1
   Index Cond: (customer_id = 101)
   Heap Fetches: 1
-> Index Only Scan using idx_inventory_inventory_film on inventory i_1
   Index Cond: (inventory_id = r_1.inventory_id)
film_popularity
-> Parallel Seq Scan on rental r
   Filter: ((rental_date >= '2024-01-01') AND (rental_date < '2025-01-01'))
   Rows Removed by Filter: 2666129
Buffers: shared hit=104500 read=15
Execution Time: 566.908 ms

Времето на извршување се намали од околу 866 ms на околу 567 ms.

Најголемата промена се забележува во делот customer_history. Пред оптимизацијата PostgreSQL користеше Parallel Seq Scan врз табелата rental и мораше да провери голем број редови за да ги најде изнајмувањата на клиентот 101. По додавање на индексот idx_rental_customer_inventory, планот користи Index Only Scan, со што овој дел од прашалникот се намали од околу 256 ms на околу 4.4 ms. Сепак, во делот film_popularity PostgreSQL сè уште користи Parallel Seq Scan врз rental за филтрирање на изнајмувањата во 2024 година. Причината е што условот за 2024 година враќа околу 2 милиони редови, па PostgreSQL проценува дека секвенцијално скенирање е поефикасно од користење индекс. Ова покажува дека додавање индекс не значи дека индексот секогаш ќе биде искористен. Иако додавањето индекси го подобри делот од прашалникот каде што се бара историјата на конкретен клиент, во планот на извршување сè уште се забележува Parallel Seq Scan врз табелата rental при пресметување на популарноста на филмовите во 2024 година. Ова се случува затоа што условот:

WHERE r.rental_date >= DATE '2024-01-01'
  AND r.rental_date < DATE '2025-01-01'

работи врз табела со над 10 милиони редови.

Бидејќи прашалникот филтрира според датум, табелата rental може да се оптимизира со партиционирање според колоната rental_date. Со range partitioning, податоците се делат во посебни партиции по години. На тој начин, кога прашалникот бара податоци само за 2024 година, PostgreSQL може да ја чита само партицијата за 2024 година, наместо целата табела.

ALTER TABLE rental RENAME TO rental_old;
CREATE TABLE rental (
    rental_id BIGINT GENERATED ALWAYS AS IDENTITY,
    rental_date TIMESTAMP NOT NULL,
    inventory_id BIGINT NOT NULL,
    customer_id BIGINT NOT NULL,
    return_date TIMESTAMP,
    staff_id BIGINT NOT NULL,
    last_update TIMESTAMP DEFAULT now() NOT NULL,
    PRIMARY KEY (rental_id, rental_date)
)
PARTITION BY RANGE (rental_date);

CREATE TABLE rental_2020 PARTITION OF rental
FOR VALUES FROM ('2020-01-01') TO ('2021-01-01');

CREATE TABLE rental_2021 PARTITION OF rental
FOR VALUES FROM ('2021-01-01') TO ('2022-01-01');

CREATE TABLE rental_2022 PARTITION OF rental
FOR VALUES FROM ('2022-01-01') TO ('2023-01-01');

CREATE TABLE rental_2023 PARTITION OF rental
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

CREATE TABLE rental_2024 PARTITION OF rental
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE rental_2025 PARTITION OF rental
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

CREATE TABLE rental_2026 PARTITION OF rental
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

Потоа податоците се префрлаат од старата табела rental_old во новата партиционирана табела rental.

INSERT INTO rental (rental_id, rental_date, inventory_id, customer_id, return_date, staff_id, last_update)
OVERRIDING SYSTEM VALUE
SELECT rental_id, rental_date, inventory_id, customer_id, return_date, staff_id, last_update
FROM rental_old;

По партиционирањето, истиот прашалник повторно се извршува.

CTE customer_history
-> Append
   -> Index Only Scan using rental_2020_customer_id_inventory_id_idx on rental_2020
      Index Cond: (customer_id = 101)
   -> Index Only Scan using rental_2021_customer_id_inventory_id_idx on rental_2021
      Index Cond: (customer_id = 101)
   -> Index Only Scan using rental_2022_customer_id_inventory_id_idx on rental_2022
      Index Cond: (customer_id = 101)
   -> Index Only Scan using rental_2023_customer_id_inventory_id_idx on rental_2023
      Index Cond: (customer_id = 101)
   -> Index Only Scan using rental_2024_customer_id_inventory_id_idx on rental_2024
      Index Cond: (customer_id = 101)

film_popularity
-> Parallel Seq Scan on rental_2024 r
   Filter: ((rental_date >= '2024-01-01') AND (rental_date < '2025-01-01'))

Buffers: shared hit=22020 read=15
Execution Time: 347.399 ms

Времето на извршување се намали на околу 347 ms. Во планот се забележува дека при пресметување на популарноста за 2024 година PostgreSQL чита само од партицијата rental_2024 (Parallel Seq Scan on rental_2024). Ова значи дека PostgreSQL применил partition pruning во делот од прашалникот каде што има услов за 2024 година и не ги чита партициите за другите години за тој дел од планот. Дополнително, делот customer_history користи Index Only Scan врз партициите, што овозможува брзо пронаоѓање на изнајмувањата за конкретниот клиент. Во табелата е прикажана споредба на времето на извршување на истиот прашалник во три различни состојби: без дополнителна оптимизација, по додавање индекси и по партиционирање на табелата rental според rental_date.

Фаза Време на извршување
Без дополнителни индекси 865.763 ms
Со индекси 566.908 ms
Со партиционирање и индекси 347.399 ms

Од резултатите се гледа дека секоја оптимизација го намалува времето на извршување. Индексите најмногу помогнаа во делот каде што се бара историјата на конкретен клиент, додека партиционирањето помогна во делот каде што се анализираат изнајмувањата за 2024 година. Партиционирањето е најкорисно кога прашалниците често филтрираат според колоната што е partition key. Во овој пример, rental_date е погодна колона за partition key бидејќи прашалникот ја анализира популарноста на филмовите за конкретна година. Наместо да се обработува целата табела rental, PostgreSQL може да ја обработи само релевантната партиција.

Last modified 2 weeks ago Last modified on 05/09/26 20:01:35
Note: See TracWiki for help on using the wiki.