[BUGFIX] Narrow single-pass column_values.unique on SQLAlchemy (Redshift WLM)#11863
Draft
leodrivera wants to merge 3 commits into
Draft
Conversation
👷 Deploy request for niobium-lead-7998 pending review.Visit the deploys page to approve it
|
Project only the target column through the windowed subquery instead of every source column. The previous shape carried all source columns (including JSON/SUPER fields on Redshift) through the window operator, which intermittently tripped WLM "low_timeout" on wide tables. "unexpected_rows" and "unexpected_index_list" are overridden to use a narrow GROUP BY/HAVING dup-keys subquery joined back to source, so they no longer require the windowed selectable to carry every source column.
218064f to
112bc85
Compare
for more information, see https://pre-commit.ci
…nique-window-function-redshift-wlm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[BUGFIX] Narrow single-pass
column_values.uniqueon SQLAlchemy (Redshift WLM)Problem
On column-store backends (observed on Amazon Redshift),
expect_column_values_to_be_uniqueagainst large wide tables (~120 columns including JSON / SUPER fields) is cancelled by the WLMlow_timeoutrule:Root cause
The SQLAlchemy implementation of
column_values.uniquepreviously expanded into:Redshift planned the
NOT INsemi-join as two full scans of the source table. Wall time on the user's table consistently exceeded the WLMlow_timeoutthreshold.Attempt history
Attempt 1 — Window-function single-pass (commit
edd207e6)Replaced
NOT IN (dup_subquery)withcol IN (SELECT col FROM (SELECT col, count() OVER (PARTITION BY col) AS rc FROM source) WHERE rc > 1). Redshift could now scan the source exactly once.Result: Failures dropped sharply but did not go to zero. WLM
low_timeoutstill occasionally tripped on the same query.Attempt 2 — Single-pass windowed selectable mirroring
compound_columns.unique(commitde58111688)Refactored to the same shape used by
compound_columns.unique: acolumn_values.count_per_valuefunction metric materializes a windowed subquery (SELECT *table_columns, count() OVER PARTITION BY col AS _num_rows FROM source) and the condition becomes a simple_num_rows < 2comparison. The MySQL/SingleStore temp-table workaround was removed (no longer needed — source is referenced once).Result: After a week of production traffic, only a single WLM cancellation occurred, but the failure mode persisted on the very wide table (~120 columns, several of which are SUPER / JSON / long-VARCHAR).
Diagnosis after Attempt 2
The remaining failure mode is not a double-scan; it is a wide-row window problem.
The windowed subquery projected every source column (the
compound_columns.uniquepattern requires this so that downstreamunexpected_rows/unexpected_index_listpaths can read original columns from the same selectable). The Redshift window operator therefore had to carry ~120 columns — including JSON SUPER fields — through partition redistribution and sort. On non-DISTKEYcolumns this translates into shipping thousands of bytes per row across slices, plus a wide sort that can spill to disk and triplow_timeout.Confirmed by inspecting the actual SQL captured in the user's error trace: the inner subquery's projection lists every column of the source table.
Attempt 3 (this PR) — Narrow windowed selectable + join-back for row-retrieval
Two changes that together address the wide-row window without re-introducing the double-scan:
column_values.count_per_valueprojects only the target column and_num_rows.The window operator now sorts narrow rows (1 column + the running count) regardless of source-table width. This is the fast path served to
unexpected_count,unexpected_values, andunexpected_value_counts.unexpected_rowsandunexpected_index_listare overridden to use a narrowGROUP BY col HAVING count(*) >= 2dup-keys subquery joined back to the source table. These paths are only exercised when the caller requestsSUMMARY/COMPLETEresult_format; the defaultBASICpath is unaffected. The dup-keys subquery itself reads only the target column, and the join back to source reads only the columns actually returned to the user.The
is_sqlalchemy_metric_selectableregistry keepscolumn_values.unique(the condition metric name) so that the framework's standard auxiliary methods correctly read theirFROMclause from the narrow windowed subquery. The previously addedcolumn_values.count_per_valueentry (Attempt 2) was redundant and is removed.SQL shape after this PR
result_formatpathunexpected_count(defaultBASIC)SELECT SUM(CASE WHEN _num_rows >= 2 THEN 1 ELSE 0 END) FROM (SELECT col, count() OVER (PARTITION BY col) AS _num_rows FROM source)unexpected_values,unexpected_value_countsSELECT col [, COUNT(*)] FROM (… narrow windowed subquery …) WHERE _num_rows >= 2 [GROUP BY col]unexpected_rows(SUMMARY/COMPLETE)SELECT t.* FROM source t JOIN (SELECT col FROM source WHERE col IS NOT NULL GROUP BY col HAVING count(*) >= 2) d ON t.col = d.colunexpected_index_listSELECT idx_cols, t.col FROM source t JOIN (… narrow dup-keys agg …) d ON t.col = d.colBefore this PR, every path paid the wide-row window cost. After this PR, the window is always narrow, and the only multi-scan path is row-retrieval which is opt-in (
SUMMARY/COMPLETE) and reads narrowly.Files changed
great_expectations/expectations/metrics/column_map_metrics/column_values_unique.py— narrow windowed selectable; subclass override of_register_metric_functionsto replace the SQLAlchemyunexpected_rowsandunexpected_index_listaux methods.great_expectations/expectations/metrics/map_metric_provider/is_sqlalchemy_metric_selectable.py— keepcolumn_values.uniqueregistered (Attempt 2 also addedcolumn_values.count_per_value; redundant and removed).tests/expectations/metrics/test_core.py— replaces thePARTITION BY-only regression assertion with two regressions:unexpected_countSQL must containPARTITION BY, must not containNOT IN, and must not project any non-target source column through the windowed subquery (regression guard against the wide-row failure mode from Attempt 2).unexpected_rowsSQL must use aGROUP BY/HAVINGdup-keys subquery joined back to the source.Compatibility / non-goals
unexpected_countpath).ExpectationAPI surface changes; only the generated SQL changes.DISTKEYon the target column further reduces shuffle but is no longer required to stay insidelow_timeouton the observed table.Operational note for users hitting this on Redshift
If you are on a wide table and
expect_column_values_to_be_uniqueis still slow after this fix, verify that the target column is the table'sDISTKEY(or at least not random-distributed). The narrow window shipped here makes shuffle volume scale with row count rather than row width, butDISTKEYon the partition column avoids the shuffle entirely.