簡體   English   中英

對 300 萬行的 PostgreSQL 數據庫進行慢速簡單更新查詢

[英]Slow simple update query on PostgreSQL database with 3 million rows

我在 Postegres 8.4 上有大約 300 萬行的表上嘗試一個簡單的UPDATE table SET column1 = 0但它需要永遠完成。 它已經運行了10多分鍾。

之前,我嘗試在該表上運行 VACUUM 和 ANALYZE 命令,並且還嘗試創建一些索引(盡管我懷疑這在這種情況下會有所不同),但似乎沒有任何幫助。

還有其他想法嗎?

更新:

這是表結構:

CREATE TABLE myTable
(
  id bigserial NOT NULL,
  title text,
  description text,
  link text,
  "type" character varying(255),
  generalFreq real,
  generalWeight real,
  author_id bigint,
  status_id bigint,
  CONSTRAINT resources_pkey PRIMARY KEY (id),
  CONSTRAINT author_pkey FOREIGN KEY (author_id)
      REFERENCES users (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT c_unique_status_id UNIQUE (status_id)
);

我正在嘗試運行UPDATE myTable SET generalFreq = 0;

我必須用每行的不同值更新 1 或 20 億行的表。 每次運行都會產生約 1 億次更改 (10%)。 我的第一次嘗試是直接在特定分區上將它們分組為 300K 更新的事務,因為如果您使用分區,Postgresql 並不總是優化准備好的查詢。

  1. 一堆“UPDATE myTable SET myField=value WHERE myId=id”的交易
    提供1,500 次更新/秒。 這意味着每次運行至少需要 18 小時。
  2. 此處使用 FILLFACTOR=50 描述的熱更新解決方案。 提供 1,600 次更新/秒。 我使用 SSD,所以這是一項代價高昂的改進,因為它使存儲大小翻了一番。
  3. 插入一個更新值的臨時表,並在使用 UPDATE...FROM 后將它們合並為18,000 次更新/秒。 如果我為每個分區做一個 VACUUM ; 否則為 100,000 up/s。 酷。
    以下是操作順序:

CREATE TEMP TABLE tempTable (id BIGINT NOT NULL, field(s) to be updated,
CONSTRAINT tempTable_pkey PRIMARY KEY (id));

根據可用RAM在緩沖區中累積一堆更新當它被填滿,或者需要更改表/分區,或者完成時:

COPY tempTable FROM buffer;
UPDATE myTable a SET field(s)=value(s) FROM tempTable b WHERE a.id=b.id;
COMMIT;
TRUNCATE TABLE tempTable;
VACUUM FULL ANALYZE myTable;

這意味着運行 1 億次更新現在需要 1.5 小時而不是 18 小時,包括真空。 為了節省時間,最后沒有必要將vacuum FULL,但即使是快速的常規vacuum 也有助於控制數據庫上的事務ID,並且不會在高峰時間獲得不需要的autovacuum。

看看這個答案: PostgreSQL 在帶有數組和大量更新的大表上運行緩慢

首先從更好的 FILLFACTOR 開始,執行 VACUUM FULL 以強制表重寫並在 UPDATE 查詢后檢查 HOT 更新:

SELECT n_tup_hot_upd, * FROM pg_stat_user_tables WHERE relname = 'myTable';

當您有很多記錄要更新時,HOT 更新會快得多。 更多關於 HOT 的信息可以在這篇文章中找到。

附言。 您需要版本 8.3 或更高版本。

等待35分鍾后。 為了完成我的 UPDATE 查詢(但仍然沒有),我決定嘗試不同的方法。 所以我所做的是一個命令:

CREATE TABLE table2 AS 
SELECT 
  all the fields of table1 except the one I wanted to update, 0 as theFieldToUpdate
from myTable

然后添加索引,然后刪除舊表並重命名新表以取代它。 這只花了1.7分鍾。 處理加上一些額外的時間來重新創建索引和約束。 但它確實有幫助! :)

當然,這樣做只是因為沒有其他人在使用數據庫。 如果這是在生產環境中,我需要先鎖定表。

今天我花了很多時間來解決類似的問題。 我找到了一個解決方案在更新之前刪除所有約束/索引 無論被更新的列是否被索引,似乎 psql 都會更新所有更新行的所有索引。 更新完成后,重新添加約束/索引。

試試這個(注意, generalFreq以 REAL 類型開始,並保持不變):

ALTER TABLE myTable ALTER COLUMN generalFreq TYPE REAL USING 0;

這將重寫表,類似於 DROP + CREATE,並重建所有索引。 但所有在一個命令。 快得多(大約 2 倍),並且您不必處理依賴關系和重新創建索引和其他東西,盡管它確實在持續時間內鎖定了表(訪問獨占 - 即完全鎖定)。 或者,如果您希望其他所有內容都排在它后面,也許這就是您想要的。 如果您不更新“太多”行,那么這種方式比更新要慢。

你是如何運行它的? 如果您循環每一行並執行更新語句,您可能會運行數百萬個單獨的更新,這就是它執行速度非常慢的原因。

如果您在一個語句中為所有記錄運行單個更新語句,它會運行得更快,如果這個過程很慢,那么它可能更多地取決於您的硬件。 300萬是很多記錄。

我建議的第一件事(來自https://dba.stackexchange.com/questions/118178/does-updating-a-row-with-the-same-value-actually-update-the-row )是更新“需要”它的行,例如:

 UPDATE myTable SET generalFreq = 0 where generalFreq != 0;

(可能還需要一個關於 generalFreq 的索引)。 然后您將更新更少的行。 雖然如果值已經全部非零,但更新較少的行“可以幫助”,因為否則它會更新它們和所有索引,無論值是否更改。

另一種選擇:如果星星在默認值和非空約束方面對齊,您可以刪除舊列並通過調整元數據即時創建另一個

在我的測試中,我注意到超過 200 000 行的大更新比 100 000 行的 2 次更新慢,即使使用臨時表也是如此。

我的解決方案是循環,在每個循環中創建一個 200 000 行的臨時表,在這個表中我計算我的值,然后用新值更新我的主表......

每 2 000 000 行,我手動“VACUUM ANALYZE mytable”,我注意到自動真空無法完成此類更新的工作。

我需要在包含一些索引的 PostgreSQL 表上更新超過 1B+ 行。 我正在研究 PostgreSQL 12 + SQLAlchemy + Python。

受此處答案的啟發,我編寫了一個臨時表和UPDATE... FROM基於更新程序,看看它是否有所作為。 然后從 Python 生成的 CSV 提供臨時表,並通過正常的 SQL 客戶端連接上傳。

使用 SQLAlchemy 的bulk_update_mappings加速天真的方法是 4x - 5x。 不是一個數量級,但仍然相當可觀,在我的情況下,這意味着批處理作業需要 1 天,而不是 1 周。

以下是執行CREATE TEMPORARY TABLECOPY FROMUPDATE FROM的相關 Python 代碼。 請參閱此要點中的完整示例

def bulk_load_psql_using_temp_table(
        dbsession: Session,
        data_as_dicts: List[dict],
):
    """Bulk update columns in PostgreSQL faster using temp table.

    Works around speed issues on `bulk_update_mapping()` and PostgreSQL.
    Your mileage and speed may vary, but it is going to be faster.
    The observation was 3x ... 4x faster when doing UPDATEs
    where one of the columns is indexed.

    Contains hardcoded temp table creation and UPDATE FROM statements.
    In our case we are bulk updating three columns.

    - Create a temp table - if not created before

    - Filling it from the in-memory CSV using COPY FROM

    - Then performing UPDATE ... FROM on the actual table from the temp table

    - Between the update chunks, clear the temp table using TRUNCATE

    Why is it faster? I have did not get a clear answer from the sources I wa reading.
    At least there should be
    less data uploaded from the client to the server,
    as CSV loading is more compact than bulk updates.

    Further reading

    - `About PSQL temp tables <https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-temporary-table/>`_

    - `Naive bulk_update_mapping approach <https://stackoverflow.com/questions/36272316/using-bulk-update-mappings-in-sqlalchemy-to-update-multiple-rows-with-different>`_

    - `Discussion on UPDATE ... FROM + temp table approach <https://stackoverflow.com/questions/3361291/slow-simple-update-query-on-postgresql-database-with-3-million-rows/24811058#24811058>_`.

    :dbsession:
        SQLAlchemy session.
        Note that we open a separate connection for the bulk update.

    :param data_as_dicts:
        In bound data as it would be given to bulk_update_mapping
    """

    # mem table created in sql
    temp_table_name = "temp_bulk_temp_loader"

    # the real table of which data we are filling
    real_table_name = "swap"

    # colums we need to copy
    columns = ["id", "sync_event_id", "sync_reserve0", "sync_reserve1"]

    # how our CSV fields are separated
    delim = ";"

    # Increase temp buffer size for updates
    temp_buffer_size = "3000MB"

    # Dump data to a local mem buffer using CSV writer.
    # No header - this is specifically addressed in copy_from()
    out = StringIO()
    writer = csv.DictWriter(out, fieldnames=columns, delimiter=delim)
    writer.writerows(data_as_dicts)

    # Update data in alternative raw connection
    engine = dbsession.bind
    conn = engine.connect()

    try:
        # No rollbacks
        conn.execution_options(isolation_level="AUTOCOMMIT")

        # See https://blog.codacy.com/how-to-update-large-tables-in-postgresql/
        conn.execute(f"""SET temp_buffers = "{temp_buffer_size}";""")

        # Temp table is dropped at the end of the session
        # https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-temporary-table/
        # This must match data_as_dicts structure.
        sql = f"""
        CREATE TEMP TABLE IF NOT EXISTS {temp_table_name}
        (
          id int,
          sync_event_id int,
          sync_reserve0 bytea,
          sync_reserve1 bytea
        );    
        """
        conn.execute(sql)

        # Clean any pending data in the temp table
        # between update chunks.
        # TODO: Not sure why this does not clear itself at conn.close()
        # as I would expect based on the documentation.
        sql = f"TRUNCATE {temp_table_name}"
        conn.execute(sql)

        # Load data from CSV to the temp table
        # https://www.psycopg.org/docs/cursor.html
        cursor = conn.connection.cursor()
        out.seek(0)
        cursor.copy_from(out, temp_table_name, sep=delim, columns=columns)

        # Fill real table from the temp table
        # This copies values from the temp table using
        # UPDATE...FROM and matching by the row id.
        sql = f"""
        UPDATE {real_table_name}  
        SET 
            sync_event_id=b.sync_event_id,
            sync_reserve0=b.sync_reserve0,
            sync_reserve1=b.sync_reserve1        
        FROM {temp_table_name} AS b 
        WHERE {real_table_name}.id=b.id;
        """
        res = conn.execute(sql)
        logger.debug("Updated %d rows", res.rowcount)
    finally:
        conn.close()

嘗試

UPDATE myTable SET generalFreq = 0.0;

可能是選角問題

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM